package-registry-manager 0.22.7 → 0.22.8
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 +14 -0
- package/package.json +3 -2
- package/src/ci-credential-cycle.mjs +121 -0
- package/src/cli.mjs +72 -1
- package/src/credential-cycle.mjs +1 -1
- package/src/credential-setup.mjs +209 -161
- package/src/github.mjs +42 -0
- package/src/index.mjs +2 -0
- package/src/organization-scan.mjs +259 -0
- package/src/organization-setup.mjs +203 -0
- package/src/publishing-policy.mjs +46 -0
- package/src/registry-state.mjs +22 -0
- package/src/setup-batch.mjs +53 -4
- package/src/setup.mjs +1 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { inspectRepository } from "./discovery.mjs";
|
|
5
|
+
import { githubServices } from "./github.mjs";
|
|
6
|
+
import { probeRegistryState } from "./registry-state.mjs";
|
|
7
|
+
import { RELEASE_FAILURE_PATTERNS } from "./publishing-policy.mjs";
|
|
8
|
+
import { credentialPolicy } from "./credential-cycle.mjs";
|
|
9
|
+
import { identityCommand } from "./repository-identity.mjs";
|
|
10
|
+
|
|
11
|
+
/** Only files used by existing discovery; no full repository clones for scans. */
|
|
12
|
+
export const SCAN_MATCHES = [
|
|
13
|
+
...[
|
|
14
|
+
"package.json",
|
|
15
|
+
"Cargo.toml",
|
|
16
|
+
"pyproject.toml",
|
|
17
|
+
"setup.py",
|
|
18
|
+
"go.mod",
|
|
19
|
+
"pom.xml",
|
|
20
|
+
"build.gradle",
|
|
21
|
+
"build.gradle.kts",
|
|
22
|
+
"composer.json",
|
|
23
|
+
"jsr.json",
|
|
24
|
+
"deno.json",
|
|
25
|
+
"*.csproj",
|
|
26
|
+
"*.fsproj",
|
|
27
|
+
"*.vbproj",
|
|
28
|
+
"*.gemspec",
|
|
29
|
+
"Dockerfile*",
|
|
30
|
+
"Containerfile*",
|
|
31
|
+
"docker-compose*.yml",
|
|
32
|
+
"docker-compose*.yaml",
|
|
33
|
+
"compose*.yml",
|
|
34
|
+
"compose*.yaml",
|
|
35
|
+
].map((name) => `**/${name}`),
|
|
36
|
+
".github/workflows/*.yml",
|
|
37
|
+
".github/workflows/*.yaml",
|
|
38
|
+
".package-registry-manager.json",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function packageFindings(inspection) {
|
|
42
|
+
const slug = `${inspection.repository.github_owner}/${inspection.repository.github_repository}`;
|
|
43
|
+
return inspection.packages
|
|
44
|
+
.filter((item) => item.publishable)
|
|
45
|
+
.flatMap((item) => {
|
|
46
|
+
const common = {
|
|
47
|
+
repository: slug,
|
|
48
|
+
registry: item.registry,
|
|
49
|
+
package: item.name,
|
|
50
|
+
manifest: item.manifest,
|
|
51
|
+
};
|
|
52
|
+
const findings = [];
|
|
53
|
+
if (item.exists_on_registry === false) {
|
|
54
|
+
findings.push({ ...common, type: "unpublished" });
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
item.exists_on_registry === true &&
|
|
58
|
+
(item.trusted_publishing === false ||
|
|
59
|
+
credentialPolicy(item.registry).mode === "token")
|
|
60
|
+
) {
|
|
61
|
+
findings.push({
|
|
62
|
+
...common,
|
|
63
|
+
type: "published-without-trusted-publishing",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const others = (item.configured_publishers ?? []).filter(
|
|
67
|
+
(publisher) =>
|
|
68
|
+
publisher.repository.toLowerCase() !== slug.toLowerCase(),
|
|
69
|
+
);
|
|
70
|
+
if (others.length) {
|
|
71
|
+
findings.push({
|
|
72
|
+
...common,
|
|
73
|
+
type: "trusted-publisher-other-repository",
|
|
74
|
+
evidence: others,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return findings;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function writeSnapshot(root, slug, files) {
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
if (
|
|
84
|
+
typeof file.path !== "string" ||
|
|
85
|
+
file.path.includes("\\") ||
|
|
86
|
+
file.path.includes(":") ||
|
|
87
|
+
file.path
|
|
88
|
+
.split("/")
|
|
89
|
+
.some(
|
|
90
|
+
(part) => !part || part === "." || part === ".." || part === ".git",
|
|
91
|
+
)
|
|
92
|
+
) {
|
|
93
|
+
throw new Error("unsafe repository file path");
|
|
94
|
+
}
|
|
95
|
+
if (typeof file.content !== "string") {
|
|
96
|
+
throw new Error("gh-manager returned no file content");
|
|
97
|
+
}
|
|
98
|
+
const target = path.join(root, file.path);
|
|
99
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
100
|
+
await writeFile(target, file.content);
|
|
101
|
+
}
|
|
102
|
+
await mkdir(path.join(root, ".git"), { recursive: true });
|
|
103
|
+
await writeFile(
|
|
104
|
+
path.join(root, ".git/config"),
|
|
105
|
+
`[remote "origin"]\n url = https://github.com/${slug}.git\n`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Keep snapshots alive only for the callback; cleanup also runs after errors. */
|
|
110
|
+
export async function withAccountScan(options, callback) {
|
|
111
|
+
if (
|
|
112
|
+
Boolean(options.org) === Boolean(options.user) ||
|
|
113
|
+
!/^[\w.-]+$/.test(options.org ?? options.user)
|
|
114
|
+
) {
|
|
115
|
+
throw new Error("choose exactly one valid --org or --user");
|
|
116
|
+
}
|
|
117
|
+
const github = options.github ?? githubServices(options);
|
|
118
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "prm-account-"));
|
|
119
|
+
try {
|
|
120
|
+
const repositories = await github.repos.list({
|
|
121
|
+
org: options.org,
|
|
122
|
+
user: options.user,
|
|
123
|
+
});
|
|
124
|
+
const scan = {
|
|
125
|
+
schema_version: 1,
|
|
126
|
+
account: options.org ?? options.user,
|
|
127
|
+
repositories: [],
|
|
128
|
+
findings: [],
|
|
129
|
+
};
|
|
130
|
+
for (const [index, repo] of repositories.entries()) {
|
|
131
|
+
const slug = repo.full_name;
|
|
132
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(slug)) {
|
|
133
|
+
throw new Error("gh-manager returned an invalid repository name");
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const root = path.join(directory, String(index));
|
|
137
|
+
await writeSnapshot(
|
|
138
|
+
root,
|
|
139
|
+
slug,
|
|
140
|
+
await github.repos.files(slug, {
|
|
141
|
+
match: SCAN_MATCHES,
|
|
142
|
+
content: true,
|
|
143
|
+
ref: repo.default_branch,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
const discovered = await inspectRepository(root, {
|
|
147
|
+
includeSkipped: options.verbose,
|
|
148
|
+
});
|
|
149
|
+
const inspection = options.offline
|
|
150
|
+
? discovered
|
|
151
|
+
: await (options.probe ?? probeRegistryState)(discovered, {
|
|
152
|
+
...options,
|
|
153
|
+
runIdentity: async (program, args) =>
|
|
154
|
+
program === "gh"
|
|
155
|
+
? slug
|
|
156
|
+
: identityCommand(program, args, {
|
|
157
|
+
...options,
|
|
158
|
+
repository: root,
|
|
159
|
+
}),
|
|
160
|
+
repository: root,
|
|
161
|
+
});
|
|
162
|
+
scan.repositories.push({
|
|
163
|
+
repository: slug,
|
|
164
|
+
default_branch: repo.default_branch,
|
|
165
|
+
inspection,
|
|
166
|
+
});
|
|
167
|
+
scan.findings.push(...packageFindings(inspection));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
scan.repositories.push({ repository: slug, error: error.message });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!options.offline) {
|
|
173
|
+
let failures;
|
|
174
|
+
if (options.org) {
|
|
175
|
+
failures = await github.runs.failures({
|
|
176
|
+
org: options.org,
|
|
177
|
+
grep: RELEASE_FAILURE_PATTERNS,
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
failures = [];
|
|
181
|
+
for (const repo of repositories) {
|
|
182
|
+
const [latest] = await github.runs.list(repo.full_name, {
|
|
183
|
+
branch: repo.default_branch,
|
|
184
|
+
limit: 1,
|
|
185
|
+
});
|
|
186
|
+
if (latest?.conclusion === "failure") {
|
|
187
|
+
const matches = await github.runs.logs(latest.id, {
|
|
188
|
+
repo: repo.full_name,
|
|
189
|
+
grep: RELEASE_FAILURE_PATTERNS,
|
|
190
|
+
});
|
|
191
|
+
if (matches.length) {
|
|
192
|
+
failures.push({
|
|
193
|
+
repository: repo.full_name,
|
|
194
|
+
runUrl: latest.html_url,
|
|
195
|
+
matches,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
scan.findings.push(
|
|
202
|
+
...failures.map((evidence) => ({
|
|
203
|
+
repository: evidence.repository,
|
|
204
|
+
type: "release-failing",
|
|
205
|
+
evidence,
|
|
206
|
+
})),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return await callback(scan, github);
|
|
210
|
+
} finally {
|
|
211
|
+
await rm(directory, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function scanRepositories(options) {
|
|
216
|
+
return withAccountScan(options, (scan) => {
|
|
217
|
+
for (const item of scan.repositories) {
|
|
218
|
+
if (item.inspection) {
|
|
219
|
+
item.inspection.repository.root = `https://github.com/${item.repository}`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return scan;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function renderScan(scan) {
|
|
227
|
+
const rows = ["Repository\tRegistry\tPackage\tFinding"];
|
|
228
|
+
for (const item of scan.findings) {
|
|
229
|
+
rows.push(
|
|
230
|
+
`${item.repository}\t${item.registry ?? "-"}\t${item.package ?? "-"}\t${item.type.replaceAll("-", " ")}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
for (const item of scan.repositories.filter((repo) => repo.error)) {
|
|
234
|
+
rows.push(`${item.repository}\t-\t-\tscan failed: ${item.error}`);
|
|
235
|
+
}
|
|
236
|
+
for (const repo of scan.repositories) {
|
|
237
|
+
for (const pkg of repo.inspection?.packages ?? []) {
|
|
238
|
+
if (
|
|
239
|
+
!pkg.publishable ||
|
|
240
|
+
scan.findings.some(
|
|
241
|
+
(finding) =>
|
|
242
|
+
finding.repository === repo.repository &&
|
|
243
|
+
finding.package === pkg.name &&
|
|
244
|
+
finding.registry === pkg.registry,
|
|
245
|
+
)
|
|
246
|
+
) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const status =
|
|
250
|
+
pkg.exists_on_registry === undefined
|
|
251
|
+
? "registry state unknown"
|
|
252
|
+
: pkg.trusted_publishing === true
|
|
253
|
+
? "published with trusted publishing"
|
|
254
|
+
: "published; trusted publishing unknown";
|
|
255
|
+
rows.push(`${repo.repository}\t${pkg.registry}\t${pkg.name}\t${status}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return `${rows.join("\n")}\n`;
|
|
259
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFile, rm, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { withAccountScan, renderScan } from "./organization-scan.mjs";
|
|
5
|
+
import { buildPlans } from "./plan.mjs";
|
|
6
|
+
import { probeEnvironment } from "./prerequisites.mjs";
|
|
7
|
+
import { executePlans } from "./setup-batch.mjs";
|
|
8
|
+
import { secretName } from "./ci-credential-cycle.mjs";
|
|
9
|
+
import { TOKEN_PROVIDERS } from "./credential-browser.mjs";
|
|
10
|
+
|
|
11
|
+
async function prepareRepository(item) {
|
|
12
|
+
const root = item.inspection.repository.root;
|
|
13
|
+
await rm(`${root}/.git`, { recursive: true, force: true });
|
|
14
|
+
const commands = [
|
|
15
|
+
["init", "--quiet"],
|
|
16
|
+
["remote", "add", "origin", `https://github.com/${item.repository}.git`],
|
|
17
|
+
["fetch", "--quiet", "--depth=1", "origin", item.default_branch],
|
|
18
|
+
["checkout", "--quiet", "--force", "--detach", "FETCH_HEAD"],
|
|
19
|
+
];
|
|
20
|
+
for (const args of commands) {
|
|
21
|
+
const result = spawnSync("git", args, { cwd: root, stdio: "pipe" });
|
|
22
|
+
if (result.status !== 0) {
|
|
23
|
+
throw new Error(`cannot prepare ${item.repository} for setup`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Plan from API snapshots; fetch worktrees only for repositories being changed. */
|
|
29
|
+
export async function accountCommand(command, options, outputPlans) {
|
|
30
|
+
return withAccountScan(options, async (scan, github) => {
|
|
31
|
+
if (command === "inspect") {
|
|
32
|
+
for (const item of scan.repositories) {
|
|
33
|
+
if (item.inspection) {
|
|
34
|
+
item.inspection.repository.root = `https://github.com/${item.repository}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
process.stdout.write(
|
|
38
|
+
options.format === "json"
|
|
39
|
+
? `${JSON.stringify(scan, null, 2)}\n`
|
|
40
|
+
: renderScan(scan),
|
|
41
|
+
);
|
|
42
|
+
return scan;
|
|
43
|
+
}
|
|
44
|
+
const items = scan.repositories.filter((item) => item.inspection);
|
|
45
|
+
const errors = scan.repositories.filter((item) => item.error);
|
|
46
|
+
if (errors.length) {
|
|
47
|
+
console.error(renderScan({ repositories: errors, findings: [] }));
|
|
48
|
+
}
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
const workflow = options.planOptions?.workflow;
|
|
51
|
+
if (workflow !== undefined) {
|
|
52
|
+
if (!/^[\w.-]+\.ya?ml$/.test(workflow)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"--workflow must be a workflow file name in .github/workflows",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
!options.planOptions.addPublishJob &&
|
|
59
|
+
!(await stat(
|
|
60
|
+
path.join(
|
|
61
|
+
item.inspection.repository.root,
|
|
62
|
+
".github/workflows",
|
|
63
|
+
workflow,
|
|
64
|
+
),
|
|
65
|
+
).then(
|
|
66
|
+
(metadata) => metadata.isFile(),
|
|
67
|
+
() => false,
|
|
68
|
+
))
|
|
69
|
+
) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`--workflow: ${item.repository} has no .github/workflows/${workflow}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const packages = items.flatMap((item) => item.inspection.packages);
|
|
77
|
+
const environment = await (options.probeEnvironment ?? probeEnvironment)({
|
|
78
|
+
offline: options.offline,
|
|
79
|
+
verbose: options.verbose,
|
|
80
|
+
npm: packages.some((item) => item.registry === "npm"),
|
|
81
|
+
python: packages.some((item) => item.registry === "pypi"),
|
|
82
|
+
});
|
|
83
|
+
let plans = items
|
|
84
|
+
.flatMap((item) => {
|
|
85
|
+
const relevant = scan.findings.filter(
|
|
86
|
+
(finding) => finding.repository === item.repository,
|
|
87
|
+
);
|
|
88
|
+
const inspection = {
|
|
89
|
+
...item.inspection,
|
|
90
|
+
packages: item.inspection.packages.filter(
|
|
91
|
+
(pkg) =>
|
|
92
|
+
(!options.package || pkg.name === options.package) &&
|
|
93
|
+
(command === "plan" ||
|
|
94
|
+
relevant.some(
|
|
95
|
+
(finding) => !finding.package || finding.package === pkg.name,
|
|
96
|
+
)),
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
return buildPlans(inspection, options.registries ?? [], {
|
|
100
|
+
...options.planOptions,
|
|
101
|
+
environment,
|
|
102
|
+
}).filter((plan) => plan.package.publishable);
|
|
103
|
+
})
|
|
104
|
+
.sort(
|
|
105
|
+
(a, b) => Number(b.registry === "npm") - Number(a.registry === "npm"),
|
|
106
|
+
);
|
|
107
|
+
const display = structuredClone(plans);
|
|
108
|
+
for (const plan of display) {
|
|
109
|
+
plan.repository.root = `https://github.com/${plan.repository.github_owner}/${plan.repository.github_repository}`;
|
|
110
|
+
}
|
|
111
|
+
outputPlans(display, options.format);
|
|
112
|
+
if (options.package && !plans.length) {
|
|
113
|
+
throw new Error(`package '${options.package}' was not found`);
|
|
114
|
+
}
|
|
115
|
+
if (command === "plan" || plans.length === 0) {
|
|
116
|
+
if (errors.length) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"some repositories could not be scanned; see scan errors",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (!plans.length && options.format !== "json") {
|
|
122
|
+
console.log("No package findings require setup.");
|
|
123
|
+
}
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
if (options.execute) {
|
|
127
|
+
for (const item of items.filter((item) =>
|
|
128
|
+
plans.some(
|
|
129
|
+
(plan) => plan.repository.root === item.inspection.repository.root,
|
|
130
|
+
),
|
|
131
|
+
)) {
|
|
132
|
+
try {
|
|
133
|
+
await (options.prepareRepository ?? prepareRepository)(item);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
item.error = error.message;
|
|
136
|
+
console.error(`${item.repository}: ${item.error}`);
|
|
137
|
+
plans = plans.filter(
|
|
138
|
+
(plan) => plan.repository.root !== item.inspection.repository.root,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!plans.length) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
"some repositories could not be prepared; see scan errors",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const secretRepositories = {};
|
|
149
|
+
for (const plan of plans) {
|
|
150
|
+
const provider = TOKEN_PROVIDERS[plan.registry];
|
|
151
|
+
if (provider) {
|
|
152
|
+
const config = JSON.parse(
|
|
153
|
+
await readFile(
|
|
154
|
+
path.join(plan.repository.root, ".package-registry-manager.json"),
|
|
155
|
+
"utf8",
|
|
156
|
+
).catch((error) => {
|
|
157
|
+
if (error.code === "ENOENT") {
|
|
158
|
+
return "{}";
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
if (config.tokens?.[plan.registry]?.level === "repo") {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const slug = `${plan.repository.github_owner}/${plan.repository.github_repository}`;
|
|
167
|
+
const name = secretName(
|
|
168
|
+
options.secretName ??
|
|
169
|
+
config.tokens?.[plan.registry]?.secret ??
|
|
170
|
+
plan.package.token_secrets?.[0] ??
|
|
171
|
+
provider.secret,
|
|
172
|
+
plan.registry,
|
|
173
|
+
slug,
|
|
174
|
+
);
|
|
175
|
+
const targets = (secretRepositories[`${plan.registry}:${name}`] ??= []);
|
|
176
|
+
if (!targets.includes(slug)) {
|
|
177
|
+
targets.push(slug);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const outcomes = await (options.executePlans ?? executePlans)(
|
|
182
|
+
plans,
|
|
183
|
+
{
|
|
184
|
+
...options,
|
|
185
|
+
github,
|
|
186
|
+
accountScan: true,
|
|
187
|
+
browser: options.openWith ? options.browser : "automated",
|
|
188
|
+
quietBrowser: true,
|
|
189
|
+
secretRepositories,
|
|
190
|
+
repository: items[0]?.inspection.repository.root,
|
|
191
|
+
workflow: options.planOptions?.workflow,
|
|
192
|
+
publisherEnvironment: options.planOptions?.publisherEnvironment,
|
|
193
|
+
},
|
|
194
|
+
true,
|
|
195
|
+
);
|
|
196
|
+
if (scan.repositories.some((item) => item.error)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
"some repositories could not be scanned; see scan errors",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return outcomes;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Registry knowledge belongs to this consumer, never to gh-manager. */
|
|
2
|
+
export const AUTH_FAILURE_PATTERNS = Object.freeze({
|
|
3
|
+
npm: [
|
|
4
|
+
"\\bENEEDAUTH\\b",
|
|
5
|
+
"\\bE401\\b",
|
|
6
|
+
"\\bE403\\b",
|
|
7
|
+
"invalid-publisher",
|
|
8
|
+
"E404.*PUT|PUT.*E404",
|
|
9
|
+
"Access token expired or revoked",
|
|
10
|
+
],
|
|
11
|
+
"crates-io": [
|
|
12
|
+
"No Trusted Publishing config found",
|
|
13
|
+
"invalid-publisher",
|
|
14
|
+
"token.*(?:invalid|expired|revoked)",
|
|
15
|
+
"authentication failed",
|
|
16
|
+
],
|
|
17
|
+
pypi: [
|
|
18
|
+
"invalid-publisher",
|
|
19
|
+
"invalid.*(?:token|authentication)",
|
|
20
|
+
"403.*Forbidden",
|
|
21
|
+
],
|
|
22
|
+
rubygems: ["API key.*(?:invalid|expired)", "invalid-publisher"],
|
|
23
|
+
nuget: ["API key.*(?:invalid|expired)", "invalid-publisher"],
|
|
24
|
+
jsr: ["invalid-publisher", "publishing.*(?:unauthorized|permission)"],
|
|
25
|
+
"docker-hub": [
|
|
26
|
+
"unauthorized: authentication required",
|
|
27
|
+
"denied: requested access",
|
|
28
|
+
"incorrect username or password",
|
|
29
|
+
],
|
|
30
|
+
"maven-central": [
|
|
31
|
+
"401.*Unauthorized",
|
|
32
|
+
"403.*Forbidden",
|
|
33
|
+
"Invalid user token",
|
|
34
|
+
],
|
|
35
|
+
"vscode-marketplace": ["Invalid.*(?:PAT|token)", "TF400813", "VSCE_PAT"],
|
|
36
|
+
"open-vsx": ["Invalid access token", "token.*(?:expired|invalid)"],
|
|
37
|
+
"chrome-web-store": [
|
|
38
|
+
"invalid_grant",
|
|
39
|
+
"invalid_client",
|
|
40
|
+
"invalid.*refresh.token",
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const RELEASE_FAILURE_PATTERNS = Object.freeze([
|
|
45
|
+
...new Set(Object.values(AUTH_FAILURE_PATTERNS).flat()),
|
|
46
|
+
]);
|
package/src/registry-state.mjs
CHANGED
|
@@ -23,6 +23,15 @@ const ENDPOINTS = {
|
|
|
23
23
|
"PACKAGE_REGISTRY_MANAGER_DOCKER_HUB_API",
|
|
24
24
|
"https://hub.docker.com/v2",
|
|
25
25
|
],
|
|
26
|
+
nuget: [
|
|
27
|
+
"PACKAGE_REGISTRY_MANAGER_NUGET_API",
|
|
28
|
+
"https://api.nuget.org/v3-flatcontainer",
|
|
29
|
+
],
|
|
30
|
+
rubygems: [
|
|
31
|
+
"PACKAGE_REGISTRY_MANAGER_RUBYGEMS_API",
|
|
32
|
+
"https://rubygems.org/api/v1",
|
|
33
|
+
],
|
|
34
|
+
jsr: ["PACKAGE_REGISTRY_MANAGER_JSR_API", "https://jsr.io"],
|
|
26
35
|
};
|
|
27
36
|
|
|
28
37
|
/** Returns the registry API base URL, honoring test overrides. */
|
|
@@ -54,6 +63,19 @@ export function registryStateUrl(packageInfo, env = process.env) {
|
|
|
54
63
|
return `${base}/crates/${encodeURIComponent(name)}`;
|
|
55
64
|
case "pypi":
|
|
56
65
|
return `${base}/pypi/${encodeURIComponent(name)}/json`;
|
|
66
|
+
case "nuget":
|
|
67
|
+
return `${base}/${encodeURIComponent(name.toLowerCase())}/index.json`;
|
|
68
|
+
case "rubygems":
|
|
69
|
+
return `${base}/gems/${encodeURIComponent(name)}.json`;
|
|
70
|
+
case "jsr":
|
|
71
|
+
return `${base}/${name
|
|
72
|
+
.split("/")
|
|
73
|
+
.map((part) =>
|
|
74
|
+
part.startsWith("@")
|
|
75
|
+
? `@${encodeURIComponent(part.slice(1))}`
|
|
76
|
+
: encodeURIComponent(part),
|
|
77
|
+
)
|
|
78
|
+
.join("/")}/meta.json`;
|
|
57
79
|
default: {
|
|
58
80
|
const [namespace, repository] = name.split("/");
|
|
59
81
|
return `${base}/namespaces/${encodeURIComponent(namespace)}/repositories/${encodeURIComponent(repository)}`;
|
package/src/setup-batch.mjs
CHANGED
|
@@ -37,8 +37,27 @@ export async function executePlans(plans, options, summary = true) {
|
|
|
37
37
|
if (plans.length === 0) {
|
|
38
38
|
throw new Error("no publishable package manifests were found");
|
|
39
39
|
}
|
|
40
|
+
const ready = [];
|
|
41
|
+
let blocked;
|
|
40
42
|
for (const plan of plans) {
|
|
41
|
-
|
|
43
|
+
try {
|
|
44
|
+
validatePlan(plan, options.execute);
|
|
45
|
+
ready.push(plan);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!options.accountScan) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
blocked ??= error;
|
|
51
|
+
console.error(
|
|
52
|
+
`${plan.repository.github_owner}/${plan.repository.github_repository}: ${plan.registry}: ${plan.package.name}: blocked: ${error.message}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (blocked) {
|
|
57
|
+
if (ready.length) {
|
|
58
|
+
await executePlans(ready, options, summary);
|
|
59
|
+
}
|
|
60
|
+
throw blocked;
|
|
42
61
|
}
|
|
43
62
|
for (const plan of plans) {
|
|
44
63
|
if (plan.mode === "complete") {
|
|
@@ -68,6 +87,32 @@ export async function executePlans(plans, options, summary = true) {
|
|
|
68
87
|
plan.steps.some((step) => step.id === "add-publishing-workflow"),
|
|
69
88
|
);
|
|
70
89
|
if (missing.length) {
|
|
90
|
+
if (options.accountScan) {
|
|
91
|
+
const outcomes = [];
|
|
92
|
+
for (const root of new Set(missing.map((plan) => plan.repository.root))) {
|
|
93
|
+
const group = missing.filter((plan) => plan.repository.root === root);
|
|
94
|
+
const result = await (options.offerWorkflow ?? offerWorkflow)(group, {
|
|
95
|
+
...options,
|
|
96
|
+
repository: root,
|
|
97
|
+
inspection: undefined,
|
|
98
|
+
prompt: options.prompt ?? prompt,
|
|
99
|
+
});
|
|
100
|
+
outcomes.push(
|
|
101
|
+
...group.map((plan) => ({
|
|
102
|
+
registry: plan.registry,
|
|
103
|
+
package: plan.package.name,
|
|
104
|
+
repository: `${plan.repository.github_owner}/${plan.repository.github_repository}`,
|
|
105
|
+
...result,
|
|
106
|
+
})),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const ready = plans.filter((plan) => !missing.includes(plan));
|
|
110
|
+
if (ready.length) {
|
|
111
|
+
outcomes.push(...(await executePlans(ready, options, false)));
|
|
112
|
+
}
|
|
113
|
+
printSummary(outcomes, summary);
|
|
114
|
+
return outcomes;
|
|
115
|
+
}
|
|
71
116
|
const result = await offerWorkflow(missing, {
|
|
72
117
|
...options,
|
|
73
118
|
prompt: options.prompt ?? prompt,
|
|
@@ -99,6 +144,9 @@ export async function executePlans(plans, options, summary = true) {
|
|
|
99
144
|
for (const plan of plans) {
|
|
100
145
|
const session = new SetupSession(plan, {
|
|
101
146
|
...options,
|
|
147
|
+
repository: options.accountScan
|
|
148
|
+
? plan.repository.root
|
|
149
|
+
: options.repository,
|
|
102
150
|
automation,
|
|
103
151
|
domains,
|
|
104
152
|
});
|
|
@@ -107,6 +155,7 @@ export async function executePlans(plans, options, summary = true) {
|
|
|
107
155
|
outcomes.push({
|
|
108
156
|
registry: plan.registry,
|
|
109
157
|
package: plan.package.name,
|
|
158
|
+
repository: `${plan.repository.github_owner}/${plan.repository.github_repository}`,
|
|
110
159
|
status: plan.steps.length === 0 ? "complete" : "configured",
|
|
111
160
|
...session.outcome,
|
|
112
161
|
});
|
|
@@ -119,7 +168,7 @@ export async function executePlans(plans, options, summary = true) {
|
|
|
119
168
|
package: plan.package.name,
|
|
120
169
|
status: "failed",
|
|
121
170
|
});
|
|
122
|
-
failure
|
|
171
|
+
failure ??= error;
|
|
123
172
|
} finally {
|
|
124
173
|
const steps = await session.cleanup({
|
|
125
174
|
deferAuth: plans.length > 1,
|
|
@@ -130,7 +179,7 @@ export async function executePlans(plans, options, summary = true) {
|
|
|
130
179
|
deferred.set(`${plan.registry}:${step.id}`, { session, step });
|
|
131
180
|
}
|
|
132
181
|
}
|
|
133
|
-
if (failure) {
|
|
182
|
+
if (failure && !options.accountScan) {
|
|
134
183
|
break;
|
|
135
184
|
}
|
|
136
185
|
}
|
|
@@ -168,7 +217,7 @@ function printSummary(outcomes, enabled) {
|
|
|
168
217
|
console.log("\nSetup summary:");
|
|
169
218
|
for (const item of outcomes) {
|
|
170
219
|
console.log(
|
|
171
|
-
`- ${item.registry}: ${item.package}: ${item.status}${item.url ? ` (${item.url})` : ""}`,
|
|
220
|
+
`- ${item.repository ? `${item.repository}: ` : ""}${item.registry}: ${item.package}: ${item.status}${item.secret_scope ? `; ${item.secret_scope} secret` : ""}${item.fallback_reason ? `; ${item.fallback_reason}` : ""}${item.url ? ` (${item.url})` : ""}`,
|
|
172
221
|
);
|
|
173
222
|
}
|
|
174
223
|
}
|
package/src/setup.mjs
CHANGED
|
@@ -681,6 +681,7 @@ export class SetupSession {
|
|
|
681
681
|
* `--browser-import default|auto` itself, once it knows a sign-in is missing.
|
|
682
682
|
*/
|
|
683
683
|
async automatedPage(browser = this.options.browserOptions, quiet = false) {
|
|
684
|
+
quiet ||= this.options.quietBrowser;
|
|
684
685
|
if (!this.automation) {
|
|
685
686
|
const domains = this.options.domains ?? signInDomains(this.plan.registry);
|
|
686
687
|
this.automation = await connectAutomation({
|