ADR-230's branching-model gate (pr-target-validator.yml) decided allowed/blocked PR targets via inline regex in github-script — untestable. Extracted the decision into committed scripts/pr-target-policy.cjs (classifyPrTarget(base,head)->{decision}), and rewired the workflow to checkout the BASE ref (trusted; fork-tamper-safe) + require the module. Behavior-identical (Codex-verified char-by-char regex equivalence + all side-effects preserved). 70 tests incl. an equivalence oracle battery + hyphen-boundary negatives. Added contents:read for the checkout. Re-attribution: no ADR-230 test references exist (issue's '2 misattributed files' claim not borne out).
Closes #1190
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
64 lines
2.0 KiB
JavaScript
64 lines
2.0 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* PR target-branch policy — ADR-230.
|
|
*
|
|
* Extracted from .github/workflows/pr-target-validator.yml so the classification
|
|
* logic can be unit-tested independently and required by the workflow at checkout
|
|
* from the TRUSTED base-branch copy (fork-tamper-safe).
|
|
*
|
|
* Pure module: no I/O, no GitHub API calls, no side effects.
|
|
*
|
|
* See: docs/branching.md, docs/adr/230-introduce-next-integration-branch.md
|
|
*/
|
|
|
|
/**
|
|
* The five patterns that allow a PR to target `main`.
|
|
* Verbatim from the github-script in pr-target-validator.yml.
|
|
*
|
|
* @type {RegExp[]}
|
|
*/
|
|
const MAIN_ALLOWED_PATTERNS = [
|
|
/^release\/\d+\.\d+\.0$/, // release branches
|
|
/^hotfix\/\d+\.\d+\.\d+$/, // hotfix branches
|
|
/^fix\/critical-/, // production-down emergencies
|
|
/^chore\/backmerge-/, // auto-backmerge from this workflow
|
|
/^revert\/critical-/, // emergency reverts
|
|
];
|
|
|
|
/**
|
|
* Classify a pull request by its base and head branch names.
|
|
*
|
|
* @param {string} base - The PR's target branch (e.g. 'next', 'main', 'release/1.2.0').
|
|
* @param {string} head - The PR's source branch (e.g. 'feat/my-feature').
|
|
* @returns {{ decision: 'allowed' | 'blocked' | 'unusual' }}
|
|
*/
|
|
function classifyPrTarget(base, head) {
|
|
// PRs targeting `next` are always fine.
|
|
if (base === 'next') {
|
|
return { decision: 'allowed' };
|
|
}
|
|
|
|
// PRs targeting `main`: only specific branch types allowed.
|
|
if (base === 'main') {
|
|
const allowed = MAIN_ALLOWED_PATTERNS.some(re => re.test(head));
|
|
if (allowed) {
|
|
return { decision: 'allowed' };
|
|
}
|
|
return { decision: 'blocked' };
|
|
}
|
|
|
|
// PRs targeting release/X.Y.0 or hotfix/X.Y.Z are fine (stabilization PRs).
|
|
if (/^release\/\d+\.\d+\.0$/.test(base) || /^hotfix\/\d+\.\d+\.\d+$/.test(base)) {
|
|
return { decision: 'allowed' };
|
|
}
|
|
|
|
// Any other target is unusual but not forbidden.
|
|
return { decision: 'unusual' };
|
|
}
|
|
|
|
module.exports = {
|
|
MAIN_ALLOWED_PATTERNS,
|
|
classifyPrTarget,
|
|
};
|