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.
- package/README.md +12 -0
- package/package.json +3 -3
- package/src/audit-config.mjs +96 -0
- package/src/automation.mjs +12 -5
- package/src/bootstrap-recovery.mjs +163 -0
- package/src/browser.mjs +2 -2
- package/src/cli-main.mjs +20 -6
- package/src/cpp-distribution.mjs +127 -0
- package/src/crates-api.mjs +9 -2
- package/src/credential-cycle.mjs +8 -3
- package/src/default-browser.mjs +1 -1
- package/src/default-scope.mjs +1 -1
- package/src/discovery.mjs +157 -13
- package/src/distribution-channel.mjs +87 -0
- package/src/exec.mjs +20 -0
- package/src/extra-flows.mjs +39 -0
- package/src/flows.mjs +19 -3
- package/src/generated-artifact.mjs +48 -0
- package/src/github.mjs +38 -1
- package/src/identity-conflicts.mjs +130 -0
- package/src/manifest-proposal.mjs +1 -1
- package/src/migration-cleanup.mjs +31 -9
- package/src/model.mjs +6 -0
- package/src/npm-release.mjs +68 -0
- package/src/nuget-ci.mjs +207 -0
- package/src/nuget-migration.mjs +98 -0
- package/src/nuget-page.mjs +100 -0
- package/src/nuget-policy.mjs +198 -0
- package/src/nuget-publish.py +145 -0
- package/src/nuget-setup.mjs +169 -0
- package/src/nuget-workflow.mjs +54 -0
- package/src/organization-scan.mjs +76 -11
- package/src/organization-setup.mjs +7 -5
- package/src/pack-review.mjs +122 -0
- package/src/plan.mjs +19 -6
- package/src/prerequisites.mjs +1 -1
- package/src/profile.mjs +1 -1
- package/src/publication-state.mjs +265 -0
- package/src/publishers.mjs +95 -2
- package/src/publishing-policy.mjs +7 -1
- package/src/publishing-state.mjs +51 -0
- package/src/python.mjs +1 -1
- package/src/registry-state.mjs +177 -25
- package/src/release-runs.mjs +3 -4
- package/src/request-policy.mjs +27 -0
- package/src/setup-batch.mjs +48 -2
- package/src/setup.mjs +101 -123
- package/src/sign-in-import.mjs +132 -40
- package/src/skips.mjs +9 -1
- package/src/source-code.mjs +2 -0
- package/src/tokens.mjs +36 -6
- package/src/webdriver-automation.mjs +3 -2
- package/src/workflow-fallback.mjs +125 -0
- package/src/workflow-migration.mjs +150 -78
- 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.
|
|
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/
|
|
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.
|
|
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
|
+
}
|
package/src/automation.mjs
CHANGED
|
@@ -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(
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
379
|
-
|
|
380
|
-
|
|
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.
|
|
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
|
+
}
|
package/src/crates-api.mjs
CHANGED
|
@@ -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
|
-
|
|
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": {
|
package/src/credential-cycle.mjs
CHANGED
|
@@ -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
|
-
|
|
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`,
|
package/src/default-browser.mjs
CHANGED
package/src/default-scope.mjs
CHANGED