package-registry-manager 0.25.1 → 0.27.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.
- package/CHANGELOG.md +10 -0
- package/package.json +2 -2
- package/src/central-packagist-steps.mjs +538 -0
- package/src/cpp-distribution.mjs +42 -5
- package/src/credential-cycle.mjs +7 -0
- package/src/credential-requirements.mjs +105 -0
- package/src/discovery.mjs +158 -47
- package/src/extra-flows.mjs +103 -13
- package/src/gradle-distribution.mjs +410 -0
- package/src/model.mjs +4 -0
- package/src/nuget-ci.mjs +32 -1
- package/src/nuget-migration.mjs +178 -1
- package/src/organization-scan.mjs +34 -2
- package/src/package-coverage.mjs +33 -9
- package/src/plan.mjs +42 -7
- package/src/publication-state.mjs +34 -0
- package/src/publishers.mjs +81 -11
- package/src/publishing-state.mjs +60 -1
- package/src/scan-sidecars.mjs +132 -0
- package/src/setup.mjs +15 -1
- package/src/source-code.mjs +1 -1
- package/src/swift-distribution.mjs +249 -0
- package/src/swift-release.mjs +180 -0
package/CHANGELOG.md
CHANGED
|
@@ -27,6 +27,13 @@ currently shares its release version with the Rust implementation.
|
|
|
27
27
|
- An npm first publish stops before the sign-in when the packed tarball
|
|
28
28
|
contains repository development directories such as `.github/` or `tests/`,
|
|
29
29
|
which a package.json without a `files` allowlist lets in.
|
|
30
|
+
- SwiftPM packages (`--registry swift`) are discovered from `Package.swift`
|
|
31
|
+
and `swift/VERSION` and verified by exact bare tag, commit, tagged manifest,
|
|
32
|
+
public release and consumer (#77). Kotlin Multiplatform Gradle publications
|
|
33
|
+
are discovered with their platform variants and verified by root POM,
|
|
34
|
+
`.module` metadata and every variant (#78).
|
|
35
|
+
- Generated Central, Packagist and SwiftPM setup steps run their own handlers;
|
|
36
|
+
manual prerequisites stay incomplete without read-back evidence (#79).
|
|
30
37
|
|
|
31
38
|
### Changed
|
|
32
39
|
|
|
@@ -62,6 +69,9 @@ currently shares its release version with the Rust implementation.
|
|
|
62
69
|
packages; they appear with `"steps": []` and a `skipped_reason`, and `setup`
|
|
63
70
|
picks the only publishable package when a registry also has private ones
|
|
64
71
|
(#4).
|
|
72
|
+
- Central and Packagist steps no longer reach the crates.io session (#79).
|
|
73
|
+
Account scans fetch referenced version sidecars at the scanned commit, so
|
|
74
|
+
they agree with local inspection (#80).
|
|
65
75
|
|
|
66
76
|
## [0.19.35] - 2026-09-21
|
|
67
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "package-registry-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Inspect repositories and guide secure package-registry setup",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/0b3ceb1590be7eb0340ae088730698b5ae9bebfc",
|
|
44
|
-
"browser-commander": "^0.34.
|
|
44
|
+
"browser-commander": "^0.34.1",
|
|
45
45
|
"command-stream": "^2.6.0",
|
|
46
46
|
"playwright": "^1.63.0",
|
|
47
47
|
"selenium-webdriver": "^4.50.0"
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { githubServices } from "./github.mjs";
|
|
2
|
+
import { AUTH_FAILURE_PATTERNS } from "./publishing-policy.mjs";
|
|
3
|
+
import { repositorySlug } from "./repository-identity.mjs";
|
|
4
|
+
import { verifySwiftConsumer, verifySwiftRelease } from "./swift-release.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generated Central, Packagist and SwiftPM steps and their handlers. None of
|
|
8
|
+
* them is ever routed to the crates.io session.
|
|
9
|
+
*/
|
|
10
|
+
export const CENTRAL_PACKAGIST_STEPS = new Set([
|
|
11
|
+
"ensure-central-credential-pair",
|
|
12
|
+
"verify-namespace",
|
|
13
|
+
"ensure-signing-key",
|
|
14
|
+
"verify-central-publication",
|
|
15
|
+
"verify-packagist-package",
|
|
16
|
+
"packagist-github-hook",
|
|
17
|
+
"packagist-update-api",
|
|
18
|
+
"verify-swift-release",
|
|
19
|
+
"verify-swift-consumer",
|
|
20
|
+
"verify-gradle-toolchain",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const MAVEN_REPOSITORY = "https://repo1.maven.org/maven2";
|
|
24
|
+
const MISSING_SECRET =
|
|
25
|
+
/secrets? (?:are |is )?missing|missing (?:secrets?|credentials?)|(?:CENTRAL_USERNAME|CENTRAL_TOKEN|GPG_PRIVATE_KEY|GPG_PASSPHRASE)[^\n]*(?:not set|missing|empty)/i;
|
|
26
|
+
|
|
27
|
+
/** Runs one generated Central/Packagist/SwiftPM step. */
|
|
28
|
+
export async function runCentralPackagistStep(session, step) {
|
|
29
|
+
const handlers = {
|
|
30
|
+
"ensure-central-credential-pair": ensureCentralPair,
|
|
31
|
+
"verify-namespace": verifyNamespace,
|
|
32
|
+
"ensure-signing-key": ensureSigningKey,
|
|
33
|
+
"verify-central-publication": verifyCentralPublication,
|
|
34
|
+
"verify-packagist-package": verifyPackagistPackage,
|
|
35
|
+
"packagist-github-hook": packagistHook,
|
|
36
|
+
"packagist-update-api": packagistUpdateApi,
|
|
37
|
+
"verify-swift-release": verifySwiftRelease,
|
|
38
|
+
"verify-swift-consumer": verifySwiftConsumer,
|
|
39
|
+
"verify-gradle-toolchain": manualPrerequisite,
|
|
40
|
+
};
|
|
41
|
+
const result = await handlers[step.id](session, step);
|
|
42
|
+
record(session, step, result);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Collects the step result. Anything short of fresh evidence keeps the
|
|
48
|
+
* setup incomplete; `stop` ends the plan before later browsers or commands.
|
|
49
|
+
*/
|
|
50
|
+
function record(session, step, result) {
|
|
51
|
+
session.stepResults ??= [];
|
|
52
|
+
session.stepResults.push({ step: step.id, ...result });
|
|
53
|
+
if (
|
|
54
|
+
!["verified", "present", "configured", "not-used"].includes(result.status)
|
|
55
|
+
) {
|
|
56
|
+
session.incomplete ??= [];
|
|
57
|
+
session.incomplete.push({ step: step.id, ...result });
|
|
58
|
+
}
|
|
59
|
+
if (result.stop) {
|
|
60
|
+
session.outcome = {
|
|
61
|
+
status: result.status,
|
|
62
|
+
incomplete: session.incomplete ?? [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
session.log(
|
|
66
|
+
`${step.id}: ${result.status}${result.reason ? ` (${result.reason})` : ""}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function services(session) {
|
|
71
|
+
return session.options.github ?? githubServices(session.options);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Selected secret presence at repository, then organization scope. */
|
|
75
|
+
async function secretPresence(session, names) {
|
|
76
|
+
const slug = repositorySlug(session.plan.repository);
|
|
77
|
+
const github = services(session);
|
|
78
|
+
const owner = slug.split("/")[0];
|
|
79
|
+
const present = [];
|
|
80
|
+
const missing = [];
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
let metadata = await github.secrets({ repo: slug }).getMetadata(name);
|
|
83
|
+
if (!metadata) {
|
|
84
|
+
try {
|
|
85
|
+
metadata = await github.secrets({ org: owner }).getMetadata(name);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (![403, 404, 422].includes(error.status)) {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
(metadata ? present : missing).push(name);
|
|
93
|
+
}
|
|
94
|
+
return { slug, present, missing };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Keeps an existing Central user-token pair; otherwise acquires one new
|
|
99
|
+
* pair in the dedicated browser, holds both fields in memory and stores
|
|
100
|
+
* them as encrypted GitHub secrets through gh-manager.
|
|
101
|
+
*/
|
|
102
|
+
async function ensureCentralPair(session, step) {
|
|
103
|
+
const names = (step.secrets ?? []).map((entry) => entry.secret);
|
|
104
|
+
if (names.length !== 2 || !session.plan.repository?.github_owner) {
|
|
105
|
+
return {
|
|
106
|
+
status: "unsupported",
|
|
107
|
+
reason:
|
|
108
|
+
"the Central pair needs GitHub repository coordinates and exactly two selected secret names",
|
|
109
|
+
stop: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const { slug, present, missing } = await secretPresence(session, names);
|
|
113
|
+
if (!missing.length) {
|
|
114
|
+
return {
|
|
115
|
+
status: "present",
|
|
116
|
+
secrets: present,
|
|
117
|
+
reason: "existing Central secrets kept; nothing rotated",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const { options } = session;
|
|
121
|
+
if (
|
|
122
|
+
options.noBrowser ||
|
|
123
|
+
options.browserOptions?.attach ||
|
|
124
|
+
options.browserOptions?.importScope === "full"
|
|
125
|
+
) {
|
|
126
|
+
return {
|
|
127
|
+
status: "manual-prerequisite",
|
|
128
|
+
secrets: missing,
|
|
129
|
+
reason: `generate a Central Portal user token at ${step.url} and store ${missing.join(" + ")} as encrypted secrets; automation needs a dedicated profile with domain-scoped sign-in import`,
|
|
130
|
+
stop: true,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const replaces = present.length
|
|
134
|
+
? ` A Central token is a matched pair, so ${present.join(" + ")} is replaced too.`
|
|
135
|
+
: "";
|
|
136
|
+
if (
|
|
137
|
+
!options.yes &&
|
|
138
|
+
!/^y(?:es)?$/i.test(
|
|
139
|
+
await session.prompt(
|
|
140
|
+
`Generate one Central Portal user token and store ${names.join(" + ")} for ${slug}?${replaces} [y/N] `,
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
) {
|
|
144
|
+
return {
|
|
145
|
+
status: "interrupted",
|
|
146
|
+
secrets: missing,
|
|
147
|
+
reason: "Central user-token creation declined",
|
|
148
|
+
stop: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const page = await session.automatedPage(
|
|
152
|
+
{
|
|
153
|
+
...options.browserOptions,
|
|
154
|
+
import: options.browserOptions?.import ?? {
|
|
155
|
+
browser: "auto",
|
|
156
|
+
profile: null,
|
|
157
|
+
},
|
|
158
|
+
importScope: "domains",
|
|
159
|
+
},
|
|
160
|
+
true,
|
|
161
|
+
);
|
|
162
|
+
await page.goto(step.url);
|
|
163
|
+
await session.prompt(
|
|
164
|
+
"Generate the user token in this browser and leave its username and password dialog open, then press Enter (never paste the values)...",
|
|
165
|
+
);
|
|
166
|
+
const pair = await page.evaluate(READ_CENTRAL_PAIR);
|
|
167
|
+
if (!pair?.username || !pair?.password) {
|
|
168
|
+
return {
|
|
169
|
+
status: "manual-prerequisite",
|
|
170
|
+
secrets: missing,
|
|
171
|
+
reason:
|
|
172
|
+
"the Central token dialog was not readable; nothing was stored, so the pair is still missing",
|
|
173
|
+
stop: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const github = services(session);
|
|
177
|
+
const values = [pair.username, pair.password];
|
|
178
|
+
// The pair is matched: both fields are written together after the
|
|
179
|
+
// explicit confirmation above, never one silently.
|
|
180
|
+
for (const [index, name] of names.entries()) {
|
|
181
|
+
await github.secrets({ repo: slug }).set(name, values[index]);
|
|
182
|
+
}
|
|
183
|
+
const check = await secretPresence(session, names);
|
|
184
|
+
if (check.missing.length) {
|
|
185
|
+
return {
|
|
186
|
+
status: "unverified",
|
|
187
|
+
secrets: check.missing,
|
|
188
|
+
reason: "gh-manager did not read the stored secrets back",
|
|
189
|
+
stop: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
session.conditions.add("central-pair-stored");
|
|
193
|
+
return {
|
|
194
|
+
status: "configured",
|
|
195
|
+
secrets: names,
|
|
196
|
+
retry: await retryCausalJobs(session, slug, names),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Settings-only repair: restart only the failed main jobs whose logs show
|
|
202
|
+
* the missing Central secrets, then follow them through gh-manager.
|
|
203
|
+
*/
|
|
204
|
+
async function retryCausalJobs(session, slug, names) {
|
|
205
|
+
const workflow = session.plan.package.workflow;
|
|
206
|
+
if (!workflow || session.options.noRetry) {
|
|
207
|
+
return { status: "not-run", reason: "no publishing workflow to retry" };
|
|
208
|
+
}
|
|
209
|
+
const github = services(session);
|
|
210
|
+
const repo = await github.rest.request(`/repos/${slug}`);
|
|
211
|
+
const runs = await github.runs.list(slug, {
|
|
212
|
+
workflow,
|
|
213
|
+
branch: repo.default_branch,
|
|
214
|
+
limit: 20,
|
|
215
|
+
});
|
|
216
|
+
const failed = runs
|
|
217
|
+
.filter((run) => run.conclusion === "failure")
|
|
218
|
+
.sort((a, b) => b.id - a.id)[0];
|
|
219
|
+
if (!failed) {
|
|
220
|
+
return { status: "not-needed", reason: "no failed main run" };
|
|
221
|
+
}
|
|
222
|
+
const auth = new RegExp(
|
|
223
|
+
[MISSING_SECRET.source, ...(AUTH_FAILURE_PATTERNS["maven-central"] ?? [])]
|
|
224
|
+
.filter(Boolean)
|
|
225
|
+
.join("|"),
|
|
226
|
+
"i",
|
|
227
|
+
);
|
|
228
|
+
const causal = [];
|
|
229
|
+
for (const job of await github.runs.jobs(slug, failed.id)) {
|
|
230
|
+
if (job.conclusion !== "failure") {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const log = await github.runs.jobLog(slug, job.id).catch(() => null);
|
|
234
|
+
if (
|
|
235
|
+
typeof log === "string" &&
|
|
236
|
+
auth.test(log) &&
|
|
237
|
+
names.some((name) => log.includes(name))
|
|
238
|
+
) {
|
|
239
|
+
causal.push(job.id);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!causal.length) {
|
|
243
|
+
return {
|
|
244
|
+
status: "unknown",
|
|
245
|
+
reason:
|
|
246
|
+
"no failed job log names the stored Central secrets; unrelated CI was not restarted",
|
|
247
|
+
run: failed.html_url,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const result = await github.repair.verify({
|
|
251
|
+
repo: slug,
|
|
252
|
+
workflow,
|
|
253
|
+
runId: failed.id,
|
|
254
|
+
jobIds: causal,
|
|
255
|
+
changes: [{ kind: "setting", changed: true, verified: true }],
|
|
256
|
+
timeout: session.options.waitTimeoutMs ?? 600000,
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
status: result.status,
|
|
260
|
+
run: result.runUrl ?? failed.html_url,
|
|
261
|
+
jobs: causal,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Reads both displayed fields of the Central token dialog; never logged. */
|
|
266
|
+
export const READ_CENTRAL_PAIR = `(() => {
|
|
267
|
+
const root = document.querySelector('[role="dialog"]') ?? document.body;
|
|
268
|
+
const text = (element) => (element?.value ?? element?.textContent ?? "").trim();
|
|
269
|
+
const field = (label) => {
|
|
270
|
+
for (const node of root.querySelectorAll("label, dt, th, span, p, div")) {
|
|
271
|
+
if (node.children.length === 0 && new RegExp("^" + label + ":?$", "i").test(text(node))) {
|
|
272
|
+
const target = node.htmlFor ? root.querySelector("#" + CSS.escape(node.htmlFor)) : null;
|
|
273
|
+
const next = target ?? node.nextElementSibling ?? node.parentElement?.nextElementSibling;
|
|
274
|
+
const value = text(next?.querySelector?.("input, code, pre") ?? next);
|
|
275
|
+
if (value && !/\\s/.test(value)) return value;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
};
|
|
280
|
+
return { username: field("username"), password: field("password") };
|
|
281
|
+
})()`;
|
|
282
|
+
|
|
283
|
+
async function http(session, url, method = "GET") {
|
|
284
|
+
const fetcher = session.options.fetch ?? globalThis.fetch;
|
|
285
|
+
try {
|
|
286
|
+
const response = await fetcher(url, {
|
|
287
|
+
method,
|
|
288
|
+
headers: { accept: "application/json" },
|
|
289
|
+
signal: AbortSignal.timeout(session.options.timeoutMs ?? 10_000),
|
|
290
|
+
});
|
|
291
|
+
if (session.options.verbose) {
|
|
292
|
+
console.error(`${method} ${url} -> ${response.status}`);
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
status: response.status,
|
|
296
|
+
json:
|
|
297
|
+
method === "GET" && response.ok
|
|
298
|
+
? await response.json().catch(() => null)
|
|
299
|
+
: null,
|
|
300
|
+
};
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return { status: "transport", reason: error.message };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Published artifacts under the namespace prove it was verified; nothing else does. */
|
|
307
|
+
async function verifyNamespace(session, step) {
|
|
308
|
+
const namespace = step.namespace;
|
|
309
|
+
if (!namespace) {
|
|
310
|
+
return {
|
|
311
|
+
status: "manual-prerequisite",
|
|
312
|
+
reason: "the package coordinates name no namespace",
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const response = await http(
|
|
316
|
+
session,
|
|
317
|
+
`https://search.maven.org/solrsearch/select?q=${encodeURIComponent(`g:"${namespace}"`)}&rows=1&wt=json`,
|
|
318
|
+
);
|
|
319
|
+
if (response.json?.response?.numFound > 0) {
|
|
320
|
+
return {
|
|
321
|
+
status: "verified",
|
|
322
|
+
namespace,
|
|
323
|
+
evidence: "artifacts are published under the namespace",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
status: "manual-prerequisite",
|
|
328
|
+
namespace,
|
|
329
|
+
reason: `no fresh read-back proves ${namespace} is verified; confirm it at ${step.url}`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function mavenPath(group, artifact, version, extension) {
|
|
334
|
+
return `${MAVEN_REPOSITORY}/${group.replaceAll(".", "/")}/${artifact}/${version}/${artifact}-${version}.${extension}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Signing works only when a published signature exists; presence is not proof. */
|
|
338
|
+
async function ensureSigningKey(session, step) {
|
|
339
|
+
const names = (step.secrets ?? []).map((entry) => entry.secret);
|
|
340
|
+
const { present, missing } = await secretPresence(session, names);
|
|
341
|
+
const [group, artifact] = (session.plan.package.name ?? "").split(":");
|
|
342
|
+
const version = session.plan.package.version;
|
|
343
|
+
if (group && artifact && version) {
|
|
344
|
+
const signature = await http(
|
|
345
|
+
session,
|
|
346
|
+
mavenPath(group, artifact, version, "pom.asc"),
|
|
347
|
+
"HEAD",
|
|
348
|
+
);
|
|
349
|
+
if (signature.status === 200) {
|
|
350
|
+
return {
|
|
351
|
+
status: "verified",
|
|
352
|
+
secrets: present,
|
|
353
|
+
evidence: "the exact release has a published signature",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
status: "manual-prerequisite",
|
|
359
|
+
secrets: present,
|
|
360
|
+
missing,
|
|
361
|
+
reason: missing.length
|
|
362
|
+
? `${missing.join(" + ")} missing; provide an armored key whose public key is on a keyserver`
|
|
363
|
+
: "signing secrets are present, but no signed release proves the key is valid and published",
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Root POM, Gradle module metadata and every referenced platform coordinate. */
|
|
368
|
+
async function verifyCentralPublication(session, step) {
|
|
369
|
+
const { group, artifact, version } = step.coordinates ?? {};
|
|
370
|
+
if (!group || !artifact || !version) {
|
|
371
|
+
return {
|
|
372
|
+
status: "unknown",
|
|
373
|
+
reason: "the exact coordinates are unresolved",
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const root = await http(
|
|
377
|
+
session,
|
|
378
|
+
mavenPath(group, artifact, version, "pom"),
|
|
379
|
+
"HEAD",
|
|
380
|
+
);
|
|
381
|
+
if (root.status !== 200) {
|
|
382
|
+
return {
|
|
383
|
+
status: root.status === 404 ? "unpublished" : "transport-unknown",
|
|
384
|
+
reason: `root POM HTTP ${root.status}`,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const missing = [];
|
|
388
|
+
const multiplatform = session.plan.package.gradle?.multiplatform;
|
|
389
|
+
let referenced = [];
|
|
390
|
+
if (multiplatform) {
|
|
391
|
+
const module = await http(
|
|
392
|
+
session,
|
|
393
|
+
mavenPath(group, artifact, version, "module"),
|
|
394
|
+
);
|
|
395
|
+
if (!module.json) {
|
|
396
|
+
return {
|
|
397
|
+
status: "partial",
|
|
398
|
+
reason: `Gradle module metadata missing (HTTP ${module.status}); the root POM alone certifies no variant`,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
referenced = (module.json.variants ?? [])
|
|
402
|
+
.map((variant) => variant["available-at"]?.module)
|
|
403
|
+
.filter(Boolean)
|
|
404
|
+
.map((entry) => `${entry.group}:${entry.module}`);
|
|
405
|
+
}
|
|
406
|
+
for (const coordinates of new Set([
|
|
407
|
+
...(step.variants ?? []),
|
|
408
|
+
...referenced,
|
|
409
|
+
])) {
|
|
410
|
+
const [variantGroup, variantArtifact] = coordinates.split(":");
|
|
411
|
+
if (multiplatform && !referenced.includes(coordinates)) {
|
|
412
|
+
missing.push(`${coordinates} (not in module metadata)`);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const pom = await http(
|
|
416
|
+
session,
|
|
417
|
+
mavenPath(variantGroup, variantArtifact, version, "pom"),
|
|
418
|
+
"HEAD",
|
|
419
|
+
);
|
|
420
|
+
if (pom.status !== 200) {
|
|
421
|
+
missing.push(`${coordinates} (HTTP ${pom.status})`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return missing.length
|
|
425
|
+
? {
|
|
426
|
+
status: "partial",
|
|
427
|
+
missing,
|
|
428
|
+
reason: "platform publications are missing",
|
|
429
|
+
}
|
|
430
|
+
: { status: "verified", variants: referenced };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const STABLE = (version) =>
|
|
434
|
+
!/^dev-|-dev$|[-.](?:alpha|beta|rc|dev)/i.test(version);
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Exact stable tag read-back from Packagist's p2 metadata. Package
|
|
438
|
+
* existence or a dev branch never passes.
|
|
439
|
+
*/
|
|
440
|
+
async function verifyPackagistPackage(session) {
|
|
441
|
+
const item = session.plan.package;
|
|
442
|
+
if (item.manifest !== "composer.json") {
|
|
443
|
+
return {
|
|
444
|
+
status: "unsupported",
|
|
445
|
+
reason: `${item.manifest} is not the canonical root Composer descriptor Packagist reads`,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const tagged = await http(
|
|
449
|
+
session,
|
|
450
|
+
`https://repo.packagist.org/p2/${item.name}.json`,
|
|
451
|
+
);
|
|
452
|
+
if (tagged.status === 404) {
|
|
453
|
+
return {
|
|
454
|
+
status: "unregistered",
|
|
455
|
+
reason: `${item.name} is not on Packagist`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if ([401, 403].includes(tagged.status)) {
|
|
459
|
+
return { status: "auth-rejected", reason: `HTTP ${tagged.status}` };
|
|
460
|
+
}
|
|
461
|
+
if (tagged.status !== 200 || !tagged.json) {
|
|
462
|
+
return {
|
|
463
|
+
status: "transport-unknown",
|
|
464
|
+
reason: tagged.reason ?? `HTTP ${tagged.status}`,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const versions = (tagged.json.packages?.[item.name] ?? [])
|
|
468
|
+
.map((entry) => entry.version)
|
|
469
|
+
.filter((version) => version && STABLE(version));
|
|
470
|
+
if (!versions.length) {
|
|
471
|
+
const dev = await http(
|
|
472
|
+
session,
|
|
473
|
+
`https://repo.packagist.org/p2/${item.name}~dev.json`,
|
|
474
|
+
);
|
|
475
|
+
return {
|
|
476
|
+
status: dev.json?.packages?.[item.name]?.length
|
|
477
|
+
? "dev-only"
|
|
478
|
+
: "stable-version-missing",
|
|
479
|
+
reason: "no stable tagged version",
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const wanted = item.version?.replace(/^v/, "");
|
|
483
|
+
if (!wanted) {
|
|
484
|
+
return {
|
|
485
|
+
status: "unknown",
|
|
486
|
+
reason: "the expected version is unresolved",
|
|
487
|
+
versions,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return versions.some((version) => version.replace(/^v/, "") === wanted)
|
|
491
|
+
? { status: "verified", version: wanted }
|
|
492
|
+
: {
|
|
493
|
+
status: "stable-version-missing",
|
|
494
|
+
reason: `${wanted} is not published; latest ${versions[0]}`,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** The GitHub hook is invisible to this tool: stays a prerequisite. */
|
|
499
|
+
async function packagistHook(session, step) {
|
|
500
|
+
const exact = session.stepResults?.find(
|
|
501
|
+
(entry) => entry.step === "verify-packagist-package",
|
|
502
|
+
);
|
|
503
|
+
return {
|
|
504
|
+
status: "manual-prerequisite",
|
|
505
|
+
reason: `confirm the GitHub integration at ${step.url}${exact?.status === "verified" ? "; the exact tag reached Packagist, but that does not prove the hook" : ""}`,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function packagistUpdateApi(session, step) {
|
|
510
|
+
if (step.state !== "configured") {
|
|
511
|
+
return {
|
|
512
|
+
status: "not-used",
|
|
513
|
+
reason:
|
|
514
|
+
"the workflow does not call the update-package API; the GitHub hook is the only update path",
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const names = (step.secrets ?? []).map((entry) => entry.secret);
|
|
518
|
+
const { present, missing } = await secretPresence(session, names);
|
|
519
|
+
return missing.length
|
|
520
|
+
? {
|
|
521
|
+
status: "manual-prerequisite",
|
|
522
|
+
missing,
|
|
523
|
+
reason: `${missing.join(" + ")} missing; add the Packagist API token as encrypted secrets`,
|
|
524
|
+
}
|
|
525
|
+
: { status: "configured", secrets: present };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Pressing Enter is never verification. */
|
|
529
|
+
async function manualPrerequisite(session, step) {
|
|
530
|
+
console.log(step.description);
|
|
531
|
+
if (step.url) {
|
|
532
|
+
console.log(` ${step.url}`);
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
status: "manual-prerequisite",
|
|
536
|
+
reason: "no automated read-back evidence",
|
|
537
|
+
};
|
|
538
|
+
}
|
package/src/cpp-distribution.mjs
CHANGED
|
@@ -22,9 +22,44 @@ export async function cppDistribution(
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
const project =
|
|
25
|
-
/\bproject\s*\(\s*([\w.-]+)\s+VERSION\s+(\d+(?:\.\d+)+)/i.exec(
|
|
25
|
+
/\bproject\s*\(\s*([\w.-]+)\s+VERSION\s+(\d+(?:\.\d+)+|\$\{(\w+)\})/i.exec(
|
|
26
|
+
cmake,
|
|
27
|
+
);
|
|
26
28
|
let name = project?.[1];
|
|
27
|
-
let version = project?.[2];
|
|
29
|
+
let version = project?.[3] ? null : project?.[2];
|
|
30
|
+
let versionSource = null;
|
|
31
|
+
if (project?.[3]) {
|
|
32
|
+
// `file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" VAR)` is resolved by
|
|
33
|
+
// reading that one sibling file; nothing in the CMake file is executed.
|
|
34
|
+
const read = new RegExp(
|
|
35
|
+
`\\bfile\\s*\\(\\s*READ\\s+"?\\$\\{CMAKE_CURRENT_(?:SOURCE|LIST)_DIR\\}/([\\w.-]+)"?\\s+${project[3]}\\s*\\)`,
|
|
36
|
+
"i",
|
|
37
|
+
).exec(cmake);
|
|
38
|
+
const literal = new RegExp(
|
|
39
|
+
`\\bset\\s*\\(\\s*${project[3]}\\s+"?(\\d+(?:\\.\\d+)+)"?\\s*\\)`,
|
|
40
|
+
"i",
|
|
41
|
+
).exec(cmake);
|
|
42
|
+
if (read) {
|
|
43
|
+
try {
|
|
44
|
+
const value = (
|
|
45
|
+
await readFile(path.join(path.dirname(manifestPath), read[1]), "utf8")
|
|
46
|
+
).trim();
|
|
47
|
+
if (/^\d+(?:\.\d+)+$/.test(value)) {
|
|
48
|
+
version = value;
|
|
49
|
+
versionSource = path.posix.join(
|
|
50
|
+
path.posix.dirname(manifest),
|
|
51
|
+
read[1],
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.code !== "ENOENT") {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} else if (literal) {
|
|
60
|
+
version = literal[1];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
28
63
|
const registry =
|
|
29
64
|
filename === "vcpkg.json"
|
|
30
65
|
? "vcpkg"
|
|
@@ -62,9 +97,11 @@ export async function cppDistribution(
|
|
|
62
97
|
manifest,
|
|
63
98
|
publishable: true,
|
|
64
99
|
distribution: {
|
|
65
|
-
version_source:
|
|
66
|
-
|
|
67
|
-
|
|
100
|
+
version_source:
|
|
101
|
+
versionSource ??
|
|
102
|
+
(project
|
|
103
|
+
? path.posix.join(path.posix.dirname(manifest), "CMakeLists.txt")
|
|
104
|
+
: manifest),
|
|
68
105
|
provider_status: "unverified",
|
|
69
106
|
...(registry === "conan"
|
|
70
107
|
? {
|
package/src/credential-cycle.mjs
CHANGED
|
@@ -20,6 +20,13 @@ export function credentialPolicy(registry) {
|
|
|
20
20
|
"ghcr: GITHUB_TOKEN with packages: write; no registry secret",
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
if (registry === "swiftpm") {
|
|
24
|
+
return {
|
|
25
|
+
mode: "github-token",
|
|
26
|
+
description:
|
|
27
|
+
"swiftpm: Git tags and GitHub releases written by the existing workflow's GITHUB_TOKEN; no SwiftPM OIDC, token secret or registry upload",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
23
30
|
if (["go-modules", "packagist"].includes(registry)) {
|
|
24
31
|
return {
|
|
25
32
|
mode: "tags",
|