dorfl 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advance-ci-template.d.ts +17 -16
- package/dist/advance-ci-template.d.ts.map +1 -1
- package/dist/advance-ci-template.js +42 -53
- package/dist/advance-ci-template.js.map +1 -1
- package/dist/advance-drivers.d.ts +8 -0
- package/dist/advance-drivers.d.ts.map +1 -1
- package/dist/advance-drivers.js +28 -4
- package/dist/advance-drivers.js.map +1 -1
- package/dist/advance-isolated.d.ts.map +1 -1
- package/dist/advance-isolated.js +3 -2
- package/dist/advance-isolated.js.map +1 -1
- package/dist/advance-lifecycle-template.d.ts +41 -40
- package/dist/advance-lifecycle-template.d.ts.map +1 -1
- package/dist/advance-lifecycle-template.js +309 -489
- package/dist/advance-lifecycle-template.js.map +1 -1
- package/dist/advance-loop-driver.d.ts.map +1 -1
- package/dist/advance-loop-driver.js +3 -2
- package/dist/advance-loop-driver.js.map +1 -1
- package/dist/advance-treeless-publish.d.ts +31 -0
- package/dist/advance-treeless-publish.d.ts.map +1 -1
- package/dist/advance-treeless-publish.js +40 -0
- package/dist/advance-treeless-publish.js.map +1 -1
- package/dist/advance.d.ts +63 -3
- package/dist/advance.d.ts.map +1 -1
- package/dist/advance.js +80 -12
- package/dist/advance.js.map +1 -1
- package/dist/advancing-lock.d.ts +9 -0
- package/dist/advancing-lock.d.ts.map +1 -1
- package/dist/advancing-lock.js +21 -1
- package/dist/advancing-lock.js.map +1 -1
- package/dist/agent-env.d.ts +61 -0
- package/dist/agent-env.d.ts.map +1 -0
- package/dist/agent-env.js +144 -0
- package/dist/agent-env.js.map +1 -0
- package/dist/agent-stop.d.ts +35 -0
- package/dist/agent-stop.d.ts.map +1 -1
- package/dist/agent-stop.js +62 -0
- package/dist/agent-stop.js.map +1 -1
- package/dist/apply-merge-action.d.ts +76 -34
- package/dist/apply-merge-action.d.ts.map +1 -1
- package/dist/apply-merge-action.js +339 -129
- package/dist/apply-merge-action.js.map +1 -1
- package/dist/apply-stuck-action.js +2 -2
- package/dist/apply-stuck-action.js.map +1 -1
- package/dist/ci-agent-result.d.ts +151 -0
- package/dist/ci-agent-result.d.ts.map +1 -0
- package/dist/ci-agent-result.js +314 -0
- package/dist/ci-agent-result.js.map +1 -0
- package/dist/ci-handoff-apply.d.ts +162 -0
- package/dist/ci-handoff-apply.d.ts.map +1 -0
- package/dist/ci-handoff-apply.js +497 -0
- package/dist/ci-handoff-apply.js.map +1 -0
- package/dist/ci-handoff-format.d.ts +369 -0
- package/dist/ci-handoff-format.d.ts.map +1 -0
- package/dist/ci-handoff-format.js +646 -0
- package/dist/ci-handoff-format.js.map +1 -0
- package/dist/ci-handoff-lfs.d.ts +146 -0
- package/dist/ci-handoff-lfs.d.ts.map +1 -0
- package/dist/ci-handoff-lfs.js +397 -0
- package/dist/ci-handoff-lfs.js.map +1 -0
- package/dist/ci-handoff.d.ts +99 -0
- package/dist/ci-handoff.d.ts.map +1 -0
- package/dist/ci-handoff.js +277 -0
- package/dist/ci-handoff.js.map +1 -0
- package/dist/ci-lock-outputs.d.ts +93 -0
- package/dist/ci-lock-outputs.d.ts.map +1 -0
- package/dist/ci-lock-outputs.js +223 -0
- package/dist/ci-lock-outputs.js.map +1 -0
- package/dist/ci-phase-build.d.ts +178 -0
- package/dist/ci-phase-build.d.ts.map +1 -0
- package/dist/ci-phase-build.js +989 -0
- package/dist/ci-phase-build.js.map +1 -0
- package/dist/ci-phase-driver.d.ts +247 -0
- package/dist/ci-phase-driver.d.ts.map +1 -0
- package/dist/ci-phase-driver.js +370 -0
- package/dist/ci-phase-driver.js.map +1 -0
- package/dist/ci-phase-intake.d.ts +172 -0
- package/dist/ci-phase-intake.d.ts.map +1 -0
- package/dist/ci-phase-intake.js +555 -0
- package/dist/ci-phase-intake.js.map +1 -0
- package/dist/ci-phase-tasking.d.ts +205 -0
- package/dist/ci-phase-tasking.d.ts.map +1 -0
- package/dist/ci-phase-tasking.js +906 -0
- package/dist/ci-phase-tasking.js.map +1 -0
- package/dist/ci-phase-treeless.d.ts +224 -0
- package/dist/ci-phase-treeless.d.ts.map +1 -0
- package/dist/ci-phase-treeless.js +1482 -0
- package/dist/ci-phase-treeless.js.map +1 -0
- package/dist/ci-read-token.d.ts +47 -0
- package/dist/ci-read-token.d.ts.map +1 -0
- package/dist/ci-read-token.js +80 -0
- package/dist/ci-read-token.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +447 -22
- package/dist/cli.js.map +1 -1
- package/dist/close-job-template.d.ts +10 -1
- package/dist/close-job-template.d.ts.map +1 -1
- package/dist/close-job-template.js +46 -14
- package/dist/close-job-template.js.map +1 -1
- package/dist/close-job.d.ts +20 -5
- package/dist/close-job.d.ts.map +1 -1
- package/dist/close-job.js +65 -12
- package/dist/close-job.js.map +1 -1
- package/dist/complete.d.ts.map +1 -1
- package/dist/complete.js +20 -1
- package/dist/complete.js.map +1 -1
- package/dist/config.d.ts +41 -44
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -4
- package/dist/config.js.map +1 -1
- package/dist/continue-branch.d.ts +63 -15
- package/dist/continue-branch.d.ts.map +1 -1
- package/dist/continue-branch.js +75 -8
- package/dist/continue-branch.js.map +1 -1
- package/dist/crashed-run-save.d.ts +73 -0
- package/dist/crashed-run-save.d.ts.map +1 -0
- package/dist/crashed-run-save.js +86 -0
- package/dist/crashed-run-save.js.map +1 -0
- package/dist/cwd-section.d.ts.map +1 -1
- package/dist/cwd-section.js +10 -2
- package/dist/cwd-section.js.map +1 -1
- package/dist/decision-engine.d.ts +10 -0
- package/dist/decision-engine.d.ts.map +1 -1
- package/dist/decision-engine.js.map +1 -1
- package/dist/do-config.d.ts +2 -24
- package/dist/do-config.d.ts.map +1 -1
- package/dist/do-config.js +1 -33
- package/dist/do-config.js.map +1 -1
- package/dist/do.d.ts +149 -1
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +115 -25
- package/dist/do.js.map +1 -1
- package/dist/dorfl-item-template.d.ts +98 -0
- package/dist/dorfl-item-template.d.ts.map +1 -0
- package/dist/dorfl-item-template.js +504 -0
- package/dist/dorfl-item-template.js.map +1 -0
- package/dist/env-config.d.ts.map +1 -1
- package/dist/env-config.js +7 -5
- package/dist/env-config.js.map +1 -1
- package/dist/frontmatter.d.ts +52 -0
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +136 -2
- package/dist/frontmatter.js.map +1 -1
- package/dist/gc.d.ts +13 -0
- package/dist/gc.d.ts.map +1 -1
- package/dist/gc.js +16 -2
- package/dist/gc.js.map +1 -1
- package/dist/git.d.ts +18 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +70 -5
- package/dist/git.js.map +1 -1
- package/dist/harness.d.ts +44 -5
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +24 -1
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/install-ci-action-pins.d.ts +123 -0
- package/dist/install-ci-action-pins.d.ts.map +1 -0
- package/dist/install-ci-action-pins.js +190 -0
- package/dist/install-ci-action-pins.js.map +1 -0
- package/dist/install-ci-actions-pr-creation.d.ts +70 -0
- package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
- package/dist/install-ci-actions-pr-creation.js +106 -0
- package/dist/install-ci-actions-pr-creation.js.map +1 -0
- package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
- package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
- package/dist/install-ci-capabilities/dorfl-item.js +26 -0
- package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
- package/dist/install-ci-core.d.ts +155 -37
- package/dist/install-ci-core.d.ts.map +1 -1
- package/dist/install-ci-core.js +304 -200
- package/dist/install-ci-core.js.map +1 -1
- package/dist/install-ci-github.d.ts +47 -2
- package/dist/install-ci-github.d.ts.map +1 -1
- package/dist/install-ci-github.js +113 -1
- package/dist/install-ci-github.js.map +1 -1
- package/dist/install-ci.d.ts +25 -6
- package/dist/install-ci.d.ts.map +1 -1
- package/dist/install-ci.js +72 -36
- package/dist/install-ci.js.map +1 -1
- package/dist/intake-marker.d.ts +6 -1
- package/dist/intake-marker.d.ts.map +1 -1
- package/dist/intake-marker.js.map +1 -1
- package/dist/intake-triage.d.ts.map +1 -1
- package/dist/intake-triage.js +6 -2
- package/dist/intake-triage.js.map +1 -1
- package/dist/intake-trigger-template.d.ts +31 -25
- package/dist/intake-trigger-template.d.ts.map +1 -1
- package/dist/intake-trigger-template.js +107 -292
- package/dist/intake-trigger-template.js.map +1 -1
- package/dist/intake.d.ts +186 -4
- package/dist/intake.d.ts.map +1 -1
- package/dist/intake.js +533 -416
- package/dist/intake.js.map +1 -1
- package/dist/integration-core.d.ts +154 -1
- package/dist/integration-core.d.ts.map +1 -1
- package/dist/integration-core.js +759 -486
- package/dist/integration-core.js.map +1 -1
- package/dist/integrator.d.ts +12 -0
- package/dist/integrator.d.ts.map +1 -1
- package/dist/integrator.js +21 -0
- package/dist/integrator.js.map +1 -1
- package/dist/isolation.d.ts +12 -2
- package/dist/isolation.d.ts.map +1 -1
- package/dist/isolation.js +19 -8
- package/dist/isolation.js.map +1 -1
- package/dist/item-lock.d.ts +93 -11
- package/dist/item-lock.d.ts.map +1 -1
- package/dist/item-lock.js +233 -65
- package/dist/item-lock.js.map +1 -1
- package/dist/land-question-residue.d.ts +58 -0
- package/dist/land-question-residue.d.ts.map +1 -0
- package/dist/land-question-residue.js +95 -0
- package/dist/land-question-residue.js.map +1 -0
- package/dist/lifecycle-gather.d.ts +16 -0
- package/dist/lifecycle-gather.d.ts.map +1 -1
- package/dist/lifecycle-gather.js +54 -6
- package/dist/lifecycle-gather.js.map +1 -1
- package/dist/merge-answer.d.ts +41 -0
- package/dist/merge-answer.d.ts.map +1 -0
- package/dist/merge-answer.js +60 -0
- package/dist/merge-answer.js.map +1 -0
- package/dist/merge-question-surfacer.d.ts +105 -30
- package/dist/merge-question-surfacer.d.ts.map +1 -1
- package/dist/merge-question-surfacer.js +202 -91
- package/dist/merge-question-surfacer.js.map +1 -1
- package/dist/merge-question-tick.d.ts +64 -0
- package/dist/merge-question-tick.d.ts.map +1 -0
- package/dist/merge-question-tick.js +96 -0
- package/dist/merge-question-tick.js.map +1 -0
- package/dist/mint-adr.d.ts.map +1 -1
- package/dist/mint-adr.js +20 -2
- package/dist/mint-adr.js.map +1 -1
- package/dist/needs-attention.d.ts +72 -2
- package/dist/needs-attention.d.ts.map +1 -1
- package/dist/needs-attention.js +275 -38
- package/dist/needs-attention.js.map +1 -1
- package/dist/phase-recorder.d.ts +168 -0
- package/dist/phase-recorder.d.ts.map +1 -0
- package/dist/phase-recorder.js +218 -0
- package/dist/phase-recorder.js.map +1 -0
- package/dist/phase.d.ts +94 -0
- package/dist/phase.d.ts.map +1 -0
- package/dist/phase.js +148 -0
- package/dist/phase.js.map +1 -0
- package/dist/pi-harness.d.ts.map +1 -1
- package/dist/pi-harness.js +19 -6
- package/dist/pi-harness.js.map +1 -1
- package/dist/prepare.d.ts.map +1 -1
- package/dist/prepare.js +4 -0
- package/dist/prepare.js.map +1 -1
- package/dist/prompt.d.ts +4 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +48 -1
- package/dist/prompt.js.map +1 -1
- package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
- package/dist/protocol/WORK-CONTRACT.md +1 -1
- package/dist/reap-branches.d.ts +48 -0
- package/dist/reap-branches.d.ts.map +1 -1
- package/dist/reap-branches.js +64 -0
- package/dist/reap-branches.js.map +1 -1
- package/dist/ref-write.d.ts +198 -0
- package/dist/ref-write.d.ts.map +1 -0
- package/dist/ref-write.js +79 -0
- package/dist/ref-write.js.map +1 -0
- package/dist/repo-config.js +8 -8
- package/dist/repo-config.js.map +1 -1
- package/dist/repo-mirror.d.ts +36 -0
- package/dist/repo-mirror.d.ts.map +1 -1
- package/dist/repo-mirror.js +91 -1
- package/dist/repo-mirror.js.map +1 -1
- package/dist/review-verdict.d.ts +1 -1
- package/dist/review-verdict.js +1 -1
- package/dist/run.d.ts +7 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +28 -4
- package/dist/run.js.map +1 -1
- package/dist/scan.d.ts +19 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +21 -8
- package/dist/scan.js.map +1 -1
- package/dist/sidecar.d.ts +14 -0
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +21 -0
- package/dist/sidecar.js.map +1 -1
- package/dist/single-job-warning.d.ts +63 -0
- package/dist/single-job-warning.d.ts.map +1 -0
- package/dist/single-job-warning.js +110 -0
- package/dist/single-job-warning.js.map +1 -0
- package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
- package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
- package/dist/spec-complete.d.ts +13 -6
- package/dist/spec-complete.d.ts.map +1 -1
- package/dist/spec-complete.js +23 -11
- package/dist/spec-complete.js.map +1 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +47 -14
- package/dist/start.js.map +1 -1
- package/dist/status.d.ts +12 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +11 -3
- package/dist/status.js.map +1 -1
- package/dist/tasking.d.ts +246 -1
- package/dist/tasking.d.ts.map +1 -1
- package/dist/tasking.js +452 -358
- package/dist/tasking.js.map +1 -1
- package/dist/triage-persist.d.ts.map +1 -1
- package/dist/triage-persist.js +21 -2
- package/dist/triage-persist.js.map +1 -1
- package/dist/verify-workflow-template.d.ts.map +1 -1
- package/dist/verify-workflow-template.js +8 -4
- package/dist/verify-workflow-template.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +4 -0
- package/dist/verify.js.map +1 -1
- package/dist/watch-session.d.ts +42 -13
- package/dist/watch-session.d.ts.map +1 -1
- package/dist/watch-session.js +52 -23
- package/dist/watch-session.js.map +1 -1
- package/dist/work-layout.d.ts +10 -4
- package/dist/work-layout.d.ts.map +1 -1
- package/dist/work-layout.js +11 -3
- package/dist/work-layout.js.map +1 -1
- package/dist/workspace.d.ts +65 -4
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +159 -11
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
- package/src/advance-ci-template.ts +80 -81
- package/src/advance-drivers.ts +45 -6
- package/src/advance-isolated.ts +3 -5
- package/src/advance-lifecycle-template.ts +375 -538
- package/src/advance-loop-driver.ts +3 -5
- package/src/advance-treeless-publish.ts +61 -0
- package/src/advance.ts +145 -15
- package/src/advancing-lock.ts +36 -1
- package/src/agent-env.ts +174 -0
- package/src/agent-stop.ts +81 -0
- package/src/apply-merge-action.ts +401 -154
- package/src/apply-stuck-action.ts +2 -2
- package/src/ci-agent-result.ts +421 -0
- package/src/ci-handoff-apply.ts +737 -0
- package/src/ci-handoff-format.ts +926 -0
- package/src/ci-handoff-lfs.ts +519 -0
- package/src/ci-handoff.ts +392 -0
- package/src/ci-lock-outputs.ts +273 -0
- package/src/ci-phase-build.ts +1328 -0
- package/src/ci-phase-driver.ts +548 -0
- package/src/ci-phase-intake.ts +861 -0
- package/src/ci-phase-tasking.ts +1255 -0
- package/src/ci-phase-treeless.ts +2028 -0
- package/src/ci-read-token.ts +90 -0
- package/src/cli.ts +591 -29
- package/src/close-job-template.ts +52 -15
- package/src/close-job.ts +77 -12
- package/src/complete.ts +19 -1
- package/src/config.ts +43 -48
- package/src/continue-branch.ts +121 -16
- package/src/crashed-run-save.ts +162 -0
- package/src/cwd-section.ts +10 -0
- package/src/decision-engine.ts +10 -0
- package/src/do-config.ts +2 -57
- package/src/do.ts +170 -28
- package/src/dorfl-item-template.ts +556 -0
- package/src/env-config.ts +7 -5
- package/src/frontmatter.ts +155 -2
- package/src/gc.ts +27 -2
- package/src/git.ts +83 -5
- package/src/harness.ts +58 -6
- package/src/index.ts +50 -1
- package/src/install-ci-action-pins.ts +209 -0
- package/src/install-ci-actions-pr-creation.ts +186 -0
- package/src/install-ci-capabilities/dorfl-item.ts +33 -0
- package/src/install-ci-core.ts +388 -233
- package/src/install-ci-github.ts +149 -1
- package/src/install-ci.ts +83 -37
- package/src/intake-marker.ts +6 -1
- package/src/intake-triage.ts +7 -3
- package/src/intake-trigger-template.ts +126 -327
- package/src/intake.ts +725 -544
- package/src/integration-core.ts +975 -523
- package/src/integrator.ts +28 -0
- package/src/isolation.ts +29 -10
- package/src/item-lock.ts +308 -92
- package/src/land-question-residue.ts +122 -0
- package/src/lifecycle-gather.ts +82 -10
- package/src/merge-answer.ts +82 -0
- package/src/merge-question-surfacer.ts +320 -112
- package/src/merge-question-tick.ts +182 -0
- package/src/mint-adr.ts +28 -2
- package/src/needs-attention.ts +350 -42
- package/src/phase-recorder.ts +350 -0
- package/src/phase.ts +177 -0
- package/src/pi-harness.ts +25 -5
- package/src/prepare.ts +4 -0
- package/src/prompt.ts +68 -1
- package/src/reap-branches.ts +107 -0
- package/src/ref-write.ts +324 -0
- package/src/repo-config.ts +8 -8
- package/src/repo-mirror.ts +107 -1
- package/src/review-verdict.ts +1 -1
- package/src/run.ts +47 -6
- package/src/scan.ts +37 -7
- package/src/sidecar.ts +36 -0
- package/src/single-job-warning.ts +135 -0
- package/src/spec-complete.ts +36 -14
- package/src/start.ts +55 -16
- package/src/status.ts +29 -3
- package/src/tasking.ts +593 -378
- package/src/triage-persist.ts +24 -2
- package/src/verify-workflow-template.ts +8 -4
- package/src/verify.ts +4 -0
- package/src/watch-session.ts +86 -28
- package/src/work-layout.ts +11 -3
- package/src/workspace.ts +210 -14
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The apply phase's validation of a handoff** (spec
|
|
3
|
+
* `ci-agent-job-without-write-token`, ADR `ci-agent-job-holds-no-write-token`
|
|
4
|
+
* decisions 3 and 4, task `ci-split-apply-rejects-hostile-bundle`).
|
|
5
|
+
*
|
|
6
|
+
* The apply job holds a write token and runs no agent. It reads the agent job's
|
|
7
|
+
* artifact as HOSTILE: every target, mode and policy comes from TRUSTED inputs
|
|
8
|
+
* (the lock job's outputs, the workflow inputs, the tree at `baseSha`, the
|
|
9
|
+
* arbiter), never from the artifact. {@link validateApplyHandoff} is the one
|
|
10
|
+
* unit the path splits call BEFORE any write. It either returns the validated
|
|
11
|
+
* handoff (the bundle's tip fetched to {@link INCOMING_TIP_REF} in the apply
|
|
12
|
+
* checkout, the recomputed integration mode, the ledger report) or throws a
|
|
13
|
+
* {@link HandoffRejected} whose `rule` and message name the broken rule, having
|
|
14
|
+
* written nothing from the artifact anywhere: not to the arbiter, and not even
|
|
15
|
+
* into the apply checkout's object store.
|
|
16
|
+
*
|
|
17
|
+
* How "nothing is fetched into the repository" holds: the bundle is first
|
|
18
|
+
* fetched into a throwaway QUARANTINE repository whose object store borrows the
|
|
19
|
+
* apply checkout's objects (read-only, through `objects/info/alternates`), and
|
|
20
|
+
* every history, path and size check runs there. Only an accepted bundle is
|
|
21
|
+
* then fetched into the apply checkout, with the same command.
|
|
22
|
+
*
|
|
23
|
+
* The LFS objects (decision 6, task `ci-split-handoff-lfs-objects`): the new
|
|
24
|
+
* commits' blobs are scanned for pointers in the quarantine, and every object
|
|
25
|
+
* is checked against its pointer (`ci-handoff-lfs.ts`) before the import. The
|
|
26
|
+
* push of the objects (before any ref) is the path split's first write.
|
|
27
|
+
*
|
|
28
|
+
* Out of scope here: the agent-result, lock-ownership and re-run rules (wired
|
|
29
|
+
* by the path splits).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import {mkdtempSync, rmSync, writeFileSync} from 'node:fs';
|
|
33
|
+
import {join, posix} from 'node:path';
|
|
34
|
+
import type {IntegrationMode} from './config.js';
|
|
35
|
+
import {
|
|
36
|
+
handoffByteLimits,
|
|
37
|
+
rejectHandoff as reject,
|
|
38
|
+
type HandoffRung,
|
|
39
|
+
} from './ci-handoff-format.js';
|
|
40
|
+
import {
|
|
41
|
+
readHandoff,
|
|
42
|
+
type HandoffLfsObject,
|
|
43
|
+
type ReadHandoff,
|
|
44
|
+
} from './ci-handoff.js';
|
|
45
|
+
import {
|
|
46
|
+
checkLfsObjects,
|
|
47
|
+
isRegularFileMode,
|
|
48
|
+
scanLfsPointers,
|
|
49
|
+
type ChangedBlob,
|
|
50
|
+
type LfsScan,
|
|
51
|
+
} from './ci-handoff-lfs.js';
|
|
52
|
+
import {parseFrontmatter} from './frontmatter.js';
|
|
53
|
+
import {git, run} from './git.js';
|
|
54
|
+
import {APPLY_LIFECYCLE_FOLDERS} from './item-path.js';
|
|
55
|
+
import {
|
|
56
|
+
REPO_CONFIG_FILENAME,
|
|
57
|
+
REPO_CONFIG_FILENAME_LEGACY,
|
|
58
|
+
} from './repo-config.js';
|
|
59
|
+
import {resolveSidecarIdentity} from './sidecar.js';
|
|
60
|
+
import {workBranchRef} from './slug-namespace.js';
|
|
61
|
+
import {WORK_ROOT, workFolderPrefix, workItemRel} from './work-layout.js';
|
|
62
|
+
|
|
63
|
+
/** The apply checkout's ref an accepted bundle's tip is fetched to. */
|
|
64
|
+
export const INCOMING_TIP_REF = 'refs/dorfl/incoming/tip';
|
|
65
|
+
|
|
66
|
+
/** The most new commits (`tip ^<arbiter>/main`) a bundle may carry. */
|
|
67
|
+
export const MAX_NEW_COMMITS = 200;
|
|
68
|
+
|
|
69
|
+
/** The reason for a merge commit in the range (spec, "History"). */
|
|
70
|
+
export const MERGE_COMMIT_REASON =
|
|
71
|
+
'the work branch contains a merge commit; rebase it and requeue';
|
|
72
|
+
|
|
73
|
+
/** The longest symlink target read (Linux `PATH_MAX`); longer is rejected. */
|
|
74
|
+
const MAX_SYMLINK_TARGET_BYTES = 4096;
|
|
75
|
+
/** How many symlinks one resolution may follow before it counts as a loop. */
|
|
76
|
+
const MAX_SYMLINK_HOPS = 40;
|
|
77
|
+
/** How many ledger paths the PR-body section lists before "and N more". */
|
|
78
|
+
const LEDGER_REPORT_MAX_PATHS = 100;
|
|
79
|
+
|
|
80
|
+
const HEX_OID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
81
|
+
const MODE_SYMLINK = '120000';
|
|
82
|
+
const MODE_GITLINK = '160000';
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Protected paths (decision 3)
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The protected path rule a repository path falls under, or `undefined`. The
|
|
90
|
+
* fixed built-in list of decision 3 (not a config key): `.github/`,
|
|
91
|
+
* `CODEOWNERS` (root, `docs/`, `.github/`), the repository config (`dorfl.json`
|
|
92
|
+
* and its legacy dotfile, which dorfl still reads), `.lfsconfig`, and
|
|
93
|
+
* `.gitattributes` at ANY depth (a nested one sets merge attributes for its
|
|
94
|
+
* subtree just like the root one). Matched case-insensitively, so a
|
|
95
|
+
* `.GitHub/` that a case-insensitive checkout would fold into `.github/` is
|
|
96
|
+
* caught too.
|
|
97
|
+
*/
|
|
98
|
+
export function protectedPathOf(path: string): string | undefined {
|
|
99
|
+
const p = path.toLowerCase();
|
|
100
|
+
if (p === '.github' || p.startsWith('.github/')) return '.github/';
|
|
101
|
+
if (p === 'codeowners' || p === 'docs/codeowners') return 'CODEOWNERS';
|
|
102
|
+
if (
|
|
103
|
+
p === REPO_CONFIG_FILENAME.toLowerCase() ||
|
|
104
|
+
p === REPO_CONFIG_FILENAME_LEGACY.toLowerCase()
|
|
105
|
+
) {
|
|
106
|
+
return REPO_CONFIG_FILENAME;
|
|
107
|
+
}
|
|
108
|
+
if (p === '.lfsconfig') return '.lfsconfig';
|
|
109
|
+
if (p === '.gitattributes' || p.endsWith('/.gitattributes')) {
|
|
110
|
+
return '.gitattributes';
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Inputs and result
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/** The trusted facts the apply job validates a handoff against. */
|
|
120
|
+
export interface ApplyTrust {
|
|
121
|
+
/** The item this run carries (lock output / workflow input). */
|
|
122
|
+
item: string;
|
|
123
|
+
/** The rung the lock job classified. */
|
|
124
|
+
rung: HandoffRung;
|
|
125
|
+
/** The arbiter's `main` the lock job classified the item at. */
|
|
126
|
+
baseSha: string;
|
|
127
|
+
/** The arbiter git remote of the apply checkout. */
|
|
128
|
+
arbiter: string;
|
|
129
|
+
/** The integration mode the workflow asked for (before the recomputed rules). */
|
|
130
|
+
integrationMode: IntegrationMode;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A handoff that passed every rule. */
|
|
134
|
+
export interface ApplyHandoff {
|
|
135
|
+
handoff: ReadHandoff;
|
|
136
|
+
/**
|
|
137
|
+
* The integration mode to land with: `merge` only when the trusted
|
|
138
|
+
* `integrationMode` is `merge` AND the untrusted-origin rule, recomputed from
|
|
139
|
+
* the task at `baseSha`, does not force `propose`.
|
|
140
|
+
*/
|
|
141
|
+
mode: IntegrationMode;
|
|
142
|
+
/** Whether the untrusted-origin rule turned a trusted `merge` into `propose`. */
|
|
143
|
+
forcedPropose: boolean;
|
|
144
|
+
/** The accepted bundle, when the handoff carries one. */
|
|
145
|
+
bundle?: {
|
|
146
|
+
/** The work branch name (`workBranchRef` of the trusted item). */
|
|
147
|
+
workBranch: string;
|
|
148
|
+
/** The validated tip, now at {@link INCOMING_TIP_REF} in the apply checkout. */
|
|
149
|
+
tip: string;
|
|
150
|
+
/** The new commits (`tip ^<arbiter>/main`), newest first. */
|
|
151
|
+
newCommits: string[];
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* The artifact's LFS objects, each referenced by a pointer of the new
|
|
155
|
+
* commits and checked against it (size and sha256), sorted by oid. The path
|
|
156
|
+
* split pushes them before any ref.
|
|
157
|
+
*/
|
|
158
|
+
lfsObjects: HandoffLfsObject[];
|
|
159
|
+
/** `work/` paths the bundle changes outside this item's own transition (propose only). */
|
|
160
|
+
ledgerOutsideItem: string[];
|
|
161
|
+
/** The PR-body section listing {@link ledgerOutsideItem}, when there are any. */
|
|
162
|
+
ledgerReport?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// The recomputed policy
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The untrusted-origin build-propose rule (ADR `untrusted-origin-build-checkpoint`,
|
|
171
|
+
* `integration-core.ts`), recomputed from the task AT `baseSha` in the apply
|
|
172
|
+
* checkout, never from the bundle (whose done-move could strip the stamp).
|
|
173
|
+
* Scope as in `performIntegration`: the task BUILD transition only; CI passes
|
|
174
|
+
* no explicit `--merge`, so there is no override. Any copy of the task at
|
|
175
|
+
* `baseSha` stamped `originTrust: untrusted` forces `propose`.
|
|
176
|
+
*/
|
|
177
|
+
export function recomputeIntegrationMode(params: {
|
|
178
|
+
repo: string;
|
|
179
|
+
baseSha: string;
|
|
180
|
+
item: string;
|
|
181
|
+
integrationMode: IntegrationMode;
|
|
182
|
+
/** Whether the handoff is a task build (`integrate` on the build rung). */
|
|
183
|
+
build: boolean;
|
|
184
|
+
env?: NodeJS.ProcessEnv;
|
|
185
|
+
}): {mode: IntegrationMode; forcedPropose: boolean} {
|
|
186
|
+
const {repo, baseSha, integrationMode, build, env} = params;
|
|
187
|
+
if (integrationMode !== 'merge' || !build) {
|
|
188
|
+
return {mode: integrationMode, forcedPropose: false};
|
|
189
|
+
}
|
|
190
|
+
const {type, slug} = resolveSidecarIdentity(params.item);
|
|
191
|
+
if (type !== 'task') return {mode: 'merge', forcedPropose: false};
|
|
192
|
+
for (const folder of APPLY_LIFECYCLE_FOLDERS.task) {
|
|
193
|
+
const spec = `${baseSha}:${workItemRel(folder, `${slug}.md`)}`;
|
|
194
|
+
const r = run('git', ['cat-file', 'blob', spec], repo, {env});
|
|
195
|
+
if (r.status !== 0) continue;
|
|
196
|
+
if (parseFrontmatter(r.stdout).originTrust === 'untrusted') {
|
|
197
|
+
return {mode: 'propose', forcedPropose: true};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {mode: 'merge', forcedPropose: false};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// The ledger report
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The PR-body section that lists the `work/` paths a propose-mode bundle
|
|
209
|
+
* changes outside its own item (decision 3, user story 23). The path names come
|
|
210
|
+
* from the hostile bundle, so each is JSON-quoted inside a code fence longer
|
|
211
|
+
* than any backtick run, which keeps a crafted name from closing the fence or
|
|
212
|
+
* starting a heading; the list is bounded.
|
|
213
|
+
*/
|
|
214
|
+
export function renderLedgerReportSection(paths: readonly string[]): string {
|
|
215
|
+
const shown = paths
|
|
216
|
+
.slice(0, LEDGER_REPORT_MAX_PATHS)
|
|
217
|
+
.map((p) => JSON.stringify(p.slice(0, 500)));
|
|
218
|
+
const body = shown.join('\n');
|
|
219
|
+
const longestRun = Math.max(
|
|
220
|
+
0,
|
|
221
|
+
...(body.match(/`+/g) ?? []).map((r) => r.length),
|
|
222
|
+
);
|
|
223
|
+
const fence = '`'.repeat(Math.max(3, longestRun + 1));
|
|
224
|
+
const more =
|
|
225
|
+
paths.length > shown.length
|
|
226
|
+
? `\n\nand ${paths.length - shown.length} more.`
|
|
227
|
+
: '';
|
|
228
|
+
return (
|
|
229
|
+
'### Ledger changes outside this item\n\n' +
|
|
230
|
+
'The work branch changes these files under `work/`, which are not part of ' +
|
|
231
|
+
"this item's own transition, a new note or a new ADR. Check them before " +
|
|
232
|
+
'merging.\n\n' +
|
|
233
|
+
`${fence}text\n${body}\n${fence}${more}\n`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Git helpers
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
/** One `git diff-tree -r -z --raw` entry. */
|
|
242
|
+
interface RawChange {
|
|
243
|
+
oldMode: string;
|
|
244
|
+
newMode: string;
|
|
245
|
+
newOid: string;
|
|
246
|
+
status: string;
|
|
247
|
+
path: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function parseRawZ(out: string): RawChange[] {
|
|
251
|
+
const fields = out.split('\0');
|
|
252
|
+
const changes: RawChange[] = [];
|
|
253
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
254
|
+
const meta = fields[i];
|
|
255
|
+
if (!meta.startsWith(':')) break;
|
|
256
|
+
const [oldMode, newMode, , newOid, status] = meta.slice(1).split(' ');
|
|
257
|
+
changes.push({oldMode, newMode, newOid, status, path: fields[i + 1]});
|
|
258
|
+
}
|
|
259
|
+
return changes;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function diffTree(
|
|
263
|
+
cwd: string,
|
|
264
|
+
from: string,
|
|
265
|
+
to: string,
|
|
266
|
+
env?: NodeJS.ProcessEnv,
|
|
267
|
+
): RawChange[] {
|
|
268
|
+
return parseRawZ(
|
|
269
|
+
git(
|
|
270
|
+
[
|
|
271
|
+
'-c',
|
|
272
|
+
'diff.renames=false',
|
|
273
|
+
'diff-tree',
|
|
274
|
+
'-r',
|
|
275
|
+
'-z',
|
|
276
|
+
'--raw',
|
|
277
|
+
'--no-renames',
|
|
278
|
+
'--no-commit-id',
|
|
279
|
+
from,
|
|
280
|
+
to,
|
|
281
|
+
],
|
|
282
|
+
cwd,
|
|
283
|
+
{env},
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function short(sha: string): string {
|
|
289
|
+
return sha.slice(0, 12);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Quote a hostile path for a rejection message. */
|
|
293
|
+
function q(path: string): string {
|
|
294
|
+
return JSON.stringify(path.slice(0, 300));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Symlinks
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Resolve `path`'s target through the commit's own symlinks, component by
|
|
303
|
+
* component, and say whether it stays inside the repository (and out of
|
|
304
|
+
* `.git`). Lexical normalisation alone is not enough: `d -> .` and
|
|
305
|
+
* `a -> d/..` are each inside on their own, but `a` resolves to the parent.
|
|
306
|
+
*/
|
|
307
|
+
function symlinkEscapes(
|
|
308
|
+
path: string,
|
|
309
|
+
symlinks: ReadonlyMap<string, string>,
|
|
310
|
+
): boolean {
|
|
311
|
+
const stack: string[] = [];
|
|
312
|
+
const initial = symlinks.get(path);
|
|
313
|
+
if (initial === undefined || initial === '' || initial.includes('\0')) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
if (initial.startsWith('/')) return true;
|
|
317
|
+
let queue = [...posix.dirname(path).split('/'), ...initial.split('/')];
|
|
318
|
+
let hops = 0;
|
|
319
|
+
while (queue.length > 0) {
|
|
320
|
+
const c = queue.shift() as string;
|
|
321
|
+
if (c === '' || c === '.') continue;
|
|
322
|
+
if (c === '..') {
|
|
323
|
+
if (stack.length === 0) return true;
|
|
324
|
+
stack.pop();
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
stack.push(c);
|
|
328
|
+
const target = symlinks.get(stack.join('/'));
|
|
329
|
+
if (target !== undefined) {
|
|
330
|
+
if (++hops > MAX_SYMLINK_HOPS) return true;
|
|
331
|
+
if (target === '' || target.startsWith('/')) return true;
|
|
332
|
+
stack.pop();
|
|
333
|
+
queue = [...target.split('/'), ...queue];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return stack.length > 0 && stack[0].toLowerCase() === '.git';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Every symlink of `commit`'s tree with its target (sizes bounded first). */
|
|
340
|
+
function treeSymlinks(
|
|
341
|
+
cwd: string,
|
|
342
|
+
commit: string,
|
|
343
|
+
env?: NodeJS.ProcessEnv,
|
|
344
|
+
): Map<string, string> {
|
|
345
|
+
const entries = git(['ls-tree', '-r', '-z', commit], cwd, {env}).split('\0');
|
|
346
|
+
const links = new Map<string, string>();
|
|
347
|
+
for (const entry of entries) {
|
|
348
|
+
if (!entry.startsWith(`${MODE_SYMLINK} `)) continue;
|
|
349
|
+
const tab = entry.indexOf('\t');
|
|
350
|
+
const oid = entry.slice(0, tab).split(' ')[2];
|
|
351
|
+
const path = entry.slice(tab + 1);
|
|
352
|
+
const size = Number(git(['cat-file', '-s', oid], cwd, {env}).trim());
|
|
353
|
+
if (size > MAX_SYMLINK_TARGET_BYTES) {
|
|
354
|
+
reject(
|
|
355
|
+
'symlink-target',
|
|
356
|
+
`the symlink ${q(path)} has an oversized target`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
links.set(path, git(['cat-file', 'blob', oid], cwd, {env}));
|
|
360
|
+
}
|
|
361
|
+
return links;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// The bundle
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
interface BundleCheck {
|
|
369
|
+
quarantine: string;
|
|
370
|
+
bundlePath: string;
|
|
371
|
+
workBranch: string;
|
|
372
|
+
baseSha: string;
|
|
373
|
+
mainSha: string;
|
|
374
|
+
ownItemPaths: ReadonlySet<string>;
|
|
375
|
+
env?: NodeJS.ProcessEnv;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Fetch the bundle's one expected ref, with fsck, into `cwd`'s incoming ref. */
|
|
379
|
+
function fetchBundle(
|
|
380
|
+
cwd: string,
|
|
381
|
+
bundlePath: string,
|
|
382
|
+
workBranch: string,
|
|
383
|
+
env?: NodeJS.ProcessEnv,
|
|
384
|
+
): {status: number; stderr: string} {
|
|
385
|
+
run('git', ['update-ref', '-d', INCOMING_TIP_REF], cwd, {env});
|
|
386
|
+
return run(
|
|
387
|
+
'git',
|
|
388
|
+
[
|
|
389
|
+
'-c',
|
|
390
|
+
'transfer.fsckObjects=true',
|
|
391
|
+
'fetch',
|
|
392
|
+
'--quiet',
|
|
393
|
+
'--no-tags',
|
|
394
|
+
'--no-write-fetch-head',
|
|
395
|
+
bundlePath,
|
|
396
|
+
`refs/heads/${workBranch}:${INCOMING_TIP_REF}`,
|
|
397
|
+
],
|
|
398
|
+
cwd,
|
|
399
|
+
{env},
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Run every history, path and size rule over the bundle inside the quarantine;
|
|
405
|
+
* return the tip, the new commits and the ledger paths outside the item.
|
|
406
|
+
*/
|
|
407
|
+
function checkBundle(c: BundleCheck): {
|
|
408
|
+
tip: string;
|
|
409
|
+
newCommits: string[];
|
|
410
|
+
ledgerOutside: string[];
|
|
411
|
+
lfs: LfsScan;
|
|
412
|
+
} {
|
|
413
|
+
const {quarantine: qdir, env} = c;
|
|
414
|
+
const fetched = fetchBundle(qdir, c.bundlePath, c.workBranch, env);
|
|
415
|
+
if (fetched.status !== 0) {
|
|
416
|
+
reject(
|
|
417
|
+
'bundle-format',
|
|
418
|
+
`the bundle could not be fetched (fsck or format): ${fetched.stderr.trim().slice(0, 500)}`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const tip = git(['rev-parse', INCOMING_TIP_REF], qdir, {env}).trim();
|
|
422
|
+
|
|
423
|
+
// History: descends from baseSha, bounded, linear.
|
|
424
|
+
if (
|
|
425
|
+
run('git', ['merge-base', '--is-ancestor', c.baseSha, tip], qdir, {env})
|
|
426
|
+
.status !== 0
|
|
427
|
+
) {
|
|
428
|
+
reject(
|
|
429
|
+
'history',
|
|
430
|
+
`the work branch tip ${short(tip)} does not descend from the base ${c.baseSha}`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
const count = Number(
|
|
434
|
+
git(['rev-list', '--count', tip, `^${c.mainSha}`], qdir, {env}).trim(),
|
|
435
|
+
);
|
|
436
|
+
if (count === 0) {
|
|
437
|
+
reject('history', 'the work branch has no new commit over main');
|
|
438
|
+
}
|
|
439
|
+
if (count > MAX_NEW_COMMITS) {
|
|
440
|
+
reject(
|
|
441
|
+
'commit-count',
|
|
442
|
+
`the work branch has ${count} new commits, over the limit of ${MAX_NEW_COMMITS}`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const merges = git(
|
|
446
|
+
['rev-list', '--min-parents=2', tip, `^${c.mainSha}`],
|
|
447
|
+
qdir,
|
|
448
|
+
{env},
|
|
449
|
+
).trim();
|
|
450
|
+
if (merges !== '') {
|
|
451
|
+
reject('merge-commit', `${MERGE_COMMIT_REASON} (${short(merges)})`);
|
|
452
|
+
}
|
|
453
|
+
const newCommits = git(['rev-list', tip, `^${c.mainSha}`], qdir, {env})
|
|
454
|
+
.trim()
|
|
455
|
+
.split('\n');
|
|
456
|
+
|
|
457
|
+
// Per commit, against its single parent: protected paths, gitlinks,
|
|
458
|
+
// symlinks, blob sizes.
|
|
459
|
+
const blobs = new Map<string, string>();
|
|
460
|
+
const fileBlobs: ChangedBlob[] = [];
|
|
461
|
+
let oldestParent = '';
|
|
462
|
+
for (const commit of newCommits) {
|
|
463
|
+
const parents = git(['rev-list', '--parents', '-n', '1', commit], qdir, {
|
|
464
|
+
env,
|
|
465
|
+
})
|
|
466
|
+
.trim()
|
|
467
|
+
.split(' ')
|
|
468
|
+
.slice(1);
|
|
469
|
+
if (parents.length !== 1) {
|
|
470
|
+
reject('history', `the commit ${short(commit)} has no parent on main`);
|
|
471
|
+
}
|
|
472
|
+
oldestParent = parents[0];
|
|
473
|
+
let touchesSymlink = false;
|
|
474
|
+
for (const ch of diffTree(qdir, parents[0], commit, env)) {
|
|
475
|
+
const rule = protectedPathOf(ch.path);
|
|
476
|
+
if (rule !== undefined) {
|
|
477
|
+
reject(
|
|
478
|
+
'protected-path',
|
|
479
|
+
`the commit ${short(commit)} changes the protected path ${q(ch.path)} ` +
|
|
480
|
+
`(${rule}); CI never lands a change to a protected path, so ` +
|
|
481
|
+
'build this task locally',
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (ch.status === 'D') continue;
|
|
485
|
+
if (ch.newMode === MODE_GITLINK) {
|
|
486
|
+
reject(
|
|
487
|
+
'gitlink',
|
|
488
|
+
`the commit ${short(commit)} adds a gitlink (submodule) at ${q(ch.path)}`,
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (ch.newMode === MODE_SYMLINK) touchesSymlink = true;
|
|
492
|
+
blobs.set(ch.newOid, ch.path);
|
|
493
|
+
if (isRegularFileMode(ch.newMode)) {
|
|
494
|
+
fileBlobs.push({oid: ch.newOid, path: ch.path});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (touchesSymlink) {
|
|
498
|
+
const links = treeSymlinks(qdir, commit, env);
|
|
499
|
+
for (const path of links.keys()) {
|
|
500
|
+
if (symlinkEscapes(path, links)) {
|
|
501
|
+
reject(
|
|
502
|
+
'symlink-target',
|
|
503
|
+
`the symlink ${q(path)} in commit ${short(commit)} points outside the repository`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (blobs.size > 0) {
|
|
510
|
+
const maxBlob = handoffByteLimits().blobBytes;
|
|
511
|
+
const sizes = git(
|
|
512
|
+
['cat-file', '--batch-check=%(objectname) %(objectsize)'],
|
|
513
|
+
qdir,
|
|
514
|
+
{env, input: [...blobs.keys()].join('\n') + '\n'},
|
|
515
|
+
)
|
|
516
|
+
.trim()
|
|
517
|
+
.split('\n');
|
|
518
|
+
for (const line of sizes) {
|
|
519
|
+
const [oid, size] = line.split(' ');
|
|
520
|
+
if (Number(size) > maxBlob) {
|
|
521
|
+
reject(
|
|
522
|
+
'size',
|
|
523
|
+
`the blob ${q(blobs.get(oid) ?? oid)} is ${size} bytes, over the limit of ${maxBlob}`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// The work/ ledger rule, on the net change of the new commits.
|
|
530
|
+
const ledgerOutside: string[] = [];
|
|
531
|
+
const notes = `${WORK_ROOT}/notes/`;
|
|
532
|
+
for (const ch of diffTree(qdir, oldestParent, tip, env)) {
|
|
533
|
+
if (!ch.path.startsWith(`${WORK_ROOT}/`)) continue;
|
|
534
|
+
if (c.ownItemPaths.has(ch.path)) continue;
|
|
535
|
+
const newNote =
|
|
536
|
+
ch.path.startsWith(notes) &&
|
|
537
|
+
ch.status === 'A' &&
|
|
538
|
+
run('git', ['cat-file', '-e', `${c.mainSha}:${ch.path}`], qdir, {env})
|
|
539
|
+
.status !== 0;
|
|
540
|
+
if (!newNote) ledgerOutside.push(ch.path);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// LFS pointers among the blobs the new commits add or change (decision 6):
|
|
544
|
+
// a blob that parses as a pointer counts, whatever `.gitattributes` says.
|
|
545
|
+
const lfs = scanLfsPointers({cwd: qdir, blobs: fileBlobs, env});
|
|
546
|
+
return {tip, newCommits, ledgerOutside, lfs};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** The item's own ledger files: its body in every lifecycle folder of its type. */
|
|
550
|
+
function ownItemPaths(item: string): Set<string> {
|
|
551
|
+
const {type, slug} = resolveSidecarIdentity(item);
|
|
552
|
+
return new Set(
|
|
553
|
+
APPLY_LIFECYCLE_FOLDERS[type].map(
|
|
554
|
+
(f) => `${workFolderPrefix(f)}${slug}.md`,
|
|
555
|
+
),
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
// The entry point
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Validate the agent job's handoff before the apply job writes anything.
|
|
565
|
+
*
|
|
566
|
+
* `dir` is the downloaded artifact under `runnerTemp`; `repo` is the apply
|
|
567
|
+
* job's own checkout of the trusted base, whose `trust.arbiter` remote is
|
|
568
|
+
* fetched fresh here. The steps, each a rejection ({@link HandoffRejected})
|
|
569
|
+
* naming its rule:
|
|
570
|
+
*
|
|
571
|
+
* 1. the artifact (`readHandoff`): layout, sizes, schema, `item` equal to the
|
|
572
|
+
* trusted item, an intent kind the trusted rung can produce;
|
|
573
|
+
* 2. the bundle's one ref must be `refs/heads/<workBranchRef(type, slug)>` of
|
|
574
|
+
* the trusted item, and its prerequisites must exist in the apply checkout;
|
|
575
|
+
* 3. in the quarantine: fetch that ref only, with `transfer.fsckObjects`;
|
|
576
|
+
* `baseSha` is an ancestor of the tip; the new set `tip ^<arbiter>/main` is
|
|
577
|
+
* non-empty, at most {@link MAX_NEW_COMMITS} and has no merge commit; per
|
|
578
|
+
* new commit, no protected path ({@link protectedPathOf}), no gitlink, no
|
|
579
|
+
* symlink resolving outside the repository, no blob over the limit;
|
|
580
|
+
* 4. the `work/` ledger rule on the net change: only this item's own
|
|
581
|
+
* transition and new `work/notes/*` files (ADRs live outside `work/`);
|
|
582
|
+
* anything else is rejected in `merge` mode and reported in `propose` mode;
|
|
583
|
+
* 5. the LFS rule (`checkLfsObjects`): every blob the new commits add or
|
|
584
|
+
* change that parses as a pointer (strict spec v1; a blob that only looks
|
|
585
|
+
* like one is rejected) needs its object in `lfs/`, of exactly its size and
|
|
586
|
+
* hashing to its oid; no object may be unreferenced; the size limit holds;
|
|
587
|
+
* 6. only then the bundle is fetched into the apply checkout at
|
|
588
|
+
* {@link INCOMING_TIP_REF}. Nothing is written to the apply checkout's LFS
|
|
589
|
+
* store here: the path split pushes {@link ApplyHandoff.lfsObjects} before
|
|
590
|
+
* any ref (`pushLfsObjects`).
|
|
591
|
+
*
|
|
592
|
+
* The integration mode is recomputed ({@link recomputeIntegrationMode}) before
|
|
593
|
+
* the ledger rule, which depends on it.
|
|
594
|
+
*/
|
|
595
|
+
export function validateApplyHandoff(params: {
|
|
596
|
+
dir: string;
|
|
597
|
+
runnerTemp: string;
|
|
598
|
+
repo: string;
|
|
599
|
+
trust: ApplyTrust;
|
|
600
|
+
env?: NodeJS.ProcessEnv;
|
|
601
|
+
}): ApplyHandoff {
|
|
602
|
+
const {dir, runnerTemp, repo, trust, env} = params;
|
|
603
|
+
if (!HEX_OID.test(trust.baseSha)) {
|
|
604
|
+
reject('history', 'the trusted baseSha is not a full commit id');
|
|
605
|
+
}
|
|
606
|
+
const handoff = readHandoff({
|
|
607
|
+
dir,
|
|
608
|
+
runnerTemp,
|
|
609
|
+
trust: {item: trust.item, rung: trust.rung},
|
|
610
|
+
});
|
|
611
|
+
const item = handoff.record.item;
|
|
612
|
+
const {mode, forcedPropose} = recomputeIntegrationMode({
|
|
613
|
+
repo,
|
|
614
|
+
baseSha: trust.baseSha,
|
|
615
|
+
item,
|
|
616
|
+
integrationMode: trust.integrationMode,
|
|
617
|
+
build: handoff.row.row === 'integrate-build',
|
|
618
|
+
env,
|
|
619
|
+
});
|
|
620
|
+
if (handoff.bundle === undefined) {
|
|
621
|
+
// `readHandoff` already refused `lfs/` without a bundle.
|
|
622
|
+
return {
|
|
623
|
+
handoff,
|
|
624
|
+
mode,
|
|
625
|
+
forcedPropose,
|
|
626
|
+
lfsObjects: [],
|
|
627
|
+
ledgerOutsideItem: [],
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const {type, slug} = resolveSidecarIdentity(item);
|
|
632
|
+
const workBranch = workBranchRef(type, slug);
|
|
633
|
+
const expectedRef = `refs/heads/${workBranch}`;
|
|
634
|
+
if (handoff.bundle.ref !== expectedRef) {
|
|
635
|
+
reject(
|
|
636
|
+
'bundle-refs',
|
|
637
|
+
`the bundle carries ${q(handoff.bundle.ref)}, not ${expectedRef}`,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// A fresh view of the arbiter's main (trusted; the apply job's own fetch).
|
|
642
|
+
git(
|
|
643
|
+
[
|
|
644
|
+
'fetch',
|
|
645
|
+
'--quiet',
|
|
646
|
+
'--no-tags',
|
|
647
|
+
trust.arbiter,
|
|
648
|
+
`+refs/heads/main:refs/remotes/${trust.arbiter}/main`,
|
|
649
|
+
],
|
|
650
|
+
repo,
|
|
651
|
+
{env},
|
|
652
|
+
);
|
|
653
|
+
const mainSha = git(
|
|
654
|
+
['rev-parse', `refs/remotes/${trust.arbiter}/main^{commit}`],
|
|
655
|
+
repo,
|
|
656
|
+
{env},
|
|
657
|
+
).trim();
|
|
658
|
+
for (const pre of handoff.bundle.prerequisites) {
|
|
659
|
+
if (
|
|
660
|
+
run('git', ['cat-file', '-e', `${pre}^{commit}`], repo, {env}).status !==
|
|
661
|
+
0
|
|
662
|
+
) {
|
|
663
|
+
reject(
|
|
664
|
+
'history',
|
|
665
|
+
`the bundle is based on ${pre}, a commit the arbiter does not have, ` +
|
|
666
|
+
`not on the base ${trust.baseSha}`,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const quarantine = mkdtempSync(join(runnerTemp, 'dorfl-quarantine-'));
|
|
672
|
+
let checked: ReturnType<typeof checkBundle>;
|
|
673
|
+
try {
|
|
674
|
+
const format = git(['rev-parse', '--show-object-format'], repo, {
|
|
675
|
+
env,
|
|
676
|
+
}).trim();
|
|
677
|
+
git(
|
|
678
|
+
['init', '--quiet', '--bare', `--object-format=${format}`, quarantine],
|
|
679
|
+
repo,
|
|
680
|
+
{env},
|
|
681
|
+
);
|
|
682
|
+
const objects = git(
|
|
683
|
+
['rev-parse', '--path-format=absolute', '--git-path', 'objects'],
|
|
684
|
+
repo,
|
|
685
|
+
{env},
|
|
686
|
+
).trim();
|
|
687
|
+
writeFileSync(
|
|
688
|
+
join(quarantine, 'objects', 'info', 'alternates'),
|
|
689
|
+
`${objects}\n`,
|
|
690
|
+
);
|
|
691
|
+
checked = checkBundle({
|
|
692
|
+
quarantine,
|
|
693
|
+
bundlePath: handoff.bundle.path,
|
|
694
|
+
workBranch,
|
|
695
|
+
baseSha: trust.baseSha,
|
|
696
|
+
mainSha,
|
|
697
|
+
ownItemPaths: ownItemPaths(item),
|
|
698
|
+
env,
|
|
699
|
+
});
|
|
700
|
+
} finally {
|
|
701
|
+
rmSync(quarantine, {recursive: true, force: true});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (mode === 'merge' && checked.ledgerOutside.length > 0) {
|
|
705
|
+
reject(
|
|
706
|
+
'ledger',
|
|
707
|
+
'in merge mode the work branch may change under work/ only its own ' +
|
|
708
|
+
'item, new notes and new ADRs, but it changes ' +
|
|
709
|
+
checked.ledgerOutside.slice(0, 20).map(q).join(', '),
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const lfsObjects = checkLfsObjects(checked.lfs, handoff.lfsObjects);
|
|
714
|
+
|
|
715
|
+
const imported = fetchBundle(repo, handoff.bundle.path, workBranch, env);
|
|
716
|
+
const landed =
|
|
717
|
+
imported.status === 0
|
|
718
|
+
? git(['rev-parse', INCOMING_TIP_REF], repo, {env}).trim()
|
|
719
|
+
: '';
|
|
720
|
+
if (landed !== checked.tip) {
|
|
721
|
+
run('git', ['update-ref', '-d', INCOMING_TIP_REF], repo, {env});
|
|
722
|
+
reject('bundle-format', 'the bundle changed between validation and import');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
handoff,
|
|
727
|
+
mode,
|
|
728
|
+
forcedPropose,
|
|
729
|
+
bundle: {workBranch, tip: checked.tip, newCommits: checked.newCommits},
|
|
730
|
+
lfsObjects,
|
|
731
|
+
ledgerOutsideItem: checked.ledgerOutside,
|
|
732
|
+
ledgerReport:
|
|
733
|
+
checked.ledgerOutside.length > 0
|
|
734
|
+
? renderLedgerReportSection(checked.ledgerOutside)
|
|
735
|
+
: undefined,
|
|
736
|
+
};
|
|
737
|
+
}
|