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.
@@ -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 steps = checks[item.registry]
69
- ? [
70
- {
71
- id: "validate-package",
72
- title: "Validate package before credential changes",
73
- kind: "check",
74
- description: "Build without publishing.",
75
- command: checks[item.registry],
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: repo.default_branch,
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: repo.default_branch,
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(
@@ -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
- ["maven-central", [/\b(?:mvn|gradle)\b/, /["'`]deploy["'`]/]],
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["'`]/]],