Skip to content

Commit 927d0ab

Browse files
authored
Merge pull request #1105 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 0c5b7b7 + 2ddca8e commit 927d0ab

2 files changed

Lines changed: 194 additions & 0 deletions

File tree

.github/scripts/validate-json.mjs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { readFile, readdir, writeFile, appendFile } from "node:fs/promises";
2+
import path from "node:path";
3+
4+
// Usage: node validate-json.mjs [--strip <prefix>] <dir> [dir...]
5+
// --strip removes a leading path prefix from reported filenames, so a PR checked
6+
// out into a subdirectory still reports repo-relative paths.
7+
const argv = process.argv.slice(2);
8+
let strip = "";
9+
const roots = [];
10+
for (let i = 0; i < argv.length; i++) {
11+
if (argv[i] === "--strip") {
12+
strip = argv[++i] ?? "";
13+
} else {
14+
roots.push(argv[i]);
15+
}
16+
}
17+
18+
async function collect(dir) {
19+
let entries;
20+
try {
21+
entries = await readdir(dir, { withFileTypes: true });
22+
} catch (error) {
23+
if (error.code === "ENOENT") return [];
24+
throw error;
25+
}
26+
const files = [];
27+
for (const entry of entries) {
28+
const full = path.join(dir, entry.name);
29+
if (entry.isDirectory()) {
30+
if (entry.name === "node_modules") continue;
31+
files.push(...(await collect(full)));
32+
} else if (entry.name.endsWith(".json")) {
33+
files.push(full);
34+
}
35+
}
36+
return files;
37+
}
38+
39+
const report = (file) => {
40+
const normalised = file.split(path.sep).join("/");
41+
return strip && normalised.startsWith(strip) ? normalised.slice(strip.length) : normalised;
42+
};
43+
44+
const failures = [];
45+
let checked = 0;
46+
47+
for (const root of roots) {
48+
for (const file of await collect(root)) {
49+
checked++;
50+
// Strip a leading UTF-8 BOM: it's tolerated by PowerShell's ConvertFrom-Json
51+
// (how the API loads these files) but rejected by Node's JSON.parse.
52+
const contents = (await readFile(file, "utf8")).replace(/^\uFEFF/, "");
53+
try {
54+
JSON.parse(contents);
55+
} catch (error) {
56+
failures.push({ file: report(file), message: error.message.replace(/\r?\n/g, " ") });
57+
}
58+
}
59+
}
60+
61+
for (const { file, message } of failures) {
62+
// Annotate the PR diff via a GitHub Actions error command.
63+
console.log(`::error file=${file}::${message}`);
64+
}
65+
console.log(`Checked ${checked} JSON file(s), ${failures.length} invalid.`);
66+
67+
// Hand the results to the workflow so it can comment on the PR.
68+
if (process.env.GITHUB_OUTPUT) {
69+
await appendFile(process.env.GITHUB_OUTPUT, `invalid_count=${failures.length}\n`);
70+
}
71+
await writeFile("json-validation-results.json", JSON.stringify(failures, null, 2));
72+
73+
process.exit(failures.length > 0 ? 1 : 0);
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
name: Validate JSON
3+
on:
4+
# pull_request_target (not pull_request) so the token can comment on fork PRs.
5+
# The PR's own code is never executed: it is checked out into ./pr as data only,
6+
# and parsed by the validator script from the trusted base checkout.
7+
pull_request_target:
8+
types: [opened, synchronize, reopened]
9+
branches:
10+
- main
11+
- dev
12+
paths:
13+
- "Config/**/*.json"
14+
- "Tools/**/*.json"
15+
- "AddMSPApp/**/*.json"
16+
- ".github/workflows/Validate_JSON.yml"
17+
- ".github/scripts/validate-json.mjs"
18+
push:
19+
branches:
20+
- dev
21+
paths:
22+
- "Config/**/*.json"
23+
- "Tools/**/*.json"
24+
- "AddMSPApp/**/*.json"
25+
- ".github/workflows/Validate_JSON.yml"
26+
- ".github/scripts/validate-json.mjs"
27+
concurrency:
28+
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.ref }}
29+
cancel-in-progress: true
30+
permissions:
31+
contents: read
32+
pull-requests: write
33+
jobs:
34+
validate:
35+
name: Parse JSON in Config, Tools and AddMSPApp
36+
runs-on: ubuntu-latest
37+
steps:
38+
- name: Checkout base (trusted validator script)
39+
uses: actions/checkout@v6
40+
41+
- name: Checkout PR head (untrusted, data only)
42+
if: github.event_name == 'pull_request_target'
43+
uses: actions/checkout@v6
44+
with:
45+
repository: ${{ github.event.pull_request.head.repo.full_name }}
46+
ref: ${{ github.event.pull_request.head.sha }}
47+
path: pr
48+
persist-credentials: false
49+
50+
- name: Validate JSON files
51+
id: validate
52+
continue-on-error: true
53+
env:
54+
ROOT: ${{ github.event_name == 'pull_request_target' && 'pr/' || '' }}
55+
run: >-
56+
node .github/scripts/validate-json.mjs --strip "$ROOT"
57+
"${ROOT}Config" "${ROOT}Tools" "${ROOT}AddMSPApp"
58+
59+
- name: Comment on PR
60+
if: github.event_name == 'pull_request_target'
61+
uses: actions/github-script@v9
62+
with:
63+
github-token: ${{ secrets.GITHUB_TOKEN }}
64+
script: |
65+
const fs = require('fs');
66+
const marker = '<!-- validate-json -->';
67+
const resultsFile = 'json-validation-results.json';
68+
if (!fs.existsSync(resultsFile)) {
69+
// The validator crashed before reporting; let its own error stand.
70+
core.warning('No validation results found — skipping PR comment.');
71+
return;
72+
}
73+
const failures = JSON.parse(fs.readFileSync(resultsFile, 'utf8'));
74+
75+
// Find a previous comment from this workflow so we update instead of piling up.
76+
const { data: comments } = await github.rest.issues.listComments({
77+
...context.repo,
78+
issue_number: context.issue.number,
79+
per_page: 100,
80+
});
81+
const existing = comments.find(
82+
(c) => c.user.type === 'Bot' && c.body.includes(marker)
83+
);
84+
85+
let body;
86+
if (failures.length > 0) {
87+
const list = failures
88+
.map(({ file, message }) => `- \`${file}\`\n > ${message}`)
89+
.join('\n');
90+
body =
91+
`${marker}\n### ⚠️ Invalid JSON detected\n\n` +
92+
`${failures.length} JSON file(s) in this PR could not be parsed. ` +
93+
`These files are loaded directly by CIPP, so a syntax error here breaks the app at runtime.\n\n` +
94+
`${list}\n\n` +
95+
`Please fix the syntax and push again — this comment will update automatically.`;
96+
} else if (existing) {
97+
body = `${marker}\n### ✅ JSON is valid\n\nAll JSON files in \`Config\`, \`Tools\` and \`AddMSPApp\` parse correctly. Thanks for fixing it!`;
98+
} else {
99+
// Nothing was ever broken — stay quiet.
100+
return;
101+
}
102+
103+
if (existing) {
104+
await github.rest.issues.updateComment({
105+
...context.repo,
106+
comment_id: existing.id,
107+
body,
108+
});
109+
} else {
110+
await github.rest.issues.createComment({
111+
...context.repo,
112+
issue_number: context.issue.number,
113+
body,
114+
});
115+
}
116+
117+
- name: Fail if any JSON is invalid
118+
if: steps.validate.outputs.invalid_count != '0'
119+
run: |
120+
echo "::error::${{ steps.validate.outputs.invalid_count }} invalid JSON file(s). See annotations above."
121+
exit 1

0 commit comments

Comments
 (0)