package-registry-manager 0.24.0 → 0.25.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.
Files changed (55) hide show
  1. package/README.md +12 -0
  2. package/package.json +3 -3
  3. package/src/audit-config.mjs +96 -0
  4. package/src/automation.mjs +12 -5
  5. package/src/bootstrap-recovery.mjs +163 -0
  6. package/src/browser.mjs +2 -2
  7. package/src/cli-main.mjs +20 -6
  8. package/src/cpp-distribution.mjs +127 -0
  9. package/src/crates-api.mjs +9 -2
  10. package/src/credential-cycle.mjs +8 -3
  11. package/src/default-browser.mjs +1 -1
  12. package/src/default-scope.mjs +1 -1
  13. package/src/discovery.mjs +157 -13
  14. package/src/distribution-channel.mjs +87 -0
  15. package/src/exec.mjs +20 -0
  16. package/src/extra-flows.mjs +39 -0
  17. package/src/flows.mjs +19 -3
  18. package/src/generated-artifact.mjs +48 -0
  19. package/src/github.mjs +38 -1
  20. package/src/identity-conflicts.mjs +130 -0
  21. package/src/manifest-proposal.mjs +1 -1
  22. package/src/migration-cleanup.mjs +31 -9
  23. package/src/model.mjs +6 -0
  24. package/src/npm-release.mjs +68 -0
  25. package/src/nuget-ci.mjs +207 -0
  26. package/src/nuget-migration.mjs +98 -0
  27. package/src/nuget-page.mjs +100 -0
  28. package/src/nuget-policy.mjs +198 -0
  29. package/src/nuget-publish.py +145 -0
  30. package/src/nuget-setup.mjs +169 -0
  31. package/src/nuget-workflow.mjs +54 -0
  32. package/src/organization-scan.mjs +76 -11
  33. package/src/organization-setup.mjs +7 -5
  34. package/src/pack-review.mjs +122 -0
  35. package/src/plan.mjs +19 -6
  36. package/src/prerequisites.mjs +1 -1
  37. package/src/profile.mjs +1 -1
  38. package/src/publication-state.mjs +265 -0
  39. package/src/publishers.mjs +95 -2
  40. package/src/publishing-policy.mjs +7 -1
  41. package/src/publishing-state.mjs +51 -0
  42. package/src/python.mjs +1 -1
  43. package/src/registry-state.mjs +177 -25
  44. package/src/release-runs.mjs +3 -4
  45. package/src/request-policy.mjs +27 -0
  46. package/src/setup-batch.mjs +48 -2
  47. package/src/setup.mjs +101 -123
  48. package/src/sign-in-import.mjs +132 -40
  49. package/src/skips.mjs +9 -1
  50. package/src/source-code.mjs +2 -0
  51. package/src/tokens.mjs +36 -6
  52. package/src/webdriver-automation.mjs +3 -2
  53. package/src/workflow-fallback.mjs +125 -0
  54. package/src/workflow-migration.mjs +150 -78
  55. package/src/workflow-proposal.mjs +42 -9
package/README.md CHANGED
@@ -70,3 +70,15 @@ local `--offline` commands keep local scope. Both CLIs' GitHub dependency needs
70
70
  Node.js 22.12 or newer and npm; JavaScript checks the runtime before loading it.
71
71
 
72
72
  See the [bootstrap, wrapper and credential guide](../docs/registry-setup.md) for branch/PR setup, dual npm names, registry policies and integration limits.
73
+
74
+ Migration tries trusted publishing first and retains token fallback and expiry
75
+ metadata by default. `--delete-token-secrets` explicitly opts into cleanup after
76
+ OIDC publication and verification that no workflow still uses the credentials.
77
+ Saved crates.io API sessions run headlessly; sign-in opens a visible window only
78
+ when user action is needed. Organization migration skips and reports scan errors
79
+ while continuing for clean repositories.
80
+
81
+ NuGet setup uses an explicit login profile, package owner and exact package IDs.
82
+ Trusted publishing is preferred; existing provided fallback credentials are retained.
83
+ See [audit and NuGet configuration](../docs/issue-73-requirements.md) for setup,
84
+ identity decisions and the distinction between verified configuration and release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "package-registry-manager",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "description": "Inspect repositories and guide secure package-registry setup",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,9 +40,9 @@
40
40
  "audit": "node scripts/npm-audit.mjs"
41
41
  },
42
42
  "dependencies": {
43
- "@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/29808e088ac8fd8374361920bbebab07a4d63ac6",
43
+ "@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/0b3ceb1590be7eb0340ae088730698b5ae9bebfc",
44
44
  "browser-commander": "^0.34.0",
45
- "command-stream": "^2.4.0",
45
+ "command-stream": "^2.6.0",
46
46
  "playwright": "^1.63.0",
47
47
  "selenium-webdriver": "^4.50.0"
48
48
  },
@@ -0,0 +1,96 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /** Explicit source decisions supplement static discovery without executing build files. */
5
+ export async function auditConfig(root) {
6
+ const config = JSON.parse(
7
+ await readFile(
8
+ path.join(root, ".package-registry-manager.json"),
9
+ "utf8",
10
+ ).catch((error) => {
11
+ if (error.code === "ENOENT") {
12
+ return "{}";
13
+ }
14
+ throw error;
15
+ }),
16
+ );
17
+ if (
18
+ config.publish !== undefined &&
19
+ (!Array.isArray(config.publish) ||
20
+ config.publish.some(
21
+ (file) =>
22
+ typeof file !== "string" ||
23
+ path.posix.normalize(file) !== file ||
24
+ file.startsWith("/") ||
25
+ file.split("/").includes(".."),
26
+ ))
27
+ ) {
28
+ throw new Error(
29
+ "publish must list exact repository-relative manifest paths",
30
+ );
31
+ }
32
+ return config;
33
+ }
34
+
35
+ /** A reviewed mapping chooses one canonical artifact; references never publish a different tarball. */
36
+ export function applyAuditConfig(items, config, workflows) {
37
+ for (const item of items.filter(Boolean)) {
38
+ const gradle = config.gradle?.find(
39
+ (entry) => entry.manifest === item.manifest,
40
+ );
41
+ if (gradle) {
42
+ if (
43
+ ![
44
+ gradle.group,
45
+ gradle.artifact,
46
+ gradle.version,
47
+ gradle.component,
48
+ ].every((value) => typeof value === "string" && /^[\w.-]+$/.test(value))
49
+ ) {
50
+ throw new Error(
51
+ "explicit Gradle publication requires resolved group, artifact, version and component",
52
+ );
53
+ }
54
+ Object.assign(item, {
55
+ registry: "maven-central",
56
+ name: `${gradle.group}:${gradle.artifact}`,
57
+ version: gradle.version,
58
+ publishable: true,
59
+ publication_component: gradle.component,
60
+ });
61
+ delete item.discovery_skip;
62
+ }
63
+ const shared = config.shared_artifacts?.find(
64
+ (entry) =>
65
+ `${entry.registry}:${entry.name}` === `${item.registry}:${item.name}`,
66
+ );
67
+ if (shared) {
68
+ item.shared_artifact = shared;
69
+ }
70
+ if (item.distribution) {
71
+ item.distribution = {
72
+ ...item.distribution,
73
+ ...config.distribution?.[item.manifest],
74
+ workflows: workflows
75
+ .filter((workflow) =>
76
+ item.registry === "conan"
77
+ ? /conan\s+upload/.test(workflow.contents)
78
+ : item.registry === "vcpkg"
79
+ ? /vcpkg.*(?:x-add-version|registry|publish)/.test(
80
+ workflow.contents,
81
+ )
82
+ : /cpack|cmake.*--install/.test(workflow.contents),
83
+ )
84
+ .map((workflow) => ({
85
+ workflow: workflow.name,
86
+ remote_variable: /CONAN_REMOTE_URL/.test(workflow.contents)
87
+ ? "CONAN_REMOTE_URL"
88
+ : undefined,
89
+ credential_names: ["CONAN_LOGIN_USERNAME", "CONAN_PASSWORD"].filter(
90
+ (name) => workflow.contents.includes(name),
91
+ ),
92
+ })),
93
+ };
94
+ }
95
+ }
96
+ }
@@ -29,14 +29,20 @@ export function relayExtensionDirectory() {
29
29
  * the `domains` import scope only the cookies of `domains`, the registry's
30
30
  * sign-in domains, are imported.
31
31
  */
32
- export function launchOptions(browser, profile, verbose = false, domains = []) {
32
+ export function launchOptions(
33
+ browser,
34
+ profile,
35
+ verbose = false,
36
+ domains = [],
37
+ headless = false,
38
+ ) {
33
39
  const options = {
34
40
  engine:
35
41
  findBrowserSource(browser.channel)?.controlProtocol === "cdp"
36
42
  ? "playwright"
37
43
  : "selenium",
38
44
  channel: browser.channel,
39
- headless: false,
45
+ headless,
40
46
  verbose,
41
47
  };
42
48
  if (browser.executable) {
@@ -82,7 +88,7 @@ export function extensionInstructions(directory) {
82
88
  * `evaluate`, `close`, and, for a launched profile, `clearCookies(domains)`.
83
89
  */
84
90
  export async function connectAutomation(
85
- { browser, profile, verbose = false, domains = [] },
91
+ { browser, profile, verbose = false, domains = [], headless = false },
86
92
  dependencies = {},
87
93
  ) {
88
94
  if (browser.attach?.mode === "extension") {
@@ -90,7 +96,7 @@ export async function connectAutomation(
90
96
  }
91
97
  if (findBrowserSource(browser.channel)?.controlProtocol === "bidi") {
92
98
  return connectWebDriverAutomation(
93
- { browser, profile, verbose, domains },
99
+ { browser, profile, verbose, domains, headless },
94
100
  dependencies,
95
101
  );
96
102
  }
@@ -99,8 +105,9 @@ export async function connectAutomation(
99
105
  }
100
106
  const connection = await (
101
107
  dependencies.launchRealBrowser ?? launchRealBrowser
102
- )(launchOptions(browser, profile, verbose, domains));
108
+ )(launchOptions(browser, profile, verbose, domains, headless));
103
109
  return {
110
+ headless,
104
111
  goto: (url) => connection.page.goto(url),
105
112
  evaluate: (script) => connection.page.evaluate(script),
106
113
  close: () => connection.close(),
@@ -0,0 +1,163 @@
1
+ import { probePackage } from "./registry-state.mjs";
2
+ import {
3
+ publisherIdentities,
4
+ publisherMatches,
5
+ repositorySlug,
6
+ } from "./repository-identity.mjs";
7
+
8
+ /** Bootstrap is a repaired prerequisite, separate from OIDC rejection. */
9
+ export function bootstrapNames(log) {
10
+ const names = [];
11
+ for (const match of log.matchAll(
12
+ /npm owner setup required for ([^:\n]+):/gi,
13
+ )) {
14
+ for (const name of match[1].split(/[,\s]+/).filter(Boolean)) {
15
+ if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(name)) {
16
+ return [];
17
+ }
18
+ names.push(name);
19
+ }
20
+ }
21
+ return [...new Set(names)];
22
+ }
23
+
24
+ /** Fresh name and exact publisher checks are equivalent to the owner-setup preflight. */
25
+ export async function verifyBootstrapRecovery(session) {
26
+ if (!session.bootstrapNames?.length) {
27
+ return;
28
+ }
29
+ for (const name of session.bootstrapNames) {
30
+ const state = await (session.options.probePackage ?? probePackage)(
31
+ { ...session.plan.package, name },
32
+ session.options,
33
+ );
34
+ if (state.exists !== true) {
35
+ throw new Error(`bootstrap preflight still unresolved for ${name}`);
36
+ }
37
+ const trust = session.plan.steps?.find(
38
+ (step) => step.id === "verify-trusted-publisher",
39
+ )?.command;
40
+ const args = trust
41
+ ? trust.args.map((argument) =>
42
+ argument === session.plan.package.name ? name : argument,
43
+ )
44
+ : ["-y", "npm@^11.15", "trust", "list", name, "--browser=false"];
45
+ const result = await session.capture({
46
+ command: { program: trust?.program ?? "npx", args },
47
+ });
48
+ if (
49
+ result.code !== 0 ||
50
+ !publisherIdentities(String(result.stdout)).some((publisher) =>
51
+ publisherMatches(publisher, session.plan.trusted_publisher),
52
+ )
53
+ ) {
54
+ throw new Error(`bootstrap publisher is not verified for ${name}`);
55
+ }
56
+ }
57
+ const slug = repositorySlug(session.plan.repository);
58
+ const result = await session.capture({
59
+ command: {
60
+ program: "gh",
61
+ args: [
62
+ "api",
63
+ `repos/${slug}/commits/${session.releaseSelection.branch}`,
64
+ "--jq",
65
+ ".sha",
66
+ ],
67
+ },
68
+ cwd: ".",
69
+ });
70
+ if (
71
+ result.code !== 0 ||
72
+ String(result.stdout).trim() !== session.releaseSelection.run.headSha
73
+ ) {
74
+ throw new Error(
75
+ "default branch changed; follow the new revision's release instead of rerunning the old SHA",
76
+ );
77
+ }
78
+ }
79
+
80
+ /** Retry only jobs whose own failed log contains the repaired prerequisite. */
81
+ export async function recoverBootstrap(session) {
82
+ await verifyBootstrapRecovery(session);
83
+ const selection = session.releaseSelection;
84
+ const slug = repositorySlug(session.plan.repository);
85
+ const jobIds = [];
86
+ for (const job of selection.jobs ?? []) {
87
+ if (job.conclusion !== "failure") {
88
+ continue;
89
+ }
90
+ const log = await session.capture({
91
+ command: {
92
+ program: "gh",
93
+ args: [
94
+ "run",
95
+ "view",
96
+ String(selection.run.databaseId),
97
+ "--repo",
98
+ slug,
99
+ "--job",
100
+ String(job.databaseId),
101
+ "--log",
102
+ ],
103
+ },
104
+ cwd: ".",
105
+ });
106
+ if (log.code !== 0) {
107
+ throw new Error(
108
+ "causal failed-job logs are unavailable; no CI was restarted",
109
+ );
110
+ }
111
+ if (
112
+ bootstrapNames(String(log.stdout)).some((name) =>
113
+ session.bootstrapNames.includes(name),
114
+ )
115
+ ) {
116
+ jobIds.push(job.databaseId);
117
+ }
118
+ }
119
+ if (!jobIds.length) {
120
+ throw new Error(
121
+ "no causal bootstrap job could be verified; unrelated failed jobs were not retried",
122
+ );
123
+ }
124
+ const changes =
125
+ session.options.recoveryChanges?.get(
126
+ `${slug}:${session.plan.trusted_publisher.workflow}`,
127
+ ) ?? [];
128
+ if (!changes.length) {
129
+ throw new Error(
130
+ "no verified configuration change in this setup; CI was not restarted",
131
+ );
132
+ }
133
+ const { githubServices } = await import("./github.mjs");
134
+ const github = session.options.github ?? githubServices(session.options);
135
+ const result = await github.repair.verify({
136
+ repo: slug,
137
+ workflow: session.plan.trusted_publisher.workflow,
138
+ runId: selection.run.databaseId,
139
+ jobIds,
140
+ changes: changes.map(() => ({
141
+ kind: "setting",
142
+ changed: true,
143
+ verified: true,
144
+ })),
145
+ timeout: session.options.waitTimeoutMs ?? 600000,
146
+ });
147
+ console.log(JSON.stringify({ bootstrap_recovery: result }));
148
+ session.conditions.delete("release-dispatch");
149
+ session.values.run_id = String(result.runId);
150
+ if (result.status !== "success") {
151
+ throw new Error(
152
+ `repaired bootstrap CI remains ${result.status}; ${result.runUrl ?? selection.run.url}`,
153
+ );
154
+ }
155
+ const { verifyNpmRelease } = await import("./npm-release.mjs");
156
+ const packages = await verifyNpmRelease(session, result.runId);
157
+ session.outcome = {
158
+ status: "verified",
159
+ url: result.runUrl,
160
+ ci: result,
161
+ packages,
162
+ };
163
+ }
package/src/browser.mjs CHANGED
@@ -42,10 +42,10 @@ export function openerSucceeded(file, code) {
42
42
  * Starts an opener through command-stream without a shell and settles once it
43
43
  * exits, or after `graceMs` while it keeps running in the background.
44
44
  */
45
- export function detachedRunner(graceMs = OPENER_GRACE_MS) {
45
+ export function detachedRunner(graceMs = OPENER_GRACE_MS, spawnChild = spawn) {
46
46
  return (file, args) =>
47
47
  new Promise((resolve, reject) => {
48
- const child = spawn(file, args, {
48
+ const child = spawnChild(file, args, {
49
49
  detached: process.platform !== "win32",
50
50
  stdio: "ignore",
51
51
  shell: false,
package/src/cli-main.mjs CHANGED
@@ -36,7 +36,8 @@ Global options:
36
36
  --org <org> | --user <login> Scan repositories through gh-manager
37
37
  --secret-name <template> Secret name; supports {REGISTRY}, {REPO}, {ORG}
38
38
  --rotate-token <secret> Explicitly replace a legacy crates.io token
39
- --migrate Convert token jobs in PRs, then verify OIDC and clean up secrets
39
+ --migrate Convert jobs to OIDC with token fallback, then verify releases
40
+ --delete-token-secrets Opt in to verified unused token and expiry cleanup
40
41
  --token-expiry-days <N> Read browser token inventory in account scans
41
42
  --format <text|json> Output format (default: text)
42
43
  --offline Do not look up packages on registries
@@ -118,6 +119,7 @@ export async function main(args = process.argv.slice(2)) {
118
119
  "secret-name": { type: "string" },
119
120
  "rotate-token": { type: "string" },
120
121
  migrate: { type: "boolean", default: false },
122
+ "delete-token-secrets": { type: "boolean", default: false },
121
123
  "token-expiry-days": { type: "string" },
122
124
  format: { type: "string", default: "text" },
123
125
  verbose: { type: "boolean", default: false },
@@ -273,6 +275,7 @@ export async function main(args = process.argv.slice(2)) {
273
275
  secretName: values["secret-name"],
274
276
  rotateToken,
275
277
  migrate: values.migrate,
278
+ deleteTokenSecrets: values["delete-token-secrets"],
276
279
  tokenExpiryDays,
277
280
  package: values.package,
278
281
  browser: values.browser,
@@ -288,6 +291,7 @@ export async function main(args = process.argv.slice(2)) {
288
291
  publisherEnvironment: values.environment,
289
292
  addPublishJob: values["add-publish-job"],
290
293
  migrate: values.migrate,
294
+ deleteTokenSecrets: values["delete-token-secrets"],
291
295
  manual: values.manual,
292
296
  verifyRelease: values["verify-release"],
293
297
  },
@@ -331,6 +335,7 @@ export async function main(args = process.argv.slice(2)) {
331
335
  const planOptions = {
332
336
  rotateToken,
333
337
  migrate: values.migrate,
338
+ deleteTokenSecrets: values["delete-token-secrets"],
334
339
  ref: values.ref,
335
340
  verifyRelease: values["verify-release"],
336
341
  manual: values.manual,
@@ -375,9 +380,10 @@ export async function main(args = process.argv.slice(2)) {
375
380
  }
376
381
 
377
382
  const plans = buildPlans(inspection, registries, planOptions);
378
- const selected = values.all
379
- ? plans.filter((plan) => plan.package.publishable)
380
- : [selectPlan(plans, values.package)];
383
+ const selected =
384
+ values.all || (values.migrate && !values.package)
385
+ ? plans.filter((plan) => plan.package.publishable)
386
+ : [selectPlan(plans, values.package)];
381
387
  outputPlans(selected, values.format);
382
388
  await executePlans(
383
389
  selected,
@@ -394,6 +400,7 @@ export async function main(args = process.argv.slice(2)) {
394
400
  secretName: values["secret-name"],
395
401
  rotateToken,
396
402
  migrate: values.migrate,
403
+ deleteTokenSecrets: values["delete-token-secrets"],
397
404
  quietBrowser: selected.some((plan) =>
398
405
  plan.steps.some((step) =>
399
406
  ["manage-registry-token", "rotate-crates-token"].includes(step.id),
@@ -408,7 +415,7 @@ export async function main(args = process.argv.slice(2)) {
408
415
  verbose: values.verbose,
409
416
  verifyRelease: values["verify-release"],
410
417
  },
411
- values.all,
418
+ values.all || values.migrate,
412
419
  );
413
420
  }
414
421
 
@@ -520,7 +527,14 @@ function outputInspection(inspection, format) {
520
527
  packageInfo.exists_on_registry ? "published" : "not published yet",
521
528
  );
522
529
  }
523
- if (packageInfo.trusted_publishing) {
530
+ if (packageInfo.registry_http_status) {
531
+ details.push(
532
+ `registry state unknown (HTTP ${packageInfo.registry_http_status})`,
533
+ );
534
+ }
535
+ if (packageInfo.publishing_policy === "oidc-with-token-fallback") {
536
+ details.push("trusted publishing + token fallback");
537
+ } else if (packageInfo.trusted_publishing) {
524
538
  details.push("trusted publishing");
525
539
  }
526
540
  process.stdout.write(
@@ -0,0 +1,127 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /** Read distribution metadata without executing a CMake file or Python recipe. */
5
+ export async function cppDistribution(
6
+ contents,
7
+ filename,
8
+ manifest,
9
+ manifestPath,
10
+ ) {
11
+ let cmake = filename === "CMakeLists.txt" ? contents : "";
12
+ if (!cmake) {
13
+ try {
14
+ cmake = await readFile(
15
+ path.join(path.dirname(manifestPath), "CMakeLists.txt"),
16
+ "utf8",
17
+ );
18
+ } catch (error) {
19
+ if (error.code !== "ENOENT") {
20
+ throw error;
21
+ }
22
+ }
23
+ }
24
+ const project =
25
+ /\bproject\s*\(\s*([\w.-]+)\s+VERSION\s+(\d+(?:\.\d+)+)/i.exec(cmake);
26
+ let name = project?.[1];
27
+ let version = project?.[2];
28
+ const registry =
29
+ filename === "vcpkg.json"
30
+ ? "vcpkg"
31
+ : filename === "conanfile.py"
32
+ ? "conan"
33
+ : "cmake";
34
+ if (registry === "vcpkg") {
35
+ const data = JSON.parse(contents);
36
+ name = data.name;
37
+ version =
38
+ data.version ??
39
+ data["version-semver"] ??
40
+ data["version-string"] ??
41
+ data["version-date"];
42
+ } else if (registry === "conan") {
43
+ name = /^\s*name\s*=\s*["']([^"']+)/m.exec(contents)?.[1] ?? name;
44
+ version = /^\s*version\s*=\s*["']([^"']+)/m.exec(contents)?.[1] ?? version;
45
+ }
46
+ if (
47
+ !name ||
48
+ (registry === "cmake" &&
49
+ !/install\s*\(|CPack|configure_package_config_file/i.test(contents))
50
+ ) {
51
+ return {
52
+ manifest,
53
+ discovery_skip:
54
+ "CMake build definition has no distribution installation/configuration",
55
+ publishable: false,
56
+ };
57
+ }
58
+ return {
59
+ registry,
60
+ name,
61
+ version: version ?? null,
62
+ manifest,
63
+ publishable: true,
64
+ distribution: {
65
+ version_source: project
66
+ ? path.posix.join(path.posix.dirname(manifest), "CMakeLists.txt")
67
+ : manifest,
68
+ provider_status: "unverified",
69
+ ...(registry === "conan"
70
+ ? {
71
+ remote_required: "CONAN_REMOTE_URL",
72
+ credential_names: ["CONAN_LOGIN_USERNAME", "CONAN_PASSWORD"],
73
+ }
74
+ : {}),
75
+ },
76
+ warnings: [
77
+ registry === "conan"
78
+ ? "Conan remote configuration and publication remain unverified; a local recipe does not establish registry presence"
79
+ : `${registry} distribution metadata does not establish a public registry publication`,
80
+ ],
81
+ };
82
+ }
83
+
84
+ /** Validation and explicit channel prerequisites; no upload is performed. */
85
+ export function cppPlan(item) {
86
+ const commands = {
87
+ cmake: {
88
+ program: "cmake",
89
+ args: ["-S", ".", "-B", "build/prm-validation"],
90
+ },
91
+ conan: { program: "conan", args: ["inspect", ".", "--format=json"] },
92
+ vcpkg: {
93
+ program: "vcpkg",
94
+ args: ["install", "--dry-run", "--x-manifest-root=."],
95
+ },
96
+ };
97
+ return [
98
+ {
99
+ id: "validate-package",
100
+ title: `Validate ${item.registry} distribution`,
101
+ kind: "check",
102
+ command: commands[item.registry],
103
+ description:
104
+ "Validate the manifest locally before reviewing any publication settings.",
105
+ },
106
+ ...(item.registry === "conan"
107
+ ? [
108
+ {
109
+ id: "audit-distribution-channel",
110
+ title: "Read Conan remote and credential health",
111
+ kind: "api",
112
+ description:
113
+ "Read effective remote configuration and credential health through gh-manager. Unknown credentials and missing remotes remain explicit; do not upload or rotate credentials.",
114
+ },
115
+ ]
116
+ : []),
117
+ {
118
+ id: "review-distribution-channel",
119
+ title: "Resolve the distribution provider and CI configuration",
120
+ kind: "manual",
121
+ description:
122
+ item.registry === "conan"
123
+ ? "Read CONAN_REMOTE_URL and credential health for CONAN_LOGIN_USERNAME/CONAN_PASSWORD through gh-manager at the effective repository/environment scope. Choose the exact remote, validate conan create and consumer tests in reviewed CI, then configure narrowly scoped existing credentials. No public registry state or permission is established by a recipe."
124
+ : `Review ${item.registry === "vcpkg" ? "the chosen vcpkg registry repository, baseline, port and version database" : "CMake install/export, package config, CPack and GitHub Release workflow"}; validate installation and a consumer in CI. Provider lookup/setup is unsupported until the destination is explicitly configured; publication remains unverified.`,
125
+ },
126
+ ];
127
+ }
@@ -350,11 +350,15 @@ export async function runCratesApiStep(session, step) {
350
350
  }
351
351
  const crate = plan.package.name;
352
352
  const domains = signInDomains(plan.registry);
353
- let page = await session.automatedPage();
353
+ let page = await session.automatedPage(undefined, false, true);
354
354
  switch (step.id) {
355
355
  case "crates-sign-in": {
356
356
  await page.goto(CRATES_IO_URL);
357
- if (!(await readSession(page)).signedIn) {
357
+ const saved = await readSession(page);
358
+ if (!saved.signedIn || !saved.emailVerified) {
359
+ page = await session.visiblePage();
360
+ }
361
+ if (!saved.signedIn) {
358
362
  // Whether imported or signed in by hand, the sign-in is this run's.
359
363
  conditions.add("crates-signed-in");
360
364
  if (await offerSignInImport(session, domains)) {
@@ -362,6 +366,9 @@ export async function runCratesApiStep(session, step) {
362
366
  }
363
367
  }
364
368
  await signIn(page, options);
369
+ console.log(
370
+ "Using your crates.io session to register trusted publishers; no action needed while API requests run.",
371
+ );
365
372
  return;
366
373
  }
367
374
  case "first-publish": {
@@ -1,8 +1,13 @@
1
1
  /** Registry-specific policy; tokens are never a fallback for OIDC registries. */
2
2
  export function credentialPolicy(registry) {
3
- if (
4
- ["npm", "pypi", "crates-io", "rubygems", "nuget", "jsr"].includes(registry)
5
- ) {
3
+ if (registry === "nuget") {
4
+ return {
5
+ mode: "oidc-with-token-fallback",
6
+ description:
7
+ "NuGet: NuGet/login OIDC first, retain provided fallback secrets; never create a persistent publishing key",
8
+ };
9
+ }
10
+ if (["npm", "pypi", "crates-io", "rubygems", "jsr"].includes(registry)) {
6
11
  return {
7
12
  mode: "trusted",
8
13
  description: `${registry}: trusted publishing; remove unused long-lived tokens after verification`,
@@ -1,4 +1,4 @@
1
- import { exec } from "command-stream";
1
+ import { exec } from "./exec.mjs";
2
2
 
3
3
  import { browserSources, catalogueName } from "./browser-catalogue.mjs";
4
4
 
@@ -1,4 +1,4 @@
1
- import { exec } from "command-stream";
1
+ import { exec } from "./exec.mjs";
2
2
 
3
3
  async function github(args) {
4
4
  try {