package-registry-manager 0.22.8 → 0.22.9
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/package.json +3 -3
- package/src/flows.mjs +10 -5
- package/src/release-runs.mjs +117 -0
- package/src/repository-repair.mjs +11 -23
- package/src/setup.mjs +21 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "package-registry-manager",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.9",
|
|
4
4
|
"description": "Inspect repositories and guide secure package-registry setup",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/29808e088ac8fd8374361920bbebab07a4d63ac6",
|
|
44
|
-
"browser-commander": "0.
|
|
45
|
-
"command-stream": "^2.0.
|
|
44
|
+
"browser-commander": "^0.30.1",
|
|
45
|
+
"command-stream": "^2.0.2",
|
|
46
46
|
"playwright": "^1.63.0",
|
|
47
47
|
"selenium-webdriver": "^4.50.0"
|
|
48
48
|
},
|
package/src/flows.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RELEASE_RUN_FIELDS } from "./release-runs.mjs";
|
|
1
2
|
import { credentialSteps } from "./credential-cycle.mjs";
|
|
2
3
|
import { DEFAULT_TRUST_NPM, NPM_TFA_URL } from "./prerequisites.mjs";
|
|
3
4
|
import {
|
|
@@ -186,9 +187,9 @@ function releaseRunSteps(context) {
|
|
|
186
187
|
return [
|
|
187
188
|
step(
|
|
188
189
|
"inspect-release-run",
|
|
189
|
-
"Read the
|
|
190
|
+
"Read the failed default-branch release run",
|
|
190
191
|
"check",
|
|
191
|
-
"
|
|
192
|
+
"Check push and workflow_dispatch runs on the default branch and prefer the newest failed publish or preflight job.",
|
|
192
193
|
{
|
|
193
194
|
command: command("gh", [
|
|
194
195
|
"run",
|
|
@@ -196,10 +197,14 @@ function releaseRunSteps(context) {
|
|
|
196
197
|
...repo,
|
|
197
198
|
"--workflow",
|
|
198
199
|
context.workflow,
|
|
200
|
+
"--branch",
|
|
201
|
+
"{default_branch}",
|
|
202
|
+
"--event",
|
|
203
|
+
"push",
|
|
199
204
|
"--limit",
|
|
200
|
-
"
|
|
205
|
+
"100",
|
|
201
206
|
"--json",
|
|
202
|
-
|
|
207
|
+
RELEASE_RUN_FIELDS,
|
|
203
208
|
]),
|
|
204
209
|
when: "trust-missing",
|
|
205
210
|
cwd: ".",
|
|
@@ -209,7 +214,7 @@ function releaseRunSteps(context) {
|
|
|
209
214
|
"read-release-failure",
|
|
210
215
|
"Read why the release run failed",
|
|
211
216
|
"check",
|
|
212
|
-
"
|
|
217
|
+
"Read the selected run's failed jobs for trusted-publishing or OIDC exchange rejection.",
|
|
213
218
|
{
|
|
214
219
|
command: command("gh", [
|
|
215
220
|
"run",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { repositorySlug } from "./repository-identity.mjs";
|
|
2
|
+
|
|
3
|
+
export const RELEASE_RUN_FIELDS =
|
|
4
|
+
"databaseId,conclusion,status,url,headSha,headBranch,event,createdAt";
|
|
5
|
+
/** Registry rejection of the workflow's trusted-publishing identity. */
|
|
6
|
+
export const PUBLISH_REJECTED =
|
|
7
|
+
/\bE404\b|404 Not Found|invalid[-_ ]publisher|(?:npm|crates\.io|PyPI) refused the OIDC (?:package |token )?exchange\s*\((?:400|401|403|404)\)|trusted publisher[^\n]*(?:does not match|mismatch)|(?:invalid|unregistered)[-_ ](?:publishing[-_ ])?publisher/i;
|
|
8
|
+
const PUBLISH_JOB = /(?:^|[^a-z])(?:publish(?:ing)?|preflight)(?:[^a-z]|$)/i;
|
|
9
|
+
|
|
10
|
+
/** Only failed publish/preflight jobs (or failed publishing steps) qualify. */
|
|
11
|
+
export function failedPublishingJob(jobs) {
|
|
12
|
+
return jobs.some(
|
|
13
|
+
(job) =>
|
|
14
|
+
job.conclusion === "failure" &&
|
|
15
|
+
(PUBLISH_JOB.test(job.name ?? "") ||
|
|
16
|
+
job.steps?.some(
|
|
17
|
+
(step) =>
|
|
18
|
+
step.conclusion === "failure" && PUBLISH_JOB.test(step.name ?? ""),
|
|
19
|
+
)),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Keep PRs, tags and other branches out even if a tool ignores its filters. */
|
|
24
|
+
export function eligibleReleaseRuns(runs, branch) {
|
|
25
|
+
const unique = new Map();
|
|
26
|
+
for (const run of runs) {
|
|
27
|
+
if (
|
|
28
|
+
run.headBranch === branch &&
|
|
29
|
+
["push", "workflow_dispatch"].includes(run.event)
|
|
30
|
+
) {
|
|
31
|
+
unique.set(run.databaseId, run);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [...unique.values()].sort(
|
|
35
|
+
(left, right) =>
|
|
36
|
+
String(right.createdAt).localeCompare(String(left.createdAt)) ||
|
|
37
|
+
Number(right.databaseId) - Number(left.databaseId),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Cache one selection so inspection and repair retry the same release. */
|
|
42
|
+
export async function selectReleaseRun(session) {
|
|
43
|
+
if (session.releaseSelection) {
|
|
44
|
+
return session.releaseSelection;
|
|
45
|
+
}
|
|
46
|
+
const slug = repositorySlug(session.plan.repository);
|
|
47
|
+
const capture = (args) =>
|
|
48
|
+
session.capture({ command: { program: "gh", args }, cwd: "." });
|
|
49
|
+
const result = await capture([
|
|
50
|
+
"api",
|
|
51
|
+
`repos/${slug}`,
|
|
52
|
+
"--jq",
|
|
53
|
+
".default_branch",
|
|
54
|
+
]);
|
|
55
|
+
const branch = String(result.stdout ?? "").trim();
|
|
56
|
+
if (
|
|
57
|
+
result.code !== 0 ||
|
|
58
|
+
!branch ||
|
|
59
|
+
branch.startsWith("-") ||
|
|
60
|
+
/[\s:]/.test(branch)
|
|
61
|
+
) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"could not determine the release workflow's default branch",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const runs = [];
|
|
67
|
+
// Each event gets its own bounded history, so PR traffic cannot hide releases.
|
|
68
|
+
for (const event of ["push", "workflow_dispatch"]) {
|
|
69
|
+
const list = await capture([
|
|
70
|
+
"run",
|
|
71
|
+
"list",
|
|
72
|
+
"--repo",
|
|
73
|
+
slug,
|
|
74
|
+
"--workflow",
|
|
75
|
+
session.plan.trusted_publisher.workflow,
|
|
76
|
+
"--branch",
|
|
77
|
+
branch,
|
|
78
|
+
"--event",
|
|
79
|
+
event,
|
|
80
|
+
"--limit",
|
|
81
|
+
"100",
|
|
82
|
+
"--json",
|
|
83
|
+
RELEASE_RUN_FIELDS,
|
|
84
|
+
]);
|
|
85
|
+
if (list.code !== 0) {
|
|
86
|
+
throw new Error("could not list the release workflow runs");
|
|
87
|
+
}
|
|
88
|
+
runs.push(...JSON.parse(String(list.stdout || "[]")));
|
|
89
|
+
}
|
|
90
|
+
const eligible = eligibleReleaseRuns(runs, branch);
|
|
91
|
+
for (const run of eligible.filter((item) => item.conclusion === "failure")) {
|
|
92
|
+
const details = await capture([
|
|
93
|
+
"run",
|
|
94
|
+
"view",
|
|
95
|
+
String(run.databaseId),
|
|
96
|
+
"--repo",
|
|
97
|
+
slug,
|
|
98
|
+
"--json",
|
|
99
|
+
"jobs",
|
|
100
|
+
]);
|
|
101
|
+
if (details.code !== 0) {
|
|
102
|
+
throw new Error("could not read the failed release jobs");
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
failedPublishingJob(JSON.parse(String(details.stdout || "{}")).jobs ?? [])
|
|
106
|
+
) {
|
|
107
|
+
session.releaseSelection = { run, publishFailure: true, branch };
|
|
108
|
+
return session.releaseSelection;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
session.releaseSelection = {
|
|
112
|
+
run: eligible[0],
|
|
113
|
+
publishFailure: false,
|
|
114
|
+
branch,
|
|
115
|
+
};
|
|
116
|
+
return session.releaseSelection;
|
|
117
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectReleaseRun } from "./release-runs.mjs";
|
|
1
2
|
import { pageFetchScript } from "./crates-api.mjs";
|
|
2
3
|
import {
|
|
3
4
|
publisherIdentities,
|
|
@@ -306,31 +307,11 @@ export async function runRepositoryRepair(session, step) {
|
|
|
306
307
|
throw new Error("verify repository repairs before retrying release jobs");
|
|
307
308
|
}
|
|
308
309
|
const expected = repositorySlug(session.plan.repository);
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
program: "gh",
|
|
312
|
-
args: [
|
|
313
|
-
"run",
|
|
314
|
-
"list",
|
|
315
|
-
"--repo",
|
|
316
|
-
expected,
|
|
317
|
-
"--workflow",
|
|
318
|
-
session.plan.trusted_publisher.workflow,
|
|
319
|
-
"--limit",
|
|
320
|
-
"1",
|
|
321
|
-
"--json",
|
|
322
|
-
"databaseId,conclusion,headSha",
|
|
323
|
-
],
|
|
324
|
-
},
|
|
325
|
-
cwd: ".",
|
|
326
|
-
});
|
|
327
|
-
if (result.code !== 0) {
|
|
328
|
-
throw new Error("could not inspect release jobs after repository repair");
|
|
329
|
-
}
|
|
330
|
-
const [run] = JSON.parse(result.stdout || "[]");
|
|
331
|
-
if (run?.conclusion !== "failure") {
|
|
310
|
+
const { run, publishFailure, branch } = await selectReleaseRun(session);
|
|
311
|
+
if (!publishFailure) {
|
|
332
312
|
return;
|
|
333
313
|
}
|
|
314
|
+
console.log(` Selected failed release run: ${run.url}`);
|
|
334
315
|
const { verifyRemoteManifest } = await import("./manifest-proposal.mjs");
|
|
335
316
|
const corrected =
|
|
336
317
|
Boolean(run.headSha) &&
|
|
@@ -363,10 +344,17 @@ export async function runRepositoryRepair(session, step) {
|
|
|
363
344
|
session.plan.trusted_publisher.workflow,
|
|
364
345
|
"--repo",
|
|
365
346
|
expected,
|
|
347
|
+
"--ref",
|
|
348
|
+
branch,
|
|
366
349
|
],
|
|
367
350
|
};
|
|
351
|
+
if (corrected) {
|
|
352
|
+
session.values ??= {};
|
|
353
|
+
session.values.failed_run_id = String(run.databaseId);
|
|
354
|
+
}
|
|
368
355
|
await session.command({
|
|
369
356
|
...step,
|
|
357
|
+
id: corrected ? "rerun-release" : "retry-repaired-release",
|
|
370
358
|
kind: "command",
|
|
371
359
|
command,
|
|
372
360
|
cwd: ".",
|
package/src/setup.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { packageDirectory } from "./plan.mjs";
|
|
|
33
33
|
import { twoFactorMode } from "./prerequisites.mjs";
|
|
34
34
|
import { getJson, probePackage } from "./registry-state.mjs";
|
|
35
35
|
import { launchBrowser, signInDomains } from "./sign-in-import.mjs";
|
|
36
|
+
import { PUBLISH_REJECTED, selectReleaseRun } from "./release-runs.mjs";
|
|
36
37
|
import { runRepositoryRepair } from "./repository-repair.mjs";
|
|
37
38
|
import {
|
|
38
39
|
publisherIdentities,
|
|
@@ -54,8 +55,6 @@ const QUIET_CHECKS = new Set(["check-sign-in", "check-pages"]);
|
|
|
54
55
|
const LISTED_PUBLISHER = /^\s*(?:id|type|file):\s*\S|"(?:id|type|file)"\s*:/m;
|
|
55
56
|
/** What `npm trust github` prints once the registry stored the publisher. */
|
|
56
57
|
const TRUST_CREATED = /Trust configuration created successfully/i;
|
|
57
|
-
/** npm's answer when a workflow publishes without an attached trusted publisher. */
|
|
58
|
-
const PUBLISH_REJECTED = /\bE404\b|404 Not Found|invalid-publisher/i;
|
|
59
58
|
/** Where `--browser` opens URLs that need no automation. */
|
|
60
59
|
export const BROWSER_MODES = ["default", "automated"];
|
|
61
60
|
|
|
@@ -245,6 +244,9 @@ export class SetupSession {
|
|
|
245
244
|
}
|
|
246
245
|
|
|
247
246
|
async check(step) {
|
|
247
|
+
if (step.id === "inspect-release-run") {
|
|
248
|
+
return this.inspectReleaseRun();
|
|
249
|
+
}
|
|
248
250
|
const result = INTERACTIVE_CHECKS.has(step.id)
|
|
249
251
|
? await this.runProcess(step, false)
|
|
250
252
|
: await this.capture(step);
|
|
@@ -273,14 +275,12 @@ export class SetupSession {
|
|
|
273
275
|
case "check-2fa":
|
|
274
276
|
case "verify-2fa":
|
|
275
277
|
return this.checkTwoFactor(step, result);
|
|
276
|
-
case "inspect-release-run":
|
|
277
|
-
return this.inspectReleaseRun(result);
|
|
278
278
|
case "read-release-failure":
|
|
279
279
|
if (result.code !== 0) {
|
|
280
280
|
console.error("warning: could not read the failed release log");
|
|
281
281
|
} else if (PUBLISH_REJECTED.test(output)) {
|
|
282
282
|
console.log(
|
|
283
|
-
" It failed at publish:
|
|
283
|
+
" It failed at publish: the registry rejected the workflow's trusted publishing identity (OIDC/E404/invalid-publisher). After attaching trust, its failed jobs can be re-run.",
|
|
284
284
|
);
|
|
285
285
|
this.conditions.add("release-failed-publish");
|
|
286
286
|
} else {
|
|
@@ -380,19 +380,24 @@ export class SetupSession {
|
|
|
380
380
|
return pagesSettingsUrl(`${github_owner}/${github_repository}`);
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
inspectReleaseRun(
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
async inspectReleaseRun() {
|
|
384
|
+
let selection;
|
|
385
|
+
try {
|
|
386
|
+
selection = await selectReleaseRun(this);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error(`warning: ${error.message}`);
|
|
386
389
|
return;
|
|
387
390
|
}
|
|
388
|
-
const
|
|
391
|
+
const { run, publishFailure } = selection;
|
|
389
392
|
if (!run) {
|
|
390
393
|
console.log(" The release workflow has not run yet.");
|
|
391
394
|
return;
|
|
392
395
|
}
|
|
393
396
|
const outcome = run.conclusion || run.status;
|
|
394
|
-
console.log(
|
|
395
|
-
|
|
397
|
+
console.log(
|
|
398
|
+
` The ${publishFailure ? "selected failed" : "latest"} release run ${outcome}: ${run.url}`,
|
|
399
|
+
);
|
|
400
|
+
if (publishFailure) {
|
|
396
401
|
this.values.failed_run_id = String(run.databaseId);
|
|
397
402
|
this.conditions.add("release-failed");
|
|
398
403
|
}
|
|
@@ -488,6 +493,11 @@ export class SetupSession {
|
|
|
488
493
|
this.values.worktree = path.join(this.temporary, "worktree");
|
|
489
494
|
this.values.pack_destination = this.temporary;
|
|
490
495
|
}
|
|
496
|
+
if (step.id === "rerun-release" && this.releaseSelection?.run) {
|
|
497
|
+
console.log(
|
|
498
|
+
` Re-running failed release jobs: ${this.releaseSelection.run.url}`,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
491
501
|
// Pack output is captured so npm's warnings can be reviewed.
|
|
492
502
|
const result =
|
|
493
503
|
step.id === "pack"
|