@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,178 @@
|
|
|
1
|
+
import { bindConsumerSource } from "../../consumer/contract/identity.js";
|
|
2
|
+
import { normalInputs } from "../../consumer/contract/entries.js";
|
|
3
|
+
import { channelRoute } from "../../workflow/pipeline/events.js";
|
|
4
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
5
|
+
import { validateGeneration } from "../../workflow/attempt/identity.js";
|
|
6
|
+
|
|
7
|
+
function exactSha(value) {
|
|
8
|
+
if (!/^[0-9a-f]{40}$/u.test(value || ""))
|
|
9
|
+
throw new Error("Pipeline source requires an exact Git commit");
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function pullRequestBinding(pr) {
|
|
14
|
+
return {
|
|
15
|
+
number: pr.number,
|
|
16
|
+
state: pr.state,
|
|
17
|
+
merged: pr.merged,
|
|
18
|
+
mergeCommit: pr.merge_commit_sha,
|
|
19
|
+
head: pr.head,
|
|
20
|
+
base: pr.base,
|
|
21
|
+
draft: pr.draft,
|
|
22
|
+
labels: pr.labels,
|
|
23
|
+
updatedAt: pr.updated_at,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function githubPipelineSource(request, repository) {
|
|
28
|
+
if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
|
|
29
|
+
throw new Error("Invalid pipeline repository");
|
|
30
|
+
const base = `/repos/${repository}`;
|
|
31
|
+
async function source(commitInput, configPath) {
|
|
32
|
+
const commit = exactSha(commitInput);
|
|
33
|
+
const git = await request(`${base}/git/commits/${commit}`);
|
|
34
|
+
const config = await request(
|
|
35
|
+
`${base}/contents/${configPath}?ref=${commit}`,
|
|
36
|
+
);
|
|
37
|
+
if (
|
|
38
|
+
config.type !== "file" ||
|
|
39
|
+
config.encoding !== "base64" ||
|
|
40
|
+
!Number.isSafeInteger(config.size) ||
|
|
41
|
+
config.size < 1 ||
|
|
42
|
+
config.size > 256_000 ||
|
|
43
|
+
typeof config.content !== "string" ||
|
|
44
|
+
config.content.length > 512_000
|
|
45
|
+
)
|
|
46
|
+
throw new Error("Pipeline TOML is missing or exceeds its byte bound");
|
|
47
|
+
const bytes = Buffer.from(config.content, "base64");
|
|
48
|
+
if (bytes.length !== config.size)
|
|
49
|
+
throw new Error("Pipeline TOML size drift");
|
|
50
|
+
return bindConsumerSource(
|
|
51
|
+
{
|
|
52
|
+
repository,
|
|
53
|
+
commit,
|
|
54
|
+
tree: exactSha(git.tree?.sha),
|
|
55
|
+
configPath,
|
|
56
|
+
configBlob: config.sha,
|
|
57
|
+
},
|
|
58
|
+
bytes,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
async function branchHead(branch) {
|
|
62
|
+
if (
|
|
63
|
+
!/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(
|
|
64
|
+
branch || "",
|
|
65
|
+
) ||
|
|
66
|
+
branch.includes("..")
|
|
67
|
+
)
|
|
68
|
+
throw new Error("Invalid protected branch");
|
|
69
|
+
const pointer = await request(`${base}/git/ref/heads/${branch}`);
|
|
70
|
+
if (pointer.object?.type !== "commit")
|
|
71
|
+
throw new Error("Protected ref is not a commit");
|
|
72
|
+
return exactSha(pointer.object.sha);
|
|
73
|
+
}
|
|
74
|
+
async function observe(pullRequest, inputs = {}) {
|
|
75
|
+
if (!Number.isSafeInteger(pullRequest) || pullRequest < 1)
|
|
76
|
+
throw new Error("Pipeline requires an exact PR number");
|
|
77
|
+
const { configPath } = normalInputs(inputs);
|
|
78
|
+
const pr = await request(`${base}/pulls/${pullRequest}`);
|
|
79
|
+
const repositoryInfo = await request(base);
|
|
80
|
+
if (pr.number !== pullRequest || !repositoryInfo.node_id)
|
|
81
|
+
throw new Error("Pipeline PR provider identity drift");
|
|
82
|
+
// Read delivery policy from the protected base. A PR may change its own
|
|
83
|
+
// product commands, but cannot lower review or grant itself a channel route.
|
|
84
|
+
const baseCommit = await branchHead(pr.base?.ref);
|
|
85
|
+
const protectedSource = await source(baseCommit, configPath);
|
|
86
|
+
const route = channelRoute(protectedSource.plan, pr, repository);
|
|
87
|
+
if (!route) {
|
|
88
|
+
if (baseCommit !== (await branchHead(pr.base.ref)))
|
|
89
|
+
throw new Error("Protected policy changed during route observation");
|
|
90
|
+
return { eligible: false, reason: "channel-not-enabled" };
|
|
91
|
+
}
|
|
92
|
+
const admitted = await source(pr.head.sha, configPath);
|
|
93
|
+
const again = await request(`${base}/pulls/${pullRequest}`);
|
|
94
|
+
if (
|
|
95
|
+
recordDigest(pullRequestBinding(pr)) !==
|
|
96
|
+
recordDigest(pullRequestBinding(again)) ||
|
|
97
|
+
baseCommit !== (await branchHead(pr.base.ref))
|
|
98
|
+
)
|
|
99
|
+
throw new Error("Pipeline source changed during observation; reobserve");
|
|
100
|
+
return {
|
|
101
|
+
eligible: true,
|
|
102
|
+
repositoryId: repositoryInfo.node_id,
|
|
103
|
+
route,
|
|
104
|
+
plan: admitted.plan,
|
|
105
|
+
protectedPlan: protectedSource.plan,
|
|
106
|
+
protectedSource: protectedSource.identity,
|
|
107
|
+
live: {
|
|
108
|
+
pullRequest,
|
|
109
|
+
observedHead: pr.head.sha,
|
|
110
|
+
targetBranch: pr.base.ref,
|
|
111
|
+
routeEnabled: true,
|
|
112
|
+
source: admitted.identity,
|
|
113
|
+
baseCommit,
|
|
114
|
+
state: pr.state,
|
|
115
|
+
merged: pr.merged === true,
|
|
116
|
+
mergeCommit: pr.merge_commit_sha,
|
|
117
|
+
draft: pr.draft === true,
|
|
118
|
+
ready: pr.labels.some((label) => label.name === "ready"),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function observeIntent(intent, generation, inputs = {}) {
|
|
123
|
+
validateGeneration(generation, intent);
|
|
124
|
+
if (intent.repository !== repository)
|
|
125
|
+
throw new Error("Recorded intent repository drift");
|
|
126
|
+
const number = intent.source.pullRequest;
|
|
127
|
+
const pr = await request(`${base}/pulls/${number}`);
|
|
128
|
+
const repositoryInfo = await request(base);
|
|
129
|
+
if (
|
|
130
|
+
repositoryInfo.node_id !== intent.source.repositoryId ||
|
|
131
|
+
pr.number !== number ||
|
|
132
|
+
pr.base?.repo?.full_name !== repository
|
|
133
|
+
)
|
|
134
|
+
throw new Error("Recorded pipeline intent provider identity drift");
|
|
135
|
+
const sameHead =
|
|
136
|
+
pr.head?.repo?.full_name === repository &&
|
|
137
|
+
pr.head.sha === generation.source.commit;
|
|
138
|
+
const live = {
|
|
139
|
+
pullRequest: number,
|
|
140
|
+
observedHead: exactSha(pr.head?.sha),
|
|
141
|
+
targetBranch: pr.base.ref,
|
|
142
|
+
state: pr.state,
|
|
143
|
+
merged: pr.merged === true,
|
|
144
|
+
mergeCommit: pr.merge_commit_sha,
|
|
145
|
+
source: sameHead ? generation.source : null,
|
|
146
|
+
baseCommit: null,
|
|
147
|
+
ready: pr.labels.some((label) => label.name === "ready"),
|
|
148
|
+
draft: pr.draft === true,
|
|
149
|
+
};
|
|
150
|
+
// Closing, updating or retargeting an admitted PR must remain recoverable
|
|
151
|
+
// even when its new TOML is invalid, its old route disappeared or it is now a fork.
|
|
152
|
+
if (
|
|
153
|
+
pr.state !== "open" ||
|
|
154
|
+
!sameHead ||
|
|
155
|
+
pr.base.ref !== intent.source.targetBranch
|
|
156
|
+
) {
|
|
157
|
+
const again = await request(`${base}/pulls/${number}`);
|
|
158
|
+
if (
|
|
159
|
+
recordDigest(pullRequestBinding(pr)) !==
|
|
160
|
+
recordDigest(pullRequestBinding(again))
|
|
161
|
+
)
|
|
162
|
+
throw new Error("Recorded intent changed during terminal observation");
|
|
163
|
+
return { eligible: true, cleanupOnly: true, live };
|
|
164
|
+
}
|
|
165
|
+
const observed = await observe(number, inputs);
|
|
166
|
+
if (observed.eligible) return observed;
|
|
167
|
+
return {
|
|
168
|
+
eligible: true,
|
|
169
|
+
cleanupOnly: true,
|
|
170
|
+
live: {
|
|
171
|
+
...live,
|
|
172
|
+
baseCommit: await branchHead(intent.source.targetBranch),
|
|
173
|
+
routeEnabled: false,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return { observe, observeIntent, source, branchHead };
|
|
178
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
import {
|
|
4
|
+
materializePipelineVersion,
|
|
5
|
+
readPipelineVersion,
|
|
6
|
+
} from "../../publication/pipeline/version.js";
|
|
7
|
+
import { verifyPipelinePublicationPlan } from "../../publication/pipeline/plan.js";
|
|
8
|
+
|
|
9
|
+
function blobBytes(value, sha) {
|
|
10
|
+
if (
|
|
11
|
+
value.encoding !== "base64" ||
|
|
12
|
+
value.size > 4 * 1024 * 1024 ||
|
|
13
|
+
typeof value.content !== "string"
|
|
14
|
+
)
|
|
15
|
+
throw new Error("Version material requires a bounded Git blob");
|
|
16
|
+
const bytes = Buffer.from(value.content, "base64");
|
|
17
|
+
const digest = createHash("sha1")
|
|
18
|
+
.update(`blob ${bytes.length}\0`)
|
|
19
|
+
.update(bytes)
|
|
20
|
+
.digest("hex");
|
|
21
|
+
if (bytes.length !== value.size || digest !== sha || value.sha !== sha)
|
|
22
|
+
throw new Error("Version material blob identity drift");
|
|
23
|
+
return bytes.toString("utf8");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function githubPipelineVersion(request, repository) {
|
|
27
|
+
const base = `/repos/${repository}`;
|
|
28
|
+
const post = (url, body) => request(url, { method: "POST", body });
|
|
29
|
+
async function inspect(source, policy) {
|
|
30
|
+
if (source.repository !== repository)
|
|
31
|
+
throw new Error("Version material repository mismatch");
|
|
32
|
+
const commit = await request(`${base}/git/commits/${source.commit}`);
|
|
33
|
+
if (commit.sha !== source.commit || commit.tree?.sha !== source.tree)
|
|
34
|
+
throw new Error("Version material source tree drift");
|
|
35
|
+
const tree = await request(`${base}/git/trees/${source.tree}?recursive=1`);
|
|
36
|
+
if (tree.truncated || tree.sha !== source.tree || !Array.isArray(tree.tree))
|
|
37
|
+
throw new Error("Version material requires a complete source tree");
|
|
38
|
+
const files = {},
|
|
39
|
+
entries = {};
|
|
40
|
+
for (const file of [
|
|
41
|
+
...policy.files,
|
|
42
|
+
...(policy.derived_files || []).map((path) => ({ path })),
|
|
43
|
+
]) {
|
|
44
|
+
if (Object.hasOwn(files, file.path)) continue;
|
|
45
|
+
const found = tree.tree.filter((entry) => entry.path === file.path);
|
|
46
|
+
if (
|
|
47
|
+
found.length !== 1 ||
|
|
48
|
+
found[0].type !== "blob" ||
|
|
49
|
+
!["100644", "100755"].includes(found[0].mode)
|
|
50
|
+
)
|
|
51
|
+
throw new Error("Version material must be a tracked regular file");
|
|
52
|
+
const entry = found[0];
|
|
53
|
+
entries[file.path] = entry;
|
|
54
|
+
files[file.path] = blobBytes(
|
|
55
|
+
await request(`${base}/git/blobs/${entry.sha}`),
|
|
56
|
+
entry.sha,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
commit,
|
|
61
|
+
files,
|
|
62
|
+
entries,
|
|
63
|
+
version: readPipelineVersion(policy, files),
|
|
64
|
+
sourceTimestamp: commit.committer.date,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function materialize(plan) {
|
|
68
|
+
verifyPipelinePublicationPlan(plan);
|
|
69
|
+
const input = await inspect(plan.source, plan.versionPolicy);
|
|
70
|
+
if (input.version !== plan.candidateVersion)
|
|
71
|
+
throw new Error("Version material changed after planning");
|
|
72
|
+
const material = materializePipelineVersion(
|
|
73
|
+
plan.versionPolicy,
|
|
74
|
+
input.files,
|
|
75
|
+
plan.version,
|
|
76
|
+
);
|
|
77
|
+
let source = plan.source;
|
|
78
|
+
if (material.changes.length)
|
|
79
|
+
source = await createMaterializedSource(plan, input, material);
|
|
80
|
+
const body = {
|
|
81
|
+
schema: "buildchain.pipeline-version-materialization/v1",
|
|
82
|
+
planRoot: plan.root,
|
|
83
|
+
protectedSource: plan.source,
|
|
84
|
+
source,
|
|
85
|
+
material,
|
|
86
|
+
};
|
|
87
|
+
return { ...body, root: recordDigest(body) };
|
|
88
|
+
}
|
|
89
|
+
async function materializeDevelopment(plan) {
|
|
90
|
+
const input = await inspect(plan.source, plan.versionPolicy);
|
|
91
|
+
const material = materializePipelineVersion(
|
|
92
|
+
plan.versionPolicy,
|
|
93
|
+
input.files,
|
|
94
|
+
plan.version,
|
|
95
|
+
);
|
|
96
|
+
const branch = `feature/buildchain-next/${plan.root.slice(7)}`;
|
|
97
|
+
const source = material.changes.length
|
|
98
|
+
? await createMaterializedSource(plan, input, material, `heads/${branch}`)
|
|
99
|
+
: plan.source;
|
|
100
|
+
const body = {
|
|
101
|
+
schema: "buildchain.pipeline-development-materialization/v1",
|
|
102
|
+
transitionRoot: plan.root,
|
|
103
|
+
protectedSource: plan.source,
|
|
104
|
+
source,
|
|
105
|
+
branch,
|
|
106
|
+
material,
|
|
107
|
+
};
|
|
108
|
+
return { ...body, root: recordDigest(body) };
|
|
109
|
+
}
|
|
110
|
+
async function createMaterializedSource(plan, input, material, targetRef) {
|
|
111
|
+
const changes = material.changes.map(({ path, content }) => ({
|
|
112
|
+
path,
|
|
113
|
+
content,
|
|
114
|
+
mode: input.entries[path].mode,
|
|
115
|
+
type: "blob",
|
|
116
|
+
}));
|
|
117
|
+
const tree = await post(`${base}/git/trees`, {
|
|
118
|
+
base_tree: plan.source.tree,
|
|
119
|
+
tree: changes,
|
|
120
|
+
});
|
|
121
|
+
const identity = {
|
|
122
|
+
name: "Buildchain automation",
|
|
123
|
+
email: "noreply@github.com",
|
|
124
|
+
date: plan.sourceTimestamp,
|
|
125
|
+
};
|
|
126
|
+
const parents = [plan.source.commit];
|
|
127
|
+
// Preserve the prior published version overlay as real Git ancestry. The
|
|
128
|
+
// resulting tree still changes only the current protected version fields;
|
|
129
|
+
// no provider force-update is needed for the next stable floating channel.
|
|
130
|
+
if (
|
|
131
|
+
plan.previousChannelCommit &&
|
|
132
|
+
plan.previousChannelCommit !== plan.source.commit
|
|
133
|
+
) {
|
|
134
|
+
if (!/^[0-9a-f]{40}$/u.test(plan.previousChannelCommit))
|
|
135
|
+
throw new Error(
|
|
136
|
+
"Version overlay parent must be an exact prior channel commit",
|
|
137
|
+
);
|
|
138
|
+
const previous = await request(
|
|
139
|
+
`${base}/git/commits/${plan.previousChannelCommit}`,
|
|
140
|
+
);
|
|
141
|
+
if (previous.sha !== plan.previousChannelCommit)
|
|
142
|
+
throw new Error("Prior version overlay commit changed");
|
|
143
|
+
parents.push(previous.sha);
|
|
144
|
+
}
|
|
145
|
+
const created = await post(`${base}/git/commits`, {
|
|
146
|
+
message: `chore(release): materialize ${plan.version}\n\nBuildchain-Plan: ${plan.root}\nSigned-off-by: Buildchain automation <noreply@github.com>`,
|
|
147
|
+
tree: tree.sha,
|
|
148
|
+
parents,
|
|
149
|
+
author: identity,
|
|
150
|
+
committer: identity,
|
|
151
|
+
});
|
|
152
|
+
const commit = await request(`${base}/git/commits/${created.sha}`);
|
|
153
|
+
if (
|
|
154
|
+
commit.tree?.sha !== tree.sha ||
|
|
155
|
+
recordDigest(commit.parents?.map(({ sha }) => sha)) !==
|
|
156
|
+
recordDigest(parents)
|
|
157
|
+
)
|
|
158
|
+
throw new Error("Materialized version commit readback mismatch");
|
|
159
|
+
const source = { ...plan.source, commit: commit.sha, tree: tree.sha };
|
|
160
|
+
const verified = await inspect(source, plan.versionPolicy);
|
|
161
|
+
if (verified.version !== plan.version)
|
|
162
|
+
throw new Error("Version material did not retain the planned version");
|
|
163
|
+
const expectedFiles = {
|
|
164
|
+
...input.files,
|
|
165
|
+
...Object.fromEntries(
|
|
166
|
+
material.changes.map(({ path, content }) => [path, content]),
|
|
167
|
+
),
|
|
168
|
+
};
|
|
169
|
+
if (recordDigest(verified.files) !== recordDigest(expectedFiles))
|
|
170
|
+
throw new Error(
|
|
171
|
+
"Version material changed document bytes beyond the planned fields",
|
|
172
|
+
);
|
|
173
|
+
const comparison = await request(
|
|
174
|
+
`${base}/compare/${plan.source.commit}...${commit.sha}`,
|
|
175
|
+
);
|
|
176
|
+
const paths = comparison.files?.map((file) => file.filename).sort();
|
|
177
|
+
if (
|
|
178
|
+
comparison.status !== "ahead" ||
|
|
179
|
+
(parents.length === 1 && comparison.total_commits !== 1) ||
|
|
180
|
+
recordDigest(paths) !==
|
|
181
|
+
recordDigest(material.changes.map(({ path }) => path).sort()) ||
|
|
182
|
+
comparison.files.some((file) => file.status !== "modified")
|
|
183
|
+
)
|
|
184
|
+
throw new Error(
|
|
185
|
+
"Version material changed paths outside the declared version fields",
|
|
186
|
+
);
|
|
187
|
+
const ref =
|
|
188
|
+
targetRef || `heads/buildchain/publication-source/${plan.root.slice(7)}`;
|
|
189
|
+
try {
|
|
190
|
+
await post(`${base}/git/refs`, { ref: `refs/${ref}`, sha: commit.sha });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
const observed = await request(`${base}/git/ref/${ref}`);
|
|
193
|
+
if (observed.object?.sha !== commit.sha) throw error;
|
|
194
|
+
}
|
|
195
|
+
const observed = await request(`${base}/git/ref/${ref}`);
|
|
196
|
+
if (observed.object?.sha !== commit.sha)
|
|
197
|
+
throw new Error("Version material ref readback mismatch");
|
|
198
|
+
return source;
|
|
199
|
+
}
|
|
200
|
+
return { inspect, materialize, materializeDevelopment };
|
|
201
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { object } from "../../consumer/contract/shape.js";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
|
|
4
|
+
export const PIPELINE_WORKER = "buildchain.pipeline-worker/v1";
|
|
5
|
+
|
|
6
|
+
export function validatePipelineWorker(binding, current, warrant) {
|
|
7
|
+
object(binding, [
|
|
8
|
+
"schema",
|
|
9
|
+
"repository",
|
|
10
|
+
"attempt",
|
|
11
|
+
"generation",
|
|
12
|
+
"sourceHead",
|
|
13
|
+
"candidateId",
|
|
14
|
+
"fencingToken",
|
|
15
|
+
"leaseGeneration",
|
|
16
|
+
"runId",
|
|
17
|
+
"runAttempt",
|
|
18
|
+
"nativeJobId",
|
|
19
|
+
"sealJobId",
|
|
20
|
+
]);
|
|
21
|
+
if (
|
|
22
|
+
binding.schema !== PIPELINE_WORKER ||
|
|
23
|
+
binding.repository !== current.intent.repository ||
|
|
24
|
+
binding.attempt !== current.identity.id ||
|
|
25
|
+
binding.generation !== current.generation.id ||
|
|
26
|
+
binding.sourceHead !== current.generation.source.commit ||
|
|
27
|
+
binding.candidateId !== warrant.candidateId ||
|
|
28
|
+
binding.fencingToken !== warrant.fencingToken ||
|
|
29
|
+
binding.leaseGeneration !== warrant.generation
|
|
30
|
+
)
|
|
31
|
+
throw new Error("Native worker does not bind the active pipeline Warrant");
|
|
32
|
+
for (const key of [
|
|
33
|
+
"runId",
|
|
34
|
+
"runAttempt",
|
|
35
|
+
"nativeJobId",
|
|
36
|
+
"sealJobId",
|
|
37
|
+
"leaseGeneration",
|
|
38
|
+
])
|
|
39
|
+
if (!Number.isSafeInteger(binding[key]) || binding[key] < 1)
|
|
40
|
+
throw new Error("Native worker provider identity is invalid");
|
|
41
|
+
if (binding.nativeJobId === binding.sealJobId)
|
|
42
|
+
throw new Error("Native execution and evidence seal must be separate jobs");
|
|
43
|
+
return binding;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function githubPipelineWorker(request) {
|
|
47
|
+
async function jobs(base, binding) {
|
|
48
|
+
const result = [];
|
|
49
|
+
for (let page = 1; page <= 100; page++) {
|
|
50
|
+
const response = await request(
|
|
51
|
+
`${base}/actions/runs/${binding.runId}/attempts/${binding.runAttempt}/jobs?per_page=100&page=${page}`,
|
|
52
|
+
);
|
|
53
|
+
if (!Array.isArray(response.jobs))
|
|
54
|
+
throw new Error("Missing provider jobs readback");
|
|
55
|
+
result.push(...response.jobs);
|
|
56
|
+
if (response.jobs.length < 100) return result;
|
|
57
|
+
}
|
|
58
|
+
throw new Error("Pipeline worker jobs exceed the readback bound");
|
|
59
|
+
}
|
|
60
|
+
async function observe(binding, current, warrant) {
|
|
61
|
+
validatePipelineWorker(binding, current, warrant);
|
|
62
|
+
const base = `/repos/${binding.repository}`;
|
|
63
|
+
const run = await request(`${base}/actions/runs/${binding.runId}`);
|
|
64
|
+
if (
|
|
65
|
+
run.id !== binding.runId ||
|
|
66
|
+
run.run_attempt !== binding.runAttempt ||
|
|
67
|
+
run.repository?.full_name !== binding.repository
|
|
68
|
+
)
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Native worker provider run changed; terminality is unproved",
|
|
71
|
+
);
|
|
72
|
+
const entries = await jobs(base, binding);
|
|
73
|
+
const selected = [binding.nativeJobId, binding.sealJobId].map((id) => {
|
|
74
|
+
const matches = entries.filter((job) => job.id === id);
|
|
75
|
+
if (
|
|
76
|
+
matches.length !== 1 ||
|
|
77
|
+
matches[0].run_id !== binding.runId ||
|
|
78
|
+
matches[0].run_attempt !== binding.runAttempt
|
|
79
|
+
)
|
|
80
|
+
throw new Error("Native worker exact job identity drift");
|
|
81
|
+
return matches[0];
|
|
82
|
+
});
|
|
83
|
+
const again = await request(`${base}/actions/runs/${binding.runId}`);
|
|
84
|
+
if (
|
|
85
|
+
again.id !== run.id ||
|
|
86
|
+
again.run_attempt !== run.run_attempt ||
|
|
87
|
+
again.status !== run.status ||
|
|
88
|
+
again.conclusion !== run.conclusion
|
|
89
|
+
)
|
|
90
|
+
throw new Error("Native worker changed during terminal readback");
|
|
91
|
+
const terminal =
|
|
92
|
+
run.status === "completed" &&
|
|
93
|
+
Boolean(run.conclusion) &&
|
|
94
|
+
selected.every(
|
|
95
|
+
(job) =>
|
|
96
|
+
job.status === "completed" && job.conclusion && job.completed_at,
|
|
97
|
+
);
|
|
98
|
+
const body = {
|
|
99
|
+
schema: "buildchain.pipeline-worker-readback/v1",
|
|
100
|
+
binding,
|
|
101
|
+
status: terminal ? "completed" : "in_progress",
|
|
102
|
+
run,
|
|
103
|
+
jobs: selected,
|
|
104
|
+
};
|
|
105
|
+
return { ...body, root: recordDigest(body) };
|
|
106
|
+
}
|
|
107
|
+
return { observe };
|
|
108
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readNpmPackResult } from "../npm/pack-result.js";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -117,7 +118,7 @@ export function hydratePublishedPublicationRegistry({
|
|
|
117
118
|
for (const version of publishedVersions) {
|
|
118
119
|
const packageDir = path.join(resolvedOutputDir, `.package-${version}`);
|
|
119
120
|
fs.mkdirSync(packageDir, { recursive: true });
|
|
120
|
-
const
|
|
121
|
+
const pack = readNpmPackResult(
|
|
121
122
|
commandRunner(
|
|
122
123
|
"npm",
|
|
123
124
|
[
|
|
@@ -132,7 +133,6 @@ export function hydratePublishedPublicationRegistry({
|
|
|
132
133
|
{ cwd: resolvedCwd },
|
|
133
134
|
),
|
|
134
135
|
);
|
|
135
|
-
const pack = Array.isArray(packed) ? packed[0] : packed;
|
|
136
136
|
if (!pack?.filename || !pack?.integrity) {
|
|
137
137
|
throw new Error(
|
|
138
138
|
`npm pack did not report filename and integrity for ${packageName}@${version}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { readNpmPackResult } from "./pack-result.js";
|
|
1
2
|
export function summarizePackPreview(stdout) {
|
|
2
|
-
const
|
|
3
|
-
const pack = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
3
|
+
const pack = readNpmPackResult(stdout);
|
|
4
4
|
const files = Array.isArray(pack?.files) ? pack.files : [];
|
|
5
5
|
const bin = files.find((file) => file.path === "bin/buildchain.mjs");
|
|
6
6
|
const fileEntries = files.map((file) => ({
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function readNpmPackResult(stdout) {
|
|
4
|
+
const parsed = JSON.parse(stdout);
|
|
5
|
+
const entries = Array.isArray(parsed)
|
|
6
|
+
? parsed
|
|
7
|
+
: typeof parsed?.name === "string"
|
|
8
|
+
? [parsed]
|
|
9
|
+
: parsed && typeof parsed === "object"
|
|
10
|
+
? Object.values(parsed)
|
|
11
|
+
: [];
|
|
12
|
+
if (entries.length !== 1)
|
|
13
|
+
throw new Error("npm pack must return exactly one package");
|
|
14
|
+
const pack = entries[0];
|
|
15
|
+
if (
|
|
16
|
+
typeof pack?.name !== "string" ||
|
|
17
|
+
!pack.name ||
|
|
18
|
+
typeof pack?.version !== "string" ||
|
|
19
|
+
!pack.version
|
|
20
|
+
) {
|
|
21
|
+
throw new Error("npm pack did not return package name and version");
|
|
22
|
+
}
|
|
23
|
+
if (
|
|
24
|
+
typeof pack.filename !== "string" ||
|
|
25
|
+
!pack.filename.endsWith(".tgz") ||
|
|
26
|
+
/[/\\]/u.test(pack.filename) ||
|
|
27
|
+
path.basename(pack.filename) !== pack.filename
|
|
28
|
+
)
|
|
29
|
+
throw new Error("npm pack must return a local tarball filename");
|
|
30
|
+
return pack;
|
|
31
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readNpmPackResult } from "./pack-result.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
@@ -11,12 +12,8 @@ export function readPackageJson(cwd) {
|
|
|
11
12
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
function parsePackResult(stdout) {
|
|
15
|
-
const
|
|
16
|
-
const pack = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
17
|
-
if (!pack?.name || !pack?.version) {
|
|
18
|
-
throw new Error("npm pack did not return package name and version");
|
|
19
|
-
}
|
|
15
|
+
export function parsePackResult(stdout) {
|
|
16
|
+
const pack = readNpmPackResult(stdout);
|
|
20
17
|
return {
|
|
21
18
|
name: pack.name,
|
|
22
19
|
version: pack.version,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const REGISTRY = "https://registry.npmjs.org";
|
|
2
|
+
|
|
3
|
+
export function pipelineNpmChannel({
|
|
4
|
+
environment = process.env,
|
|
5
|
+
fetchImpl = globalThis.fetch,
|
|
6
|
+
}) {
|
|
7
|
+
async function json(url, options = {}) {
|
|
8
|
+
const response = await fetchImpl(url, { ...options, redirect: "error" });
|
|
9
|
+
if (!response.ok)
|
|
10
|
+
throw new Error(
|
|
11
|
+
`npm channel provider rejected the operation with HTTP ${response.status}`,
|
|
12
|
+
);
|
|
13
|
+
return response.json();
|
|
14
|
+
}
|
|
15
|
+
async function credential(name) {
|
|
16
|
+
if (environment.NODE_AUTH_TOKEN) return environment.NODE_AUTH_TOKEN;
|
|
17
|
+
if (
|
|
18
|
+
!environment.ACTIONS_ID_TOKEN_REQUEST_URL ||
|
|
19
|
+
!environment.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
|
20
|
+
)
|
|
21
|
+
throw new Error(
|
|
22
|
+
"npm channel publication requires its hosted package-scoped OIDC or configured publisher credential",
|
|
23
|
+
);
|
|
24
|
+
const url = new URL(environment.ACTIONS_ID_TOKEN_REQUEST_URL);
|
|
25
|
+
if (url.protocol !== "https:")
|
|
26
|
+
throw new Error("OIDC request requires the provider HTTPS endpoint");
|
|
27
|
+
url.searchParams.set("audience", "npm:registry.npmjs.org");
|
|
28
|
+
const identity = await json(url.href, {
|
|
29
|
+
headers: {
|
|
30
|
+
authorization: `Bearer ${environment.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`,
|
|
31
|
+
accept: "application/json",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (typeof identity.value !== "string" || !identity.value)
|
|
35
|
+
throw new Error("Hosted OIDC provider omitted its identity token");
|
|
36
|
+
const exchange = await json(
|
|
37
|
+
`${REGISTRY}/-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(name)}`,
|
|
38
|
+
{
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
authorization: `Bearer ${identity.value}`,
|
|
42
|
+
accept: "application/json",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
if (typeof exchange.token !== "string" || !exchange.token)
|
|
47
|
+
throw new Error(
|
|
48
|
+
"npm OIDC exchange omitted its package-scoped credential",
|
|
49
|
+
);
|
|
50
|
+
return exchange.token;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
async observe(effect) {
|
|
54
|
+
if (effect.access === "restricted" && !environment.NODE_AUTH_TOKEN)
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Restricted npm readback requires a configured package read credential",
|
|
57
|
+
);
|
|
58
|
+
const headers = environment.NODE_AUTH_TOKEN
|
|
59
|
+
? { authorization: `Bearer ${environment.NODE_AUTH_TOKEN}` }
|
|
60
|
+
: {};
|
|
61
|
+
const tags = await json(
|
|
62
|
+
`${REGISTRY}/-/package/${encodeURIComponent(effect.name)}/dist-tags`,
|
|
63
|
+
{ headers },
|
|
64
|
+
);
|
|
65
|
+
const version = tags[effect.tag] || null;
|
|
66
|
+
if (version !== null && typeof version !== "string")
|
|
67
|
+
throw new Error("npm channel has no exact version");
|
|
68
|
+
return version
|
|
69
|
+
? { state: "present", version }
|
|
70
|
+
: { state: "absent", version: null };
|
|
71
|
+
},
|
|
72
|
+
async apply(effect) {
|
|
73
|
+
const token = await credential(effect.name);
|
|
74
|
+
await json(
|
|
75
|
+
`${REGISTRY}/-/package/${encodeURIComponent(effect.name)}/dist-tags/${encodeURIComponent(effect.tag)}`,
|
|
76
|
+
{
|
|
77
|
+
method: "PUT",
|
|
78
|
+
headers: {
|
|
79
|
+
authorization: `Bearer ${token}`,
|
|
80
|
+
"content-type": "application/json",
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(effect.version),
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|