Skip to content
122 changes: 122 additions & 0 deletions .github/actions/infrastructure/get-changed-files/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Get Changed Files Action

A reusable composite action that retrieves the list of files changed in a pull request or push event.

## Features

- Supports both `pull_request` and `push` events
- Optional filtering by file pattern
- Returns files as JSON array for easy consumption
- Filters out deleted files (only returns added, modified, or renamed files)
- Handles up to 100 changed files per request

## Usage

### Basic Usage (Pull Requests Only)

```yaml
- name: Get changed files
id: changed-files
uses: "./.github/actions/infrastructure/get-changed-files"

- name: Process files
run: |
echo "Changed files: ${{ steps.changed-files.outputs.files }}"
echo "Count: ${{ steps.changed-files.outputs.count }}"
```
### With Filtering
```yaml
# Get only markdown files
- name: Get changed markdown files
id: changed-md
uses: "./.github/actions/infrastructure/get-changed-files"
with:
filter: '*.md'

# Get only GitHub workflow/action files
- name: Get changed GitHub files
id: changed-github
uses: "./.github/actions/infrastructure/get-changed-files"
with:
filter: '.github/'
```
### Support Both PR and Push Events
```yaml
- name: Get changed files
id: changed-files
uses: "./.github/actions/infrastructure/get-changed-files"
with:
event-types: 'pull_request,push'
```
## Inputs
| Name | Description | Required | Default |
|------|-------------|----------|---------|
| `filter` | Optional filter pattern (e.g., `*.md` for markdown files, `.github/` for GitHub files) | No | `''` |
| `event-types` | Comma-separated list of event types to support (`pull_request`, `push`) | No | `pull_request` |

## Outputs

| Name | Description |
|------|-------------|
| `files` | JSON array of changed file paths |
| `count` | Number of changed files |

## Filter Patterns

The action supports simple filter patterns:

- **Extension matching**: Use `*.ext` to match files with a specific extension
- Example: `*.md` matches all markdown files
- Example: `*.yml` matches all YAML files

- **Path prefix matching**: Use a path prefix to match files in a directory
- Example: `.github/` matches all files in the `.github` directory
- Example: `tools/` matches all files in the `tools` directory

## Example: Processing Changed Files

```yaml
- name: Get changed files
id: changed-files
uses: "./.github/actions/infrastructure/get-changed-files"
- name: Process each file
shell: pwsh
env:
CHANGED_FILES: ${{ steps.changed-files.outputs.files }}
run: |
$changedFilesJson = $env:CHANGED_FILES
$changedFiles = $changedFilesJson | ConvertFrom-Json
foreach ($file in $changedFiles) {
Write-Host "Processing: $file"
# Your processing logic here
}
```

## Limitations

- Simple filter patterns only (no complex glob or regex patterns)

## Pagination

The action automatically handles pagination to fetch **all** changed files in a PR, regardless of how many files were changed:

- Fetches files in batches of 100 per page
- Continues fetching until all files are retrieved
- Logs a note when pagination occurs, showing the total file count
- **No file limit** - all changed files will be processed, even in very large PRs

This ensures that critical workflows (such as merge conflict checking, link validation, etc.) don't miss files due to pagination limits.

## Related Actions

- **markdownlinks**: Uses this pattern to get changed markdown files
- **merge-conflict-checker**: Uses this pattern to get changed files for conflict detection
- **path-filters**: Similar functionality but with more complex filtering logic
117 changes: 117 additions & 0 deletions .github/actions/infrastructure/get-changed-files/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: 'Get Changed Files'
description: 'Gets the list of files changed in a pull request or push event'
inputs:
filter:
description: 'Optional filter pattern (e.g., "*.md" for markdown files, ".github/" for GitHub files)'
required: false
default: ''
event-types:
description: 'Comma-separated list of event types to support (pull_request, push)'
required: false
default: 'pull_request'
outputs:
files:
description: 'JSON array of changed file paths'
value: ${{ steps.get-files.outputs.files }}
count:
description: 'Number of changed files'
value: ${{ steps.get-files.outputs.count }}
runs:
using: 'composite'
steps:
- name: Get changed files
id: get-files
uses: actions/github-script@v7
with:
script: |
const eventTypes = '${{ inputs.event-types }}'.split(',').map(t => t.trim());
const filter = '${{ inputs.filter }}';
let changedFiles = [];
if (eventTypes.includes('pull_request') && context.eventName === 'pull_request') {
console.log(`Getting files changed in PR #${context.payload.pull_request.number}`);
// Fetch all files changed in the PR with pagination
let allFiles = [];
let page = 1;
let fetchedCount;
do {
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
per_page: 100,
page: page
});
allFiles = allFiles.concat(files);
fetchedCount = files.length;
page++;
} while (fetchedCount === 100);
if (allFiles.length >= 100) {
console.log(`Note: This PR has ${allFiles.length} changed files. All files fetched using pagination.`);
}
changedFiles = allFiles
.filter(file => file.status === 'added' || file.status === 'modified' || file.status === 'renamed')
.map(file => file.filename);
} else if (eventTypes.includes('push') && context.eventName === 'push') {
console.log(`Getting files changed in push to ${context.ref}`);
const { data: comparison } = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: context.payload.before,
head: context.payload.after,
});
changedFiles = comparison.files
.filter(file => file.status === 'added' || file.status === 'modified' || file.status === 'renamed')
.map(file => file.filename);
} else {
core.setFailed(`Unsupported event type: ${context.eventName}. Supported types: ${eventTypes.join(', ')}`);
return;
}
// Apply filter if provided
if (filter) {
const filterLower = filter.toLowerCase();
const beforeFilter = changedFiles.length;
changedFiles = changedFiles.filter(file => {
const fileLower = file.toLowerCase();
// Support simple patterns like "*.md" or ".github/"
if (filterLower.startsWith('*.')) {
const ext = filterLower.substring(1);
return fileLower.endsWith(ext);
} else {
return fileLower.startsWith(filterLower);
}
});
console.log(`Filter '${filter}' applied: ${beforeFilter} → ${changedFiles.length} files`);
}
// Calculate simple hash for verification
const crypto = require('crypto');
const filesJson = JSON.stringify(changedFiles.sort());
const hash = crypto.createHash('sha256').update(filesJson).digest('hex').substring(0, 8);
// Log changed files in a collapsible group
core.startGroup(`Changed Files (${changedFiles.length} total, hash: ${hash})`);
if (changedFiles.length > 0) {
changedFiles.forEach(file => console.log(` - ${file}`));
} else {
console.log(' (no files changed)');
}
core.endGroup();
console.log(`Found ${changedFiles.length} changed files`);
core.setOutput('files', JSON.stringify(changedFiles));
core.setOutput('count', changedFiles.length);
branding:
icon: 'file-text'
color: 'blue'
47 changes: 9 additions & 38 deletions .github/actions/infrastructure/markdownlinks/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,52 +31,23 @@ runs:
steps:
- name: Get changed markdown files
id: changed-files
uses: actions/github-script@v7
uses: "./.github/actions/infrastructure/get-changed-files"
with:
script: |
let changedMarkdownFiles = [];

if (context.eventName === 'pull_request') {
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});

changedMarkdownFiles = files
.filter(file => file.filename.endsWith('.md'))
.map(file => file.filename);
} else if (context.eventName === 'push') {
const { data: comparison } = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: context.payload.before,
head: context.payload.after,
});

changedMarkdownFiles = comparison.files
.filter(file => file.filename.endsWith('.md'))
.map(file => file.filename);
} else {
core.setFailed(`Unsupported event type: ${context.eventName}. This action only supports 'pull_request' and 'push' events.`);
return;
}

console.log('Changed markdown files:', changedMarkdownFiles);
core.setOutput('files', JSON.stringify(changedMarkdownFiles));
core.setOutput('count', changedMarkdownFiles.length);
return changedMarkdownFiles;
filter: '*.md'
event-types: 'pull_request,push'

- name: Verify markdown links
id: verify
shell: pwsh
env:
CHANGED_FILES_JSON: ${{ steps.changed-files.outputs.files }}
run: |
Write-Host "Starting markdown link verification..." -ForegroundColor Cyan

# Get changed markdown files from previous step
$changedFilesJson = '${{ steps.changed-files.outputs.files }}'
# Get changed markdown files from environment variable (secure against injection)
$changedFilesJson = $env:CHANGED_FILES_JSON
$changedFiles = $changedFilesJson | ConvertFrom-Json

if ($changedFiles.Count -eq 0) {
Write-Host "No markdown files changed, skipping verification" -ForegroundColor Yellow
"total=0" >> $env:GITHUB_OUTPUT
Expand All @@ -85,7 +56,7 @@ runs:
"skipped=0" >> $env:GITHUB_OUTPUT
exit 0
}

Write-Host "Changed markdown files: $($changedFiles.Count)" -ForegroundColor Cyan
$changedFiles | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray }

Expand Down
Loading
Loading