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
226 changes: 226 additions & 0 deletions .github/workflows/release-prepare.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
name: Release prepare

on:
workflow_dispatch:
inputs:
bump_kind:
description: 'How to bump the version (ignored if version_override is set)'
type: choice
options: [build, patch, minor, major]
default: patch
version_override:
description: 'Explicit version, e.g. 0.8.1-rc0 (overrides bump_kind)'
type: string
default: ''
expected_current:
description: 'Optional safety check: fail if current version.properties does not match (e.g. 0.8.0-rc0)'
type: string
default: ''
dry_run:
description: 'When true: compute and validate only. When false: commit, tag, push (triggers release-tag.yml).'
type: boolean
default: true

permissions:
contents: read

concurrency:
group: release-prepare-main
cancel-in-progress: false

jobs:
compute-and-validate:
name: Compute and validate
runs-on: ubuntu-22.04
permissions:
contents: read
outputs:
new_name: ${{ steps.plan.outputs.new_name }}
new_code: ${{ steps.plan.outputs.new_code }}
current_name: ${{ steps.plan.outputs.current_name }}
env:
INPUT_BUMP_KIND: ${{ inputs.bump_kind }}
INPUT_VERSION_OVERRIDE: ${{ inputs.version_override }}
INPUT_EXPECTED_CURRENT: ${{ inputs.expected_current }}
steps:
- name: Guard ref must be main
run: |
if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then
echo "Must dispatch from main, got ${GITHUB_REF}" >&2
exit 1
fi

- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: main
fetch-depth: 0
persist-credentials: false

- name: Compute and validate
id: plan
run: |
set -euo pipefail
args=("--mode=plan")
if [[ -n "${INPUT_VERSION_OVERRIDE}" ]]; then
args+=("--version-override=${INPUT_VERSION_OVERRIDE}")
else
args+=("--bump-kind=${INPUT_BUMP_KIND}")
fi
if [[ -n "${INPUT_EXPECTED_CURRENT}" ]]; then
args+=("--expected-current=${INPUT_EXPECTED_CURRENT}")
fi
./tools/release/bump.sh "${args[@]}" | tee plan.txt
{
grep -E '^current_name=' plan.txt
grep -E '^new_name=' plan.txt
grep -E '^new_code=' plan.txt
} >> "$GITHUB_OUTPUT"

- name: Tag collision check (local + remote)
env:
NEW_NAME: ${{ steps.plan.outputs.new_name }}
run: |
set -euo pipefail
if git rev-parse --verify "refs/tags/v${NEW_NAME}" >/dev/null 2>&1; then
echo "Local tag v${NEW_NAME} already exists" >&2
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/v${NEW_NAME}" >/dev/null; then
echo "Remote tag v${NEW_NAME} already exists" >&2
exit 1
fi

- name: Write step summary
env:
CURRENT_NAME: ${{ steps.plan.outputs.current_name }}
NEW_NAME: ${{ steps.plan.outputs.new_name }}
NEW_CODE: ${{ steps.plan.outputs.new_code }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
{
echo "## Release plan"
echo
echo "| | |"
echo "|---|---|"
echo "| Current | \`${CURRENT_NAME}\` |"
echo "| New | \`${NEW_NAME}\` (code ${NEW_CODE}) |"
echo "| Tag | \`v${NEW_NAME}\` |"
echo "| Dry run | \`${DRY_RUN}\` |"
echo
echo "### bump.sh output"
echo
echo '```'
cat plan.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

push-and-dispatch:
name: Push and dispatch
needs: compute-and-validate
if: ${{ !inputs.dry_run }}
runs-on: ubuntu-22.04
permissions:
contents: read
env:
INPUT_BUMP_KIND: ${{ inputs.bump_kind }}
INPUT_VERSION_OVERRIDE: ${{ inputs.version_override }}
NEW_NAME: ${{ needs.compute-and-validate.outputs.new_name }}
NEW_CODE: ${{ needs.compute-and-validate.outputs.new_code }}
CURRENT_NAME_AT_PLAN: ${{ needs.compute-and-validate.outputs.current_name }}
steps:
- name: Mint App token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 #v3.1.1
with:
client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

- name: Resolve bot identity
id: bot
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
set -euo pipefail
user_id=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq .id)
echo "user_name=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT"
echo "user_email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT"

- name: Checkout main with credentials
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: main
fetch-depth: 0
persist-credentials: true
token: ${{ steps.app-token.outputs.token }}

- name: Verify state still matches plan from Job 1
run: |
set -euo pipefail
./tools/release/bump.sh --mode=check --expected-current="${CURRENT_NAME_AT_PLAN}"

- name: Re-check tag collision (state may have moved between jobs)
run: |
set -euo pipefail
if git rev-parse --verify "refs/tags/v${NEW_NAME}" >/dev/null 2>&1; then
echo "Local tag v${NEW_NAME} already exists" >&2
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/v${NEW_NAME}" >/dev/null; then
echo "Remote tag v${NEW_NAME} already exists" >&2
exit 1
fi

- name: Apply bump
run: |
set -euo pipefail
args=("--mode=write" "--expected-current=${CURRENT_NAME_AT_PLAN}")
if [[ -n "${INPUT_VERSION_OVERRIDE}" ]]; then
args+=("--version-override=${INPUT_VERSION_OVERRIDE}")
else
args+=("--bump-kind=${INPUT_BUMP_KIND}")
fi
./tools/release/bump.sh "${args[@]}"

- name: Verify post-write state matches plan
run: |
set -euo pipefail
./tools/release/bump.sh --mode=check --expected-current="${NEW_NAME}"

- name: Configure git identity
env:
BOT_USER_NAME: ${{ steps.bot.outputs.user_name }}
BOT_USER_EMAIL: ${{ steps.bot.outputs.user_email }}
run: |
git config user.name "${BOT_USER_NAME}"
git config user.email "${BOT_USER_EMAIL}"

- name: Commit and tag
run: |
set -euo pipefail
git add version.properties VERSION
git commit -m "Release: ${NEW_NAME}"
git tag -a "v${NEW_NAME}" -m "Release v${NEW_NAME}"

- name: Atomic push (commit + tag)
run: |
set -euo pipefail
git push --atomic origin "HEAD:refs/heads/main" "refs/tags/v${NEW_NAME}"

- name: Write step summary
run: |
set -euo pipefail
{
echo "## Released"
echo
echo "| | |"
echo "|---|---|"
echo "| Tag | \`v${NEW_NAME}\` |"
echo "| Version code | \`${NEW_CODE}\` |"
echo "| Bump commit | on \`main\` |"
echo "| Downstream | triggered \`release-tag.yml\` |"
echo
echo "Watch the [Tagged releases](../../actions/workflows/release-tag.yml) workflow for the build + upload."
} >> "$GITHUB_STEP_SUMMARY"
72 changes: 48 additions & 24 deletions .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,53 @@ on:
permissions:
contents: read

concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false

jobs:
validate-tag:
name: Validate tag
runs-on: ubuntu-22.04
steps:
- name: Check tag-name format
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
if [[ ! "${REF_NAME}" =~ ^v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}-rc[0-9]{1,2}$ ]]; then
echo "Tag '${REF_NAME}' does not match v<M.m.p-rcN>" >&2
echo "Releases must be cut via the 'Release prepare' workflow." >&2
exit 1
fi

- name: Checkout source code
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
fetch-depth: 1
persist-credentials: false

- name: Verify version.properties matches tag
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
# Strip leading 'v' to get the bare version name.
tag_name="${REF_NAME#v}"
# bump.sh in check mode emits current_name=...
parsed=$(./tools/release/bump.sh --mode=check)
current_name=$(echo "$parsed" | grep -E '^current_name=' | cut -d= -f2)
if [[ "$current_name" != "$tag_name" ]]; then
echo "Tag '${REF_NAME}' does not match version.properties name '$current_name'" >&2
echo "version.properties + VERSION must equal the tag — releases must be cut via 'Release prepare'." >&2
exit 1
fi

release-github:
needs: validate-tag
name: Create GitHub release
permissions:
contents: write
Expand All @@ -41,44 +86,23 @@ jobs:
- name: Setup project and build environment
uses: ./.github/actions/common-setup

- name: Assemble beta APK
if: contains(github.ref_name, '-beta')
run: ./gradlew assembleFossBeta
env:
VERSION: ${{ github.ref }}
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

- name: Assemble production APK
if: "!contains(github.ref_name, '-beta')"
run: ./gradlew assembleFossRelease
env:
VERSION: ${{ github.ref }}
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

- name: Create pre-release
if: contains(github.ref_name, '-beta') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b #v2.5.0
with:
prerelease: true
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
generate_release_notes: true
files: app/build/outputs/apk/foss/beta/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create release
if: "!contains(github.ref_name, '-beta') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)"
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b #v2.5.0
if: "!(github.event_name == 'workflow_dispatch' && inputs.dry_run)"
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda #v3.0.0
with:
prerelease: false
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
generate_release_notes: true
fail_on_unmatched_files: true
files: app/build/outputs/apk/foss/release/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Loading