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
97 changes: 40 additions & 57 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
name: Release

# Cuts a public release, end to end: bumps the version, builds a signed
# Android release APK, publishes it as a GitHub Release with the
# CHANGELOG.md entry for that version as the release notes, AND deploys
# the same version of the web app to GitHub Pages — all in one run, so
# the Android release and the live site are always on the same version.
# Manual-dispatch only: this pushes a version-bump commit to `main`,
# publishes a public GitHub Release, and redeploys the public site, so it
# never runs on its own. See ci.yml for the workflow that runs on every
# PR/push instead (tests only, no publishing).
# Cuts a public release, end to end: builds a signed Android release APK,
# publishes it as a GitHub Release with the CHANGELOG.md entry for that
# version as the release notes, AND deploys the same version of the web
# app to GitHub Pages — all in one run, so the Android release and the
# live site are always on the same version. Manual-dispatch only.
#
# The release version is READ from package.json's "version" field, never
# written by this workflow — `main` requires pull requests (see repo
# Settings -> Branches), and a workflow that never needs to commit
# anything to `main` never has to deal with that at all, unlike a
# workflow that tries to bump-and-push its own version first (see the
# commit that replaced that approach, and docs/STATUS.md's loose ends,
# for what didn't work and why). Same pattern already proven out in
# github.com/Marcogn/ThePatientGamerHelper's release.yml: bump the
# version yourself in a normal PR (and add the matching CHANGELOG.md
# entry) before triggering this workflow — it refuses to overwrite a
# release/tag that already exists, so re-running it before bumping
# anything is a safe no-op failure, not a duplicate release.
#
# See ci.yml for the workflow that runs on every PR/push instead (tests
# only, no publishing).
#
# Like android-build.yml, this never uploads Android build output via
# actions/upload-artifact — the APK here is instead attached directly to
Expand All @@ -17,15 +29,6 @@ name: Release

on:
workflow_dispatch:
inputs:
version:
description: >-
Version to release, e.g. 1.0.0 (no "v" prefix). Leave blank to
release the version already in package.json as-is — this is
what the first release (1.0.0) should do, since nothing needs
bumping yet.
required: false
type: string

permissions:
contents: write
Expand Down Expand Up @@ -57,45 +60,25 @@ jobs:

- run: npm ci

- name: Determine release version
- name: Read release version and refuse to overwrite an existing one
id: version
env:
GH_TOKEN: ${{ github.token }}
run: |
INPUT_VERSION="${{ inputs.version }}"
if [ -n "$INPUT_VERSION" ]; then
VERSION="$INPUT_VERSION"
else
VERSION=$(node -p "require('./package.json').version")
fi
VERSION=$(node -p "require('./package.json').version")

if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::'$VERSION' is not a plain X.Y.Z semantic version."
echo "::error::package.json's version ('$VERSION') is not a plain X.Y.Z semantic version."
exit 1
fi

if git ls-remote --exit-code --tags origin "refs/tags/v$VERSION" >/dev/null 2>&1; then
echo "::error::Tag v$VERSION already exists. Pass a new 'version' input (or bump package.json first) and re-run."
if gh release view "v$VERSION" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "::error::Release v$VERSION already exists. Bump \"version\" in package.json (and add a matching CHANGELOG.md entry) via a normal PR, merge it, then re-run this workflow."
exit 1
fi

echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Sync version into package.json and Android build.gradle
id: bump
run: |
VERSION="${{ steps.version.outputs.version }}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))

npm version "$VERSION" --no-git-tag-version --allow-same-version
sed -i "s/versionName \".*\"/versionName \"$VERSION\"/" android/app/build.gradle
sed -i "s/versionCode [0-9]*/versionCode $VERSION_CODE/" android/app/build.gradle

if git diff --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Run web test suite
run: npm run test

Expand All @@ -113,6 +96,18 @@ jobs:
- name: Sync Android web bundle into Android project
run: npx cap sync android

# Only the build in this checkout needs Android's versionName/
# versionCode to match package.json's version — never committed
# back, since package.json is the only file anyone needs to bump.
- name: Sync Android version from package.json
run: |
VERSION="${{ steps.version.outputs.version }}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))

sed -i "s/versionName \".*\"/versionName \"$VERSION\"/" android/app/build.gradle
sed -i "s/versionCode [0-9]*/versionCode $VERSION_CODE/" android/app/build.gradle

- name: Ensure signing secrets are present
run: |
if [ -z "$ANDROID_KEYSTORE_BASE64" ]; then
Expand Down Expand Up @@ -165,18 +160,6 @@ jobs:
working-directory: android
run: rm -f release.keystore keystore.properties

# Only publish anything now that both builds have actually succeeded —
# a failed build should never leave a version bump stranded on main,
# a half-published release, or a stale site deploy.
- name: Commit version bump
if: steps.bump.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json android/app/build.gradle
git commit -m "chore(release): v${{ steps.version.outputs.version }}"
git push origin HEAD:main

- name: Extract changelog section
id: changelog
run: |
Expand Down
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,12 @@ for manual completion. The block is still imported.
`applicationId`/`namespace` in `android/app/build.gradle`, which must
match it. Changing `appId` post-release changes the app's identity on
any device that installed it under the old one.
- `package.json`'s `version` field outside of the release workflow (see
- `package.json`'s `version` field without also adding a matching
`## [X.Y.Z]` entry to `CHANGELOG.md` in the same change (see
[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) → "Keeping the web and
Android releases in sync"). It drives both the GitHub Release tag and
the Android `versionName`/`versionCode`; bumping it by hand outside
that workflow will desync the two.
Android releases in sync"). The release workflow reads this field
directly — it's the trigger for the next `Release` run, not just a
cosmetic number.

## Common Pitfalls

Expand Down
25 changes: 14 additions & 11 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,17 @@ correctly.
## Keeping the web and Android releases in sync

CoverDex ships from one `package.json` version, read by both build
targets. `.github/workflows/release.yml` (manual-dispatch only — see
[`docs/android/BUILD.md`](android/BUILD.md) → "Cutting a public release")
is the single workflow that publishes anything public: given a version,
it bumps `package.json`/`android/app/build.gradle`, runs the test suite,
builds and publishes the signed Android release as a GitHub Release, and
in the same run redeploys GitHub Pages from the same checkout — so a
new Android release and the GitHub Pages deploy always carry the same
version number, published together, with nothing left to drift between
them. `.github/workflows/ci.yml` is the separate, non-publishing workflow
that just validates every PR and push to `main` (tests + a production
build check).
targets. To cut a release: bump `"version"` in `package.json` and add a
matching `## [X.Y.Z]` entry to `CHANGELOG.md`, merge that through a
normal PR, then trigger `.github/workflows/release.yml` (manual-dispatch
only — see [`docs/android/BUILD.md`](android/BUILD.md) → "Cutting a
public release"). It's the single workflow that publishes anything
public: it *reads* that version (never bumps it itself — see "Why this
workflow never writes to `main`" in the doc above for why), runs the
test suite, builds and publishes the signed Android release as a GitHub
Release, and in the same run redeploys GitHub Pages from the same
checkout — so the Android release and the GitHub Pages deploy always
carry the same version number, published together, with nothing left to
drift between them. `.github/workflows/ci.yml` is the separate,
non-publishing workflow that just validates every PR and push to `main`
(tests + a production build check).
75 changes: 47 additions & 28 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ short version plus what isn't obvious from a feature list.
`version` field, shown in Settings → App (`__APP_VERSION__`, injected
at build time by `vite.config.ts`). `.github/workflows/release.yml`
(manual-dispatch only) is the single workflow that publishes anything
public: given a version, it bumps that field (and Android's
`versionName`/`versionCode`), builds and publishes a signed release APK
as a GitHub Release with `CHANGELOG.md`'s matching section as release
notes, and in the same run redeploys GitHub Pages — see
[`docs/android/BUILD.md`](android/BUILD.md) → "Cutting a public
release" and [`docs/DEVELOPMENT.md`](DEVELOPMENT.md) → "Keeping the web
and Android releases in sync".
public: it *reads* that field (bumping it is a normal PR, not something
the workflow does itself — see "Loose ends" below for why), builds and
publishes a signed release APK as a GitHub Release with `CHANGELOG.md`'s
matching section as release notes, and in the same run redeploys GitHub
Pages — see [`docs/android/BUILD.md`](android/BUILD.md) → "Cutting a
public release" and [`docs/DEVELOPMENT.md`](DEVELOPMENT.md) → "Keeping
the web and Android releases in sync".
- Actions workflows were consolidated down to three this session:
`ci.yml` (tests + build check on every PR/push to `main`, no
publishing — replaces the old `pr-check.yml`), `release.yml` (the
Expand Down Expand Up @@ -88,28 +88,47 @@ reasoning before re-implementing it; it hasn't changed.

## Loose ends from this session (2026-08-17)

- **`release.yml` has never actually been run.** It's been reviewed
carefully (version validation, tag-exists check, build-before-publish
ordering, signing required) and the repo's other workflows build fine
locally, but the workflow itself — the version bump commit landing on
`main`, the tag/release creation, the changelog extraction, the asset
upload, and the Pages deploy at the end — has not been exercised end to
end in CI. Trigger it once for the real `1.0.0` release and confirm:
the GitHub Release appears with the APK attached and installable, the
live site at `marcogn.github.io/CoverDex` shows the same version in
Settings → App, and the release notes match `CHANGELOG.md`'s `[1.0.0]`
section.
- **`CHANGELOG.md` needs a new `## [X.Y.Z]` entry before every release
after `1.0.0`.** The workflow falls back to a generic "see CHANGELOG.md
/ README.md" note if it can't find a matching heading — better than
failing the release, but not a substitute for real notes.
- **`release.yml` still hasn't completed a successful end-to-end run.**
Three real attempts against `1.0.0` so far, each catching a real
problem: (1) failed on "Decode release keystore" — the
`ANDROID_KEYSTORE_BASE64` secret wasn't valid base64, fixed by
re-setting it via `gh secret set`, and the step now fails with an
actionable error instead of a bare `base64: invalid input`; (2) got
past the Android build but then failed pushing the version bump
straight to `main` (`GH006: Protected branch update failed`, "Require
a pull request before merging"); (3) the fix for that — opening and
auto-merging a small PR — worked but was more machinery than needed.
Comparing against `ThePatientGamerHelper`'s already-working
`release.yml` (same kind of branch protection on its `main`) showed the
actual fix: don't bump the version *in* the release workflow at all.
`release.yml` now only **reads** `package.json`'s version and refuses
to run if that version's release already exists — the maintainer bumps
`package.json` and adds the `CHANGELOG.md` entry via a normal PR first,
same as any other change, so the workflow never needs to write to
`main` and branch protection never comes up. See
`docs/android/BUILD.md` → "Why this workflow never writes to `main`".
**Before the next trigger**, `package.json`'s `"version"` needs to
already say `1.0.0` on `main` (it does, untouched since the project's
start) and `CHANGELOG.md` needs its `[1.0.0]` entry (it does). Trigger
it and confirm: the GitHub Release appears with the APK attached and
installable, the live site at `marcogn.github.io/CoverDex` shows the
same version in Settings → App, and the release notes match
`CHANGELOG.md`'s `[1.0.0]` section.
- **`CHANGELOG.md` needs a new `## [X.Y.Z]` entry, and `package.json`'s
`"version"` needs bumping to match, via a normal PR before every
release after `1.0.0`.** `release.yml` refuses to run at all if the
version it reads has already been released (no silent duplicate/
overwritten release), and falls back to a generic "see CHANGELOG.md /
README.md" note if it can't find a matching `CHANGELOG.md` heading for
the version it did find — better than failing the release outright,
but not a substitute for real notes.
- **GitHub Pages no longer redeploys on every push to `main`** — only
`release.yml` deploys it now (see "What's implemented" above). If a
docs-only or urgent web fix needs to go live without a full Android
release, either trigger `release.yml` anyway (it's a no-op version bump
if `package.json` is unchanged, still rebuilds and redeploys Pages) or
do a one-off manual deploy per `docs/DEVELOPMENT.md` → "Manual
deployment".
`release.yml` deploys it now (see "What's implemented" above), and
since that workflow now refuses to run for an already-released
version, it can no longer double as a "just redeploy Pages" trick
either. For a docs-only or urgent web fix that needs to go live without
a full Android release, use the one-off manual deploy in
`docs/DEVELOPMENT.md` → "Manual deployment" instead.

## Loose ends from the previous session (2026-08-04)

Expand Down
Loading
Loading