Skip to content

Commit 7a6e95e

Browse files
committed
ci: make the desktop package version authoritative and auto-cut its release tag
Desktop 0.2.0 sat on main unreleased while every installed client stayed on 0.1.6: changesets bumps apps/desktop/package.json, but Desktop Release only fires on a desktop-v* tag and nothing cut one. - release.yml now cuts desktop-v<version> whenever the version changes on main, using the release-bot App token so the tag push actually starts the build. - desktop-release.yml no longer stamps the tag's version into the tree. The package version is the source of truth and a disagreeing tag fails the build, so tagging an arbitrary commit can no longer mint an unreviewed release. - A tagged commit must be an ancestor of main before anything is built.
1 parent 35c89a7 commit 7a6e95e

2 files changed

Lines changed: 72 additions & 24 deletions

File tree

.github/workflows/desktop-release.yml

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ jobs:
3333
with:
3434
persist-credentials: false
3535

36+
# apps/desktop/package.json is the single source of truth for the version.
37+
# The tag only selects which commit ships. Stamping the tag's version into
38+
# the tree instead would let `git tag desktop-v9.9.9 <any commit>` silently
39+
# rewrite that commit into a release nobody reviewed, so a disagreement is
40+
# a hard failure rather than a rewrite.
3641
- name: Resolve the desktop version
3742
id: resolve
3843
shell: bash
@@ -41,23 +46,43 @@ jobs:
4146
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/desktop-v') }}
4247
run: |
4348
set -euo pipefail
44-
# A tag build is authoritative; a manual run falls back to whatever
45-
# version the checked-out tree declares.
46-
if [ "$IS_TAG" = 'true' ]; then
47-
version="${TAG_NAME#desktop-v}"
48-
else
49-
version="$(node -p 'require("./apps/desktop/package.json").version')"
50-
fi
49+
version="$(node -p 'require("./apps/desktop/package.json").version')"
5150
if [ -z "$version" ]; then
5251
echo 'Could not resolve a desktop version' >&2
5352
exit 1
5453
fi
54+
if [ "$IS_TAG" = 'true' ]; then
55+
tag_version="${TAG_NAME#desktop-v}"
56+
if [ "$tag_version" != "$version" ]; then
57+
echo "::error::Tag ${TAG_NAME} does not match apps/desktop/package.json (${version})." >&2
58+
echo 'Bump the package version on main first, then tag that commit.' >&2
59+
exit 1
60+
fi
61+
fi
5562
# electron-builder names the GitHub release `v${version}`; the tag we
5663
# push here (`desktop-v*`) only triggers the workflow.
5764
echo "version=${version}" >> "$GITHUB_OUTPUT"
5865
echo "tag=v${version}" >> "$GITHUB_OUTPUT"
5966
echo "Releasing desktop ${version} as tag v${version}."
6067
68+
# A release built from a commit that never landed on main ships code no
69+
# review gate ever saw. `compare` reports `identical` or `behind` when the
70+
# commit is an ancestor of main, and `diverged`/`ahead` when it is not.
71+
- name: Require the tagged commit to be on main
72+
if: startsWith(github.ref, 'refs/tags/desktop-v')
73+
shell: bash
74+
env:
75+
GH_TOKEN: ${{ github.token }}
76+
run: |
77+
set -euo pipefail
78+
status="$(gh api "repos/${GITHUB_REPOSITORY}/compare/main...${GITHUB_SHA}" --jq '.status')"
79+
case "$status" in
80+
identical|behind) echo "${GITHUB_SHA} is on main (${status})." ;;
81+
*)
82+
echo "::error::${GITHUB_SHA} is not on main (compare status: ${status})." >&2
83+
exit 1 ;;
84+
esac
85+
6186
- name: Mint releases-repo token
6287
id: releases_token
6388
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # pinned from v3.2.0
@@ -107,14 +132,6 @@ jobs:
107132

108133
- run: pnpm install --frozen-lockfile
109134

110-
- name: Stamp desktop version for tag builds
111-
if: startsWith(github.ref, 'refs/tags/desktop-v')
112-
env:
113-
TAG_NAME: ${{ github.ref_name }}
114-
run: |
115-
export DESKTOP_VERSION="${TAG_NAME#desktop-v}"
116-
node -e 'const fs = require("node:fs"); const path = "apps/desktop/package.json"; const packageJson = JSON.parse(fs.readFileSync(path, "utf8")); packageJson.version = process.env.DESKTOP_VERSION; fs.writeFileSync(path, `${JSON.stringify(packageJson, null, 2)}\n`);'
117-
118135
- name: Build workspace
119136
run: pnpm --workspace-root run build
120137

@@ -233,15 +250,6 @@ jobs:
233250
234251
- run: pnpm install --frozen-lockfile
235252
236-
- name: Stamp desktop version for tag builds
237-
if: startsWith(github.ref, 'refs/tags/desktop-v')
238-
shell: bash
239-
env:
240-
TAG_NAME: ${{ github.ref_name }}
241-
run: |
242-
export DESKTOP_VERSION="${TAG_NAME#desktop-v}"
243-
node -e 'const fs = require("node:fs"); const path = "apps/desktop/package.json"; const packageJson = JSON.parse(fs.readFileSync(path, "utf8")); packageJson.version = process.env.DESKTOP_VERSION; fs.writeFileSync(path, `${JSON.stringify(packageJson, null, 2)}\n`);'
244-
245253
- name: Build workspace
246254
run: pnpm --workspace-root run build
247255

.github/workflows/release.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,46 @@ jobs:
126126
# No NPM_TOKEN on purpose: changesets prefers it over OIDC when set, so
127127
# defining it would silently downgrade publishing to a long-lived token.
128128

129+
# apps/desktop is a private workspace package: changesets bumps its
130+
# version but nothing publishes it, and Desktop Release only fires on a
131+
# `desktop-v*` tag. A bump that nobody tags therefore leaves every
132+
# installed desktop client on the previous version with nothing red to
133+
# show for it — which is exactly how 0.2.0 sat unreleased behind 0.1.6.
134+
# Read both versions out of git rather than the working tree: the
135+
# changesets action rewrites package.json in place on the run that opens
136+
# the version PR, and that rewrite is not a release.
137+
# The App token matters: a tag pushed with GITHUB_TOKEN would not start
138+
# Desktop Release, because GitHub refuses to trigger workflows from it.
139+
# A desktop tag that fails to cut costs one manual `git tag`; a failed step
140+
# here would block npm, the Marketplace and the CDN behind it. Never let
141+
# this be the thing that stops a release — but annotate every skip, since
142+
# a silent no-op is the exact failure being fixed.
143+
- name: Cut the desktop release tag on a version bump
144+
continue-on-error: true
145+
env:
146+
GH_TOKEN: ${{ steps.release-bot.outputs.token }}
147+
run: |
148+
set -uo pipefail
149+
read_version() { git show "$1:apps/desktop/package.json" 2>/dev/null | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).version' 2>/dev/null; }
150+
prev="$(read_version 'HEAD^' || true)"
151+
curr="$(read_version 'HEAD' || true)"
152+
if [ -z "$prev" ] || [ -z "$curr" ] || [ "$prev" = "$curr" ]; then
153+
echo "::notice::Desktop version unchanged or unreadable (prev='${prev}' curr='${curr}'); no tag cut."
154+
exit 0
155+
fi
156+
tag="desktop-v${curr}"
157+
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
158+
echo "::notice::${tag} already exists; leaving it alone."
159+
exit 0
160+
fi
161+
if ! gh api "repos/${GITHUB_REPOSITORY}/git/refs" \
162+
-f ref="refs/tags/${tag}" \
163+
-f sha="${GITHUB_SHA}" > /dev/null; then
164+
echo "::error::Could not cut ${tag}. Desktop ${curr} will not ship until someone pushes it: git tag ${tag} ${GITHUB_SHA} && git push origin ${tag}"
165+
exit 1
166+
fi
167+
echo "Cut ${tag} at ${GITHUB_SHA} (desktop ${prev} -> ${curr})."
168+
129169
- name: Request CodeRabbit review on version PR
130170
if: steps.changesets.outputs.published != 'true'
131171
env:

0 commit comments

Comments
 (0)