package-registry-manager 0.25.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 (45) hide show
  1. package/README.md +5 -0
  2. package/package.json +2 -2
  3. package/src/audit-config.mjs +96 -0
  4. package/src/bootstrap-recovery.mjs +163 -0
  5. package/src/cpp-distribution.mjs +127 -0
  6. package/src/credential-cycle.mjs +8 -3
  7. package/src/default-browser.mjs +1 -1
  8. package/src/default-scope.mjs +1 -1
  9. package/src/discovery.mjs +151 -12
  10. package/src/distribution-channel.mjs +87 -0
  11. package/src/exec.mjs +20 -0
  12. package/src/extra-flows.mjs +39 -0
  13. package/src/flows.mjs +19 -3
  14. package/src/generated-artifact.mjs +48 -0
  15. package/src/github.mjs +4 -0
  16. package/src/identity-conflicts.mjs +130 -0
  17. package/src/manifest-proposal.mjs +1 -1
  18. package/src/model.mjs +6 -0
  19. package/src/npm-release.mjs +68 -0
  20. package/src/nuget-ci.mjs +207 -0
  21. package/src/nuget-migration.mjs +98 -0
  22. package/src/nuget-page.mjs +100 -0
  23. package/src/nuget-policy.mjs +198 -0
  24. package/src/nuget-publish.py +145 -0
  25. package/src/nuget-setup.mjs +169 -0
  26. package/src/nuget-workflow.mjs +54 -0
  27. package/src/organization-scan.mjs +41 -2
  28. package/src/pack-review.mjs +122 -0
  29. package/src/plan.mjs +7 -2
  30. package/src/prerequisites.mjs +1 -1
  31. package/src/profile.mjs +1 -1
  32. package/src/publication-state.mjs +265 -0
  33. package/src/publishers.mjs +5 -2
  34. package/src/publishing-policy.mjs +7 -1
  35. package/src/publishing-state.mjs +14 -0
  36. package/src/python.mjs +1 -1
  37. package/src/registry-state.mjs +118 -3
  38. package/src/release-runs.mjs +3 -4
  39. package/src/setup-batch.mjs +48 -2
  40. package/src/setup.mjs +82 -120
  41. package/src/sign-in-import.mjs +132 -40
  42. package/src/skips.mjs +9 -1
  43. package/src/tokens.mjs +32 -4
  44. package/src/workflow-migration.mjs +15 -2
  45. package/src/workflow-proposal.mjs +6 -2
package/README.md CHANGED
@@ -77,3 +77,8 @@ OIDC publication and verification that no workflow still uses the credentials.
77
77
  Saved crates.io API sessions run headlessly; sign-in opens a visible window only
78
78
  when user action is needed. Organization migration skips and reports scan errors
79
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.25.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,7 +40,7 @@
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/293e9a8ab9d6d91a08e95f4e09c2a0dcad9bc809",
43
+ "@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/0b3ceb1590be7eb0340ae088730698b5ae9bebfc",
44
44
  "browser-commander": "^0.34.0",
45
45
  "command-stream": "^2.6.0",
46
46
  "playwright": "^1.63.0",
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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 {