mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
8e695cc267
The ruleset required only `lint` (shared's eslint and prettier) and `check-target`. Types, lint, coverage, end-to-end and the S3 contract were advisory, and the three health PRs merged with red checks. Nothing on a PR into dev built the Dockerfile or rendered the Helm chart either, which is how #2610 shipped a Dockerfile no build could parse. ci-ok needs every gate, test, image and chart job and is meant to be the one check the ruleset requires. It runs with always(), fails when a job it needs failed or was cancelled, and passes when they passed or were skipped. The Sonar scan stays outside: its quality gate reports on the PR by itself, and an outage there should not block merges. A required check cannot sit behind a workflow-level path filter: on a docs-only PR the workflow never starts and the check never reports. The filter moves into a `changes` job. It diffs the PR's merge commit against its base (or a push against the previous head) and scripts/ci/ changed-areas.mjs maps the files to code, image and deploy areas. It fails closed: no usable diff, an empty one or a path it cannot read cleanly runs everything. Its tests run in the same job. docker-image builds the Dockerfile with buildx (layer cache written by pushes only, read by PRs), starts it with the same hardening as docker-compose.yml, waits up to three minutes for /api/health/ready, checks the VERSION file and that / serves the client, and prints the container log. deploy-manifests lints the chart, renders it with the default values and with each secret, persistence and ingress mode, validates the output with kubeconform, and runs `docker compose config` on every compose file. helm and kubeconform are pinned by version and sha256. While the file is open: every job runs on ubuntu-24.04 instead of the moving ubuntu-latest, reads its Node version from .nvmrc (the plugin-sdk jobs ran 22 under a comment saying it matched the server, which runs 24), download-artifact moves from v6, still on Node 20, to v7, and the Sonar action is pinned by commit.
118 lines
4.3 KiB
JavaScript
118 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Decides which parts of the test workflow a change needs.
|
|
//
|
|
// test.yml used to filter on `paths:` at the workflow level. A required check
|
|
// cannot live behind such a filter: when the filter skips the whole workflow,
|
|
// the check never reports and the pull request waits for it forever. So the
|
|
// workflow always runs, its first job lists the changed files and pipes them
|
|
// through this script, and every other job asks the answer whether to run.
|
|
// The aggregate `ci-ok` job then treats a skipped job as passed.
|
|
//
|
|
// Usage:
|
|
// node scripts/ci/changed-areas.mjs --files <list> one changed path per line
|
|
// node scripts/ci/changed-areas.mjs --all no usable list, run everything
|
|
//
|
|
// Prints `<area>=true|false` per area and appends the same lines to
|
|
// $GITHUB_OUTPUT when it is set.
|
|
//
|
|
// It fails closed. A list it cannot read is an error, and anything it cannot
|
|
// classify with confidence (an empty list, a path git had to quote) turns every
|
|
// area on: running a job too often costs minutes, skipping one it needed lets a
|
|
// break through.
|
|
|
|
import { appendFileSync, readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
// The workflow file and this script decide what runs, so a change to either
|
|
// runs everything.
|
|
const ALWAYS = ['.github/workflows/test.yml', 'scripts/ci/'];
|
|
|
|
export const AREAS = {
|
|
// The type, lint, test, coverage, bundle and end-to-end jobs. npm workspaces
|
|
// hoist every dependency into the root lockfile, so a dependency bump touches
|
|
// none of the package directories. The Dockerfile and the entrypoint script
|
|
// are pinned by server/tests/unit/docker-entrypoint.test.ts.
|
|
code: [
|
|
'server/',
|
|
'client/',
|
|
'shared/',
|
|
'plugin-sdk/',
|
|
'package.json',
|
|
'package-lock.json',
|
|
'.nvmrc',
|
|
'sonar-project.properties',
|
|
'Dockerfile',
|
|
...ALWAYS,
|
|
],
|
|
// The image build and its boot check: everything the Dockerfile copies or
|
|
// installs from.
|
|
image: [
|
|
'server/',
|
|
'client/',
|
|
'shared/',
|
|
'package.json',
|
|
'package-lock.json',
|
|
'.nvmrc',
|
|
'Dockerfile',
|
|
'.dockerignore',
|
|
...ALWAYS,
|
|
],
|
|
// The Helm chart and the compose files.
|
|
deploy: ['charts/', /^docker-compose[^/]*\.ya?ml$/, ...ALWAYS],
|
|
};
|
|
|
|
// Markdown inside the packages (CLAUDE.md, READMEs, PATTERN.md) changes no
|
|
// test, no build and no scan.
|
|
const isMarkdown = (path) => path.endsWith('.md');
|
|
|
|
function matches(rule, path) {
|
|
if (rule instanceof RegExp) return rule.test(path);
|
|
return rule.endsWith('/') ? path.startsWith(rule) : path === rule;
|
|
}
|
|
|
|
/**
|
|
* @param {string[] | null} files changed paths, or null when there is no usable list
|
|
* @returns {Record<string, boolean>}
|
|
*/
|
|
export function classify(files) {
|
|
const all = Object.fromEntries(Object.keys(AREAS).map((area) => [area, true]));
|
|
if (files === null) return all;
|
|
const paths = files.map((line) => line.replace(/\r$/, '').trim()).filter(Boolean);
|
|
if (paths.length === 0) return all;
|
|
// core.quotePath=false keeps ordinary non-ASCII names unquoted. A path that
|
|
// still arrives quoted carries control characters, and matching on its
|
|
// escaped form could miss it.
|
|
if (paths.some((path) => path.startsWith('"'))) return all;
|
|
|
|
const result = {};
|
|
for (const [area, rules] of Object.entries(AREAS)) {
|
|
result[area] = paths.some((path) => !isMarkdown(path) && rules.some((rule) => matches(rule, path)));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
if (argv.length === 1 && argv[0] === '--all') return { all: true };
|
|
if (argv.length === 2 && argv[0] === '--files' && argv[1]) return { all: false, list: argv[1] };
|
|
throw new Error('usage: changed-areas.mjs --files <list> | --all');
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
// An unreadable list throws and fails the job instead of running on a guess.
|
|
const files = args.all ? null : readFileSync(args.list, 'utf8').split('\n');
|
|
const result = classify(files);
|
|
const lines = Object.entries(result).map(([area, on]) => `${area}=${on}`);
|
|
console.log(lines.join('\n'));
|
|
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `${lines.join('\n')}\n`);
|
|
}
|
|
|
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
try {
|
|
main();
|
|
} catch (err) {
|
|
console.error(err instanceof Error ? err.message : err);
|
|
process.exit(1);
|
|
}
|
|
}
|