@kungfu-tech/buildchain 4.1.3-alpha.6 → 4.1.3-alpha.8
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/decisions/0008-pipeline-product-publication.md +29 -8
- package/architecture/maintainability-policy.json +4 -4
- package/dist/site/buildchain-contract.json +2 -2
- package/dist/site/buildchain-site.json +1 -1
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/public-surface-audit.json +1 -1
- package/dist/site/publication-registry.json +1 -1
- package/dist/site/site-manifest.json +1 -1
- package/dist/site/workflow-registry.json +1 -1
- package/package.json +1 -1
- package/packages/core/providers/github/pipeline-product-release.js +10 -3
- package/packages/core/publication/npm/pipeline-channel.js +0 -51
- package/packages/core/publication/npm/pipeline-provider.js +29 -1
- package/packages/core/publication/pipeline/apply.js +2 -1
- package/packages/core/publication/pipeline/capsules.js +4 -1
- package/packages/core/publication/pipeline/distribution.js +29 -23
- package/packages/core/publication/pipeline/effects.js +34 -4
- package/packages/core/publication/pipeline/settle.js +0 -1
- package/packages/core/workflow/attempt/journal.js +11 -9
- package/packages/core/workflow/pipeline/recovery-integration.js +14 -1
|
@@ -8,12 +8,12 @@ confidence: medium
|
|
|
8
8
|
sensitivity: public
|
|
9
9
|
evidence_grade: B
|
|
10
10
|
review_state: unreviewed
|
|
11
|
-
last_reviewed: 2026-09-
|
|
11
|
+
last_reviewed: 2026-09-18
|
|
12
12
|
ai_provenance:
|
|
13
13
|
model_family: GPT-6
|
|
14
14
|
product: Codex
|
|
15
|
-
generated_at: 2026-09-
|
|
16
|
-
visible_context: Product contract, hosted pipeline, provider
|
|
15
|
+
generated_at: 2026-09-18
|
|
16
|
+
visible_context: Product contract, hosted pipeline, npm provider documentation, publication readback and local failure-path tests.
|
|
17
17
|
invisible_context_boundary: No published entry qualification, production publication or external consumer adoption is established by this document.
|
|
18
18
|
---
|
|
19
19
|
|
|
@@ -61,12 +61,24 @@ before effects and retains exact successful readback before moving on. npm uses
|
|
|
61
61
|
the sealed tarball, ignores lifecycle scripts and requires the registry integrity
|
|
62
62
|
to match. GitHub assets and exact tags are never replaced. A lost response is
|
|
63
63
|
reconciled from current provider state; completed packages are not republished.
|
|
64
|
+
The npm publish command sets `alpha` for prereleases or `latest` for stable
|
|
65
|
+
releases directly. There is no temporary npm tag or subsequent npm channel
|
|
66
|
+
mutation. A partially completed multi-provider release resumes its missing
|
|
67
|
+
effects without repeating an already published package.
|
|
64
68
|
The Release contains product assets requested by TOML, its Passport, qualification,
|
|
65
69
|
Capsule aggregate, invocation and attestation bundle.
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
An accepted npm publication can remain unavailable while the registry scans it.
|
|
72
|
+
The publisher permits fifteen minutes of bounded readback delays after a
|
|
73
|
+
successful publish, retaining the writer fence before every observation. It
|
|
74
|
+
never repeats the publish command while waiting. A conflicting integrity fails
|
|
75
|
+
immediately; an unavailable version keeps its pending receipt for normal attempt
|
|
76
|
+
recovery. This accounts for npm's documented
|
|
77
|
+
[publish-time scanning delay](https://github.blog/changelog/2026-07-28-npm-publish-time-malware-scanning-and-dual-use-metadata/).
|
|
78
|
+
|
|
79
|
+
Publication success does not finish the business attempt. Distribution moves
|
|
80
|
+
only the major Git channel, with exact prior readback and no forced Git update.
|
|
81
|
+
It neither queries nor modifies npm for new publications. Divergent Git channel history stops for
|
|
70
82
|
source reconciliation. Alpha then prepares the next development version through
|
|
71
83
|
an ordinary protected PR. Its original publication and receipts remain successful
|
|
72
84
|
while that PR waits for review, queue integration or verification. Anchored
|
|
@@ -78,13 +90,22 @@ proof and cannot regress its version. Stable and Alpha retain their distinct
|
|
|
78
90
|
transition identities and the original successful publication.
|
|
79
91
|
|
|
80
92
|
Provider authorization is a one-time repository setup. Hosted npm trusted
|
|
81
|
-
publishing
|
|
82
|
-
|
|
93
|
+
publishing authorizes package publication together with its final dist-tag; no
|
|
94
|
+
npm distribution credential is required. `BUILDCHAIN_NPM_TOKEN` remains optional
|
|
95
|
+
for consumers using token-based publication or restricted-package readback.
|
|
96
|
+
Internal PR creation uses the repository's automation App
|
|
83
97
|
(`BUILDCHAIN_APP_CLIENT_ID` variable and `BUILDCHAIN_APP_PRIVATE_KEY` secret), or
|
|
84
98
|
`BUILDCHAIN_AUTOMATION_TOKEN`. It does not use `GITHUB_TOKEN` to create a PR whose
|
|
85
99
|
ordinary checks would be suppressed. No such credential reaches product commands.
|
|
86
100
|
Review and branch protections still apply to generated PRs.
|
|
87
101
|
|
|
102
|
+
The earlier two-step npm design left immutable publication and distribution
|
|
103
|
+
receipts. Recovery preserves those exact effect identities. Historical channel
|
|
104
|
+
effects are read-only: an already published version with a missing channel needs
|
|
105
|
+
a one-time authenticated tag correction, after which ordinary attempt recovery
|
|
106
|
+
can finish without changing package bytes or retaining a CI write token. New
|
|
107
|
+
publications never create those historical channel effects.
|
|
108
|
+
|
|
88
109
|
The local tests cover real npm packing, native archives and PDFs, source and
|
|
89
110
|
version drift, independent provider inventory, signature-verifier rejection,
|
|
90
111
|
immutable retention, lost provider responses and protected next-development
|
|
@@ -27,16 +27,16 @@
|
|
|
27
27
|
"repositoryBudgets": {
|
|
28
28
|
"baselineRevision": "9569f69c2cb609372b8a3d65ce5dec8fb7b34a84",
|
|
29
29
|
"maxHandMaintainedSourceFiles": 1479,
|
|
30
|
-
"maxHandMaintainedSourceLines":
|
|
30
|
+
"maxHandMaintainedSourceLines": 218720,
|
|
31
31
|
"maxWorkflowFiles": 44,
|
|
32
32
|
"maxWorkflowLines": 8029,
|
|
33
|
-
"latestTransition": "
|
|
34
|
-
"latestTransitionRationale": "Charge
|
|
33
|
+
"latestTransition": "Bounded provider readback and safe npm publication failure diagnostics.",
|
|
34
|
+
"latestTransitionRationale": "Charge sixteen source lines for bounded read-only reconciliation after one immutable write, plus twenty-eight measured source lines for fixed-allowlist npm failure codes. Keep one sealed npm publish invocation, exclude raw stdout/stderr and credentials from reported errors, and preserve all per-file, function, complexity and exception limits.",
|
|
35
35
|
"rationale": "Bounded modules separate candidate, evidence, native proof and execution, canonical failure verification, closed-set execution transfer, process-boundary verification, Warrant state and qualification, authority state, qualification, Landing acquisition, verified-attempt admission, Landing settlement and fence validation, provider attempt and heartbeat readback, sealed terminal evidence, hosted heartbeat, resume, store, options, command-specific adapters, and CLI orchestration responsibilities. The protected delivery workflow gives candidate and seal jobs no provider write credential, recursively rejects transfer drift, scopes every transfer artifact to the exact provider run attempt, chains every durable heartbeat state and receipt root on a distinct hosted runner domain, and permits qualification or exact failure settlement only after the finalizer verifies live jobs and the latest durable authority state. Public Landing admission and heartbeat always perform non-injectable fresh exact-attempt provider readback, tolerate only a not-yet-materialized seal job while retaining exact native and duplicate-job rejection, and heartbeat-loss coordination never invokes GitHub's run-scoped cancellation API where a successor rerun could race. The hosted process boundary anchors trust in the kernel-resolved versioned Runner.Worker path while accepting an equivalent argv-zero spelling with the same basename; its public boundary marker is allowed only at the one exact non-secret value, so a forged value under the same credential-like name still fails closed. The credentialless transfer seal validates the rooted native reuse decision against the Warrant-bound source, closure, dependency, toolchain, environment and command inputs; qualifiedBase stays in the proof and decision where it is rooted and later independently rechecked, rather than being assumed on the Warrant schema. V2 mutation cannot initialize a second empty authority beside a legacy ref: migration reads and roots the canonical live v1 ref, rechecks it, applies the explicit configured v2 policy, and atomically replaces its bytes through one expected-old non-force update, so old v1 controllers fail closed while the prior commit remains immutable history. Candidate ancestry classifies the GitHub artifact runtime token as credential authority. Every Landing settlement, including an unexpired fence, requires product-owned terminal readback. Terminal cleanup reads the immutable historical attempt, derives workflow and hosted Landing job identity from live run data, reads the exact protected ref head and compares containment against that immutable SHA, and seals merged only when the admitted merge-group head is contained by that protected base; a later different-head merge settles the old attempt as dequeued. Terminal normalization preserves transfer, finalizer boundary, native and seal job, admitted provider attempt, protected-base head, containment result, and terminal readback coordinates. The public reusable workflow truthfully retains its single-flight v1 authority without advertising v2 migration, while bounded v2 remains a separate opt-in CLI, Node API, and schema contract; the tracked self-caller stays on public v4-alpha with matching stable and alpha locks, and transient train selection is dispatch-only. Next-development parity adds one versioned transition authority, one durable controller, one bounded reservation adapter, and pure public-contract recovery evidence while retaining the existing thin floating-channel callers byte-for-byte. The floating-consumer gate reuses the existing contract-world evaluator so SHA drift passes only under the selected lock's compatibility policy, breaking surface drift still fails closed, and external certification reconstructs the receipt from the observed workflow SHA rather than the lock's historical evidence SHA. Tail reseal adds no Stage Capsule effect or general rebuild authority: it verifies the exact retained four-platform bytes, admits one known macOS tail failure through live provider readback, fences signing and finalization to explicit credentials and Warrant identity, and requires standard candidate Passport plus provider and protected readback. Compatibility Facts parity isolates canonical encoding, temporal verification, lifecycle validation, fixture projection, public authority projection, and CLI adaptation into bounded modules; legacy digest and proof arrays are derived only from exact current Fact lineage. Publication Rehearsal adds no production release authority: the content-addressed capsule contract binds source, candidate, manifest, config, canonical data-only provider bindings, provider policy, observations and core version separately from filesystem, runtime and rehearsal-only provider authority; simulate, replay and explicitly authorized provider rehearsal all reuse the release-tail planner, while same-commit local dogfood resolves the new reusable contract and exact current runtime before external callers remain on floating v4-alpha. Cross-runtime candidate recovery preserves original runtime and provider roots and creates resume lineage only after exact public ref, tag, ancestry, npm version and integrity readback. Adopter Delivery restores the reviewed public v3 gate, drivers, Passport binding, N-1 bootstrap and exact archive loader through one public v4 surface while keeping provider and release authority explicit non-claims. Floating Alpha bootstrap uses only the existing exact resolver and protected Alpha private shell to move an otherwise stale floating shell, retains public v4 stable promotion, and removes itself after protected readback. CI lane change-budget enforcement adds a bounded parser, evaluator, guard, and tests without creating another merge authority. Minimal consumer contract adds exactly seven bounded contract modules, one generated-contract gate and six product build/test example sources; the source-file ceiling increases by fourteen for this reviewed feature slice. Existing per-file, function, complexity and debt limits are unchanged. The final readable template sources require exact source-line and total-automation ceilings of 203394 and 224451 respectively; this charges all generated example sources rather than hiding them from measurement. Qualified landing status projection adds 51 source lines and four internal action-contract lines within existing job permissions; per-file, function, complexity and exception budgets are unchanged. Delivery scheduling preserves its exact queued or running provider owner before Warrant acquisition, adding 31 measured source lines within the existing structural budgets.",
|
|
36
36
|
"governance": "legacy-debt-sunset-2026q4",
|
|
37
37
|
"maxActionDefinitions": 283,
|
|
38
38
|
"maxActionDefinitionLines": 12399,
|
|
39
|
-
"maxAutomationImplementationLines":
|
|
39
|
+
"maxAutomationImplementationLines": 238300
|
|
40
40
|
},
|
|
41
41
|
"sourceBudgets": {
|
|
42
42
|
"newFileLines": 600,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"product": {
|
|
5
5
|
"name": "Buildchain",
|
|
6
6
|
"package": "@kungfu-tech/buildchain",
|
|
7
|
-
"version": "4.1.3-alpha.
|
|
7
|
+
"version": "4.1.3-alpha.8",
|
|
8
8
|
"repository": "https://github.com/kungfu-systems/buildchain"
|
|
9
9
|
},
|
|
10
10
|
"majorLine": "v4",
|
|
@@ -2356,5 +2356,5 @@
|
|
|
2356
2356
|
"compatibilityFactCutRoot": "sha256:58f33e498596505a26b6fc3c7ae5acef847ff699eee6c7e17a7b318a40bdf0d6",
|
|
2357
2357
|
"compatibilityProofRegistryRoot": "sha256:2f6477405c7891b754b33ad6515d55618bd320e7cdc59330d7cc645e17201e72",
|
|
2358
2358
|
"compatibilityDigest": "sha256:81e32696723fe82fe29d2b2214cd56cf6c87bbeae5cff1a201de7ee7fd10ee9c",
|
|
2359
|
-
"contractDigest": "sha256:
|
|
2359
|
+
"contractDigest": "sha256:834eb062f357b24ca6f46a4e62ef48c2809361286c08e50780d8a4d1f0991447"
|
|
2360
2360
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"contract": "kungfu-buildchain-public-surface-reverse-audit",
|
|
22
22
|
"path": "dist/site/public-surface-audit.json",
|
|
23
23
|
"status": "passed",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "46693c0dc7f2c1aaa7d259df3145f8c1c8bf7910b0e2f28bbbd7155d9cbd234a",
|
|
25
25
|
"summary": {
|
|
26
26
|
"cliCommandCount": 117,
|
|
27
27
|
"workflowCount": 44,
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
"contract": "kungfu-buildchain-public-surface-reverse-audit",
|
|
247
247
|
"path": "dist/site/public-surface-audit.json",
|
|
248
248
|
"status": "passed",
|
|
249
|
-
"sha256": "
|
|
249
|
+
"sha256": "46693c0dc7f2c1aaa7d259df3145f8c1c8bf7910b0e2f28bbbd7155d9cbd234a",
|
|
250
250
|
"summary": {
|
|
251
251
|
"cliCommandCount": 117,
|
|
252
252
|
"workflowCount": 44,
|
|
@@ -10814,7 +10814,7 @@
|
|
|
10814
10814
|
"workflowRegistryPath": "dist/site/workflow-registry.json",
|
|
10815
10815
|
"pageRegistryPath": "dist/site/page-registry.json",
|
|
10816
10816
|
"cliRegistryDigest": "e43fdb0289df88871f3f0febbe04dcb9311a252a2b1df9b3a83a2d10c9d4a70f",
|
|
10817
|
-
"workflowRegistryDigest": "
|
|
10817
|
+
"workflowRegistryDigest": "380c2eb00655d9a8928765aa836dd37b99543d492daadc50fb9c8faf94bb9469",
|
|
10818
10818
|
"pageRegistryDigest": "80d9a72bc3833c1f9ec4ad39f3dbab4ebce9b51c795d390a3526acf904054eb7"
|
|
10819
10819
|
},
|
|
10820
10820
|
"comparison": {
|
|
@@ -1263,7 +1263,7 @@
|
|
|
1263
1263
|
"status": "preview",
|
|
1264
1264
|
"owner": "buildchain-workflows",
|
|
1265
1265
|
"maturity": "preview",
|
|
1266
|
-
"introducedVersion": "4.1.3-alpha.
|
|
1266
|
+
"introducedVersion": "4.1.3-alpha.8",
|
|
1267
1267
|
"compatibilityPromise": "current-contract-only",
|
|
1268
1268
|
"deprecationReplacement": "",
|
|
1269
1269
|
"sunsetCondition": "explicit-contract-change-review",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "4.1.3-alpha.
|
|
3
|
+
"version": "4.1.3-alpha.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { setTimeout } from "node:timers/promises";
|
|
2
3
|
import { pipelineProductPayload } from "./pipeline-product-payload.js";
|
|
3
4
|
|
|
4
5
|
const sha256 = (bytes) =>
|
|
@@ -48,6 +49,7 @@ export function githubPipelineProductRelease({
|
|
|
48
49
|
throw new Error("Release requires the exact immutable publication tag");
|
|
49
50
|
let value = await findRelease();
|
|
50
51
|
if (!value && create) {
|
|
52
|
+
let failure;
|
|
51
53
|
try {
|
|
52
54
|
await github.rest.repos.createRelease({
|
|
53
55
|
owner,
|
|
@@ -61,12 +63,17 @@ export function githubPipelineProductRelease({
|
|
|
61
63
|
make_latest: "false",
|
|
62
64
|
});
|
|
63
65
|
} catch (error) {
|
|
66
|
+
failure = error;
|
|
67
|
+
}
|
|
68
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
69
|
+
if (attempt) await setTimeout(1000);
|
|
64
70
|
value = await findRelease();
|
|
65
|
-
if (
|
|
71
|
+
if (value) break;
|
|
66
72
|
}
|
|
67
|
-
value = await findRelease();
|
|
68
73
|
if (!value)
|
|
69
|
-
throw
|
|
74
|
+
throw (
|
|
75
|
+
failure || new Error("Release creation lacks exact provider readback")
|
|
76
|
+
);
|
|
70
77
|
}
|
|
71
78
|
if (
|
|
72
79
|
value &&
|
|
@@ -12,43 +12,6 @@ export function pipelineNpmChannel({
|
|
|
12
12
|
);
|
|
13
13
|
return response.json();
|
|
14
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
15
|
return {
|
|
53
16
|
async observe(effect) {
|
|
54
17
|
if (effect.access === "restricted" && !environment.NODE_AUTH_TOKEN)
|
|
@@ -69,19 +32,5 @@ export function pipelineNpmChannel({
|
|
|
69
32
|
? { state: "present", version }
|
|
70
33
|
: { state: "absent", version: null };
|
|
71
34
|
},
|
|
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
35
|
};
|
|
87
36
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
4
|
import { publishedDigest, runNpm } from "./registry.js";
|
|
4
5
|
import { createNativeChildEnvironment } from "../../dev-delivery/native/execution.js";
|
|
5
6
|
import {
|
|
@@ -8,6 +9,33 @@ import {
|
|
|
8
9
|
writeImmutablePublicationFile,
|
|
9
10
|
} from "../pipeline/files.js";
|
|
10
11
|
|
|
12
|
+
function publicationFailureCode(result) {
|
|
13
|
+
const code = stripVTControlCharacters(String(result.stderr ?? "")).match(
|
|
14
|
+
/^npm (?:error|ERR!) code ([A-Z0-9_]+)\r?$/m,
|
|
15
|
+
)?.[1];
|
|
16
|
+
return [
|
|
17
|
+
"ENEEDAUTH",
|
|
18
|
+
"EOTP",
|
|
19
|
+
"E401",
|
|
20
|
+
"E403",
|
|
21
|
+
"E404",
|
|
22
|
+
"EPUBLISHCONFLICT",
|
|
23
|
+
"EPRIVATE",
|
|
24
|
+
"EINTEGRITY",
|
|
25
|
+
"EUSAGE",
|
|
26
|
+
"EINVALIDPACKAGENAME",
|
|
27
|
+
"ETARGET",
|
|
28
|
+
"ENOENT",
|
|
29
|
+
"ETIMEDOUT",
|
|
30
|
+
"ECONNRESET",
|
|
31
|
+
"EAI_AGAIN",
|
|
32
|
+
"ENETUNREACH",
|
|
33
|
+
"ENOTFOUND",
|
|
34
|
+
].includes(code)
|
|
35
|
+
? code
|
|
36
|
+
: "unclassified";
|
|
37
|
+
}
|
|
38
|
+
|
|
11
39
|
export function pipelineNpmEnvironment(directory, environment = process.env) {
|
|
12
40
|
const cwd = path.resolve(directory);
|
|
13
41
|
const config = path.join(cwd, ".buildchain-publisher.npmrc");
|
|
@@ -102,7 +130,7 @@ export function pipelineNpmProvider({
|
|
|
102
130
|
});
|
|
103
131
|
if (result.status !== 0)
|
|
104
132
|
throw new Error(
|
|
105
|
-
`Sealed npm publication failed with exit ${result.status}`,
|
|
133
|
+
`Sealed npm publication failed with exit ${result.status} (npm code ${publicationFailureCode(result)})`,
|
|
106
134
|
);
|
|
107
135
|
},
|
|
108
136
|
};
|
|
@@ -115,13 +115,14 @@ export async function applyPipelinePublication(
|
|
|
115
115
|
matches: (effect, value) => providerFor(effect).matches(effect, value),
|
|
116
116
|
apply: (effect) => providerFor(effect).apply(effect),
|
|
117
117
|
};
|
|
118
|
+
const receipts = await journal.materials("publication/effect/");
|
|
118
119
|
const effects = pipelinePublicationEffects({
|
|
119
120
|
plan,
|
|
120
121
|
qualified,
|
|
121
122
|
documents,
|
|
122
123
|
evidence: evidence.map(({ bytes, ...asset }) => asset),
|
|
124
|
+
receipts,
|
|
123
125
|
});
|
|
124
|
-
const receipts = await journal.materials("publication/effect/");
|
|
125
126
|
let fence = journal.fence;
|
|
126
127
|
if (context.recovery?.preserveTransaction) {
|
|
127
128
|
const readback = await observeRecoveryPublicationEffects({
|
|
@@ -72,7 +72,10 @@ export function pipelineProductCapsules({
|
|
|
72
72
|
identityRoot: stageCapsuleIdentityRoot(identity),
|
|
73
73
|
retentionPromise: {
|
|
74
74
|
class: "github-artifact",
|
|
75
|
-
retainUntil: bundle.providerArtifact.expires_at
|
|
75
|
+
retainUntil: bundle.providerArtifact.expires_at.replace(
|
|
76
|
+
/T(\d{2}:\d{2}:\d{2})Z$/u,
|
|
77
|
+
"T$1.000Z",
|
|
78
|
+
),
|
|
76
79
|
},
|
|
77
80
|
capsuleRoot: `sha256:${"0".repeat(64)}`,
|
|
78
81
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
2
|
import { pipelineNpmChannel } from "../npm/pipeline-channel.js";
|
|
3
|
-
import { applyPipelineEffects } from "./effects.js";
|
|
4
|
-
import { compareDevelopmentVersions } from "../publication-development.js";
|
|
3
|
+
import { applyPipelineEffects, pipelinePublicationEffects } from "./effects.js";
|
|
5
4
|
|
|
6
5
|
export async function distributePipelineProducts(
|
|
7
6
|
context,
|
|
@@ -9,12 +8,10 @@ export async function distributePipelineProducts(
|
|
|
9
8
|
journal,
|
|
10
9
|
retained,
|
|
11
10
|
environment,
|
|
12
|
-
directory,
|
|
13
11
|
) {
|
|
14
12
|
const { plan, materialization } = context;
|
|
15
13
|
const channel = `v${plan.version.split(".")[0]}${plan.channel === "alpha" ? "-alpha" : ""}`;
|
|
16
14
|
const refPath = `/repos/${host.repository}/git/ref/tags/${channel}`;
|
|
17
|
-
const npmTag = plan.channel === "alpha" ? "alpha" : "latest";
|
|
18
15
|
const npm = pipelineNpmChannel({ environment });
|
|
19
16
|
async function observe(effect) {
|
|
20
17
|
if (effect.kind === "git-channel") {
|
|
@@ -25,7 +22,14 @@ export async function distributePipelineProducts(
|
|
|
25
22
|
? { state: "present", commit: value.object.sha }
|
|
26
23
|
: { state: "absent", commit: null };
|
|
27
24
|
}
|
|
28
|
-
|
|
25
|
+
// Only retained historical plans contain npm channel effects. New releases
|
|
26
|
+
// set their channel in npm publish and never enter a separate npm operation.
|
|
27
|
+
const value = await npm.observe(effect);
|
|
28
|
+
if (value.version !== effect.version)
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Historical npm channel ${effect.name}@${effect.tag} requires a one-time authenticated correction to ${effect.version}; recovery never republishes or rewrites npm tags`,
|
|
31
|
+
);
|
|
32
|
+
return value;
|
|
29
33
|
}
|
|
30
34
|
const plans = await journal.materials("publication/distribution-plan/");
|
|
31
35
|
if (plans.length > 1)
|
|
@@ -40,18 +44,26 @@ export async function distributePipelineProducts(
|
|
|
40
44
|
commit: materialization.source.commit,
|
|
41
45
|
},
|
|
42
46
|
];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
name: artifact.package.name,
|
|
50
|
-
access: artifact.targets.find(({ provider }) => provider === "npm")
|
|
51
|
-
.access,
|
|
52
|
-
tag: npmTag,
|
|
53
|
-
version: plan.version,
|
|
47
|
+
const receipts = await journal.materials("publication/effect/");
|
|
48
|
+
if (receipts.length) {
|
|
49
|
+
const published = pipelinePublicationEffects({
|
|
50
|
+
plan,
|
|
51
|
+
...retained,
|
|
52
|
+
receipts,
|
|
54
53
|
});
|
|
54
|
+
for (const effect of published.filter(
|
|
55
|
+
(item) =>
|
|
56
|
+
item.kind === "npm-package" && item.tag.startsWith("buildchain-"),
|
|
57
|
+
))
|
|
58
|
+
raw.push({
|
|
59
|
+
id: `npm-channel:${effect.product}`,
|
|
60
|
+
kind: "npm-channel",
|
|
61
|
+
name: effect.name,
|
|
62
|
+
access: effect.access,
|
|
63
|
+
tag: plan.channel === "alpha" ? "alpha" : "latest",
|
|
64
|
+
version: effect.version,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
55
67
|
effects = [];
|
|
56
68
|
for (const effect of raw) {
|
|
57
69
|
const before = await observe(effect);
|
|
@@ -81,12 +93,6 @@ export async function distributePipelineProducts(
|
|
|
81
93
|
throw new Error(
|
|
82
94
|
"Floating channel changed since retained distribution admission",
|
|
83
95
|
);
|
|
84
|
-
if (
|
|
85
|
-
effect.kind === "npm-channel" &&
|
|
86
|
-
current.version &&
|
|
87
|
-
compareDevelopmentVersions(current.version, effect.version) > 0
|
|
88
|
-
)
|
|
89
|
-
throw new Error("npm channel cannot regress a newer published version");
|
|
90
96
|
await journal.fence();
|
|
91
97
|
if (effect.kind === "git-channel") {
|
|
92
98
|
if (current.state === "absent")
|
|
@@ -101,7 +107,7 @@ export async function distributePipelineProducts(
|
|
|
101
107
|
{ method: "PATCH", body: { sha: effect.commit, force: false } },
|
|
102
108
|
);
|
|
103
109
|
}
|
|
104
|
-
|
|
110
|
+
throw new Error("Distribution cannot write npm channels");
|
|
105
111
|
},
|
|
106
112
|
};
|
|
107
113
|
const receipts = await applyPipelineEffects({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout } from "node:timers/promises";
|
|
1
2
|
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
3
|
|
|
3
4
|
const KINDS = new Set([
|
|
@@ -14,6 +15,7 @@ export function pipelinePublicationEffects({
|
|
|
14
15
|
qualified,
|
|
15
16
|
documents,
|
|
16
17
|
evidence = [],
|
|
18
|
+
receipts = [],
|
|
17
19
|
}) {
|
|
18
20
|
const effects = [
|
|
19
21
|
{
|
|
@@ -34,7 +36,7 @@ export function pipelinePublicationEffects({
|
|
|
34
36
|
version: artifact.package.version,
|
|
35
37
|
integrity: artifact.package.integrity,
|
|
36
38
|
access: target.access || "public",
|
|
37
|
-
tag:
|
|
39
|
+
tag: plan.channel === "alpha" ? "alpha" : "latest",
|
|
38
40
|
});
|
|
39
41
|
if (target.provider === "github-release")
|
|
40
42
|
effects.push({
|
|
@@ -72,7 +74,24 @@ export function pipelinePublicationEffects({
|
|
|
72
74
|
tag: plan.tag,
|
|
73
75
|
prerelease: plan.channel === "alpha",
|
|
74
76
|
});
|
|
75
|
-
return effects.map((effect) =>
|
|
77
|
+
return effects.map((effect) => {
|
|
78
|
+
if (effect.kind === "npm-package") {
|
|
79
|
+
// Historical receipts keep their exact identity across runtime repairs.
|
|
80
|
+
const original = {
|
|
81
|
+
...effect,
|
|
82
|
+
tag: `buildchain-${plan.root.slice(7, 23)}`,
|
|
83
|
+
};
|
|
84
|
+
if (
|
|
85
|
+
receipts.some(
|
|
86
|
+
(receipt) =>
|
|
87
|
+
receipt.effectId === effect.id &&
|
|
88
|
+
receipt.effectRoot === recordDigest(original),
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
effect = original;
|
|
92
|
+
}
|
|
93
|
+
return { ...effect, root: recordDigest(effect) };
|
|
94
|
+
});
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
// This executor owns only bounded provider operations. Journal admission/fencing
|
|
@@ -85,6 +104,7 @@ export async function applyPipelineEffects({
|
|
|
85
104
|
provider,
|
|
86
105
|
fence,
|
|
87
106
|
retain,
|
|
107
|
+
wait = setTimeout,
|
|
88
108
|
}) {
|
|
89
109
|
const results = [];
|
|
90
110
|
for (const effect of effects) {
|
|
@@ -133,11 +153,21 @@ export async function applyPipelineEffects({
|
|
|
133
153
|
} catch (error) {
|
|
134
154
|
failure = error;
|
|
135
155
|
}
|
|
136
|
-
|
|
156
|
+
// Accepted npm versions remain unavailable during registry scanning.
|
|
157
|
+
// Bound that wait to fifteen minutes without repeating the write.
|
|
158
|
+
const scanning = effect.kind === "npm-package" && !failure;
|
|
159
|
+
const maximumReads = scanning ? 31 : 3;
|
|
160
|
+
for (let attempt = 0; attempt < maximumReads; attempt++) {
|
|
161
|
+
if (attempt) await wait(scanning ? 30_000 : 1000);
|
|
162
|
+
await fence();
|
|
163
|
+
observed = await provider.observe(effect);
|
|
164
|
+
if (observed.state !== "absent" || provider.matches(effect, observed))
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
137
167
|
if (!provider.matches(effect, observed)) {
|
|
138
168
|
if (failure) throw failure;
|
|
139
169
|
throw new Error(
|
|
140
|
-
|
|
170
|
+
`Publication write lacks exact successful provider readback (${effect.kind})`,
|
|
141
171
|
);
|
|
142
172
|
}
|
|
143
173
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout } from "node:timers/promises";
|
|
1
2
|
import {
|
|
2
3
|
canonicalJson,
|
|
3
4
|
recordDigest,
|
|
@@ -72,16 +73,17 @@ export function atomicAttemptJournal(provider, intent) {
|
|
|
72
73
|
}
|
|
73
74
|
// A lost response can follow a committed append. Reconcile the immutable
|
|
74
75
|
// record in live history; never retry the old parent with force=true.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
77
|
+
if (attempt) await setTimeout(1000);
|
|
78
|
+
const result = await read();
|
|
79
|
+
if (
|
|
80
|
+
result.snapshot.records.some(
|
|
81
|
+
(event) => canonicalJson(event) === canonicalJson(record),
|
|
82
|
+
)
|
|
79
83
|
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
84
|
-
return result;
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
throw failure || new Error("Attempt append missing from provider readback");
|
|
85
87
|
}
|
|
86
88
|
return { read, append };
|
|
87
89
|
}
|
|
@@ -7,10 +7,23 @@ export async function qualifyRecoveryIntegration(session, admission, host) {
|
|
|
7
7
|
...session.observed.history.at(-1),
|
|
8
8
|
intent: session.intent,
|
|
9
9
|
};
|
|
10
|
+
const policyAdmission = await host.source.observe(
|
|
11
|
+
session.intent.source.pullRequest,
|
|
12
|
+
{ "config-path": current.generation.source.configPath },
|
|
13
|
+
);
|
|
14
|
+
if (
|
|
15
|
+
!policyAdmission.eligible ||
|
|
16
|
+
!policyAdmission.live.merged ||
|
|
17
|
+
policyAdmission.live.targetBranch !== session.intent.source.targetBranch ||
|
|
18
|
+
policyAdmission.live.mergeCommit !== admission.live.mergeCommit ||
|
|
19
|
+
recordDigest(policyAdmission.live.source) !==
|
|
20
|
+
recordDigest(current.generation.source)
|
|
21
|
+
)
|
|
22
|
+
throw new Error("Recovery observed changed protected integration");
|
|
10
23
|
const integration = await host.integration.observe(current);
|
|
11
24
|
const review = await host.policy.observeMerged(
|
|
12
25
|
current,
|
|
13
|
-
|
|
26
|
+
policyAdmission.protectedPlan.review,
|
|
14
27
|
integration,
|
|
15
28
|
);
|
|
16
29
|
if (!review.review || !review.checksPassing)
|