@kungfu-tech/buildchain 4.1.3-alpha.9 → 4.1.4-alpha.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/AGENTS.md +11 -13
- package/CONTRIBUTING.md +32 -9
- package/README.md +56 -140
- package/actions/release/promotion/ref/README.md +1 -1
- package/architecture/action-taxonomy.json +1 -1
- package/architecture/agent-change-map.md +32 -45
- package/architecture/build-orchestration.json +1 -1
- package/architecture/capability-state-machine-manifest.json +5 -5
- package/architecture/ci-lane-change-budget.json +92 -92
- package/architecture/code-layout.json +2 -12
- package/architecture/core-mechanisms.json +38 -39
- package/architecture/cross-platform-adopter-qualification.json +1 -1
- package/architecture/dev-delivery-orchestration.json +2 -2
- package/architecture/floating-consumer-policy.json +3 -3
- package/architecture/internal-capabilities.json +27 -28
- package/architecture/maintainability-debt.json +60 -94
- package/architecture/maintainability-policy.json +7 -13
- package/architecture/minimal-consumer-migration.json +43 -5014
- package/architecture/platform-stage-checkpoints.json +1 -1
- package/architecture/release-tail-contract.json +2 -2
- package/architecture/release-topology.json +33 -25
- package/architecture/stage-capsule-qualification.json +1 -1
- package/architecture/universal-workflow-bootstrap.json +60 -60
- package/architecture/universal-workflow-capability-policy.json +3 -3
- package/architecture/universal-workflow-fault-campaign.json +7 -7
- package/architecture/workflow-taxonomy.json +59 -60
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/dist/site/buildchain-contract.json +138 -66
- package/dist/site/buildchain-site.json +257 -331
- package/dist/site/capability-registry.json +4 -4
- package/dist/site/cli-registry.json +6 -142
- package/dist/site/controller-registry.json +19 -19
- package/dist/site/kfd-claims.json +481 -529
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +22 -22
- package/dist/site/node-api-registry.json +82 -339
- package/dist/site/page-registry.json +229 -304
- package/dist/site/public-surface-audit.json +544 -754
- package/dist/site/publication-authority-registry.json +316 -316
- package/dist/site/publication-registry.json +1 -1
- package/dist/site/site-manifest.json +23 -23
- package/dist/site/workflow-registry.json +1071 -1027
- package/docs/MAP.md +28 -20
- package/docs/auditable-demo.md +17 -49
- package/docs/bootstrap-recovery.md +46 -37
- package/docs/cli-reference.md +2 -46
- package/docs/cli.md +97 -186
- package/docs/code-organization.md +7 -5
- package/docs/dev-alpha-candidate-patrol.md +19 -42
- package/docs/dev-delivery-orchestration.md +15 -11
- package/docs/dev-delivery-qualification-landing-adr.md +10 -9
- package/docs/dev-delivery-warrant.md +17 -16
- package/docs/dev-qualification-patrol.md +25 -20
- package/docs/getting-started.md +80 -139
- package/docs/github-artifact-attestation.md +31 -81
- package/docs/install.md +66 -30
- package/docs/lifecycle-protocol.md +19 -57
- package/docs/migration-inventory.md +17 -45
- package/docs/next-development-transition.md +13 -13
- package/docs/node-api-reference.md +122 -138
- package/docs/oci-publication.md +13 -10
- package/docs/publication-artifacts.md +128 -486
- package/docs/publication-authority.md +15 -45
- package/docs/publish-transaction.md +13 -11
- package/docs/release-candidate.md +126 -355
- package/docs/release-discussions.md +36 -67
- package/docs/release-flow.md +80 -339
- package/docs/release-governance.md +114 -962
- package/docs/release-promotion-request.md +45 -43
- package/docs/release-propagation.md +13 -27
- package/docs/release-tail-provider-plane.md +38 -65
- package/docs/reusable-build-surface.md +78 -196
- package/docs/runtime-train-validation.md +8 -6
- package/docs/stable-candidate-patrol.md +13 -66
- package/docs/v4-adopter-delivery.md +15 -15
- package/docs/v4-declarative-release-tail-migration.md +19 -17
- package/docs/v4-production-release.md +11 -6
- package/docs/v4-publication-rehearsal.md +12 -27
- package/docs/v4-stage-capsule.md +14 -20
- package/docs/v4-tail-reseal.md +34 -20
- package/docs/web-surface-deployments.md +26 -264
- package/docs/workflow-catalog.md +31 -31
- package/docs/workflow-path-migration.md +10 -4
- package/package.json +5 -4
- package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
- package/packages/core/adoption/cli/output.mjs +1 -9
- package/packages/core/adoption/cli/product-gate.mjs +1 -15
- package/packages/core/adoption/cli/project.mjs +9 -10
- package/packages/core/adoption/cli/standards.mjs +1 -14
- package/packages/core/adoption/cli/support.mjs +0 -2
- package/packages/core/adoption/cli/surface-register.mjs +0 -2
- package/packages/core/adoption/commands/init-repo.mjs +77 -485
- package/packages/core/adoption/consumer-init.js +279 -0
- package/packages/core/adoption/qualification/admission.js +1 -1
- package/packages/core/build/cli/cache.mjs +0 -2
- package/packages/core/build/cli/facts.mjs +0 -3
- package/packages/core/build/cli/homebrew.mjs +0 -4
- package/packages/core/build/cli/lifecycle.mjs +0 -3
- package/packages/core/build/github-artifact-attestation.js +2 -2
- package/packages/core/build/platform-stage-checkpoints.js +1 -1
- package/packages/core/build/signing/dispatch.js +1 -1
- package/packages/core/consumer/buildchain-config.js +9 -0
- package/packages/core/consumer/contract/inspection.js +351 -9
- package/packages/core/consumer/contract/javascript-syntax.js +439 -0
- package/packages/core/consumer/contract/javascript-values.js +61 -0
- package/packages/core/consumer/contract/javascript.js +555 -0
- package/packages/core/consumer/contract/local-validation.js +84 -0
- package/packages/core/contracts/buildchain-contract.js +39 -8
- package/packages/core/contracts/cli/introspection.mjs +1 -9
- package/packages/core/contracts/public-surface-cli.js +0 -1
- package/packages/core/dev-delivery/dev-delivery-contract-surface.js +2 -2
- package/packages/core/governance/buildchain-publication-authority.js +24 -24
- package/packages/core/governance/cli/doctor.mjs +58 -42
- package/packages/core/governance/commands/architecture.mjs +6 -1
- package/packages/core/governance/commands/audit-publication-control-plane.mjs +1 -1
- package/packages/core/governance/commands/workflow-friction-report.mjs +1 -1
- package/packages/core/governance/github-governance-authority.js +2 -2
- package/packages/core/governance/publication/collection.js +1 -1
- package/packages/core/index.js +1 -3
- package/packages/core/observability/cli/candidate.mjs +0 -2
- package/packages/core/observability/cli/context.mjs +0 -5
- package/packages/core/observability/cli/diagnostics.mjs +0 -7
- package/packages/core/observability/cli/events.mjs +2 -10
- package/packages/core/paper/commands/paper-work-fleet-cli.mjs +1 -1
- package/packages/core/paper/commands/paper.mjs +33 -422
- package/packages/core/paper/consumer-status.js +72 -0
- package/packages/core/paper/operations/identity.js +0 -3
- package/packages/core/paper/operations/migration.js +460 -0
- package/packages/core/paper/operations/preflight-checks.js +8 -68
- package/packages/core/paper/operations/scaffold.js +16 -326
- package/packages/core/paper/operations/status.js +27 -53
- package/packages/core/paper/paper-agent-entry.js +36 -147
- package/packages/core/paper/paper-fleet.js +24 -39
- package/packages/core/paper/paper-repository.js +16 -0
- package/packages/core/paper/paper-runtime-channels.js +1 -1
- package/packages/core/paper/paper-scaffold-content.js +153 -162
- package/packages/core/paper/paper-work.js +3 -10
- package/packages/core/paper/paper.js +43 -5
- package/packages/core/providers/github/artifact-attestation.js +1 -1
- package/packages/core/providers/github/pipeline-context-artifacts.js +172 -0
- package/packages/core/providers/github/pipeline-release-evidence.js +17 -98
- package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
- package/packages/core/providers/github/pipeline-stable-source.js +22 -4
- package/packages/core/providers/github/release-evidence-assets.js +97 -0
- package/packages/core/publication/authority/control-plane.js +2 -2
- package/packages/core/publication/authority/seal.js +1 -1
- package/packages/core/publication/cli/dispatch.mjs +1 -12
- package/packages/core/publication/pipeline/actions.js +16 -9
- package/packages/core/publication/pipeline/apply.js +17 -9
- package/packages/core/publication/pipeline/context.js +29 -0
- package/packages/core/publication/pipeline/journal.js +13 -1
- package/packages/core/publication/pipeline/prepare.js +15 -6
- package/packages/core/publication/pipeline/recovery-import.js +1 -1
- package/packages/core/publication/pipeline/recovery-materials.js +63 -1
- package/packages/core/publication/pipeline/recovery-prepare.js +21 -9
- package/packages/core/publication/pipeline/stable-entry.js +12 -3
- package/packages/core/publication/pipeline/stable-products.js +21 -19
- package/packages/core/publication/pipeline/version-actions.js +15 -6
- package/packages/core/publication/publication-rehearsal-projection.js +0 -9
- package/packages/core/release/cli/dispatch.mjs +1 -4
- package/packages/core/release/cli/inspection-handlers.mjs +1 -33
- package/packages/core/release/cli/release-handlers.mjs +2 -19
- package/packages/core/release/cli/verification-handlers.mjs +4 -25
- package/packages/core/release/next-development-projection.js +13 -13
- package/packages/core/release/passport/locations.js +13 -8
- package/packages/core/release/release-passport.js +70 -5
- package/packages/core/web/cli/badges.mjs +0 -4
- package/packages/core/web/publication-admission.js +1 -1
- package/packages/core/web/publication-capability.js +1 -1
- package/packages/core/web/readme-badges.js +1 -1
- package/packages/core/workflow/cli/main.mjs +1 -4
- package/packages/core/workflow/cli/process.mjs +1 -1
- package/packages/core/workflow/commands/buildchain-cli-help.mjs +2 -8
- package/packages/core/workflow/engine/conformance.js +1 -1
- package/packages/core/workflow/pipeline/build.js +21 -1
- package/packages/core/workflow/pipeline/delivery-observation.js +6 -2
- package/packages/core/workflow/workflow-taxonomy.mjs +61 -17
- package/scripts/check-code-layout.mjs +2 -0
- package/scripts/check-core-mechanism-inventory.mjs +29 -13
- package/scripts/check-floating-consumer-policy-contract.mjs +3 -3
- package/scripts/check-inventory.mjs +27 -27
- package/scripts/check-maintainability.mjs +28 -5
- package/scripts/check-public-dogfood-contract.mjs +3 -3
- package/scripts/check-release-topology.mjs +1 -1
- package/scripts/check-self-consumer-contract.mjs +7 -7
- package/scripts/check-universal-workflow-bootstrap.mjs +1 -1
- package/scripts/generate-channel-promotion-workflow.mjs +1 -1
- package/scripts/generate-minimal-consumer-contract.mjs +21 -1
- package/scripts/generate-site-bundle.mjs +2 -1
- package/scripts/generate-tail-reseal-fixture.mjs +3 -3
- package/scripts/inventory/promotion.mjs +1 -1
- package/scripts/next-development-self-dogfood-harness.mjs +1 -1
- package/scripts/next-development-self-dogfood.mjs +4 -4
- package/scripts/site-capability-metadata.mjs +2 -6
- package/scripts/source-metric-lineage.mjs +62 -7
- package/scripts/verify-golden-path.mjs +31 -35
- package/templates/native-dev-delivery.yml +1 -1
- package/templates/universal-buildchain-bootstrap.yml +1 -1
- package/packages/core/paper/operations/bootstrap.js +0 -75
- package/packages/core/paper/operations/plans.js +0 -213
- package/packages/core/paper/operations/provisioning.js +0 -113
- package/packages/core/paper/operations/scaffold-config.js +0 -77
- package/packages/core/paper/operations/scaffold-workflows.js +0 -116
- package/packages/core/paper/operations/workspace.js +0 -60
- package/packages/core/paper/paper-npm-bootstrap.js +0 -492
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { collectPaperAgentEntry } from "./paper-agent-entry.js";
|
|
3
3
|
import {
|
|
4
|
-
PAPER_PATHS,
|
|
5
4
|
PAPER_WORK_BRANCH_PATTERN,
|
|
6
5
|
gitResult,
|
|
7
6
|
gitValue,
|
|
8
7
|
normalizedWorkBranch,
|
|
9
8
|
paperDevelopmentRef,
|
|
10
9
|
paperWorkSource,
|
|
11
|
-
readJson,
|
|
12
10
|
remoteBranchObservation,
|
|
13
11
|
rootedPlan,
|
|
14
12
|
workCheck,
|
|
@@ -30,12 +28,7 @@ function failedActions(checks) {
|
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
function agentEntryForWork(cwd, buildchainSha) {
|
|
33
|
-
|
|
34
|
-
return collectPaperAgentEntry({
|
|
35
|
-
cwd,
|
|
36
|
-
buildchainSha: buildchainSha || policy?.runtime?.sourceSha || "",
|
|
37
|
-
mode: "contract",
|
|
38
|
-
});
|
|
31
|
+
return collectPaperAgentEntry({ cwd, buildchainSha, mode: "contract" });
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
export function createPaperWorkStartPlan({
|
|
@@ -74,7 +67,7 @@ export function createPaperWorkStartPlan({
|
|
|
74
67
|
workCheck(
|
|
75
68
|
"agent-entry.current",
|
|
76
69
|
agentEntry.ok,
|
|
77
|
-
"The
|
|
70
|
+
"The Paper product configuration and shared consumer wiring are current.",
|
|
78
71
|
"buildchain paper migrate --write --json",
|
|
79
72
|
),
|
|
80
73
|
workCheck(
|
|
@@ -252,7 +245,7 @@ export function createPaperWorkSubmitPlan({
|
|
|
252
245
|
workCheck(
|
|
253
246
|
"agent-entry.current",
|
|
254
247
|
agentEntry.ok,
|
|
255
|
-
"The
|
|
248
|
+
"The Paper product configuration and shared consumer wiring are current.",
|
|
256
249
|
"buildchain paper migrate --write --json",
|
|
257
250
|
),
|
|
258
251
|
workCheck(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { paperConsumerStatus } from "./consumer-status.js";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import {
|
|
3
4
|
paperConfig,
|
|
@@ -44,15 +45,52 @@ import {
|
|
|
44
45
|
paperPreflightNextActions,
|
|
45
46
|
} from "./operations/preflight-checks.js";
|
|
46
47
|
export function collectPaperSourcePolicy({ cwd = process.cwd() } = {}) {
|
|
48
|
+
const consumer = paperConsumerStatus(cwd);
|
|
49
|
+
if (consumer) return consumer;
|
|
47
50
|
const agentEntry = collectPaperAgentEntry({ cwd, mode: "ci" });
|
|
48
51
|
const provisioning = validatePaperProvisioningAuthority(cwd);
|
|
49
|
-
const checks = [
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const checks = [
|
|
53
|
+
...agentEntry.checks,
|
|
54
|
+
{
|
|
55
|
+
id: "provisioning.authority",
|
|
56
|
+
status: provisioning.valid ? "pass" : "fail",
|
|
57
|
+
message: provisioning.errors.join("; "),
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
try {
|
|
61
|
+
validateBuildchainConfig(cwd, { requireLifecycleStages: ["verify"] });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
checks.push({
|
|
64
|
+
id: "config.publication",
|
|
65
|
+
status: "fail",
|
|
66
|
+
message: error.message,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
ok: checks.every(
|
|
71
|
+
(check) => check.status === "pass" || check.status === "skip",
|
|
72
|
+
),
|
|
73
|
+
checks,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function collectPaperPreflight(options = {}) {
|
|
78
|
+
const consumer = paperConsumerStatus(
|
|
79
|
+
path.resolve(options.cwd || process.cwd()),
|
|
80
|
+
);
|
|
81
|
+
if (!consumer) return collectLegacyPaperPreflight(options);
|
|
82
|
+
const result =
|
|
83
|
+
options.agentEntryMode && options.agentEntryMode !== "contract"
|
|
84
|
+
? collectPaperAgentEntry({
|
|
85
|
+
cwd: consumer.cwd,
|
|
86
|
+
mode: options.agentEntryMode,
|
|
87
|
+
env: options.env,
|
|
88
|
+
})
|
|
89
|
+
: consumer;
|
|
90
|
+
return { ...result, contract: PAPER_PREFLIGHT_CONTRACT };
|
|
53
91
|
}
|
|
54
92
|
|
|
55
|
-
|
|
93
|
+
function collectLegacyPaperPreflight({
|
|
56
94
|
cwd = process.cwd(),
|
|
57
95
|
buildchainRoot = process.cwd(),
|
|
58
96
|
buildchainVersion = "",
|
|
@@ -10,7 +10,7 @@ export function verifyArtifactAttestationSigner(request, execute = command) {
|
|
|
10
10
|
"--repo",
|
|
11
11
|
request.repository,
|
|
12
12
|
"--signer-workflow",
|
|
13
|
-
"kungfu-systems/buildchain/.github/workflows
|
|
13
|
+
"kungfu-systems/buildchain/.github/workflows/.release-artifact-attestation.yml",
|
|
14
14
|
"--signer-digest",
|
|
15
15
|
request.signerDigest,
|
|
16
16
|
"--source-digest",
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import artifact from "@actions/artifact";
|
|
4
|
+
import {
|
|
5
|
+
canonicalJson,
|
|
6
|
+
recordDigest,
|
|
7
|
+
} from "../../release/discussion/envelope.js";
|
|
8
|
+
import { publicationPath } from "../../publication/pipeline/files.js";
|
|
9
|
+
|
|
10
|
+
const INLINE_LIMIT = 32 * 1024;
|
|
11
|
+
const MATERIAL_LIMIT = 16 * 1024 * 1024;
|
|
12
|
+
const REFERENCE = "buildchain.pipeline-context-reference/v1";
|
|
13
|
+
const SCHEMAS = [
|
|
14
|
+
"buildchain.pipeline-publication-context/v1",
|
|
15
|
+
"buildchain.pipeline-version-context/v1",
|
|
16
|
+
];
|
|
17
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
18
|
+
|
|
19
|
+
function execution(env) {
|
|
20
|
+
const result = {
|
|
21
|
+
repository: env.GITHUB_REPOSITORY,
|
|
22
|
+
runId: Number(env.GITHUB_RUN_ID),
|
|
23
|
+
runAttempt: Number(env.GITHUB_RUN_ATTEMPT),
|
|
24
|
+
};
|
|
25
|
+
if (
|
|
26
|
+
!/^[\w.-]+\/[\w.-]+$/u.test(result.repository || "") ||
|
|
27
|
+
!Number.isSafeInteger(result.runId) ||
|
|
28
|
+
result.runId < 1 ||
|
|
29
|
+
!Number.isSafeInteger(result.runAttempt) ||
|
|
30
|
+
result.runAttempt < 1
|
|
31
|
+
)
|
|
32
|
+
throw new Error("Context transfer requires its exact provider execution");
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function projection(value, env) {
|
|
37
|
+
const current = execution(env);
|
|
38
|
+
if (
|
|
39
|
+
!SCHEMAS.includes(value.schema) ||
|
|
40
|
+
value.runId !== current.runId ||
|
|
41
|
+
value.runAttempt !== current.runAttempt
|
|
42
|
+
)
|
|
43
|
+
throw new Error("Context transfer belongs to another provider execution");
|
|
44
|
+
const source =
|
|
45
|
+
value.schema === SCHEMAS[0]
|
|
46
|
+
? value.materialization?.source
|
|
47
|
+
: value.preparation?.source;
|
|
48
|
+
if (
|
|
49
|
+
source?.repository !== current.repository ||
|
|
50
|
+
!/^[0-9a-f]{40}$/u.test(source?.commit || "")
|
|
51
|
+
)
|
|
52
|
+
throw new Error("Context transfer requires an exact repository source");
|
|
53
|
+
return {
|
|
54
|
+
schema: REFERENCE,
|
|
55
|
+
contextSchema: value.schema,
|
|
56
|
+
...current,
|
|
57
|
+
contextRoot: recordDigest(value),
|
|
58
|
+
...(value.schema === SCHEMAS[0]
|
|
59
|
+
? { materialization: { source } }
|
|
60
|
+
: { preparation: { source } }),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function workspace(env) {
|
|
65
|
+
const directory = path.join(
|
|
66
|
+
env.GITHUB_WORKSPACE,
|
|
67
|
+
".buildchain/context-material",
|
|
68
|
+
);
|
|
69
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
70
|
+
return fs.mkdtempSync(path.join(directory, "transfer-"));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Only an immutable reference and the checkout projection cross job outputs.
|
|
74
|
+
// The original context remains unchanged in the authoritative attempt journal.
|
|
75
|
+
export async function writePipelineContext(value, env, client = artifact) {
|
|
76
|
+
const serialized = canonicalJson(value);
|
|
77
|
+
const bytes = Buffer.byteLength(serialized);
|
|
78
|
+
if (bytes <= INLINE_LIMIT) return serialized;
|
|
79
|
+
if (bytes > MATERIAL_LIMIT)
|
|
80
|
+
throw new Error("Pipeline context exceeds its retained material bound");
|
|
81
|
+
const selected = projection(value, env);
|
|
82
|
+
const directory = workspace(env);
|
|
83
|
+
try {
|
|
84
|
+
const file = path.join(directory, "context.json");
|
|
85
|
+
fs.writeFileSync(file, serialized, { flag: "wx" });
|
|
86
|
+
const uploaded = await client.uploadArtifact(
|
|
87
|
+
`buildchain-context-${selected.runAttempt}-${selected.contextRoot.slice(7)}`,
|
|
88
|
+
[file],
|
|
89
|
+
directory,
|
|
90
|
+
{ retentionDays: 30, compressionLevel: 0 },
|
|
91
|
+
);
|
|
92
|
+
const digest = `sha256:${String(uploaded.digest || "").replace(/^sha256:/u, "")}`;
|
|
93
|
+
if (
|
|
94
|
+
!Number.isSafeInteger(uploaded.id) ||
|
|
95
|
+
uploaded.id < 1 ||
|
|
96
|
+
!DIGEST.test(digest)
|
|
97
|
+
)
|
|
98
|
+
throw new Error("Context upload omitted immutable provider coordinates");
|
|
99
|
+
const encoded = canonicalJson({
|
|
100
|
+
...selected,
|
|
101
|
+
bytes,
|
|
102
|
+
artifact: { id: uploaded.id, digest },
|
|
103
|
+
});
|
|
104
|
+
if (Buffer.byteLength(encoded) > INLINE_LIMIT)
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Context checkout projection exceeds the job output bound",
|
|
107
|
+
);
|
|
108
|
+
return encoded;
|
|
109
|
+
} finally {
|
|
110
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateReference(value, env) {
|
|
115
|
+
const current = execution(env);
|
|
116
|
+
if (
|
|
117
|
+
!SCHEMAS.includes(value.contextSchema) ||
|
|
118
|
+
value.repository !== current.repository ||
|
|
119
|
+
value.runId !== current.runId ||
|
|
120
|
+
value.runAttempt !== current.runAttempt ||
|
|
121
|
+
!DIGEST.test(value.contextRoot || "") ||
|
|
122
|
+
!Number.isSafeInteger(value.bytes) ||
|
|
123
|
+
value.bytes <= INLINE_LIMIT ||
|
|
124
|
+
value.bytes > MATERIAL_LIMIT ||
|
|
125
|
+
!Number.isSafeInteger(value.artifact?.id) ||
|
|
126
|
+
value.artifact.id < 1 ||
|
|
127
|
+
!DIGEST.test(value.artifact?.digest || "")
|
|
128
|
+
)
|
|
129
|
+
throw new Error(
|
|
130
|
+
"Context reference changed its execution or material boundary",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function readPipelineContext(encoded, env, client = artifact) {
|
|
135
|
+
if (Buffer.byteLength(encoded) > INLINE_LIMIT)
|
|
136
|
+
throw new Error("Pipeline context must use bounded job output transport");
|
|
137
|
+
const reference = JSON.parse(encoded);
|
|
138
|
+
if (reference.schema !== REFERENCE) return reference;
|
|
139
|
+
validateReference(reference, env);
|
|
140
|
+
const directory = workspace(env);
|
|
141
|
+
try {
|
|
142
|
+
// No findBy or repository token: the artifact SDK scopes this read to the
|
|
143
|
+
// current workflow execution, including credentialless product build jobs.
|
|
144
|
+
const downloaded = await client.downloadArtifact(reference.artifact.id, {
|
|
145
|
+
path: directory,
|
|
146
|
+
expectedHash: reference.artifact.digest,
|
|
147
|
+
});
|
|
148
|
+
if (
|
|
149
|
+
downloaded.digestMismatch ||
|
|
150
|
+
canonicalJson(fs.readdirSync(directory)) !== '["context.json"]'
|
|
151
|
+
)
|
|
152
|
+
throw new Error("Context artifact changed its archive or file inventory");
|
|
153
|
+
const file = publicationPath(directory, "context.json");
|
|
154
|
+
if (fs.statSync(file).size !== reference.bytes)
|
|
155
|
+
throw new Error("Context artifact changed its retained byte length");
|
|
156
|
+
const value = JSON.parse(
|
|
157
|
+
new TextDecoder("utf-8", { fatal: true }).decode(fs.readFileSync(file)),
|
|
158
|
+
);
|
|
159
|
+
const expected = {
|
|
160
|
+
...projection(value, env),
|
|
161
|
+
bytes: reference.bytes,
|
|
162
|
+
artifact: reference.artifact,
|
|
163
|
+
};
|
|
164
|
+
if (recordDigest(expected) !== recordDigest(reference))
|
|
165
|
+
throw new Error(
|
|
166
|
+
"Context artifact differs from its exact checkout projection or root",
|
|
167
|
+
);
|
|
168
|
+
return value;
|
|
169
|
+
} finally {
|
|
170
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
1
|
+
import { readReleaseEvidenceAssets } from "./release-evidence-assets.js";
|
|
3
2
|
|
|
4
3
|
const NAMES = [
|
|
5
4
|
"plan",
|
|
@@ -9,103 +8,23 @@ const NAMES = [
|
|
|
9
8
|
"attestation",
|
|
10
9
|
"release",
|
|
11
10
|
];
|
|
12
|
-
const LIMIT = 8 * 1024 * 1024;
|
|
13
|
-
|
|
14
|
-
async function inventory(request, prefix) {
|
|
15
|
-
const assets = [],
|
|
16
|
-
ids = new Set(),
|
|
17
|
-
names = new Set();
|
|
18
|
-
for (let page = 1; page <= 20; page++) {
|
|
19
|
-
const values = await request(`${prefix}/assets?per_page=100&page=${page}`);
|
|
20
|
-
if (!Array.isArray(values) || values.length > 100)
|
|
21
|
-
throw new Error("Published evidence asset inventory is incomplete");
|
|
22
|
-
for (const value of values) {
|
|
23
|
-
if (
|
|
24
|
-
!Number.isSafeInteger(value.id) ||
|
|
25
|
-
value.id < 1 ||
|
|
26
|
-
typeof value.name !== "string" ||
|
|
27
|
-
!value.name ||
|
|
28
|
-
ids.has(value.id) ||
|
|
29
|
-
names.has(value.name)
|
|
30
|
-
)
|
|
31
|
-
throw new Error("Published evidence asset inventory is ambiguous");
|
|
32
|
-
ids.add(value.id);
|
|
33
|
-
names.add(value.name);
|
|
34
|
-
assets.push(value);
|
|
35
|
-
}
|
|
36
|
-
if (values.length < 100) return assets;
|
|
37
|
-
}
|
|
38
|
-
throw new Error(
|
|
39
|
-
"Published evidence inventory exceeds its complete-read bound",
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function selectedAssets(assets) {
|
|
44
|
-
return NAMES.map((id) => {
|
|
45
|
-
const name = `buildchain.${id}.json`;
|
|
46
|
-
const asset = assets.find((value) => value.name === name);
|
|
47
|
-
if (!asset) return { name, missing: true };
|
|
48
|
-
if (
|
|
49
|
-
asset.state !== "uploaded" ||
|
|
50
|
-
!Number.isSafeInteger(asset.size) ||
|
|
51
|
-
asset.size < 1 ||
|
|
52
|
-
asset.size > LIMIT ||
|
|
53
|
-
!/^sha256:[0-9a-f]{64}$/u.test(asset.digest || "")
|
|
54
|
-
)
|
|
55
|
-
throw new Error("Published evidence asset lacks bounded immutable bytes");
|
|
56
|
-
return { id: asset.id, name, size: asset.size, digest: asset.digest };
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Download only fixed evidence names from the exact already-observed release.
|
|
61
|
-
// There is no caller-supplied URL, artifact selector or provider write here.
|
|
62
11
|
export async function readPipelineReleaseEvidence(host, candidate) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
throw new Error("Published evidence requires exact release coordinates");
|
|
70
|
-
const prefix = `/repos/${repository}/releases/${candidate.id}`;
|
|
71
|
-
const selected = selectedAssets(await inventory(host.request, prefix));
|
|
72
|
-
const missing = selected
|
|
73
|
-
.filter((value) => value.missing)
|
|
74
|
-
.map(({ name }) => name);
|
|
75
|
-
if (missing.length) return { status: "missing", missing };
|
|
76
|
-
const [owner, repo] = repository.split("/");
|
|
12
|
+
const result = await readReleaseEvidenceAssets(
|
|
13
|
+
host,
|
|
14
|
+
candidate,
|
|
15
|
+
NAMES.map((id) => `buildchain.${id}.json`),
|
|
16
|
+
);
|
|
17
|
+
if (result.status !== "present") return result;
|
|
77
18
|
const values = {};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
asset_id: asset.id,
|
|
84
|
-
headers: { accept: "application/octet-stream" },
|
|
85
|
-
});
|
|
86
|
-
const bytes = Buffer.from(result.data);
|
|
87
|
-
if (
|
|
88
|
-
bytes.length !== asset.size ||
|
|
89
|
-
bytes.length > LIMIT ||
|
|
90
|
-
`sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
|
|
91
|
-
asset.digest
|
|
92
|
-
)
|
|
93
|
-
throw new Error(
|
|
94
|
-
"Published evidence download differs from provider bytes",
|
|
95
|
-
);
|
|
96
|
-
const id = asset.name.slice("buildchain.".length, -".json".length);
|
|
97
|
-
if (id === "attestation") bundle = bytes;
|
|
98
|
-
else
|
|
99
|
-
values[id] = JSON.parse(
|
|
100
|
-
new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
if (
|
|
104
|
-
recordDigest(selectedAssets(await inventory(host.request, prefix))) !==
|
|
105
|
-
recordDigest(selected)
|
|
106
|
-
)
|
|
107
|
-
throw new Error(
|
|
108
|
-
"Published evidence assets changed during provider readback",
|
|
19
|
+
for (const id of NAMES.filter((name) => name !== "attestation"))
|
|
20
|
+
values[id] = JSON.parse(
|
|
21
|
+
new TextDecoder("utf-8", { fatal: true }).decode(
|
|
22
|
+
result.values[`buildchain.${id}.json`],
|
|
23
|
+
),
|
|
109
24
|
);
|
|
110
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
...result,
|
|
27
|
+
values,
|
|
28
|
+
bundle: result.values["buildchain.attestation.json"],
|
|
29
|
+
};
|
|
111
30
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import {
|
|
3
|
+
SETTLEMENT_ASSET,
|
|
4
|
+
verifyPublicationSettlement,
|
|
5
|
+
} from "../../publication/settlement/transaction.js";
|
|
6
|
+
import { readReleaseEvidenceAssets } from "./release-evidence-assets.js";
|
|
7
|
+
|
|
8
|
+
// Earlier publications materialized the floating entry after the exact tag.
|
|
9
|
+
// Only their completed immutable settlement can bind those two commits.
|
|
10
|
+
export async function readPipelineStableBaseline(plan, host, release, prior) {
|
|
11
|
+
if ((prior?.commit || null) === plan.previousChannelCommit) return null;
|
|
12
|
+
if (!release || !prior || !plan.previousChannelCommit)
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Stable comparison differs from the retained published channel",
|
|
15
|
+
);
|
|
16
|
+
const evidence = await readReleaseEvidenceAssets(host, release, [
|
|
17
|
+
SETTLEMENT_ASSET,
|
|
18
|
+
"buildchain.release.json",
|
|
19
|
+
]);
|
|
20
|
+
if (evidence.status !== "present")
|
|
21
|
+
throw new Error(
|
|
22
|
+
"Stable channel differs without a completed publication settlement",
|
|
23
|
+
);
|
|
24
|
+
const read = (name) =>
|
|
25
|
+
JSON.parse(
|
|
26
|
+
new TextDecoder("utf-8", { fatal: true }).decode(evidence.values[name]),
|
|
27
|
+
);
|
|
28
|
+
const settlement = read(SETTLEMENT_ASSET),
|
|
29
|
+
publicPassport = read("buildchain.release.json"),
|
|
30
|
+
repository = plan.source.repository,
|
|
31
|
+
tag = release.tag,
|
|
32
|
+
sourceSha = prior.commit;
|
|
33
|
+
if (
|
|
34
|
+
settlement.schemaVersion !== 1 ||
|
|
35
|
+
settlement.contract !== "buildchain-v4-publication-settlement/v1" ||
|
|
36
|
+
settlement.release?.tag !== tag ||
|
|
37
|
+
settlement.release?.sourceSha !== sourceSha ||
|
|
38
|
+
settlement.release?.channel !== "stable"
|
|
39
|
+
)
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Stable baseline settlement has different release coordinates",
|
|
42
|
+
);
|
|
43
|
+
const verified = verifyPublicationSettlement(settlement.documents, {
|
|
44
|
+
repository,
|
|
45
|
+
tag,
|
|
46
|
+
sourceSha,
|
|
47
|
+
publicPassport,
|
|
48
|
+
});
|
|
49
|
+
const { product, providerState, productState, invocation, passport } =
|
|
50
|
+
settlement.documents;
|
|
51
|
+
const ref = `refs/tags/v${plan.version.split(".")[0]}`,
|
|
52
|
+
updates = product.updates.filter((value) => value.ref === ref);
|
|
53
|
+
const subject = {
|
|
54
|
+
repository,
|
|
55
|
+
sourceSha,
|
|
56
|
+
tag,
|
|
57
|
+
version: tag.slice(1),
|
|
58
|
+
channel: "stable",
|
|
59
|
+
};
|
|
60
|
+
if (
|
|
61
|
+
invocation.target.channel !== "stable" ||
|
|
62
|
+
passport.release.channel !== "stable" ||
|
|
63
|
+
product.promotedSha !== plan.previousChannelCommit ||
|
|
64
|
+
updates.length !== 1 ||
|
|
65
|
+
updates[0].action !== "converged-release-tag" ||
|
|
66
|
+
updates[0].sha !== plan.previousChannelCommit ||
|
|
67
|
+
recordDigest(productState.subject) !== recordDigest(subject) ||
|
|
68
|
+
recordDigest(providerState.subject) !==
|
|
69
|
+
recordDigest({
|
|
70
|
+
...subject,
|
|
71
|
+
sourceSha: plan.previousChannelCommit,
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Stable baseline settlement does not bind the retained floating channel",
|
|
76
|
+
);
|
|
77
|
+
return {
|
|
78
|
+
releaseId: release.id,
|
|
79
|
+
tag,
|
|
80
|
+
sourceSha,
|
|
81
|
+
channelRef: ref,
|
|
82
|
+
channelCommit: plan.previousChannelCommit,
|
|
83
|
+
receiptRoot: verified.receiptRoot,
|
|
84
|
+
assets: evidence.assets,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -5,6 +5,7 @@ import { compareDevelopmentVersions } from "../../publication/publication-develo
|
|
|
5
5
|
import { githubPipelineSource } from "./pipeline-source.js";
|
|
6
6
|
import { githubPipelineVersion } from "./pipeline-version.js";
|
|
7
7
|
import { normalInputs } from "../../consumer/contract/entries.js";
|
|
8
|
+
import { readPipelineStableBaseline } from "./pipeline-stable-baseline.js";
|
|
8
9
|
|
|
9
10
|
const SHA = /^[0-9a-f]{40}$/u;
|
|
10
11
|
const STABLE = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
|
|
@@ -295,10 +296,12 @@ export async function readPipelineStableSource(plan, host) {
|
|
|
295
296
|
const prior = selected.comparison
|
|
296
297
|
? await exactTag(get, selected.comparison.tag)
|
|
297
298
|
: null;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
const baseline = await readPipelineStableBaseline(
|
|
300
|
+
plan,
|
|
301
|
+
{ ...host, request },
|
|
302
|
+
selected.comparison,
|
|
303
|
+
prior,
|
|
304
|
+
);
|
|
302
305
|
const current = await treeFiles(get, pointer.commit);
|
|
303
306
|
if (current.tree !== candidate.identity.tree)
|
|
304
307
|
throw new Error(
|
|
@@ -335,6 +338,20 @@ export async function readPipelineStableSource(plan, host) {
|
|
|
335
338
|
prior,
|
|
336
339
|
latest,
|
|
337
340
|
});
|
|
341
|
+
if (
|
|
342
|
+
baseline &&
|
|
343
|
+
recordDigest(
|
|
344
|
+
await readPipelineStableBaseline(
|
|
345
|
+
plan,
|
|
346
|
+
{ ...host, request },
|
|
347
|
+
selected.comparison,
|
|
348
|
+
prior,
|
|
349
|
+
),
|
|
350
|
+
) !== recordDigest(baseline)
|
|
351
|
+
)
|
|
352
|
+
throw new Error(
|
|
353
|
+
"Stable baseline evidence changed during provider readback",
|
|
354
|
+
);
|
|
338
355
|
const body = {
|
|
339
356
|
schema: "buildchain.pipeline-stable-source/v1",
|
|
340
357
|
planRoot: plan.root,
|
|
@@ -347,6 +364,7 @@ export async function readPipelineStableSource(plan, host) {
|
|
|
347
364
|
comparisonStable: selected.comparison
|
|
348
365
|
? { ...selected.comparison, sha: prior.commit, tree: previous.tree }
|
|
349
366
|
: null,
|
|
367
|
+
...(baseline ? { comparisonChannel: baseline } : {}),
|
|
350
368
|
impact,
|
|
351
369
|
changedPaths: changedPaths(previous.files, current.files),
|
|
352
370
|
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
|
|
4
|
+
const LIMIT = 8 * 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
async function inventory(request, prefix) {
|
|
7
|
+
const assets = [],
|
|
8
|
+
ids = new Set(),
|
|
9
|
+
names = new Set();
|
|
10
|
+
for (let page = 1; page <= 20; page++) {
|
|
11
|
+
const values = await request(`${prefix}/assets?per_page=100&page=${page}`);
|
|
12
|
+
if (!Array.isArray(values) || values.length > 100)
|
|
13
|
+
throw new Error("Published evidence asset inventory is incomplete");
|
|
14
|
+
for (const value of values) {
|
|
15
|
+
if (
|
|
16
|
+
!Number.isSafeInteger(value.id) ||
|
|
17
|
+
value.id < 1 ||
|
|
18
|
+
typeof value.name !== "string" ||
|
|
19
|
+
!value.name ||
|
|
20
|
+
ids.has(value.id) ||
|
|
21
|
+
names.has(value.name)
|
|
22
|
+
)
|
|
23
|
+
throw new Error("Published evidence asset inventory is ambiguous");
|
|
24
|
+
ids.add(value.id);
|
|
25
|
+
names.add(value.name);
|
|
26
|
+
assets.push(value);
|
|
27
|
+
}
|
|
28
|
+
if (values.length < 100) return assets;
|
|
29
|
+
}
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Published evidence inventory exceeds its complete-read bound",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function selectedAssets(assets, names) {
|
|
36
|
+
return names.map((name) => {
|
|
37
|
+
const asset = assets.find((value) => value.name === name);
|
|
38
|
+
if (!asset) return { name, missing: true };
|
|
39
|
+
if (
|
|
40
|
+
asset.state !== "uploaded" ||
|
|
41
|
+
!Number.isSafeInteger(asset.size) ||
|
|
42
|
+
asset.size < 1 ||
|
|
43
|
+
asset.size > LIMIT ||
|
|
44
|
+
!/^sha256:[0-9a-f]{64}$/u.test(asset.digest || "")
|
|
45
|
+
)
|
|
46
|
+
throw new Error("Published evidence asset lacks bounded immutable bytes");
|
|
47
|
+
return { id: asset.id, name, size: asset.size, digest: asset.digest };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Download only fixed evidence names from the exact already-observed release.
|
|
52
|
+
// There is no caller-supplied URL, artifact selector or provider write here.
|
|
53
|
+
export async function readReleaseEvidenceAssets(host, candidate, names) {
|
|
54
|
+
const repository = host.repository;
|
|
55
|
+
if (
|
|
56
|
+
!/^[\w.-]+\/[\w.-]+$/u.test(repository || "") ||
|
|
57
|
+
!Number.isSafeInteger(candidate.id) ||
|
|
58
|
+
candidate.id < 1
|
|
59
|
+
)
|
|
60
|
+
throw new Error("Published evidence requires exact release coordinates");
|
|
61
|
+
const prefix = `/repos/${repository}/releases/${candidate.id}`;
|
|
62
|
+
const selected = selectedAssets(await inventory(host.request, prefix), names);
|
|
63
|
+
const missing = selected
|
|
64
|
+
.filter((value) => value.missing)
|
|
65
|
+
.map(({ name }) => name);
|
|
66
|
+
if (missing.length) return { status: "missing", missing };
|
|
67
|
+
const [owner, repo] = repository.split("/");
|
|
68
|
+
const values = {};
|
|
69
|
+
for (const asset of selected) {
|
|
70
|
+
const result = await host.github.rest.repos.getReleaseAsset({
|
|
71
|
+
owner,
|
|
72
|
+
repo,
|
|
73
|
+
asset_id: asset.id,
|
|
74
|
+
headers: { accept: "application/octet-stream" },
|
|
75
|
+
});
|
|
76
|
+
const bytes = Buffer.from(result.data);
|
|
77
|
+
if (
|
|
78
|
+
bytes.length !== asset.size ||
|
|
79
|
+
bytes.length > LIMIT ||
|
|
80
|
+
`sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
|
|
81
|
+
asset.digest
|
|
82
|
+
)
|
|
83
|
+
throw new Error(
|
|
84
|
+
"Published evidence download differs from provider bytes",
|
|
85
|
+
);
|
|
86
|
+
values[asset.name] = bytes;
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
recordDigest(
|
|
90
|
+
selectedAssets(await inventory(host.request, prefix), names),
|
|
91
|
+
) !== recordDigest(selected)
|
|
92
|
+
)
|
|
93
|
+
throw new Error(
|
|
94
|
+
"Published evidence assets changed during provider readback",
|
|
95
|
+
);
|
|
96
|
+
return { status: "present", values, assets: selected };
|
|
97
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export function publicationControlPlaneRequest(request) {
|
|
2
2
|
const kind = request.autoAdmissionKind;
|
|
3
3
|
const defaults = {
|
|
4
|
-
"release-candidate": ".github/workflows
|
|
5
|
-
"publication-artifact": ".github/workflows
|
|
4
|
+
"release-candidate": ".github/workflows/.release-candidate-promote.yml",
|
|
5
|
+
"publication-artifact": ".github/workflows/.release-paper.yml",
|
|
6
6
|
"binary-release-assets": ".github/workflows/.release-binary-assets.yml",
|
|
7
7
|
};
|
|
8
8
|
if (!defaults[kind])
|
|
@@ -26,7 +26,7 @@ export function sealManagedPublicationAdmission({
|
|
|
26
26
|
);
|
|
27
27
|
const suffix = binary ? ":binary-release-assets" : paper ? ":paper" : "";
|
|
28
28
|
const defaults = {
|
|
29
|
-
"release-candidate": ".github/workflows
|
|
29
|
+
"release-candidate": ".github/workflows/.release-candidate-promote.yml",
|
|
30
30
|
"publication-artifact": ".github/workflows/public-release-paper-sealed.yml",
|
|
31
31
|
"binary-release-assets": ".github/workflows/.release-binary-assets.yml",
|
|
32
32
|
};
|