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
57 changes: 37 additions & 20 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@
#
# Manual-only release: cuts app/build.gradle.kts's versionName/versionCode and
# CHANGELOG.md's [Unreleased] section for the version given in the dispatch
# form, commits and pushes that bump straight to main, then builds a signed
# release APK and publishes it as a GitHub Release. The version input is
# always typed by hand (no auto-computed default — GitHub's dispatch form
# can't pre-fill a value computed from repo state) and is only validated to
# be greater than the current versionName. The workflow still refuses to
# overwrite an existing release/tag. The GitHub Release body is never the
# full cut section — see "Extract changelog highlights" below for what
# actually gets published, and CLAUDE.md's "Changelog and release process"
# for the entry convention it depends on.
# form (as local, uncommitted edits), builds and signs the release APK
# against those edits, and only once the GitHub Release is actually
# published does it commit and push the version bump straight to main. This
# order is deliberate: if the build/signing step fails (e.g. a bad keystore
# password), main is never touched and the same version input can simply be
# re-run once the underlying problem is fixed — nothing is "spent" on a
# failed attempt. An earlier version of this workflow committed the bump
# before building, which meant a signing failure would leave main
# permanently bumped/changelog-cut with no release ever published for that
# version, and no clean way to retry short of faking a new changelog entry
# — see docs/implementation-decisions.md (found and fixed via the sibling
# HackDex-Tracker project, which hit this for real on its first release).
# The version input is always typed by hand (no auto-computed default —
# GitHub's dispatch form can't pre-fill a value computed from repo state)
# and is only validated to be greater than the current versionName. The
# workflow still refuses to overwrite an existing release/tag. The GitHub
# Release body is never the full cut section — see "Extract changelog
# highlights" below for what actually gets published, and CLAUDE.md's
# "Changelog and release process" for the entry convention it depends on.
name: Release

on:
Expand Down Expand Up @@ -103,18 +113,9 @@ jobs:
sed -i -E "s/versionCode = [0-9]+/versionCode = $NEW_CODE/" app/build.gradle.kts
sed -i -E "s/versionName = \"[^\"]*\"/versionName = \"$NEW_VERSION\"/" app/build.gradle.kts

- name: Commit and push version bump
env:
NEW_VERSION: ${{ inputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md app/build.gradle.kts
git commit -m "Cut release $NEW_VERSION"
git push origin HEAD:main

# Single source of truth for the release version: app/build.gradle.kts's versionName,
# now freshly bumped by the step above.
# now freshly bumped by the step above (still only a local, uncommitted edit at this
# point — see the top-of-file comment for why the commit/push is deferred to the end).
- name: Read app version
id: version
run: |
Expand Down Expand Up @@ -270,3 +271,19 @@ jobs:
--repo "$GITHUB_REPOSITORY" \
--title "ThePatientGamerHelper $RELEASE_TAG" \
--notes-file release-notes.md

# Only reached once the release above actually exists. Pushes with the credentials the
# initial Checkout step already configured for this job (its RELEASE_PUSH_TOKEN input) —
# a PAT, not the default GITHUB_TOKEN, so this commit authenticates as an actual repo admin
# and can bypass main's branch protection (a GITHUB_TOKEN push is attributed to the
# github-actions[bot] identity, which isn't covered by an "admins bypass required pull
# requests" exception).
- name: Commit and push version bump
env:
NEW_VERSION: ${{ inputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md app/build.gradle.kts
git commit -m "Cut release $NEW_VERSION"
git push origin HEAD:main
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ versioning follows the app's `versionName` in `app/build.gradle.kts`.

## [Unreleased]

- **Fixed a `release.yml` bug that could burn a version number on a failed signed build.** The
workflow used to commit and push the `versionName`/`versionCode` bump and the cut
`CHANGELOG.md` section to `main` *before* attempting the signed build. Found on the sibling
HackDex-Tracker project, which hit this for real on its first release run: a signing failure
left `main` permanently bumped with no release ever published, and every retry then failed
immediately because `[Unreleased]` was already empty. This project's releases have all
succeeded so far, but the same latent bug applied here. Reordered so the commit/push only
happens after `gh release create` actually succeeds; a build/signing failure now leaves `main`
untouched and the same version can simply be re-run. See `docs/implementation-decisions.md`.

## [1.0.5] - 2026-08-20

### Fixed
Expand Down
9 changes: 7 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -788,8 +788,13 @@ something to do unprompted) is a single step: manually trigger the
and leaves a fresh empty `## [Unreleased]` above it.
3. Bumps `versionCode` (+1) and `versionName` in `app/build.gradle.kts`
to match.
4. Commits and pushes that bump straight to `main`, then proceeds to
build/sign/publish the release from those same files.
4. Builds/signs/publishes the release from those same files, still only
as local, uncommitted edits at this point, and **only after the
GitHub Release is actually published** commits and pushes that bump
straight to `main`. This order is deliberate — see
`docs/implementation-decisions.md`: a build/signing failure (e.g. a
bad keystore password) now leaves `main` untouched instead of burning
the version number with no release to show for it.

There is no auto-computed default for the `version` input — GitHub's
manual dispatch form can't pre-fill a value computed from repo state, so
Expand Down
26 changes: 26 additions & 0 deletions docs/implementation-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1211,3 +1211,29 @@ scope this task didn't ask for — a new persistence surface for the
former, a `backlog_items` Room migration for the latter — for a benefit
(repeated cold-start lookups, one more stat) nobody had flagged as
needed. Add either separately if a concrete need shows up.

## `release.yml` now commits the version bump *after* the signed build, not before

Found on the sibling `HackDex-Tracker` project, which hit this for real
on its first `Release` run: the workflow used to commit and push the
`versionName`/`versionCode` bump and the cut `CHANGELOG.md` section to
`main` *before* attempting the signed build. If that build then fails
(a keystore-password mismatch, an expired secret, anything in the
signing/build step), `main` is left permanently bumped with
`[Unreleased]` emptied out, but no GitHub Release ever published for
that version — every retry with a higher version number then fails
immediately with "`CHANGELOG.md`'s `[Unreleased]` section is empty -
nothing to release", since there's no new changelog content to cut. The
version number is effectively burned with nothing to show for it, and
the only way out would be fabricating a new changelog entry just to
unblock the workflow.

This project's own releases have all succeeded so far (this was a
latent bug here, not one that had actually bitten a real run), but the
same reordering applies: the version-bump/changelog-cut edits are still
made first, but only as local, uncommitted working-tree changes. The
signed build runs against those local edits, and the `git commit`/
`git push` to `main` is now the very last step, reached only once
`gh release create` has actually succeeded. A build/signing failure now
leaves `main` completely untouched, so the exact same version input can
simply be re-run once the underlying problem is fixed.