package-registry-manager 0.22.4 → 0.22.6
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 +7 -3
- package/package.json +4 -4
- package/src/bootstrap-reference.mjs +76 -0
- package/src/browser-catalogue.mjs +3 -3
- package/src/cli.mjs +67 -26
- package/src/containers.mjs +8 -1
- package/src/credential-browser.mjs +82 -0
- package/src/credential-cycle.mjs +91 -0
- package/src/credential-setup.mjs +210 -0
- package/src/credential-verification.mjs +96 -0
- package/src/discovery.mjs +113 -8
- package/src/extra-flows.mjs +88 -0
- package/src/flows.mjs +29 -19
- package/src/index.mjs +1 -1
- package/src/model.mjs +15 -0
- package/src/npm-policy.mjs +69 -0
- package/src/package-coverage.mjs +106 -0
- package/src/plan.mjs +51 -30
- package/src/prerequisites.mjs +2 -0
- package/src/publishers.mjs +114 -28
- package/src/python.mjs +149 -0
- package/src/setup-batch.mjs +174 -0
- package/src/setup.mjs +75 -61
- package/src/sign-in-import.mjs +8 -0
- package/src/skips.mjs +3 -0
- package/src/source-code.mjs +61 -0
- package/src/tokens.mjs +8 -0
- package/src/version-guard.mjs +34 -0
- package/src/workflow-proposal.mjs +362 -0
- package/src/workflows.mjs +49 -10
package/src/python.mjs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { exec } from "command-stream";
|
|
4
|
+
|
|
5
|
+
// requires-python describes interpreter releases. Reject unsupported or
|
|
6
|
+
// prerelease syntax rather than silently choosing an incompatible executable.
|
|
7
|
+
function release(value) {
|
|
8
|
+
return /^\d+(?:\.\d+){0,2}$/.test(value)
|
|
9
|
+
? value.split(".").map(Number)
|
|
10
|
+
: null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function compare(left, right) {
|
|
14
|
+
for (let i = 0; i < 3; i += 1) {
|
|
15
|
+
const difference = (left[i] ?? 0) - (right[i] ?? 0);
|
|
16
|
+
if (difference) {
|
|
17
|
+
return Math.sign(difference);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Tests stable interpreter versions against requires-python specifiers. */
|
|
24
|
+
export function pythonMatches(version, requirement = "") {
|
|
25
|
+
const actual = release(version);
|
|
26
|
+
if (!actual) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (!requirement.trim()) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return requirement.split(",").every((clause) => {
|
|
33
|
+
const match =
|
|
34
|
+
/^\s*(~=|==|!=|<=|>=|<|>)\s*(\d+(?:\.\d+){0,2})(\.\*)?\s*$/.exec(clause);
|
|
35
|
+
if (!match) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const [, operator, text, wildcard] = match;
|
|
39
|
+
const expected = release(text);
|
|
40
|
+
if (wildcard) {
|
|
41
|
+
const equal = expected.every((part, i) => actual[i] === part);
|
|
42
|
+
return operator === "==" ? equal : operator === "!=" && !equal;
|
|
43
|
+
}
|
|
44
|
+
const order = compare(actual, expected);
|
|
45
|
+
switch (operator) {
|
|
46
|
+
case "==":
|
|
47
|
+
return order === 0;
|
|
48
|
+
case "!=":
|
|
49
|
+
return order !== 0;
|
|
50
|
+
case ">=":
|
|
51
|
+
return order >= 0;
|
|
52
|
+
case "<=":
|
|
53
|
+
return order <= 0;
|
|
54
|
+
case ">":
|
|
55
|
+
return order > 0;
|
|
56
|
+
case "<":
|
|
57
|
+
return order < 0;
|
|
58
|
+
case "~=": {
|
|
59
|
+
if (expected.length < 2) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const upper = expected.slice(0, -1);
|
|
63
|
+
upper[upper.length - 1] += 1;
|
|
64
|
+
return order >= 0 && compare(actual, upper) < 0;
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Selects the first installed compatible interpreter, preferring PATH defaults. */
|
|
73
|
+
export function choosePython(tools, requirement) {
|
|
74
|
+
return tools.find((tool) => pythonMatches(tool.version, requirement)) ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function candidatePrograms() {
|
|
78
|
+
const candidates = new Set(["python", "python3"]);
|
|
79
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
80
|
+
if (!directory) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const entries = await readdir(directory).catch(() => []);
|
|
84
|
+
for (const entry of entries.sort()) {
|
|
85
|
+
if (/^python3\.\d+(?:\.exe)?$/.test(entry)) {
|
|
86
|
+
candidates.add(entry);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return [...candidates].slice(0, 32);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Probes only interpreter versions, without loading manifests or package code. */
|
|
94
|
+
export async function probePython(options = {}) {
|
|
95
|
+
const candidates = options.candidates ?? (await candidatePrograms());
|
|
96
|
+
const probe =
|
|
97
|
+
options.probe ??
|
|
98
|
+
(async (program, args) => {
|
|
99
|
+
if (options.verbose) {
|
|
100
|
+
console.error(`+ ${program} ${args.join(" ")}`);
|
|
101
|
+
}
|
|
102
|
+
return exec(program, args, {
|
|
103
|
+
capture: true,
|
|
104
|
+
mirror: false,
|
|
105
|
+
stdin: "ignore",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
const tools = [];
|
|
109
|
+
for (const program of candidates) {
|
|
110
|
+
try {
|
|
111
|
+
const result = await probe(program, ["--version"]);
|
|
112
|
+
const version = /^Python (\d+\.\d+\.\d+)\s*$/.exec(
|
|
113
|
+
String(result.stdout ?? "").trim(),
|
|
114
|
+
)?.[1];
|
|
115
|
+
if (result.code === 0 && version) {
|
|
116
|
+
tools.push({ program, version });
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// An unavailable candidate is not an installed interpreter.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return tools;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Adds a Python prerequisite and selects the executable used by the build step. */
|
|
126
|
+
export function applyPython(plan, tools) {
|
|
127
|
+
const build = plan.steps.find((step) => step.id === "build-package");
|
|
128
|
+
if (plan.registry !== "pypi" || !build || !tools) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const requirement = plan.package.requires_python ?? "";
|
|
132
|
+
const selected = choosePython(tools, requirement);
|
|
133
|
+
const item = {
|
|
134
|
+
id: "python",
|
|
135
|
+
title: "Python",
|
|
136
|
+
detected: selected
|
|
137
|
+
? `${selected.program} (${selected.version})`
|
|
138
|
+
: "no compatible interpreter found",
|
|
139
|
+
required: `Install Python satisfying ${requirement || "the project requirements"} and its build module (python -m pip install build)`,
|
|
140
|
+
ok: Boolean(selected),
|
|
141
|
+
};
|
|
142
|
+
plan.prerequisites = [
|
|
143
|
+
...(plan.prerequisites ?? []).filter((entry) => entry.id !== "python"),
|
|
144
|
+
item,
|
|
145
|
+
];
|
|
146
|
+
if (selected) {
|
|
147
|
+
build.command = { program: selected.program, args: ["-m", "build"] };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { protectLegacyProfile } from "./profile.mjs";
|
|
3
|
+
import { SetupSession, prompt } from "./setup.mjs";
|
|
4
|
+
import { offerWorkflow } from "./workflow-proposal.mjs";
|
|
5
|
+
import { signInDomains } from "./sign-in-import.mjs";
|
|
6
|
+
import { oidcReleaseNote } from "./approvals.mjs";
|
|
7
|
+
|
|
8
|
+
/** Validates a plan before running any registry or repository mutations. */
|
|
9
|
+
export function validatePlan(plan, execute = true) {
|
|
10
|
+
if (!plan.package.publishable) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`${plan.package.name} is not publishable: ${(plan.package.problems ?? []).join("; ")}`,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
if (
|
|
16
|
+
execute &&
|
|
17
|
+
plan.skipped_reason &&
|
|
18
|
+
!plan.steps.some((step) => step.id === "add-publishing-workflow")
|
|
19
|
+
) {
|
|
20
|
+
throw new Error(plan.skipped_reason);
|
|
21
|
+
}
|
|
22
|
+
const python = plan.prerequisites?.find(
|
|
23
|
+
(item) => item.id === "python" && item.ok === false,
|
|
24
|
+
);
|
|
25
|
+
if (execute && python) {
|
|
26
|
+
throw new Error(`${python.detected}; ${python.required}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Runs one package, using the same executor as setup --all. */
|
|
31
|
+
export async function executePlan(plan, options) {
|
|
32
|
+
return executePlans([plan], options, false);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Runs all plans with one browser session and defers sign-out until the end. */
|
|
36
|
+
export async function executePlans(plans, options, summary = true) {
|
|
37
|
+
if (plans.length === 0) {
|
|
38
|
+
throw new Error("no publishable package manifests were found");
|
|
39
|
+
}
|
|
40
|
+
for (const plan of plans) {
|
|
41
|
+
validatePlan(plan, options.execute);
|
|
42
|
+
}
|
|
43
|
+
for (const plan of plans) {
|
|
44
|
+
if (plan.mode === "complete") {
|
|
45
|
+
console.log(
|
|
46
|
+
`${plan.package.name} already publishes through trusted publishing; ${plan.steps.length ? "only the repository checks remain." : "nothing to do."}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
plans.every((plan) => plan.mode === "complete" && plan.steps.length === 0)
|
|
52
|
+
) {
|
|
53
|
+
const outcomes = plans.map((plan) => ({
|
|
54
|
+
registry: plan.registry,
|
|
55
|
+
package: plan.package.name,
|
|
56
|
+
status: "complete",
|
|
57
|
+
}));
|
|
58
|
+
printSummary(outcomes, summary);
|
|
59
|
+
return outcomes;
|
|
60
|
+
}
|
|
61
|
+
if (!options.execute) {
|
|
62
|
+
console.log(
|
|
63
|
+
"Dry run only. Re-run with --execute to run these steps and open the registry.",
|
|
64
|
+
);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const missing = plans.filter((plan) =>
|
|
68
|
+
plan.steps.some((step) => step.id === "add-publishing-workflow"),
|
|
69
|
+
);
|
|
70
|
+
if (missing.length) {
|
|
71
|
+
const result = await offerWorkflow(missing, {
|
|
72
|
+
...options,
|
|
73
|
+
prompt: options.prompt ?? prompt,
|
|
74
|
+
});
|
|
75
|
+
const outcomes = plans.map((plan) => ({
|
|
76
|
+
registry: plan.registry,
|
|
77
|
+
package: plan.package.name,
|
|
78
|
+
status: missing.includes(plan) ? result.status : "blocked",
|
|
79
|
+
...(result.url ? { url: result.url } : {}),
|
|
80
|
+
}));
|
|
81
|
+
printSummary(outcomes, summary);
|
|
82
|
+
return outcomes;
|
|
83
|
+
}
|
|
84
|
+
if (options.browserProfile) {
|
|
85
|
+
await protectLegacyProfile(
|
|
86
|
+
options.repository,
|
|
87
|
+
options.browserProfile,
|
|
88
|
+
options,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const domains = [
|
|
92
|
+
...new Set(plans.flatMap((plan) => signInDomains(plan.registry))),
|
|
93
|
+
];
|
|
94
|
+
let automation = options.automation ?? null;
|
|
95
|
+
const deferred = new Map();
|
|
96
|
+
const outcomes = [];
|
|
97
|
+
let failure;
|
|
98
|
+
try {
|
|
99
|
+
for (const plan of plans) {
|
|
100
|
+
const session = new SetupSession(plan, {
|
|
101
|
+
...options,
|
|
102
|
+
automation,
|
|
103
|
+
domains,
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
await session.run();
|
|
107
|
+
outcomes.push({
|
|
108
|
+
registry: plan.registry,
|
|
109
|
+
package: plan.package.name,
|
|
110
|
+
status: plan.steps.length === 0 ? "complete" : "configured",
|
|
111
|
+
});
|
|
112
|
+
if (plan.trusted_publisher?.workflow) {
|
|
113
|
+
console.log(`\n${oidcReleaseNote(plan.trusted_publisher.workflow)}`);
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
outcomes.push({
|
|
117
|
+
registry: plan.registry,
|
|
118
|
+
package: plan.package.name,
|
|
119
|
+
status: "failed",
|
|
120
|
+
});
|
|
121
|
+
failure = error;
|
|
122
|
+
} finally {
|
|
123
|
+
const steps = await session.cleanup({
|
|
124
|
+
deferAuth: plans.length > 1,
|
|
125
|
+
closeBrowser: false,
|
|
126
|
+
});
|
|
127
|
+
automation = session.automation;
|
|
128
|
+
for (const step of steps) {
|
|
129
|
+
deferred.set(`${plan.registry}:${step.id}`, { session, step });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (failure) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
for (const { session, step } of deferred.values()) {
|
|
138
|
+
session.automation = automation;
|
|
139
|
+
try {
|
|
140
|
+
await session.runStep(step);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`warning: ${step.id} failed: ${error.message}`);
|
|
143
|
+
} finally {
|
|
144
|
+
if (session.shim?.directory) {
|
|
145
|
+
await rm(session.shim.directory, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await automation?.close();
|
|
150
|
+
for (const plan of plans.slice(outcomes.length)) {
|
|
151
|
+
outcomes.push({
|
|
152
|
+
registry: plan.registry,
|
|
153
|
+
package: plan.package.name,
|
|
154
|
+
status: "not-run",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
printSummary(outcomes, summary);
|
|
158
|
+
}
|
|
159
|
+
if (failure) {
|
|
160
|
+
throw failure;
|
|
161
|
+
}
|
|
162
|
+
return outcomes;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function printSummary(outcomes, enabled) {
|
|
166
|
+
if (enabled) {
|
|
167
|
+
console.log("\nSetup summary:");
|
|
168
|
+
for (const item of outcomes) {
|
|
169
|
+
console.log(
|
|
170
|
+
`- ${item.registry}: ${item.package}: ${item.status}${item.url ? ` (${item.url})` : ""}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/setup.mjs
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
approvalDeadline,
|
|
17
17
|
TWO_FACTOR_HINT,
|
|
18
|
-
oidcReleaseNote,
|
|
19
18
|
withFreshLinks,
|
|
20
19
|
} from "./approvals.mjs";
|
|
21
20
|
import { connectAutomation } from "./automation.mjs";
|
|
@@ -27,10 +26,11 @@ import {
|
|
|
27
26
|
} from "./browser.mjs";
|
|
28
27
|
import { detectDefaultBrowser, openWithCommand } from "./default-browser.mjs";
|
|
29
28
|
import { CLEANUP_CONDITIONS } from "./flows.mjs";
|
|
29
|
+
import { setupCredential } from "./credential-setup.mjs";
|
|
30
|
+
import { checkNamePolicy, policyRefusal } from "./npm-policy.mjs";
|
|
30
31
|
import { pagesSettingsUrl, pagesState } from "./pages.mjs";
|
|
31
32
|
import { packageDirectory } from "./plan.mjs";
|
|
32
33
|
import { twoFactorMode } from "./prerequisites.mjs";
|
|
33
|
-
import { protectLegacyProfile } from "./profile.mjs";
|
|
34
34
|
import { getJson, probePackage } from "./registry-state.mjs";
|
|
35
35
|
import { launchBrowser, signInDomains } from "./sign-in-import.mjs";
|
|
36
36
|
import { reportTokenSecrets, verifyTokenRevoked } from "./tokens.mjs";
|
|
@@ -54,54 +54,9 @@ const PUBLISH_REJECTED = /\bE404\b|404 Not Found|invalid-publisher/i;
|
|
|
54
54
|
/** Where `--browser` opens URLs that need no automation. */
|
|
55
55
|
export const BROWSER_MODES = ["default", "automated"];
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
* Runs a setup plan. Without `options.execute` it only reports a dry run.
|
|
59
|
-
* Steps whose `when` condition does not hold are skipped, so a re-run after a
|
|
60
|
-
* partial success resumes where the previous run stopped.
|
|
61
|
-
*/
|
|
62
|
-
export async function executePlan(plan, options) {
|
|
63
|
-
if (!plan.package.publishable) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
`${plan.package.name} is not publishable: ${(plan.package.problems ?? []).join("; ")}`,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
if (plan.mode === "complete") {
|
|
69
|
-
const remaining =
|
|
70
|
-
plan.steps.length === 0
|
|
71
|
-
? "nothing to do."
|
|
72
|
-
: "only the repository checks remain.";
|
|
73
|
-
console.log(
|
|
74
|
-
`${plan.package.name} already publishes through trusted publishing; ${remaining}`,
|
|
75
|
-
);
|
|
76
|
-
if (plan.steps.length === 0) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (!options.execute) {
|
|
81
|
-
console.log(
|
|
82
|
-
"Dry run only. Re-run with --execute to run these steps and open the registry.",
|
|
83
|
-
);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (options.browserProfile) {
|
|
87
|
-
await protectLegacyProfile(
|
|
88
|
-
options.repository,
|
|
89
|
-
options.browserProfile,
|
|
90
|
-
options,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
const session = new SetupSession(plan, options);
|
|
94
|
-
try {
|
|
95
|
-
await session.run();
|
|
96
|
-
} finally {
|
|
97
|
-
await session.cleanup();
|
|
98
|
-
}
|
|
99
|
-
if (plan.trusted_publisher?.workflow) {
|
|
100
|
-
console.log(`\n${oidcReleaseNote(plan.trusted_publisher.workflow)}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
57
|
+
export { executePlan, executePlans } from "./setup-batch.mjs";
|
|
103
58
|
|
|
104
|
-
class SetupSession {
|
|
59
|
+
export class SetupSession {
|
|
105
60
|
constructor(plan, options) {
|
|
106
61
|
this.plan = plan;
|
|
107
62
|
this.options = options;
|
|
@@ -135,9 +90,22 @@ class SetupSession {
|
|
|
135
90
|
* Runs every cleanup step of the plan whose condition holds, including
|
|
136
91
|
* those after a step that failed (#24).
|
|
137
92
|
*/
|
|
138
|
-
async cleanup() {
|
|
93
|
+
async cleanup({ deferAuth = false, closeBrowser = true } = {}) {
|
|
94
|
+
const deferred = [];
|
|
139
95
|
for (const step of cleanupSteps(this.plan)) {
|
|
140
|
-
if (
|
|
96
|
+
if (
|
|
97
|
+
deferAuth &&
|
|
98
|
+
["sign-out", "crates-sign-out"].includes(step.id) &&
|
|
99
|
+
this.conditions.has(step.when)
|
|
100
|
+
) {
|
|
101
|
+
if (step.id === "sign-out" && this.options.keepSession) {
|
|
102
|
+
console.log(
|
|
103
|
+
"Keeping the npm session (--keep-session); run npm logout to revoke it.",
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
deferred.push(step);
|
|
107
|
+
}
|
|
108
|
+
} else if (step.id === "sign-out" && this.keepsSession(step)) {
|
|
141
109
|
console.log(
|
|
142
110
|
"Keeping the npm session (--keep-session): its token stays in npm's user configuration until you run npm logout, and the next run reuses it while npm whoami succeeds.",
|
|
143
111
|
);
|
|
@@ -148,13 +116,17 @@ class SetupSession {
|
|
|
148
116
|
);
|
|
149
117
|
} else if (this.conditions.has(step.when)) {
|
|
150
118
|
this.log(`==> ${step.title}`);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
119
|
+
try {
|
|
120
|
+
const result = await this.runProcess(step, true);
|
|
121
|
+
if (result.code !== 0) {
|
|
122
|
+
console.error(`warning: ${step.id} exited with ${result.code}`);
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error(`warning: ${step.id} failed: ${error.message}`);
|
|
154
126
|
}
|
|
155
127
|
}
|
|
156
128
|
}
|
|
157
|
-
if (this.automation) {
|
|
129
|
+
if (this.automation && closeBrowser) {
|
|
158
130
|
await this.automation.close();
|
|
159
131
|
}
|
|
160
132
|
for (const directory of [this.temporary, this.shim?.directory]) {
|
|
@@ -162,6 +134,9 @@ class SetupSession {
|
|
|
162
134
|
await rm(directory, { recursive: true, force: true });
|
|
163
135
|
}
|
|
164
136
|
}
|
|
137
|
+
this.shim = null;
|
|
138
|
+
this.temporary = null;
|
|
139
|
+
return deferred;
|
|
165
140
|
}
|
|
166
141
|
|
|
167
142
|
keepsSession(step) {
|
|
@@ -175,6 +150,9 @@ class SetupSession {
|
|
|
175
150
|
async runStep(step) {
|
|
176
151
|
switch (step.kind) {
|
|
177
152
|
case "check":
|
|
153
|
+
if (step.id === "check-name-policy") {
|
|
154
|
+
return checkNamePolicy(this.plan.package.name, this.options);
|
|
155
|
+
}
|
|
178
156
|
if (step.id === "check-registry") {
|
|
179
157
|
return this.checkRegistry();
|
|
180
158
|
}
|
|
@@ -194,9 +172,25 @@ class SetupSession {
|
|
|
194
172
|
case "browser":
|
|
195
173
|
return this.browser(step);
|
|
196
174
|
case "api":
|
|
197
|
-
return
|
|
175
|
+
return step.id === "manage-registry-token"
|
|
176
|
+
? setupCredential(this)
|
|
177
|
+
: runCratesApiStep(this, step);
|
|
198
178
|
default:
|
|
199
179
|
console.log(step.description);
|
|
180
|
+
if (step.id === "confirm-oidc-cleanup") {
|
|
181
|
+
if (this.conditions.has("oidc-release-verified")) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const answer = await this.prompt(
|
|
185
|
+
"Has a new release succeeded through OIDC without a stored registry token? [y/N] ",
|
|
186
|
+
);
|
|
187
|
+
if (!/^(?:y|yes)$/i.test(answer)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
"a verified OIDC release is required before deleting token secrets",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
200
194
|
if (step.url) {
|
|
201
195
|
console.log(` ${step.url}`);
|
|
202
196
|
}
|
|
@@ -475,6 +469,14 @@ class SetupSession {
|
|
|
475
469
|
? await this.capture(step)
|
|
476
470
|
: await this.runProcess(step, true);
|
|
477
471
|
if (result.code !== 0) {
|
|
472
|
+
if (
|
|
473
|
+
step.id === "first-publish" &&
|
|
474
|
+
policyRefusal(String(result.stderr) + String(result.stdout))
|
|
475
|
+
) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
"npm refused this name under its policy; no retry. Choose @owner/name or a longer descriptive name.",
|
|
478
|
+
);
|
|
479
|
+
}
|
|
478
480
|
if (step.id === "attach-trusted-publisher") {
|
|
479
481
|
console.error(
|
|
480
482
|
"warning: npm trust failed; falling back to the browser form",
|
|
@@ -527,6 +529,15 @@ class SetupSession {
|
|
|
527
529
|
|
|
528
530
|
recordPack(output) {
|
|
529
531
|
const packed = packedEntry(output);
|
|
532
|
+
if (
|
|
533
|
+
packed.name !== this.plan.package.name ||
|
|
534
|
+
(this.plan.package.version &&
|
|
535
|
+
packed.version !== this.plan.package.version)
|
|
536
|
+
) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
"bootstrap ref package identity/version differs from inspection; inspect the selected ref again before publication",
|
|
539
|
+
);
|
|
540
|
+
}
|
|
530
541
|
for (const file of packed.files ?? []) {
|
|
531
542
|
console.log(` ${String(file.size).padStart(8)} ${file.path}`);
|
|
532
543
|
}
|
|
@@ -551,6 +562,9 @@ class SetupSession {
|
|
|
551
562
|
(step.id !== "confirm-provenance" ||
|
|
552
563
|
isTrustedRelease(document, this.values))
|
|
553
564
|
) {
|
|
565
|
+
if (step.id === "confirm-provenance") {
|
|
566
|
+
this.conditions.add("oidc-release-verified");
|
|
567
|
+
}
|
|
554
568
|
console.log(` ${url} is ready`);
|
|
555
569
|
return;
|
|
556
570
|
}
|
|
@@ -641,16 +655,16 @@ class SetupSession {
|
|
|
641
655
|
* The automated page, launched once. The crates.io sign-in step offers
|
|
642
656
|
* `--browser-import default|auto` itself, once it knows a sign-in is missing.
|
|
643
657
|
*/
|
|
644
|
-
async automatedPage(browser = this.options.browserOptions) {
|
|
658
|
+
async automatedPage(browser = this.options.browserOptions, quiet = false) {
|
|
645
659
|
if (!this.automation) {
|
|
646
|
-
const domains = signInDomains(this.plan.registry);
|
|
660
|
+
const domains = this.options.domains ?? signInDomains(this.plan.registry);
|
|
647
661
|
this.automation = await connectAutomation({
|
|
648
662
|
browser: await launchBrowser(browser, domains, {
|
|
649
663
|
deferred: this.plan.registry === "crates-io",
|
|
650
|
-
verbose: this.options.verbose,
|
|
664
|
+
verbose: quiet ? false : this.options.verbose,
|
|
651
665
|
}),
|
|
652
666
|
profile: this.options.browserProfile,
|
|
653
|
-
verbose: this.options.verbose,
|
|
667
|
+
verbose: quiet ? false : this.options.verbose,
|
|
654
668
|
domains,
|
|
655
669
|
});
|
|
656
670
|
}
|
|
@@ -872,7 +886,7 @@ function render(command) {
|
|
|
872
886
|
}
|
|
873
887
|
|
|
874
888
|
/** Asks a question on the terminal; a closed stdin answers with "". */
|
|
875
|
-
async function prompt(message) {
|
|
889
|
+
export async function prompt(message) {
|
|
876
890
|
const terminal = createInterface({ input: stdin, output: stdout });
|
|
877
891
|
try {
|
|
878
892
|
return await new Promise((resolve) => {
|
package/src/sign-in-import.mjs
CHANGED
|
@@ -15,6 +15,14 @@ export const IMPORT_SCOPES = ["domains", "full"];
|
|
|
15
15
|
|
|
16
16
|
/** The domains whose cookies hold each registry's sign-in. */
|
|
17
17
|
export const SIGN_IN_DOMAINS = {
|
|
18
|
+
"docker-hub": ["docker.com", "docker.io"],
|
|
19
|
+
"maven-central": ["sonatype.com"],
|
|
20
|
+
"vscode-marketplace": ["dev.azure.com", "login.microsoftonline.com"],
|
|
21
|
+
"open-vsx": ["open-vsx.org", "github.com"],
|
|
22
|
+
"chrome-web-store": ["google.com"],
|
|
23
|
+
rubygems: ["rubygems.org", "github.com"],
|
|
24
|
+
nuget: ["nuget.org", "login.microsoftonline.com"],
|
|
25
|
+
jsr: ["jsr.io", "github.com"],
|
|
18
26
|
"crates-io": ["crates.io", "github.com"],
|
|
19
27
|
npm: ["npmjs.com"],
|
|
20
28
|
pypi: ["pypi.org", "github.com"],
|
package/src/skips.mjs
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Removes comments without interpreting comment markers inside quoted strings. */
|
|
2
|
+
export function stripComments(contents, language = "shell") {
|
|
3
|
+
const cStyle = /\.(?:[cm]?[jt]s|[cm]ts|rs)$/.test(language);
|
|
4
|
+
let quote = null;
|
|
5
|
+
let block = false;
|
|
6
|
+
let line = false;
|
|
7
|
+
let escaped = false;
|
|
8
|
+
let result = "";
|
|
9
|
+
for (let index = 0; index < contents.length; index += 1) {
|
|
10
|
+
const character = contents[index];
|
|
11
|
+
const next = contents[index + 1];
|
|
12
|
+
if (character === "\n" && line) {
|
|
13
|
+
line = false;
|
|
14
|
+
}
|
|
15
|
+
if (line || block) {
|
|
16
|
+
if (block && character === "*" && next === "/") {
|
|
17
|
+
block = false;
|
|
18
|
+
result += " ";
|
|
19
|
+
index += 1;
|
|
20
|
+
} else {
|
|
21
|
+
result += character === "\n" ? "\n" : " ";
|
|
22
|
+
}
|
|
23
|
+
} else if (quote) {
|
|
24
|
+
result += character;
|
|
25
|
+
if (!escaped && character === quote) {
|
|
26
|
+
quote = null;
|
|
27
|
+
}
|
|
28
|
+
escaped = !escaped && character === "\\";
|
|
29
|
+
} else if (["'", '"', "`"].includes(character)) {
|
|
30
|
+
quote = character;
|
|
31
|
+
result += character;
|
|
32
|
+
} else if (
|
|
33
|
+
(cStyle && character === "/" && next === "/") ||
|
|
34
|
+
(!cStyle && character === "#")
|
|
35
|
+
) {
|
|
36
|
+
line = true;
|
|
37
|
+
result += " ";
|
|
38
|
+
} else if (cStyle && character === "/" && next === "*") {
|
|
39
|
+
block = true;
|
|
40
|
+
result += " ";
|
|
41
|
+
index += 1;
|
|
42
|
+
} else {
|
|
43
|
+
result += character;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether a prefix places a publishing command in an executed command. */
|
|
50
|
+
export function commandPosition(prefix) {
|
|
51
|
+
const segment = prefix.split(/[;&|]/).at(-1).trimStart();
|
|
52
|
+
return (
|
|
53
|
+
/^(?:(?:npx|bunx|exec|sudo|env|time|command|pnpm\s+exec|yarn|\w+=\S*)\s+)*$/.test(
|
|
54
|
+
segment,
|
|
55
|
+
) ||
|
|
56
|
+
/(?:\$|shell)\s*`\s*$/.test(segment) ||
|
|
57
|
+
/(?:exec(?:Sync)?|spawn(?:Sync)?|run(?:_command)?|system|check_call|check_output|Popen)\s*\(\s*["'`]\s*$/.test(
|
|
58
|
+
segment,
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
}
|
package/src/tokens.mjs
CHANGED
|
@@ -29,6 +29,14 @@ export function tokenSecretSteps(packageInfo, slug) {
|
|
|
29
29
|
},
|
|
30
30
|
cwd: ".",
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
id: "confirm-oidc-cleanup",
|
|
34
|
+
title: "Confirm a successful OIDC release before deleting secrets",
|
|
35
|
+
kind: "manual",
|
|
36
|
+
description:
|
|
37
|
+
"Verify that a new release succeeded through OIDC without a stored registry token. Confirm registry acceptance and remove token references from every workflow before deleting unused secrets.",
|
|
38
|
+
when: "token-secret-present",
|
|
39
|
+
},
|
|
32
40
|
{
|
|
33
41
|
id: "delete-token-secret",
|
|
34
42
|
title: "Delete the leftover token secrets",
|