-
Notifications
You must be signed in to change notification settings - Fork 6
fix(devops): transport exact pre-push evidence #493
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,20 @@ | ||
| import process from 'node:process'; | ||
| import { parsePrePushInput, serializePrePushUpdates } from '../signing/signing-core.mjs'; | ||
| import { runNodeScript } from './shared.mjs'; | ||
|
|
||
| let input = ''; | ||
| process.stdin.setEncoding('utf8'); | ||
| for await (const chunk of process.stdin) input += chunk; | ||
| try { | ||
| const updates = parsePrePushInput(input); | ||
| process.env.WORLD_SCRIPT_PREPUSH_UPDATES = serializePrePushUpdates(updates); | ||
| } catch (error) { | ||
| console.error( | ||
| `pre-push evidence capture failed closed: ${error instanceof Error ? error.message : 'invalid input'}`, | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (runNodeScript('scripts/signing/verify-outgoing.mjs', process.argv.slice(2)) !== 0) | ||
| process.exit(1); | ||
| process.exit(runNodeScript('scripts/ci-prepush-lowend.mjs')); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -245,6 +245,103 @@ export function parseRefUpdate(line) { | |
| return { localRef: fields[0], localSha: fields[1], remoteRef: fields[2], remoteSha: fields[3] }; | ||
| } | ||
|
|
||
| // QNBS-v3: parse the hook stream once so signing and admission consume identical push evidence. | ||
| export function parsePrePushInput(input) { | ||
| if (typeof input !== 'string') throw new Error('pre-push input must be text'); | ||
| const lines = input.split(/\r?\n/).filter((line) => line.length > 0); | ||
| if (lines.length === 0) throw new Error('pre-push input is empty'); | ||
|
Comment on lines
+251
to
+252
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When AGENTS.md reference: AGENTS.md:L297-L303 Useful? React with 👍 / 👎. |
||
| const updates = lines.map(parseRefUpdate); | ||
| if (updates.some((update) => !update)) throw new Error('invalid pre-push ref-update input'); | ||
| return updates; | ||
| } | ||
|
|
||
| export function serializePrePushUpdates(updates) { | ||
| if (!Array.isArray(updates) || updates.length === 0) | ||
| throw new Error('pre-push updates are empty'); | ||
| const lines = updates.map((update) => { | ||
| const fields = [update.localRef, update.localSha, update.remoteRef, update.remoteSha]; | ||
| if (fields.some((field) => typeof field !== 'string' || field.length === 0)) | ||
| throw new Error('pre-push update contains an invalid field'); | ||
| return fields.join(' '); | ||
| }); | ||
| return JSON.stringify(lines); | ||
| } | ||
|
|
||
| export function parseSerializedPrePushUpdates(serialized) { | ||
| if (typeof serialized !== 'string' || serialized.length === 0) | ||
| throw new Error('serialized pre-push input is missing'); | ||
| let lines; | ||
| try { | ||
| lines = JSON.parse(serialized); | ||
| } catch { | ||
| throw new Error('serialized pre-push input is not valid JSON'); | ||
| } | ||
| if (!Array.isArray(lines) || lines.some((line) => typeof line !== 'string')) | ||
| throw new Error('serialized pre-push input must contain text lines'); | ||
| return parsePrePushInput(lines.join('\n')); | ||
| } | ||
|
|
||
| function changedFilesBetween(base, head, cwd) { | ||
| const result = runGit(['diff', '--no-renames', '--name-only', '-z', base, head, '--'], { cwd }); | ||
| if (result.status !== 0) throw new Error('cannot resolve changed paths for outgoing ref'); | ||
| return result.stdout.split('\0').filter((path) => path.length > 0); | ||
| } | ||
|
|
||
| export function resolvePushEvidence(input, cwd = process.cwd(), dependencies = {}) { | ||
| try { | ||
| const updates = Array.isArray(input) | ||
| ? input.every((item) => typeof item === 'string') | ||
| ? parsePrePushInput(input.join('\n')) | ||
| : input | ||
| : parsePrePushInput(input); | ||
| if (updates.length === 0 || updates.some((update) => !update)) | ||
| throw new Error('pre-push updates are empty or invalid'); | ||
| const commitExists = | ||
| dependencies.commitExists ?? | ||
| ((sha) => isSha(sha) && runGit(['cat-file', '-e', `${sha}^{commit}`], { cwd }).status === 0); | ||
| const resolveFiles = | ||
| dependencies.changedFilesBetween ?? ((base, head) => changedFilesBetween(base, head, cwd)); | ||
| const changedFiles = new Set(); | ||
| const evidenceUpdates = []; | ||
| for (const update of updates) { | ||
| if ( | ||
| (!isSha(update.localSha) && !isZeroSha(update.localSha)) || | ||
| (!isSha(update.remoteSha) && !isZeroSha(update.remoteSha)) | ||
| ) | ||
| throw new Error(`invalid SHA in update for ${update.remoteRef}`); | ||
| if (isZeroSha(update.localSha)) { | ||
| evidenceUpdates.push({ ...update, disposition: 'DELETED' }); | ||
| continue; | ||
| } | ||
| if (!commitExists(update.localSha)) | ||
| throw new Error(`local outgoing object is unavailable for ${update.localRef}`); | ||
| if (update.remoteRef.startsWith('refs/tags/')) { | ||
| evidenceUpdates.push({ ...update, disposition: 'TAG' }); | ||
| continue; | ||
| } | ||
| if (!update.remoteRef.startsWith('refs/heads/')) | ||
| throw new Error(`unsupported outgoing ref ${update.remoteRef}`); | ||
| const base = isZeroSha(update.remoteSha) ? EMPTY_TREE : update.remoteSha; | ||
| if (!isZeroSha(base) && base !== EMPTY_TREE && !commitExists(base)) | ||
| throw new Error(`remote base object is unavailable for ${update.remoteRef}`); | ||
| for (const path of resolveFiles(base, update.localSha)) changedFiles.add(path); | ||
| evidenceUpdates.push({ | ||
| ...update, | ||
| base, | ||
| disposition: isZeroSha(update.remoteSha) ? 'NEW_BRANCH' : 'UPDATED', | ||
| }); | ||
| } | ||
| return { updates: evidenceUpdates, changedFiles: [...changedFiles], evidenceState: 'RESOLVED' }; | ||
| } catch (error) { | ||
| return { | ||
| updates: [], | ||
| changedFiles: [], | ||
| evidenceState: 'INVALID', | ||
| reason: error instanceof Error ? error.message : 'invalid push evidence', | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| function refSha(ref, cwd) { | ||
| const sha = gitOutput(['rev-parse', '--verify', `${ref}^{commit}`], { cwd }); | ||
| return isSha(sha) ? sha : null; | ||
|
|
@@ -339,14 +436,25 @@ export function classifyTagVerification({ | |
| : commitVerification; | ||
| } | ||
|
|
||
| export function verifyOutgoingUpdates(lines, remote, cwd = process.cwd(), dependencies = {}) { | ||
| export function verifyOutgoingUpdates(input, remote, cwd = process.cwd(), dependencies = {}) { | ||
| const verifyCommit = dependencies.verifyCommitObject ?? ((sha) => verifyCommitObject(sha, cwd)); | ||
| const verifyTag = dependencies.verifyTagObject ?? ((sha) => verifyTagObject(sha, cwd)); | ||
| const getIntroducedCommits = | ||
| dependencies.introducedCommits ?? ((update) => introducedCommits(update, remote, cwd)); | ||
| const updates = lines.map(parseRefUpdate); | ||
| if (updates.some((update) => !update)) | ||
| return { ok: false, reason: 'invalid pre-push ref-update input' }; | ||
| let updates; | ||
| try { | ||
| updates = | ||
| Array.isArray(input) && input.every((item) => typeof item === 'string') | ||
| ? parsePrePushInput(input.join('\n')) | ||
| : input; | ||
|
Comment on lines
+446
to
+449
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The updated declaration permits Useful? React with 👍 / 👎. |
||
| if (!Array.isArray(updates) || updates.length === 0 || updates.some((update) => !update)) | ||
| throw new Error('invalid pre-push ref-update input'); | ||
| } catch (error) { | ||
| return { | ||
| ok: false, | ||
| reason: error instanceof Error ? error.message : 'invalid pre-push ref-update input', | ||
| }; | ||
| } | ||
| const reports = []; | ||
| for (const update of updates) { | ||
| if (isZeroSha(update.localSha)) continue; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Storing the complete update stream in
WORLD_SCRIPT_PREPUSH_UPDATESputs the serialized data into the child processes' inherited environment. Large multi-ref pushes can exceed the operating system's environment/argument-size limit, causingspawnSyncinrunNodeScriptto fail and rejecting an otherwise valid push. Use a bounded file or pipe-based handoff instead of environment transport. [possible bug]Severity Level: Major⚠️
Prompt for AI Agent 🤖