@kungfu-tech/buildchain 2.0.14-alpha.3 → 2.0.14-alpha.5
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/README.md +8 -7
- package/docs/reusable-build-surface.md +103 -45
- package/package.json +1 -1
- package/scripts/build-contract-core.mjs +214 -38
- package/scripts/resolve-build-contract.mjs +25 -5
package/README.md
CHANGED
|
@@ -34,11 +34,11 @@ protocol for non-Node projects.
|
|
|
34
34
|
|
|
35
35
|
Buildchain release automation is branch-driven:
|
|
36
36
|
|
|
37
|
-
| Merge path
|
|
38
|
-
|
|
|
39
|
-
| `dev/vX/vX.Y -> alpha/vX/vX.Y`
|
|
40
|
-
| `alpha/vX/vX.Y -> release/vX/vX.Y`
|
|
41
|
-
| `release/vX/vX.Y -> publish-gate/major` | publish the next major from a reviewed production line | `v(X+1).0.0`
|
|
37
|
+
| Merge path | Meaning | Exact tag | Floating tags and branches |
|
|
38
|
+
| --------------------------------------- | ------------------------------------------------------ | ---------------- | ----------------------------------------------- |
|
|
39
|
+
| `dev/vX/vX.Y -> alpha/vX/vX.Y` | publish the next testable alpha for a minor line | `vX.Y.Z-alpha.N` | `vX.Y-alpha`, `alpha/vX/vX.Y`, `dev/vX/vX.Y` |
|
|
40
|
+
| `alpha/vX/vX.Y -> release/vX/vX.Y` | publish production for that minor line | `vX.Y.Z` | `vX.Y`, usually `vX`, `release/vX/vX.Y` |
|
|
41
|
+
| `release/vX/vX.Y -> publish-gate/major` | publish the next major from a reviewed production line | `v(X+1).0.0` | `v(X+1)`, `v(X+1).0`, `release/v(X+1)/v(X+1).0` |
|
|
42
42
|
|
|
43
43
|
After a production release, Buildchain prepares the next alpha source commit for
|
|
44
44
|
the same minor line and moves `dev/vX/vX.Y`, `alpha/vX/vX.Y`, and
|
|
@@ -102,8 +102,9 @@ Buildchain v2 ships these active surfaces:
|
|
|
102
102
|
- `.github/workflows/.build.yml` as the reusable build surface for
|
|
103
103
|
tri-platform runner presets, custom runner matrices, caller-provided lifecycle
|
|
104
104
|
commands, trusted event gating, artifact name templates, expected artifact
|
|
105
|
-
checks,
|
|
106
|
-
release manifests, and
|
|
105
|
+
checks, optional digest-pinned Linux job containers, deterministic artifact
|
|
106
|
+
manifests, publish-gate source locks, resolved release manifests, and
|
|
107
|
+
aggregate build summaries;
|
|
107
108
|
- `.github/workflows/.web-surface.yml` as the reusable site/app surface for PR
|
|
108
109
|
preview plans, closed-PR preview cleanup plans, push-main staging promotion
|
|
109
110
|
plans, and explicit production environment gates;
|
|
@@ -17,6 +17,7 @@ jobs:
|
|
|
17
17
|
working-directory: .
|
|
18
18
|
artifact-name: libnode
|
|
19
19
|
runner-preset: kungfu-v4-self-hosted
|
|
20
|
+
linux-container-preset: kungfu-verify
|
|
20
21
|
artifact-name-template: "{artifact}-{platform}-{sha}"
|
|
21
22
|
artifact-paths: |
|
|
22
23
|
dist
|
|
@@ -29,20 +30,20 @@ jobs:
|
|
|
29
30
|
|
|
30
31
|
`runner-preset` is the stable first-class surface for known runner fleets:
|
|
31
32
|
|
|
32
|
-
| Preset
|
|
33
|
-
|
|
|
34
|
-
| `github-hosted`
|
|
33
|
+
| Preset | Platforms |
|
|
34
|
+
| ----------------------- | ------------------------------------------------------------------------ |
|
|
35
|
+
| `github-hosted` | `ubuntu-24.04`, `macos-latest`, `windows-2022` |
|
|
35
36
|
| `kungfu-v4-self-hosted` | Kungfu Linux x64, macOS ARM64, and Windows x64 self-hosted runner labels |
|
|
36
|
-
| `custom`
|
|
37
|
+
| `custom` | Requires `platforms-json` |
|
|
37
38
|
|
|
38
39
|
Callers can still provide a custom matrix with `platforms-json`. Each platform
|
|
39
40
|
object has:
|
|
40
41
|
|
|
41
|
-
| Field
|
|
42
|
-
|
|
|
43
|
-
| `id`
|
|
44
|
-
| `name`
|
|
45
|
-
| `runner` | JSON string passed to `runs-on` after `fromJSON`
|
|
42
|
+
| Field | Meaning |
|
|
43
|
+
| -------- | ------------------------------------------------- |
|
|
44
|
+
| `id` | Stable artifact/platform key, such as `linux-x64` |
|
|
45
|
+
| `name` | Human-readable job name |
|
|
46
|
+
| `runner` | JSON string passed to `runs-on` after `fromJSON` |
|
|
46
47
|
|
|
47
48
|
The runner field is intentionally a JSON string so callers can pass either
|
|
48
49
|
GitHub-hosted runners or multi-label self-hosted runners without Buildchain
|
|
@@ -52,28 +53,85 @@ Only include platforms that should run. GitHub schedules matrix jobs before
|
|
|
52
53
|
steps execute, so a disabled entry with unavailable runner labels can still
|
|
53
54
|
block the workflow queue.
|
|
54
55
|
|
|
56
|
+
## Linux Job Containers
|
|
57
|
+
|
|
58
|
+
Linux build platforms can run inside a digest-pinned job container while macOS
|
|
59
|
+
and Windows keep using native runners. This is the recommended way to remove
|
|
60
|
+
moving Linux runner prerequisites from Buildchain consumers: the Linux host only
|
|
61
|
+
needs a GitHub Actions runner, Docker, and network access; common verification
|
|
62
|
+
tools come from the image contract.
|
|
63
|
+
|
|
64
|
+
Use the Kungfu verification image for lifecycle stages that need Git, jq,
|
|
65
|
+
Python, uv, and fnm, but do not need native compilation:
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
jobs:
|
|
69
|
+
build:
|
|
70
|
+
uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v2
|
|
71
|
+
with:
|
|
72
|
+
runner-preset: kungfu-v4-self-hosted
|
|
73
|
+
linux-container-preset: kungfu-verify
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`kungfu-verify` resolves to:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
ghcr.io/kungfu-systems/build-images/kungfu-verify@sha256:11f0ba64267ce88174a4f73a9bf833ff4e9c59cd16ec3d08a6432a06c2be6fb1
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Callers that own a different Linux image can pass it explicitly:
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
with:
|
|
86
|
+
linux-container-preset: custom
|
|
87
|
+
linux-container-image: ghcr.io/example/project-build@sha256:<digest>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`linux-container-image` should be pinned by digest. A floating tag makes the
|
|
91
|
+
runner surface mutable and weakens Buildchain's release evidence.
|
|
92
|
+
|
|
93
|
+
The workflow splits the matrix into two build jobs:
|
|
94
|
+
|
|
95
|
+
- Linux platforms go to `build-linux-container` when a Linux container is
|
|
96
|
+
configured.
|
|
97
|
+
- All other platforms go to `build-native`.
|
|
98
|
+
|
|
99
|
+
Artifact names, manifest paths, expected artifact checks, publish-source locks,
|
|
100
|
+
and aggregate summaries are the same in both jobs. The split is an execution
|
|
101
|
+
detail, not a different artifact contract.
|
|
102
|
+
|
|
103
|
+
The container image provides `fnm` but does not preinstall Node. Buildchain uses
|
|
104
|
+
`fnm` inside the container to install the requested `node-version` before it
|
|
105
|
+
runs Buildchain runtime scripts or lifecycle actions.
|
|
106
|
+
|
|
107
|
+
Do not use `kungfu-verify` for stages that need CMake, Ninja, ccache, Conan, or
|
|
108
|
+
Docker image publishing. Those should use a heavier native-build image or remain
|
|
109
|
+
on a host runner until their image contract is explicit.
|
|
110
|
+
|
|
55
111
|
## Workflow Outputs
|
|
56
112
|
|
|
57
113
|
The reusable workflow exposes the resolved contract:
|
|
58
114
|
|
|
59
|
-
| Output
|
|
60
|
-
|
|
|
61
|
-
| `runner-preset`
|
|
62
|
-
| `platforms-json`
|
|
63
|
-
| `platform-count`
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `publish-
|
|
70
|
-
| `publish-
|
|
71
|
-
| `publish-
|
|
72
|
-
| `publish-source-
|
|
73
|
-
| `publish-source-
|
|
74
|
-
| `publish-source-
|
|
75
|
-
| `publish-source-
|
|
76
|
-
| `
|
|
115
|
+
| Output | Meaning |
|
|
116
|
+
| --------------------------------- | ------------------------------------------------------------------------------- |
|
|
117
|
+
| `runner-preset` | Resolved preset, or `custom` when `platforms-json` was provided |
|
|
118
|
+
| `platforms-json` | Exact matrix JSON used by the build job |
|
|
119
|
+
| `platform-count` | Number of matrix platforms |
|
|
120
|
+
| `linux-container-enabled` | `true` when Linux platforms are routed through a job container |
|
|
121
|
+
| `linux-container-image` | Resolved digest-pinned Linux job container image |
|
|
122
|
+
| `build-summary-artifact` | Uploaded aggregate summary artifact name |
|
|
123
|
+
| `build-summary-json` | Compact aggregate JSON with platform count, file count, and byte total |
|
|
124
|
+
| `trusted-event` | `true` when the event is trusted enough to reach build runners |
|
|
125
|
+
| `publish-channel` | Resolved publish channel requested by the caller |
|
|
126
|
+
| `publish-allowed` | `true` only when this event/ref may publish after verification |
|
|
127
|
+
| `publish-reason` | Human-readable reason for the publish gate decision |
|
|
128
|
+
| `publish-source-ref` | Gate source ref that was resolved before checkout |
|
|
129
|
+
| `publish-source-sha` | Exact source commit used by checkout, build, verify, and artifacts |
|
|
130
|
+
| `publish-source-locked` | `true` when a `publish-gate/*` source ref was explicitly locked |
|
|
131
|
+
| `publish-source-channel` | `alpha`, `release`, `anchor`, or `major` parsed from the source ref |
|
|
132
|
+
| `publish-source-line` | Product line parsed from source refs such as `v22/v22.22` |
|
|
133
|
+
| `publish-source-consumer-version` | Consumer package version parsed from source refs |
|
|
134
|
+
| `release-manifest-json` | Resolved release manifest including source lock, version state, and anchor data |
|
|
77
135
|
|
|
78
136
|
The aggregate summary is intentionally an artifact as well as an output. GitHub
|
|
79
137
|
Actions matrix outputs are not a reliable place to carry every platform's full
|
|
@@ -107,12 +165,12 @@ jobs:
|
|
|
107
165
|
|
|
108
166
|
Default channels are:
|
|
109
167
|
|
|
110
|
-
| Channel
|
|
111
|
-
|
|
|
112
|
-
| `none`
|
|
113
|
-
| `alpha`
|
|
114
|
-
| `release` | `release/vN/vN.M` branches or release tags such as `vN.M.P`, `vN.M`, `vN`
|
|
115
|
-
| `major`
|
|
168
|
+
| Channel | Allowed refs |
|
|
169
|
+
| --------- | ---------------------------------------------------------------------------------------------------- |
|
|
170
|
+
| `none` | Never publishes; this is the default |
|
|
171
|
+
| `alpha` | `alpha/vN/vN.M` branches or exact `vN.M.P-alpha.K` tags |
|
|
172
|
+
| `release` | `release/vN/vN.M` branches or release tags such as `vN.M.P`, `vN.M`, `vN` |
|
|
173
|
+
| `major` | `publish-gate/major`, legacy `major-gate`, or next-major release tags such as `vN.0.0`, `vN.0`, `vN` |
|
|
116
174
|
|
|
117
175
|
Pull request events always produce `publish-allowed=false`, even when the PR is
|
|
118
176
|
from the same repository. Untrusted fork events also produce
|
|
@@ -138,13 +196,13 @@ why it was or was not eligible to publish.
|
|
|
138
196
|
source tree is the publish decision about?" A caller can pass `publish-source-ref`
|
|
139
197
|
to bind a publish run to a reviewed gate branch before any checkout happens:
|
|
140
198
|
|
|
141
|
-
| Ref
|
|
142
|
-
|
|
|
143
|
-
| `publish-gate/alpha/<line>/<consumer-version>`
|
|
144
|
-
| `publish-gate/release/<line>/<consumer-version>` | Build and publish a production candidate for a consumer line
|
|
145
|
-
| `publish-gate/anchor`
|
|
146
|
-
| `publish-gate/major`
|
|
147
|
-
| `major-gate`
|
|
199
|
+
| Ref | Meaning |
|
|
200
|
+
| ------------------------------------------------ | --------------------------------------------------------------------------- |
|
|
201
|
+
| `publish-gate/alpha/<line>/<consumer-version>` | Build and publish an alpha candidate for a consumer line |
|
|
202
|
+
| `publish-gate/release/<line>/<consumer-version>` | Build and publish a production candidate for a consumer line |
|
|
203
|
+
| `publish-gate/anchor` | Resolve an explicit anchor request; it does not publish artifacts by itself |
|
|
204
|
+
| `publish-gate/major` | Gate the next major source state |
|
|
205
|
+
| `major-gate` | Legacy compatibility alias for the major gate |
|
|
148
206
|
|
|
149
207
|
For alpha and release refs, `<line>` is intentionally allowed to contain `/`, so
|
|
150
208
|
Kungfu-style lines such as `v22/v22.22` stay readable. The final path segment is
|
|
@@ -310,11 +368,11 @@ produce a new GitHub Actions run but keep the same source SHA/platform contract.
|
|
|
310
368
|
`expected-artifacts-json` fails the build before upload when the artifact does
|
|
311
369
|
not match the caller's declared contract. Supported checks are:
|
|
312
370
|
|
|
313
|
-
| Field
|
|
314
|
-
|
|
|
315
|
-
| `minFiles`
|
|
316
|
-
| `maxFiles`
|
|
317
|
-
| `minTotalBytes` | Minimum total byte count
|
|
371
|
+
| Field | Meaning |
|
|
372
|
+
| --------------- | ------------------------------------ |
|
|
373
|
+
| `minFiles` | Minimum number of manifest files |
|
|
374
|
+
| `maxFiles` | Maximum number of manifest files |
|
|
375
|
+
| `minTotalBytes` | Minimum total byte count |
|
|
318
376
|
| `requiredPaths` | Exact manifest paths that must exist |
|
|
319
377
|
|
|
320
378
|
## Trusted Event Gate
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.0.14-alpha.
|
|
3
|
+
"version": "2.0.14-alpha.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Kungfu Buildchain reusable workflows, release governance, and CLI tooling.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -4,29 +4,36 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
export const RUNNER_PRESETS = Object.freeze({
|
|
6
6
|
"github-hosted": [
|
|
7
|
-
{ id: "linux-x64", name: "Linux x64", runner:
|
|
8
|
-
{ id: "macos", name: "macOS", runner:
|
|
9
|
-
{ id: "windows-x64", name: "Windows x64", runner:
|
|
7
|
+
{ id: "linux-x64", name: "Linux x64", runner: '["ubuntu-24.04"]' },
|
|
8
|
+
{ id: "macos", name: "macOS", runner: '["macos-latest"]' },
|
|
9
|
+
{ id: "windows-x64", name: "Windows x64", runner: '["windows-2022"]' },
|
|
10
10
|
],
|
|
11
11
|
"kungfu-v4-self-hosted": [
|
|
12
12
|
{
|
|
13
13
|
id: "linux-x64",
|
|
14
14
|
name: "Linux x64",
|
|
15
|
-
runner:
|
|
15
|
+
runner: '["self-hosted","Linux","X64","kungfu-build-v4-linux-x64"]',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
id: "macos-arm64",
|
|
19
19
|
name: "macOS ARM64",
|
|
20
|
-
runner:
|
|
20
|
+
runner: '["self-hosted","macOS","ARM64","kungfu-build-v4-macos-arm64"]',
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
id: "windows-x64",
|
|
24
24
|
name: "Windows x64",
|
|
25
|
-
runner:
|
|
25
|
+
runner: '["self-hosted","Windows","X64","kungfu-build-v4-windows-x64"]',
|
|
26
26
|
},
|
|
27
27
|
],
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
export const LINUX_CONTAINER_PRESETS = Object.freeze({
|
|
31
|
+
"kungfu-verify": {
|
|
32
|
+
image:
|
|
33
|
+
"ghcr.io/kungfu-systems/build-images/kungfu-verify@sha256:11f0ba64267ce88174a4f73a9bf833ff4e9c59cd16ec3d08a6432a06c2be6fb1",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
30
37
|
const RUNNER_PRESET_ALIASES = Object.freeze({
|
|
31
38
|
github: "github-hosted",
|
|
32
39
|
"github-hosted-default": "github-hosted",
|
|
@@ -35,6 +42,14 @@ const RUNNER_PRESET_ALIASES = Object.freeze({
|
|
|
35
42
|
"kungfu-v4": "kungfu-v4-self-hosted",
|
|
36
43
|
});
|
|
37
44
|
|
|
45
|
+
const LINUX_CONTAINER_PRESET_ALIASES = Object.freeze({
|
|
46
|
+
"": "",
|
|
47
|
+
none: "",
|
|
48
|
+
off: "",
|
|
49
|
+
false: "",
|
|
50
|
+
verify: "kungfu-verify",
|
|
51
|
+
});
|
|
52
|
+
|
|
38
53
|
export const DEFAULT_ARTIFACT_NAME_TEMPLATE = "{artifact}-{platform}-{sha}";
|
|
39
54
|
export const DEFAULT_PUBLISH_REFS = Object.freeze({
|
|
40
55
|
alpha: [
|
|
@@ -138,7 +153,10 @@ export function parsePublishSourceRef(value = "") {
|
|
|
138
153
|
if (sourceRef === "publish-gate/major" || sourceRef === "major-gate") {
|
|
139
154
|
return {
|
|
140
155
|
sourceRef,
|
|
141
|
-
fullRef:
|
|
156
|
+
fullRef:
|
|
157
|
+
sourceRef === "major-gate"
|
|
158
|
+
? "refs/heads/major-gate"
|
|
159
|
+
: "refs/heads/publish-gate/major",
|
|
142
160
|
enabled: true,
|
|
143
161
|
channel: "major",
|
|
144
162
|
line: "",
|
|
@@ -147,7 +165,9 @@ export function parsePublishSourceRef(value = "") {
|
|
|
147
165
|
legacyAlias: sourceRef === "major-gate",
|
|
148
166
|
};
|
|
149
167
|
}
|
|
150
|
-
const match = sourceRef.match(
|
|
168
|
+
const match = sourceRef.match(
|
|
169
|
+
/^publish-gate\/(alpha|release)\/(.+)\/([^/]+)$/,
|
|
170
|
+
);
|
|
151
171
|
if (!match) {
|
|
152
172
|
throw new Error(
|
|
153
173
|
`unsupported publish source ref: ${sourceRef}; expected publish-gate/alpha/<line>/<version>, publish-gate/release/<line>/<version>, publish-gate/anchor, publish-gate/major, or major-gate`,
|
|
@@ -155,10 +175,14 @@ export function parsePublishSourceRef(value = "") {
|
|
|
155
175
|
}
|
|
156
176
|
const [, channel, line, consumerVersion] = match;
|
|
157
177
|
if (!line.includes("/")) {
|
|
158
|
-
throw new Error(
|
|
178
|
+
throw new Error(
|
|
179
|
+
`publish source line must include a major/minor path: ${sourceRef}`,
|
|
180
|
+
);
|
|
159
181
|
}
|
|
160
182
|
if (!/^[A-Za-z0-9._+~-]+$/.test(consumerVersion)) {
|
|
161
|
-
throw new Error(
|
|
183
|
+
throw new Error(
|
|
184
|
+
`publish source consumer version contains unsupported characters: ${consumerVersion}`,
|
|
185
|
+
);
|
|
162
186
|
}
|
|
163
187
|
return {
|
|
164
188
|
sourceRef,
|
|
@@ -242,7 +266,9 @@ export async function createResolvedReleaseManifest({
|
|
|
242
266
|
}));
|
|
243
267
|
if (lock.consumerVersion) {
|
|
244
268
|
if (resolvedVersionFiles.length === 0) {
|
|
245
|
-
throw new Error(
|
|
269
|
+
throw new Error(
|
|
270
|
+
"publish source consumer version requires configured version.files",
|
|
271
|
+
);
|
|
246
272
|
}
|
|
247
273
|
for (const file of resolvedVersionFiles) {
|
|
248
274
|
if (file.version !== lock.consumerVersion) {
|
|
@@ -254,7 +280,11 @@ export async function createResolvedReleaseManifest({
|
|
|
254
280
|
}
|
|
255
281
|
|
|
256
282
|
const anchorManifest = loadConfiguredAnchorManifest(cwd, loadedConfig);
|
|
257
|
-
if (
|
|
283
|
+
if (
|
|
284
|
+
lock.consumerVersion &&
|
|
285
|
+
anchorManifest?.fields?.npmVersion &&
|
|
286
|
+
anchorManifest.fields.npmVersion !== lock.consumerVersion
|
|
287
|
+
) {
|
|
258
288
|
throw new Error(
|
|
259
289
|
`anchor manifest npmVersion ${anchorManifest.fields.npmVersion} does not match ${lock.consumerVersion}`,
|
|
260
290
|
);
|
|
@@ -287,17 +317,29 @@ export async function createResolvedReleaseManifest({
|
|
|
287
317
|
anchorRequest,
|
|
288
318
|
publish: {
|
|
289
319
|
registry: publishRegistry,
|
|
290
|
-
distTag:
|
|
320
|
+
distTag:
|
|
321
|
+
distTag ||
|
|
322
|
+
(lock.channel === "release"
|
|
323
|
+
? "latest"
|
|
324
|
+
: lock.channel === "alpha"
|
|
325
|
+
? "alpha"
|
|
326
|
+
: ""),
|
|
291
327
|
visibilityGate,
|
|
292
328
|
},
|
|
293
329
|
};
|
|
294
330
|
}
|
|
295
331
|
|
|
296
|
-
export function verifyPublishSourceLock({
|
|
332
|
+
export function verifyPublishSourceLock({
|
|
333
|
+
sourceRef = "",
|
|
334
|
+
expectedSha = "",
|
|
335
|
+
currentSha = "",
|
|
336
|
+
} = {}) {
|
|
297
337
|
const expected = assertSha(expectedSha, "expectedSha");
|
|
298
338
|
const current = assertSha(currentSha, "currentSha");
|
|
299
339
|
if (expected !== current) {
|
|
300
|
-
throw new Error(
|
|
340
|
+
throw new Error(
|
|
341
|
+
`publish source ref moved: ${sourceRef || "<unknown>"} expected ${expected}, got ${current}`,
|
|
342
|
+
);
|
|
301
343
|
}
|
|
302
344
|
return {
|
|
303
345
|
ok: true,
|
|
@@ -356,8 +398,14 @@ export function planPackageSetPublish({
|
|
|
356
398
|
for (const pkg of normalized) {
|
|
357
399
|
const already = existing.get(existingPackageKey(pkg));
|
|
358
400
|
if (already) {
|
|
359
|
-
if (
|
|
360
|
-
|
|
401
|
+
if (
|
|
402
|
+
pkg.integrity &&
|
|
403
|
+
already.integrity &&
|
|
404
|
+
pkg.integrity !== already.integrity
|
|
405
|
+
) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`existing package integrity mismatch: ${pkg.name}@${pkg.version}`,
|
|
408
|
+
);
|
|
361
409
|
}
|
|
362
410
|
steps.push({ action: "accept-existing", package: pkg });
|
|
363
411
|
} else {
|
|
@@ -406,7 +454,9 @@ function normalizePublishRefs(value = "") {
|
|
|
406
454
|
try {
|
|
407
455
|
new RegExp(value);
|
|
408
456
|
} catch (error) {
|
|
409
|
-
throw new Error(
|
|
457
|
+
throw new Error(
|
|
458
|
+
`publish-refs-json.${key}[${index}] is invalid: ${error.message}`,
|
|
459
|
+
);
|
|
410
460
|
}
|
|
411
461
|
return value;
|
|
412
462
|
});
|
|
@@ -434,6 +484,65 @@ function normalizeRunnerPreset(value) {
|
|
|
434
484
|
return RUNNER_PRESET_ALIASES[preset] || preset;
|
|
435
485
|
}
|
|
436
486
|
|
|
487
|
+
function normalizeLinuxContainerPreset(value) {
|
|
488
|
+
const preset = String(value || "").trim();
|
|
489
|
+
return LINUX_CONTAINER_PRESET_ALIASES[preset] ?? preset;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function resolveLinuxContainer({
|
|
493
|
+
linuxContainerPreset = "",
|
|
494
|
+
linuxContainerImage = "",
|
|
495
|
+
} = {}) {
|
|
496
|
+
const explicitImage = String(linuxContainerImage || "").trim();
|
|
497
|
+
const preset = normalizeLinuxContainerPreset(linuxContainerPreset);
|
|
498
|
+
if (explicitImage) {
|
|
499
|
+
if (preset && preset !== "custom") {
|
|
500
|
+
throw new Error(
|
|
501
|
+
"linux-container-image cannot be combined with a named linux-container-preset",
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
enabled: true,
|
|
506
|
+
preset: preset || "custom",
|
|
507
|
+
image: explicitImage,
|
|
508
|
+
source: "linux-container-image",
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
if (!preset) {
|
|
512
|
+
return {
|
|
513
|
+
enabled: false,
|
|
514
|
+
preset: "",
|
|
515
|
+
image: "",
|
|
516
|
+
source: "none",
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const resolved = LINUX_CONTAINER_PRESETS[preset];
|
|
520
|
+
if (!resolved) {
|
|
521
|
+
throw new Error(`unsupported linux-container-preset: ${preset}`);
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
enabled: true,
|
|
525
|
+
preset,
|
|
526
|
+
image: resolved.image,
|
|
527
|
+
source: "linux-container-preset",
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function platformIsLinux(platform) {
|
|
532
|
+
const id = String(platform?.id || "").toLowerCase();
|
|
533
|
+
const name = String(platform?.name || "").toLowerCase();
|
|
534
|
+
if (id.includes("linux") || name.includes("linux")) {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
const runnerLabels = parseJsonArray(
|
|
538
|
+
String(platform?.runner || "[]"),
|
|
539
|
+
"platform.runner",
|
|
540
|
+
).map((label) => String(label || "").toLowerCase());
|
|
541
|
+
return runnerLabels.some(
|
|
542
|
+
(label) => label.includes("linux") || label.includes("ubuntu"),
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
437
546
|
function normalizePlatform(platform, index) {
|
|
438
547
|
const id = String(platform?.id || "").trim();
|
|
439
548
|
const name = String(platform?.name || id).trim();
|
|
@@ -451,19 +560,43 @@ function normalizePlatform(platform, index) {
|
|
|
451
560
|
return { id, name, runner };
|
|
452
561
|
}
|
|
453
562
|
|
|
454
|
-
export function resolveRunnerMatrix({
|
|
563
|
+
export function resolveRunnerMatrix({
|
|
564
|
+
runnerPreset = "github-hosted",
|
|
565
|
+
platformsJson = "",
|
|
566
|
+
linuxContainerPreset = "",
|
|
567
|
+
linuxContainerImage = "",
|
|
568
|
+
} = {}) {
|
|
455
569
|
const customPlatformsJson = String(platformsJson || "").trim();
|
|
570
|
+
const linuxContainer = resolveLinuxContainer({
|
|
571
|
+
linuxContainerPreset,
|
|
572
|
+
linuxContainerImage,
|
|
573
|
+
});
|
|
456
574
|
if (customPlatformsJson) {
|
|
457
|
-
const platforms = parseJsonArray(customPlatformsJson, "platforms-json").map(
|
|
575
|
+
const platforms = parseJsonArray(customPlatformsJson, "platforms-json").map(
|
|
576
|
+
normalizePlatform,
|
|
577
|
+
);
|
|
458
578
|
if (platforms.length === 0) {
|
|
459
579
|
throw new Error("platforms-json must include at least one platform");
|
|
460
580
|
}
|
|
581
|
+
const containerPlatforms = linuxContainer.enabled
|
|
582
|
+
? platforms.filter(platformIsLinux)
|
|
583
|
+
: [];
|
|
584
|
+
const nativePlatforms = linuxContainer.enabled
|
|
585
|
+
? platforms.filter((platform) => !platformIsLinux(platform))
|
|
586
|
+
: platforms;
|
|
461
587
|
return {
|
|
462
588
|
source: "platforms-json",
|
|
463
589
|
runnerPreset: "custom",
|
|
464
590
|
platforms,
|
|
465
591
|
platformsJson: JSON.stringify(platforms),
|
|
466
592
|
platformCount: platforms.length,
|
|
593
|
+
nativePlatforms,
|
|
594
|
+
nativePlatformsJson: JSON.stringify(nativePlatforms),
|
|
595
|
+
nativePlatformCount: nativePlatforms.length,
|
|
596
|
+
containerPlatforms,
|
|
597
|
+
containerPlatformsJson: JSON.stringify(containerPlatforms),
|
|
598
|
+
containerPlatformCount: containerPlatforms.length,
|
|
599
|
+
linuxContainer,
|
|
467
600
|
};
|
|
468
601
|
}
|
|
469
602
|
|
|
@@ -475,12 +608,25 @@ export function resolveRunnerMatrix({ runnerPreset = "github-hosted", platformsJ
|
|
|
475
608
|
if (!platforms) {
|
|
476
609
|
throw new Error(`unsupported runner-preset: ${preset}`);
|
|
477
610
|
}
|
|
611
|
+
const containerPlatforms = linuxContainer.enabled
|
|
612
|
+
? platforms.filter(platformIsLinux)
|
|
613
|
+
: [];
|
|
614
|
+
const nativePlatforms = linuxContainer.enabled
|
|
615
|
+
? platforms.filter((platform) => !platformIsLinux(platform))
|
|
616
|
+
: platforms;
|
|
478
617
|
return {
|
|
479
618
|
source: "runner-preset",
|
|
480
619
|
runnerPreset: preset,
|
|
481
620
|
platforms,
|
|
482
621
|
platformsJson: JSON.stringify(platforms),
|
|
483
622
|
platformCount: platforms.length,
|
|
623
|
+
nativePlatforms,
|
|
624
|
+
nativePlatformsJson: JSON.stringify(nativePlatforms),
|
|
625
|
+
nativePlatformCount: nativePlatforms.length,
|
|
626
|
+
containerPlatforms,
|
|
627
|
+
containerPlatformsJson: JSON.stringify(containerPlatforms),
|
|
628
|
+
containerPlatformCount: containerPlatforms.length,
|
|
629
|
+
linuxContainer,
|
|
484
630
|
};
|
|
485
631
|
}
|
|
486
632
|
|
|
@@ -506,7 +652,8 @@ export function resolvePublishGate({
|
|
|
506
652
|
trusted: isTrusted,
|
|
507
653
|
publishChannel: channel,
|
|
508
654
|
publishAllowed: false,
|
|
509
|
-
publishReason:
|
|
655
|
+
publishReason:
|
|
656
|
+
"anchor gates resolve source state but do not publish artifacts",
|
|
510
657
|
};
|
|
511
658
|
}
|
|
512
659
|
if (!isTrusted) {
|
|
@@ -537,7 +684,9 @@ export function resolvePublishGate({
|
|
|
537
684
|
};
|
|
538
685
|
}
|
|
539
686
|
const refValue = String(ref || "");
|
|
540
|
-
const matchedPattern = patterns.find((pattern) =>
|
|
687
|
+
const matchedPattern = patterns.find((pattern) =>
|
|
688
|
+
new RegExp(pattern).test(refValue),
|
|
689
|
+
);
|
|
541
690
|
if (!matchedPattern) {
|
|
542
691
|
return {
|
|
543
692
|
trusted: true,
|
|
@@ -573,7 +722,9 @@ export function resolveArtifactContract({
|
|
|
573
722
|
runId = "",
|
|
574
723
|
runAttempt = "",
|
|
575
724
|
} = {}) {
|
|
576
|
-
const baseName =
|
|
725
|
+
const baseName =
|
|
726
|
+
String(artifactName || "buildchain-artifact").trim() ||
|
|
727
|
+
"buildchain-artifact";
|
|
577
728
|
const template =
|
|
578
729
|
String(artifactNameTemplate || "").trim() || DEFAULT_ARTIFACT_NAME_TEMPLATE;
|
|
579
730
|
const replacements = {
|
|
@@ -588,15 +739,22 @@ export function resolveArtifactContract({
|
|
|
588
739
|
runId,
|
|
589
740
|
runAttempt,
|
|
590
741
|
};
|
|
591
|
-
const resolved = template.replace(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
742
|
+
const resolved = template.replace(
|
|
743
|
+
/\{([A-Za-z][A-Za-z0-9]*)\}/g,
|
|
744
|
+
(match, key) => {
|
|
745
|
+
if (!Object.hasOwn(replacements, key)) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
`unsupported artifact-name-template placeholder: ${match}`,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
return replacements[key] || "";
|
|
751
|
+
},
|
|
752
|
+
);
|
|
597
753
|
const safeName = sanitizeArtifactName(resolved);
|
|
598
754
|
if (!safeName) {
|
|
599
|
-
throw new Error(
|
|
755
|
+
throw new Error(
|
|
756
|
+
"artifact-name-template resolved to an empty artifact name",
|
|
757
|
+
);
|
|
600
758
|
}
|
|
601
759
|
return {
|
|
602
760
|
artifactName: safeName,
|
|
@@ -619,19 +777,28 @@ export function parseExpectedArtifactsJson(value = "") {
|
|
|
619
777
|
if (expected.minFiles !== undefined) {
|
|
620
778
|
normalized.minFiles = Number(expected.minFiles);
|
|
621
779
|
if (!Number.isInteger(normalized.minFiles) || normalized.minFiles < 0) {
|
|
622
|
-
throw new Error(
|
|
780
|
+
throw new Error(
|
|
781
|
+
"expected-artifacts-json.minFiles must be a non-negative integer",
|
|
782
|
+
);
|
|
623
783
|
}
|
|
624
784
|
}
|
|
625
785
|
if (expected.maxFiles !== undefined) {
|
|
626
786
|
normalized.maxFiles = Number(expected.maxFiles);
|
|
627
787
|
if (!Number.isInteger(normalized.maxFiles) || normalized.maxFiles < 0) {
|
|
628
|
-
throw new Error(
|
|
788
|
+
throw new Error(
|
|
789
|
+
"expected-artifacts-json.maxFiles must be a non-negative integer",
|
|
790
|
+
);
|
|
629
791
|
}
|
|
630
792
|
}
|
|
631
793
|
if (expected.minTotalBytes !== undefined) {
|
|
632
794
|
normalized.minTotalBytes = Number(expected.minTotalBytes);
|
|
633
|
-
if (
|
|
634
|
-
|
|
795
|
+
if (
|
|
796
|
+
!Number.isInteger(normalized.minTotalBytes) ||
|
|
797
|
+
normalized.minTotalBytes < 0
|
|
798
|
+
) {
|
|
799
|
+
throw new Error(
|
|
800
|
+
"expected-artifacts-json.minTotalBytes must be a non-negative integer",
|
|
801
|
+
);
|
|
635
802
|
}
|
|
636
803
|
}
|
|
637
804
|
if (expected.requiredPaths !== undefined) {
|
|
@@ -639,9 +806,13 @@ export function parseExpectedArtifactsJson(value = "") {
|
|
|
639
806
|
throw new Error("expected-artifacts-json.requiredPaths must be an array");
|
|
640
807
|
}
|
|
641
808
|
normalized.requiredPaths = expected.requiredPaths.map((entry, index) => {
|
|
642
|
-
const pathValue = String(entry || "")
|
|
809
|
+
const pathValue = String(entry || "")
|
|
810
|
+
.replace(/\\/g, "/")
|
|
811
|
+
.trim();
|
|
643
812
|
if (!pathValue) {
|
|
644
|
-
throw new Error(
|
|
813
|
+
throw new Error(
|
|
814
|
+
`expected-artifacts-json.requiredPaths[${index}] must be non-empty`,
|
|
815
|
+
);
|
|
645
816
|
}
|
|
646
817
|
return pathValue;
|
|
647
818
|
});
|
|
@@ -650,7 +821,10 @@ export function parseExpectedArtifactsJson(value = "") {
|
|
|
650
821
|
}
|
|
651
822
|
|
|
652
823
|
export function createArtifactSummary({ artifactName, platform, files }) {
|
|
653
|
-
const totalBytes = files.reduce(
|
|
824
|
+
const totalBytes = files.reduce(
|
|
825
|
+
(sum, file) => sum + Number(file.size || 0),
|
|
826
|
+
0,
|
|
827
|
+
);
|
|
654
828
|
const digest = crypto.createHash("sha256");
|
|
655
829
|
for (const file of files) {
|
|
656
830
|
digest.update(`${file.path}\0${file.size}\0${file.sha256}\n`);
|
|
@@ -713,7 +887,9 @@ export function writeGitHubOutputs(outputs) {
|
|
|
713
887
|
}
|
|
714
888
|
return;
|
|
715
889
|
}
|
|
716
|
-
const lines = Object.entries(outputs).map(
|
|
890
|
+
const lines = Object.entries(outputs).map(
|
|
891
|
+
([key, value]) => `${key}=${value}`,
|
|
892
|
+
);
|
|
717
893
|
fs.appendFileSync(outputPath, `${lines.join("\n")}\n`);
|
|
718
894
|
}
|
|
719
895
|
|
|
@@ -15,25 +15,40 @@ function readArg(name, fallback = "") {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function resolveBuildContractCli() {
|
|
18
|
-
const mode = readArg(
|
|
18
|
+
const mode = readArg(
|
|
19
|
+
"mode",
|
|
20
|
+
process.env.BUILDCHAIN_CONTRACT_MODE || "artifact",
|
|
21
|
+
);
|
|
19
22
|
if (mode === "runners") {
|
|
20
23
|
const resolved = resolveRunnerMatrix({
|
|
21
24
|
runnerPreset: process.env.BUILDCHAIN_RUNNER_PRESET || "github-hosted",
|
|
22
25
|
platformsJson: process.env.BUILDCHAIN_PLATFORMS_JSON || "",
|
|
26
|
+
linuxContainerPreset: process.env.BUILDCHAIN_LINUX_CONTAINER_PRESET || "",
|
|
27
|
+
linuxContainerImage: process.env.BUILDCHAIN_LINUX_CONTAINER_IMAGE || "",
|
|
23
28
|
});
|
|
24
29
|
writeGitHubOutputs({
|
|
25
30
|
"runner-preset": resolved.runnerPreset,
|
|
26
31
|
"platforms-json": resolved.platformsJson,
|
|
27
32
|
"platform-count": String(resolved.platformCount),
|
|
28
33
|
"platform-source": resolved.source,
|
|
34
|
+
"native-platforms-json": resolved.nativePlatformsJson,
|
|
35
|
+
"native-platform-count": String(resolved.nativePlatformCount),
|
|
36
|
+
"container-platforms-json": resolved.containerPlatformsJson,
|
|
37
|
+
"container-platform-count": String(resolved.containerPlatformCount),
|
|
38
|
+
"linux-container-enabled": String(resolved.linuxContainer.enabled),
|
|
39
|
+
"linux-container-preset": resolved.linuxContainer.preset,
|
|
40
|
+
"linux-container-image": resolved.linuxContainer.image,
|
|
41
|
+
"linux-container-source": resolved.linuxContainer.source,
|
|
29
42
|
});
|
|
30
43
|
return resolved;
|
|
31
44
|
}
|
|
32
45
|
if (mode === "artifact") {
|
|
33
46
|
const resolved = resolveArtifactContract({
|
|
34
|
-
artifactName:
|
|
47
|
+
artifactName:
|
|
48
|
+
process.env.BUILDCHAIN_ARTIFACT_NAME || "buildchain-artifact",
|
|
35
49
|
artifactNameTemplate:
|
|
36
|
-
process.env.BUILDCHAIN_ARTIFACT_NAME_TEMPLATE ||
|
|
50
|
+
process.env.BUILDCHAIN_ARTIFACT_NAME_TEMPLATE ||
|
|
51
|
+
"{artifact}-{platform}-{sha}",
|
|
37
52
|
platformId: process.env.BUILDCHAIN_PLATFORM_ID || "",
|
|
38
53
|
platformName: process.env.BUILDCHAIN_PLATFORM_NAME || "",
|
|
39
54
|
sha: process.env.BUILDCHAIN_SOURCE_SHA || process.env.GITHUB_SHA || "",
|
|
@@ -53,11 +68,16 @@ export function resolveBuildContractCli() {
|
|
|
53
68
|
throw new Error(`unsupported build contract mode: ${mode}`);
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
if (
|
|
71
|
+
if (
|
|
72
|
+
process.argv[1] &&
|
|
73
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
74
|
+
) {
|
|
57
75
|
try {
|
|
58
76
|
resolveBuildContractCli();
|
|
59
77
|
} catch (error) {
|
|
60
|
-
console.error(
|
|
78
|
+
console.error(
|
|
79
|
+
`::error::${String(error.message || error).replace(/\r?\n/g, "%0A")}`,
|
|
80
|
+
);
|
|
61
81
|
process.exitCode = 1;
|
|
62
82
|
}
|
|
63
83
|
}
|