From 6ebaadaee1005f407f4a265b5c2a1b641772805e Mon Sep 17 00:00:00 2001 From: Bardesss <30936735+bardesss@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:43:51 +0100 Subject: [PATCH 1/5] Release every commit since the last tag and never from an old run --- .github/scripts/next-version-test.sh | 28 ++++++++++++++++++++ .github/scripts/next-version.sh | 35 +++++++++++++++++++++++++ .github/workflows/ci.yml | 2 ++ .github/workflows/release.yml | 38 ++++++++++++++-------------- 4 files changed, 84 insertions(+), 19 deletions(-) create mode 100755 .github/scripts/next-version-test.sh create mode 100755 .github/scripts/next-version.sh diff --git a/.github/scripts/next-version-test.sh b/.github/scripts/next-version-test.sh new file mode 100755 index 0000000..1fee52b --- /dev/null +++ b/.github/scripts/next-version-test.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Checks next-version.sh against the release rules. +set -u +cd "$(dirname "$0")" + +fail=0 +assert_eq() { + if [ "$1" != "$2" ]; then + echo "FAIL: expected '$1', got '$2' (case: $3)" + fail=1 + else + echo "ok: $3" + fi +} + +run() { printf "$2" | ./next-version.sh "$1"; } + +assert_eq "v0.3.1" "$(run v0.3.0 'Fix a')" "patch by default" +assert_eq "v0.3.1" "$(run v0.3.0 'Fix a\nBump x [skip release]')" "skipped commit does not hide a shipped one" +assert_eq "" "$(run v0.3.0 'Bump x [skip release]')" "all skipped ships nothing" +assert_eq "v0.4.0" "$(run v0.3.0 'Fix a\nAdd b [minor]')" "minor beats patch" +assert_eq "v1.0.0" "$(run v0.3.0 'Add b [minor]\nRework [major]')" "major beats minor" +assert_eq "v0.4.0" "$(run v0.3.99 'Fix a')" "patch clamps at 99" +assert_eq "v0.1.0" "$(run '' 'First')" "no tag starts at 0.1.0" +assert_eq "v0.3.1" "$(run v0.3.0 'Fix a\nAdd b [minor] [skip release]')" "skipped minor does not bump" +assert_eq "v1.0.0" "$(run v0.99.0 'Add b [minor]')" "minor clamps at 99" + +exit "$fail" diff --git a/.github/scripts/next-version.sh b/.github/scripts/next-version.sh new file mode 100755 index 0000000..1245147 --- /dev/null +++ b/.github/scripts/next-version.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Prints the next release tag, or nothing when there is nothing to ship. +# Usage: next-version.sh , commit titles one per line on stdin. +# [major] beats [minor] beats a patch bump. Titles with [skip release] are ignored. +set -eu + +latest="${1:-}" +bump=none + +while IFS= read -r title || [ -n "$title" ]; do + case "$title" in + *"[skip release]"*) continue ;; + *"[major]"*) bump=major ;; + *"[minor]"*) [ "$bump" = major ] || bump=minor ;; + *) [ "$bump" != none ] || bump=patch ;; + esac +done + +[ "$bump" != none ] || exit 0 + +if [ -z "$latest" ]; then + echo "v0.1.0" + exit 0 +fi + +IFS=. read -r major minor patch <<< "${latest#v}" +case "$bump" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + *) patch=$((patch + 1)) ;; +esac +# versionCode allows at most 99 for minor and patch +if [ "$patch" -gt 99 ]; then minor=$((minor + 1)); patch=0; fi +if [ "$minor" -gt 99 ]; then major=$((major + 1)); minor=0; fi +echo "v$major.$minor.$patch" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5639a8b..7de7e8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Test the release version script + run: bash .github/scripts/next-version-test.sh - uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0 with: distribution: temurin diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1998418..daffd65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,8 @@ name: Release # Every push to main ships a release. The version is the latest v* tag plus one patch. -# Put [minor] or [major] in the PR title for a bigger bump, or [skip release] to ship nothing. +# Every commit since that tag counts: [minor] or [major] in a PR title makes a bigger bump, and +# [skip release] on every one of them ships nothing. on: push: branches: [main] @@ -31,35 +32,34 @@ jobs: EVENT: ${{ github.event_name }} MESSAGE: ${{ github.event.head_commit.message }} INPUT_VERSION: ${{ inputs.versionName }} + GH_TOKEN: ${{ github.token }} steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 - name: Work out the version id: next run: | if [ "$EVENT" = "workflow_dispatch" ]; then tag="v$INPUT_VERSION" else - title=$(printf '%s\n' "$MESSAGE" | head -n 1) - case "$title" in - *"[skip release]"*) - echo "Skipped by [skip release]" - exit 0 - ;; - esac + tip=$(git ls-remote "https://github.com/$GITHUB_REPOSITORY" refs/heads/main | cut -f1) + if [ "$tip" != "$GITHUB_SHA" ]; then + echo "A newer push releases this" + exit 0 + fi latest=$(git ls-remote --tags --refs "https://github.com/$GITHUB_REPOSITORY" 'v*' \ | sed 's|.*refs/tags/||' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1) if [ -z "$latest" ]; then - tag="v0.1.0" + titles=$(printf '%s\n' "$MESSAGE" | head -n 1) else - IFS=. read -r major minor patch <<< "${latest#v}" - case "$title" in - *"[major]"*) major=$((major + 1)); minor=0; patch=0 ;; - *"[minor]"*) minor=$((minor + 1)); patch=0 ;; - *) patch=$((patch + 1)) ;; - esac - # versionCode allows at most 99 for minor and patch - if [ "$patch" -gt 99 ]; then minor=$((minor + 1)); patch=0; fi - if [ "$minor" -gt 99 ]; then major=$((major + 1)); minor=0; fi - tag="v$major.$minor.$patch" + titles=$(gh api "repos/$GITHUB_REPOSITORY/compare/$latest...$GITHUB_SHA" --paginate \ + --jq '.commits[].commit.message | split("\n")[0]') + fi + tag=$(printf '%s\n' "$titles" | .github/scripts/next-version.sh "$latest") + if [ -z "$tag" ]; then + echo "Skipped by [skip release]" + exit 0 fi fi if ! printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then From b7a651ef55ec07a76d78a5bec0cd61daa77a3ccf Mon Sep 17 00:00:00 2001 From: Bardesss <30936735+bardesss@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:46:49 +0100 Subject: [PATCH 2/5] Ship nothing when no commit is new --- .github/scripts/next-version-test.sh | 10 +++++++++- .github/scripts/next-version.sh | 1 + .github/workflows/release.yml | 7 ++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/scripts/next-version-test.sh b/.github/scripts/next-version-test.sh index 1fee52b..6f5dd52 100755 --- a/.github/scripts/next-version-test.sh +++ b/.github/scripts/next-version-test.sh @@ -13,7 +13,7 @@ assert_eq() { fi } -run() { printf "$2" | ./next-version.sh "$1"; } +run() { printf '%b' "$2" | ./next-version.sh "$1"; } assert_eq "v0.3.1" "$(run v0.3.0 'Fix a')" "patch by default" assert_eq "v0.3.1" "$(run v0.3.0 'Fix a\nBump x [skip release]')" "skipped commit does not hide a shipped one" @@ -25,4 +25,12 @@ assert_eq "v0.1.0" "$(run '' 'First')" "no tag starts at 0.1.0" assert_eq "v0.3.1" "$(run v0.3.0 'Fix a\nAdd b [minor] [skip release]')" "skipped minor does not bump" assert_eq "v1.0.0" "$(run v0.99.0 'Add b [minor]')" "minor clamps at 99" +assert_eq "v1.0.0" "$(run v0.3.0 'Rework [major] +Add b [minor]')" "major wins when it comes first" +assert_eq "" "$(printf '' | ./next-version.sh v0.3.0)" "empty stdin ships nothing" +assert_eq "" "$(run v0.3.0 ' +')" "one blank line ships nothing" +assert_eq "" "$(run '' ' +')" "blank line with no tag ships nothing" + exit "$fail" diff --git a/.github/scripts/next-version.sh b/.github/scripts/next-version.sh index 1245147..d408c12 100755 --- a/.github/scripts/next-version.sh +++ b/.github/scripts/next-version.sh @@ -9,6 +9,7 @@ bump=none while IFS= read -r title || [ -n "$title" ]; do case "$title" in + "") continue ;; *"[skip release]"*) continue ;; *"[major]"*) bump=major ;; *"[minor]"*) [ "$bump" = major ] || bump=minor ;; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index daffd65..e9b4f27 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,12 @@ jobs: if [ "$EVENT" = "workflow_dispatch" ]; then tag="v$INPUT_VERSION" else - tip=$(git ls-remote "https://github.com/$GITHUB_REPOSITORY" refs/heads/main | cut -f1) + tip=$(git ls-remote "https://github.com/$GITHUB_REPOSITORY" refs/heads/main) || exit 1 + tip=${tip%%[[:space:]]*} + if [ -z "$tip" ]; then + echo "Could not read the tip of main" + exit 1 + fi if [ "$tip" != "$GITHUB_SHA" ]; then echo "A newer push releases this" exit 0 From 02377c965f94348dc34e0df989656ddd7d7ed216 Mon Sep 17 00:00:00 2001 From: Bardesss <30936735+bardesss@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:48:41 +0100 Subject: [PATCH 3/5] Run release tests apart from the signing key and sign only on main --- .github/workflows/release.yml | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9b4f27..602847b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,10 +74,31 @@ jobs: echo "Version $tag" echo "tag=$tag" >> "$GITHUB_OUTPUT" - build: + # runs without any secret, so test code never sees the signing key + test: needs: version if: needs.version.outputs.tag != '' runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0 + with: + distribution: temurin + java-version: 21 + - uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 + with: + cache-disabled: true + - name: Run the tests and lint + run: ./gradlew :app:testDebugUnitTest :app:lintDebug + + # signing only happens on main, and the release environment is limited to main + build: + needs: [version, test] + if: needs.version.outputs.tag != '' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: release permissions: contents: read env: @@ -93,9 +114,6 @@ jobs: # the signed build never uses a cache another workflow wrote cache-disabled: true - - name: Run the tests and lint - run: ./gradlew :app:testDebugUnitTest :app:lintDebug - - name: Decode the keystore env: KEYSTORE_B64: ${{ secrets.WICK_KEYSTORE_B64 }} @@ -108,7 +126,7 @@ jobs: WICK_KEYSTORE_PASSWORD: ${{ secrets.WICK_KEYSTORE_PASSWORD }} WICK_KEY_ALIAS: ${{ secrets.WICK_KEY_ALIAS }} WICK_KEY_PASSWORD: ${{ secrets.WICK_KEY_PASSWORD }} - run: ./gradlew :app:assembleRelease -PversionName="${TAG#v}" + run: ./gradlew --no-daemon :app:assembleRelease -PversionName="${TAG#v}" - name: Check the signature run: | From 160ed7a67d3c2d27fa7c6140f0a1361b05e54d27 Mon Sep 17 00:00:00 2001 From: Bardesss <30936735+bardesss@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:51:48 +0100 Subject: [PATCH 4/5] Ignore more kinds of key files --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 343cc13..9d20029 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Android / Gradle .gradle/ +.kotlin/ build/ local.properties captures/ @@ -12,6 +13,10 @@ captures/ *.jks *.keystore keystore.properties +*.b64 +*.p12 +*.pem +.env # IDE .idea/ From 65a3bbf80c25e293ad213e7918bc0726be546b0f Mon Sep 17 00:00:00 2001 From: Bardesss <30936735+bardesss@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:52:29 +0100 Subject: [PATCH 5/5] Say that a dry run builds only from main --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 602847b..b0ef410 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: inputs: versionName: - description: Version to build (dry run, nothing is published) + description: Version to build (dry run from main only, nothing is published) default: 0.0.1 # one release at a time, so each run sees the tag of the one before