From 3a2991d1e8f23b4191d7b432de83ea2f28aa9885 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 3 Nov 2026 00:01:34 +0100 Subject: [PATCH 1/2] fix(release): build before publish so bin survives, tag versions already on npm npm publish validated bin before prepublishOnly built dist/, so CI would publish a tarball without the changeset command. The publish script also skipped tagging any version already on npm, so the hand-published 0.1.0 never got a tag or GitHub release. Tags are lightweight now so they do not carry the test-fixture git identity. --- .github/workflows/release.yml | 3 +++ scripts/publish.ts | 25 ++++++++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fc3c70..9a9e45e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,9 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 - run: bun install --frozen-lockfile + # npm publish drops a `bin` whose file is missing before prepublishOnly runs, so dist/ + # must exist up front or the tarball ships without the `changeset` command. + - run: bun run build # With changesets pending, opens or updates the "Version Packages" PR; once it is # merged, publishes. npm authenticates through trusted publishing (OIDC). - uses: changesets/action@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2 diff --git a/scripts/publish.ts b/scripts/publish.ts index 1d74530..804b21a 100644 --- a/scripts/publish.ts +++ b/scripts/publish.ts @@ -1,7 +1,7 @@ /** * The `publish-script` of `.github/workflows/release.yml`: publishes the current version when npm - * does not have it yet, tags it, and reports the tag through `CHANGESETS_OUTPUT` so - * changesets/action pushes it and cuts the GitHub release. + * does not have it yet, tags it when the tag is missing (also for a version published by hand), and + * reports the tag through `CHANGESETS_OUTPUT` so changesets/action pushes it and cuts the GitHub release. */ import { NodeRuntime, NodeServices } from "@effect/platform-node" import { Config, Console, Effect, FileSystem, Option, Schema } from "effect" @@ -18,17 +18,24 @@ const program = Effect.gen(function* () { const { name, version } = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest))( yield* fs.readFileString("package.json"), ) + const tag = `${name}@${version}` const published = yield* run("npm", ["view", `${name}@${version}`, "version"], ".") if (published.stdout.trim() === version) { - return yield* Console.log(`${name}@${version} is already on npm.`) + yield* Console.log(`${tag} is already on npm.`) + } else { + const publish = yield* run("npm", ["publish", "--provenance", "--access", "public"], ".") + yield* Console.log(publish.stdout) + if (publish.exitCode !== 0) { + return yield* new ReleaseError({ message: `npm publish failed:\n${publish.stderr}` }) + } } - const publish = yield* run("npm", ["publish", "--provenance", "--access", "public"], ".") - yield* Console.log(publish.stdout) - if (publish.exitCode !== 0) { - return yield* new ReleaseError({ message: `npm publish failed:\n${publish.stderr}` }) + // Exit code 2 means the remote has no such tag. + const remoteTag = yield* run("git", ["ls-remote", "--exit-code", "--tags", "origin", `refs/tags/${tag}`], ".") + if (remoteTag.exitCode === 0) return yield* Console.log(`${tag} is already tagged.`) + if (remoteTag.exitCode !== 2) { + return yield* new ReleaseError({ message: `git ls-remote failed:\n${remoteTag.stderr}` }) } - const tag = `${name}@${version}` - const tagged = yield* run("git", ["tag", "-a", tag, "-m", tag], ".") + const tagged = yield* run("git", ["tag", tag], ".") if (tagged.exitCode !== 0) return yield* new ReleaseError({ message: `git tag failed:\n${tagged.stderr}` }) yield* Console.log(`New tag: ${tag}`) const output = yield* Config.option(Config.String("CHANGESETS_OUTPUT")) From 7944a78564aff7cdbea1b3110f8ac2cc0e6a686d Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 3 Nov 2026 00:10:01 +0100 Subject: [PATCH 2/2] fix(release): report a fresh publish even when tagged, reuse an unpushed local tag --- scripts/publish.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/scripts/publish.ts b/scripts/publish.ts index 804b21a..1ac5132 100644 --- a/scripts/publish.ts +++ b/scripts/publish.ts @@ -20,24 +20,30 @@ const program = Effect.gen(function* () { ) const tag = `${name}@${version}` const published = yield* run("npm", ["view", `${name}@${version}`, "version"], ".") - if (published.stdout.trim() === version) { - yield* Console.log(`${tag} is already on npm.`) - } else { + const publishNow = published.stdout.trim() !== version + if (publishNow) { const publish = yield* run("npm", ["publish", "--provenance", "--access", "public"], ".") yield* Console.log(publish.stdout) if (publish.exitCode !== 0) { return yield* new ReleaseError({ message: `npm publish failed:\n${publish.stderr}` }) } + } else { + yield* Console.log(`${tag} is already on npm.`) } // Exit code 2 means the remote has no such tag. const remoteTag = yield* run("git", ["ls-remote", "--exit-code", "--tags", "origin", `refs/tags/${tag}`], ".") - if (remoteTag.exitCode === 0) return yield* Console.log(`${tag} is already tagged.`) - if (remoteTag.exitCode !== 2) { + if (remoteTag.exitCode !== 0 && remoteTag.exitCode !== 2) { return yield* new ReleaseError({ message: `git ls-remote failed:\n${remoteTag.stderr}` }) } - const tagged = yield* run("git", ["tag", tag], ".") - if (tagged.exitCode !== 0) return yield* new ReleaseError({ message: `git tag failed:\n${tagged.stderr}` }) - yield* Console.log(`New tag: ${tag}`) + const tagMissing = remoteTag.exitCode === 2 + if (!publishNow && !tagMissing) return yield* Console.log(`${tag} is already tagged.`) + // A retry in the same checkout reuses the tag an earlier run created but never pushed. + const localTag = yield* run("git", ["rev-parse", "--quiet", "--verify", `refs/tags/${tag}`], ".") + if (tagMissing && localTag.exitCode !== 0) { + const tagged = yield* run("git", ["tag", tag], ".") + if (tagged.exitCode !== 0) return yield* new ReleaseError({ message: `git tag failed:\n${tagged.stderr}` }) + yield* Console.log(`New tag: ${tag}`) + } const output = yield* Config.option(Config.String("CHANGESETS_OUTPUT")) if (Option.isSome(output)) { yield* fs.writeFileString(output.value, `${JSON.stringify({ type: "git-tag", tag, packageName: name })}\n`, {