@kungfu-tech/buildchain 4.1.3-alpha.0 → 4.1.3-alpha.2
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/architecture/action-taxonomy.json +16 -0
- package/architecture/agent-change-map.md +245 -4
- package/architecture/ci-lane-change-budget.json +289 -0
- package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
- package/architecture/decisions/0006-business-attempt-journal.md +89 -0
- package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
- package/architecture/decisions/0008-pipeline-product-publication.md +99 -0
- package/architecture/internal-capabilities.json +340 -4
- package/architecture/maintainability-debt.json +3 -1
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +7558 -0
- package/architecture/release-topology.json +248 -11
- package/architecture/universal-workflow-bootstrap.json +10 -0
- package/architecture/universal-workflow-capability-policy.json +6 -6
- package/architecture/workflow-taxonomy.json +55 -0
- package/contracts/release-discussion-v1.schema.json +3 -0
- package/dist/readers/business-attempt.cjs +805 -0
- package/dist/readers/release-discussion.cjs +4 -0
- package/dist/site/buildchain-contract.json +7 -5
- package/dist/site/buildchain-site.json +25 -15
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/kfd-claims.json +311 -10
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +9 -9
- package/dist/site/page-registry.json +20 -10
- package/dist/site/public-surface-audit.json +201 -7
- package/dist/site/publication-authority-registry.json +121 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +416 -7
- package/docs/node-api-reference.md +12 -12
- package/docs/release-discussions.md +44 -5
- package/docs/runtime-entry.md +45 -5
- package/docs/workflow-catalog.md +5 -0
- package/package.json +4 -3
- package/packages/core/consumer/contract/entries.js +85 -0
- package/packages/core/consumer/contract/examples.js +96 -0
- package/packages/core/consumer/contract/identity.js +65 -0
- package/packages/core/consumer/contract/inspection.js +45 -0
- package/packages/core/consumer/contract/plan.js +99 -0
- package/packages/core/consumer/contract/products.js +101 -0
- package/packages/core/consumer/contract/shape.js +46 -0
- package/packages/core/dev-delivery/candidate/admission.js +9 -5
- package/packages/core/dev-delivery/candidate/reservation.js +11 -0
- package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
- package/packages/core/dev-delivery/native/actions.js +7 -1
- package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
- package/packages/core/dev-delivery/native/heartbeat.js +6 -4
- package/packages/core/dev-delivery/queue/landing-action.js +5 -0
- package/packages/core/governance/buildchain-publication-authority.js +5 -0
- package/packages/core/paper/operations/bootstrap.js +2 -0
- package/packages/core/paper/paper-npm-bootstrap.js +2 -2
- package/packages/core/providers/github/attempt-index.js +60 -0
- package/packages/core/providers/github/attempt-journal.js +154 -0
- package/packages/core/providers/github/discussions/materials.js +23 -7
- package/packages/core/providers/github/discussions/transport.js +16 -5
- package/packages/core/providers/github/pipeline-checkout.js +49 -0
- package/packages/core/providers/github/pipeline-events.js +52 -0
- package/packages/core/providers/github/pipeline-integration.js +95 -0
- package/packages/core/providers/github/pipeline-policy.js +213 -0
- package/packages/core/providers/github/pipeline-product-payload.js +50 -0
- package/packages/core/providers/github/pipeline-product-release.js +191 -0
- package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
- package/packages/core/providers/github/pipeline-run-entry.js +86 -0
- package/packages/core/providers/github/pipeline-runs.js +132 -0
- package/packages/core/providers/github/pipeline-source.js +178 -0
- package/packages/core/providers/github/pipeline-version.js +201 -0
- package/packages/core/providers/github/pipeline-worker.js +108 -0
- package/packages/core/publication/candidate/registry-hydration.js +2 -2
- package/packages/core/publication/npm/pack-preview.js +2 -2
- package/packages/core/publication/npm/pack-result.js +31 -0
- package/packages/core/publication/npm/package.js +3 -6
- package/packages/core/publication/npm/pipeline-channel.js +87 -0
- package/packages/core/publication/npm/pipeline-provider.js +109 -0
- package/packages/core/publication/npm/registry.js +12 -9
- package/packages/core/publication/pipeline/actions.js +100 -0
- package/packages/core/publication/pipeline/apply.js +182 -0
- package/packages/core/publication/pipeline/build-segments.js +107 -0
- package/packages/core/publication/pipeline/capsules.js +89 -0
- package/packages/core/publication/pipeline/context.js +39 -0
- package/packages/core/publication/pipeline/development-anchor.js +91 -0
- package/packages/core/publication/pipeline/development-current.js +63 -0
- package/packages/core/publication/pipeline/development-pr.js +85 -0
- package/packages/core/publication/pipeline/development-proof.js +76 -0
- package/packages/core/publication/pipeline/development-transition.js +112 -0
- package/packages/core/publication/pipeline/distribution.js +124 -0
- package/packages/core/publication/pipeline/documents.js +156 -0
- package/packages/core/publication/pipeline/effects.js +162 -0
- package/packages/core/publication/pipeline/files.js +66 -0
- package/packages/core/publication/pipeline/imported-materials.js +32 -0
- package/packages/core/publication/pipeline/journal.js +110 -0
- package/packages/core/publication/pipeline/next-development.js +116 -0
- package/packages/core/publication/pipeline/pack.js +151 -0
- package/packages/core/publication/pipeline/package-policy.js +22 -0
- package/packages/core/publication/pipeline/plan.js +114 -0
- package/packages/core/publication/pipeline/prepare.js +151 -0
- package/packages/core/publication/pipeline/qualification.js +170 -0
- package/packages/core/publication/pipeline/qualify.js +104 -0
- package/packages/core/publication/pipeline/recovery-admission.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
- package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
- package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
- package/packages/core/publication/pipeline/recovery-import.js +80 -0
- package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
- package/packages/core/publication/pipeline/recovery-materials.js +104 -0
- package/packages/core/publication/pipeline/recovery-plan.js +46 -0
- package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
- package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
- package/packages/core/publication/pipeline/recovery-readback.js +56 -0
- package/packages/core/publication/pipeline/recovery-signing.js +58 -0
- package/packages/core/publication/pipeline/sealed-products.js +79 -0
- package/packages/core/publication/pipeline/settle.js +43 -0
- package/packages/core/publication/pipeline/signing.js +140 -0
- package/packages/core/publication/pipeline/source-plan.js +83 -0
- package/packages/core/publication/pipeline/version.js +81 -0
- package/packages/core/publication/publication-reproducibility.js +2 -2
- package/packages/core/release/discussion/binary.js +11 -0
- package/packages/core/release/discussion/checkpoints.js +45 -4
- package/packages/core/release/discussion/envelope.js +1 -0
- package/packages/core/release/discussion/evidence.js +51 -0
- package/packages/core/release/discussion/presentation.js +92 -0
- package/packages/core/release/discussion/publication.js +3 -1
- package/packages/core/release/discussion/qualification.js +1 -0
- package/packages/core/release/discussion/reader.js +9 -0
- package/packages/core/release/discussion/session.js +2 -0
- package/packages/core/release/discussion/store.js +28 -31
- package/packages/core/release/discussion/threads.js +83 -0
- package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
- package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
- package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
- package/packages/core/runtime/buildchain-domain.wasm +0 -0
- package/packages/core/runtime/domain-wasm-artifact.js +2 -2
- package/packages/core/runtime/entry/actions.js +27 -25
- package/packages/core/runtime/entry/attempt.js +25 -0
- package/packages/core/runtime/entry/selection.js +14 -0
- package/packages/core/runtime/entry/source.js +62 -0
- package/packages/core/workflow/attempt/identity.js +165 -0
- package/packages/core/workflow/attempt/journal.js +87 -0
- package/packages/core/workflow/attempt/materials.js +72 -0
- package/packages/core/workflow/attempt/reader-entry.js +12 -0
- package/packages/core/workflow/attempt/reader.js +149 -0
- package/packages/core/workflow/attempt/records.js +153 -0
- package/packages/core/workflow/attempt/store.js +75 -0
- package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
- package/packages/core/workflow/pipeline/action-output.js +11 -0
- package/packages/core/workflow/pipeline/actions.js +62 -0
- package/packages/core/workflow/pipeline/build-control.js +140 -0
- package/packages/core/workflow/pipeline/build-evidence.js +92 -0
- package/packages/core/workflow/pipeline/build-qualification.js +145 -0
- package/packages/core/workflow/pipeline/build-result.js +69 -0
- package/packages/core/workflow/pipeline/build.js +95 -0
- package/packages/core/workflow/pipeline/cancellation.js +169 -0
- package/packages/core/workflow/pipeline/channel-control.js +127 -0
- package/packages/core/workflow/pipeline/controller.js +133 -0
- package/packages/core/workflow/pipeline/delivery-control.js +204 -0
- package/packages/core/workflow/pipeline/delivery-observation.js +118 -0
- package/packages/core/workflow/pipeline/delivery-request.js +198 -0
- package/packages/core/workflow/pipeline/events.js +84 -0
- package/packages/core/workflow/pipeline/fence.js +42 -0
- package/packages/core/workflow/pipeline/group-control.js +56 -0
- package/packages/core/workflow/pipeline/guard-build.js +37 -0
- package/packages/core/workflow/pipeline/guard.js +156 -0
- package/packages/core/workflow/pipeline/host.js +114 -0
- package/packages/core/workflow/pipeline/materials.js +58 -0
- package/packages/core/workflow/pipeline/notifications.js +36 -0
- package/packages/core/workflow/pipeline/parent-notification.js +27 -0
- package/packages/core/workflow/pipeline/platforms.js +21 -0
- package/packages/core/workflow/pipeline/progress.js +108 -0
- package/packages/core/workflow/pipeline/projection.js +20 -0
- package/packages/core/workflow/pipeline/reconcile.js +194 -0
- package/packages/core/workflow/pipeline/recovery-action.js +30 -0
- package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
- package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
- package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
- package/packages/core/workflow/pipeline/recovery-build.js +121 -0
- package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
- package/packages/core/workflow/pipeline/recovery-integration.js +66 -0
- package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
- package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
- package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
- package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
- package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
- package/packages/core/workflow/pipeline/recovery-session.js +129 -0
- package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
- package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
- package/packages/core/workflow/pipeline/runtime-source.js +111 -0
- package/packages/core/workflow/pipeline/selection.js +106 -0
- package/packages/core/workflow/pipeline/session.js +72 -0
- package/packages/core/workflow/pipeline/settlement.js +137 -0
- package/packages/core/workflow/pipeline/wake.js +38 -0
- package/packages/core/workflow/pipeline/web-status.js +108 -0
- package/scripts/build-release-discussion-reader.mjs +11 -7
- package/scripts/check-pipeline-publication-topology.mjs +36 -0
- package/scripts/check-release-topology.mjs +6 -1
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/verify-golden-path.mjs +5 -1
- package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/binary/package.json +6 -0
- package/templates/minimal-consumer/binary/src/build.mjs +5 -0
- package/templates/minimal-consumer/binary/src/hello.c +2 -0
- package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
- package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/npm/package.json +6 -0
- package/templates/minimal-consumer/npm/src/build.mjs +5 -0
- package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
- package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/paper/package.json +6 -0
- package/templates/minimal-consumer/paper/src/build.mjs +12 -0
- package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { object, relativePath, text } from "./shape.js";
|
|
3
|
+
import { compileConsumerPlan, CONSUMER_CONTRACT } from "./plan.js";
|
|
4
|
+
|
|
5
|
+
export function contentDigest(bytes) {
|
|
6
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Source identities are prepared from trusted GitHub readback, never caller inputs.
|
|
10
|
+
export function bindConsumerSource(source, configBytes) {
|
|
11
|
+
object(
|
|
12
|
+
source,
|
|
13
|
+
["repository", "commit", "tree", "configPath", "configBlob"],
|
|
14
|
+
[],
|
|
15
|
+
"source",
|
|
16
|
+
);
|
|
17
|
+
text(
|
|
18
|
+
source.repository,
|
|
19
|
+
"source.repository",
|
|
20
|
+
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u,
|
|
21
|
+
);
|
|
22
|
+
for (const key of ["commit", "tree", "configBlob"])
|
|
23
|
+
text(source[key], `source.${key}`, /^[0-9a-f]{40}$/u);
|
|
24
|
+
relativePath(source.configPath, "source.configPath");
|
|
25
|
+
const bytes = Buffer.from(configBytes);
|
|
26
|
+
const blob = createHash("sha1")
|
|
27
|
+
.update(`blob ${bytes.length}\0`)
|
|
28
|
+
.update(bytes)
|
|
29
|
+
.digest("hex");
|
|
30
|
+
if (blob !== source.configBlob)
|
|
31
|
+
throw new Error("source.configBlob: config bytes do not match Git blob");
|
|
32
|
+
const plan = compileConsumerPlan(bytes.toString("utf8"));
|
|
33
|
+
const identity = {
|
|
34
|
+
schema: "buildchain.consumer-source/v1",
|
|
35
|
+
repository: source.repository,
|
|
36
|
+
commit: source.commit,
|
|
37
|
+
tree: source.tree,
|
|
38
|
+
configPath: source.configPath,
|
|
39
|
+
configBlob: blob,
|
|
40
|
+
configDigest: contentDigest(bytes),
|
|
41
|
+
contract: CONSUMER_CONTRACT,
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
identity,
|
|
45
|
+
identityDigest: contentDigest(JSON.stringify(identity)),
|
|
46
|
+
plan,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function consumerContractLock({ entry, runtime, configDigest }) {
|
|
51
|
+
for (const [name, value] of Object.entries({ entry, runtime })) {
|
|
52
|
+
object(value, ["repository", "sha"], [], name);
|
|
53
|
+
if (value.repository !== "kungfu-systems/buildchain")
|
|
54
|
+
throw new Error(`${name}: untrusted Buildchain repository`);
|
|
55
|
+
text(value.sha, `${name}.sha`, /^[0-9a-f]{40}$/u);
|
|
56
|
+
}
|
|
57
|
+
text(configDigest, "configDigest", /^sha256:[0-9a-f]{64}$/u);
|
|
58
|
+
return {
|
|
59
|
+
schema: "buildchain.consumer-contract-lock/v2",
|
|
60
|
+
contract: CONSUMER_CONTRACT,
|
|
61
|
+
entry: { ...entry },
|
|
62
|
+
runtime: { ...runtime },
|
|
63
|
+
configDigest,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { consumerWorkflows } from "./entries.js";
|
|
2
|
+
import { compileConsumerPlan, CONFIG_PATH } from "./plan.js";
|
|
3
|
+
|
|
4
|
+
const INTERNAL_WIRING = [
|
|
5
|
+
/\b(?:request-json|native-roots-json|used-nonces-json|warrant-result|source-proof|integration-proof)\b/iu,
|
|
6
|
+
/\bbuildchain\s+(?:dev\s+(?:warrant|proof)|release|promote|recover|publish)\b/iu,
|
|
7
|
+
/\b(?:npm\s+publish|gh\s+release\s+(?:create|upload)|gh\s+run\s+download)\b/iu,
|
|
8
|
+
/actions\/download-artifact@/u,
|
|
9
|
+
/\.buildchain\/runtime\/|packages\/core\/(?:publication|release|dev-delivery)\//u,
|
|
10
|
+
/\b(?:fencingToken|candidateRoot|sourceProofRoot|usedNonces|generationRoot|compareAndSwap)\b/u,
|
|
11
|
+
/buildchain[-./][\w-]*(?:warrant|proof|fence|nonce|candidate|authority|transaction)/iu,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// The supplied file map must be the complete tracked consumer tree, including
|
|
15
|
+
// package scripts and the source files they invoke. Runtime source is never run.
|
|
16
|
+
export function inspectConsumerContract(
|
|
17
|
+
files,
|
|
18
|
+
{ channel = "v4", configPath = CONFIG_PATH } = {},
|
|
19
|
+
) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
let plan;
|
|
22
|
+
try {
|
|
23
|
+
plan = compileConsumerPlan(files[configPath]);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
issues.push(`${configPath}: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
const workflows = consumerWorkflows(channel, configPath);
|
|
28
|
+
for (const [file, expected] of Object.entries(workflows))
|
|
29
|
+
if (files[file] !== expected)
|
|
30
|
+
issues.push(`${file}: must match the generated thin caller`);
|
|
31
|
+
for (const [file, source] of Object.entries(files)) {
|
|
32
|
+
if (
|
|
33
|
+
/^\.github\/workflows\/.*\.ya?ml$/u.test(file) &&
|
|
34
|
+
!Object.hasOwn(workflows, file)
|
|
35
|
+
)
|
|
36
|
+
issues.push(`${file}: extra consumer workflow is forbidden`);
|
|
37
|
+
if (!/\.(?:toml|json|ya?ml|[cm]?js|ts|py|sh|ps1|rb)$/u.test(file)) continue;
|
|
38
|
+
for (const pattern of INTERNAL_WIRING)
|
|
39
|
+
if (pattern.test(String(source)))
|
|
40
|
+
issues.push(
|
|
41
|
+
`${file}: consumer owns internal orchestration (${pattern.source})`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return { ok: issues.length === 0, issues, plan };
|
|
45
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { parse } from "smol-toml";
|
|
2
|
+
import { choice, list, object, relativePath, text, unique } from "./shape.js";
|
|
3
|
+
import { compileProducts } from "./products.js";
|
|
4
|
+
|
|
5
|
+
export const CONSUMER_CONTRACT = "buildchain.consumer-contract/v2";
|
|
6
|
+
export const CONSUMER_PLAN = "buildchain.consumer-plan/v1";
|
|
7
|
+
export const CONFIG_PATH = ".buildchain/buildchain.toml";
|
|
8
|
+
|
|
9
|
+
function compileVersion(value) {
|
|
10
|
+
object(value, ["strategy", "files"], ["derived_files"], "version");
|
|
11
|
+
choice(value.strategy, ["semver", "anchored"], "version.strategy");
|
|
12
|
+
list(value.files, "version.files", (item, field) => {
|
|
13
|
+
object(item, ["path", "format", "key"], [], field);
|
|
14
|
+
relativePath(item.path, `${field}.path`);
|
|
15
|
+
choice(item.format, ["json", "toml"], `${field}.format`);
|
|
16
|
+
text(item.key, `${field}.key`, /^[A-Za-z0-9_.-]+$/u);
|
|
17
|
+
});
|
|
18
|
+
unique(
|
|
19
|
+
value.files.map((item) => `${item.path}:${item.key}`),
|
|
20
|
+
"version.files",
|
|
21
|
+
);
|
|
22
|
+
if (value.derived_files !== undefined)
|
|
23
|
+
list(value.derived_files, "version.derived_files", relativePath);
|
|
24
|
+
return structuredClone(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function compileChannels(values) {
|
|
28
|
+
const channels = list(values, "channels", (value, field) => {
|
|
29
|
+
object(value, ["from", "to", "operation"], [], field);
|
|
30
|
+
text(
|
|
31
|
+
value.from,
|
|
32
|
+
`${field}.from`,
|
|
33
|
+
/^(?:feature|dev|alpha|release)\/[A-Za-z0-9/*._-]+$/u,
|
|
34
|
+
);
|
|
35
|
+
text(
|
|
36
|
+
value.to,
|
|
37
|
+
`${field}.to`,
|
|
38
|
+
/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u,
|
|
39
|
+
);
|
|
40
|
+
choice(
|
|
41
|
+
value.operation,
|
|
42
|
+
["develop", "alpha", "stable", "major"],
|
|
43
|
+
`${field}.operation`,
|
|
44
|
+
);
|
|
45
|
+
const source = value.from.split("/")[0],
|
|
46
|
+
target = value.to.split("/")[0];
|
|
47
|
+
const legal = {
|
|
48
|
+
develop: source === "feature" && target === "dev",
|
|
49
|
+
alpha: source === "dev" && target === "alpha",
|
|
50
|
+
stable: source === "alpha" && target === "release",
|
|
51
|
+
major: source === "release" && value.to === "publish-gate/major",
|
|
52
|
+
};
|
|
53
|
+
if (
|
|
54
|
+
!legal[value.operation] ||
|
|
55
|
+
value.from.includes("..") ||
|
|
56
|
+
value.to.includes("..")
|
|
57
|
+
)
|
|
58
|
+
throw new Error(`${field}: unlawful channel route`);
|
|
59
|
+
return { ...value };
|
|
60
|
+
});
|
|
61
|
+
unique(
|
|
62
|
+
channels.map((value) => `${value.from}:${value.to}`),
|
|
63
|
+
"channels",
|
|
64
|
+
);
|
|
65
|
+
return channels;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function compileReview(value) {
|
|
69
|
+
object(
|
|
70
|
+
value,
|
|
71
|
+
["minimum_approvals", "code_owners", "merge_queue"],
|
|
72
|
+
[],
|
|
73
|
+
"review",
|
|
74
|
+
);
|
|
75
|
+
if (!Number.isInteger(value.minimum_approvals) || value.minimum_approvals < 1)
|
|
76
|
+
throw new Error(
|
|
77
|
+
"review.minimum_approvals: independent approval is required",
|
|
78
|
+
);
|
|
79
|
+
if (value.code_owners !== true || value.merge_queue !== true)
|
|
80
|
+
throw new Error(
|
|
81
|
+
"review: code owners and protected merge queue are required",
|
|
82
|
+
);
|
|
83
|
+
return { ...value };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// This compiler receives bytes. It has no filesystem, process, network or provider port.
|
|
87
|
+
export function compileConsumerPlan(source) {
|
|
88
|
+
const config = parse(source);
|
|
89
|
+
object(config, ["schema", "products", "version", "channels", "review"]);
|
|
90
|
+
choice(config.schema, [2], "schema");
|
|
91
|
+
return {
|
|
92
|
+
schema: CONSUMER_PLAN,
|
|
93
|
+
contract: CONSUMER_CONTRACT,
|
|
94
|
+
products: compileProducts(config.products),
|
|
95
|
+
version: compileVersion(config.version),
|
|
96
|
+
channels: compileChannels(config.channels),
|
|
97
|
+
review: compileReview(config.review),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
choice,
|
|
3
|
+
list,
|
|
4
|
+
object,
|
|
5
|
+
relativePath,
|
|
6
|
+
slug,
|
|
7
|
+
text,
|
|
8
|
+
unique,
|
|
9
|
+
} from "./shape.js";
|
|
10
|
+
|
|
11
|
+
export const PRODUCT_TYPES = ["npm", "binary", "paper"];
|
|
12
|
+
export const PLATFORMS = [
|
|
13
|
+
"linux-x64",
|
|
14
|
+
"linux-arm64",
|
|
15
|
+
"macos-arm64",
|
|
16
|
+
"macos-x64",
|
|
17
|
+
"windows-x64",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function artifact(value, location) {
|
|
21
|
+
object(value, ["id", "path", "kind"], [], location);
|
|
22
|
+
slug(value.id, `${location}.id`);
|
|
23
|
+
relativePath(value.path, `${location}.path`);
|
|
24
|
+
choice(value.kind, ["npm-package", "archive", "pdf"], `${location}.kind`);
|
|
25
|
+
return { ...value };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function target(value, location) {
|
|
29
|
+
object(value, ["provider", "artifacts"], ["access"], location);
|
|
30
|
+
choice(value.provider, ["npm", "github-release"], `${location}.provider`);
|
|
31
|
+
list(value.artifacts, `${location}.artifacts`, slug);
|
|
32
|
+
unique(value.artifacts, `${location}.artifacts`);
|
|
33
|
+
if (value.provider === "npm")
|
|
34
|
+
choice(value.access, ["public", "restricted"], `${location}.access`);
|
|
35
|
+
else if (Object.hasOwn(value, "access"))
|
|
36
|
+
throw new Error(`${location}.access: npm only`);
|
|
37
|
+
return structuredClone(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function compileProducts(values) {
|
|
41
|
+
const products = list(values, "products", (value, location) => {
|
|
42
|
+
object(
|
|
43
|
+
value,
|
|
44
|
+
["id", "type", "platforms", "build", "verify", "artifacts", "targets"],
|
|
45
|
+
["install", "directory"],
|
|
46
|
+
location,
|
|
47
|
+
);
|
|
48
|
+
slug(value.id, `${location}.id`);
|
|
49
|
+
choice(value.type, PRODUCT_TYPES, `${location}.type`);
|
|
50
|
+
list(value.platforms, `${location}.platforms`, (item, field) =>
|
|
51
|
+
choice(item, PLATFORMS, field),
|
|
52
|
+
);
|
|
53
|
+
unique(value.platforms, `${location}.platforms`);
|
|
54
|
+
for (const operation of [
|
|
55
|
+
"build",
|
|
56
|
+
"verify",
|
|
57
|
+
...(value.install !== undefined ? ["install"] : []),
|
|
58
|
+
])
|
|
59
|
+
list(value[operation], `${location}.${operation}`, text);
|
|
60
|
+
if (value.directory !== undefined)
|
|
61
|
+
relativePath(value.directory, `${location}.directory`);
|
|
62
|
+
const artifacts = list(value.artifacts, `${location}.artifacts`, artifact);
|
|
63
|
+
unique(
|
|
64
|
+
artifacts.map((item) => item.id),
|
|
65
|
+
`${location}.artifacts`,
|
|
66
|
+
);
|
|
67
|
+
const targets = list(value.targets, `${location}.targets`, target);
|
|
68
|
+
unique(
|
|
69
|
+
targets.map((item) => item.provider),
|
|
70
|
+
`${location}.targets`,
|
|
71
|
+
);
|
|
72
|
+
const expectedKind = {
|
|
73
|
+
npm: "npm-package",
|
|
74
|
+
binary: "archive",
|
|
75
|
+
paper: "pdf",
|
|
76
|
+
}[value.type];
|
|
77
|
+
if (artifacts.some((item) => item.kind !== expectedKind))
|
|
78
|
+
throw new Error(`${location}: artifact kind does not match product type`);
|
|
79
|
+
const published = targets.flatMap((item) => item.artifacts);
|
|
80
|
+
if (
|
|
81
|
+
artifacts.some((item) => !published.includes(item.id)) ||
|
|
82
|
+
published.some((id) => !artifacts.some((item) => item.id === id))
|
|
83
|
+
)
|
|
84
|
+
throw new Error(
|
|
85
|
+
`${location}: publication targets must cover exactly declared artifacts`,
|
|
86
|
+
);
|
|
87
|
+
if (
|
|
88
|
+
targets.some((item) => item.provider === "npm") !==
|
|
89
|
+
(value.type === "npm")
|
|
90
|
+
)
|
|
91
|
+
throw new Error(
|
|
92
|
+
`${location}: npm products require an npm target; other products cannot publish to npm`,
|
|
93
|
+
);
|
|
94
|
+
return structuredClone({ ...value, artifacts, targets });
|
|
95
|
+
});
|
|
96
|
+
unique(
|
|
97
|
+
products.map((item) => item.id),
|
|
98
|
+
"products",
|
|
99
|
+
);
|
|
100
|
+
return products;
|
|
101
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Closed data validation shared by TOML, event identities and managed locks.
|
|
2
|
+
export function object(value, required, optional = [], location = "config") {
|
|
3
|
+
if (!value || Object.getPrototypeOf(value) !== Object.prototype)
|
|
4
|
+
throw new Error(`${location}: expected a table`);
|
|
5
|
+
for (const key of Object.keys(value))
|
|
6
|
+
if (![...required, ...optional].includes(key))
|
|
7
|
+
throw new Error(`${location}.${key}: unknown field`);
|
|
8
|
+
for (const key of required)
|
|
9
|
+
if (!Object.hasOwn(value, key))
|
|
10
|
+
throw new Error(`${location}.${key}: required field`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function text(value, location, pattern = /\S/u) {
|
|
15
|
+
if (typeof value !== "string" || !pattern.test(value) || value.includes("\0"))
|
|
16
|
+
throw new Error(`${location}: invalid string`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function choice(value, choices, location) {
|
|
21
|
+
if (!choices.includes(value))
|
|
22
|
+
throw new Error(`${location}: expected ${choices.join(" or ")}`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function list(value, location, validate) {
|
|
27
|
+
if (!Array.isArray(value) || !value.length)
|
|
28
|
+
throw new Error(`${location}: expected a nonempty list`);
|
|
29
|
+
return value.map((item, index) => validate(item, `${location}[${index}]`));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function unique(values, location) {
|
|
33
|
+
if (new Set(values).size !== values.length)
|
|
34
|
+
throw new Error(`${location}: duplicate identity`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function relativePath(value, location) {
|
|
38
|
+
text(value, location, /^[A-Za-z0-9_.][A-Za-z0-9_./*-]*$/u);
|
|
39
|
+
if (value.split("/").some((part) => !part || part === ".." || part === "."))
|
|
40
|
+
throw new Error(`${location}: expected a repository-relative path`);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function slug(value, location) {
|
|
45
|
+
return text(value, location, /^[a-z][a-z0-9-]*$/u);
|
|
46
|
+
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
-
sourceCoordinates,
|
|
6
|
-
validateNativeContract,
|
|
7
|
-
} from "./coordinates.js";
|
|
4
|
+
import { sourceCoordinates, validateNativeContract } from "./coordinates.js";
|
|
8
5
|
import {
|
|
9
6
|
verifyProjectCutReplayProof,
|
|
10
7
|
createSourceQualificationProof,
|
|
@@ -16,6 +13,7 @@ import { assertBranchUnlocked } from "../../providers/dev-delivery/protection.js
|
|
|
16
13
|
import { deriveSourcePaths } from "./source-paths.js";
|
|
17
14
|
import { admissionPolicyRequest } from "../admission/request.js";
|
|
18
15
|
import { runAdmissionTransaction } from "../admission/transaction.js";
|
|
16
|
+
import { guardPipelineAdmission } from "../../workflow/pipeline/guard.js";
|
|
19
17
|
export function admitDeliveryRequest(input, defaultBranch) {
|
|
20
18
|
const target = sourceCoordinates({
|
|
21
19
|
branch: input["target-branch"],
|
|
@@ -46,6 +44,7 @@ export async function qualifyDeliverySource(
|
|
|
46
44
|
},
|
|
47
45
|
dependencies = {},
|
|
48
46
|
) {
|
|
47
|
+
const pipeline = await guardPipelineAdmission(request, { repository, token });
|
|
49
48
|
const outputs = {
|
|
50
49
|
"runtime-sha": runtimeSha,
|
|
51
50
|
"qualify-outcome": "skipped",
|
|
@@ -146,7 +145,12 @@ export async function qualifyDeliverySource(
|
|
|
146
145
|
if (!affectedPaths.length)
|
|
147
146
|
affectedPaths = await (dependencies.derivePaths || deriveSourcePaths)(
|
|
148
147
|
source,
|
|
149
|
-
{
|
|
148
|
+
{
|
|
149
|
+
token,
|
|
150
|
+
apiUrl,
|
|
151
|
+
environment,
|
|
152
|
+
qualifiedBase: pipeline?.history.at(-1).generation.baseCommit,
|
|
153
|
+
},
|
|
150
154
|
);
|
|
151
155
|
const receiptRoot = qualification.result.receiptRoot;
|
|
152
156
|
if (!/^sha256:[0-9a-f]{64}$/u.test(receiptRoot || ""))
|
|
@@ -10,6 +10,8 @@ import { dispatchDeliveryHandoff } from "../warrant/handoff.js";
|
|
|
10
10
|
import { resolveCandidateAffectedPaths } from "./evidence.js";
|
|
11
11
|
import { deliverySubmissionRequest } from "./submission.js";
|
|
12
12
|
import { jsonList } from "../warrant/values.js";
|
|
13
|
+
import { guardPipelineAdmission } from "../../workflow/pipeline/guard.js";
|
|
14
|
+
import { wakePipelineCandidate } from "../../workflow/pipeline/wake.js";
|
|
13
15
|
export async function reserveDeliveryCandidate(
|
|
14
16
|
{
|
|
15
17
|
workspace,
|
|
@@ -31,6 +33,7 @@ export async function reserveDeliveryCandidate(
|
|
|
31
33
|
);
|
|
32
34
|
if (!predecessorsOk || proofOutcome !== "success")
|
|
33
35
|
throw new Error("Delivery candidate submission failed");
|
|
36
|
+
await guardPipelineAdmission(input, connection);
|
|
34
37
|
const service =
|
|
35
38
|
dependencies.service ||
|
|
36
39
|
createDeliveryWarrantService({ ...connection, branch });
|
|
@@ -77,6 +80,14 @@ export async function reserveDeliveryCandidate(
|
|
|
77
80
|
warrant.pullRequestNumber !== Number(input["expected-pr-number"]) ||
|
|
78
81
|
warrant.sourceHead !== input["expected-head-sha"]
|
|
79
82
|
) {
|
|
83
|
+
if (input["pipeline-attempt"]) {
|
|
84
|
+
await wakePipelineCandidate(warrant, { ...connection, branch });
|
|
85
|
+
return {
|
|
86
|
+
"warrant-outcome": "success",
|
|
87
|
+
"warrant-handoff-required": "true",
|
|
88
|
+
"warrant-handoff-dispatched": "true",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
80
91
|
outputs = await dispatchDeliveryHandoff(
|
|
81
92
|
{
|
|
82
93
|
warrant,
|
|
@@ -52,7 +52,7 @@ export function pathsAtQualifiedSource(directory, qualifiedBase, source) {
|
|
|
52
52
|
|
|
53
53
|
export async function deriveSourcePaths(
|
|
54
54
|
source,
|
|
55
|
-
{ token, apiUrl, environment = process.env },
|
|
55
|
+
{ token, apiUrl, environment = process.env, qualifiedBase },
|
|
56
56
|
provider = new GitHubTwoPhaseClient({
|
|
57
57
|
repository: source.repository,
|
|
58
58
|
token,
|
|
@@ -65,10 +65,18 @@ export async function deriveSourcePaths(
|
|
|
65
65
|
/^[1-9]\d*$/u.test(String(source.sourceWorkflowRunId || "")),
|
|
66
66
|
"Exact source run coordinates are required",
|
|
67
67
|
);
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if (qualifiedBase === undefined) {
|
|
69
|
+
const run = await provider.request(
|
|
70
|
+
`/repos/${source.repository}/actions/runs/${source.sourceWorkflowRunId}`,
|
|
71
|
+
);
|
|
72
|
+
qualifiedBase = qualifiedRunBase(run, source);
|
|
73
|
+
}
|
|
74
|
+
// The internal pipeline guard already requalifies the source run and immutable
|
|
75
|
+
// generation. Zero-delta recovery still verifies the full source identity below.
|
|
76
|
+
requireValue(
|
|
77
|
+
/^[0-9a-f]{40}$/u.test(qualifiedBase || ""),
|
|
78
|
+
"Exact qualified source base is required",
|
|
70
79
|
);
|
|
71
|
-
const qualifiedBase = qualifiedRunBase(run, source);
|
|
72
80
|
const directory = fs.mkdtempSync(
|
|
73
81
|
path.join(os.tmpdir(), "buildchain-source-paths-"),
|
|
74
82
|
);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
qualifyTransferredNative,
|
|
6
6
|
} from "./transactions.js";
|
|
7
7
|
import { admitProviderFinalizer } from "./finalizer-admission.js";
|
|
8
|
+
import { guardPipelineAdmission } from "../../workflow/pipeline/guard.js";
|
|
8
9
|
function request(core, env) {
|
|
9
10
|
const candidate = JSON.parse(
|
|
10
11
|
core.getInput("request-json", { required: true }),
|
|
@@ -41,11 +42,16 @@ export async function executeDeliveryNativeAction(core, env) {
|
|
|
41
42
|
}
|
|
42
43
|
export async function qualifyTransferredNativeAction(core, env) {
|
|
43
44
|
const context = deliveryActionContext(core, env);
|
|
45
|
+
const input = request(core, env);
|
|
46
|
+
await guardPipelineAdmission(input.request, {
|
|
47
|
+
repository: env.GITHUB_REPOSITORY,
|
|
48
|
+
token: core.getInput("token", { required: true }),
|
|
49
|
+
});
|
|
44
50
|
outputs(
|
|
45
51
|
core,
|
|
46
52
|
await qualifyTransferredNative({
|
|
47
53
|
...context,
|
|
48
|
-
candidate:
|
|
54
|
+
candidate: input.candidate,
|
|
49
55
|
sourceProofRoot: core.getInput("source-proof-root", { required: true }),
|
|
50
56
|
token: core.getInput("token", { required: true }),
|
|
51
57
|
apiUrl: env.GITHUB_API_URL || "https://api.github.com",
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { deliveryActionContext } from "./action-context.js";
|
|
4
4
|
import { heartbeatDeliveryAttempt } from "./heartbeat.js";
|
|
5
|
+
import { guardPipelineDelivery } from "../../workflow/pipeline/guard.js";
|
|
5
6
|
export async function heartbeatDeliveryAttemptAction(core, env) {
|
|
6
7
|
const { workspace, run } = deliveryActionContext(core, env);
|
|
7
8
|
const admission = JSON.parse(
|
|
@@ -20,6 +21,21 @@ export async function heartbeatDeliveryAttemptAction(core, env) {
|
|
|
20
21
|
apiUrl: env.GITHUB_API_URL || "https://api.github.com",
|
|
21
22
|
leaseSeconds: Number(core.getInput("lease-seconds")),
|
|
22
23
|
heartbeatSeconds: Number(core.getInput("heartbeat-seconds")),
|
|
24
|
+
beforeHeartbeat: () => {
|
|
25
|
+
const warrant = admission.observation?.activeWarrant || admission.warrant;
|
|
26
|
+
return guardPipelineDelivery(
|
|
27
|
+
{
|
|
28
|
+
"pipeline-attempt": core.getInput("pipeline-attempt"),
|
|
29
|
+
"target-branch": core.getInput("branch", { required: true }),
|
|
30
|
+
"expected-pr-number": warrant?.pullRequestNumber,
|
|
31
|
+
"expected-head-sha": warrant?.sourceHead,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
repository: env.GITHUB_REPOSITORY,
|
|
35
|
+
token: core.getInput("token", { required: true }),
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
},
|
|
23
39
|
});
|
|
24
40
|
fs.writeFileSync(
|
|
25
41
|
path.join(workspace, ".buildchain/provider-heartbeat-receipt.json"),
|
|
@@ -68,13 +68,14 @@ export async function heartbeatDeliveryAttempt(options) {
|
|
|
68
68
|
heartbeatSeconds: options.heartbeatSeconds,
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
|
-
heartbeat: ({
|
|
71
|
+
heartbeat: async ({
|
|
72
72
|
expectedOldStateRoot,
|
|
73
73
|
fencingToken,
|
|
74
74
|
leaseGeneration,
|
|
75
75
|
leaseSeconds,
|
|
76
|
-
}) =>
|
|
77
|
-
|
|
76
|
+
}) => {
|
|
77
|
+
await options.beforeHeartbeat?.();
|
|
78
|
+
return runDevDeliveryCommand({
|
|
78
79
|
command: "heartbeat",
|
|
79
80
|
repository: options.repository,
|
|
80
81
|
branch: options.branch,
|
|
@@ -85,7 +86,8 @@ export async function heartbeatDeliveryAttempt(options) {
|
|
|
85
86
|
execute: true,
|
|
86
87
|
token: options.token,
|
|
87
88
|
apiUrl: options.apiUrl,
|
|
88
|
-
})
|
|
89
|
+
});
|
|
90
|
+
},
|
|
89
91
|
readJobs: () =>
|
|
90
92
|
githubJson(
|
|
91
93
|
options.apiUrl,
|
|
@@ -3,9 +3,14 @@ import { deliveryActionContext } from "../native/action-context.js";
|
|
|
3
3
|
import { admissionPolicyRequest } from "../admission/request.js";
|
|
4
4
|
import { runAdmissionTransaction } from "../admission/transaction.js";
|
|
5
5
|
import { enforceLanding } from "./completion.js";
|
|
6
|
+
import { guardPipelineAdmission } from "../../workflow/pipeline/guard.js";
|
|
6
7
|
export async function admitLandingAction(core, env) {
|
|
7
8
|
const context = deliveryActionContext(core, env);
|
|
8
9
|
const request = JSON.parse(core.getInput("request-json", { required: true }));
|
|
10
|
+
await guardPipelineAdmission(request, {
|
|
11
|
+
repository: env.GITHUB_REPOSITORY,
|
|
12
|
+
token: core.getInput("token", { required: true }),
|
|
13
|
+
});
|
|
9
14
|
const result = await runAdmissionTransaction(
|
|
10
15
|
{
|
|
11
16
|
...admissionPolicyRequest(request, {
|
|
@@ -39,6 +39,11 @@ const DESCRIPTORS = Object.freeze([
|
|
|
39
39
|
[".github/workflows/public-ops-warrant-cancel.yml", "governance-write"],
|
|
40
40
|
[".github/workflows/self-ops-merge-queue.yml", "governance-write"],
|
|
41
41
|
[".github/workflows/public-ops-dev-auto-merge.yml", "governance-write"],
|
|
42
|
+
[".github/workflows/public-ops-pipeline.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "fixed", ".github/workflows/.release-pipeline-products.yml"],
|
|
43
|
+
[".github/workflows/public-ops-recover.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "fixed", ".github/workflows/.release-pipeline-products.yml"],
|
|
44
|
+
[".github/workflows/.ops-pipeline-execute.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "fixed", ".github/workflows/.release-pipeline-products.yml"],
|
|
45
|
+
[".github/workflows/.release-pipeline-products.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "fixed"],
|
|
46
|
+
[".github/workflows/.ops-pipeline-delivery.yml", "governance-write"],
|
|
42
47
|
[".github/workflows/self-ops-housekeeping-daily.yml", "governance-write"],
|
|
43
48
|
[".github/workflows/self-ops-housekeeping-monthly.yml", "governance-write"],
|
|
44
49
|
[".github/workflows/self-ops-housekeeping-weekly.yml", "governance-write"],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readNpmPackResult } from "../../publication/npm/pack-result.js";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import { jsonText, extractUrls, safeParseJson, toPosix } from "./files.js";
|
|
@@ -66,6 +67,7 @@ export function executePaperNpmBootstrap(options = {}) {
|
|
|
66
67
|
path,
|
|
67
68
|
resolvePaperRepository,
|
|
68
69
|
safeParseJson,
|
|
70
|
+
readNpmPackResult,
|
|
69
71
|
toPosix,
|
|
70
72
|
validatePaperProvisioningAuthority,
|
|
71
73
|
writePaperReceipt,
|
|
@@ -20,6 +20,7 @@ function executePaperNpmBootstrap(options = {}, runtime) {
|
|
|
20
20
|
path,
|
|
21
21
|
resolvePaperRepository,
|
|
22
22
|
safeParseJson,
|
|
23
|
+
readNpmPackResult,
|
|
23
24
|
toPosix,
|
|
24
25
|
validatePaperProvisioningAuthority,
|
|
25
26
|
writePaperReceipt,
|
|
@@ -208,8 +209,7 @@ function executePaperNpmBootstrap(options = {}, runtime) {
|
|
|
208
209
|
pack: {
|
|
209
210
|
status: pack.ok ? "pass" : "fail",
|
|
210
211
|
entryCount: (() => {
|
|
211
|
-
const
|
|
212
|
-
const value = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
212
|
+
const value = pack.ok ? readNpmPackResult(pack.stdout) : null;
|
|
213
213
|
return Array.isArray(value?.files) ? value.files.length : 0;
|
|
214
214
|
})(),
|
|
215
215
|
},
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Immutable lookup pointers are derived from admitted journal commits. The
|
|
2
|
+
// canonical intent journal must contain the selected root before a pointer can
|
|
3
|
+
// resolve; an orphan commit or pointer cannot create attempt authority.
|
|
4
|
+
export function githubAttemptIndex(request, journal, repository) {
|
|
5
|
+
if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
|
|
6
|
+
throw new Error("Invalid attempt lookup repository");
|
|
7
|
+
const base = `/repos/${repository}`;
|
|
8
|
+
function ref(attempt) {
|
|
9
|
+
if (!/^attempt-[0-9a-f]{64}$/u.test(attempt || ""))
|
|
10
|
+
throw new Error("Invalid business attempt selector");
|
|
11
|
+
return `buildchain/attempt-index/${attempt}`;
|
|
12
|
+
}
|
|
13
|
+
function root(snapshot, attempt) {
|
|
14
|
+
const roots = snapshot.records.filter(
|
|
15
|
+
(record) => record.attempt === attempt && record.node === "attempt",
|
|
16
|
+
);
|
|
17
|
+
if (roots.length !== 1)
|
|
18
|
+
throw new Error("Lookup does not identify one admitted attempt root");
|
|
19
|
+
return roots[0];
|
|
20
|
+
}
|
|
21
|
+
async function resolve(attempt) {
|
|
22
|
+
const pointer = await request(`${base}/git/ref/heads/${ref(attempt)}`, {
|
|
23
|
+
allow404: true,
|
|
24
|
+
});
|
|
25
|
+
if (!pointer) throw new Error("Business attempt lookup is not retained");
|
|
26
|
+
const indexed = await journal.readCommit(repository, pointer.object?.sha);
|
|
27
|
+
const selected = root(indexed.snapshot, attempt);
|
|
28
|
+
const current = await journal.read(indexed.snapshot.intent);
|
|
29
|
+
if (!current || root(current.snapshot, attempt).id !== selected.id)
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Lookup attempt is absent from the authoritative journal",
|
|
32
|
+
);
|
|
33
|
+
return { ...current, selectedAttempt: attempt };
|
|
34
|
+
}
|
|
35
|
+
async function retain(intent, attempt) {
|
|
36
|
+
if (intent.repository !== repository)
|
|
37
|
+
throw new Error("Attempt lookup repository drift");
|
|
38
|
+
const current = await journal.read(intent);
|
|
39
|
+
if (!current) throw new Error("Cannot index an unadmitted attempt");
|
|
40
|
+
const expected = root(current.snapshot, attempt);
|
|
41
|
+
let failure;
|
|
42
|
+
try {
|
|
43
|
+
await request(`${base}/git/refs`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: { ref: `refs/heads/${ref(attempt)}`, sha: current.commit },
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
failure = error;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const result = await resolve(attempt);
|
|
52
|
+
if (root(result.snapshot, attempt).id !== expected.id)
|
|
53
|
+
throw new Error("Conflicting business attempt lookup");
|
|
54
|
+
return result;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw failure || error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { retain, resolve };
|
|
60
|
+
}
|