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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "package-registry-manager",
3
- "version": "0.22.8",
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.27.0",
45
- "command-stream": "^2.0.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 latest release run",
190
+ "Read the failed default-branch release run",
190
191
  "check",
191
- "Look up the newest run of the release workflow; a run that npm rejected can be re-run once trust is attached.",
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
- "1",
205
+ "100",
201
206
  "--json",
202
- "databaseId,conclusion,status,url",
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
- "Search the failed jobs' log for npm's E404 or invalid-publisher rejection.",
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 result = await session.capture({
310
- command: {
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: npm rejected the workflow (E404/invalid-publisher) because no trusted publisher is attached yet. After attaching trust, its failed jobs can be re-run.",
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(result) {
384
- if (result.code !== 0) {
385
- console.error("warning: could not list the release workflow runs");
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 [run] = JSON.parse(String(result.stdout || "[]"));
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(` The latest release run ${outcome}: ${run.url}`);
395
- if (run.conclusion === "failure") {
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"