Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README feature list says the action "Handles up to 100 changed files per request", but the Pagination section later states there is "No file limit" for PRs. Please update the features bullet to match the current pagination behavior (and clarify if push events still have API limits).

Suggested change
- Handles up to 100 changed files per request
- Supports pagination for pull requests (no file limit) and respects GitHub API limits for push events (up to 100 files per request)

Copilot uses AI. Check for mistakes.

## 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
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pin actions/github-script to a specific patch version (as done elsewhere in this repo, e.g. actions/github-script@v7.0.1) to reduce supply-chain risk and keep builds reproducible.

Suggested change
uses: actions/github-script@v7
uses: actions/github-script@v7.0.1

Copilot uses AI. Check for mistakes.
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