Files
msd-core/scripts/pr-target-policy.cjs
Tom Boucher 1fa7bc594c refactor(#1190): extract ADR-230 PR-target branch policy into a tested, fork-safe seam (#1246)
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>
2026-06-14 17:16:19 -04:00

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,
};