ci(release): release from the tag that bun run release pushes (#1572)

The v0.5.15 release run failed to sign the Windows DLLs. It ran on the
push of the release commit to main, and Azure, which signs them, trusts
only runs on a tag (subject repo:anomalyco/opentui:ref:refs/heads/main
had no match).

`bun run release` now tags the release commit and pushes it together
with the tag, and release.yml releases again on the push of a vX.Y.Z
tag, so signing runs on the tag as it did before. A push to main that
raises the version still releases when the version has no tag yet, as
when a release pull request is merged, but that needs Azure to trust
main first.
This commit is contained in:
Simon Klee authored and GitHub committed 2026-10-07 13:03:58 +02:00
1 parent 615e3e0d47
commit f1d3c93469
2 files changed
+57 -27

No files matched your search

+32 -14
View File
@@ -1,16 +1,21 @@
name: Release
# A push to main, or to a maintenance branch of an older line such as 0.5.x, releases when it raises the
# version in packages/core/package.json: a "Release vX.Y.Z" commit, pushed by `bun run release` or
# merged from the pull request that the release button opens. A vX.Y.Z-dry.N tag is a dry run. A tag
# that contains "snapshot" publishes a snapshot.
# A push of a vX.Y.Z tag releases that version: `bun run release` pushes the tag with its "Release
# vX.Y.Z" commit. The Windows signing in Azure trusts only runs on a tag.
#
# A push to main, or to a maintenance branch of an older line such as 0.5.x, that raises the version
# in packages/core/package.json releases it when the version has no tag yet: a release commit merged
# from the pull request that the release button opens. That run tags the commit itself, and needs Azure
# to trust main and the maintenance branches.
#
# A vX.Y.Z-dry.N tag is a dry run. A tag that contains "snapshot" publishes a snapshot.
on:
push:
branches: [main, "[0-9]+.[0-9]+.x"]
# Path filters do not apply to tag pushes.
paths: [packages/core/package.json]
tags:
- "v*-dry.*"
- "v*"
- "*snapshot*"
permissions:
@@ -74,12 +79,18 @@ jobs:
run: |
set -euo pipefail
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
# Only vX.Y.Z-dry.N tags reach this job.
TAG=${GITHUB_REF#refs/tags/v}
IS_DRY_RUN=true
VERSION=$(echo "$TAG" | sed -E 's/-dry\.[0-9]+$//')
RELEASE_TITLE="Release v$TAG (DRY RUN)"
echo "This is a DRY RUN release"
if [[ "$TAG" =~ -dry\. ]]; then
IS_DRY_RUN=true
VERSION=$(echo "$TAG" | sed -E 's/-dry\.[0-9]+$//')
RELEASE_TITLE="Release v$TAG (DRY RUN)"
echo "This is a DRY RUN release"
else
IS_DRY_RUN=false
VERSION="$TAG"
RELEASE_TITLE="Release v$TAG"
echo "This is a PRODUCTION release"
fi
else
# A version that does not go up, such as a reverted release commit, is not a release. A
# failure of the comparison fails the job.
@@ -97,6 +108,13 @@ jobs:
echo "::error::$CURRENT is not a version of $GITHUB_REF_NAME"
exit 1
fi
# `bun run release` pushes the tag with the commit, and the run of the tag push releases it.
# This also skips a rerun of all jobs after the tag step below; rerun the failed jobs instead.
if git ls-remote --exit-code --tags origin "refs/tags/v$CURRENT" > /dev/null; then
echo "v$CURRENT is already tagged, so the run of its tag push releases it"
echo "release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
TAG="$CURRENT"
IS_DRY_RUN=false
VERSION="$TAG"
@@ -115,11 +133,11 @@ jobs:
echo "Is Dry Run: $IS_DRY_RUN"
echo "Release Title: $RELEASE_TITLE"
# Before the build, so a release stops early when the tag cannot be created. Needs the "Main tags"
# ruleset to allow creating v* tags. A tag already at this commit, from a rerun or pushed by hand,
# is kept. A tag pushed with GITHUB_TOKEN starts no workflow.
# A release from a branch push tags its commit before the build, so it stops early when the tag
# cannot be created. Needs the "Main tags" ruleset to let GITHUB_TOKEN create v* tags. A tag pushed
# with GITHUB_TOKEN starts no workflow.
- name: Tag the release commit
if: steps.extract.outputs.isDryRun == 'false'
if: github.ref_type == 'branch' && steps.extract.outputs.isDryRun == 'false'
env:
TAG: v${{ steps.extract.outputs.fullTag }}
run: |
+25 -13
View File
@@ -15,9 +15,10 @@ import { compareVersions, registryIntegrity } from "./npm-publish"
// bun run release <patch|minor|major|version> [--pr | --dry-run] [--no-watch]
//
// 1. Checks that the branch has no uncommitted changes and matches its origin branch.
// 2. Runs prepare-release, commits "Release vX.Y.Z", and pushes the commit to the branch. That needs
// the right to bypass the branch rules. A maintenance branch takes only versions of its line.
// 3. Follows the release.yml run of the commit. Its publish job ends when npm serves every package,
// 2. Runs prepare-release, commits "Release vX.Y.Z", tags it vX.Y.Z, and pushes the commit to the
// branch together with the tag. That needs the right to bypass the branch and tag rules. A
// maintenance branch takes only versions of its line.
// 3. Follows the release.yml run of the tag push. Its publish job ends when npm serves every package,
// and this script reports that time.
//
// --pr pushes the commit to a release/vX.Y.Z branch and opens a pull request instead. Merging it
@@ -40,7 +41,8 @@ interface Release {
// The branch that releases it: main or a maintenance branch.
branch: string
sha: string
// The ref that was pushed, without refs/heads/ or refs/tags/.
// The tag whose push starts the release run, or the pull request branch. Without refs/tags/ or
// refs/heads/.
ref: string
pushedAt: number
}
@@ -215,12 +217,14 @@ function remoteTagCommit(tag: string): string | undefined {
return peeled?.split(/\s+/)[0] || undefined
}
function push(ref: string, sha: string): void {
// Pushes the refs together. The last one is the ref whose push starts the release run.
function push(refspecs: readonly string[], sha: string): void {
try {
run("git", ["push", "origin", ref], { inherit: true })
run("git", ["push", "--atomic", "origin", ...refspecs], { inherit: true })
} catch (error) {
// The push can reach origin and still fail here, for example when the connection drops.
const target = ref.slice(ref.indexOf(":") + 1)
const last = refspecs.at(-1)!
const target = last.slice(last.indexOf(":") + 1)
const landed = target.startsWith("refs/tags/")
? remoteTagCommit(target.slice("refs/tags/".length))
: remoteSha(target)
@@ -283,16 +287,23 @@ async function pushRelease(options: Options, branch: string, base: string): Prom
console.log(`Prepared ${previous} -> ${version}. Committing...`)
run("git", ["commit", "--quiet", "--all", "--message", `Release v${version}`], { inherit: true })
if (options.mode === "dry-run") {
const name = nextDryRunTag(version)
if (options.mode !== "pr") {
const name = options.mode === "dry-run" ? nextDryRunTag(version) : `v${version}`
run("git", ["tag", "--annotate", name, "--message", `Release ${name}`], { inherit: true })
tag = name
}
const sha = git("rev-parse", "HEAD")
stopIfInterrupted()
const ref = tag ?? prBranch ?? branch
push(tag ? `refs/tags/${tag}` : `HEAD:refs/heads/${ref}`, sha)
// A release pushes the commit with its tag. The run of the tag push releases it, with the tag as
// its ref, which is the ref that the Windows signing in Azure trusts.
const ref = tag ?? prBranch!
const refspecs = {
push: [`HEAD:refs/heads/${branch}`, `refs/tags/${ref}`],
"dry-run": [`refs/tags/${ref}`],
pr: [`HEAD:refs/heads/${ref}`],
}[options.mode]
push(refspecs, sha)
const release = { version, branch, sha, ref, pushedAt: Date.now() }
if (options.mode !== "push") git("switch", "--quiet", branch)
if (prBranch) git("branch", "--quiet", "--delete", "--force", prBranch)
@@ -382,14 +393,15 @@ async function main(): Promise<void> {
console.log(`Releasing ${repo} ${branch} at ${short(base)}: ${options.target}, ${options.mode}`)
const release = await pushRelease(options, branch, base)
console.log(`Pushed Release v${release.version} (${short(release.sha)}) to ${release.ref}`)
const pushedTo = options.mode === "push" ? `${release.branch} and ${release.ref}` : release.ref
console.log(`Pushed Release v${release.version} (${short(release.sha)}) to ${pushedTo}`)
if (options.mode === "pr") {
const url = openPullRequest(repo, release)
report([`Opened ${url}`, `- Merge it to release v${release.version}.`])
return
}
const tag = options.mode === "dry-run" ? release.ref : `v${release.version}`
const tag = release.ref
const releaseRun = await findReleaseRun(repo, release)
console.log(`Release run: ${releaseRun.html_url}`)
if (!options.watch) {