@async/pipeline 0.8.9 → 0.9.1

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,746 +1,6 @@
1
- import { spawnSync } from "node:child_process";
2
- import { existsSync, readFileSync, rmSync } from "node:fs";
3
- import { chmod, cp, mkdtemp, readFile, writeFile } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
5
- import { isAbsolute, join, relative, resolve } from "node:path";
6
- const GITHUB_REGISTRY = "https://npm.pkg.github.com";
7
- const NPM_REGISTRY = "https://registry.npmjs.org/";
8
- const COMMENT_MARKER = "<!-- github-packages-pr-preview -->";
9
- const SHA_PATTERN = /^[0-9a-f]{40}$/;
10
- const NAME_PATTERN = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/;
11
- const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
12
- const RELEASE_DOCTOR_REGISTRY_ATTEMPTS = 12;
13
- const RELEASE_DOCTOR_REGISTRY_RETRY_DELAY_MS = 5000;
14
- export async function publishGitHubPackage(mode, options) {
15
- const context = await readPackageContext(options.cwd, options.packagePath);
16
- const { packageDir, manifest } = context;
17
- assertPublicPackage(manifest);
18
- const repository = options.env.GITHUB_REPOSITORY ?? packageRepositoryName(manifest) ?? "";
19
- const owner = (options.namespace ?? options.env.GITHUB_REPOSITORY_OWNER ?? repository.split("/")[0] ?? "").toLowerCase();
20
- const registry = normalizeRegistry(options.registry ?? GITHUB_REGISTRY);
21
- const shouldComment = options.comment ?? true;
22
- if (!repository || !owner) {
23
- throw new Error("Set GITHUB_REPOSITORY or package.json repository so GitHub Packages publishing can resolve the repository owner.");
24
- }
25
- const mirrorName = githubMirrorPackageName(manifest.name, owner);
26
- const token = options.env.GITHUB_TOKEN ?? options.env.NODE_AUTH_TOKEN;
27
- const apiBase = (options.env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
28
- const releaseContext = await resolveGitHubPublishContext(mode, { manifest, repository, env: options.env, io: options.io });
29
- if (!token) {
30
- throw new Error("Set GITHUB_TOKEN (or NODE_AUTH_TOKEN) with packages:write to publish to GitHub Packages.");
31
- }
32
- if (!existsSync(join(packageDir, "dist"))) {
33
- throw new Error(`${relativeLabel(options.cwd, packageDir)}/dist is missing. Build before publishing.`);
34
- }
35
- const stagingDir = await mkdtemp(join(tmpdir(), "async-pipeline-github-publish-"));
36
- try {
37
- const staged = {
38
- ...manifest,
39
- name: mirrorName,
40
- version: releaseContext.version,
41
- publishConfig: { registry }
42
- };
43
- delete staged.scripts;
44
- delete staged.devDependencies;
45
- await writeFile(join(stagingDir, "package.json"), `${JSON.stringify(staged, null, 2)}\n`, "utf8");
46
- await cp(join(packageDir, "dist"), join(stagingDir, "dist"), { recursive: true });
47
- for (const extra of ["LICENSE", "README.md"]) {
48
- if (existsSync(join(packageDir, extra))) {
49
- await cp(join(packageDir, extra), join(stagingDir, extra));
50
- }
51
- }
52
- const registryUrl = new URL(registry);
53
- const registryAuthPath = `${registryUrl.host}${registryUrl.pathname.replace(/\/$/, "")}`;
54
- const npmConfig = join(stagingDir, ".github-packages.npmrc");
55
- await writeFile(npmConfig, `@${owner}:registry=${registry}\n//${registryAuthPath}/:_authToken=${token}\n`, "utf8");
56
- await chmod(npmConfig, 0o600);
57
- const npm = (args, runOptions = {}) => spawnSync("npm", args, {
58
- cwd: stagingDir,
59
- stdio: runOptions.capture ? ["ignore", "pipe", "pipe"] : "inherit",
60
- encoding: "utf8",
61
- env: { ...options.env, NPM_CONFIG_USERCONFIG: npmConfig }
62
- });
63
- const spec = `${mirrorName}@${releaseContext.version}`;
64
- const view = npm(["view", spec, "version", "--registry", registry], { capture: true });
65
- const viewOutput = npmOutput(view);
66
- const exists = view.status === 0;
67
- if (!exists && !isMissingVersion(view)) {
68
- options.io.stderr(viewOutput.slice(0, 2000));
69
- throw new Error(`Could not check whether ${spec} already exists on GitHub Packages; refusing to guess. See npm output above.`);
70
- }
71
- if (exists) {
72
- options.io.stdout(`${spec} already exists on GitHub Packages; skipping publish.\n`);
73
- }
74
- else {
75
- options.io.stdout(`Publishing ${spec} to GitHub Packages with tag ${releaseContext.distTag}.\n`);
76
- const publish = npm(["publish", "--tag", releaseContext.distTag, "--ignore-scripts", "--registry", registry]);
77
- if (publish.status !== 0) {
78
- throw new Error(`Failed to publish ${spec} to GitHub Packages. Check the job's packages:write permission, package visibility, and whether this immutable version already exists.`);
79
- }
80
- }
81
- const moveDistTag = () => {
82
- const result = npm(["dist-tag", "add", spec, releaseContext.distTag, "--registry", registry]);
83
- if (result.status !== 0) {
84
- throw new Error(`Failed to move GitHub Packages dist-tag ${releaseContext.distTag} to ${spec}.`);
85
- }
86
- };
87
- const ghApi = async (path, init = {}) => {
88
- const response = await fetch(`${apiBase}${path}`, {
89
- ...init,
90
- headers: {
91
- authorization: `Bearer ${token}`,
92
- accept: "application/vnd.github+json",
93
- "content-type": "application/json",
94
- "x-github-api-version": "2022-11-28",
95
- ...init.headers
96
- }
97
- });
98
- if (!response.ok) {
99
- const text = await response.text();
100
- throw new Error(`GitHub API ${init.method ?? "GET"} ${path} failed with ${response.status}: ${text.slice(0, 500)}`);
101
- }
102
- return response.status === 204 ? null : response.json();
103
- };
104
- if (mode === "release") {
105
- moveDistTag();
106
- }
107
- else if (mode === "main") {
108
- const branch = await guardedApi(() => ghApi(`/repos/${repository}/branches/main`), "Could not read the current main branch head");
109
- const branchSha = asRecord(asRecord(branch).commit).sha;
110
- if (branchSha === options.env.GITHUB_SHA) {
111
- moveDistTag();
112
- }
113
- else {
114
- options.io.stdout(`::notice::Not moving ${releaseContext.distTag}: main moved from ${options.env.GITHUB_SHA} to ${String(branchSha)}.\n`);
115
- }
116
- }
117
- else {
118
- const pr = releaseContext.prContext;
119
- if (!pr)
120
- throw new Error("Internal error: missing PR context.");
121
- const pull = await guardedApi(() => ghApi(`/repos/${repository}/pulls/${pr.number}`), "Could not read the current PR head");
122
- const currentHead = asRecord(asRecord(pull).head).sha;
123
- if (currentHead !== pr.headSha) {
124
- options.io.stdout(`::notice::Not moving ${releaseContext.distTag}: PR head moved from ${pr.headSha} to ${String(currentHead)}.\n`);
125
- return;
126
- }
127
- moveDistTag();
128
- if (!shouldComment) {
129
- options.io.stdout("Skipping PR preview comment.\n");
130
- return;
131
- }
132
- const installTarget = (versionOrTag) => {
133
- const target = `${mirrorName}@${versionOrTag}`;
134
- return mirrorName === manifest.name ? target : `${manifest.name}@npm:${target}`;
135
- };
136
- const body = [
137
- COMMENT_MARKER,
138
- "### Preview package",
139
- "",
140
- `Preview for PR head \`${pr.headSha}\` (built from its merge with main), published to GitHub Packages as \`${mirrorName}\`.`,
141
- "",
142
- "Latest successful build for this PR:",
143
- "```sh",
144
- `pnpm add ${installTarget(releaseContext.distTag)}`,
145
- "```",
146
- "",
147
- "Exact commit build:",
148
- "```sh",
149
- `pnpm add ${installTarget(releaseContext.version)}`,
150
- "```",
151
- "",
152
- `Requires GitHub Packages auth and \`@${owner}:registry=${registry}\` in your npm config.`
153
- ].join("\n");
154
- const comments = await guardedApi(() => ghApi(`/repos/${repository}/issues/${pr.number}/comments?per_page=100`), "Could not list PR comments");
155
- const previous = Array.isArray(comments)
156
- ? comments.find((comment) => {
157
- const record = asRecord(comment);
158
- return typeof record.body === "string"
159
- && record.body.includes(COMMENT_MARKER)
160
- && asRecord(record.user).login === "github-actions[bot]";
161
- })
162
- : undefined;
163
- if (previous) {
164
- await guardedApi(() => ghApi(`/repos/${repository}/issues/comments/${String(asRecord(previous).id)}`, { method: "PATCH", body: JSON.stringify({ body }) }), "Failed to update the PR preview comment");
165
- }
166
- else {
167
- await guardedApi(() => ghApi(`/repos/${repository}/issues/${pr.number}/comments`, { method: "POST", body: JSON.stringify({ body }) }), "Failed to create the PR preview comment");
168
- }
169
- }
170
- options.io.stdout(`GitHub Packages ${mode} publish complete: ${spec} (${releaseContext.distTag}).\n`);
171
- }
172
- finally {
173
- rmSync(stagingDir, { force: true, recursive: true });
174
- }
175
- }
176
- export async function publishNpmPackage(options) {
177
- const { packageDir, manifest } = await readPackageContext(options.cwd, options.packagePath);
178
- assertPublicPackage(manifest);
179
- const spec = `${manifest.name}@${manifest.version}`;
180
- const auth = await prepareNpmPublishAuth(options);
181
- try {
182
- const npm = (args, runOptions = {}) => spawnSync("npm", args, {
183
- cwd: packageDir,
184
- encoding: "utf8",
185
- stdio: runOptions.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
186
- env: {
187
- ...auth.env,
188
- NPM_CONFIG_CACHE: options.env.NPM_CONFIG_CACHE ?? join(options.cwd, ".async", "npm-cache")
189
- }
190
- });
191
- const ensurePublicAccess = () => {
192
- if (!auth.hasTraditionalAuth) {
193
- options.io.stdout("Skipping npm access public check because no npm token is configured; trusted publishing only authenticates npm publish.\n");
194
- return;
195
- }
196
- options.io.stdout(`Ensuring ${manifest.name} is public on npm.\n`);
197
- const access = npm(["access", "set", "status=public", manifest.name, "--registry", NPM_REGISTRY], { inherit: true });
198
- if (access.status !== 0) {
199
- throw new Error(`Failed to set npm package access for ${manifest.name}.`);
200
- }
201
- };
202
- const view = npm(["view", spec, "version", "--registry", NPM_REGISTRY]);
203
- if (view.status === 0 && view.stdout.trim() === manifest.version) {
204
- ensurePublicAccess();
205
- options.io.stdout(`${spec} is already published to npm; skipping.\n`);
206
- return;
207
- }
208
- if (!isMissingVersion(view)) {
209
- options.io.stderr(npmOutput(view).slice(0, 2000));
210
- throw new Error(`Could not determine whether ${spec} exists on npm; refusing to guess.`);
211
- }
212
- options.io.stdout(`Publishing ${spec} to npm with provenance.\n`);
213
- const publish = npm(["publish", "--access", "public", "--registry", NPM_REGISTRY, "--provenance"], { inherit: true });
214
- if (publish.status !== 0) {
215
- throw new Error(`Failed to publish ${spec} to npm.`);
216
- }
217
- ensurePublicAccess();
218
- }
219
- finally {
220
- if (auth.cleanupDir)
221
- rmSync(auth.cleanupDir, { force: true, recursive: true });
222
- }
223
- }
224
- export async function ensureGitHubRelease(options) {
225
- const { manifest } = await readPackageContext(options.cwd, options.packagePath);
226
- assertPublicPackage(manifest);
227
- if (!SEMVER_PATTERN.test(manifest.version)) {
228
- throw new Error(`${manifest.name} version must be simple semver for release creation. Found: ${manifest.version}`);
229
- }
230
- const releaseNotes = await readChangelogReleaseNotes(options.cwd);
231
- const currentReleaseNotes = releaseNotes.get(`v${manifest.version}`);
232
- if (!currentReleaseNotes) {
233
- throw new Error(`CHANGELOG.md has no parseable, non-empty "## ${manifest.version} - <date>" entry.`);
234
- }
235
- const repository = options.env.GITHUB_REPOSITORY ?? packageRepositoryName(manifest);
236
- if (!repository) {
237
- throw new Error("Set GITHUB_REPOSITORY or package.json repository so release creation can resolve GitHub state.");
238
- }
239
- const token = options.env.GITHUB_TOKEN;
240
- if (!token) {
241
- throw new Error("Set GITHUB_TOKEN with contents:write so release creation can create tags and GitHub Releases.");
242
- }
243
- const targetSha = options.env.GITHUB_SHA;
244
- if (!targetSha || !SHA_PATTERN.test(targetSha)) {
245
- throw new Error("Set GITHUB_SHA to the commit that the release tag should point at.");
246
- }
247
- const apiBase = (options.env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
248
- const tagName = `v${manifest.version}`;
249
- const encodedTagName = encodeURIComponent(tagName);
250
- const ghApi = async (path, init = {}, allowMissing = false) => {
251
- const response = await fetch(`${apiBase}${path}`, {
252
- ...init,
253
- headers: {
254
- authorization: `Bearer ${token}`,
255
- accept: "application/vnd.github+json",
256
- "content-type": "application/json",
257
- "x-github-api-version": "2022-11-28",
258
- ...init.headers
259
- }
260
- });
261
- if (allowMissing && response.status === 404)
262
- return undefined;
263
- if (!response.ok) {
264
- const text = await response.text();
265
- throw new Error(`GitHub API ${init.method ?? "GET"} ${path} failed with ${response.status}: ${text.slice(0, 500)}`);
266
- }
267
- return response.status === 204 ? null : response.json();
268
- };
269
- const resolveRefCommit = async (ref) => {
270
- const object = asRecord(asRecord(ref).object);
271
- const type = object.type;
272
- const sha = object.sha;
273
- if (type === "commit" && typeof sha === "string")
274
- return sha;
275
- if (type === "tag" && typeof sha === "string") {
276
- const tag = await ghApi(`/repos/${repository}/git/tags/${sha}`);
277
- const target = asRecord(asRecord(tag).object);
278
- if (target.type === "commit" && typeof target.sha === "string")
279
- return target.sha;
280
- }
281
- throw new Error(`Release tag ${tagName} points to an unsupported Git object.`);
282
- };
283
- const existingRef = await ghApi(`/repos/${repository}/git/ref/tags/${encodedTagName}`, {}, true);
284
- if (existingRef) {
285
- const existingSha = await resolveRefCommit(existingRef);
286
- if (existingSha !== targetSha) {
287
- throw new Error(`Release tag ${tagName} already points to ${existingSha}; refusing to move it to ${targetSha}.`);
288
- }
289
- options.io.stdout(`OK Git tag: ${tagName} -> ${targetSha}\n`);
290
- }
291
- else {
292
- await ghApi(`/repos/${repository}/git/refs`, {
293
- method: "POST",
294
- body: JSON.stringify({ ref: `refs/tags/${tagName}`, sha: targetSha })
295
- });
296
- options.io.stdout(`Created Git tag ${tagName} -> ${targetSha}.\n`);
297
- }
298
- const existingRelease = await ghApi(`/repos/${repository}/releases/tags/${encodedTagName}`, {}, true);
299
- if (existingRelease) {
300
- options.io.stdout(`OK GitHub Release: ${repository}@${tagName}\n`);
301
- }
302
- else {
303
- await ghApi(`/repos/${repository}/releases`, {
304
- method: "POST",
305
- body: JSON.stringify({
306
- tag_name: tagName,
307
- target_commitish: targetSha,
308
- name: `${manifest.name} ${tagName}`,
309
- body: currentReleaseNotes.releaseBody,
310
- draft: false,
311
- prerelease: manifest.version.includes("-")
312
- })
313
- });
314
- options.io.stdout(`Created GitHub Release ${repository}@${tagName}.\n`);
315
- }
316
- await syncGitHubReleaseNotes(repository, releaseNotes, token, apiBase, options.io);
317
- }
318
- export async function runReleaseDoctor(options) {
319
- const { manifest } = await readPackageContext(options.cwd, options.packagePath);
320
- assertPublicPackage(manifest);
321
- if (!SEMVER_PATTERN.test(manifest.version)) {
322
- throw new Error(`${manifest.name} version must be simple semver for release doctor. Found: ${manifest.version}`);
323
- }
324
- const repository = options.env.GITHUB_REPOSITORY ?? packageRepositoryName(manifest);
325
- const owner = (options.env.GITHUB_REPOSITORY_OWNER ?? repository?.split("/")[0] ?? "").toLowerCase();
326
- if (!repository || !owner) {
327
- throw new Error("Set GITHUB_REPOSITORY or package.json repository so release doctor can resolve GitHub state.");
328
- }
329
- const npmPackage = `${manifest.name}@${manifest.version}`;
330
- const githubPackage = `${githubMirrorPackageName(manifest.name, owner)}@${manifest.version}`;
331
- assertReleaseTagMatches(manifest, options.env);
332
- await assertRegistryVersion(npmPackage, NPM_REGISTRY, options, "npm");
333
- await assertRegistryVersion(githubPackage, GITHUB_REGISTRY, options, "GitHub Packages");
334
- await assertGitHubRelease(repository, manifest.version, options);
335
- await assertGitHubReleaseNotes(repository, await readChangelogReleaseNotes(options.cwd), options);
336
- options.io.stdout(`Release doctor passed for ${manifest.name}@${manifest.version}.\n`);
337
- }
338
- async function resolveGitHubPublishContext(mode, options) {
339
- if (mode === "release") {
340
- if (!SEMVER_PATTERN.test(options.manifest.version)) {
341
- throw new Error(`${options.manifest.name} version must be simple semver for a stable mirror. Found: ${options.manifest.version}`);
342
- }
343
- assertReleaseTagMatches(options.manifest, options.env);
344
- return { version: options.manifest.version, distTag: "latest" };
345
- }
346
- if (mode === "main") {
347
- const sha = options.env.GITHUB_SHA;
348
- if (!sha || !SHA_PATTERN.test(sha)) {
349
- throw new Error("main mode needs GITHUB_SHA (40-char lowercase hex). Run it from the generated workflow on a push to main.");
350
- }
351
- return { version: `0.0.0-main.sha.${sha}`, distTag: "main" };
352
- }
353
- const event = await readGitHubEvent(options.env, "pr mode needs GITHUB_EVENT_PATH with a pull_request payload. Run it from the generated workflow on a pull request.");
354
- const pullRequest = asRecord(event).pull_request;
355
- const number = Number(asRecord(pullRequest).number ?? asRecord(event).number);
356
- const head = asRecord(asRecord(pullRequest).head);
357
- const headSha = head.sha;
358
- const headRepo = asRecord(head.repo).full_name;
359
- if (!Number.isInteger(number) || number <= 0 || typeof headSha !== "string" || !SHA_PATTERN.test(headSha)) {
360
- throw new Error("pr mode could not read a positive number and head.sha from the pull_request payload.");
361
- }
362
- if (headRepo !== options.repository) {
363
- options.io.stdout(`Skipping preview publish: PR #${number} head is ${typeof headRepo === "string" ? headRepo : "a deleted repo"}, not ${options.repository}.\n`);
364
- return Promise.reject(new LifecycleSkip());
365
- }
366
- return {
367
- version: `0.0.0-pr.${number}.sha.${headSha}`,
368
- distTag: `pr-${number}`,
369
- prContext: { number, headSha }
370
- };
371
- }
372
- export async function runLifecycleCli(action, io) {
373
- try {
374
- await action();
375
- return 0;
376
- }
377
- catch (error) {
378
- if (error instanceof LifecycleSkip)
379
- return 0;
380
- io.stderr(`::error::${error instanceof Error ? error.message : String(error)}\n`);
381
- return 1;
382
- }
383
- }
384
- async function readPackageContext(cwd, packagePath) {
385
- const packageDir = resolve(cwd, packagePath);
386
- const relativePackageDir = relative(cwd, packageDir);
387
- if (relativePackageDir === ".." || relativePackageDir.startsWith("../") || relativePackageDir.startsWith("..\\") || isAbsolute(relativePackageDir)) {
388
- throw new Error(`Package path "${packagePath}" must stay inside ${cwd}.`);
389
- }
390
- const manifest = JSON.parse(await readFile(join(packageDir, "package.json"), "utf8"));
391
- if (typeof manifest.name !== "string" || typeof manifest.version !== "string") {
392
- throw new Error(`${relativeLabel(cwd, packageDir)}/package.json must include name and version.`);
393
- }
394
- return { packageDir, manifest };
395
- }
396
- function assertPublicPackage(manifest) {
397
- if (manifest.private) {
398
- throw new Error(`${manifest.name} is marked private; refusing to publish.`);
399
- }
400
- }
401
- function githubMirrorPackageName(packageName, owner) {
402
- const leaf = packageName.startsWith("@") ? packageName.split("/")[1] : packageName;
403
- const mirrorName = `@${owner}/${leaf}`;
404
- if (!NAME_PATTERN.test(mirrorName)) {
405
- throw new Error(`GitHub Packages package name must be a simple lowercase scoped npm name. Found: ${mirrorName}`);
406
- }
407
- return mirrorName;
408
- }
409
- function normalizeRegistry(registry) {
410
- const normalized = registry.trim().replace(/\/$/, "");
411
- try {
412
- const url = new URL(normalized);
413
- if (url.protocol !== "https:" && url.protocol !== "http:") {
414
- throw new Error("unsupported protocol");
415
- }
416
- }
417
- catch {
418
- throw new Error(`GitHub package registry must be an HTTP(S) URL. Found: ${registry}`);
419
- }
420
- return normalized;
421
- }
422
- function packageRepositoryName(manifest) {
423
- const repository = manifest.repository;
424
- const url = typeof repository === "string"
425
- ? repository
426
- : typeof repository === "object" && repository !== null && "url" in repository && typeof repository.url === "string"
427
- ? repository.url
428
- : undefined;
429
- const match = url?.match(/github\.com[:/]([^/\s]+)\/([^/\s.]+)(?:\.git)?/i);
430
- return match ? `${match[1]}/${match[2]}` : undefined;
431
- }
432
- async function readGitHubEvent(env, missingMessage) {
433
- const eventPath = env.GITHUB_EVENT_PATH;
434
- if (!eventPath || !existsSync(eventPath)) {
435
- throw new Error(missingMessage);
436
- }
437
- return JSON.parse(await readFile(eventPath, "utf8"));
438
- }
439
- async function prepareNpmPublishAuth(options) {
440
- const token = npmAuthToken(options.env);
441
- const env = { ...options.env };
442
- if (!token) {
443
- if (!env.NODE_AUTH_TOKEN)
444
- delete env.NODE_AUTH_TOKEN;
445
- if (!env.NPM_TOKEN)
446
- delete env.NPM_TOKEN;
447
- return { env, hasTraditionalAuth: Boolean(env.NPM_CONFIG_USERCONFIG) };
448
- }
449
- const cleanupDir = await mkdtemp(join(tmpdir(), "async-pipeline-npm-publish-"));
450
- const registryUrl = new URL(NPM_REGISTRY);
451
- const registryAuthPath = `${registryUrl.host}${registryUrl.pathname.replace(/\/$/, "")}`;
452
- const userconfig = join(cleanupDir, ".npmrc");
453
- await writeFile(userconfig, `//${registryAuthPath}/:_authToken=${token}\n`, "utf8");
454
- await chmod(userconfig, 0o600);
455
- delete env.NPM_TOKEN;
456
- delete env.NODE_AUTH_TOKEN;
457
- env.NPM_CONFIG_USERCONFIG = userconfig;
458
- return { env, hasTraditionalAuth: true, cleanupDir };
459
- }
460
- function npmAuthToken(env) {
461
- const token = env.NPM_TOKEN ?? env.NODE_AUTH_TOKEN;
462
- return token && token.trim().length > 0 ? token : undefined;
463
- }
464
- async function assertRegistryVersion(spec, registry, options, label) {
465
- let cleanupDir;
466
- let userconfig;
467
- if (registry === GITHUB_REGISTRY) {
468
- const token = options.env.GITHUB_TOKEN ?? options.env.NODE_AUTH_TOKEN;
469
- if (!token) {
470
- throw new Error("Set GITHUB_TOKEN or NODE_AUTH_TOKEN so release doctor can verify GitHub Packages.");
471
- }
472
- const scopePart = spec.startsWith("@") ? spec.split("/")[0] : undefined;
473
- const scope = scopePart?.slice(1);
474
- if (!scope) {
475
- throw new Error(`Cannot infer GitHub Packages scope from ${spec}.`);
476
- }
477
- cleanupDir = await mkdtemp(join(tmpdir(), "async-pipeline-release-doctor-"));
478
- const registryUrl = new URL(GITHUB_REGISTRY);
479
- const registryAuthPath = `${registryUrl.host}${registryUrl.pathname.replace(/\/$/, "")}`;
480
- userconfig = join(cleanupDir, ".npmrc");
481
- await writeFile(userconfig, `@${scope}:registry=${GITHUB_REGISTRY}\n//${registryAuthPath}/:_authToken=${token}\n`, "utf8");
482
- await chmod(userconfig, 0o600);
483
- }
484
- let lastView;
485
- try {
486
- const attempts = positiveInt(options.env.ASYNC_PIPELINE_RELEASE_DOCTOR_REGISTRY_ATTEMPTS, RELEASE_DOCTOR_REGISTRY_ATTEMPTS);
487
- const delayMs = positiveInt(options.env.ASYNC_PIPELINE_RELEASE_DOCTOR_REGISTRY_RETRY_DELAY_MS, RELEASE_DOCTOR_REGISTRY_RETRY_DELAY_MS);
488
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
489
- const view = spawnSync("npm", ["view", spec, "version", "--registry", registry], {
490
- cwd: options.cwd,
491
- encoding: "utf8",
492
- stdio: ["ignore", "pipe", "pipe"],
493
- env: {
494
- ...options.env,
495
- ...(userconfig ? { NPM_CONFIG_USERCONFIG: userconfig } : {}),
496
- NPM_CONFIG_CACHE: options.env.NPM_CONFIG_CACHE ?? join(options.cwd, ".async", "npm-cache")
497
- }
498
- });
499
- lastView = view;
500
- if (view.status === 0) {
501
- options.io.stdout(`OK ${label}: ${spec}\n`);
502
- return;
503
- }
504
- if (!isMissingVersion(view)) {
505
- options.io.stderr(npmOutput(view).slice(0, 2000));
506
- throw new Error(`Release doctor could not verify ${spec} on ${label}.`);
507
- }
508
- if (attempt < attempts) {
509
- options.io.stdout(`Waiting for ${label} to expose ${spec} (${attempt}/${attempts}).\n`);
510
- await sleep(delayMs);
511
- }
512
- }
513
- if (lastView)
514
- options.io.stderr(npmOutput(lastView).slice(0, 2000));
515
- throw new Error(`Release doctor could not find ${spec} on ${label}.`);
516
- }
517
- finally {
518
- if (cleanupDir)
519
- rmSync(cleanupDir, { force: true, recursive: true });
520
- }
521
- }
522
- async function assertGitHubRelease(repository, version, options) {
523
- const token = options.env.GITHUB_TOKEN;
524
- if (!token) {
525
- throw new Error("Set GITHUB_TOKEN so release doctor can verify the GitHub Release.");
526
- }
527
- const apiBase = (options.env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
528
- const response = await fetch(`${apiBase}/repos/${repository}/releases/tags/v${version}`, {
529
- headers: {
530
- authorization: `Bearer ${token}`,
531
- accept: "application/vnd.github+json",
532
- "x-github-api-version": "2022-11-28"
533
- }
534
- });
535
- if (!response.ok) {
536
- const text = await response.text();
537
- throw new Error(`Release doctor could not verify GitHub Release v${version}: ${response.status} ${text.slice(0, 500)}`);
538
- }
539
- options.io.stdout(`OK GitHub Release: ${repository}@v${version}\n`);
540
- }
541
- async function assertGitHubReleaseNotes(repository, releaseNotes, options) {
542
- const token = options.env.GITHUB_TOKEN;
543
- if (!token) {
544
- throw new Error("Set GITHUB_TOKEN so release doctor can verify GitHub Release descriptions.");
545
- }
546
- const apiBase = (options.env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
547
- const releases = await listGitHubReleases(repository, token, apiBase);
548
- const failures = releaseNoteFailures(releases, releaseNotes);
549
- if (failures.length > 0) {
550
- throw new Error(`GitHub Release descriptions do not match CHANGELOG.md: ${failures.join("; ")}`);
551
- }
552
- options.io.stdout(`OK GitHub Release descriptions: ${releases.filter((release) => semverTagVersion(release.tagName)).length} semver release(s) match CHANGELOG.md\n`);
553
- }
554
- async function syncGitHubReleaseNotes(repository, releaseNotes, token, apiBase, io) {
555
- const releases = await listGitHubReleases(repository, token, apiBase);
556
- const failures = releaseNoteMissingFailures(releases, releaseNotes);
557
- if (failures.length > 0) {
558
- throw new Error(`Cannot sync GitHub Release descriptions from CHANGELOG.md: ${failures.join("; ")}`);
559
- }
560
- let updated = 0;
561
- for (const release of releases) {
562
- const note = releaseNotes.get(release.tagName);
563
- if (!note || normalizeReleaseBody(release.body) === normalizeReleaseBody(note.releaseBody))
564
- continue;
565
- if (release.id === undefined) {
566
- throw new Error(`Cannot update GitHub Release ${release.tagName}: release id is missing from the GitHub API response.`);
567
- }
568
- await githubApi(apiBase, token, `/repos/${repository}/releases/${encodeURIComponent(String(release.id))}`, {
569
- method: "PATCH",
570
- body: JSON.stringify({ body: note.releaseBody })
571
- });
572
- updated += 1;
573
- io.stdout(`Updated GitHub Release notes: ${repository}@${release.tagName}\n`);
574
- }
575
- if (updated === 0) {
576
- io.stdout("GitHub Release descriptions already match CHANGELOG.md.\n");
577
- }
578
- }
579
- function releaseNoteFailures(releases, releaseNotes) {
580
- const failures = [];
581
- for (const release of releases) {
582
- if (!semverTagVersion(release.tagName))
583
- continue;
584
- const note = releaseNotes.get(release.tagName);
585
- if (!note) {
586
- failures.push(`${release.tagName} has no parseable, non-empty CHANGELOG.md section`);
587
- }
588
- else if (normalizeReleaseBody(release.body) !== normalizeReleaseBody(note.releaseBody)) {
589
- failures.push(`${release.tagName} body differs`);
590
- }
591
- }
592
- return failures;
593
- }
594
- function releaseNoteMissingFailures(releases, releaseNotes) {
595
- const failures = [];
596
- for (const release of releases) {
597
- if (!semverTagVersion(release.tagName))
598
- continue;
599
- if (!releaseNotes.has(release.tagName)) {
600
- failures.push(`${release.tagName} has no parseable, non-empty CHANGELOG.md section`);
601
- }
602
- }
603
- return failures;
604
- }
605
- async function readChangelogReleaseNotes(cwd) {
606
- const changelog = await readFile(join(cwd, "CHANGELOG.md"), "utf8");
607
- const allHeadingPattern = /^##[ \t]+(.+?)[ \t]*$/gm;
608
- const headingPattern = /^##[ \t]+(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)[ \t]+-[ \t]+(.+?)[ \t]*$/gm;
609
- const allHeadings = [...changelog.matchAll(allHeadingPattern)];
610
- const headings = [...changelog.matchAll(headingPattern)];
611
- const notes = new Map();
612
- for (const heading of headings) {
613
- const version = heading?.[1];
614
- const rawDate = heading?.[2];
615
- if (!version || !rawDate || heading.index === undefined)
616
- continue;
617
- const date = rawDate.trim();
618
- const start = heading.index + heading[0].length;
619
- const nextHeading = allHeadings.find((candidate) => (candidate.index ?? -1) > heading.index);
620
- const end = nextHeading?.index ?? changelog.length;
621
- const body = changelog.slice(start, end).trim();
622
- if (!body)
623
- continue;
624
- const tagName = `v${version}`;
625
- notes.set(tagName, {
626
- tagName,
627
- version,
628
- date,
629
- body,
630
- releaseBody: renderReleaseBody(version, date, body)
631
- });
632
- }
633
- return notes;
634
- }
635
- function renderReleaseBody(version, date, body) {
636
- return [
637
- `Release notes from \`CHANGELOG.md\` for ${version} (${date}).`,
638
- "",
639
- body,
640
- "",
641
- "---",
642
- `Source: \`CHANGELOG.md\` in tag \`v${version}\`.`,
643
- ""
644
- ].join("\n");
645
- }
646
- function normalizeReleaseBody(body) {
647
- return body.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trimEnd();
648
- }
649
- async function listGitHubReleases(repository, token, apiBase) {
650
- const releases = [];
651
- for (let page = 1;; page += 1) {
652
- const response = await githubApi(apiBase, token, `/repos/${repository}/releases?per_page=100&page=${page}`);
653
- const pageReleases = Array.isArray(response) ? response : [];
654
- for (const release of pageReleases) {
655
- const record = asRecord(release);
656
- const tagName = record.tag_name;
657
- if (typeof tagName !== "string")
658
- continue;
659
- releases.push({
660
- id: typeof record.id === "number" || typeof record.id === "string" ? record.id : undefined,
661
- tagName,
662
- body: typeof record.body === "string" ? record.body : ""
663
- });
664
- }
665
- if (pageReleases.length < 100)
666
- return releases;
667
- }
668
- }
669
- async function githubApi(apiBase, token, path, init = {}, allowMissing = false) {
670
- const response = await fetch(`${apiBase}${path}`, {
671
- ...init,
672
- headers: {
673
- authorization: `Bearer ${token}`,
674
- accept: "application/vnd.github+json",
675
- "content-type": "application/json",
676
- "x-github-api-version": "2022-11-28",
677
- ...init.headers
678
- }
679
- });
680
- if (allowMissing && response.status === 404)
681
- return undefined;
682
- if (!response.ok) {
683
- const text = await response.text();
684
- throw new Error(`GitHub API ${init.method ?? "GET"} ${path} failed with ${response.status}: ${text.slice(0, 500)}`);
685
- }
686
- return response.status === 204 ? null : response.json();
687
- }
688
- function semverTagVersion(tagName) {
689
- const match = /^v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(tagName);
690
- return match ? match[1] : undefined;
691
- }
692
- function assertReleaseTagMatches(manifest, env) {
693
- const refTag = env.GITHUB_REF?.startsWith("refs/tags/") ? env.GITHUB_REF.slice("refs/tags/".length) : undefined;
694
- if (refTag && refTag.replace(/^v/, "") !== manifest.version) {
695
- throw new Error(`Release tag ${refTag} does not match ${manifest.name} version ${manifest.version}. Publish from a matching tag such as v${manifest.version}.`);
696
- }
697
- if (env.GITHUB_EVENT_NAME === "release") {
698
- const eventPath = env.GITHUB_EVENT_PATH;
699
- if (!eventPath || !existsSync(eventPath)) {
700
- throw new Error("release events need GITHUB_EVENT_PATH to verify the release tag matches package.json.");
701
- }
702
- const event = JSON.parse(readFileSync(eventPath, "utf8"));
703
- const tagName = asRecord(asRecord(event).release).tag_name;
704
- if (typeof tagName !== "string" || tagName.length === 0) {
705
- throw new Error("Release event payload did not include release.tag_name.");
706
- }
707
- if (tagName.replace(/^v/, "") !== manifest.version) {
708
- throw new Error(`Release tag ${tagName} does not match ${manifest.name} version ${manifest.version}. Publish from a matching tag such as v${manifest.version}.`);
709
- }
710
- }
711
- }
712
- function positiveInt(value, fallback) {
713
- if (!value)
714
- return fallback;
715
- const parsed = Number(value);
716
- return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
717
- }
718
- function sleep(ms) {
719
- return new Promise((resolveSleep) => {
720
- setTimeout(resolveSleep, ms);
721
- });
722
- }
723
- async function guardedApi(operation, message) {
724
- try {
725
- return await operation();
726
- }
727
- catch (error) {
728
- throw new Error(`${message}: ${error instanceof Error ? error.message : String(error)}`);
729
- }
730
- }
731
- function isMissingVersion(result) {
732
- return result.status !== 0 && /(^|[\s])(E404|404)([\s]|$)|not found/i.test(npmOutput(result));
733
- }
734
- function npmOutput(result) {
735
- return `${result.stdout ?? ""}${result.stderr ?? ""}`;
736
- }
737
- function relativeLabel(cwd, target) {
738
- const path = relative(cwd, target);
739
- return path || ".";
740
- }
741
- function asRecord(value) {
742
- return typeof value === "object" && value !== null ? value : {};
743
- }
744
- class LifecycleSkip extends Error {
745
- }
746
- //# sourceMappingURL=package-lifecycle.js.map
1
+ const message = "Package lifecycle commands moved out of the @async/pipeline npm tarball. Use generated workflows with async/actions publish, preview, and pages steps.";
2
+ export function publishGitHubPackage() { throw new Error(message); }
3
+ export function publishNpmPackage() { throw new Error(message); }
4
+ export function ensureGitHubRelease() { throw new Error(message); }
5
+ export function runReleaseDoctor() { throw new Error(message); }
6
+ export async function runLifecycleCli(_action, io) { io?.stderr?.(`${message}\n`); return 1; }