package-registry-manager 0.26.0 → 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 +1 -1
- package/src/central-packagist-steps.mjs +538 -0
- package/src/credential-cycle.mjs +7 -0
- package/src/discovery.mjs +21 -36
- package/src/extra-flows.mjs +21 -12
- package/src/gradle-distribution.mjs +410 -0
- package/src/model.mjs +4 -0
- package/src/organization-scan.mjs +34 -2
- package/src/plan.mjs +14 -0
- package/src/publishers.mjs +9 -2
- package/src/scan-sidecars.mjs +132 -0
- package/src/setup.mjs +15 -1
- package/src/swift-distribution.mjs +249 -0
- package/src/swift-release.mjs +180 -0
package/src/extra-flows.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { credentialRequirements } from "./credential-requirements.mjs";
|
|
2
2
|
import { credentialSteps } from "./credential-cycle.mjs";
|
|
3
3
|
import { tokenSecretSteps } from "./tokens.mjs";
|
|
4
|
+
import {
|
|
5
|
+
centralPublicationStep,
|
|
6
|
+
gradleValidationSteps,
|
|
7
|
+
} from "./gradle-distribution.mjs";
|
|
4
8
|
|
|
5
9
|
/** Additional OIDC registries never fall back to a stored publishing token. */
|
|
6
10
|
export function trustedFlow(item, context) {
|
|
@@ -65,17 +69,20 @@ export function tokenFlow(item) {
|
|
|
65
69
|
},
|
|
66
70
|
"open-vsx": { program: "npx", args: ["--no-install", "vsce", "package"] },
|
|
67
71
|
};
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
const gradle = item.registry === "maven-central" && item.gradle;
|
|
73
|
+
const steps = gradle
|
|
74
|
+
? gradleValidationSteps(item)
|
|
75
|
+
: checks[item.registry]
|
|
76
|
+
? [
|
|
77
|
+
{
|
|
78
|
+
id: "validate-package",
|
|
79
|
+
title: "Validate package before credential changes",
|
|
80
|
+
kind: "check",
|
|
81
|
+
description: "Build without publishing.",
|
|
82
|
+
command: checks[item.registry],
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
: [];
|
|
79
86
|
if (item.registry === "maven-central") {
|
|
80
87
|
const group = /^([\w.-]+):/.exec(item.name ?? "")?.[1];
|
|
81
88
|
steps.push({
|
|
@@ -88,7 +95,9 @@ export function tokenFlow(item) {
|
|
|
88
95
|
...(group ? { namespace: group } : {}),
|
|
89
96
|
state: "manual-prerequisite",
|
|
90
97
|
});
|
|
91
|
-
return steps.concat(mavenCredentialSteps(item)
|
|
98
|
+
return steps.concat(mavenCredentialSteps(item), [
|
|
99
|
+
centralPublicationStep(item),
|
|
100
|
+
]);
|
|
92
101
|
}
|
|
93
102
|
return steps.concat(
|
|
94
103
|
credentialSteps(item.registry, item.token_secrets?.[0] ?? item.registry),
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kotlin Multiplatform targets and the artifact suffix the Kotlin Gradle
|
|
6
|
+
* plugin publishes each one under (`<artifact>-<suffix>`), next to the root
|
|
7
|
+
* publication that carries the Gradle module metadata.
|
|
8
|
+
*/
|
|
9
|
+
const KMP_TARGETS = new Map([
|
|
10
|
+
["jvm", "jvm"],
|
|
11
|
+
["js", "js"],
|
|
12
|
+
["wasmJs", "wasm-js"],
|
|
13
|
+
["wasmWasi", "wasm-wasi"],
|
|
14
|
+
["androidTarget", "android"],
|
|
15
|
+
["android", "android"],
|
|
16
|
+
["iosArm64", "iosarm64"],
|
|
17
|
+
["iosX64", "iosx64"],
|
|
18
|
+
["iosSimulatorArm64", "iossimulatorarm64"],
|
|
19
|
+
["macosArm64", "macosarm64"],
|
|
20
|
+
["macosX64", "macosx64"],
|
|
21
|
+
["tvosArm64", "tvosarm64"],
|
|
22
|
+
["tvosX64", "tvosx64"],
|
|
23
|
+
["tvosSimulatorArm64", "tvossimulatorarm64"],
|
|
24
|
+
["watchosArm32", "watchosarm32"],
|
|
25
|
+
["watchosArm64", "watchosarm64"],
|
|
26
|
+
["watchosX64", "watchosx64"],
|
|
27
|
+
["watchosSimulatorArm64", "watchossimulatorarm64"],
|
|
28
|
+
["watchosDeviceArm64", "watchosdevicearm64"],
|
|
29
|
+
["linuxX64", "linuxx64"],
|
|
30
|
+
["linuxArm64", "linuxarm64"],
|
|
31
|
+
["mingwX64", "mingwx64"],
|
|
32
|
+
["androidNativeArm32", "androidnativearm32"],
|
|
33
|
+
["androidNativeArm64", "androidnativearm64"],
|
|
34
|
+
["androidNativeX86", "androidnativex86"],
|
|
35
|
+
["androidNativeX64", "androidnativex64"],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/** Gradle tasks that publish to the Central Portal, by plugin. */
|
|
39
|
+
export const GRADLE_PUBLISH_TASKS = Object.freeze([
|
|
40
|
+
"publishToMavenCentral",
|
|
41
|
+
"publishAndReleaseToMavenCentral",
|
|
42
|
+
"publishAllPublicationsToMavenCentralRepository",
|
|
43
|
+
"publish",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const SKIP_REASON =
|
|
47
|
+
"Gradle application/build definition has no resolved Maven publication coordinates and component";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads a Gradle build script as text, never running Gradle, and resolves
|
|
51
|
+
* the Maven publication: classic `maven-publish` coordinates, or the
|
|
52
|
+
* vanniktech plugin's `coordinates(group, artifact, version)` with Kotlin
|
|
53
|
+
* Multiplatform platform publications. `version.toString()` and
|
|
54
|
+
* `libs.versions.*` references resolve from the script, gradle.properties
|
|
55
|
+
* or the version catalog; anything else stays unknown with its reason.
|
|
56
|
+
*/
|
|
57
|
+
export async function gradleDistribution(contents, manifest, manifestPath) {
|
|
58
|
+
const code = stripGradleComments(contents);
|
|
59
|
+
const directory = path.dirname(manifestPath);
|
|
60
|
+
const sibling = (name) => readSibling(directory, name);
|
|
61
|
+
const properties = parseProperties(
|
|
62
|
+
(await sibling("gradle.properties")) ?? "",
|
|
63
|
+
);
|
|
64
|
+
const settings =
|
|
65
|
+
(await sibling("settings.gradle.kts")) ??
|
|
66
|
+
(await sibling("settings.gradle")) ??
|
|
67
|
+
"";
|
|
68
|
+
// A top-level assignment wins; plugin declarations such as
|
|
69
|
+
// `kotlin("jvm") version "2.0.0"` never name the project version.
|
|
70
|
+
const literal = (key, text = code) =>
|
|
71
|
+
[
|
|
72
|
+
`^${key}\\s*(?:=\\s*)?['"]([^'"$]+)['"]`,
|
|
73
|
+
`(?:^|[\\s{;])${key}\\s*=\\s*['"]([^'"$]+)['"]`,
|
|
74
|
+
...(key === "version"
|
|
75
|
+
? []
|
|
76
|
+
: [`(?:^|[\\s{;])${key}\\s+['"]([^'"$]+)['"]`]),
|
|
77
|
+
]
|
|
78
|
+
.map((source) => new RegExp(source, "m").exec(text)?.[1])
|
|
79
|
+
.find(Boolean) ?? null;
|
|
80
|
+
const rootName = literal("rootProject\\.name", stripGradleComments(settings));
|
|
81
|
+
const vanniktech =
|
|
82
|
+
/com\.vanniktech\.maven\.publish/.test(code) &&
|
|
83
|
+
/\bmavenPublishing\s*\{/.test(code);
|
|
84
|
+
const coordinates =
|
|
85
|
+
/\bcoordinates\s*\(\s*([^,()]+(?:\([^)]*\))?)\s*,\s*([^,()]+(?:\([^)]*\))?)\s*,\s*([^()]+?(?:\([^)]*\))?)\s*\)/.exec(
|
|
86
|
+
code,
|
|
87
|
+
);
|
|
88
|
+
if (!vanniktech) {
|
|
89
|
+
return classicGradle(code, manifest, literal, rootName);
|
|
90
|
+
}
|
|
91
|
+
const problems = [];
|
|
92
|
+
const resolve = async (expression, role) => {
|
|
93
|
+
const value = expression?.trim();
|
|
94
|
+
if (!value) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const quoted = /^"([^"$]+)"$|^'([^'$]+)'$/.exec(value);
|
|
98
|
+
if (quoted) {
|
|
99
|
+
return quoted[1] ?? quoted[2];
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
/^(?:project\.)?version(?:\.toString\(\))?$|^"\$\{?version\}?"$/.test(
|
|
103
|
+
value,
|
|
104
|
+
)
|
|
105
|
+
) {
|
|
106
|
+
return literal("version") ?? properties.version ?? null;
|
|
107
|
+
}
|
|
108
|
+
if (/^(?:project\.)?group(?:\.toString\(\))?$/.test(value)) {
|
|
109
|
+
return literal("group") ?? properties.group ?? null;
|
|
110
|
+
}
|
|
111
|
+
const catalog = /^libs\.versions\.([\w.]+)\.get\(\)$/.exec(value);
|
|
112
|
+
if (catalog) {
|
|
113
|
+
const toml =
|
|
114
|
+
(await readSibling(directory, "gradle/libs.versions.toml")) ?? "";
|
|
115
|
+
const key = catalog[1].replaceAll(".", "[-_.]");
|
|
116
|
+
const found = new RegExp(`^\\s*${key}\\s*=\\s*"([^"]+)"`, "m").exec(
|
|
117
|
+
/^\[versions\]\s*$([\s\S]*?)(?=^\[|(?![\s\S]))/m.exec(toml)?.[1] ?? "",
|
|
118
|
+
)?.[1];
|
|
119
|
+
if (found) {
|
|
120
|
+
return found;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const property =
|
|
124
|
+
/^(?:providers\.gradleProperty|property|findProperty)\(\s*"([\w.-]+)"\s*\)(?:\.get\(\))?$/.exec(
|
|
125
|
+
value,
|
|
126
|
+
);
|
|
127
|
+
if (property && properties[property[1]]) {
|
|
128
|
+
return properties[property[1]];
|
|
129
|
+
}
|
|
130
|
+
problems.push(
|
|
131
|
+
`dynamic Gradle ${role} expression ${value}; list the reviewed value under "gradle" in .package-registry-manager.json`,
|
|
132
|
+
);
|
|
133
|
+
return null;
|
|
134
|
+
};
|
|
135
|
+
const group =
|
|
136
|
+
(await resolve(coordinates?.[1], "group")) ??
|
|
137
|
+
literal("group") ??
|
|
138
|
+
properties.GROUP ??
|
|
139
|
+
null;
|
|
140
|
+
const artifact =
|
|
141
|
+
(await resolve(coordinates?.[2], "artifact")) ??
|
|
142
|
+
properties.POM_ARTIFACT_ID ??
|
|
143
|
+
rootName;
|
|
144
|
+
const version =
|
|
145
|
+
(await resolve(coordinates?.[3], "version")) ??
|
|
146
|
+
(coordinates
|
|
147
|
+
? null
|
|
148
|
+
: (literal("version") ??
|
|
149
|
+
properties.VERSION_NAME ??
|
|
150
|
+
properties.version ??
|
|
151
|
+
null));
|
|
152
|
+
if (!group || !artifact) {
|
|
153
|
+
return {
|
|
154
|
+
manifest,
|
|
155
|
+
publishable: false,
|
|
156
|
+
discovery_skip: `${SKIP_REASON}${problems.length ? `: ${problems.join("; ")}` : ""}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const multiplatform =
|
|
160
|
+
/kotlin\s*\(\s*["']multiplatform["']\s*\)|org\.jetbrains\.kotlin\.multiplatform/.test(
|
|
161
|
+
code,
|
|
162
|
+
);
|
|
163
|
+
const wrapper = (await exists(path.join(directory, "gradlew")))
|
|
164
|
+
? "./gradlew"
|
|
165
|
+
: "gradle";
|
|
166
|
+
const task = /publishToMavenCentral\s*\(/.test(code)
|
|
167
|
+
? "publishToMavenCentral"
|
|
168
|
+
: "publishAllPublicationsToMavenCentralRepository";
|
|
169
|
+
const item = {
|
|
170
|
+
registry: "maven-central",
|
|
171
|
+
name: `${group}:${artifact}`,
|
|
172
|
+
version,
|
|
173
|
+
manifest,
|
|
174
|
+
publishable: true,
|
|
175
|
+
build_tool: "gradle",
|
|
176
|
+
gradle: {
|
|
177
|
+
plugin: "com.vanniktech.maven.publish",
|
|
178
|
+
wrapper,
|
|
179
|
+
working_directory: path.posix.dirname(manifest),
|
|
180
|
+
publish_task: task,
|
|
181
|
+
automatic_release: /automaticRelease\s*=\s*true/.test(code),
|
|
182
|
+
signing: /signAllPublications\s*\(/.test(code),
|
|
183
|
+
multiplatform,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
if (multiplatform) {
|
|
187
|
+
item.variants = kmpVariants(code, group, artifact);
|
|
188
|
+
item.gradle.requirements = gradleRequirements(item.variants);
|
|
189
|
+
}
|
|
190
|
+
if (problems.length) {
|
|
191
|
+
item.warnings = problems;
|
|
192
|
+
}
|
|
193
|
+
return item;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function classicGradle(code, manifest, literal, rootName) {
|
|
197
|
+
const group = literal("groupId") ?? literal("group");
|
|
198
|
+
const artifact =
|
|
199
|
+
literal("artifactId") ?? literal("archivesBaseName") ?? rootName;
|
|
200
|
+
const version = literal("version");
|
|
201
|
+
if (
|
|
202
|
+
!group ||
|
|
203
|
+
!artifact ||
|
|
204
|
+
!version ||
|
|
205
|
+
!/maven-publish|MavenPublication/.test(code) ||
|
|
206
|
+
!/from\s*\(?\s*components|artifact\s*\(/.test(code)
|
|
207
|
+
) {
|
|
208
|
+
return { manifest, publishable: false, discovery_skip: SKIP_REASON };
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
registry: "maven-central",
|
|
212
|
+
name: `${group}:${artifact}`,
|
|
213
|
+
version,
|
|
214
|
+
manifest,
|
|
215
|
+
publishable: true,
|
|
216
|
+
build_tool: "gradle",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The platform publications a Kotlin Multiplatform build declares, each
|
|
222
|
+
* marked when it only exists behind a condition (such as an Android SDK)
|
|
223
|
+
* or needs a macOS host (Apple targets).
|
|
224
|
+
*/
|
|
225
|
+
export function kmpVariants(code, group, artifact) {
|
|
226
|
+
const block = kotlinBlock(code);
|
|
227
|
+
const variants = [];
|
|
228
|
+
const names = [...KMP_TARGETS.keys()].join("|");
|
|
229
|
+
const expression = new RegExp(
|
|
230
|
+
`(?<![\\w.])(${names})\\s*(?:\\(\\s*(?:"[^"]*")?\\s*\\)|\\{)|KotlinMultiplatformAndroidLibraryTarget>\\s*\\(\\s*"(android)"`,
|
|
231
|
+
"g",
|
|
232
|
+
);
|
|
233
|
+
for (const match of block.text.matchAll(expression)) {
|
|
234
|
+
const target = match[1] ?? match[2];
|
|
235
|
+
const suffix = KMP_TARGETS.get(target);
|
|
236
|
+
if (variants.some((item) => item.suffix === suffix)) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const conditional = insideCondition(block.text, match.index);
|
|
240
|
+
const apple = /^(?:ios|macos|tvos|watchos)/.test(target);
|
|
241
|
+
variants.push({
|
|
242
|
+
target: target === "androidTarget" ? "android" : target,
|
|
243
|
+
suffix,
|
|
244
|
+
coordinates: `${group}:${artifact}-${suffix}`,
|
|
245
|
+
...(apple ? { host: "macos" } : {}),
|
|
246
|
+
...(target.startsWith("android") && !target.startsWith("androidNative")
|
|
247
|
+
? { requires: "Android SDK" }
|
|
248
|
+
: {}),
|
|
249
|
+
...(conditional ? { conditional: true } : {}),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return variants;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function gradleRequirements(variants) {
|
|
256
|
+
const requirements = [];
|
|
257
|
+
if (variants.some((item) => item.host === "macos")) {
|
|
258
|
+
requirements.push({
|
|
259
|
+
kind: "runner",
|
|
260
|
+
requirement: "macOS",
|
|
261
|
+
reason:
|
|
262
|
+
"Kotlin/Native publishes Apple targets only from a macOS host; a Linux runner silently omits them",
|
|
263
|
+
targets: variants
|
|
264
|
+
.filter((item) => item.host === "macos")
|
|
265
|
+
.map((item) => item.target),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (variants.some((item) => item.requires === "Android SDK")) {
|
|
269
|
+
requirements.push({
|
|
270
|
+
kind: "sdk",
|
|
271
|
+
requirement: "Android SDK (ANDROID_HOME or ANDROID_SDK_ROOT)",
|
|
272
|
+
reason:
|
|
273
|
+
"the Android publication exists only when the build finds an SDK; without it the release lacks the -android artifact",
|
|
274
|
+
targets: ["android"],
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
requirements.push({
|
|
278
|
+
kind: "toolchain",
|
|
279
|
+
requirement: "JDK and the Gradle wrapper",
|
|
280
|
+
reason: "the wrapper pins the Gradle version that builds every variant",
|
|
281
|
+
});
|
|
282
|
+
return requirements;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function kotlinBlock(code) {
|
|
286
|
+
const start = /(?:^|\n)\s*kotlin\s*\{/.exec(code);
|
|
287
|
+
if (!start) {
|
|
288
|
+
return { text: "" };
|
|
289
|
+
}
|
|
290
|
+
const open = code.indexOf("{", start.index);
|
|
291
|
+
let depth = 0;
|
|
292
|
+
for (let index = open; index < code.length; index += 1) {
|
|
293
|
+
if (code[index] === "{") {
|
|
294
|
+
depth += 1;
|
|
295
|
+
} else if (code[index] === "}") {
|
|
296
|
+
depth -= 1;
|
|
297
|
+
if (depth === 0) {
|
|
298
|
+
return { text: code.slice(open + 1, index) };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { text: code.slice(open + 1) };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Whether the position sits inside an `if (...) { ... }` block. */
|
|
306
|
+
function insideCondition(text, position) {
|
|
307
|
+
const stack = [];
|
|
308
|
+
for (let index = 0; index < position; index += 1) {
|
|
309
|
+
if (text[index] === "{") {
|
|
310
|
+
stack.push(
|
|
311
|
+
/\bif\s*\([^{}]*\)\s*$/.test(
|
|
312
|
+
text.slice(Math.max(0, index - 200), index),
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
} else if (text[index] === "}") {
|
|
316
|
+
stack.pop();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return stack.some(Boolean);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Removes Kotlin/Groovy comments while keeping `//` inside URLs. */
|
|
323
|
+
export function stripGradleComments(contents) {
|
|
324
|
+
return contents
|
|
325
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
326
|
+
.replace(/(^|[^:"\\])\/\/[^\n]*/g, "$1");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function parseProperties(contents) {
|
|
330
|
+
const values = {};
|
|
331
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
332
|
+
const match = /^\s*([\w.-]+)\s*[=:]\s*(.*?)\s*$/.exec(line);
|
|
333
|
+
if (match && !line.trimStart().startsWith("#")) {
|
|
334
|
+
values[match[1]] = match[2];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return values;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function readSibling(directory, name) {
|
|
341
|
+
try {
|
|
342
|
+
return await readFile(path.join(directory, name), "utf8");
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function exists(file) {
|
|
352
|
+
try {
|
|
353
|
+
return (await stat(file)).isFile();
|
|
354
|
+
} catch {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Validation and Central steps for a Gradle-built Maven Central package. */
|
|
360
|
+
export function gradleValidationSteps(item) {
|
|
361
|
+
const gradle = item.gradle ?? {};
|
|
362
|
+
const wrapper = gradle.wrapper ?? "./gradlew";
|
|
363
|
+
const steps = [
|
|
364
|
+
{
|
|
365
|
+
id: "validate-package",
|
|
366
|
+
title: "Validate the Gradle publication without uploading",
|
|
367
|
+
kind: "check",
|
|
368
|
+
description: `Run the official Gradle check and local publication tasks in ${gradle.working_directory ?? "the build directory"}; nothing reaches Central.`,
|
|
369
|
+
command: {
|
|
370
|
+
program: wrapper,
|
|
371
|
+
args: [
|
|
372
|
+
"--no-daemon",
|
|
373
|
+
"--console=plain",
|
|
374
|
+
"check",
|
|
375
|
+
"publishToMavenLocal",
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
];
|
|
380
|
+
if (gradle.requirements?.length) {
|
|
381
|
+
steps.push({
|
|
382
|
+
id: "verify-gradle-toolchain",
|
|
383
|
+
title: "Confirm the build hosts and SDKs for every variant",
|
|
384
|
+
kind: "manual",
|
|
385
|
+
state: "manual-prerequisite",
|
|
386
|
+
requirements: gradle.requirements,
|
|
387
|
+
description: `${gradle.requirements.map((entry) => `${entry.requirement}: ${entry.reason}`).join(". ")}. A missing runner, SDK or toolchain is separate from Central authentication.`,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return steps;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Exact Central read-back for a Gradle publication: the root POM, the Gradle
|
|
395
|
+
* module metadata and every platform publication it references. A root POM
|
|
396
|
+
* alone never certifies the platform variants.
|
|
397
|
+
*/
|
|
398
|
+
export function centralPublicationStep(item) {
|
|
399
|
+
const [group, artifact] = (item.name ?? "").split(":");
|
|
400
|
+
const variants = item.variants ?? [];
|
|
401
|
+
return {
|
|
402
|
+
id: "verify-central-publication",
|
|
403
|
+
title: `Verify ${item.name}:${item.version ?? "(unresolved)"}${variants.length ? ` and its ${variants.length} platform publications` : ""} on Central`,
|
|
404
|
+
kind: "api",
|
|
405
|
+
coordinates: { group, artifact, version: item.version ?? null },
|
|
406
|
+
variants: variants.map((entry) => entry.coordinates),
|
|
407
|
+
description: `Read the exact root POM and ${item.gradle?.multiplatform ? "Gradle module metadata (.module)" : "artifact"} from repo1.maven.org, then every platform coordinate the metadata references (${variants.map((entry) => entry.suffix).join(", ") || "none"}). Missing module metadata or a missing variant keeps the release partial; a skipped or unlogged ${item.gradle?.publish_task ?? "publish"} job is never publication. Published versions are immutable and never re-uploaded.`,
|
|
408
|
+
url: `https://central.sonatype.com/artifact/${group}/${artifact}`,
|
|
409
|
+
};
|
|
410
|
+
}
|
package/src/model.mjs
CHANGED
|
@@ -16,12 +16,16 @@ export const REGISTRIES = Object.freeze([
|
|
|
16
16
|
"cmake",
|
|
17
17
|
"conan",
|
|
18
18
|
"vcpkg",
|
|
19
|
+
"swiftpm",
|
|
19
20
|
]);
|
|
20
21
|
|
|
21
22
|
const REGISTRY_ALIASES = new Map([
|
|
22
23
|
["cmake", "cmake"],
|
|
23
24
|
["conan", "conan"],
|
|
24
25
|
["vcpkg", "vcpkg"],
|
|
26
|
+
["swift", "swiftpm"],
|
|
27
|
+
["swiftpm", "swiftpm"],
|
|
28
|
+
["swift-package-manager", "swiftpm"],
|
|
25
29
|
["ruby", "rubygems"],
|
|
26
30
|
["rubygems", "rubygems"],
|
|
27
31
|
["gem", "rubygems"],
|
|
@@ -10,6 +10,7 @@ import { credentialPolicy } from "./credential-cycle.mjs";
|
|
|
10
10
|
import { identityCommand } from "./repository-identity.mjs";
|
|
11
11
|
import { tokenExpiryFindings } from "./crates-tokens.mjs";
|
|
12
12
|
import { scriptReferences } from "./publishers.mjs";
|
|
13
|
+
import { fetchVersionSidecars } from "./scan-sidecars.mjs";
|
|
13
14
|
|
|
14
15
|
/** Only files used by existing discovery; no full repository clones for scans. */
|
|
15
16
|
export const SCAN_MATCHES = [
|
|
@@ -25,6 +26,7 @@ export const SCAN_MATCHES = [
|
|
|
25
26
|
"pom.xml",
|
|
26
27
|
"build.gradle",
|
|
27
28
|
"build.gradle.kts",
|
|
29
|
+
"Package.swift",
|
|
28
30
|
"composer.json",
|
|
29
31
|
"jsr.json",
|
|
30
32
|
"deno.json",
|
|
@@ -186,10 +188,15 @@ export async function withAccountScan(options, callback) {
|
|
|
186
188
|
throw new Error("gh-manager returned an invalid repository name");
|
|
187
189
|
}
|
|
188
190
|
const root = path.join(directory, String(index));
|
|
191
|
+
// One immutable commit for every read, so sidecars match manifests.
|
|
192
|
+
const ref = await headCommit(github, slug, repo.default_branch);
|
|
193
|
+
if (ref !== repo.default_branch) {
|
|
194
|
+
item.commit = ref;
|
|
195
|
+
}
|
|
189
196
|
const files = await github.repos.files(slug, {
|
|
190
197
|
match: SCAN_MATCHES,
|
|
191
198
|
content: true,
|
|
192
|
-
ref
|
|
199
|
+
ref,
|
|
193
200
|
});
|
|
194
201
|
// Fetch only scripts named by manifests/workflows, rather than all source blobs.
|
|
195
202
|
const scripts = scriptReferences(
|
|
@@ -202,10 +209,20 @@ export async function withAccountScan(options, callback) {
|
|
|
202
209
|
...(await github.repos.files(slug, {
|
|
203
210
|
match: scripts.map((script) => `**/${script}`),
|
|
204
211
|
content: true,
|
|
205
|
-
ref
|
|
212
|
+
ref,
|
|
206
213
|
})),
|
|
207
214
|
);
|
|
208
215
|
}
|
|
216
|
+
const sidecars = await fetchVersionSidecars(
|
|
217
|
+
github,
|
|
218
|
+
slug,
|
|
219
|
+
files,
|
|
220
|
+
ref,
|
|
221
|
+
onError,
|
|
222
|
+
);
|
|
223
|
+
if (sidecars.requested.length) {
|
|
224
|
+
item.version_sidecars = sidecars;
|
|
225
|
+
}
|
|
209
226
|
await writeSnapshot(root, slug, files);
|
|
210
227
|
const discovered = await inspectRepository(root, {
|
|
211
228
|
includeSkipped: options.verbose,
|
|
@@ -293,6 +310,21 @@ export async function withAccountScan(options, callback) {
|
|
|
293
310
|
}
|
|
294
311
|
}
|
|
295
312
|
|
|
313
|
+
/** The default branch's commit, or the branch itself when unreadable. */
|
|
314
|
+
async function headCommit(github, slug, branch) {
|
|
315
|
+
if (!github.rest?.request || !branch) {
|
|
316
|
+
return branch;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const commit = await github.rest.request(
|
|
320
|
+
`/repos/${slug}/commits/${encodeURIComponent(branch)}`,
|
|
321
|
+
);
|
|
322
|
+
return /^[0-9a-f]{40}$/.test(commit?.sha ?? "") ? commit.sha : branch;
|
|
323
|
+
} catch {
|
|
324
|
+
return branch;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
296
328
|
/** Only inspect the newest default-branch release; successful runs need no logs. */
|
|
297
329
|
async function releaseHealth(github, item, findings, onError) {
|
|
298
330
|
const workflows = new Set(
|
package/src/plan.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cppPlan } from "./cpp-distribution.mjs";
|
|
2
|
+
import { swiftPlan } from "./swift-distribution.mjs";
|
|
2
3
|
import {
|
|
3
4
|
BOOTSTRAP_CONDITIONS,
|
|
4
5
|
cratesFlow,
|
|
@@ -137,6 +138,19 @@ function buildPlan(inspection, packageInfo, options) {
|
|
|
137
138
|
case "nuget":
|
|
138
139
|
case "jsr":
|
|
139
140
|
return flowPlan(inspection, packageInfo, options);
|
|
141
|
+
case "swiftpm": {
|
|
142
|
+
const { github_owner, github_repository } = inspection.repository ?? {};
|
|
143
|
+
return basePlan(
|
|
144
|
+
inspection,
|
|
145
|
+
packageInfo,
|
|
146
|
+
swiftPlan(
|
|
147
|
+
packageInfo,
|
|
148
|
+
github_owner && github_repository
|
|
149
|
+
? `${github_owner}/${github_repository}`
|
|
150
|
+
: null,
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
140
154
|
case "go-modules":
|
|
141
155
|
steps = [
|
|
142
156
|
check(
|
package/src/publishers.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { swiftReleaseEvidence } from "./swift-distribution.mjs";
|
|
1
2
|
import { authenticationFacts, publishingPolicy } from "./publishing-state.mjs";
|
|
2
3
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
@@ -23,7 +24,7 @@ export const JOB_PATTERNS = new Map([
|
|
|
23
24
|
["docker-hub", /docker\/login-action|\bdocker\s+(?:login|push)\b/],
|
|
24
25
|
[
|
|
25
26
|
"maven-central",
|
|
26
|
-
/(?:\.\/|\b)mvnw?\s+(?:-\S+\s+)*deploy\b|(?:\.\/|\b)gradlew?\s+(?:-\S+\s+)*publish\b/,
|
|
27
|
+
/(?:\.\/|\b)mvnw?\s+(?:-\S+\s+)*deploy\b|(?:\.\/|\b)gradlew?\s+(?:-\S+\s+)*publish(?:ToMavenCentral|AndReleaseToMavenCentral|AllPublicationsTo\w+|ToSonatype)?\b/,
|
|
27
28
|
],
|
|
28
29
|
["vscode-marketplace", /\bvsce\s+publish\b/],
|
|
29
30
|
["open-vsx", /\bovsx\s+publish\b/],
|
|
@@ -57,6 +58,9 @@ export const RELEASE_EVIDENCE = new Map([
|
|
|
57
58
|
/\bgh\s+release\s+create\b|\bgit\s+push\b[^\n]*\btags?\b|proxy\.golang\.org/,
|
|
58
59
|
],
|
|
59
60
|
["cmake", /\bgh\s+release\s+(?:create|upload)\b/],
|
|
61
|
+
// SwiftPM consumers resolve Git tags: a tag or release that names the
|
|
62
|
+
// Swift package is the publication.
|
|
63
|
+
["swiftpm", { test: swiftReleaseEvidence }],
|
|
60
64
|
]);
|
|
61
65
|
|
|
62
66
|
// Scripts usually pass the subcommand as a separate argument, as in
|
|
@@ -67,7 +71,10 @@ const SCRIPT_PATTERNS = new Map([
|
|
|
67
71
|
["rubygems", [/\bgem\b/, /["'`]push["'`]/]],
|
|
68
72
|
["nuget", [/\b(?:dotnet|nuget)\b/, /["'`]push["'`]/]],
|
|
69
73
|
["jsr", [/\b(?:deno|jsr)\b/, /["'`]publish["'`]/]],
|
|
70
|
-
[
|
|
74
|
+
[
|
|
75
|
+
"maven-central",
|
|
76
|
+
[/\b(?:mvnw?|gradlew?)\b/, /["'`](?:deploy|publish\w*)["'`]/],
|
|
77
|
+
],
|
|
71
78
|
["vscode-marketplace", [/\bvsce\b/, /["'`]publish["'`]/]],
|
|
72
79
|
["open-vsx", [/\bovsx\b/, /["'`]publish["'`]/]],
|
|
73
80
|
["npm", [/\b(?:npm|pnpm|yarn)\b/, /["'`]publish["'`]/]],
|