2929 pull-requests : read
3030 statuses : write
3131 steps :
32- # Trusted main definition: no candidate checkout, artifacts, or executable inputs.
32+ # Execute only the helper belonging to this immutable workflow revision.
33+ - name : Check out the trusted review policy
34+ uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
35+ with :
36+ ref : ${{ github.workflow_sha }}
37+ persist-credentials : false
38+
3339 - name : Re-evaluate current PR and queue after review changes
40+ env :
41+ GH_TOKEN : ${{ github.token }}
42+ GIT_CONFIG_COUNT : ' 2'
43+ GIT_CONFIG_KEY_0 : credential.helper
44+ GIT_CONFIG_VALUE_0 : ' '
45+ GIT_CONFIG_KEY_1 : credential.https://github.com.helper
46+ GIT_CONFIG_VALUE_1 : ' !gh auth git-credential'
3447 uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
3548 with :
3649 script : |
@@ -56,58 +69,21 @@ jobs:
5669 if (!/^[0-9a-f]{40}$/.test(sha || '')) throw new Error('Invalid policy merge base');
5770 return sha;
5871 }
59- async function readPolicy(ref) {
60- const {data: file} = await github.rest.repos.getContent({
61- ...context.repo, path: '.castiron-ratchet.json', ref,
62- });
63- if (file.type !== 'file' || file.encoding !== 'base64' || !Number.isSafeInteger(file.size) ||
64- file.size > 4096 || typeof file.content !== 'string') throw new Error('Invalid policy file');
65- const content = Buffer.from(file.content, 'base64');
66- if (content.length !== file.size) throw new Error('Invalid policy content');
67- const policy = JSON.parse(content.toString('utf8'), (key, value, source) =>
68- key === 'max_custom_patch_lines' && typeof value === 'number' ? BigInt(source.source) : value);
69- if (!policy || typeof policy !== 'object' || typeof policy.max_custom_patch_lines !== 'bigint' ||
70- policy.max_custom_patch_lines < 0n) throw new Error('Invalid policy budget');
71- return policy;
72- }
73- async function supportsEffectivePinReview(ref) {
74- let file;
72+ async function requiresHuman(pr, mainSha) {
73+ const path = require('node:path');
74+ const fs = require('node:fs');
75+ const root = fs.mkdtempSync(path.join(process.env.RUNNER_TEMP, 'castiron-review-'));
7576 try {
76- ({data: file} = await github.rest.repos.getContent({
77- ...context.repo, path: 'scripts/castiron/custom_code_policy.py', ref,
78- }));
79- } catch (error) {
80- if (error.status === 404) return false;
81- throw error;
77+ const result = await exec.getExecOutput('python3', ['-I',
78+ 'scripts/castiron/custom_code_policy.py', 'review-required',
79+ '--repository', repo, '--repo', path.join(root, 'objects.git'),
80+ '--base', pr.base.sha, '--head', pr.head.sha, '--main', mainSha], {silent: true});
81+ const decision = JSON.parse(result.stdout);
82+ if (typeof decision.requires_human !== 'boolean') throw new Error('Invalid review decision');
83+ return decision.requires_human;
84+ } finally {
85+ fs.rmSync(root, {recursive: true, force: true});
8286 }
83- if (file.type !== 'file' || file.encoding !== 'base64' || !Number.isSafeInteger(file.size) ||
84- file.size < 0 || typeof file.content !== 'string') throw new Error('Invalid checker contract');
85- const content = Buffer.from(file.content, 'base64');
86- if (content.length !== file.size) throw new Error('Invalid checker content');
87- // Read data only from the effective checker, never import or execute it.
88- const declarations = content.toString('utf8').match(/^PIN_REVIEW_POLICY = .*$/gm) || [];
89- return declarations.length === 1 && declarations[0] === 'PIN_REVIEW_POLICY = "effective-policy-v1"';
90- }
91- async function pinRequiresHuman(pr, headPolicy, mainSha) {
92- const targetPolicy = await readPolicy(pr.base.sha);
93- const pinSha = pin => pin?.repository === repo && /^[0-9a-f]{40}$/.test(pin.main_sha || '')
94- ? pin.main_sha : null;
95- const proposedChecker = pinSha(headPolicy.next_policy);
96- const previousChecker = targetPolicy.next_policy ? pinSha(targetPolicy.next_policy) : mainSha;
97- if (!proposedChecker || !/^[0-9a-f]{40}$/.test(previousChecker || '') ||
98- !/^[0-9a-f]{40}$/.test(mainSha || '')) return true;
99- // Older checkers require human review for every pin edit. Retain that
100- // requirement until the protected checker explicitly supports the exemption.
101- if (!await supportsEffectivePinReview(previousChecker)) return true;
102- const [previous, proposed, previousBase, proposedBase] = await Promise.all([
103- readPolicy(previousChecker),
104- readPolicy(proposedChecker), mergeBase(previousChecker, proposedChecker),
105- mergeBase(proposedChecker, mainSha),
106- ]);
107- return previousBase !== previousChecker || proposedBase !== proposedChecker ||
108- proposed.max_custom_patch_lines > previous.max_custom_patch_lines ||
109- proposed.schema_version !== previous.schema_version ||
110- proposed.generated_files !== previous.generated_files || proposed.ratchet !== previous.ratchet;
11187 }
11288 const dirtyTargets = new Set();
11389 // Retry discovery within this job timeout while targets are changing.
@@ -146,18 +122,17 @@ jobs:
146122 const pr = matching[0];
147123 const files = await github.paginate(github.rest.pulls.listFiles, {...context.repo, pull_number: pr.number});
148124 if (!files.some(file => file.filename === '.castiron-ratchet.json')) return;
149- const baseline = await mergeBase(pr.base.sha, pr.head.sha);
150- const [basePolicy, headPolicy] = await Promise.all([readPolicy(baseline), readPolicy(pr.head.sha)]);
151- const pinChanged = !require('node:util').isDeepStrictEqual(basePolicy.next_policy ?? null, headPolicy.next_policy ?? null);
152- let requiresHuman = headPolicy.max_custom_patch_lines > basePolicy.max_custom_patch_lines ||
153- (headPolicy.ratchet === 'go' && basePolicy.ratchet !== 'go');
154- let mainSha = null;
155- if (!requiresHuman && pinChanged) {
156- const {data: main} = await github.rest.git.getRef({...context.repo, ref: 'heads/main'});
157- mainSha = main.object.sha;
158- requiresHuman = await pinRequiresHuman(pr, headPolicy, mainSha);
125+ const {data: main} = await github.rest.git.getRef({...context.repo, ref: 'heads/main'});
126+ const mainSha = main.object.sha;
127+ let requiresReview;
128+ try {
129+ requiresReview = await requiresHuman(pr, mainSha);
130+ } catch (error) {
131+ const {data: currentMain} = await github.rest.git.getRef({...context.repo, ref: 'heads/main'});
132+ if (currentMain.object.sha !== mainSha) continue;
133+ throw error;
159134 }
160- if (!requiresHuman ) {
135+ if (!requiresReview ) {
161136 const [{data: current}, {data: target}, main] = await Promise.all([
162137 github.rest.pulls.get({...context.repo, pull_number: pr.number}),
163138 github.rest.git.getRef({...context.repo, ref: 'heads/next'}),
0 commit comments