@kungfu-tech/buildchain 2.4.6-alpha.0 → 2.4.6-alpha.1
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 +3 -0
- package/actions/promote-buildchain-ref/README.md +20 -0
- package/actions/report-buildchain-issue/README.md +32 -1
- package/dist/site/release-provenance.json +1 -0
- package/docs/MAP.md +4 -0
- package/docs/release-candidate.md +62 -0
- package/docs/reusable-build-surface.md +11 -0
- package/package.json +3 -1
- package/packages/core/index.js +11 -0
- package/packages/core/issue-reporting.js +156 -2
- package/packages/core/release-candidate.js +183 -0
- package/scripts/generate-release-candidate-passport.mjs +88 -0
package/README.md
CHANGED
|
@@ -122,6 +122,8 @@ Buildchain repository with a scoped issue-write token:
|
|
|
122
122
|
|
|
123
123
|
The action deduplicates by fingerprint, comments on existing open reports, and
|
|
124
124
|
is fail-soft by default so issue reporting does not hide the original failure.
|
|
125
|
+
Use `report-kind: workflow-friction` when Buildchain workflows should report
|
|
126
|
+
their own repeated release friction back to the Buildchain issue tracker.
|
|
125
127
|
|
|
126
128
|
## Use Buildchain
|
|
127
129
|
|
|
@@ -252,6 +254,7 @@ npm pack --dry-run --json --registry=https://registry.npmjs.org/
|
|
|
252
254
|
- [Site bundle contract](docs/site-bundle-contract.md)
|
|
253
255
|
- [Lifecycle protocol](docs/lifecycle-protocol.md)
|
|
254
256
|
- [Reusable build surface](docs/reusable-build-surface.md)
|
|
257
|
+
- [Release candidate passport](docs/release-candidate.md)
|
|
255
258
|
- [Consumer issue reporting](docs/consumer-issue-reporting.md)
|
|
256
259
|
- [Publish transaction](docs/publish-transaction.md)
|
|
257
260
|
- [Release governance](docs/release-governance.md)
|
|
@@ -131,6 +131,26 @@ channel ref such as `alpha/v22/v22.22` or `release/v22/v22.22` to already point
|
|
|
131
131
|
at the locked `publish-source-sha`. If not, maintainers should merge the source
|
|
132
132
|
commit through the channel PR first.
|
|
133
133
|
|
|
134
|
+
For promote-only release candidates, attach the PR-stage reusable build evidence
|
|
135
|
+
and fail before publish-gate side effects if it no longer matches:
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
- uses: kungfu-systems/buildchain/actions/promote-buildchain-ref@v2
|
|
139
|
+
with:
|
|
140
|
+
token: ${{ secrets.BUILDCHAIN_PROMOTION_TOKEN }}
|
|
141
|
+
sha: ${{ needs.build.outputs.publish-source-sha }}
|
|
142
|
+
target-ref: alpha/v22/v22.22
|
|
143
|
+
promote-only-release-candidate: "true"
|
|
144
|
+
release-candidate-passport-path: .buildchain/artifacts/release-candidate-passport.json
|
|
145
|
+
release-candidate-build-summary-path: .buildchain/artifacts/build-summary.json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The action validates repository, channel, source SHA, platform matrix, and the
|
|
149
|
+
aggregate build-summary hash before it writes version state, opens release-state,
|
|
150
|
+
runs publish transaction logic, or moves tags/branches. If validation fails, run
|
|
151
|
+
or attach the verified channel PR build first instead of promoting a stale or
|
|
152
|
+
unproven artifact set.
|
|
153
|
+
|
|
134
154
|
When enabled, the action creates or resumes a release transaction keyed by
|
|
135
155
|
repository, version, source SHA, and target ref. It persists that transaction to
|
|
136
156
|
a machine-managed branch under `buildchain/release-state/<version>`, with
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# report-buildchain-issue
|
|
2
2
|
|
|
3
|
-
Create or update a Buildchain repository issue from a consumer workflow
|
|
3
|
+
Create or update a Buildchain repository issue from a consumer workflow or from
|
|
4
|
+
Buildchain's own workflow-friction feedback loop.
|
|
4
5
|
|
|
5
6
|
This action is intended for consumer repositories that need to report
|
|
6
7
|
Buildchain-owned failures with enough evidence for maintainers to act. It
|
|
@@ -44,3 +45,33 @@ By default issue reporting is fail-soft:
|
|
|
44
45
|
|
|
45
46
|
Use `dry-run: "true"` to verify the computed fingerprint and body shape without
|
|
46
47
|
calling GitHub.
|
|
48
|
+
|
|
49
|
+
For Buildchain-owned workflow friction, use `report-kind: workflow-friction`.
|
|
50
|
+
This uses a separate marker and default labels so duplicate PRs, duplicate
|
|
51
|
+
builds, transient API failures, stale release-state, or missing RC evidence can
|
|
52
|
+
be grouped without mixing with consumer failure reports:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
permissions:
|
|
56
|
+
issues: write
|
|
57
|
+
|
|
58
|
+
steps:
|
|
59
|
+
- uses: kungfu-systems/buildchain/actions/report-buildchain-issue@v2
|
|
60
|
+
if: failure()
|
|
61
|
+
with:
|
|
62
|
+
token: ${{ github.token }}
|
|
63
|
+
report-kind: workflow-friction
|
|
64
|
+
target-repository: ${{ github.repository }}
|
|
65
|
+
repository: ${{ github.repository }}
|
|
66
|
+
workflow: ${{ github.workflow }}
|
|
67
|
+
run-id: ${{ github.run_id }}
|
|
68
|
+
run-attempt: ${{ github.run_attempt }}
|
|
69
|
+
channel: alpha/v2/v2.4
|
|
70
|
+
source-sha: ${{ github.sha }}
|
|
71
|
+
friction-class: duplicate-build
|
|
72
|
+
comment-cooldown-hours: "24"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`comment-cooldown-hours` is optional. When it is greater than zero, Buildchain
|
|
76
|
+
still deduplicates by fingerprint but skips adding another comment if the
|
|
77
|
+
existing issue already received a recent update.
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"./diagnostics": "./packages/core/diagnostics.js",
|
|
14
14
|
"./issue-reporting": "./packages/core/issue-reporting.js",
|
|
15
15
|
"./logging": "./packages/core/logging.js",
|
|
16
|
+
"./release-candidate": "./packages/core/release-candidate.js",
|
|
16
17
|
"./release-passport": "./packages/core/release-passport.js",
|
|
17
18
|
"./site/buildchain-site.json": "./dist/site/buildchain-site.json",
|
|
18
19
|
"./site/site-manifest.json": "./dist/site/site-manifest.json",
|
package/docs/MAP.md
CHANGED
|
@@ -31,6 +31,7 @@ running artifact), *use* (consume / extend) - and a **status**:
|
|
|
31
31
|
| How do I declare version files and custom lifecycle commands? | [`lifecycle-protocol.md`](lifecycle-protocol.md) | use | stable |
|
|
32
32
|
| How does publish evidence, recovery, and finalization work? | [`publish-transaction.md`](publish-transaction.md) | verify | stable |
|
|
33
33
|
| How do I publish or verify release passport artifacts? | [`release-passport.md`](release-passport.md) | use | stable |
|
|
34
|
+
| How do I prove a PR-stage reusable build is the artifact source promoted later? | [`release-candidate.md`](release-candidate.md) + [`reusable-build-surface.md`](reusable-build-surface.md) | verify | stable |
|
|
34
35
|
| Why are binary release assets archived by platform, and where is the single bundle? | [`binary-distribution.md`](binary-distribution.md) | verify | stable |
|
|
35
36
|
| How do I add timestamped logs inside build scripts? | [`toolkit-observability.md`](toolkit-observability.md) | use | stable |
|
|
36
37
|
| What package-owned facts should buildchain.libkungfu.dev render? | [`site-bundle-contract.md`](site-bundle-contract.md) | use | stable |
|
|
@@ -67,6 +68,9 @@ running artifact), *use* (consume / extend) - and a **status**:
|
|
|
67
68
|
[`reusable-build-surface.md`](reusable-build-surface.md).
|
|
68
69
|
- **consumer workflow feedback / automatic Buildchain GitHub issues** ->
|
|
69
70
|
[`consumer-issue-reporting.md`](consumer-issue-reporting.md).
|
|
71
|
+
- **PR-stage RC artifacts / promote-only release candidates** ->
|
|
72
|
+
[`release-candidate.md`](release-candidate.md) and
|
|
73
|
+
[`reusable-build-surface.md`](reusable-build-surface.md).
|
|
70
74
|
- **infra contract / observed infrastructure outputs / downstream contract propagation** ->
|
|
71
75
|
[`infra-contract.md`](infra-contract.md).
|
|
72
76
|
- **standalone binary install / platform archives / GitHub Release bundle** ->
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Release Candidate Passport
|
|
2
|
+
|
|
3
|
+
The release-candidate passport is the pre-promotion evidence contract produced
|
|
4
|
+
after a reusable build matrix succeeds and before any publish-gate side effects
|
|
5
|
+
run. It is different from the release passport:
|
|
6
|
+
|
|
7
|
+
- `release-candidate-passport.json` proves which source SHA, channel, runtime,
|
|
8
|
+
workflow run, and platform artifacts were verified before promotion.
|
|
9
|
+
- `buildchain.release.json` is generated after publish finalization and remains
|
|
10
|
+
the durable audit entrypoint for the published release.
|
|
11
|
+
|
|
12
|
+
Enable it on the reusable build workflow:
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v2
|
|
18
|
+
with:
|
|
19
|
+
artifact-name: libnode
|
|
20
|
+
release-candidate: true
|
|
21
|
+
publish-channel: alpha
|
|
22
|
+
publish-source-ref: publish-gate/alpha/v22/v22.22/22.22.3-kf.3-alpha.7
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
When the platform matrix and aggregate summaries complete, Buildchain uploads:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
<artifact-name>-release-candidate-<publish-source-sha>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The passport contract is `kungfu-buildchain-release-candidate-passport`. It
|
|
32
|
+
contains:
|
|
33
|
+
|
|
34
|
+
- repository and pull request context;
|
|
35
|
+
- target channel, target ref, and product version or a non-publish
|
|
36
|
+
`source-<shortSha>` candidate label;
|
|
37
|
+
- source head SHA, merge ref SHA, and source tree hash;
|
|
38
|
+
- Buildchain runtime ref/SHA and workflow shell ref;
|
|
39
|
+
- workflow run id/attempt/url;
|
|
40
|
+
- normalized platform matrix and artifact summaries;
|
|
41
|
+
- the hash of the aggregate `build-summary.json`.
|
|
42
|
+
|
|
43
|
+
Promotion workflows that should not rebuild artifacts can enable:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
- uses: kungfu-systems/buildchain/actions/promote-buildchain-ref@v2
|
|
47
|
+
with:
|
|
48
|
+
token: ${{ secrets.BUILDCHAIN_PROMOTION_TOKEN }}
|
|
49
|
+
sha: ${{ needs.build.outputs.publish-source-sha }}
|
|
50
|
+
target-ref: alpha/v22/v22.22
|
|
51
|
+
promote-only-release-candidate: "true"
|
|
52
|
+
release-candidate-passport-path: .buildchain/artifacts/release-candidate-passport.json
|
|
53
|
+
release-candidate-build-summary-path: .buildchain/artifacts/build-summary.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
With `promote-only-release-candidate: "true"`, promotion fails before
|
|
57
|
+
version-state, publish transaction, tag, or branch side effects when the
|
|
58
|
+
passport does not match the repository, channel, source SHA, platform matrix,
|
|
59
|
+
or build-summary hash. The diagnostic tells maintainers to run or attach the
|
|
60
|
+
verified channel PR build first instead of allowing publish-gate to discover
|
|
61
|
+
the mismatch late.
|
|
62
|
+
|
|
@@ -25,6 +25,7 @@ jobs:
|
|
|
25
25
|
expected-artifacts-json: >-
|
|
26
26
|
{"minFiles":2,"requiredPaths":["dist/libnode.tar.gz","dist/checksums.txt"]}
|
|
27
27
|
process-summary-path: .buildchain/diagnostics/process-summary.json
|
|
28
|
+
release-candidate: true
|
|
28
29
|
publish-channel: release
|
|
29
30
|
publish-source-ref: publish-gate/release/v22/v22.22/22.22.3-kf.0
|
|
30
31
|
```
|
|
@@ -190,6 +191,8 @@ The reusable workflow exposes the resolved contract:
|
|
|
190
191
|
| `linux-container-image` | Resolved digest-pinned Linux job container image |
|
|
191
192
|
| `build-summary-artifact` | Uploaded aggregate summary artifact name |
|
|
192
193
|
| `build-diagnostics-summary-artifact` | Uploaded aggregate diagnostics summary artifact name |
|
|
194
|
+
| `release-candidate-passport-artifact` | Uploaded PR-stage release-candidate passport artifact name when `release-candidate` is enabled |
|
|
195
|
+
| `release-candidate-passport-json` | Compact release-candidate passport JSON when `release-candidate` is enabled |
|
|
193
196
|
| `build-summary-json` | Compact aggregate JSON with platform count, file count, and byte total |
|
|
194
197
|
| `build-diagnostics-summary-json` | Compact aggregate diagnostics JSON with platform, lifecycle warning/error, diagnostics contract warning, and sidecar manifest warning totals |
|
|
195
198
|
| `trusted-event` | `true` when the event is trusted enough to reach build runners |
|
|
@@ -233,6 +236,14 @@ release jobs can detect drifting diagnostics JSON contracts and missing or
|
|
|
233
236
|
drifting diagnostics sidecar manifests without downloading the per-platform
|
|
234
237
|
diagnostics artifacts first.
|
|
235
238
|
|
|
239
|
+
Set `release-candidate: true` when the successful reusable build is meant to be
|
|
240
|
+
the artifact source promoted later. Buildchain then uploads
|
|
241
|
+
`release-candidate-passport.json` under the
|
|
242
|
+
`<artifact-name>-release-candidate-<publish-source-sha>` artifact name. Promotion
|
|
243
|
+
jobs can pass that passport to `promote-buildchain-ref` with
|
|
244
|
+
`promote-only-release-candidate: "true"` so source, channel, platforms, and the
|
|
245
|
+
aggregate build-summary hash are checked before publish-gate side effects.
|
|
246
|
+
|
|
236
247
|
## Publish Gate
|
|
237
248
|
|
|
238
249
|
Buildchain separates "may build/verify" from "may publish." A same-repository
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.4.6-alpha.
|
|
3
|
+
"version": "2.4.6-alpha.1",
|
|
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",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"./diagnostics": "./packages/core/diagnostics.js",
|
|
17
17
|
"./issue-reporting": "./packages/core/issue-reporting.js",
|
|
18
18
|
"./logging": "./packages/core/logging.js",
|
|
19
|
+
"./release-candidate": "./packages/core/release-candidate.js",
|
|
19
20
|
"./release-passport": "./packages/core/release-passport.js",
|
|
20
21
|
"./site/buildchain-site.json": "./dist/site/buildchain-site.json",
|
|
21
22
|
"./site/site-manifest.json": "./dist/site/site-manifest.json",
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"docs/product-mechanism.md",
|
|
51
52
|
"docs/publish-transaction.md",
|
|
52
53
|
"docs/release-passport.md",
|
|
54
|
+
"docs/release-candidate.md",
|
|
53
55
|
"docs/release-flow.md",
|
|
54
56
|
"docs/release-governance.md",
|
|
55
57
|
"docs/reusable-build-surface.md",
|
package/packages/core/index.js
CHANGED
|
@@ -85,6 +85,13 @@ export {
|
|
|
85
85
|
writeDiagnosticsArtifact,
|
|
86
86
|
} from "./diagnostics.js";
|
|
87
87
|
|
|
88
|
+
export {
|
|
89
|
+
RELEASE_CANDIDATE_PASSPORT_CONTRACT,
|
|
90
|
+
createReleaseCandidatePassport,
|
|
91
|
+
sha256Json,
|
|
92
|
+
validateReleaseCandidatePassport,
|
|
93
|
+
} from "./release-candidate.js";
|
|
94
|
+
|
|
88
95
|
export {
|
|
89
96
|
AGENT_INDEX_CONTRACT,
|
|
90
97
|
ARTIFACT_EVIDENCE_CONTRACT,
|
|
@@ -107,9 +114,11 @@ export {
|
|
|
107
114
|
|
|
108
115
|
export {
|
|
109
116
|
BUILDCHAIN_CONSUMER_ISSUE_CONTRACT,
|
|
117
|
+
BUILDCHAIN_WORKFLOW_FRICTION_ISSUE_CONTRACT,
|
|
110
118
|
DEFAULT_BUILDCHAIN_ISSUE_REPOSITORY,
|
|
111
119
|
GitHubIssueRequestError,
|
|
112
120
|
buildConsumerIssueReport,
|
|
121
|
+
buildWorkflowFrictionIssueReport,
|
|
113
122
|
computeConsumerIssueFingerprint,
|
|
114
123
|
consumerIssueMarker,
|
|
115
124
|
createGitHubIssueRequest,
|
|
@@ -118,5 +127,7 @@ export {
|
|
|
118
127
|
readOptionalIssueBodyFile,
|
|
119
128
|
redactIssueText,
|
|
120
129
|
reportBuildchainIssue,
|
|
130
|
+
reportWorkflowFrictionIssue,
|
|
121
131
|
truncateUtf8,
|
|
132
|
+
workflowFrictionMarker,
|
|
122
133
|
} from "./issue-reporting.js";
|
|
@@ -2,9 +2,11 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
|
|
4
4
|
export const BUILDCHAIN_CONSUMER_ISSUE_CONTRACT = "kungfu-buildchain-consumer-issue";
|
|
5
|
+
export const BUILDCHAIN_WORKFLOW_FRICTION_ISSUE_CONTRACT = "kungfu-buildchain-workflow-friction-issue";
|
|
5
6
|
export const DEFAULT_BUILDCHAIN_ISSUE_REPOSITORY = "kungfu-systems/buildchain";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_LABELS = ["buildchain-consumer-feedback"];
|
|
9
|
+
const DEFAULT_FRICTION_LABELS = ["buildchain-feedback", "workflow-friction"];
|
|
8
10
|
const DEFAULT_MAX_BODY_BYTES = 60_000;
|
|
9
11
|
const DEFAULT_RETRY_DELAYS_MS = [500, 1_500, 3_000];
|
|
10
12
|
|
|
@@ -75,6 +77,10 @@ export function consumerIssueMarker(fingerprint) {
|
|
|
75
77
|
return `buildchain-consumer-issue:fingerprint=${fingerprint}`;
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
export function workflowFrictionMarker(fingerprint) {
|
|
81
|
+
return `buildchain-workflow-friction:fingerprint=${fingerprint}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
78
84
|
export function truncateUtf8(text, maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
79
85
|
const value = String(text ?? "");
|
|
80
86
|
if (!Number.isFinite(maxBytes) || maxBytes <= 0 || Buffer.byteLength(value, "utf8") <= maxBytes) {
|
|
@@ -185,8 +191,95 @@ export function buildConsumerIssueReport(options = {}) {
|
|
|
185
191
|
};
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
export function buildWorkflowFrictionIssueReport(options = {}) {
|
|
195
|
+
const target = normalizeIssueRepository(options.targetRepository);
|
|
196
|
+
const env = options.env || process.env;
|
|
197
|
+
const repository = options.repository || env.GITHUB_REPOSITORY || "";
|
|
198
|
+
const workflow = options.workflow || env.GITHUB_WORKFLOW || "";
|
|
199
|
+
const runId = options.runId || env.GITHUB_RUN_ID || "";
|
|
200
|
+
const runAttempt = options.runAttempt || env.GITHUB_RUN_ATTEMPT || "";
|
|
201
|
+
const runUrl = options.runUrl || githubRunUrl(env, repository, runId);
|
|
202
|
+
const channel = options.channel || "";
|
|
203
|
+
const releaseIntent = options.releaseIntent || options.version || "";
|
|
204
|
+
const sourceRef = options.sourceRef || env.GITHUB_REF || "";
|
|
205
|
+
const sourceSha = options.sourceSha || env.GITHUB_SHA || "";
|
|
206
|
+
const frictionClass = options.frictionClass || "workflow-friction";
|
|
207
|
+
const fingerprint = options.fingerprint || computeConsumerIssueFingerprint({
|
|
208
|
+
targetRepository: target.fullName,
|
|
209
|
+
repository,
|
|
210
|
+
workflow,
|
|
211
|
+
channel,
|
|
212
|
+
releaseIntent,
|
|
213
|
+
sourceRef,
|
|
214
|
+
sourceSha,
|
|
215
|
+
frictionClass,
|
|
216
|
+
});
|
|
217
|
+
const marker = workflowFrictionMarker(fingerprint);
|
|
218
|
+
const heavyBuilds = Array.isArray(options.heavyBuilds) ? options.heavyBuilds : [];
|
|
219
|
+
const relatedRuns = Array.isArray(options.relatedRuns) ? options.relatedRuns : [];
|
|
220
|
+
const title = redactIssueText(
|
|
221
|
+
options.title || `[Buildchain feedback] ${frictionClass}: ${repository || "unknown repository"}`,
|
|
222
|
+
);
|
|
223
|
+
const body = truncateUtf8(
|
|
224
|
+
redactIssueText(
|
|
225
|
+
[
|
|
226
|
+
`<!-- ${marker} -->`,
|
|
227
|
+
`# Buildchain workflow friction`,
|
|
228
|
+
``,
|
|
229
|
+
options.summary ? `## Summary\n\n${options.summary}` : "",
|
|
230
|
+
`## Context`,
|
|
231
|
+
``,
|
|
232
|
+
`- Repository: ${repository || "(unknown)"}`,
|
|
233
|
+
`- Workflow: ${workflow || "(unknown)"}`,
|
|
234
|
+
`- Run: ${runUrl || runId || "(unknown)"}`,
|
|
235
|
+
`- Attempt: ${runAttempt || "(unknown)"}`,
|
|
236
|
+
`- Pull request: ${options.pullRequest || "(unknown)"}`,
|
|
237
|
+
`- Channel: ${channel || "(unknown)"}`,
|
|
238
|
+
`- Release intent: ${releaseIntent || "(unknown)"}`,
|
|
239
|
+
`- Source ref: ${sourceRef || "(unknown)"}`,
|
|
240
|
+
`- Source SHA: ${sourceSha || "(unknown)"}`,
|
|
241
|
+
`- Source tree: ${options.sourceTreeHash || "(unknown)"}`,
|
|
242
|
+
`- Friction class: ${frictionClass}`,
|
|
243
|
+
`- Fingerprint: ${fingerprint}`,
|
|
244
|
+
``,
|
|
245
|
+
relatedRuns.length ? `## Related runs\n\n${relatedRuns.map((run) => `- ${run}`).join("\n")}` : "",
|
|
246
|
+
heavyBuilds.length ? `## Heavy build jobs\n\n${heavyBuilds.map((job) => `- ${job.name || job}: ${job.durationMs || ""}`).join("\n")}` : "",
|
|
247
|
+
options.diagnosis ? `## Diagnosis\n\n${options.diagnosis}` : "",
|
|
248
|
+
options.nextAction ? `## Suggested next action\n\n${options.nextAction}` : "",
|
|
249
|
+
].filter(Boolean).join("\n"),
|
|
250
|
+
),
|
|
251
|
+
Number(options.maxBodyBytes || DEFAULT_MAX_BODY_BYTES),
|
|
252
|
+
);
|
|
253
|
+
const commentBody = truncateUtf8(
|
|
254
|
+
redactIssueText(
|
|
255
|
+
[
|
|
256
|
+
`New matching Buildchain workflow friction observed.`,
|
|
257
|
+
``,
|
|
258
|
+
`- Repository: ${repository || "(unknown)"}`,
|
|
259
|
+
`- Workflow: ${workflow || "(unknown)"}`,
|
|
260
|
+
`- Run: ${runUrl || runId || "(unknown)"}`,
|
|
261
|
+
`- Attempt: ${runAttempt || "(unknown)"}`,
|
|
262
|
+
`- Friction class: ${frictionClass}`,
|
|
263
|
+
`- Fingerprint: ${fingerprint}`,
|
|
264
|
+
options.summary ? `\n${options.summary}` : "",
|
|
265
|
+
].join("\n"),
|
|
266
|
+
),
|
|
267
|
+
Number(options.maxBodyBytes || DEFAULT_MAX_BODY_BYTES),
|
|
268
|
+
);
|
|
269
|
+
return {
|
|
270
|
+
contract: BUILDCHAIN_WORKFLOW_FRICTION_ISSUE_CONTRACT,
|
|
271
|
+
targetRepository: target.fullName,
|
|
272
|
+
fingerprint,
|
|
273
|
+
marker,
|
|
274
|
+
title,
|
|
275
|
+
body,
|
|
276
|
+
commentBody,
|
|
277
|
+
labels: parseIssueLabels(options.labels ?? DEFAULT_FRICTION_LABELS),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
188
281
|
export async function reportBuildchainIssue(options = {}) {
|
|
189
|
-
const report = buildConsumerIssueReport(options);
|
|
282
|
+
const report = options.report || buildConsumerIssueReport(options);
|
|
190
283
|
const target = normalizeIssueRepository(report.targetRepository);
|
|
191
284
|
const mode = options.mode || "create-or-comment";
|
|
192
285
|
if (options.dryRun) {
|
|
@@ -219,6 +312,26 @@ export async function reportBuildchainIssue(options = {}) {
|
|
|
219
312
|
report,
|
|
220
313
|
};
|
|
221
314
|
}
|
|
315
|
+
const cooldown = await evaluateCommentCooldown({
|
|
316
|
+
request,
|
|
317
|
+
target,
|
|
318
|
+
issueNumber: existing.number,
|
|
319
|
+
cooldownHours: options.commentCooldownHours,
|
|
320
|
+
now: options.now,
|
|
321
|
+
});
|
|
322
|
+
if (cooldown.active) {
|
|
323
|
+
return {
|
|
324
|
+
ok: true,
|
|
325
|
+
action: "cooldown",
|
|
326
|
+
created: false,
|
|
327
|
+
commented: false,
|
|
328
|
+
issueNumber: existing.number,
|
|
329
|
+
issueUrl: existing.html_url || existing.url || "",
|
|
330
|
+
fingerprint: report.fingerprint,
|
|
331
|
+
report,
|
|
332
|
+
cooldown,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
222
335
|
await request({
|
|
223
336
|
method: "POST",
|
|
224
337
|
path: `/repos/${target.owner}/${target.repo}/issues/${existing.number}/comments`,
|
|
@@ -251,6 +364,13 @@ export async function reportBuildchainIssue(options = {}) {
|
|
|
251
364
|
};
|
|
252
365
|
}
|
|
253
366
|
|
|
367
|
+
export async function reportWorkflowFrictionIssue(options = {}) {
|
|
368
|
+
return reportBuildchainIssue({
|
|
369
|
+
...options,
|
|
370
|
+
report: buildWorkflowFrictionIssueReport(options),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
254
374
|
export function readOptionalIssueBodyFile(filePath) {
|
|
255
375
|
const resolved = String(filePath || "").trim();
|
|
256
376
|
if (!resolved) {
|
|
@@ -321,13 +441,47 @@ export function createGitHubIssueRequest({
|
|
|
321
441
|
|
|
322
442
|
async function findOpenConsumerIssue({ request, target, report }) {
|
|
323
443
|
const query = encodeURIComponent(
|
|
324
|
-
`repo:${target.fullName} is:issue is:open "${consumerIssueMarker(report.fingerprint)}"`,
|
|
444
|
+
`repo:${target.fullName} is:issue is:open "${report.marker || consumerIssueMarker(report.fingerprint)}"`,
|
|
325
445
|
);
|
|
326
446
|
const result = await request({ method: "GET", path: `/search/issues?q=${query}&per_page=1` });
|
|
327
447
|
const items = Array.isArray(result.items) ? result.items : [];
|
|
328
448
|
return items[0];
|
|
329
449
|
}
|
|
330
450
|
|
|
451
|
+
async function evaluateCommentCooldown({
|
|
452
|
+
request,
|
|
453
|
+
target,
|
|
454
|
+
issueNumber,
|
|
455
|
+
cooldownHours = 0,
|
|
456
|
+
now = new Date(),
|
|
457
|
+
}) {
|
|
458
|
+
const hours = Number(cooldownHours || 0);
|
|
459
|
+
if (!Number.isFinite(hours) || hours <= 0) {
|
|
460
|
+
return { active: false, cooldownHours: 0 };
|
|
461
|
+
}
|
|
462
|
+
const result = await request({
|
|
463
|
+
method: "GET",
|
|
464
|
+
path: `/repos/${target.owner}/${target.repo}/issues/${issueNumber}/comments?per_page=100`,
|
|
465
|
+
});
|
|
466
|
+
const comments = Array.isArray(result) ? result : [];
|
|
467
|
+
const latest = comments[comments.length - 1]?.created_at || "";
|
|
468
|
+
if (!latest) {
|
|
469
|
+
return { active: false, cooldownHours: hours };
|
|
470
|
+
}
|
|
471
|
+
const latestMs = Date.parse(latest);
|
|
472
|
+
const nowMs = now instanceof Date ? now.getTime() : Date.parse(String(now));
|
|
473
|
+
if (!Number.isFinite(latestMs) || !Number.isFinite(nowMs)) {
|
|
474
|
+
return { active: false, cooldownHours: hours, latestCommentAt: latest };
|
|
475
|
+
}
|
|
476
|
+
const elapsedHours = (nowMs - latestMs) / 3_600_000;
|
|
477
|
+
return {
|
|
478
|
+
active: elapsedHours >= 0 && elapsedHours < hours,
|
|
479
|
+
cooldownHours: hours,
|
|
480
|
+
latestCommentAt: latest,
|
|
481
|
+
elapsedHours,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
331
485
|
async function createIssueWithLabelFallback({ request, target, report }) {
|
|
332
486
|
const payload = {
|
|
333
487
|
title: report.title,
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const RELEASE_CANDIDATE_PASSPORT_CONTRACT = "kungfu-buildchain-release-candidate-passport";
|
|
4
|
+
|
|
5
|
+
function nowIso() {
|
|
6
|
+
return new Date().toISOString();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function optionalString(value) {
|
|
10
|
+
return value === undefined || value === null ? "" : String(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function nonEmptyString(value, label) {
|
|
14
|
+
const normalized = optionalString(value).trim();
|
|
15
|
+
if (!normalized) {
|
|
16
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
17
|
+
}
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stableJson(value) {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
24
|
+
}
|
|
25
|
+
if (value && typeof value === "object") {
|
|
26
|
+
return `{${Object.keys(value)
|
|
27
|
+
.sort()
|
|
28
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
29
|
+
.join(",")}}`;
|
|
30
|
+
}
|
|
31
|
+
return JSON.stringify(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function sha256Json(value) {
|
|
35
|
+
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizePlatformEntry(platform = {}, index = 0) {
|
|
39
|
+
const platformId = optionalString(platform.platform?.id || platform.platformId || platform.platform || `platform-${index}`);
|
|
40
|
+
const artifacts = Array.isArray(platform.summary?.files)
|
|
41
|
+
? platform.summary.files
|
|
42
|
+
: Array.isArray(platform.artifacts)
|
|
43
|
+
? platform.artifacts
|
|
44
|
+
: [];
|
|
45
|
+
return {
|
|
46
|
+
platformId,
|
|
47
|
+
artifactName: optionalString(platform.artifactName),
|
|
48
|
+
runner: {
|
|
49
|
+
labels: Array.isArray(platform.runner?.labels) ? platform.runner.labels.map(String) : [],
|
|
50
|
+
os: optionalString(platform.runner?.os),
|
|
51
|
+
arch: optionalString(platform.runner?.arch),
|
|
52
|
+
},
|
|
53
|
+
lifecycle: platform.observability?.lifecycle?.stages || {},
|
|
54
|
+
summary: {
|
|
55
|
+
fileCount: Number(platform.summary?.fileCount || 0),
|
|
56
|
+
totalBytes: Number(platform.summary?.totalBytes || 0),
|
|
57
|
+
},
|
|
58
|
+
artifacts: artifacts.map((artifact, artifactIndex) => ({
|
|
59
|
+
name: optionalString(artifact.name || artifact.path || `artifact-${artifactIndex}`),
|
|
60
|
+
path: optionalString(artifact.path),
|
|
61
|
+
size: Number(artifact.size || artifact.sizeBytes || 0),
|
|
62
|
+
sha256: optionalString(artifact.sha256 || artifact.digest || artifact.checksum).replace(/^sha256:/, ""),
|
|
63
|
+
})),
|
|
64
|
+
manifestPath: optionalString(platform.manifestPath),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createReleaseCandidatePassport({
|
|
69
|
+
repository = "",
|
|
70
|
+
pullRequest = {},
|
|
71
|
+
targetChannel = "",
|
|
72
|
+
version = "",
|
|
73
|
+
sourceHeadSha = "",
|
|
74
|
+
baseSha = "",
|
|
75
|
+
mergeRefSha = "",
|
|
76
|
+
sourceTreeHash = "",
|
|
77
|
+
buildSummary = {},
|
|
78
|
+
buildchain = {},
|
|
79
|
+
workflow = {},
|
|
80
|
+
createdAt = nowIso(),
|
|
81
|
+
} = {}) {
|
|
82
|
+
const normalizedSummary = buildSummary && typeof buildSummary === "object" ? buildSummary : {};
|
|
83
|
+
const sourceSha = sourceHeadSha || normalizedSummary.publishSource?.sha || normalizedSummary.git?.sha || "";
|
|
84
|
+
const channel = targetChannel || normalizedSummary.publishSource?.channel || normalizedSummary.publishGate?.channel || "";
|
|
85
|
+
const candidate = {
|
|
86
|
+
schemaVersion: 1,
|
|
87
|
+
contract: RELEASE_CANDIDATE_PASSPORT_CONTRACT,
|
|
88
|
+
createdAt,
|
|
89
|
+
repository: nonEmptyString(repository || normalizedSummary.git?.repository, "repository"),
|
|
90
|
+
pullRequest: {
|
|
91
|
+
number: optionalString(pullRequest.number),
|
|
92
|
+
url: optionalString(pullRequest.url),
|
|
93
|
+
headRef: optionalString(pullRequest.headRef),
|
|
94
|
+
baseRef: optionalString(pullRequest.baseRef),
|
|
95
|
+
},
|
|
96
|
+
target: {
|
|
97
|
+
channel: nonEmptyString(channel, "targetChannel"),
|
|
98
|
+
ref: optionalString(normalizedSummary.publishSource?.ref || normalizedSummary.git?.ref),
|
|
99
|
+
version: nonEmptyString(version || normalizedSummary.publishSource?.consumerVersion, "version"),
|
|
100
|
+
},
|
|
101
|
+
source: {
|
|
102
|
+
headSha: nonEmptyString(sourceSha, "sourceHeadSha"),
|
|
103
|
+
baseSha: optionalString(baseSha),
|
|
104
|
+
mergeRefSha: optionalString(mergeRefSha || sourceSha),
|
|
105
|
+
treeHash: optionalString(sourceTreeHash || sha256Json(normalizedSummary.platforms || [])),
|
|
106
|
+
},
|
|
107
|
+
buildchain: {
|
|
108
|
+
ref: optionalString(buildchain.ref || normalizedSummary.runtime?.ref),
|
|
109
|
+
sha: optionalString(buildchain.sha || normalizedSummary.runtime?.sha),
|
|
110
|
+
version: optionalString(buildchain.version),
|
|
111
|
+
workflowShellRef: optionalString(buildchain.workflowShellRef || normalizedSummary.runtime?.workflowShellRef),
|
|
112
|
+
},
|
|
113
|
+
workflow: {
|
|
114
|
+
name: optionalString(workflow.name),
|
|
115
|
+
runId: optionalString(workflow.runId || normalizedSummary.git?.runId),
|
|
116
|
+
runAttempt: optionalString(workflow.runAttempt || normalizedSummary.git?.runAttempt),
|
|
117
|
+
url: optionalString(workflow.url),
|
|
118
|
+
},
|
|
119
|
+
platformMatrix: (Array.isArray(normalizedSummary.platforms) ? normalizedSummary.platforms : [])
|
|
120
|
+
.map((platform, index) => normalizePlatformEntry(platform, index)),
|
|
121
|
+
diagnostics: {
|
|
122
|
+
buildSummaryContract: optionalString(normalizedSummary.contract),
|
|
123
|
+
buildSummaryHash: sha256Json(normalizedSummary),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
candidate.candidateHash = sha256Json({
|
|
127
|
+
repository: candidate.repository,
|
|
128
|
+
target: candidate.target,
|
|
129
|
+
source: candidate.source,
|
|
130
|
+
platformMatrix: candidate.platformMatrix,
|
|
131
|
+
buildchain: candidate.buildchain,
|
|
132
|
+
});
|
|
133
|
+
return candidate;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function validateReleaseCandidatePassport({
|
|
137
|
+
passport,
|
|
138
|
+
repository = "",
|
|
139
|
+
targetChannel = "",
|
|
140
|
+
version = "",
|
|
141
|
+
sourceHeadSha = "",
|
|
142
|
+
buildSummary = undefined,
|
|
143
|
+
requirePlatforms = true,
|
|
144
|
+
} = {}) {
|
|
145
|
+
const errors = [];
|
|
146
|
+
const check = (condition, message) => {
|
|
147
|
+
if (!condition) {
|
|
148
|
+
errors.push(message);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
check(passport && typeof passport === "object" && !Array.isArray(passport), "passport must be an object");
|
|
152
|
+
if (!passport || typeof passport !== "object" || Array.isArray(passport)) {
|
|
153
|
+
return { ok: false, errors };
|
|
154
|
+
}
|
|
155
|
+
check(passport.contract === RELEASE_CANDIDATE_PASSPORT_CONTRACT, `contract must be ${RELEASE_CANDIDATE_PASSPORT_CONTRACT}`);
|
|
156
|
+
check(Number(passport.schemaVersion) === 1, "schemaVersion must be 1");
|
|
157
|
+
if (repository) {
|
|
158
|
+
check(passport.repository === repository, `repository mismatch: expected ${repository}, got ${passport.repository || "<empty>"}`);
|
|
159
|
+
}
|
|
160
|
+
if (targetChannel) {
|
|
161
|
+
check(passport.target?.channel === targetChannel, `target channel mismatch: expected ${targetChannel}, got ${passport.target?.channel || "<empty>"}`);
|
|
162
|
+
}
|
|
163
|
+
if (version) {
|
|
164
|
+
check(passport.target?.version === version, `version mismatch: expected ${version}, got ${passport.target?.version || "<empty>"}`);
|
|
165
|
+
}
|
|
166
|
+
if (sourceHeadSha) {
|
|
167
|
+
check(passport.source?.headSha === sourceHeadSha, `source head mismatch: expected ${sourceHeadSha}, got ${passport.source?.headSha || "<empty>"}`);
|
|
168
|
+
}
|
|
169
|
+
check(Boolean(passport.source?.mergeRefSha || passport.source?.treeHash), "source mergeRefSha or treeHash is required");
|
|
170
|
+
check(Array.isArray(passport.platformMatrix), "platformMatrix must be an array");
|
|
171
|
+
if (requirePlatforms) {
|
|
172
|
+
check((passport.platformMatrix || []).length > 0, "platformMatrix must include at least one platform");
|
|
173
|
+
}
|
|
174
|
+
for (const [index, platform] of (passport.platformMatrix || []).entries()) {
|
|
175
|
+
check(Boolean(platform.platformId), `platformMatrix[${index}].platformId is required`);
|
|
176
|
+
check(Boolean(platform.artifactName), `platformMatrix[${index}].artifactName is required`);
|
|
177
|
+
}
|
|
178
|
+
if (buildSummary) {
|
|
179
|
+
const expectedHash = sha256Json(buildSummary);
|
|
180
|
+
check(passport.diagnostics?.buildSummaryHash === expectedHash, "build summary hash mismatch");
|
|
181
|
+
}
|
|
182
|
+
return { ok: errors.length === 0, errors };
|
|
183
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
createReleaseCandidatePassport,
|
|
7
|
+
validateReleaseCandidatePassport,
|
|
8
|
+
} from "../packages/core/release-candidate.js";
|
|
9
|
+
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
10
|
+
|
|
11
|
+
function env(name, fallback = "") {
|
|
12
|
+
return process.env[name] || fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readJsonFile(filePath) {
|
|
16
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function generateReleaseCandidatePassportCli() {
|
|
20
|
+
const buildSummaryPath = path.resolve(env("BUILDCHAIN_BUILD_SUMMARY_PATH", ".buildchain/artifacts/build-summary.json"));
|
|
21
|
+
const outputPath = path.resolve(env("BUILDCHAIN_RC_PASSPORT_PATH", ".buildchain/artifacts/release-candidate-passport.json"));
|
|
22
|
+
const buildSummary = readJsonFile(buildSummaryPath);
|
|
23
|
+
const sourceSha = env("BUILDCHAIN_RC_SOURCE_HEAD_SHA", buildSummary.publishSource?.sha || buildSummary.git?.sha || "");
|
|
24
|
+
const version = env(
|
|
25
|
+
"BUILDCHAIN_RC_VERSION",
|
|
26
|
+
buildSummary.publishSource?.consumerVersion || `source-${String(sourceSha).slice(0, 12)}`,
|
|
27
|
+
);
|
|
28
|
+
const passport = createReleaseCandidatePassport({
|
|
29
|
+
repository: env("GITHUB_REPOSITORY", buildSummary.git?.repository || ""),
|
|
30
|
+
pullRequest: {
|
|
31
|
+
number: env("BUILDCHAIN_PULL_REQUEST_NUMBER"),
|
|
32
|
+
url: env("BUILDCHAIN_PULL_REQUEST_URL"),
|
|
33
|
+
headRef: env("BUILDCHAIN_PULL_REQUEST_HEAD_REF"),
|
|
34
|
+
baseRef: env("BUILDCHAIN_PULL_REQUEST_BASE_REF"),
|
|
35
|
+
},
|
|
36
|
+
targetChannel: env("BUILDCHAIN_RC_TARGET_CHANNEL", buildSummary.publishSource?.channel || buildSummary.publishGate?.channel || ""),
|
|
37
|
+
version,
|
|
38
|
+
sourceHeadSha: sourceSha,
|
|
39
|
+
baseSha: env("BUILDCHAIN_RC_BASE_SHA"),
|
|
40
|
+
mergeRefSha: env("BUILDCHAIN_RC_MERGE_REF_SHA", buildSummary.git?.sha || ""),
|
|
41
|
+
sourceTreeHash: env("BUILDCHAIN_RC_SOURCE_TREE_HASH"),
|
|
42
|
+
buildSummary,
|
|
43
|
+
buildchain: {
|
|
44
|
+
ref: env("BUILDCHAIN_RUNTIME_REF", buildSummary.runtime?.ref || ""),
|
|
45
|
+
sha: env("BUILDCHAIN_RUNTIME_SHA", buildSummary.runtime?.sha || ""),
|
|
46
|
+
version: env("BUILDCHAIN_RUNTIME_VERSION"),
|
|
47
|
+
workflowShellRef: env("BUILDCHAIN_WORKFLOW_SHELL_REF", buildSummary.runtime?.workflowShellRef || ""),
|
|
48
|
+
},
|
|
49
|
+
workflow: {
|
|
50
|
+
name: env("GITHUB_WORKFLOW"),
|
|
51
|
+
runId: env("GITHUB_RUN_ID", buildSummary.git?.runId || ""),
|
|
52
|
+
runAttempt: env("GITHUB_RUN_ATTEMPT", buildSummary.git?.runAttempt || ""),
|
|
53
|
+
url: env("BUILDCHAIN_WORKFLOW_RUN_URL"),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const validation = validateReleaseCandidatePassport({
|
|
57
|
+
passport,
|
|
58
|
+
repository: env("GITHUB_REPOSITORY", buildSummary.git?.repository || ""),
|
|
59
|
+
sourceHeadSha: sourceSha,
|
|
60
|
+
buildSummary,
|
|
61
|
+
});
|
|
62
|
+
if (!validation.ok) {
|
|
63
|
+
throw new Error(`release candidate passport invalid: ${validation.errors.join("; ")}`);
|
|
64
|
+
}
|
|
65
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
66
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(passport, null, 2)}\n`);
|
|
67
|
+
writeGitHubOutputs({
|
|
68
|
+
"release-candidate-passport-path": path.relative(process.cwd(), outputPath).split(path.sep).join("/"),
|
|
69
|
+
"release-candidate-passport-json": JSON.stringify({
|
|
70
|
+
contract: passport.contract,
|
|
71
|
+
repository: passport.repository,
|
|
72
|
+
target: passport.target,
|
|
73
|
+
source: passport.source,
|
|
74
|
+
candidateHash: passport.candidateHash,
|
|
75
|
+
platformCount: passport.platformMatrix.length,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
return passport;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
82
|
+
try {
|
|
83
|
+
generateReleaseCandidatePassportCli();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`::error::${String(error.message || error).replace(/\r?\n/g, "%0A")}`);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
}
|
|
88
|
+
}
|