Skip to content

Commit aacb7d2

Browse files
committed
Introduce a simple file change detection GitHub action
1 parent d715561 commit aacb7d2

File tree

7 files changed

+220
-46
lines changed

7 files changed

+220
-46
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: "Detect file changes"
2+
description: "Detects if any files matching given patterns changed in the current pull request."
3+
author: "GraalVM Reachability Metadata repository maintainers"
4+
inputs:
5+
file-patterns:
6+
description: "Glob pattern or list of glob patterns to check (supports '!' for negation, and *, **, ? wildcards)."
7+
required: true
8+
github_token:
9+
description: "GitHub token used to read PR changed files via the GitHub API."
10+
required: false
11+
default: ${{ github.token }}
12+
runs:
13+
using: "node20"
14+
main: "index.js"
15+
outputs:
16+
changes_detected:
17+
description: "true if any file matches the specified patterns, false otherwise"
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// In-repo file change filter for GitHub pull request workflows.
2+
//
3+
// Responsibilities:
4+
// - Accepts a YAML-like input ('patterns') listing glob patterns
5+
// (supports negation via '!' and wildcards *, **, ?).
6+
// - Fetches changed files for the current pull request using the GitHub REST API.
7+
// - Determines if any changed file matches the pattern set (negations are respected).
8+
// - Emits a single GitHub Action output: 'changed=true' if any match, otherwise 'false'.
9+
10+
const fs = require('fs');
11+
const https = require('https');
12+
13+
function getInput(name, required = false) {
14+
const key = `INPUT_${name.replace(/ /g, '_').toUpperCase()}`;
15+
const val = process.env[key];
16+
if (required && (!val || val.trim() === '')) {
17+
throw new Error(`Input required and not supplied: ${name}`);
18+
}
19+
return val || '';
20+
}
21+
22+
function httpJson(options) {
23+
return new Promise((resolve, reject) => {
24+
const req = https.request({ method: 'GET', ...options }, (res) => {
25+
let data = '';
26+
res.on('data', (chunk) => (data += chunk));
27+
res.on('end', () => {
28+
if (res.statusCode && res.statusCode >= 400) {
29+
return reject(
30+
new Error(`HTTP ${res.statusCode} ${res.statusMessage}: ${data || ''}`)
31+
);
32+
}
33+
try {
34+
resolve(data ? JSON.parse(data) : null);
35+
} catch (e) {
36+
reject(new Error(`Failed to parse JSON: ${e.message}. Body: ${data}`));
37+
}
38+
});
39+
});
40+
req.on('error', reject);
41+
req.end();
42+
});
43+
}
44+
45+
async function getChangedFilesFromPR() {
46+
const eventName = process.env.GITHUB_EVENT_NAME || '';
47+
if (!['pull_request', 'pull_request_target'].includes(eventName)) return [];
48+
49+
const eventPath = process.env.GITHUB_EVENT_PATH;
50+
if (!eventPath || !fs.existsSync(eventPath)) return [];
51+
52+
const payload = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
53+
const pr = payload.pull_request;
54+
if (!pr || !pr.number) return [];
55+
56+
const [owner, repo] = (process.env.GITHUB_REPOSITORY || '').split('/');
57+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.INPUT_GITHUB_TOKEN;
58+
if (!owner || !repo || !token) return [];
59+
60+
const files = [];
61+
let page = 1;
62+
const perPage = 100;
63+
64+
while (true) {
65+
const path = `/repos/${owner}/${repo}/pulls/${pr.number}/files?per_page=${perPage}&page=${page}`;
66+
const headers = {
67+
'User-Agent': 'paths-filter-lite',
68+
Authorization: `token ${token}`,
69+
Accept: 'application/vnd.github+json',
70+
};
71+
const res = await httpJson({ hostname: 'api.github.com', path, headers });
72+
if (!Array.isArray(res) || res.length === 0) break;
73+
files.push(...res.map(f => f.filename).filter(Boolean));
74+
if (res.length < perPage) break;
75+
page += 1;
76+
}
77+
78+
return files;
79+
}
80+
81+
function parsePatterns(yamlLike) {
82+
const lines = (yamlLike || '').split(/\r?\n/);
83+
const patterns = [];
84+
for (const line of lines) {
85+
const trimmed = line.trim();
86+
if (!trimmed) continue;
87+
if (trimmed.startsWith('-')) {
88+
let pat = trimmed.slice(1).trim();
89+
if ((pat.startsWith("'") && pat.endsWith("'")) || (pat.startsWith('"') && pat.endsWith('"'))) {
90+
pat = pat.slice(1, -1);
91+
}
92+
patterns.push(pat);
93+
}
94+
}
95+
return patterns;
96+
}
97+
98+
function escapeRegexChar(ch) {
99+
return ch.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
100+
}
101+
102+
function globToRegex(glob) {
103+
let re = '';
104+
for (let i = 0; i < glob.length; i++) {
105+
const ch = glob[i];
106+
if (ch === '*') {
107+
if (glob[i + 1] === '*') {
108+
re += '.*';
109+
i++;
110+
} else {
111+
re += '[^/]*';
112+
}
113+
} else if (ch === '?') {
114+
re += '[^/]';
115+
} else {
116+
re += escapeRegexChar(ch);
117+
}
118+
}
119+
return `^${re}$`;
120+
}
121+
122+
function compilePatterns(patternsRaw) {
123+
const patterns = (patternsRaw || []).map(s => {
124+
let negative = false;
125+
s = s.trim();
126+
if (s.startsWith('!')) {
127+
negative = true;
128+
s = s.slice(1).trim();
129+
}
130+
return { negative, rx: new RegExp(globToRegex(s)) };
131+
});
132+
const hasPositive = patterns.some(p => !p.negative);
133+
return { patterns, hasPositive };
134+
}
135+
136+
function fileMatches(file, compiled) {
137+
let included = compiled.hasPositive ? false : true;
138+
for (const p of compiled.patterns) {
139+
if (p.rx.test(file)) included = p.negative ? false : true;
140+
}
141+
return included;
142+
}
143+
144+
(async function main() {
145+
try {
146+
const patternsInput = getInput('file-patterns', true);
147+
const patterns = parsePatterns(patternsInput);
148+
const compiled = compilePatterns(patterns);
149+
150+
const changedFiles = await getChangedFilesFromPR();
151+
console.log(`Changed files detected (${changedFiles.length}):`);
152+
changedFiles.forEach(f => console.log(`- ${f}`));
153+
154+
const matched = changedFiles.some(f => fileMatches(f, compiled));
155+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `changed=${matched ? 'true' : 'false'}\n`);
156+
console.log(`Files match filter -> ${matched}`);
157+
} catch (err) {
158+
console.log(`paths-filter-lite encountered an error: ${err?.message || err}`);
159+
process.exit(0);
160+
}
161+
})();

.github/workflows/checkstyle.yml

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,23 @@ jobs:
1414

1515
- name: "🔎 Detect relevant file changes"
1616
id: filter
17-
uses: dorny/paths-filter@v3
17+
uses: ./.github/actions/detect-file-changes
1818
with:
19-
filters: |
20-
relevant_files_changed:
21-
- '!docs/**'
22-
- '!**.md'
23-
- '!library-and-framework-list*.json'
24-
predicate-quantifier: 'every'
19+
file-patterns: |
20+
- '!docs/**'
21+
- '!**.md'
22+
- '!library-and-framework-list*.json'
2523
2624
- uses: actions/setup-python@v4
27-
if: steps.filter.outputs.relevant_files_changed == 'true'
25+
if: steps.filter.outputs.changed == 'true'
2826

2927
- name: "🔧 Setup java"
30-
if: steps.filter.outputs.relevant_files_changed == 'true'
28+
if: steps.filter.outputs.changed == 'true'
3129
uses: actions/setup-java@v4
3230
with:
3331
distribution: 'graalvm'
3432
java-version: '21'
3533

3634
- name: "Run Checkstyle"
37-
if: steps.filter.outputs.relevant_files_changed == 'true'
35+
if: steps.filter.outputs.changed == 'true'
3836
run: ./gradlew checkstyle

.github/workflows/library-and-framework-list-validation.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@ jobs:
1414

1515
- name: "🔎 Detect relevant file changes"
1616
id: filter
17-
uses: dorny/paths-filter@v3
17+
uses: ./.github/actions/detect-file-changes
1818
with:
19-
filters: |
20-
relevant_files_changed:
21-
- 'library-and-framework-list*.json'
19+
file-patterns: |
20+
- 'library-and-framework-list*.json'
2221
2322
- uses: actions/setup-python@v4
2423
with:
2524
python-version: '3.10'
26-
if: steps.filter.outputs.relevant_files_changed == 'true'
25+
if: steps.filter.outputs.changed == 'true'
26+
2727

2828
- name: Check that the JSON file is well-formatted and sorted by artifact
29-
if: steps.filter.outputs.relevant_files_changed == 'true'
29+
if: steps.filter.outputs.changed == 'true'
3030
run: |
3131
JSON="library-and-framework-list.json"
3232
if ! jq -e . "${JSON}" >/dev/null; then
@@ -40,7 +40,7 @@ jobs:
4040
fi
4141
4242
- name: Check that the JSON file conforms to the schema
43-
if: steps.filter.outputs.relevant_files_changed == 'true'
43+
if: steps.filter.outputs.changed == 'true'
4444
run: |
4545
pip install check-jsonschema
4646
check-jsonschema --schemafile library-and-framework-list-schema.json library-and-framework-list.json

.github/workflows/scan-docker-images.yml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,26 @@ jobs:
2020

2121
- name: "🔎 Detect relevant file changes"
2222
id: filter
23-
uses: dorny/paths-filter@v3
23+
uses: ./.github/actions/detect-file-changes
2424
with:
25-
filters: |
26-
relevant_files_changed:
27-
- 'tests/tck-build-logic/src/main/resources/allowed-docker-images/**'
25+
file-patterns: |
26+
- 'tests/tck-build-logic/src/main/resources/allowed-docker-images/**'
2827
2928
- uses: actions/setup-java@v4
30-
if: github.event_name == 'schedule' || steps.filter.outputs.relevant_files_changed == 'true'
29+
if: github.event_name == 'schedule' || steps.filter.outputs.changed == 'true'
3130
with:
3231
distribution: 'graalvm'
3332
java-version: '21'
3433
github-token: ${{ secrets.GITHUB_TOKEN }}
3534

3635
- name: "Install required tools"
37-
if: github.event_name == 'schedule' || steps.filter.outputs.relevant_files_changed == 'true'
36+
if: github.event_name == 'schedule' || steps.filter.outputs.changed == 'true'
3837
run: |
3938
curl -sSfL https://get.anchore.io/grype/v0.104.0/install.sh | sudo sh -s -- -b /usr/local/bin
4039
sudo apt-get install jq
4140
4241
- name: "🔎 Check changed docker images"
43-
if: github.event_name == 'pull_request' && steps.filter.outputs.relevant_files_changed == 'true'
42+
if: github.event_name == 'pull_request' && steps.filter.outputs.changed == 'true'
4443
run: ./gradlew checkAllowedDockerImages --baseCommit=${{ github.event.pull_request.base.sha }} --newCommit=${{ github.event.pull_request.head.sha }}
4544

4645
- name: "🔎 Check all docker images"

.github/workflows/test-changed-infrastructure.yml

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
outputs:
1818
matrix: ${{ steps.set-matrix.outputs.matrix }}
1919
none-found: ${{ steps.set-matrix.outputs.none-found }}
20-
relevant-files-changed: ${{ steps.filter.outputs.relevant_files_changed }}
20+
relevant-files-changed: ${{ steps.filter.outputs.changed }}
2121
steps:
2222
- name: "☁️ Checkout repository"
2323
uses: actions/checkout@v4
@@ -26,32 +26,31 @@ jobs:
2626

2727
- name: "🔎 Detect relevant file changes"
2828
id: filter
29-
uses: dorny/paths-filter@v3
29+
uses: ./.github/actions/detect-file-changes
3030
with:
31-
filters: |
32-
relevant_files_changed:
33-
- 'tests/tck-build-logic/**'
34-
- 'gradle/**'
35-
- 'build.gradle'
36-
- 'settings.gradle'
37-
- 'gradle.properties'
31+
file-patterns: |
32+
- 'tests/tck-build-logic/**'
33+
- 'gradle/**'
34+
- 'build.gradle'
35+
- 'settings.gradle'
36+
- 'gradle.properties'
3837
3938
- name: "🔧 Setup java"
40-
if: steps.filter.outputs.relevant_files_changed == 'true'
39+
if: steps.filter.outputs.changed == 'true'
4140
uses: actions/setup-java@v4
4241
with:
4342
distribution: 'graalvm'
4443
java-version: '21'
4544

4645
- name: "🕸️ Populate matrix"
4746
id: set-matrix
48-
if: steps.filter.outputs.relevant_files_changed == 'true'
47+
if: steps.filter.outputs.changed == 'true'
4948
run: |
5049
./gradlew generateInfrastructureChangedCoordinatesMatrix -PbaseCommit=${{ github.event.pull_request.base.sha }} -PnewCommit=${{ github.event.pull_request.head.sha }}
5150
5251
test-changed-infrastructure:
5352
name: "🧪 ${{ matrix.coordinates }} (GraalVM for JDK ${{ matrix.version }} @ ${{ matrix.os }})"
54-
if: needs.get-changed-infrastructure.outputs.relevant-files-changed == 'true' && needs.get-changed-infrastructure.result == 'success' && needs.get-changed-infrastructure.outputs.none-found != 'true' && github.repository == 'oracle/graalvm-reachability-metadata'
53+
if: needs.get-changed-infrastructure.outputs.relevant-files-changed == 'true' && needs.get-changed-infrastructure.result == 'success' && needs.get-changed-infrastructure.outputs.none-found != 'true' && github.repository == 'jormundur00/graalvm-reachability-metadata'
5554
runs-on: ${{ matrix.os }}
5655
timeout-minutes: 20
5756
needs: get-changed-infrastructure

.github/workflows/test-changed-metadata.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
outputs:
1818
matrix: ${{ steps.set-matrix.outputs.matrix }}
1919
none-found: ${{ steps.set-matrix.outputs.none-found }}
20-
relevant-files-changed: ${{ steps.filter.outputs.relevant_files_changed }}
20+
relevant-files-changed: ${{ steps.filter.outputs.changed }}
2121
steps:
2222
- name: "☁️ Checkout repository"
2323
uses: actions/checkout@v4
@@ -26,30 +26,30 @@ jobs:
2626

2727
- name: "🔎 Detect relevant file changes"
2828
id: filter
29-
uses: dorny/paths-filter@v3
29+
uses: ./.github/actions/detect-file-changes
3030
with:
31-
filters: |
32-
relevant_files_changed:
33-
- 'metadata/**'
34-
- 'tests/src/**'
31+
file-patterns: |
32+
- 'metadata/**'
33+
- 'tests/src/**'
3534
3635
- name: "🔧 Setup Java"
37-
if: steps.filter.outputs.relevant_files_changed == 'true'
36+
if: steps.filter.outputs.changed == 'true'
3837
uses: actions/setup-java@v4
3938
with:
4039
distribution: 'graalvm'
4140
java-version: '21'
42-
github-token: ${{ secrets.GITHUB_TOKEN }}
4341

4442
- name: "🕸️ Populate matrix"
4543
id: set-matrix
46-
if: steps.filter.outputs.relevant_files_changed == 'true'
44+
if: steps.filter.outputs.changed == 'true'
4745
run: |
4846
./gradlew generateChangedCoordinatesMatrix -PbaseCommit=${{ github.event.pull_request.base.sha }} -PnewCommit=${{ github.event.pull_request.head.sha }}
47+
echo "Matrix:"
48+
echo "${{ steps.set-matrix.outputs.matrix }}"
4949
5050
test-changed-metadata:
5151
name: "🧪 ${{ matrix.coordinates }} (GraalVM for JDK ${{ matrix.version }} @ ${{ matrix.os }})"
52-
if: needs.get-changed-metadata.outputs.relevant-files-changed == 'true' && needs.get-changed-metadata.result == 'success' && needs.get-changed-metadata.outputs.none-found != 'true' && github.repository == 'oracle/graalvm-reachability-metadata'
52+
if: needs.get-changed-metadata.outputs.relevant-files-changed == 'true' && needs.get-changed-metadata.result == 'success' && needs.get-changed-metadata.outputs.none-found != 'true' && github.repository == 'jormundur00/graalvm-reachability-metadata'
5353
runs-on: ${{ matrix.os }}
5454
timeout-minutes: 20
5555
needs: get-changed-metadata

0 commit comments

Comments
 (0)