From a0994a2a45efbb55d7f0db13da6a3d6aa6b5a1e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 11:06:00 +0000 Subject: [PATCH 1/2] Route the release version bump through a PR instead of a direct push to main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second real Release run got past the (now-fixed) keystore issue but failed at "Commit version bump" with GH006: Protected branch update failed for refs/heads/main — main requires pull requests, and the workflow's default GITHUB_TOKEN has no bypass for that (unlike the repo owner merging PRs by hand). Confirmed there's no ruleset and no "Lock branch" rule involved, just "Require a pull request before merging". release.yml now opens a small chore(release): vX.Y.Z branch/PR against main and merges it (approve + squash --auto) using the workflow's own token, instead of pushing directly — no branch-protection bypass or PAT needed, just the "Allow GitHub Actions to create and approve pull requests" repo setting that's already been turned on. If the merge can't be automated (e.g. repo-wide auto-merge is off, or required checks are still pending), the step only warns and moves on: the GitHub Release, APK, and Pages deploy don't depend on the PR having landed, only on the build already on disk. Documented the mechanism and the two repo settings it relies on in docs/android/BUILD.md, and updated the STATUS.md loose end to reflect both real failures hit so far and their fixes. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01WH1WrPxkRsDyMo76MPn3Us --- .github/workflows/release.yml | 47 ++++++++++++++++++++++++++++------- docs/STATUS.md | 29 ++++++++++++++------- docs/android/BUILD.md | 32 ++++++++++++++++++++++-- 3 files changed, 88 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c714b0a..01bbb19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,10 +5,20 @@ name: Release # 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). +# Manual-dispatch only: this opens and merges a version-bump PR against +# `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). +# +# `main` requires pull requests (see repo Settings -> Branches), so the +# version bump goes through a small PR opened and merged by this +# workflow rather than a direct push — no branch-protection bypass +# needed, just the "Allow GitHub Actions to create and approve pull +# requests" repo setting (Settings -> Actions -> General). If that PR +# can't be auto-merged (e.g. required status checks still pending and +# repo-wide auto-merge isn't enabled), the job only warns and moves on — +# the GitHub Release, APK, and Pages deploy below don't depend on it +# having landed yet, only on the local build. # # Like android-build.yml, this never uploads Android build output via # actions/upload-artifact — the APK here is instead attached directly to @@ -29,6 +39,7 @@ on: permissions: contents: write + pull-requests: write pages: write id-token: write @@ -166,16 +177,34 @@ jobs: 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 + # a failed build should never leave a version-bump PR opened against + # main, a half-published release, or a stale site deploy. + - name: Open and merge version-bump PR if: steps.bump.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} run: | + VERSION="${{ steps.version.outputs.version }}" + BRANCH="release/v$VERSION" + git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" 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 + git commit -m "chore(release): v$VERSION" + git push origin "$BRANCH" + + PR_URL=$(gh pr create \ + --base main \ + --head "$BRANCH" \ + --title "chore(release): v$VERSION" \ + --body "Automated version bump for the v$VERSION release. Opened and merged by \`release.yml\` — see the matching GitHub Release for what shipped.") + + gh pr review "$PR_URL" --approve --body "Automated release version bump." || true + + if ! gh pr merge "$PR_URL" --squash --auto; then + echo "::warning::Could not auto-merge the version-bump PR ($PR_URL) — merge it manually. The GitHub Release, APK, and Pages deploy below don't depend on it and will still publish." + fi - name: Extract changelog section id: changelog diff --git a/docs/STATUS.md b/docs/STATUS.md index aeb5bcf..ba6f9ce 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -88,15 +88,26 @@ 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 +- **`release.yml` still hasn't completed a successful end-to-end run.** + Two real attempts against `1.0.0` so far, each catching a real problem: + the first failed on "Decode release keystore" (`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`, see the commit that did that). The second got + past the Android build entirely but then failed pushing the version + bump straight to `main`, which turned out to be protected (`GH006: + Protected branch update failed`, "Require a pull request before + merging") — `release.yml` now opens and merges a small + `chore(release): vX.Y.Z` PR instead of pushing directly (see + `docs/android/BUILD.md` → "Branch protection and the version-bump + PR"), which needed two repo settings the maintainer enabled: Workflow + permissions → read/write + "Allow GitHub Actions to create and approve + pull requests", and (optionally, for a fully unattended merge) "Allow + auto-merge" under Settings → General. Trigger it once more for the + real `1.0.0` release and confirm: the version-bump PR merges (or, if + auto-merge isn't enabled, merge it manually when the run warns about + it), 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 diff --git a/docs/android/BUILD.md b/docs/android/BUILD.md index baab5a1..7aedad0 100644 --- a/docs/android/BUILD.md +++ b/docs/android/BUILD.md @@ -195,8 +195,10 @@ for the first release): internal testing), since an unsigned APK can't be installed at all and this release is public. 4. Only once both builds have actually succeeded, it publishes - everything: commits the version bump to `main` and pushes it, publishes - a GitHub Release tagged `vX.Y.Z` with the matching + everything: opens a small `chore(release): vX.Y.Z` PR against `main` + with the version bump and merges it (`main` requires pull requests — + see "Branch protection and the version-bump PR" below), publishes a + GitHub Release tagged `vX.Y.Z` with the matching [`CHANGELOG.md`](../../CHANGELOG.md) section (`## [X.Y.Z]` up to the next version heading) as the release notes and the signed APK attached directly (as a plain file, not zipped, not through @@ -213,6 +215,32 @@ Firebase. Before running it for a version after `1.0.0`, add a new `## [X.Y.Z]` entry to `CHANGELOG.md` first so the release has real notes instead of the generic fallback text. +### Branch protection and the version-bump PR + +`main` has "Require a pull request before merging" turned on (repo +Settings → Branches), so `release.yml` can't just `git push` the version +bump directly — a straight push gets rejected with `GH006: Protected +branch update failed`. Instead it opens a real PR (`release/vX.Y.Z` → +`main`), approves it, and merges it with `gh pr merge --squash --auto`, +all as the workflow's own `GITHUB_TOKEN`. Two repo settings make this +possible without weakening branch protection at all: + +- **Settings → Actions → General → Workflow permissions**: "Read and + write permissions", plus "Allow GitHub Actions to create and approve + pull requests" — without the second one, `gh pr review --approve` + fails with "GitHub Actions is not permitted to approve pull requests". +- **Settings → General → Pull Requests → "Allow auto-merge"** — without + this, `gh pr merge --auto` errors out. This isn't strictly required: + the step only emits a `::warning::` and moves on if the merge can't be + automated (e.g. auto-merge is off, or a required status check is still + pending), since the GitHub Release, APK, and Pages deploy don't depend + on the PR having landed yet — only on the files already built on disk. + A stray unmerged `release/vX.Y.Z` PR just needs a manual merge + afterward if that happens. + +No bypass list, no PAT, no loosening of "require pull request" — the +workflow follows the same rule everyone else does. + ## Running the Espresso smoke test locally `android/app/src/androidTest/java/com/marcogn/coverdex/MainActivitySmokeTest.java` From 70aeb9f859bb6105ef8b7545c66ae9d6a24b428c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 11:19:04 +0000 Subject: [PATCH 2/2] =?UTF-8?q?Stop=20bumping=20the=20version=20in=20relea?= =?UTF-8?q?se.yml=20=E2=80=94=20read=20it,=20never=20write=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PR-based version bump from the previous commit worked but was more machinery than the problem needed: pull-requests: write, two extra repo settings (Allow GitHub Actions to create and approve pull requests, Allow auto-merge), and a non-fatal fallback for when those aren't configured just right. Comparing against github.com/Marcogn/ThePatientGamerHelper's release.yml — a sibling project's already-working release pipeline, with the same kind of branch protection on main — showed a simpler fix: that workflow never bumps anything, it only reads versionName from an already-merged build.gradle.kts and refuses to overwrite an existing release. Applied the same pattern here. release.yml now reads package.json's "version" directly (no workflow input anymore) and fails outright if a GitHub Release for that version already exists, instead of silently no-op'ing. Bumping the version is now an entirely normal, human-reviewed PR like any other change — the release workflow only ever reads what's already on main, so it never touches branch protection at all, and pull-requests: write is dropped from its permissions. Android's versionName/versionCode are still patched from the same version, just locally in the build checkout, never committed. Updated CLAUDE.md, docs/DEVELOPMENT.md, docs/android/BUILD.md, and docs/STATUS.md to match — including that "trigger release.yml as a Pages-only redeploy trick" no longer works now that it refuses to run for an already-released version (use the manual deploy steps instead). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01WH1WrPxkRsDyMo76MPn3Us --- .github/workflows/release.yml | 124 +++++++++++----------------------- CLAUDE.md | 9 +-- docs/DEVELOPMENT.md | 25 ++++--- docs/STATUS.md | 84 ++++++++++++----------- docs/android/BUILD.md | 105 ++++++++++++++-------------- 5 files changed, 157 insertions(+), 190 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01bbb19..80d37f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,24 +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 opens and merges a version-bump PR against -# `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. # -# `main` requires pull requests (see repo Settings -> Branches), so the -# version bump goes through a small PR opened and merged by this -# workflow rather than a direct push — no branch-protection bypass -# needed, just the "Allow GitHub Actions to create and approve pull -# requests" repo setting (Settings -> Actions -> General). If that PR -# can't be auto-merged (e.g. required status checks still pending and -# repo-wide auto-merge isn't enabled), the job only warns and moves on — -# the GitHub Release, APK, and Pages deploy below don't depend on it -# having landed yet, only on the local build. +# 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 @@ -27,19 +29,9 @@ 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 - pull-requests: write pages: write id-token: write @@ -68,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 @@ -124,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 @@ -176,36 +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 PR opened against - # main, a half-published release, or a stale site deploy. - - name: Open and merge version-bump PR - if: steps.bump.outputs.changed == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - VERSION="${{ steps.version.outputs.version }}" - BRANCH="release/v$VERSION" - - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "$BRANCH" - git add package.json package-lock.json android/app/build.gradle - git commit -m "chore(release): v$VERSION" - git push origin "$BRANCH" - - PR_URL=$(gh pr create \ - --base main \ - --head "$BRANCH" \ - --title "chore(release): v$VERSION" \ - --body "Automated version bump for the v$VERSION release. Opened and merged by \`release.yml\` — see the matching GitHub Release for what shipped.") - - gh pr review "$PR_URL" --approve --body "Automated release version bump." || true - - if ! gh pr merge "$PR_URL" --squash --auto; then - echo "::warning::Could not auto-merge the version-bump PR ($PR_URL) — merge it manually. The GitHub Release, APK, and Pages deploy below don't depend on it and will still publish." - fi - - name: Extract changelog section id: changelog run: | diff --git a/CLAUDE.md b/CLAUDE.md index 5a83715..5695b7d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 63b2fce..e7eb4e9 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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). diff --git a/docs/STATUS.md b/docs/STATUS.md index ba6f9ce..455efd3 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -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 @@ -89,38 +89,46 @@ reasoning before re-implementing it; it hasn't changed. ## Loose ends from this session (2026-08-17) - **`release.yml` still hasn't completed a successful end-to-end run.** - Two real attempts against `1.0.0` so far, each catching a real problem: - the first failed on "Decode release keystore" (`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`, see the commit that did that). The second got - past the Android build entirely but then failed pushing the version - bump straight to `main`, which turned out to be protected (`GH006: - Protected branch update failed`, "Require a pull request before - merging") — `release.yml` now opens and merges a small - `chore(release): vX.Y.Z` PR instead of pushing directly (see - `docs/android/BUILD.md` → "Branch protection and the version-bump - PR"), which needed two repo settings the maintainer enabled: Workflow - permissions → read/write + "Allow GitHub Actions to create and approve - pull requests", and (optionally, for a fully unattended merge) "Allow - auto-merge" under Settings → General. Trigger it once more for the - real `1.0.0` release and confirm: the version-bump PR merges (or, if - auto-merge isn't enabled, merge it manually when the run warns about - it), 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. + 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) diff --git a/docs/android/BUILD.md b/docs/android/BUILD.md index 7aedad0..e367046 100644 --- a/docs/android/BUILD.md +++ b/docs/android/BUILD.md @@ -181,65 +181,66 @@ reachable there either. `.github/workflows/release.yml` is the one workflow that publishes anywhere public — everything above (Firebase App Distribution) only ever reaches invited testers. It's manual-only (**Actions → Release → Run -workflow**) and, given a version like `1.0.0` (or a blank input, which -reuses whatever version is already in `package.json` — the right choice -for the first release): - -1. Validates the version and checks a `vX.Y.Z` tag doesn't already exist. -2. Writes that version into `package.json`, `package-lock.json`, and - `android/app/build.gradle`'s `versionName`/`versionCode` (derived - deterministically from the semver: `major*10000 + minor*100 + patch`). +workflow**), takes no inputs, and: + +1. **Reads** the release version straight from `package.json`'s + `"version"` field — it never writes or bumps it. To cut a new + release: bump `"version"` in `package.json` yourself, add a matching + `## [X.Y.Z]` entry to [`CHANGELOG.md`](../../CHANGELOG.md), merge that + through a normal PR like any other change, then trigger this workflow. + This is the same pattern already proven out in + [`ThePatientGamerHelper`](https://github.com/Marcogn/ThePatientGamerHelper)'s + `release.yml`: the workflow that *publishes* a release should never + also be the one *deciding* the version, because publishing needs to + run non-interactively and version bumps are exactly the kind of + change `main`'s branch protection ("Require a pull request before + merging") exists to gate. A workflow that only ever reads already- + merged files never runs into that at all — see "Why this workflow + never writes to `main`" below for what didn't work first. +2. Refuses to run if a GitHub Release tagged `vX.Y.Z` already exists — + re-running it before bumping `package.json` is a safe no-op failure, + not a duplicate or overwritten release. 3. Runs the web test suite, builds the plain web app (for GitHub Pages) - and the Android bundle, and builds a **signed** release APK — signing - is required here (unlike `android-build.yml`'s optional signing for - internal testing), since an unsigned APK can't be installed at all and - this release is public. -4. Only once both builds have actually succeeded, it publishes - everything: opens a small `chore(release): vX.Y.Z` PR against `main` - with the version bump and merges it (`main` requires pull requests — - see "Branch protection and the version-bump PR" below), publishes a - GitHub Release tagged `vX.Y.Z` with the matching - [`CHANGELOG.md`](../../CHANGELOG.md) section (`## [X.Y.Z]` up to the - next version heading) as the release notes and the signed APK attached - directly (as a plain file, not zipped, not through - `actions/upload-artifact`), and redeploys GitHub Pages from the same - build — all in the same run, so the Android release and the live site - always carry the same version. See + and the Android bundle, patches `android/app/build.gradle`'s + `versionName`/`versionCode` to match (derived deterministically from + the semver: `major*10000 + minor*100 + patch` — this edit is never + committed, it only affects the build in this checkout), and builds a + **signed** release APK — signing is required here (unlike + `android-build.yml`'s optional signing for internal testing), since an + unsigned APK can't be installed at all and this release is public. +4. Only once that build has actually succeeded, it publishes everything: + a GitHub Release tagged `vX.Y.Z` with the matching `CHANGELOG.md` + section (`## [X.Y.Z]` up to the next version heading) as the release + notes and the signed APK attached directly (as a plain file, not + zipped, not through `actions/upload-artifact`), and redeploys GitHub + Pages from the same build — all in the same run, so the Android + release and the live site always carry the same version. See [`docs/DEVELOPMENT.md`](../DEVELOPMENT.md) → "Keeping the web and Android releases in sync". Requires the same `ANDROID_KEYSTORE_BASE64`/`ANDROID_KEYSTORE_PASSWORD`/ `ANDROID_KEY_ALIAS`/`ANDROID_KEY_PASSWORD` secrets as `android-build.yml` (see above) — no Firebase secrets needed, this workflow never touches -Firebase. Before running it for a version after `1.0.0`, add a new -`## [X.Y.Z]` entry to `CHANGELOG.md` first so the release has real notes -instead of the generic fallback text. - -### Branch protection and the version-bump PR - -`main` has "Require a pull request before merging" turned on (repo -Settings → Branches), so `release.yml` can't just `git push` the version -bump directly — a straight push gets rejected with `GH006: Protected -branch update failed`. Instead it opens a real PR (`release/vX.Y.Z` → -`main`), approves it, and merges it with `gh pr merge --squash --auto`, -all as the workflow's own `GITHUB_TOKEN`. Two repo settings make this -possible without weakening branch protection at all: - -- **Settings → Actions → General → Workflow permissions**: "Read and - write permissions", plus "Allow GitHub Actions to create and approve - pull requests" — without the second one, `gh pr review --approve` - fails with "GitHub Actions is not permitted to approve pull requests". -- **Settings → General → Pull Requests → "Allow auto-merge"** — without - this, `gh pr merge --auto` errors out. This isn't strictly required: - the step only emits a `::warning::` and moves on if the merge can't be - automated (e.g. auto-merge is off, or a required status check is still - pending), since the GitHub Release, APK, and Pages deploy don't depend - on the PR having landed yet — only on the files already built on disk. - A stray unmerged `release/vX.Y.Z` PR just needs a manual merge - afterward if that happens. - -No bypass list, no PAT, no loosening of "require pull request" — the -workflow follows the same rule everyone else does. +Firebase. + +### Why this workflow never writes to `main` + +The first two real attempts at cutting `1.0.0` both had `release.yml` +bump `package.json`/`android/app/build.gradle` itself and then either +`git push` the commit straight to `main` (rejected outright: `GH006: +Protected branch update failed for refs/heads/main`, since `main` +requires pull requests) or open-and-auto-merge a small PR for it (worked, +but added real complexity — a `pull-requests: write` permission, two +extra repo settings to get right, an `if ! ... then warn` fallback for +when auto-merge isn't configured or a status check is still pending). +Comparing against a sibling project's already-working release pipeline +showed a simpler way out: `release.yml` doesn't need to be able to write +to `main` at all if it never tries to. Bumping the version is now a +completely ordinary, human-reviewed PR like any other change to this +repo — `release.yml` only ever reads what's already there, so it never +touches branch protection in the first place. No bypass list, no PAT, no +special repo settings beyond the signing secrets every release needs +anyway. ## Running the Espresso smoke test locally