Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion packages/cli/test/release-bundle-publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { createHash, generateKeyPairSync, type KeyObject, sign } from "node:cryp
import { readFileSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { loadBundle, publicKeysFor, RELEASE_KEY } from "../../../scripts/publish-ci.ts";
import {
loadBundle,
publicKeysFor,
RELEASE_KEY,
waitForDraft,
} from "../../../scripts/publish-ci.ts";
import { buildReleaseBundle, writeReleaseBundle } from "../../../scripts/release-bundle/build.ts";
import { readBuildOutput, readDeploySecrets } from "../../../scripts/release-bundle/read-build.ts";
import { releaseAssetNames } from "../../../scripts/release-bundle/statement.ts";
Expand Down Expand Up @@ -102,3 +107,39 @@ describe("publish-ci publicKeysFor", () => {
expect(() => publicKeysFor({ dryRun: true, dryRunPublicKey: k.raw })).toThrow(/GITHUB_RUN_ID/);
});
});

describe("publish-ci waitForDraft", () => {
it("returns at once when the draft is listed", () => {
const slept: number[] = [];
waitForDraft(
"v1.0.0",
() => 1,
(ms) => slept.push(ms),
);
expect(slept).toEqual([]);
});

it("waits with backoff until the list shows the draft (the dry run of 2026-11-12)", () => {
const seen = [0, 0, 0, 1];
const slept: number[] = [];
waitForDraft(
"v0.8.0-dryrun.171",
() => seen.shift() ?? 1,
(ms) => slept.push(ms),
);
expect(slept).toEqual([1000, 2000, 4000]);
});

it("gives up after its tries with the tag in the error", () => {
const slept: number[] = [];
expect(() =>
waitForDraft(
"v1.0.0",
() => 0,
(ms) => slept.push(ms),
4,
),
).toThrow("the draft v1.0.0 did not appear in the releases list after 4 tries");
expect(slept).toEqual([1000, 2000, 4000]);
});
});
22 changes: 22 additions & 0 deletions scripts/publish-ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,27 @@ async function npmPublish(pkgs: readonly Pkg[], publishes: readonly NpmPublish[]
);
}

/**
* GitHub's releases list shows a just-created draft only after a few seconds, and every later step
* finds the draft through that list (a draft whose tag is not pushed has no `releases/tags/` URL).
* Waits until `count()` sees it: 1, 2, 4, then 8 s per try, about a minute in all.
*/
export function waitForDraft(
tag: string,
count: () => number,
sleep: (ms: number) => void = sleepSync,
tries = 10,
): void {
for (let i = 0; count() === 0; i++) {
if (i + 1 >= tries)
throw new Error(`the draft ${tag} did not appear in the releases list after ${tries} tries`);
sleep(1000 * 2 ** Math.min(i, 3));
}
}
function sleepSync(ms: number): void {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}

function releasesOf(tag: string): ReleaseView[] {
const pages = JSON.parse(
sh("gh", ["api", "--paginate", "--slurp", `repos/${REPO}/releases?per_page=100`], true),
Expand Down Expand Up @@ -218,6 +239,7 @@ function githubPort(pkgs: readonly Pkg[], version: string, dryRun: boolean): Pub
...(o.prerelease ? ["--prerelease"] : []),
...(o.target ? ["--target", o.target] : ["--verify-tag"]),
]);
waitForDraft(tag, () => releasesOf(tag).length);
},
upload: (tag, assets) =>
sh("gh", ["release", "upload", tag, ...assets.map((a) => a.file), "--clobber"]),
Expand Down
Loading