@intentius/chant 0.46.0 → 0.50.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/audit/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +30 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +9 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +12 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +13 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +20 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +137 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +6 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +41 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +140 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +18 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +49 -2
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +29 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +40 -4
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +13 -1
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.test.ts +41 -0
- package/src/build.ts +39 -6
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +268 -1
- package/src/cli/commands/audit.ts +87 -18
- package/src/cli/commands/build.test.ts +245 -0
- package/src/cli/commands/build.ts +210 -21
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +12 -3
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +96 -15
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +323 -3
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +14 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/auto-release.ts +6 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +13 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/composite.ts +31 -2
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +95 -6
- package/src/fold/subset.ts +66 -2
- package/src/index.ts +4 -0
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +151 -5
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +60 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
- package/src/lint/rules/index.ts +7 -1
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +198 -6
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +40 -3
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +212 -29
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/op.test.ts +25 -2
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/receipt-store.ts +211 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +51 -2
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +30 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
package/src/lifecycle/git.ts
CHANGED
|
@@ -29,95 +29,160 @@ const STATE_BRANCH = "chant/lifecycle";
|
|
|
29
29
|
* this module that pass a non-env value (like `_builds`) are relying on that
|
|
30
30
|
* generic behavior, not on any env-specific semantics.
|
|
31
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* Bounded retry budget for {@link writeBlobToPath}'s own internal CAS retry
|
|
34
|
+
* (#1959 finding 1). A conflict caused by some *other* env/file entry
|
|
35
|
+
* changing concurrently (the ordinary case — e.g. two operator ticks
|
|
36
|
+
* targeting different envs) is safe to resolve by simply rebuilding the tree
|
|
37
|
+
* against the new tip and retrying; this bounds how many times it does that
|
|
38
|
+
* before giving up and surfacing the conflict.
|
|
39
|
+
*/
|
|
40
|
+
const WRITE_BLOB_RETRY_ATTEMPTS = 5;
|
|
41
|
+
|
|
32
42
|
export async function writeBlobToPath(
|
|
33
43
|
environment: string,
|
|
34
44
|
filename: string,
|
|
35
45
|
content: string,
|
|
36
46
|
commitMessage: string,
|
|
37
|
-
opts?: { cwd?: string },
|
|
47
|
+
opts?: { cwd?: string; expectPriorPathSha?: string | null },
|
|
38
48
|
): Promise<string> {
|
|
39
49
|
const rt = getRuntime();
|
|
40
50
|
const cwd = opts?.cwd;
|
|
41
51
|
|
|
42
|
-
// 1. Write blob — hash-object reads from stdin
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
// 1. Write blob — hash-object reads from stdin. Fed directly via spawn's
|
|
53
|
+
// stdin (no shell involved), so content is written byte-for-byte: no `sh`
|
|
54
|
+
// `echo` reinterpreting backslash-escape sequences (e.g. `\n` inside JSON,
|
|
55
|
+
// as in a serialized kubectl.kubernetes.io/last-applied-configuration
|
|
56
|
+
// annotation) and no shell-quoting dance around embedded single quotes.
|
|
57
|
+
// Content-addressed and idempotent, so this stays outside the retry loop
|
|
58
|
+
// below — nothing about a CAS conflict on the ref ever invalidates it.
|
|
59
|
+
const blobResult = await rt.spawn(["git", "hash-object", "-w", "--stdin"], { cwd, stdin: content });
|
|
48
60
|
if (blobResult.exitCode !== 0) {
|
|
49
61
|
throw new Error(`git hash-object failed: ${blobResult.stderr}`);
|
|
50
62
|
}
|
|
51
63
|
const blobSha = blobResult.stdout.trim();
|
|
52
64
|
|
|
53
|
-
// 2. Read existing tree (if branch exists) to preserve other env/file entries
|
|
54
|
-
const existingTree = await readTree(cwd);
|
|
55
|
-
|
|
56
|
-
// 3. Build new tree entries
|
|
57
65
|
const path = `${environment}/${filename}`;
|
|
58
|
-
const entries = mergeTreeEntry(existingTree, path, blobSha);
|
|
59
|
-
|
|
60
|
-
// mktree needs a nested tree structure. Build env subtree first, then root tree.
|
|
61
|
-
// Build env subtree
|
|
62
|
-
const envEntries = entries
|
|
63
|
-
.filter((e) => e.env === environment)
|
|
64
|
-
.map((e) => `${e.mode} ${e.type} ${e.sha}\t${e.name}`)
|
|
65
|
-
.join("\n");
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
67
|
+
// 2-5. Read tree, build the commit, and CAS-update the branch ref — retried
|
|
68
|
+
// on a conflict (#1959 finding 1). `writeBlobToPath` had no retry of its
|
|
69
|
+
// own before this: a conflict from step 5's `updateRefCAS` simply threw,
|
|
70
|
+
// breaking every pre-existing caller (observation-baseline.ts,
|
|
71
|
+
// snapshot.ts, build-ledger-store.ts, and — via `appendReleaseRecordLine`
|
|
72
|
+
// below — release-ledger.ts) the moment any concurrent writer touched the
|
|
73
|
+
// orphan branch, e.g. a live `chant operator` ticking a different env.
|
|
74
|
+
//
|
|
75
|
+
// Safety of a *blind* retry (same `content`/`blobSha`, freshly rebuilt
|
|
76
|
+
// tree) hinges on whether *our own* target path (`environment/filename`)
|
|
77
|
+
// is what actually caused the conflict:
|
|
78
|
+
// - If some OTHER path changed (the ordinary multi-env/multi-file case),
|
|
79
|
+
// our data is unaffected — rebuilding the tree from the new tip and
|
|
80
|
+
// retrying the ref update is always correct, no matter what kind of
|
|
81
|
+
// write the caller is doing (replace or append).
|
|
82
|
+
// - If OUR OWN path changed concurrently, a blind retry using `content`
|
|
83
|
+
// computed before that race would silently clobber the other writer's
|
|
84
|
+
// change — safe only for a caller whose `content` is a self-contained
|
|
85
|
+
// replacement (writeObservationBaseline, writeSnapshot,
|
|
86
|
+
// persistBuildManifest), never for a read-modify-write caller whose
|
|
87
|
+
// `content` already embeds a stale read (an append). Since this
|
|
88
|
+
// function can't tell those apart, it does NOT blind-retry in that
|
|
89
|
+
// case — it re-throws immediately so a read-modify-write caller's own
|
|
90
|
+
// outer retry (appendConvergeRecord, appendGateResolution,
|
|
91
|
+
// appendReleaseRecordLine below) can re-read and recompute `content`
|
|
92
|
+
// fresh before trying again, exactly as they already do.
|
|
93
|
+
//
|
|
94
|
+
// - A read-modify-write caller must pass `expectPriorPathSha`. Without it
|
|
95
|
+
// attempt 1 has no prior sha to compare, so a commit landing between the
|
|
96
|
+
// caller's baseline read and this function's first tree read reads as the
|
|
97
|
+
// ambient starting state rather than a conflict, and is overwritten from
|
|
98
|
+
// the caller's stale content.
|
|
99
|
+
let lastErr: unknown;
|
|
100
|
+
let priorPathSha: string | null | undefined = opts?.expectPriorPathSha;
|
|
101
|
+
for (let attempt = 1; attempt <= WRITE_BLOB_RETRY_ATTEMPTS; attempt++) {
|
|
102
|
+
// 2. Read existing tree (if branch exists) to preserve other env/file
|
|
103
|
+
// entries. `tip` is the commit sha `entries` was read from, and must stay
|
|
104
|
+
// the commit parent and CAS oldValue below. Re-resolving the branch name
|
|
105
|
+
// there instead can observe a newer tip than `entries` reflects, which
|
|
106
|
+
// makes the CAS succeed against a tree built from a stale read.
|
|
107
|
+
const { tip, entries: existingTree } = await readTree(cwd);
|
|
108
|
+
const currentPathSha = existingTree.find((e) => e.env === environment && e.name === filename)?.sha ?? null;
|
|
109
|
+
|
|
110
|
+
if (priorPathSha !== undefined && currentPathSha !== priorPathSha) {
|
|
111
|
+
// Our own path moved since our previous attempt started — a genuine
|
|
112
|
+
// content-level race on the exact file we're writing, not just a CAS
|
|
113
|
+
// conflict from a sibling path. Not safe to paper over here.
|
|
114
|
+
throw lastErr ?? new RefCASConflictError(`refs/heads/${STATE_BRANCH}`, priorPathSha, "target path changed concurrently");
|
|
87
115
|
}
|
|
88
|
-
|
|
116
|
+
priorPathSha = currentPathSha;
|
|
89
117
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
{ cwd },
|
|
93
|
-
);
|
|
94
|
-
if (rootTreeResult.exitCode !== 0) {
|
|
95
|
-
throw new Error(`git mktree (root) failed: ${rootTreeResult.stderr}`);
|
|
96
|
-
}
|
|
97
|
-
const rootTreeSha = rootTreeResult.stdout.trim();
|
|
118
|
+
// 3. Build new tree entries
|
|
119
|
+
const entries = mergeTreeEntry(existingTree, path, blobSha);
|
|
98
120
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
106
|
-
if (commitResult.exitCode !== 0) {
|
|
107
|
-
throw new Error(`git commit-tree failed: ${commitResult.stderr}`);
|
|
108
|
-
}
|
|
109
|
-
const commitSha = commitResult.stdout.trim();
|
|
121
|
+
// mktree needs a nested tree structure. Build env subtree first, then root tree.
|
|
122
|
+
// Build env subtree
|
|
123
|
+
const envEntries = entries
|
|
124
|
+
.filter((e) => e.env === environment)
|
|
125
|
+
.map((e) => `${e.mode} ${e.type} ${e.sha}\t${e.name}`)
|
|
126
|
+
.join("\n");
|
|
110
127
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
128
|
+
const envTreeResult = await rt.spawn(["git", "mktree"], { cwd, stdin: `${envEntries}\n` });
|
|
129
|
+
if (envTreeResult.exitCode !== 0) {
|
|
130
|
+
throw new Error(`git mktree (env) failed: ${envTreeResult.stderr}`);
|
|
131
|
+
}
|
|
132
|
+
const envTreeSha = envTreeResult.stdout.trim();
|
|
133
|
+
|
|
134
|
+
// Build root tree: collect env subtrees
|
|
135
|
+
const rootEntries: string[] = [];
|
|
136
|
+
const envsSeen = new Set<string>();
|
|
137
|
+
for (const e of entries) {
|
|
138
|
+
if (!envsSeen.has(e.env)) {
|
|
139
|
+
envsSeen.add(e.env);
|
|
140
|
+
if (e.env === environment) {
|
|
141
|
+
rootEntries.push(`040000 tree ${envTreeSha}\t${environment}`);
|
|
142
|
+
} else {
|
|
143
|
+
rootEntries.push(`040000 tree ${e.envTreeSha!}\t${e.env}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
119
147
|
|
|
120
|
-
|
|
148
|
+
const rootTreeResult = await rt.spawn(["git", "mktree"], {
|
|
149
|
+
cwd,
|
|
150
|
+
stdin: `${rootEntries.join("\n")}\n`,
|
|
151
|
+
});
|
|
152
|
+
if (rootTreeResult.exitCode !== 0) {
|
|
153
|
+
throw new Error(`git mktree (root) failed: ${rootTreeResult.stderr}`);
|
|
154
|
+
}
|
|
155
|
+
const rootTreeSha = rootTreeResult.stdout.trim();
|
|
156
|
+
|
|
157
|
+
// 4. Create commit — parented on `tip`, the exact sha `existingTree` was
|
|
158
|
+
// read from (see the comment on step 2), not a fresh re-resolution of
|
|
159
|
+
// the branch name.
|
|
160
|
+
const parentRef = tip;
|
|
161
|
+
const parentArgs = parentRef ? ["-p", parentRef] : [];
|
|
162
|
+
const commitResult = await rt.spawn(
|
|
163
|
+
["git", "commit-tree", ...parentArgs, "-m", commitMessage, rootTreeSha],
|
|
164
|
+
{ cwd },
|
|
165
|
+
);
|
|
166
|
+
if (commitResult.exitCode !== 0) {
|
|
167
|
+
throw new Error(`git commit-tree failed: ${commitResult.stderr}`);
|
|
168
|
+
}
|
|
169
|
+
const commitSha = commitResult.stdout.trim();
|
|
170
|
+
|
|
171
|
+
// 5. Update ref — CAS-guarded against `parentRef` (#1485): closes the
|
|
172
|
+
// local race two concurrent writers used to hit silently (whoever called
|
|
173
|
+
// update-ref last simply overwrote the other's tree, no error). A
|
|
174
|
+
// conflict here throws RefCASConflictError instead of clobbering.
|
|
175
|
+
try {
|
|
176
|
+
await updateRefCAS(`refs/heads/${STATE_BRANCH}`, commitSha, parentRef, { cwd });
|
|
177
|
+
return commitSha;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (!(err instanceof RefCASConflictError)) throw err;
|
|
180
|
+
lastErr = err;
|
|
181
|
+
// Loop and rebuild against the new tip — see the safety analysis
|
|
182
|
+
// above; the top of the next iteration decides whether that's safe.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
throw lastErr;
|
|
121
186
|
}
|
|
122
187
|
|
|
123
188
|
/**
|
|
@@ -141,6 +206,27 @@ export async function readBlobFromPath(
|
|
|
141
206
|
return result.stdout;
|
|
142
207
|
}
|
|
143
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Read the blob SHA stored at `<environment>/<filename>` on the orphan branch,
|
|
211
|
+
* or `null` if absent. Sibling of `readBlobFromPath` returning the
|
|
212
|
+
* content-address rather than the content. A read-modify-write ledger append
|
|
213
|
+
* pairs this with {@link readBlobBySha} to pin its baseline read to an exact
|
|
214
|
+
* sha, then passes that sha as `writeBlobToPath`'s `expectPriorPathSha`.
|
|
215
|
+
*/
|
|
216
|
+
export async function readPathSha(
|
|
217
|
+
environment: string,
|
|
218
|
+
filename: string,
|
|
219
|
+
opts?: { cwd?: string },
|
|
220
|
+
): Promise<string | null> {
|
|
221
|
+
const rt = getRuntime();
|
|
222
|
+
const result = await rt.spawn(
|
|
223
|
+
["git", "rev-parse", "--verify", `${STATE_BRANCH}:${environment}/${filename}`],
|
|
224
|
+
{ cwd: opts?.cwd },
|
|
225
|
+
);
|
|
226
|
+
if (result.exitCode !== 0) return null;
|
|
227
|
+
return result.stdout.trim() || null;
|
|
228
|
+
}
|
|
229
|
+
|
|
144
230
|
/**
|
|
145
231
|
* Storage key for a snapshot on the orphan branch. Single-stack projects key by
|
|
146
232
|
* lexicon (`<env>/<lexicon>.json`, unchanged). A multi-stack project (see
|
|
@@ -216,6 +302,20 @@ export async function readSnapshotAt(
|
|
|
216
302
|
* Returns the new orphan-branch commit SHA — the caller still owns pushing
|
|
217
303
|
* via `pushLifecycle` under the same concurrent-write lease `writeSnapshot`
|
|
218
304
|
* uses.
|
|
305
|
+
*
|
|
306
|
+
* Retries the whole read-modify-append cycle on `RefCASConflictError`
|
|
307
|
+
* (#1959 finding 1), the same shape `appendConvergeRecord`
|
|
308
|
+
* (./converge-ledger.ts) and `appendGateResolution` (./gate-ledger.ts) use
|
|
309
|
+
* for their own append-only ledgers: `writeBlobToPath`'s own retry only
|
|
310
|
+
* absorbs a conflict caused by some *other* env/file changing — a conflict
|
|
311
|
+
* on this exact `releases.jsonl` (e.g. two deploys to the same env racing)
|
|
312
|
+
* needs `existing` re-read fresh so the appended line list is rebuilt onto
|
|
313
|
+
* whatever the other writer just committed, not silently dropped by
|
|
314
|
+
* retrying with a blob computed from a stale read.
|
|
315
|
+
*
|
|
316
|
+
* The baseline read must be `readPathSha` + `readBlobBySha` rather than
|
|
317
|
+
* `readBlobFromPath`, so the exact sha `existing` came from can be passed as
|
|
318
|
+
* `expectPriorPathSha`. See `writeBlobToPath` for the race that closes.
|
|
219
319
|
*/
|
|
220
320
|
export async function appendReleaseRecordLine(
|
|
221
321
|
environment: string,
|
|
@@ -223,9 +323,23 @@ export async function appendReleaseRecordLine(
|
|
|
223
323
|
opts?: { cwd?: string },
|
|
224
324
|
): Promise<string> {
|
|
225
325
|
const filename = "releases.jsonl";
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
326
|
+
|
|
327
|
+
let lastErr: unknown;
|
|
328
|
+
for (let attempt = 1; attempt <= WRITE_BLOB_RETRY_ATTEMPTS; attempt++) {
|
|
329
|
+
try {
|
|
330
|
+
const priorSha = await readPathSha(environment, filename, opts);
|
|
331
|
+
const existing = priorSha ? await readBlobBySha(priorSha, opts) : null;
|
|
332
|
+
const content = existing ? `${existing.replace(/\n$/, "")}\n${recordJson}` : recordJson;
|
|
333
|
+
return await writeBlobToPath(environment, filename, content, "Release record", {
|
|
334
|
+
...opts,
|
|
335
|
+
expectPriorPathSha: priorSha,
|
|
336
|
+
});
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (!(err instanceof RefCASConflictError)) throw err;
|
|
339
|
+
lastErr = err;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
throw lastErr;
|
|
229
343
|
}
|
|
230
344
|
|
|
231
345
|
/**
|
|
@@ -432,6 +546,251 @@ export async function fetchLifecycle(opts?: { cwd?: string }): Promise<boolean>
|
|
|
432
546
|
return fetchResult.exitCode === 0;
|
|
433
547
|
}
|
|
434
548
|
|
|
549
|
+
// ── Generic ref CAS (#1485) ──────────────────────────────────────────────────
|
|
550
|
+
//
|
|
551
|
+
// `writeBlobToPath`'s own `update-ref` call (above) had no compare-and-swap
|
|
552
|
+
// until this issue: two concurrent local writers could each build a tree from
|
|
553
|
+
// what they read as "current", and whichever called `update-ref` last simply
|
|
554
|
+
// overwrote the ref with no error — the other writer's change vanished
|
|
555
|
+
// silently. The primitives below give any caller (this module's own
|
|
556
|
+
// `writeBlobToPath`, and `./lease.ts`'s lease ref) a compare-and-swap guard
|
|
557
|
+
// building on what `git update-ref` already supports natively: pass the
|
|
558
|
+
// value you last observed as `<oldvalue>`, and the update only lands if the
|
|
559
|
+
// ref still points there. No lock file — `update-ref` itself is already an
|
|
560
|
+
// atomic local mutex (lockfile-then-rename under the hood), so this is
|
|
561
|
+
// exactly the "extend the ref-write helper" shape, not a second mechanism.
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Thrown by {@link updateRefCAS}/{@link deleteRefCAS} when `ref` no longer
|
|
565
|
+
* points at the `oldValue` the caller last observed — another writer moved
|
|
566
|
+
* it concurrently. Deliberately a distinct type from a generic git failure so
|
|
567
|
+
* callers (a lease acquire, a retried ledger append) can tell "I lost a race"
|
|
568
|
+
* from "git itself failed" and react differently to each.
|
|
569
|
+
*/
|
|
570
|
+
export class RefCASConflictError extends Error {
|
|
571
|
+
constructor(
|
|
572
|
+
public readonly ref: string,
|
|
573
|
+
public readonly expected: string | null,
|
|
574
|
+
stderr: string,
|
|
575
|
+
) {
|
|
576
|
+
super(
|
|
577
|
+
`ref "${ref}" moved concurrently — expected ${expected ?? "(ref must not exist)"}, ` +
|
|
578
|
+
`but another writer updated it first. git stderr: ${stderr.trim()}`,
|
|
579
|
+
);
|
|
580
|
+
this.name = "RefCASConflictError";
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Thrown by {@link updateRefCAS}/{@link deleteRefCAS} when the ref update
|
|
586
|
+
* failed because git found a stale `.lock` file already sitting next to the
|
|
587
|
+
* ref (#1959 finding 2) — what a `chant operator`/`chant approve`/etc.
|
|
588
|
+
* process leaves behind when it is killed (SIGKILL, OOM, `kill -9`) mid-write,
|
|
589
|
+
* *before* `git update-ref`'s own lockfile-then-rename completes. This is
|
|
590
|
+
* exactly the crash this feature must recover from, and it is NOT a CAS
|
|
591
|
+
* conflict: nobody else actually holds the ref (no other writer is racing,
|
|
592
|
+
* the previous one is simply dead), so it must never be misread as "someone
|
|
593
|
+
* else updated it first" — see `./lease.ts`'s `acquireLease`, which used to
|
|
594
|
+
* (before this fix) read this as "lease held by someone else" and quietly
|
|
595
|
+
* back off forever, since the dead process's lock file never goes away on
|
|
596
|
+
* its own.
|
|
597
|
+
*/
|
|
598
|
+
export class StaleLockError extends Error {
|
|
599
|
+
constructor(
|
|
600
|
+
public readonly ref: string,
|
|
601
|
+
public readonly lockPath: string,
|
|
602
|
+
stderr: string,
|
|
603
|
+
) {
|
|
604
|
+
super(
|
|
605
|
+
`ref "${ref}" has a stale lock file at ${lockPath} — a previous git process ` +
|
|
606
|
+
`(e.g. a killed \`chant operator\`) was interrupted mid-write and left it ` +
|
|
607
|
+
`behind. Fix: remove it (\`rm ${lockPath}\`) and retry. git stderr: ${stderr.trim()}`,
|
|
608
|
+
);
|
|
609
|
+
this.name = "StaleLockError";
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Matches git's "Unable to create '<path>.lock': File exists." — the one stable, version-independent phrase every `git update-ref`/`git commit` etc. failure due to a leftover lock file uses, regardless of which ref or which git version. */
|
|
614
|
+
const STALE_LOCK_RE = /Unable to create '([^']+)':\s*File exists/;
|
|
615
|
+
|
|
616
|
+
/** Read the SHA `ref` currently points to, or `null` if it doesn't exist. Works for any ref, not just the lifecycle branch. */
|
|
617
|
+
export async function readRefSha(ref: string, opts?: { cwd?: string }): Promise<string | null> {
|
|
618
|
+
const rt = getRuntime();
|
|
619
|
+
const result = await rt.spawn(["git", "rev-parse", "--verify", ref], { cwd: opts?.cwd });
|
|
620
|
+
if (result.exitCode !== 0) return null;
|
|
621
|
+
return result.stdout.trim() || null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Turn a failed `git update-ref`'s stderr into the right error type (#1959
|
|
626
|
+
* finding 2). Real git collapses several distinct failure modes into the
|
|
627
|
+
* same nonzero exit code — a genuine CAS mismatch, a stale lock file left by
|
|
628
|
+
* a killed process, and an outright bad ref name all just say "update_ref
|
|
629
|
+
* failed" — so this doesn't trust the exit code alone:
|
|
630
|
+
*
|
|
631
|
+
* 1. A stale `.lock` file has a stable, distinctive message (see
|
|
632
|
+
* `STALE_LOCK_RE`) — checked first and reported as its own
|
|
633
|
+
* {@link StaleLockError}, never as a conflict.
|
|
634
|
+
* 2. Otherwise, re-read the ref's actual current value and compare it to
|
|
635
|
+
* `oldValue`: if it genuinely differs, this is a real CAS conflict —
|
|
636
|
+
* {@link RefCASConflictError}. This is the authoritative check (not
|
|
637
|
+
* stderr text matching) so it holds across git versions/locales.
|
|
638
|
+
* 3. If the ref's actual value still matches `oldValue` — the CAS itself
|
|
639
|
+
* should have succeeded — the failure is something else entirely (a bad
|
|
640
|
+
* ref name, a permissions problem, disk full, ...) and is surfaced as a
|
|
641
|
+
* plain `Error`, not miscategorized as either of the above.
|
|
642
|
+
*/
|
|
643
|
+
async function classifyRefFailure(
|
|
644
|
+
ref: string,
|
|
645
|
+
oldValue: string | null,
|
|
646
|
+
stderr: string,
|
|
647
|
+
opts?: { cwd?: string },
|
|
648
|
+
): Promise<Error> {
|
|
649
|
+
const lockMatch = stderr.match(STALE_LOCK_RE);
|
|
650
|
+
if (lockMatch) {
|
|
651
|
+
return new StaleLockError(ref, lockMatch[1], stderr);
|
|
652
|
+
}
|
|
653
|
+
const actual = await readRefSha(ref, opts);
|
|
654
|
+
if (actual !== oldValue) {
|
|
655
|
+
return new RefCASConflictError(ref, oldValue, stderr);
|
|
656
|
+
}
|
|
657
|
+
return new Error(`git update-ref failed for ref "${ref}": ${stderr.trim()}`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Compare-and-swap update of an arbitrary ref. `oldValue` is the SHA the
|
|
662
|
+
* caller last observed the ref at, or `null` to assert the ref does not yet
|
|
663
|
+
* exist (git's own convention: an empty `<oldvalue>` argument to
|
|
664
|
+
* `update-ref` means "must not exist"). Throws {@link RefCASConflictError}
|
|
665
|
+
* when the ref genuinely moved since `oldValue` was read, {@link
|
|
666
|
+
* StaleLockError} when a leftover lock file from a killed process is
|
|
667
|
+
* blocking the write, or a plain `Error` for anything else — never silently
|
|
668
|
+
* overwrites, and never misclassifies one failure as another (#1959 finding
|
|
669
|
+
* 2; see {@link classifyRefFailure}).
|
|
670
|
+
*/
|
|
671
|
+
export async function updateRefCAS(
|
|
672
|
+
ref: string,
|
|
673
|
+
newValue: string,
|
|
674
|
+
oldValue: string | null,
|
|
675
|
+
opts?: { cwd?: string },
|
|
676
|
+
): Promise<void> {
|
|
677
|
+
const rt = getRuntime();
|
|
678
|
+
const result = await rt.spawn(["git", "update-ref", ref, newValue, oldValue ?? ""], { cwd: opts?.cwd });
|
|
679
|
+
if (result.exitCode !== 0) {
|
|
680
|
+
throw await classifyRefFailure(ref, oldValue, result.stderr ?? "", opts);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Compare-and-swap delete of an arbitrary ref — `oldValue` is required (no
|
|
686
|
+
* "delete unconditionally" escape hatch here) so releasing a lease you no
|
|
687
|
+
* longer hold can never delete someone else's newer one. Same failure
|
|
688
|
+
* classification as {@link updateRefCAS} (#1959 finding 2).
|
|
689
|
+
*/
|
|
690
|
+
export async function deleteRefCAS(ref: string, oldValue: string, opts?: { cwd?: string }): Promise<void> {
|
|
691
|
+
const rt = getRuntime();
|
|
692
|
+
const result = await rt.spawn(["git", "update-ref", "-d", ref, oldValue], { cwd: opts?.cwd });
|
|
693
|
+
if (result.exitCode !== 0) {
|
|
694
|
+
throw await classifyRefFailure(ref, oldValue, result.stderr ?? "", opts);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Write arbitrary content as a git blob object — no tree, no commit, no ref
|
|
700
|
+
* update. The building block a CAS ref's value can point at directly: a
|
|
701
|
+
* lease record (`./lease.ts`) has no meaningful "tree of files", so its ref
|
|
702
|
+
* targets a blob SHA rather than a commit the way `writeBlobToPath`'s tree-
|
|
703
|
+
* building pipeline does.
|
|
704
|
+
*/
|
|
705
|
+
export async function writeBlob(content: string, opts?: { cwd?: string }): Promise<string> {
|
|
706
|
+
const rt = getRuntime();
|
|
707
|
+
const result = await rt.spawn(["git", "hash-object", "-w", "--stdin"], { cwd: opts?.cwd, stdin: content });
|
|
708
|
+
if (result.exitCode !== 0) throw new Error(`git hash-object failed: ${result.stderr}`);
|
|
709
|
+
return result.stdout.trim();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Read a blob's raw content by its SHA (whatever object a ref points at directly). Returns `null` when the object doesn't exist locally. */
|
|
713
|
+
export async function readBlobBySha(sha: string, opts?: { cwd?: string }): Promise<string | null> {
|
|
714
|
+
const rt = getRuntime();
|
|
715
|
+
const result = await rt.spawn(["git", "cat-file", "blob", sha], { cwd: opts?.cwd });
|
|
716
|
+
if (result.exitCode !== 0) return null;
|
|
717
|
+
return result.stdout;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Push one arbitrary ref (e.g. a lease ref) to the remote, guarded the same
|
|
722
|
+
* way {@link pushLifecycle} guards the ledger branch: `--force-with-lease`
|
|
723
|
+
* keyed to the remote SHA last observed locally, so a concurrent push from a
|
|
724
|
+
* second machine is rejected rather than silently clobbered. Plain `--force`
|
|
725
|
+
* underneath that lease — a lease ref's value is a bare blob SHA, not a
|
|
726
|
+
* commit descending from the previous one, so there is no "fast-forward" to
|
|
727
|
+
* preserve, only the CAS the lease guard already provides.
|
|
728
|
+
*
|
|
729
|
+
* Returns `false` (never throws) when no remote is configured — a
|
|
730
|
+
* remote-less project's lease is local-only by construction (see
|
|
731
|
+
* `./lease.ts`'s module doc), or when the push itself is rejected (the
|
|
732
|
+
* caller re-reads and retries; see `acquireLease`).
|
|
733
|
+
*/
|
|
734
|
+
export async function pushRef(ref: string, opts?: { cwd?: string }): Promise<boolean> {
|
|
735
|
+
const rt = getRuntime();
|
|
736
|
+
const remoteResult = await rt.spawn(["git", "remote"], { cwd: opts?.cwd });
|
|
737
|
+
if (remoteResult.exitCode !== 0 || !remoteResult.stdout.trim()) return false;
|
|
738
|
+
const remote = remoteResult.stdout.trim().split("\n")[0];
|
|
739
|
+
|
|
740
|
+
const remoteRef = `refs/remotes/${remote}/${ref.replace(/^refs\//, "")}`;
|
|
741
|
+
const expectedResult = await rt.spawn(["git", "rev-parse", "--verify", remoteRef], { cwd: opts?.cwd });
|
|
742
|
+
const expected = expectedResult.exitCode === 0 ? expectedResult.stdout.trim() : null;
|
|
743
|
+
const lease = `${ref}:${expected ?? ""}`;
|
|
744
|
+
|
|
745
|
+
const pushResult = await rt.spawn(
|
|
746
|
+
["git", "push", "--force", `--force-with-lease=${lease}`, remote, `${ref}:${ref}`],
|
|
747
|
+
{ cwd: opts?.cwd },
|
|
748
|
+
);
|
|
749
|
+
return pushResult.exitCode === 0;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Fetch one arbitrary remote ref into a local ref of a possibly *different*
|
|
754
|
+
* name (#1959 finding 3). `+` forces the update even when it isn't a
|
|
755
|
+
* fast-forward (a lease ref's new value is rarely a descendant of its old
|
|
756
|
+
* one). Returns `false` (never throws) when no remote is configured.
|
|
757
|
+
*
|
|
758
|
+
* The `localRef !== remoteRef` shape exists so a read path can observe what
|
|
759
|
+
* the remote currently holds without ever touching a local ref another code
|
|
760
|
+
* path treats as CAS-authoritative — see {@link fetchRef}'s doc and
|
|
761
|
+
* `./lease.ts`'s `readLease`, which fetches into a side tracking ref
|
|
762
|
+
* (`refs/chant/lease-remote/<op>`) for exactly this reason.
|
|
763
|
+
*/
|
|
764
|
+
export async function fetchRefInto(
|
|
765
|
+
remoteRef: string,
|
|
766
|
+
localRef: string,
|
|
767
|
+
opts?: { cwd?: string },
|
|
768
|
+
): Promise<boolean> {
|
|
769
|
+
const rt = getRuntime();
|
|
770
|
+
const remoteResult = await rt.spawn(["git", "remote"], { cwd: opts?.cwd });
|
|
771
|
+
if (remoteResult.exitCode !== 0 || !remoteResult.stdout.trim()) return false;
|
|
772
|
+
const remote = remoteResult.stdout.trim().split("\n")[0];
|
|
773
|
+
const fetchResult = await rt.spawn(["git", "fetch", remote, `+${remoteRef}:${localRef}`], { cwd: opts?.cwd });
|
|
774
|
+
return fetchResult.exitCode === 0;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Fetch one arbitrary ref from remote into the same local ref name.
|
|
779
|
+
*
|
|
780
|
+
* **Caution for a read path (#1959 finding 3):** this force-overwrites
|
|
781
|
+
* `ref` locally (`+ref:ref`) — safe for a ref only a CAS write path ever
|
|
782
|
+
* mutates locally between fetches, but NOT safe to call from a plain read
|
|
783
|
+
* before every read if some other local writer (in the same clone) might be
|
|
784
|
+
* mid-write: fetching here would force the local ref back to whatever the
|
|
785
|
+
* remote last had, clobbering a just-written, not-yet-pushed local value out
|
|
786
|
+
* from under it. `./lease.ts`'s `readLease` used to do exactly that; it now
|
|
787
|
+
* uses {@link fetchRefInto} against a side tracking ref instead. Prefer
|
|
788
|
+
* `fetchRefInto` for any new read-before-decide path.
|
|
789
|
+
*/
|
|
790
|
+
export async function fetchRef(ref: string, opts?: { cwd?: string }): Promise<boolean> {
|
|
791
|
+
return fetchRefInto(ref, ref, opts);
|
|
792
|
+
}
|
|
793
|
+
|
|
435
794
|
/**
|
|
436
795
|
* Get the current HEAD commit SHA of the main working branch.
|
|
437
796
|
*/
|
|
@@ -465,17 +824,25 @@ async function getStateBranchTip(cwd?: string): Promise<string | null> {
|
|
|
465
824
|
return result.stdout.trim();
|
|
466
825
|
}
|
|
467
826
|
|
|
468
|
-
|
|
827
|
+
/**
|
|
828
|
+
* Read the orphan branch's tip and every env/file tree entry under it as one
|
|
829
|
+
* consistent snapshot. Every listing must stay pinned to the resolved `tip`
|
|
830
|
+
* sha, never to `STATE_BRANCH`: the read spans several `ls-tree` calls (root
|
|
831
|
+
* plus one per env subtree) and a branch name re-resolves on each, so a
|
|
832
|
+
* concurrent commit splices entries from two commits into one array
|
|
833
|
+
* undetectably. Callers need the returned `tip` as their commit parent.
|
|
834
|
+
*/
|
|
835
|
+
async function readTree(cwd?: string): Promise<{ tip: string | null; entries: TreeEntry[] }> {
|
|
469
836
|
const rt = getRuntime();
|
|
470
837
|
const tip = await getStateBranchTip(cwd);
|
|
471
|
-
if (!tip) return [];
|
|
838
|
+
if (!tip) return { tip: null, entries: [] };
|
|
472
839
|
|
|
473
|
-
// List root tree to get env directories
|
|
840
|
+
// List root tree to get env directories — pinned to `tip`, not `STATE_BRANCH`.
|
|
474
841
|
const rootResult = await rt.spawn(
|
|
475
|
-
["git", "ls-tree",
|
|
842
|
+
["git", "ls-tree", tip],
|
|
476
843
|
{ cwd },
|
|
477
844
|
);
|
|
478
|
-
if (rootResult.exitCode !== 0) return [];
|
|
845
|
+
if (rootResult.exitCode !== 0) return { tip, entries: [] };
|
|
479
846
|
|
|
480
847
|
const entries: TreeEntry[] = [];
|
|
481
848
|
const lines = rootResult.stdout.trim().split("\n").filter(Boolean);
|
|
@@ -487,9 +854,9 @@ async function readTree(cwd?: string): Promise<TreeEntry[]> {
|
|
|
487
854
|
const [, mode, type, sha, name] = match;
|
|
488
855
|
|
|
489
856
|
if (type === "tree") {
|
|
490
|
-
// This is an env directory — list its contents
|
|
857
|
+
// This is an env directory — list its contents, still pinned to `tip`.
|
|
491
858
|
const envResult = await rt.spawn(
|
|
492
|
-
["git", "ls-tree", `${
|
|
859
|
+
["git", "ls-tree", `${tip}:${name}/`],
|
|
493
860
|
{ cwd },
|
|
494
861
|
);
|
|
495
862
|
if (envResult.exitCode !== 0) continue;
|
|
@@ -510,7 +877,7 @@ async function readTree(cwd?: string): Promise<TreeEntry[]> {
|
|
|
510
877
|
}
|
|
511
878
|
}
|
|
512
879
|
|
|
513
|
-
return entries;
|
|
880
|
+
return { tip, entries };
|
|
514
881
|
}
|
|
515
882
|
|
|
516
883
|
function mergeTreeEntry(
|
|
@@ -531,8 +898,3 @@ function mergeTreeEntry(
|
|
|
531
898
|
});
|
|
532
899
|
return entries;
|
|
533
900
|
}
|
|
534
|
-
|
|
535
|
-
function shellQuoteLines(input: string): string {
|
|
536
|
-
// Escape for printf in shell
|
|
537
|
-
return `'${input.replace(/'/g, "'\\''")}'`;
|
|
538
|
-
}
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ export * from "./deep-diff";
|
|
|
7
7
|
export * from "./deep-observe";
|
|
8
8
|
export * from "./observation-baseline";
|
|
9
9
|
export * from "./change-set";
|
|
10
|
+
export * from "./unobserved-gate";
|
|
11
|
+
export * from "./receipt-plan";
|
|
10
12
|
export * from "./affected";
|
|
11
13
|
export * from "./release-ledger";
|
|
12
14
|
export * from "./build-ledger";
|
|
@@ -14,3 +16,7 @@ export * from "./build-ledger-store";
|
|
|
14
16
|
export * from "./oras-referrer-lookup";
|
|
15
17
|
export * from "./status";
|
|
16
18
|
export * from "./teardown";
|
|
19
|
+
export * from "./symptoms";
|
|
20
|
+
export * from "./converge-ledger";
|
|
21
|
+
export * from "./scenario";
|
|
22
|
+
export * from "./scenario-eval";
|