package-registry-manager 0.25.0 → 0.26.0

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 (48) hide show
  1. package/README.md +5 -0
  2. package/package.json +3 -3
  3. package/src/audit-config.mjs +96 -0
  4. package/src/bootstrap-recovery.mjs +163 -0
  5. package/src/cpp-distribution.mjs +164 -0
  6. package/src/credential-cycle.mjs +8 -3
  7. package/src/credential-requirements.mjs +105 -0
  8. package/src/default-browser.mjs +1 -1
  9. package/src/default-scope.mjs +1 -1
  10. package/src/discovery.mjs +289 -24
  11. package/src/distribution-channel.mjs +87 -0
  12. package/src/exec.mjs +20 -0
  13. package/src/extra-flows.mjs +122 -2
  14. package/src/flows.mjs +19 -3
  15. package/src/generated-artifact.mjs +48 -0
  16. package/src/github.mjs +4 -0
  17. package/src/identity-conflicts.mjs +130 -0
  18. package/src/manifest-proposal.mjs +1 -1
  19. package/src/model.mjs +6 -0
  20. package/src/npm-release.mjs +68 -0
  21. package/src/nuget-ci.mjs +238 -0
  22. package/src/nuget-migration.mjs +275 -0
  23. package/src/nuget-page.mjs +100 -0
  24. package/src/nuget-policy.mjs +198 -0
  25. package/src/nuget-publish.py +145 -0
  26. package/src/nuget-setup.mjs +169 -0
  27. package/src/nuget-workflow.mjs +54 -0
  28. package/src/organization-scan.mjs +41 -2
  29. package/src/pack-review.mjs +122 -0
  30. package/src/package-coverage.mjs +33 -9
  31. package/src/plan.mjs +35 -9
  32. package/src/prerequisites.mjs +1 -1
  33. package/src/profile.mjs +1 -1
  34. package/src/publication-state.mjs +299 -0
  35. package/src/publishers.mjs +78 -12
  36. package/src/publishing-policy.mjs +7 -1
  37. package/src/publishing-state.mjs +74 -1
  38. package/src/python.mjs +1 -1
  39. package/src/registry-state.mjs +118 -3
  40. package/src/release-runs.mjs +3 -4
  41. package/src/setup-batch.mjs +48 -2
  42. package/src/setup.mjs +82 -120
  43. package/src/sign-in-import.mjs +132 -40
  44. package/src/skips.mjs +9 -1
  45. package/src/source-code.mjs +1 -1
  46. package/src/tokens.mjs +32 -4
  47. package/src/workflow-migration.mjs +15 -2
  48. package/src/workflow-proposal.mjs +6 -2
@@ -1,8 +1,12 @@
1
+ import { credentialRequirements } from "./credential-requirements.mjs";
1
2
  import { credentialSteps } from "./credential-cycle.mjs";
2
3
  import { tokenSecretSteps } from "./tokens.mjs";
3
4
 
4
5
  /** Additional OIDC registries never fall back to a stored publishing token. */
5
6
  export function trustedFlow(item, context) {
7
+ if (item.registry === "nuget") {
8
+ return nugetFlow(item, context);
9
+ }
6
10
  const specs = {
7
11
  rubygems: [
8
12
  "gem",
@@ -73,16 +77,132 @@ export function tokenFlow(item) {
73
77
  ]
74
78
  : [];
75
79
  if (item.registry === "maven-central") {
80
+ const group = /^([\w.-]+):/.exec(item.name ?? "")?.[1];
76
81
  steps.push({
77
82
  id: "verify-namespace",
78
- title: "Verify a Central namespace",
83
+ title: `Verify the Central namespace${group ? ` ${group}` : ""}`,
79
84
  kind: "browser",
80
85
  description:
81
- "Sign in to the Central Portal and verify the namespace used by the package coordinates.",
86
+ "Sign in to the Central Portal and verify the namespace used by the package coordinates. Publishing stays blocked until the namespace reads back as verified.",
82
87
  url: "https://central.sonatype.com/publishing/namespaces",
88
+ ...(group ? { namespace: group } : {}),
89
+ state: "manual-prerequisite",
83
90
  });
91
+ return steps.concat(mavenCredentialSteps(item));
84
92
  }
85
93
  return steps.concat(
86
94
  credentialSteps(item.registry, item.token_secrets?.[0] ?? item.registry),
87
95
  );
88
96
  }
97
+
98
+ /**
99
+ * Central publishing needs a provider-specific pair plus signing material; no
100
+ * OIDC is claimed. Missing secrets are requested, present ones are kept, and
101
+ * nothing is rotated or revoked.
102
+ */
103
+ export function mavenCredentialSteps(item) {
104
+ const requirements =
105
+ item.credential_requirements ?? credentialRequirements("maven-central");
106
+ const pick = (...roles) =>
107
+ requirements.secrets.filter((entry) => roles.includes(entry.role));
108
+ const describe = (entries) =>
109
+ entries.map((entry) => entry.secret).join(" + ");
110
+ const pair = pick("central-username", "central-token");
111
+ const signing = pick("gpg-private-key", "gpg-passphrase");
112
+ return [
113
+ {
114
+ id: "ensure-central-credential-pair",
115
+ title: `Ensure the Central user-token pair ${describe(pair)}`,
116
+ kind: "api",
117
+ confirm: true,
118
+ secrets: pair,
119
+ oidc: "unsupported",
120
+ description: `${requirements.oidc_note} Generate a Central Portal user token (username + password pair) only when ${describe(pair)} is missing, hold the values in memory, and store them as encrypted GitHub secrets through gh-manager. Existing secrets are kept; nothing is rotated or revoked.`,
121
+ url: "https://central.sonatype.com/usertoken",
122
+ },
123
+ {
124
+ id: "ensure-signing-key",
125
+ title: `Ensure the signing prerequisites ${describe(signing)}`,
126
+ kind: "manual",
127
+ confirm: true,
128
+ secrets: signing,
129
+ state: "manual-prerequisite",
130
+ description:
131
+ "Maven Central rejects unsigned artifacts. Provide an armored GPG private key whose public key is on a public keyserver, and its passphrase, as encrypted GitHub secrets; keys are never written to disk or printed.",
132
+ },
133
+ ];
134
+ }
135
+
136
+ /** Public Packagist: tags plus hook or update API; no OIDC, explicit states otherwise. */
137
+ export function packagistSteps(item) {
138
+ const requirements =
139
+ item.credential_requirements ?? credentialRequirements("packagist");
140
+ const api = requirements.secrets;
141
+ const referenced = api.every((entry) => entry.referenced);
142
+ return [
143
+ {
144
+ id: "verify-packagist-package",
145
+ title: "Read back the Packagist package and exact stable tag",
146
+ kind: "check",
147
+ description: `Read https://repo.packagist.org/p2/${item.name}.json and require the exact stable version ${item.version ?? "(unresolved)"} from a tag; dev branches do not count.`,
148
+ url: `https://packagist.org/packages/${item.name}`,
149
+ },
150
+ {
151
+ id: "packagist-github-hook",
152
+ title: "Confirm the Packagist GitHub integration",
153
+ kind: "manual",
154
+ state: "manual-prerequisite",
155
+ description:
156
+ 'Packagist updates on tag pushes through its GitHub hook/app integration. GitHub does not expose that installation to this tool, so confirm it on the package page (no "not auto-updated" warning).',
157
+ url: `https://packagist.org/packages/${item.name}`,
158
+ },
159
+ {
160
+ id: "packagist-update-api",
161
+ title: referenced
162
+ ? `Keep the update-package API path ${api.map((entry) => entry.secret).join(" + ")}`
163
+ : "Packagist update-package API is not configured",
164
+ kind: "manual",
165
+ state: referenced ? "configured" : "unsupported-without-credentials",
166
+ secrets: api,
167
+ oidc: "unsupported",
168
+ description: `${requirements.oidc_note} ${referenced ? "The workflow calls the update-package API; keep both secrets as encrypted GitHub secrets. Nothing is rotated or revoked." : "Rely on the GitHub hook, or add PACKAGIST_USERNAME and PACKAGIST_TOKEN (Packagist profile API token) as encrypted GitHub secrets for the update-package API."}`,
169
+ url: "https://packagist.org/profile/",
170
+ },
171
+ ];
172
+ }
173
+
174
+ /** NuGet uses explicit account policy and preserves provided fallback credentials. */
175
+ export function nugetFlow(item, context) {
176
+ return [
177
+ {
178
+ id: "validate-package",
179
+ title: "Validate NuGet package",
180
+ kind: "check",
181
+ description:
182
+ "Build and test the selected project before configuration; do not upload.",
183
+ command: {
184
+ program: "dotnet",
185
+ args: [
186
+ "test",
187
+ item.manifest.split("/").at(-1),
188
+ "--configuration",
189
+ "Release",
190
+ ],
191
+ },
192
+ },
193
+ {
194
+ id: "configure-nuget-policy",
195
+ title: "Verify exact NuGet trusted publishing policy",
196
+ kind: "api",
197
+ description:
198
+ "Read explicit nuget.username, package_owner, package_ids and operations from .package-registry-manager.json; resolve immutable GitHub IDs, verify the authenticated profile, discover before creating a narrow policy, read activation back and ensure effective NUGET_USER through gh-manager. Retain existing fallback secrets.",
199
+ },
200
+ {
201
+ id: "verify-nuget-release",
202
+ title: "Verify NuGet CI and exact public consumer",
203
+ kind: "api",
204
+ description: `Verify ${context.workflow}: successful OIDC exchange and actual upload are separate gates. Retain existing token fallbacks. Require the repaired current-main revision, every required build/test/documentation/mirror/release stage, exact public package download and consumer restore/build. A skipped stage or placeholder remains incomplete.`,
205
+ },
206
+ ...(context.slug ? tokenSecretSteps(item, context.slug) : []),
207
+ ];
208
+ }
package/src/flows.mjs CHANGED
@@ -244,6 +244,11 @@ export function npmFlow(packageInfo, context) {
244
244
  name,
245
245
  "--browser=false",
246
246
  ]);
247
+ const artifact =
248
+ packageInfo.generated_alias ?? packageInfo.generated_artifact;
249
+ const packCwd = artifact
250
+ ? "{generated_package}"
251
+ : packageCwd(context.directory);
247
252
  const steps = [
248
253
  step(
249
254
  "validate-package",
@@ -276,6 +281,17 @@ export function npmFlow(packageInfo, context) {
276
281
  { command: command("npm", ["whoami"]) },
277
282
  ),
278
283
  ...worktreeSteps(),
284
+ ...(artifact
285
+ ? [
286
+ step(
287
+ "prepare-generated-artifact",
288
+ "Generate the selected npm artifact",
289
+ "check",
290
+ "Run the declared generator in the clean checkout and validate the generated name/version before packing.",
291
+ { when: "package-missing" },
292
+ ),
293
+ ]
294
+ : []),
279
295
  step(
280
296
  "pack",
281
297
  "Pack the package",
@@ -290,7 +306,7 @@ export function npmFlow(packageInfo, context) {
290
306
  "{pack_destination}",
291
307
  ]),
292
308
  when: "package-missing",
293
- cwd: packageCwd(context.directory),
309
+ cwd: packCwd,
294
310
  },
295
311
  ),
296
312
  step(
@@ -337,7 +353,7 @@ export function npmFlow(packageInfo, context) {
337
353
  "--provenance=false",
338
354
  ]),
339
355
  when: "package-missing",
340
- cwd: packageCwd(context.directory),
356
+ cwd: packCwd,
341
357
  },
342
358
  ),
343
359
  // Sign in right before publishing, so the sign-in, publish, and trust
@@ -373,7 +389,7 @@ export function npmFlow(packageInfo, context) {
373
389
  "--provenance=false",
374
390
  ]),
375
391
  when: "package-missing",
376
- cwd: packageCwd(context.directory),
392
+ cwd: packCwd,
377
393
  confirm: true,
378
394
  },
379
395
  ),
@@ -0,0 +1,48 @@
1
+ import path from "node:path";
2
+ import { readFile, realpath } from "node:fs/promises";
3
+
4
+ /** Generate each declared artifact, then pack only the selected validated directory. */
5
+ export async function prepareGeneratedArtifact(session) {
6
+ const item = session.plan.package;
7
+ const metadata = item.generated_alias ?? item.generated_artifact;
8
+ if (!metadata?.generator) {
9
+ throw new Error(
10
+ "generated npm artifact has no declared generator; configure it before bootstrap",
11
+ );
12
+ }
13
+ const source = await realpath(
14
+ path.resolve(session.values.worktree, path.dirname(item.manifest)),
15
+ );
16
+ const generator = await realpath(path.resolve(source, metadata.generator));
17
+ if (!generator.startsWith(`${source}${path.sep}`)) {
18
+ throw new Error("npm artifact generator must be inside the source package");
19
+ }
20
+ const destination = path.join(session.values.pack_destination, "generated");
21
+ for (const command of [
22
+ {
23
+ program: "npm",
24
+ args: ["ci", "--ignore-scripts", "--no-audit", "--no-fund"],
25
+ },
26
+ { program: "node", args: [generator, "--output-dir", destination] },
27
+ ]) {
28
+ const result = await session.capture({ command, cwd: source });
29
+ if (result.code !== 0) {
30
+ throw new Error(
31
+ `${command.program} failed while generating npm artifacts`,
32
+ );
33
+ }
34
+ }
35
+ const selected = await realpath(path.resolve(destination, item.name));
36
+ if (!selected.startsWith(`${await realpath(destination)}${path.sep}`)) {
37
+ throw new Error("invalid generated package path");
38
+ }
39
+ const actual = JSON.parse(
40
+ await readFile(path.join(selected, "package.json"), "utf8"),
41
+ );
42
+ if (actual.name !== item.name || actual.version !== item.version) {
43
+ throw new Error(
44
+ "generated artifact identity/version differs from inspection; nothing was published",
45
+ );
46
+ }
47
+ session.values.generated_package = selected;
48
+ }
package/src/github.mjs CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  createRunManager,
7
7
  createSecretHealth,
8
8
  createSecretManager,
9
+ createVariableManager,
10
+ createRepairVerifier,
9
11
  } from "@link-foundation/gh-manager";
10
12
 
11
13
  /** GitHub-only operations, supplied by the installed gh-manager library. */
@@ -32,6 +34,8 @@ export function githubServices(options = {}) {
32
34
  repos: createRepoManager(context),
33
35
  runs: createRunManager(context),
34
36
  health: createSecretHealth(context),
37
+ variables: (scope) => createVariableManager({ ...context, scope }),
38
+ repair: createRepairVerifier(context),
35
39
  secrets: (scope) => {
36
40
  if (typeof scope.repo === "string") {
37
41
  const [owner, name, extra] = scope.repo.split("/");
@@ -0,0 +1,130 @@
1
+ /** Registry normalization follows the registry's identity rules, not processing order. */
2
+ export function registryIdentity(registry, name) {
3
+ const normalized = registry === "go-modules" ? name : name.toLowerCase();
4
+ return `${registry}:${registry === "pypi" ? normalized.replace(/[-_.]+/g, "-") : registry === "crates-io" ? normalized.replaceAll("_", "-") : normalized}`;
5
+ }
6
+
7
+ /** Keep all source evidence so a collision cannot be hidden by --package filtering. */
8
+ export function identityConflicts(inspections) {
9
+ const groups = new Map();
10
+ for (const inspection of inspections) {
11
+ const repo = inspection.repository ?? {};
12
+ const repository =
13
+ repo.github_owner && repo.github_repository
14
+ ? `${repo.github_owner}/${repo.github_repository}`
15
+ : (repo.root ?? ".");
16
+ for (const item of inspection.packages.filter((p) => p.publishable)) {
17
+ const identity = registryIdentity(item.registry, item.name);
18
+ const source = {
19
+ repository,
20
+ manifest: item.manifest,
21
+ source_artifact: item.generated_alias?.source_manifest ?? item.manifest,
22
+ version: item.version,
23
+ registry_repository: item.registry_repository,
24
+ registry_owners: item.registry_owners,
25
+ permissions: "unknown",
26
+ shared_artifact: item.shared_artifact,
27
+ };
28
+ if (!groups.has(identity)) {
29
+ groups.set(identity, []);
30
+ }
31
+ const group = groups.get(identity);
32
+ if (
33
+ !group.some(
34
+ (entry) =>
35
+ entry.repository === repository && entry.manifest === item.manifest,
36
+ )
37
+ ) {
38
+ group.push(source);
39
+ }
40
+ }
41
+ }
42
+ return [...groups]
43
+ .filter(([, sources]) => sources.length > 1)
44
+ .map(([identity, sources]) => ({ identity, sources }));
45
+ }
46
+
47
+ /** Annotate each source before selection or external mutation. */
48
+ export function markIdentityConflicts(inspections) {
49
+ const conflicts = identityConflicts(inspections);
50
+ for (const inspection of inspections) {
51
+ for (const item of inspection.packages) {
52
+ const conflict = conflicts.find(
53
+ (group) =>
54
+ group.identity === registryIdentity(item.registry, item.name),
55
+ );
56
+ if (conflict) {
57
+ item.identity_conflicts = conflict.sources;
58
+ }
59
+ }
60
+ }
61
+ return conflicts;
62
+ }
63
+
64
+ export function validateIdentities(plans) {
65
+ const conflicts = identityConflicts(
66
+ plans.map((plan) => ({
67
+ repository: plan.repository,
68
+ packages: [plan.package],
69
+ })),
70
+ );
71
+ const annotated = plans.filter(
72
+ (plan) => plan.package.identity_conflicts?.length,
73
+ );
74
+ const unresolved = [
75
+ ...conflicts,
76
+ ...annotated.map((p) => ({
77
+ identity: registryIdentity(p.registry, p.package.name),
78
+ sources: p.package.identity_conflicts,
79
+ })),
80
+ ].filter((group) => !reviewedMapping(group, plans));
81
+ if (unresolved.length) {
82
+ throw new Error(
83
+ `conflicting package identities require distinct configured names before setup: ${JSON.stringify(unresolved)}`,
84
+ );
85
+ }
86
+ }
87
+
88
+ /** Require a complete, identical source/version inventory and one selected canonical publisher. */
89
+ function reviewedMapping(group, plans) {
90
+ const mapping = group.sources[0].shared_artifact;
91
+ if (
92
+ !mapping?.reviewed ||
93
+ typeof mapping.artifact !== "string" ||
94
+ !mapping.artifact ||
95
+ !mapping.publisher ||
96
+ !Array.isArray(mapping.sources)
97
+ ) {
98
+ return false;
99
+ }
100
+ const key = (source) =>
101
+ JSON.stringify([
102
+ source.repository,
103
+ source.manifest,
104
+ source.version ?? null,
105
+ ]);
106
+ if (
107
+ new Set(mapping.sources.map(key)).size !== group.sources.length ||
108
+ !group.sources.every(
109
+ (source) =>
110
+ mapping.sources.some((entry) => key(entry) === key(source)) &&
111
+ JSON.stringify(source.shared_artifact) === JSON.stringify(mapping),
112
+ )
113
+ ) {
114
+ return false;
115
+ }
116
+ return plans
117
+ .filter(
118
+ (plan) =>
119
+ registryIdentity(plan.registry, plan.package.name) === group.identity,
120
+ )
121
+ .every((plan) => {
122
+ const repo = plan.repository;
123
+ return (
124
+ (repo.github_owner
125
+ ? `${repo.github_owner}/${repo.github_repository}`
126
+ : repo.root) === mapping.publisher.repository &&
127
+ plan.package.manifest === mapping.publisher.manifest
128
+ );
129
+ });
130
+ }
@@ -1,7 +1,7 @@
1
1
  import { mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { exec } from "command-stream";
4
+ import { exec } from "./exec.mjs";
5
5
  import {
6
6
  githubSlug,
7
7
  manifestUrls,
package/src/model.mjs CHANGED
@@ -13,9 +13,15 @@ export const REGISTRIES = Object.freeze([
13
13
  "vscode-marketplace",
14
14
  "open-vsx",
15
15
  "chrome-web-store",
16
+ "cmake",
17
+ "conan",
18
+ "vcpkg",
16
19
  ]);
17
20
 
18
21
  const REGISTRY_ALIASES = new Map([
22
+ ["cmake", "cmake"],
23
+ ["conan", "conan"],
24
+ ["vcpkg", "vcpkg"],
19
25
  ["ruby", "rubygems"],
20
26
  ["rubygems", "rubygems"],
21
27
  ["gem", "rubygems"],
@@ -0,0 +1,68 @@
1
+ import { probePackage } from "./registry-state.mjs";
2
+ import { repositorySlug } from "./repository-identity.mjs";
3
+
4
+ /** The successful npm CLI line binds each name to the actual released version. */
5
+ export function publishedNpmVersions(source, names) {
6
+ const result = new Map();
7
+ for (const match of source.matchAll(
8
+ /(?:^|\s)\+\s+((?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*)@([0-9][0-9A-Za-z.+-]*)(?=\s|$)/g,
9
+ )) {
10
+ if (names.includes(match[1])) {
11
+ result.set(match[1], match[2]);
12
+ }
13
+ }
14
+ return result;
15
+ }
16
+
17
+ /** Verify every generated name against its own exact public version after CI. */
18
+ export async function verifyNpmRelease(session, runId) {
19
+ const item = session.plan.package;
20
+ const names = (item.generated_alias ?? item.generated_artifact)?.names ?? [
21
+ item.name,
22
+ ];
23
+ const slug = repositorySlug(session.plan.repository);
24
+ const capture = (args) =>
25
+ session.capture({
26
+ command: {
27
+ program: "gh",
28
+ args: ["run", "view", String(runId), "--repo", slug, ...args],
29
+ },
30
+ cwd: ".",
31
+ });
32
+ const result = await capture(["--json", "jobs"]);
33
+ if (result.code !== 0) {
34
+ throw new Error(
35
+ "release jobs are unavailable; exact alias versions remain unverified",
36
+ );
37
+ }
38
+ const versions = new Map();
39
+ for (const job of JSON.parse(result.stdout).jobs ?? []) {
40
+ const log = await capture(["--job", String(job.databaseId), "--log"]);
41
+ if (log.code !== 0) {
42
+ continue;
43
+ }
44
+ for (const entry of publishedNpmVersions(String(log.stdout), names)) {
45
+ versions.set(...entry);
46
+ }
47
+ }
48
+ const evidence = [];
49
+ for (const name of names) {
50
+ const version = versions.get(name);
51
+ if (!version) {
52
+ throw new Error(
53
+ `successful release log does not establish the actual version for ${name}`,
54
+ );
55
+ }
56
+ const state = await (session.options.probePackage ?? probePackage)(
57
+ { ...item, name, version },
58
+ session.options,
59
+ );
60
+ if (state.manifest_version_exists !== true) {
61
+ throw new Error(
62
+ `exact released version ${name}@${version} remains unverified`,
63
+ );
64
+ }
65
+ evidence.push({ name, version, url: state.version_evidence_url });
66
+ }
67
+ return evidence;
68
+ }