chore(ci): Move release process to GitHub Actions

This commit is contained in:
darken
2026-05-01 08:00:15 +02:00
committed by Matthias Urhahn
parent d04abdf80c
commit 5a0ba1b0bd
8 changed files with 926 additions and 464 deletions
+1
View File
@@ -43,3 +43,4 @@ Detailed guidelines are in `.claude/rules/`:
- `pull-requests.md` — PR title and description conventions - `pull-requests.md` — PR title and description conventions
- `agent-instructions.md` — Sub-agent delegation and critical thinking - `agent-instructions.md` — Sub-agent delegation and critical thinking
- `screenshots.md` — Play Store screenshot pipeline, commands, adding new screens - `screenshots.md` — Play Store screenshot pipeline, commands, adding new screens
- `release.md` — Release workflow (`Release prepare` dispatch), inputs, channel mapping, rollback
+63
View File
@@ -0,0 +1,63 @@
# Release Process
Releases are cut via the **Release prepare** workflow (`.github/workflows/release-prepare.yml`). It bumps `version.properties` and `VERSION`, commits to `main`, tags `v<version>`, pushes atomically, and dispatches `release-tag.yml` which builds, signs, and uploads.
## Dispatch
```bash
# Plan only — no commit, no tag, no push.
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=true
# Real cut.
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=false
```
After `dry_run=false`: Job 1 computes + writes the summary, Job 2 pauses for `foss-production` environment approval, then commits/tags/pushes/dispatches. `release-tag.yml` then runs `validate-tag` and the existing `release-github` + `release-gplay` jobs (both with their own environment approvals).
## Inputs
| Input | Default | Notes |
|---|---|---|
| `bump_kind` | `build` | `build` \| `patch` \| `minor` \| `major` |
| `version_type` | `keep-current` | Preserves current `rc`/`beta`. Set explicitly to switch. |
| `version_override` | empty | e.g. `5.1.2-rc0`. Bypasses bump_kind/version_type. |
| `expected_current` | empty | Optional: fail if `version.properties` ≠ this. Useful for tight coordination. |
| `dry_run` | `true` | Default is plan-only. |
Bump rules: `build` increments build; `patch`/`minor`/`major` zero everything to the right of the bumped field. All numeric fields bounded `0..99` (the `versionCode` formula collapses at ≥100).
## Local
```bash
./tools/release/bump.sh --mode=plan --bump-kind=build --version-type=keep-current
./tools/release/bump.sh --mode=check
bats tools/release/bump.bats
```
## Channel mapping
| Tag suffix | FOSS APK | GitHub release | Fastlane lane | Play track | Rollout |
|---|---|---|---|---|---|
| `-beta*` | `assembleFossBeta` | pre-release | `beta` | `beta` | 10% |
| `-rc*` (or anything else) | `assembleFossRelease` | full release | `production` | **`beta`** | 10% |
`lane :production` in `Fastfile` uploads to Play's **beta** track at 10% — manually promoted to production via Play Console.
## Rollback
| Stage reached | Steps |
|---|---|
| Bump on `main`, downstream not started | `git push origin :refs/tags/v<bad>`, `git revert <bump-sha>`, push |
| GitHub release created | Above + `gh release delete v<bad> --yes --cleanup-tag` |
| Play upload completed | Above + halt rollout in Play Console (or `bundle exec fastlane supply --track beta --rollout 0 --version-code <bad-code>`) |
| Job 2 ran but downstream rejected at env approval | Treat as first row — bump+tag are public on `main` regardless of downstream outcome |
`bump.sh` enforces strict `versionCode` monotonicity, so re-using a code is impossible without manually editing `version.properties`.
## Defense in depth
`release-tag.yml` includes `validate-tag` which: (1) regex-checks `github.ref_name`, (2) runs `bump.sh --mode=check`, (3) asserts the parsed name matches the tag. Manual `gh workflow run release-tag.yml --ref vfoo` or hand-pushed tags fail before any build.
## Stuck-dispatch recovery
If Job 2's atomic push lands but `gh workflow run release-tag.yml` fails (rare — Job 1's auth precheck should prevent it), the tag is public but no pipeline runs. Re-dispatch: `gh workflow run release-tag.yml --ref v<new> -f dry_run=false`.
+227
View File
@@ -0,0 +1,227 @@
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: build
version_type:
description: 'Channel for the new version (keep-current preserves current type)'
type: choice
options: [keep-current, rc, beta]
default: keep-current
version_override:
description: 'Explicit version, e.g. 5.1.2-rc0 (overrides bump_kind/version_type)'
type: string
default: ''
expected_current:
description: 'Optional safety check: fail if current version.properties does not match (e.g. 5.1.1-rc0)'
type: string
default: ''
dry_run:
description: 'When true: compute and validate only. When false: commit, tag, push, dispatch 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
actions: read
outputs:
new_name: ${{ steps.plan.outputs.new_name }}
new_code: ${{ steps.plan.outputs.new_code }}
current_name: ${{ steps.plan.outputs.current_name }}
env:
GH_TOKEN: ${{ github.token }}
INPUT_BUMP_KIND: ${{ inputs.bump_kind }}
INPUT_VERSION_TYPE: ${{ inputs.version_type }}
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: Verify gh auth and dispatch capability
run: gh workflow view release-tag.yml > /dev/null
- 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}")
args+=("--version-type=${INPUT_VERSION_TYPE}")
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
environment: foss-production
permissions:
contents: write
actions: write
env:
GH_TOKEN: ${{ github.token }}
INPUT_BUMP_KIND: ${{ inputs.bump_kind }}
INPUT_VERSION_TYPE: ${{ inputs.version_type }}
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: Checkout main with credentials
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: main
fetch-depth: 0
persist-credentials: true
token: ${{ secrets.GITHUB_TOKEN }}
- name: Re-validate after approval wait
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 during approval)
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}")
args+=("--version-type=${INPUT_VERSION_TYPE}")
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
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
- 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: Dispatch release-tag.yml
run: |
set -euo pipefail
gh workflow run release-tag.yml --ref "v${NEW_NAME}" -f dry_run=false
- 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 | dispatched \`release-tag.yml\` |"
echo
echo "Watch the [Tagged releases](../../actions/workflows/release-tag.yml) workflow for the build + upload."
} >> "$GITHUB_STEP_SUMMARY"
+43
View File
@@ -14,8 +14,50 @@ on:
permissions: permissions:
contents: read contents: read
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
jobs: jobs:
validate-tag:
name: Validate tag
runs-on: ubuntu-22.04
steps:
- name: Check tag-name format
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|beta)[0-9]{1,2}$ ]]; then
echo "Tag '${REF_NAME}' does not match v<M.m.p-(rc|beta)n>" >&2
echo "Releases must be cut via the 'Release prepare' workflow." >&2
exit 1
fi
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
fetch-depth: 1
persist-credentials: false
- name: Verify version.properties matches tag
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: release-github:
needs: validate-tag
name: Create GitHub release name: Create GitHub release
permissions: permissions:
contents: write contents: write
@@ -91,6 +133,7 @@ jobs:
run: gh workflow run pages.yml --ref main run: gh workflow run pages.yml --ref main
release-gplay: release-gplay:
needs: validate-tag
name: Create Google Play release name: Create Google Play release
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
environment: gplay-production environment: gplay-production
-463
View File
@@ -1,463 +0,0 @@
#!/bin/bash
# Based on
# * https://gist.github.com/jv-k/703e79306554c26a65a7cfdb9ca119c6
# * https://github.com/jv-k/ver-bump
# █▄▄ █░█ █▀▄▀█ █▀█ ▄▄ █░█ █▀▀ █▀█ █▀ █ █▀█ █▄░█
# █▄█ █▄█ █░▀░█ █▀▀ ░░ ▀▄▀ ██▄ █▀▄ ▄█ █ █▄█ █░▀█
#
#
# Description:
# - This script automates bumping the git software project's version using automation.
# - It does several things that are typically required for releasing a Git repository, like git tagging,
# automatic updating of CHANGELOG.md, and incrementing the version number in various JSON files.
# - Increments / suggests the current software project's version number
# - Adds a Git tag, named after the chosen version number
# - Updates CHANGELOG.md
# - Updates VERSION file
# - Commits files to a new branch
# - Pushes to remote (optionally)
# - Updates "version" : "x.x.x" tag in JSON files if [-v file1 -v file2...] argument is supplied.
#
# Usage:
# ./bump-version.sh [-v <version number>] [-m <release message>] [-j <file1>] [-j <file2>].. [-n] [-p] [-b] [-h]
#
# Options:
# -v <version number> Specify a manual version number
# -m <release message> Custom release message.
# -f <filename.json> Update version number inside JSON files.
# * For multiple files, add a separate -f option for each one,
# * For example: ./bump-version.sh -f src/plugin/package.json -f composer.json
# -p <repository alias> Push commits to remote repository, eg `-p origin`
# -n Don't perform a commit automatically.
# * You may want to do that yourself, for example.
# -b Don't create automatic `release-<version>` branch
# -h Show help message.
#
# Detailed notes:
# The contents of the `VERSION` file which should be a semantic version number such as "1.2.3"
# or even "1.2.3-beta+001.ab"
#
# It pulls a list of changes from git history & prepends to a file called CHANGELOG.md
# under the title of the new version # number, allows the user to review and update the changelist
#
# Creates a Git tag with the version number
#
# - Creates automatic `release-<version>` branch
#
# Commits the new version to the current repository
#
# Optionally pushes the commit to remote repository
#
# Make sure to set execute permissions for the script, eg `$ chmod 755 bump-version.sh`
#
# Credits:
# https://github.com/jv-k/bump-version
#
# - Inspired by the scripts from @pete-otaqui and @mareksuscak
# https://gist.github.com/pete-otaqui/4188238
# https://gist.github.com/mareksuscak/1f206fbc3bb9d97dec9c
#
NOW="$(date +'%B %d, %Y')"
# ANSI/VT100 colours
YELLOW='\033[1;33m'
LIGHTYELLOW='\033[0;33m'
RED='\033[0;31m'
LIGHTRED='\033[1;31m'
GREEN='\033[0;32m'
LIGHTGREEN='\033[1;32m'
BLUE='\033[0;34m'
LIGHTBLUE='\033[1;34m'
PURPLE='\033[0;35m'
LIGHTPURPLE='\033[1;35m'
CYAN='\033[0;36m'
LIGHTCYAN='\033[1;36m'
WHITE='\033[1;37m'
LIGHTGRAY='\033[0;37m'
DARKGRAY='\033[1;30m'
BOLD="\033[1m"
INVERT="\033[7m"
RESET='\033[0m'
# Default options
FLAG_JSON="false"
FLAG_PUSH="false"
I_OK="✅"
I_STOP="🚫"
I_ERROR="❌"
I_END="👋🏻"
S_NORM="${WHITE}"
S_LIGHT="${LIGHTGRAY}"
S_NOTICE="${GREEN}"
S_QUESTION="${YELLOW}"
S_WARN="${LIGHTRED}"
S_ERROR="${RED}"
V_SUGGEST="0.1.2-rc5" # This is suggested in case VERSION file or user supplied version via -v is missing
V_MAJOR="" # 0
V_MINOR="" # 1
V_PATCH="" # 2
V_BUILD_TYPE="" # rc
V_BUILD_COUNTER="" # 5
V_NAME=""
V_CODE=""
SCRIPT_VER="1.0"
GIT_MSG="Release: "
REL_NOTE=""
REL_PREFIX="release/"
PUSH_DEST="origin"
# Show credits & help
usage() {
echo -e "$GREEN" \
"\n █▄▄ █░█ █▀▄▀█ █▀█ ▄▄ █░█ █▀▀ █▀█ █▀ █ █▀█ █▄░█ " \
"\n █▄█ █▄█ █░▀░█ █▀▀ ░░ ▀▄▀ ██▄ █▀▄ ▄█ █ █▄█ █░▀█ " \
"\n\t\t\t\t\t$LIGHTGRAY v${SCRIPT_VER}"
echo -e " ${S_NORM}${BOLD}Usage:${RESET}" \
"\n $0 [-v <version number>] [-m <release message>] [-n] [-p] [-h]" 1>&2
echo -e "\n ${S_NORM}${BOLD}Options:${RESET}"
echo -e " $S_WARN-v$S_NORM <version number>\tSpecify a manual version number"
echo -e " $S_WARN-m$S_NORM <release message>\tCustom release message."
echo -e " $S_WARN-p$S_NORM \t\t\tPush commits to ORIGIN. "
echo -e " $S_WARN-n$S_NORM \t\t\tDon't perform a commit automatically. " \
"\n\t\t\t* You may want to do that manually after checking everything, for example."
echo -e " $S_WARN-b$S_NORM \t\t\tDon't create automatic \`release-<version>\` branch"
echo -e " $S_WARN-h$S_NORM \t\t\tShow this help message. "
echo -e "\n ${S_NORM}${BOLD}Original author: $S_LIGHT https://github.com/jv-t/bump-version $RESET"
echo -e "\n ${S_NORM}${BOLD}Changes by: $S_LIGHT https://github.com/d4rken $RESET\n"
}
# If there are no commits in repo, quit, because you can't tag with zero commits.
check-commits-exist() {
git rev-parse HEAD &>/dev/null
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Your current branch doesn't have any commits yet. Can't tag without at least one commit." >&2
echo
exit 1
fi
}
exit_abnormal() {
echo -e " ${S_LIGHT}––––––"
usage # Show help
exit 1
}
# Process script options
process-arguments() {
local OPTIONS OPTIND OPTARG
# Get positional parameters
JSON_FILES=()
while getopts ":v:p:m:hbn" OPTIONS; do # Note: Adding the first : before the flags takes control of flags and prevents default error msgs.
case "$OPTIONS" in
h)
# Show help
exit_abnormal
;;
v)
# User has supplied a version number
V_USR_SUPPLIED=$OPTARG
;;
m)
REL_NOTE=$OPTARG
# Custom release note
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Release note:" ${S_NORM}"'"$REL_NOTE"'"
;;
p)
FLAG_PUSH=true
PUSH_DEST=${OPTARG} # Replace default with user input
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Pushing to <${S_NORM}${PUSH_DEST}${S_LIGHT}>, as the last action in this script."
;;
n)
FLAG_NOCOMMIT=true
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable commit after tagging."
;;
b)
FLAG_NOBRANCH=true
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable committing to new branch."
;;
\?)
echo -e "\n${I_ERROR}${S_ERROR} Invalid option: ${S_WARN}-$OPTARG" >&2
echo
exit_abnormal
;;
:)
echo -e "\n${I_ERROR}${S_ERROR} Option ${S_WARN}-$OPTARG ${S_ERROR}requires an argument." >&2
echo
exit_abnormal
;;
esac
done
}
# Suggests version from VERSION file, or grabs from user supplied -v <version>.
# If none is set, suggest default from options.
process-version() {
V_RAW=""
V_FILE_REGEX='^([0-9]+\.[0-9]+\.[0-9]+-[a-zA-Z]+[0-9]+) ([0-9]+)$'
V_FILE_RAW="$(cat VERSION)"
if [ -f VERSION ] && [ -s VERSION ] && [[ $V_FILE_RAW =~ $V_FILE_REGEX ]]; then
V_PREV="${BASH_REMATCH[1]}"
V_SUGGEST=$V_PREV
echo -e "\n${S_NOTICE}Current version from <${S_NORM}VERSION${S_NOTICE}> file: ${S_NORM}$V_PREV"
else
echo -ne "\n${S_WARN}The [${S_NORM}VERSION${S_WARN}] "
if [ ! -f VERSION ]; then
echo "VERSION file was not found."
elif [ ! -s VERSION ]; then
echo "VERSION file is empty."
else
echo "could not be parsed."
fi
fi
# If a version number is supplied by the user with [-v <version number>], then use it
if [ -n "$V_USR_SUPPLIED" ]; then
echo -e "\n${S_NOTICE}You selected version using [-v]:" "${S_WARN}${V_USR_SUPPLIED}"
V_RAW="${V_USR_SUPPLIED}"
else
echo -ne "\n${S_QUESTION}Enter a new version number [${S_NORM}$V_SUGGEST${S_QUESTION}]: "
echo -ne "$S_WARN"
read -r V_RAW
fi
if [ -z "$V_RAW" ]; then
V_RAW=$V_PREV
fi
if [ -z "$V_RAW" ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error: No version was supplied (no file, no CLI)\n"
exit_abnormal
fi
SEMVER_REGEX='^([0-9]+)\.([0-9]+)\.([0-9]+)-([a-zA-Z]+)([0-9]+)$'
echo -e "\n${S_NOTICE}Parsing ${V_RAW}"
if [[ $V_RAW =~ $SEMVER_REGEX ]]; then
echo -e "\n${I_OK} ${S_NOTICE} Successfully parsed ${V_RAW} to ${BASH_REMATCH[0]}"
V_MAJOR="${BASH_REMATCH[1]}"
echo "V_MAJOR=$V_MAJOR"
V_MINOR="${BASH_REMATCH[2]}"
echo "V_MINOR=$V_MINOR"
V_PATCH="${BASH_REMATCH[3]}"
echo "V_PATCH=$V_PATCH"
V_BUILD_TYPE="${BASH_REMATCH[4]}"
echo "V_BUILD_TYPE=$V_BUILD_TYPE"
V_BUILD_COUNTER="${BASH_REMATCH[5]}"
echo "V_BUILD_COUNTER=$V_BUILD_COUNTER"
else
echo -e "\n${I_STOP} ${S_ERROR}Error: Failed to parse $V_RAW\n"
exit_abnormal
fi
# If no version was provided, bump the previous version
if [ -z "$V_USR_SUPPLIED" ]; then
if [ "$V_BUILD_COUNTER" -eq "$V_BUILD_COUNTER" ] 2>/dev/null; then # discard stderr (2) output to black hole (suppress it)
V_BUILD_COUNTER=$((V_BUILD_COUNTER + 1)) # Increment
fi
fi
V_NAME="$V_MAJOR.$V_MINOR.$V_PATCH-$V_BUILD_TYPE$V_BUILD_COUNTER"
V_CODE=$((V_MAJOR * 10000000 + V_MINOR * 100000 + V_PATCH * 1000 + V_BUILD_COUNTER * 10))
echo -e "${S_NOTICE}Setting version to [${S_NORM}${V_NAME} (${V_CODE})${S_NOTICE}] ...."
}
# Only tag if tag doesn't already exist
check-tag-exists() {
TAG_CHECK_EXISTS=$(git tag -l v"$V_NAME")
if [ -n "$TAG_CHECK_EXISTS" ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error: A release with that tag version number already exists!\n"
exit 0
fi
}
# $1 : version
# $2 : release note
create-tag() {
if [ -z "$2" ]; then
# Default release note
git tag -a "v$1" -m "Tag version $1."
else
# Custom release note
git tag -a "v$1" -m "$2"
fi
echo -e "\n${I_OK} ${S_NOTICE}Added GIT tag"
}
# Update version.properties which is used by Gradle to generate the `versionName` and `versionCode`
do-version-properties() {
PROPS_FILE_NAME="version.properties"
echo -e "\n${S_NOTICE}Parsing ${PROPS_FILE_NAME}:\n"
V_MAJOR_REGEX='^([a-zA-Z\.]+major)=([0-9]+)$'
V_MINOR_REGEX='^([a-zA-Z\.]+minor)=([0-9]+)$'
V_PATCH_REGEX='^([a-zA-Z\.]+patch)=([0-9]+)$'
V_BUILD_REGEX='^([a-zA-Z\.]+build)=([0-9]+)$'
V_TYPE_REGEX='^([a-zA-Z\.]+type)=(rc|beta)$'
PROPS_FILE_NEW=""
LAST_LINE=$(wc -l <$PROPS_FILE_NAME)
CURRENT_LINE=0
while read -r line; do
CURRENT_LINE=$((CURRENT_LINE + 1))
if [[ $line =~ $V_MAJOR_REGEX ]]; then
updated="${BASH_REMATCH[1]}=${V_MAJOR}"
echo "Found major, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
elif [[ $line =~ $V_MINOR_REGEX ]]; then
updated="${BASH_REMATCH[1]}=${V_MINOR}"
echo "Found minor, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
elif [[ $line =~ $V_PATCH_REGEX ]]; then
updated="${BASH_REMATCH[1]}=${V_PATCH}"
echo "Found patch, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
elif [[ $line =~ $V_BUILD_REGEX ]]; then
updated="${BASH_REMATCH[1]}=${V_BUILD_COUNTER}"
echo "Found build, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
elif [[ $line =~ $V_TYPE_REGEX ]]; then
updated="${BASH_REMATCH[1]}=${V_BUILD_TYPE}"
echo "Found type, replacing: $line -> $updated"
PROPS_FILE_NEW+=$updated
else
PROPS_FILE_NEW+="$line"
fi
if [[ $CURRENT_LINE -ne $LAST_LINE ]]; then
PROPS_FILE_NEW+="\n"
fi
done <"$PROPS_FILE_NAME"
echo -e "$PROPS_FILE_NEW" >"$PROPS_FILE_NAME"
git add "$PROPS_FILE_NAME"
echo -e "\n${I_OK} ${S_NOTICE}Updated [${S_NORM}${PROPS_FILE_NAME}${S_NOTICE}] file"
}
# Update a version file that can be parsed by third-parties, e.g. F-Droid
do-versionfile() {
[ -f VERSION ] && ACTION_MSG="Updated" || ACTION_MSG="Created"
echo "${V_NAME} ${V_CODE}" >VERSION # Create file
echo -e "\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}VERSION${S_NOTICE}] file"
# Stage file for commit
git add VERSION
}
# Does the release branch already exist?
check-branch-exist() {
[ "$FLAG_NOBRANCH" = true ] && return
BRANCH_MSG=$(git rev-parse --verify "${REL_PREFIX}${V_NAME}" 2>&1)
if [ "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error: Branch <${S_NORM}${REL_PREFIX}${V_NAME}${S_ERROR}> already exists!\n"
exit 1
fi
}
# Create release branch if desired
do-branch() {
[ "$FLAG_NOBRANCH" = true ] && return
echo -e "\n${S_NOTICE}Creating new release branch..."
BRANCH_MSG=$(git branch "${REL_PREFIX}${V_NAME}" 2>&1)
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error\n$BRANCH_MSG\n"
exit 1
else
BRANCH_MSG=$(git checkout "${REL_PREFIX}${V_NAME}" 2>&1)
echo -e "\n${I_OK} ${S_NOTICE}${BRANCH_MSG}"
fi
}
# Stage & commit all files modified by this script
do-commit() {
[ "$FLAG_NOCOMMIT" = true ] && return
echo -e "\n${S_NOTICE}Committing..."
COMMIT_MSG=$(git commit -m "${GIT_MSG}" 2>&1)
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_ERROR}Error\n$COMMIT_MSG\n"
exit 1
else
echo -e "\n${I_OK} ${S_NOTICE}$COMMIT_MSG"
fi
}
# Pushes files + tags to remote repo. Changes are staged by earlier functions
do-push() {
[ "$FLAG_NOCOMMIT" = true ] && return
if [ "$FLAG_PUSH" = true ]; then
CONFIRM="Y"
else
echo -ne "\n${S_QUESTION}Push tags to <${S_NORM}${PUSH_DEST}${S_QUESTION}>? [${S_NORM}N/y${S_QUESTION}]: "
read CONFIRM
fi
case "$CONFIRM" in
[yY][eE][sS] | [yY])
echo -e "\n${S_NOTICE}Pushing files + tags to <${S_NORM}${PUSH_DEST}${S_NOTICE}>..."
PUSH_MSG=$(git push "${PUSH_DEST}" v"$V_NAME" 2>&1) # Push new tag
PUSH_MSG+="\n"
PUSH_MSG+=$(git push 2>&1) # Push new tag
if [ ! "$?" -eq 0 ]; then
echo -e "\n${I_STOP} ${S_WARN}Warning\n$PUSH_MSG"
# exit 1
else
echo -e "\n${I_OK} ${S_NOTICE}$PUSH_MSG"
fi
;;
esac
}
#### Initiate Script ###########################
check-commits-exist
# Process and prepare
process-arguments "$@"
process-version
GIT_MSG+="${V_NAME}"
check-branch-exist
check-tag-exists
echo -e "\n${S_LIGHT}––––––"
# Update steps
do-version-properties
do-versionfile
do-branch
do-commit
create-tag "${V_NAME}" "${REL_NOTE}"
do-push
echo -e "\n${S_LIGHT}––––––"
echo -e "\n${I_OK} ${S_NOTICE}"Bumped $([ -n "${V_PREV}" ] && echo "${V_PREV} >" || echo "to ") "$V_NAME"
echo -e "\n${GREEN}Done ${I_END}\n"
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env bats
# Unit tests for bump.sh. Run with: bats tools/release/bump.bats
setup() {
BUMP_SH="${BATS_TEST_DIRNAME}/bump.sh"
TMP_REPO="$(mktemp -d)"
cat > "$TMP_REPO/version.properties" <<'EOF'
### Updated by release.sh ###
project.versioning.major=5
project.versioning.minor=1
project.versioning.patch=1
project.versioning.build=0
project.versioning.type=rc
#############################
EOF
echo "5.1.1-rc0 50101000" > "$TMP_REPO/VERSION"
}
teardown() {
rm -rf "$TMP_REPO"
}
bump() {
"$BUMP_SH" --repo-root="$TMP_REPO" "$@"
}
# ----- check mode ----------------------------------------------------------
@test "check: passes on consistent state" {
run bump --mode=check
[ "$status" -eq 0 ]
[[ "$output" == *"current_name=5.1.1-rc0"* ]]
[[ "$output" == *"current_code=50101000"* ]]
}
@test "check: fails on VERSION/version.properties drift (name)" {
echo "5.1.0-rc0 50100000" > "$TMP_REPO/VERSION"
run bump --mode=check
[ "$status" -ne 0 ]
[[ "$output" == *"drift"* ]]
}
@test "check: fails on VERSION/version.properties drift (code)" {
echo "5.1.1-rc0 99999999" > "$TMP_REPO/VERSION"
run bump --mode=check
[ "$status" -ne 0 ]
[[ "$output" == *"drift"* ]]
}
@test "check: fails on duplicate key" {
echo "project.versioning.major=6" >> "$TMP_REPO/version.properties"
run bump --mode=check
[ "$status" -ne 0 ]
[[ "$output" == *"major"* ]]
[[ "$output" == *"found 2"* ]]
}
@test "check: fails on missing key" {
sed -i '/project\.versioning\.type=/d' "$TMP_REPO/version.properties"
run bump --mode=check
[ "$status" -ne 0 ]
[[ "$output" == *"type"* ]]
[[ "$output" == *"found 0"* ]]
}
@test "check: fails on bad type" {
sed -i 's/^project\.versioning\.type=.*/project.versioning.type=foo/' "$TMP_REPO/version.properties"
run bump --mode=check
[ "$status" -ne 0 ]
}
@test "check: fails on malformed VERSION" {
echo "garbage" > "$TMP_REPO/VERSION"
run bump --mode=check
[ "$status" -ne 0 ]
[[ "$output" == *"VERSION file does not match"* ]]
}
# ----- plan: bump kinds ----------------------------------------------------
@test "plan: build bump increments build" {
run bump --mode=plan --bump-kind=build --version-type=keep-current
[ "$status" -eq 0 ]
[[ "$output" == *"new_name=5.1.1-rc1"* ]]
[[ "$output" == *"new_code=50101010"* ]]
}
@test "plan: patch bump zeros build" {
run bump --mode=plan --bump-kind=patch --version-type=keep-current
[ "$status" -eq 0 ]
[[ "$output" == *"new_name=5.1.2-rc0"* ]]
[[ "$output" == *"new_code=50102000"* ]]
}
@test "plan: minor bump zeros patch and build" {
run bump --mode=plan --bump-kind=minor --version-type=keep-current
[ "$status" -eq 0 ]
[[ "$output" == *"new_name=5.2.0-rc0"* ]]
[[ "$output" == *"new_code=50200000"* ]]
}
@test "plan: major bump zeros minor, patch, build" {
run bump --mode=plan --bump-kind=major --version-type=keep-current
[ "$status" -eq 0 ]
[[ "$output" == *"new_name=6.0.0-rc0"* ]]
[[ "$output" == *"new_code=60000000"* ]]
}
@test "plan: version-type beta switches type" {
run bump --mode=plan --bump-kind=build --version-type=beta
[ "$status" -eq 0 ]
[[ "$output" == *"new_name=5.1.1-beta1"* ]]
}
@test "plan: keep-current preserves type" {
# change current type to beta in fixture
sed -i 's/^project\.versioning\.type=.*/project.versioning.type=beta/' "$TMP_REPO/version.properties"
echo "5.1.1-beta0 50101000" > "$TMP_REPO/VERSION"
run bump --mode=plan --bump-kind=build --version-type=keep-current
[ "$status" -eq 0 ]
[[ "$output" == *"new_name=5.1.1-beta1"* ]]
}
# ----- plan: override ------------------------------------------------------
@test "plan: version-override accepts valid version" {
run bump --mode=plan --version-override=5.2.0-rc0
[ "$status" -eq 0 ]
[[ "$output" == *"new_name=5.2.0-rc0"* ]]
[[ "$output" == *"new_code=50200000"* ]]
}
@test "plan: version-override rejects bad regex (build=100)" {
run bump --mode=plan --version-override=5.1.2-rc100
[ "$status" -ne 0 ]
[[ "$output" == *"does not match"* ]]
}
@test "plan: version-override rejects bad type" {
run bump --mode=plan --version-override=5.1.2-alpha0
[ "$status" -ne 0 ]
}
@test "plan: version-override rejects leading zero" {
run bump --mode=plan --version-override=5.01.0-rc0
[ "$status" -ne 0 ]
[[ "$output" == *"leading zero"* ]]
}
@test "plan: version-override rejects no-op identity" {
run bump --mode=plan --version-override=5.1.1-rc0
[ "$status" -ne 0 ]
[[ "$output" == *"no-op"* ]]
}
@test "plan: version-override rejects monotonicity break" {
run bump --mode=plan --version-override=5.1.0-rc0
[ "$status" -ne 0 ]
[[ "$output" == *"monotonicity"* ]]
}
# ----- plan: bounds --------------------------------------------------------
@test "plan: rejects build overflow when bumping past 99" {
sed -i 's/^project\.versioning\.build=.*/project.versioning.build=99/' "$TMP_REPO/version.properties"
echo "5.1.1-rc99 50101990" > "$TMP_REPO/VERSION"
run bump --mode=plan --bump-kind=build --version-type=keep-current
[ "$status" -ne 0 ]
[[ "$output" == *"out of range"* ]]
}
@test "plan: rejects patch overflow when bumping past 99" {
sed -i 's/^project\.versioning\.patch=.*/project.versioning.patch=99/' "$TMP_REPO/version.properties"
echo "5.1.99-rc0 50199000" > "$TMP_REPO/VERSION"
run bump --mode=plan --bump-kind=patch --version-type=keep-current
[ "$status" -ne 0 ]
[[ "$output" == *"out of range"* ]]
}
# ----- plan: expected-current ----------------------------------------------
@test "plan: expected-current matches passes" {
run bump --mode=plan --bump-kind=build --version-type=keep-current --expected-current=5.1.1-rc0
[ "$status" -eq 0 ]
}
@test "plan: expected-current mismatch fails" {
run bump --mode=plan --bump-kind=build --version-type=keep-current --expected-current=5.0.0-rc0
[ "$status" -ne 0 ]
[[ "$output" == *"expected-current"* ]]
}
# ----- write mode ----------------------------------------------------------
@test "write: rewrites both files correctly" {
run bump --mode=write --bump-kind=build --version-type=keep-current
[ "$status" -eq 0 ]
grep -q '^project\.versioning\.build=1$' "$TMP_REPO/version.properties"
[ "$(cat "$TMP_REPO/VERSION")" = "5.1.1-rc1 50101010" ]
}
@test "write: preserves comment block" {
run bump --mode=write --bump-kind=patch --version-type=keep-current
[ "$status" -eq 0 ]
grep -q '^### Updated by tools/release/bump.sh ###$' "$TMP_REPO/version.properties"
grep -q '^#############################$' "$TMP_REPO/version.properties"
}
@test "write: preserves key order" {
run bump --mode=write --bump-kind=patch --version-type=keep-current
[ "$status" -eq 0 ]
grep -nE '^project\.versioning\.' "$TMP_REPO/version.properties" > "$TMP_REPO/order.txt"
# Five keys, in order: major, minor, patch, build, type.
run cat "$TMP_REPO/order.txt"
[[ "${lines[0]}" == *"major="* ]]
[[ "${lines[1]}" == *"minor="* ]]
[[ "${lines[2]}" == *"patch="* ]]
[[ "${lines[3]}" == *"build="* ]]
[[ "${lines[4]}" == *"type="* ]]
}
@test "write: type switch persists" {
run bump --mode=write --bump-kind=build --version-type=beta
[ "$status" -eq 0 ]
grep -q '^project\.versioning\.type=beta$' "$TMP_REPO/version.properties"
[ "$(cat "$TMP_REPO/VERSION")" = "5.1.1-beta1 50101010" ]
}
@test "write: idempotent re-check passes" {
run bump --mode=write --bump-kind=build --version-type=keep-current
[ "$status" -eq 0 ]
run bump --mode=check
[ "$status" -eq 0 ]
[[ "$output" == *"current_name=5.1.1-rc1"* ]]
}
@test "write: header refresh idempotent if already updated" {
sed -i 's|^### Updated by release\.sh ###$|### Updated by tools/release/bump.sh ###|' "$TMP_REPO/version.properties"
run bump --mode=write --bump-kind=build --version-type=keep-current
[ "$status" -eq 0 ]
[ "$(grep -c '^### Updated by tools/release/bump.sh ###$' "$TMP_REPO/version.properties")" -eq 1 ]
}
# ----- mode parsing --------------------------------------------------------
@test "rejects missing --mode" {
run "$BUMP_SH" --repo-root="$TMP_REPO"
[ "$status" -ne 0 ]
}
@test "rejects invalid --mode" {
run "$BUMP_SH" --repo-root="$TMP_REPO" --mode=foo
[ "$status" -ne 0 ]
}
@test "rejects unknown flag" {
run "$BUMP_SH" --repo-root="$TMP_REPO" --mode=check --frobnicate=yes
[ "$status" -ne 0 ]
}
+332
View File
@@ -0,0 +1,332 @@
#!/usr/bin/env bash
# Source of truth for version bumping. Used by release-prepare.yml and release-tag.yml.
# Mirrors the versionCode formula in buildSrc/src/main/java/ProjectConfig.kt.
set -euo pipefail
usage() {
cat <<'EOF'
Usage: bump.sh --mode=<check|plan|write> [options]
Modes:
check Validate version.properties + VERSION are consistent. No mutation.
plan check + compute the new version per inputs. Print plan to stdout.
write plan + rewrite version.properties and VERSION, verify post-condition.
Options (for plan/write):
--bump-kind=build|patch|minor|major (default: build)
--version-type=keep-current|rc|beta (default: keep-current)
--version-override=<M.m.p-(rc|beta)n> (overrides bump-kind/version-type)
--expected-current=<M.m.p-(rc|beta)n> (fail if current version differs)
--repo-root=<path> (default: current working directory)
Output (plan/write modes):
Human-readable report on stderr; KEY=value pairs on stdout for parsing:
current_name=...
current_code=...
new_name=...
new_code=...
EOF
}
die() {
echo "ERROR: $*" >&2
exit 1
}
log() {
echo "$*" >&2
}
# ----- argument parsing ----------------------------------------------------
mode=""
bump_kind="build"
version_type="keep-current"
version_override=""
expected_current=""
repo_root=""
for arg in "$@"; do
case "$arg" in
--mode=*) mode="${arg#*=}" ;;
--bump-kind=*) bump_kind="${arg#*=}" ;;
--version-type=*) version_type="${arg#*=}" ;;
--version-override=*) version_override="${arg#*=}" ;;
--expected-current=*) expected_current="${arg#*=}" ;;
--repo-root=*) repo_root="${arg#*=}" ;;
-h|--help) usage; exit 0 ;;
*) die "unknown argument: $arg" ;;
esac
done
case "$mode" in
check|plan|write) ;;
"") usage >&2; exit 2 ;;
*) die "invalid --mode: $mode (expected check|plan|write)" ;;
esac
case "$bump_kind" in
build|patch|minor|major) ;;
*) die "invalid --bump-kind: $bump_kind" ;;
esac
case "$version_type" in
keep-current|rc|beta) ;;
*) die "invalid --version-type: $version_type" ;;
esac
if [[ -z "$repo_root" ]]; then
repo_root="$(pwd)"
fi
if [[ ! -d "$repo_root" ]]; then
die "repo root does not exist: $repo_root"
fi
props_file="$repo_root/version.properties"
version_file="$repo_root/VERSION"
# ----- helpers -------------------------------------------------------------
# Reject leading zeros on numeric components (allow plain "0").
no_leading_zero() {
local n="$1" label="$2"
[[ "$n" =~ ^0$ || "$n" =~ ^[1-9][0-9]*$ ]] || die "$label has leading zero or invalid digits: '$n'"
}
bound_0_99() {
local n="$1" label="$2"
[[ "$n" -ge 0 && "$n" -le 99 ]] || die "$label out of range 0..99: $n"
}
parse_name() {
# Sets globals: pn_major, pn_minor, pn_patch, pn_type, pn_build
local name="$1" label="$2"
if [[ ! "$name" =~ ^([0-9]{1,2})\.([0-9]{1,2})\.([0-9]{1,2})-(rc|beta)([0-9]{1,2})$ ]]; then
die "$label does not match <M.m.p-(rc|beta)n>: '$name'"
fi
pn_major="${BASH_REMATCH[1]}"
pn_minor="${BASH_REMATCH[2]}"
pn_patch="${BASH_REMATCH[3]}"
pn_type="${BASH_REMATCH[4]}"
pn_build="${BASH_REMATCH[5]}"
no_leading_zero "$pn_major" "$label major"
no_leading_zero "$pn_minor" "$label minor"
no_leading_zero "$pn_patch" "$label patch"
no_leading_zero "$pn_build" "$label build"
}
# Compute versionCode the same way ProjectConfig.kt does.
compute_code() {
local major="$1" minor="$2" patch="$3" build="$4"
echo $(( major * 10000000 + minor * 100000 + patch * 1000 + build * 10 ))
}
format_name() {
local major="$1" minor="$2" patch="$3" type="$4" build="$5"
echo "${major}.${minor}.${patch}-${type}${build}"
}
# Count exact matches of an anchored regex in a file.
count_matches() {
local pattern="$1" file="$2"
grep -cE "$pattern" "$file" || true
}
# ----- read & validate current state ---------------------------------------
[[ -f "$props_file" ]] || die "missing version.properties at $props_file"
[[ -f "$version_file" ]] || die "missing VERSION file at $version_file"
# Each of the five keys must appear exactly once on its own line.
expect_one() {
local key_pattern="$1" label="$2" file="$3"
local n
n=$(count_matches "$key_pattern" "$file")
[[ "$n" == "1" ]] || die "$label: expected exactly 1 line in $file matching '$key_pattern', found $n"
}
expect_one '^project\.versioning\.major=[0-9]+$' "major" "$props_file"
expect_one '^project\.versioning\.minor=[0-9]+$' "minor" "$props_file"
expect_one '^project\.versioning\.patch=[0-9]+$' "patch" "$props_file"
expect_one '^project\.versioning\.build=[0-9]+$' "build" "$props_file"
expect_one '^project\.versioning\.type=(rc|beta)$' "type" "$props_file"
cur_major=$(grep -E '^project\.versioning\.major=' "$props_file" | cut -d= -f2)
cur_minor=$(grep -E '^project\.versioning\.minor=' "$props_file" | cut -d= -f2)
cur_patch=$(grep -E '^project\.versioning\.patch=' "$props_file" | cut -d= -f2)
cur_build=$(grep -E '^project\.versioning\.build=' "$props_file" | cut -d= -f2)
cur_type=$(grep -E '^project\.versioning\.type=' "$props_file" | cut -d= -f2)
no_leading_zero "$cur_major" "current major"
no_leading_zero "$cur_minor" "current minor"
no_leading_zero "$cur_patch" "current patch"
no_leading_zero "$cur_build" "current build"
bound_0_99 "$cur_major" "current major"
bound_0_99 "$cur_minor" "current minor"
bound_0_99 "$cur_patch" "current patch"
bound_0_99 "$cur_build" "current build"
cur_name=$(format_name "$cur_major" "$cur_minor" "$cur_patch" "$cur_type" "$cur_build")
cur_code=$(compute_code "$cur_major" "$cur_minor" "$cur_patch" "$cur_build")
# VERSION file: exactly one line, "<name> <code>".
version_line_count=$(wc -l < "$version_file" | tr -d ' ')
# Allow trailing newline (line count 1) — reject anything else.
[[ "$version_line_count" -ge 1 && "$version_line_count" -le 1 ]] \
|| die "VERSION file must have exactly one line, found $version_line_count"
version_content=$(head -n1 "$version_file")
if [[ ! "$version_content" =~ ^([^[:space:]]+)\ ([0-9]+)$ ]]; then
die "VERSION file does not match '<name> <code>': '$version_content'"
fi
file_name="${BASH_REMATCH[1]}"
file_code="${BASH_REMATCH[2]}"
# Drift check: VERSION must agree with version.properties.
[[ "$file_name" == "$cur_name" ]] \
|| die "drift: VERSION name '$file_name' != version.properties name '$cur_name'"
[[ "$file_code" == "$cur_code" ]] \
|| die "drift: VERSION code '$file_code' != computed code '$cur_code'"
if [[ -n "$expected_current" ]]; then
[[ "$expected_current" == "$cur_name" ]] \
|| die "expected-current '$expected_current' != actual current '$cur_name'"
fi
log "current: $cur_name (code $cur_code)"
if [[ "$mode" == "check" ]]; then
echo "current_name=$cur_name"
echo "current_code=$cur_code"
exit 0
fi
# ----- compute new version -------------------------------------------------
if [[ -n "$version_override" ]]; then
parse_name "$version_override" "version-override"
new_major="$pn_major"
new_minor="$pn_minor"
new_patch="$pn_patch"
new_type="$pn_type"
new_build="$pn_build"
else
new_major="$cur_major"
new_minor="$cur_minor"
new_patch="$cur_patch"
new_build="$cur_build"
case "$bump_kind" in
build) new_build=$((cur_build + 1)) ;;
patch) new_patch=$((cur_patch + 1)); new_build=0 ;;
minor) new_minor=$((cur_minor + 1)); new_patch=0; new_build=0 ;;
major) new_major=$((cur_major + 1)); new_minor=0; new_patch=0; new_build=0 ;;
esac
case "$version_type" in
keep-current) new_type="$cur_type" ;;
rc|beta) new_type="$version_type" ;;
esac
fi
bound_0_99 "$new_major" "new major"
bound_0_99 "$new_minor" "new minor"
bound_0_99 "$new_patch" "new patch"
bound_0_99 "$new_build" "new build"
case "$new_type" in
rc|beta) ;;
*) die "computed type must be rc or beta, got '$new_type'" ;;
esac
new_name=$(format_name "$new_major" "$new_minor" "$new_patch" "$new_type" "$new_build")
new_code=$(compute_code "$new_major" "$new_minor" "$new_patch" "$new_build")
# Sanity: int range. Android versionCode is Int (max 2_147_483_647).
# At major=99 the code is ~990M, well within range — keep the assertion anyway.
[[ "$new_code" -le 2147483647 ]] || die "new versionCode exceeds Int.MAX_VALUE: $new_code"
[[ "$new_name" != "$cur_name" ]] || die "no-op: new name equals current ($new_name)"
[[ "$new_code" -gt "$cur_code" ]] \
|| die "monotonicity: new code ($new_code) is not greater than current ($cur_code)"
log "new: $new_name (code $new_code)"
# ----- output --------------------------------------------------------------
echo "current_name=$cur_name"
echo "current_code=$cur_code"
echo "new_name=$new_name"
echo "new_code=$new_code"
if [[ "$mode" == "plan" ]]; then
log ""
log "--- diff (plan) ---"
log " version.properties:"
log " major: $cur_major -> $new_major"
log " minor: $cur_minor -> $new_minor"
log " patch: $cur_patch -> $new_patch"
log " build: $cur_build -> $new_build"
log " type: $cur_type -> $new_type"
log " VERSION:"
log " -$cur_name $cur_code"
log " +$new_name $new_code"
exit 0
fi
# ----- write mode ----------------------------------------------------------
# Use sed -E with anchored patterns. Replace each property line in place.
# sed exits 0 even if a pattern doesn't match — we count matches before and
# verify by re-parsing after.
sed_inplace() {
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' -E "$@"
else
sed -i -E "$@"
fi
}
sed_inplace "s#^(project\.versioning\.major=)[0-9]+\$#\1${new_major}#" "$props_file"
sed_inplace "s#^(project\.versioning\.minor=)[0-9]+\$#\1${new_minor}#" "$props_file"
sed_inplace "s#^(project\.versioning\.patch=)[0-9]+\$#\1${new_patch}#" "$props_file"
sed_inplace "s#^(project\.versioning\.build=)[0-9]+\$#\1${new_build}#" "$props_file"
sed_inplace "s#^(project\.versioning\.type=)(rc|beta)\$#\1${new_type}#" "$props_file"
# Header comment refresh (idempotent — only updates if it still says the old text).
sed_inplace "s@^### Updated by release\.sh ###\$@### Updated by tools/release/bump.sh ###@" "$props_file"
# Rewrite VERSION (single-line file).
printf '%s %s\n' "$new_name" "$new_code" > "$version_file"
# ----- post-condition ------------------------------------------------------
# Re-read and verify the rewrite landed exactly as planned.
post_major=$(grep -E '^project\.versioning\.major=' "$props_file" | cut -d= -f2)
post_minor=$(grep -E '^project\.versioning\.minor=' "$props_file" | cut -d= -f2)
post_patch=$(grep -E '^project\.versioning\.patch=' "$props_file" | cut -d= -f2)
post_build=$(grep -E '^project\.versioning\.build=' "$props_file" | cut -d= -f2)
post_type=$(grep -E '^project\.versioning\.type=' "$props_file" | cut -d= -f2)
[[ "$post_major" == "$new_major" ]] || die "post-condition: major did not write ($post_major != $new_major)"
[[ "$post_minor" == "$new_minor" ]] || die "post-condition: minor did not write ($post_minor != $new_minor)"
[[ "$post_patch" == "$new_patch" ]] || die "post-condition: patch did not write ($post_patch != $new_patch)"
[[ "$post_build" == "$new_build" ]] || die "post-condition: build did not write ($post_build != $new_build)"
[[ "$post_type" == "$new_type" ]] || die "post-condition: type did not write ($post_type != $new_type)"
# Each key still appears exactly once.
expect_one '^project\.versioning\.major=[0-9]+$' "post major" "$props_file"
expect_one '^project\.versioning\.minor=[0-9]+$' "post minor" "$props_file"
expect_one '^project\.versioning\.patch=[0-9]+$' "post patch" "$props_file"
expect_one '^project\.versioning\.build=[0-9]+$' "post build" "$props_file"
expect_one '^project\.versioning\.type=(rc|beta)$' "post type" "$props_file"
# VERSION re-parse.
post_version_content=$(head -n1 "$version_file")
[[ "$post_version_content" == "$new_name $new_code" ]] \
|| die "post-condition: VERSION did not write ('$post_version_content' != '$new_name $new_code')"
log "wrote: $new_name (code $new_code)"
+1 -1
View File
@@ -1,4 +1,4 @@
### Updated by release.sh ### ### Updated by tools/release/bump.sh ###
project.versioning.major=5 project.versioning.major=5
project.versioning.minor=1 project.versioning.minor=1
project.versioning.patch=1 project.versioning.patch=1