From c239ad0ea125b2f656a68648b6ebffc8a05166aa Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:43:08 +0300 Subject: [PATCH] fix(release): wait for a new draft to show in the releases list The first Release dry run (run 43184427067) created the draft v0.8.0-dryrun.171 and failed one second later with "expected one release v0.8.0-dryrun.171, found 0": GitHub's releases list shows a just-created draft only after a few seconds, and a draft whose tag is not pushed has no releases/tags/ URL. createDraft now waits until the list shows it (1, 2, 4, then 8 s per try). The cleanup step found and deleted the draft 1.6 s later. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .../cli/test/release-bundle-publish.test.ts | 43 ++++++++++++++++++- scripts/publish-ci.ts | 22 ++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/release-bundle-publish.test.ts b/packages/cli/test/release-bundle-publish.test.ts index 94eef4cb..b60c2860 100644 --- a/packages/cli/test/release-bundle-publish.test.ts +++ b/packages/cli/test/release-bundle-publish.test.ts @@ -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"; @@ -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]); + }); +}); diff --git a/scripts/publish-ci.ts b/scripts/publish-ci.ts index 2a188c0b..b2d9469d 100644 --- a/scripts/publish-ci.ts +++ b/scripts/publish-ci.ts @@ -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), @@ -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"]),