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>
This commit is contained in:
63
scripts/pr-target-policy.cjs
Normal file
63
scripts/pr-target-policy.cjs
Normal file
@@ -0,0 +1,63 @@
|
||||
'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,
|
||||
};
|
||||
Reference in New Issue
Block a user