diff --git a/.github/scripts/next-version-test.sh b/.github/scripts/next-version-test.sh new file mode 100755 index 0000000..6f5dd52 --- /dev/null +++ b/.github/scripts/next-version-test.sh @@ -0,0 +1,36 @@ +#!/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 '%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" +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" + +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 new file mode 100755 index 0000000..d408c12 --- /dev/null +++ b/.github/scripts/next-version.sh @@ -0,0 +1,36 @@ +#!/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 + "") continue ;; + *"[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..b0ef410 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,14 +1,15 @@ 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] 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 @@ -31,35 +32,39 @@ 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) || 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 + 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 @@ -69,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: @@ -88,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 }} @@ -103,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: | 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/