chore: merge main into release/v1.1.0 (#42)
Beta — Codeberg pre-release / detect (push) Successful in 6s
Beta — Codeberg pre-release / beta (push) Successful in 26m27s

Brings the exact tag checks from #40 into the release branch. beta.yaml runs from the branch's own files, and the old prefix-matching lookup treated v1.1.0-beta.1 as a shipped v1.1.0, so the 1.1.0-beta.2 run was refused.

The one conflict was in publish_codeberg_release.sh: kept #40's stricter upload handling plus the offline APK assets from #41. scripts/release_gate.sh now reports cut=true for 1.1.0-beta.2.

Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/agendula/pulls/42
This commit is contained in:
Jean-Luc Makiolaandmakiolaj committed 2026-10-06 19:19:06 +02:00
1 parent c0eb03e5df
commit 89775e44a7
7 files changed
+125 -140

No files matched your search

+9 -39
View File
@@ -3,7 +3,7 @@ name: Beta — Codeberg pre-release
# A beta is cut by pushing a release branch whose committed versionName is # A beta is cut by pushing a release branch whose committed versionName is
# X.Y.Z-beta.N (see docs/RELEASING.md). Same model as release.yaml: the # X.Y.Z-beta.N (see docs/RELEASING.md). Same model as release.yaml: the
# committed version is the trigger and the vX.Y.Z-beta.N tag is an output. If # committed version is the trigger and the vX.Y.Z-beta.N tag is an output. If
# no tag exists for that version yet, this runs the unit tests, builds and # its Codeberg pre-release doesn't carry the APK yet, this runs the unit tests, builds and
# signs the APK with the app key, records a Gitea pre-release (with the R8 # signs the APK with the app key, records a Gitea pre-release (with the R8
# mapping) and publishes a Codeberg pre-release with the APK + SHA-256. # mapping) and publishes a Codeberg pre-release with the APK + SHA-256.
# #
@@ -36,53 +36,23 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Resolve version and whether it is a new beta - name: Resolve version and whether it still needs publishing
id: v id: v
env:
# Codeberg, not Gitea: see the same lookup in release.yaml.
TAG_API: https://codeberg.org/api/v1/repos/jlmakiola/agendula
run: | run: |
set -e set -e
INFO=$(bash scripts/version_info.sh) INFO=$(bash scripts/version_info.sh)
echo "$INFO" echo "$INFO"
echo "$INFO" >> "$GITHUB_OUTPUT" echo "$INFO" >> "$GITHUB_OUTPUT"
VERSION=$(echo "$INFO" | sed -n 's/^version=//p') if [ "$(bash scripts/version_info.sh channel)" != beta ]; then
BASE=$(echo "$INFO" | sed -n 's/^base_version=//p') echo "Not a beta — nothing to do."
if [ "$(echo "$INFO" | sed -n 's/^channel=//p')" != "beta" ]; then
echo "versionName $VERSION is not a beta — nothing to do."
echo "is_beta=false" >> "$GITHUB_OUTPUT" echo "is_beta=false" >> "$GITHUB_OUTPUT"
exit 0 exit 0
fi fi
# Fatal on anything but a clean 200/404, as in release.yaml: guessing # Done only once its Codeberg pre-release carries the APK, so a failed
# "no tag" during an outage would re-cut a published beta. # publish is redone by the next push; refused unless newer than the
tag_status() { # latest stable release.
curl -s -o /dev/null -w '%{http_code}' "$TAG_API/git/refs/tags/$1" || echo 000 GATE=$(bash scripts/release_gate.sh)
} echo "is_beta=${GATE#cut=}" >> "$GITHUB_OUTPUT"
# A beta of a version that already shipped stable would carry a lower
# versionCode than the stable one: nobody could install it over it.
case "$(tag_status "v$BASE")" in
200)
echo "v$BASE already shipped as stable; a beta of it is pointless. Bump the version." >&2
exit 1 ;;
404) ;;
*) echo "Codeberg tag lookup for v$BASE failed — refusing to guess." >&2; exit 1 ;;
esac
STATUS=$(tag_status "v$VERSION")
case "$STATUS" in
200)
echo "Tag v$VERSION already exists on Codeberg — nothing to release."
echo "is_beta=false" >> "$GITHUB_OUTPUT"
;;
404)
echo "No tag for v$VERSION on Codeberg yet — cutting the beta."
echo "is_beta=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "Codeberg tag lookup for v$VERSION returned HTTP $STATUS." >&2
echo "Refusing to guess: treating this as 'no tag' could re-cut a published beta." >&2
exit 1
;;
esac
beta: beta:
needs: detect needs: detect
+5 -35
View File
@@ -62,17 +62,6 @@ jobs:
- name: Resolve version and whether it is a new release - name: Resolve version and whether it is a new release
id: v id: v
env:
# Tags are read from Codeberg, which is canonical — deliberately NOT
# from the Gitea API this workflow runs on. The Codeberg -> Gitea sync
# is a push mirror, i.e. `git push --mirror`, which deletes refs the
# source does not have. A tag minted here on Gitea is therefore wiped
# by the next sync (Codeberg does not have it yet) and only reappears
# once the tag push at the end of this workflow propagates back.
# Asking Gitea inside that window would report "no tag" for a release
# that already shipped, and cut it a second time.
# Public repo, so this read needs no token.
TAG_API: https://codeberg.org/api/v1/repos/jlmakiola/agendula
run: | run: |
set -e set -e
# versionName -> versionCode, channel and the pre-release flag (set # versionName -> versionCode, channel and the pre-release flag (set
@@ -94,30 +83,11 @@ jobs:
echo "versionName $VERSION on main is a beta. Set the stable version before merging to main." >&2 echo "versionName $VERSION on main is a beta. Set the stable version before merging to main." >&2
exit 1 exit 1
fi fi
# A tag for this version already existing means the release shipped on # Tags are read from Codeberg, the canonical forge, by exact name: its
# an earlier push; do nothing. Absent => this merge cuts the release. # git/refs/tags API matches by prefix, so v1.1.0-beta.1 would read as
# # v1.1.0. A lookup error is fatal rather than read as "no tag".
# Anything other than a clean 200/404 is treated as fatal rather than GATE=$(bash scripts/release_gate.sh)
# as "no tag". A Codeberg outage or a network blip would otherwise echo "is_release=${GATE#cut=}" >> "$GITHUB_OUTPUT"
# read as absent and re-cut a release that has already shipped —
# republishing to F-Droid. Failing here is recoverable; a duplicate
# release is not.
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$TAG_API/git/refs/tags/v$VERSION" || echo 000)
case "$STATUS" in
200)
echo "Tag v$VERSION already exists on Codeberg — nothing to release."
echo "is_release=false" >> "$GITHUB_OUTPUT"
;;
404)
echo "No tag for v$VERSION on Codeberg yet — cutting the release."
echo "is_release=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "Codeberg tag lookup for v$VERSION returned HTTP $STATUS." >&2
echo "Refusing to guess: treating this as 'no tag' could re-cut a shipped release." >&2
exit 1
;;
esac
# Before a single Gradle task runs: F-Droid truncates the in-client # Before a single Gradle task runs: F-Droid truncates the in-client
# changelog, so an over-long one would reach users cut off mid-sentence. # changelog, so an over-long one would reach users cut off mid-sentence.
+7 -5
View File
@@ -124,9 +124,11 @@ and to the stable release.
2. **Optionally verify it on a device** with `scripts/verify-release.sh`, as for 2. **Optionally verify it on a device** with `scripts/verify-release.sh`, as for
a stable release. a stable release.
3. **Push the branch to Codeberg.** When it reaches Gitea, `beta.yaml` sees a 3. **Push the branch to Codeberg.** When it reaches Gitea, `beta.yaml` sees a
beta version without a tag, runs the unit tests, builds and signs the APK, beta whose Codeberg pre-release doesn't carry its APK yet, runs the unit
creates the `vX.Y.Z-beta.1` tag + a Gitea pre-release (with the R8 mapping) tests, builds and signs the APK, creates the `vX.Y.Z-beta.1` tag + a Gitea
and publishes the **Codeberg pre-release** with the APK + `.sha256`. pre-release (with the R8 mapping) and publishes the **Codeberg
pre-release** with the APK + `.sha256`. If that publish fails part-way, the
next push of the branch redoes it.
4. **Next round:** bump to `-beta.2` (and its `versionCode`) and push again. 4. **Next round:** bump to `-beta.2` (and its `versionCode`) and push again.
5. **Going stable:** set `versionName = "X.Y.Z"` and its `versionCode` 5. **Going stable:** set `versionName = "X.Y.Z"` and its `versionCode`
(`1.1.0` → `1010099`), then continue from step 2 of (`1.1.0` → `1010099`), then continue from step 2 of
@@ -142,8 +144,8 @@ Who gets a beta:
- **Play** never. - **Play** never.
Guards: a beta version can't reach `main` (CI fails the PR, and `release.yaml`'s Guards: a beta version can't reach `main` (CI fails the PR, and `release.yaml`'s
`detect` refuses one as a backstop), `beta.yaml` refuses a beta of a version `detect` refuses one as a backstop), `beta.yaml` refuses a beta that isn't
that already shipped stable, and betas start at 1.1.0 (the 1.0.x codes have no newer than the latest stable release, and betas start at 1.1.0 (the 1.0.x codes have no
room below them). room below them).
--- ---
+10 -10
View File
@@ -1,13 +1,13 @@
#This file is generated by updateDaemonJvm #This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect toolchainUrl.FREE_BSD.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-aarch64-b1163.116.tar.gz
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect toolchainUrl.FREE_BSD.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect toolchainUrl.LINUX.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-aarch64-b1163.116.tar.gz
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect toolchainUrl.LINUX.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect toolchainUrl.MAC_OS.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-osx-aarch64-b1163.116.tar.gz
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect toolchainUrl.MAC_OS.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-osx-x64-b1163.116.tar.gz
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect toolchainUrl.UNIX.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-aarch64-b1163.116.tar.gz
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect toolchainUrl.UNIX.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect toolchainUrl.WINDOWS.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-windows-aarch64-b1163.116.tar.gz
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect toolchainUrl.WINDOWS.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-windows-x64-b1163.116.tar.gz
toolchainVendor=JETBRAINS toolchainVendor=JETBRAINS
toolchainVersion=21 toolchainVersion=21
+32 -48
View File
@@ -1,24 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Publish TAG on Codeberg, which is canonical for tags and releases, with the # Publishes TAG on Codeberg with the signed APK and its SHA-256 attached.
# signed APK and its SHA-256 checksum attached: the direct-download channel # Upserts, so re-runs are safe. A missing TOKEN skips a stable release but fails
# that Obtainium follows. Shared by .gitea/workflows/release.yaml (stable) and # a pre-release, where Codeberg is the only channel.
# beta.yaml (pre-release). Run from the repo checkout: it pushes the tag.
# #
# Codeberg push-mirrors branches and tags to Gitea, but releases aren't git # Env: TOKEN, API (.../api/v1/repos/<owner>/<repo>), TAG, SHA, PRERELEASE,
# objects and don't sync in either direction, so this pushes the tag straight # NOTES_FILE, APK, optional APK_OFFLINE (the offline flavor, #39)
# to Codeberg and creates the release over the API. Skips cleanly when TOKEN is
# unset; fails loudly on anything else (see release.yaml for why).
#
# Env:
# TOKEN CODEBERG_RELEASE_TOKEN (write:repository); empty = skip
# API https://codeberg.org/api/v1/repos/<owner>/<repo>
# TAG, SHA vX.Y.Z[-beta.N] and the commit it marks
# PRERELEASE true | false
# NOTES_FILE release notes (scripts/release_notes.sh)
# APK the signed release APK (full flavor)
# APK_OFFLINE optional: the signed offline-flavor APK (issue #39)
set -euo pipefail set -euo pipefail
if [ -z "${TOKEN:-}" ]; then if [ -z "${TOKEN:-}" ]; then
if [ "${PRERELEASE:-}" = true ]; then
echo "CODEBERG_RELEASE_TOKEN not set — a pre-release has nowhere else to go." >&2
exit 1
fi
echo "CODEBERG_RELEASE_TOKEN not set — skipping Codeberg publish." echo "CODEBERG_RELEASE_TOKEN not set — skipping Codeberg publish."
exit 0 exit 0
fi fi
@@ -42,16 +34,8 @@ if [ -n "${APK_OFFLINE:-}" ]; then
ASSETS+=("$ASSET_OFFLINE" "$ASSET_OFFLINE.sha256") ASSETS+=("$ASSET_OFFLINE" "$ASSET_OFFLINE.sha256")
fi fi
# Push the tag to Codeberg ourselves. Under Codeberg-canonical the mirror runs # Push the tag first and create the release without target_commitish: a POST
# Codeberg -> Gitea, so waiting for a tag to arrive from Gitea (what 0.3.2 did) # naming a commit Codeberg hasn't received yet 500s.
# would wait forever. The tag minted on Gitea is in fact *deleted* by the next
# mirror sync until Codeberg has it, so pushing it here is what makes it
# durable on both forges.
#
# Pushing the ref first and creating the release with NO target_commitish is
# deliberate: a release POST carrying a target_commitish for a commit or tag
# Codeberg hasn't received yet is what produced the empty-bodied 500s.
# Attaching to a ref that already exists doesn't need the API to write one.
HOST=${API#https://}; HOST=${HOST%%/*} HOST=${API#https://}; HOST=${HOST%%/*}
REPO=${API#*/repos/} REPO=${API#*/repos/}
git tag -f "$TAG" "$SHA" git tag -f "$TAG" "$SHA"
@@ -69,38 +53,38 @@ print(json.dumps({
})) }))
PY PY
# Create (or update) the release. Codeberg 500s on a POST/GET against a tag it # Codeberg 500s on a freshly pushed tag for a few seconds, hence the retries.
# has only just received (the release request outruns the indexing of the ref
# pushed a moment ago), so a single attempt can fail even though the very same
# call succeeds seconds later. Retry with backoff, and PATCH in place if a
# release already exists (re-run safe). A 5xx body still exits curl 0, so the
# loop, not `set -e`, controls the flow.
ID="" ID=""
for attempt in 1 2 3 4 5 6; do for attempt in 1 2 3 4 5 6; do
EXIST=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty' 2>/dev/null || true) EXIST=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty' 2>/dev/null || true)
if [ -n "$EXIST" ]; then if [ -n "$EXIST" ]; then
curl -s -o /dev/null -w "release PATCH HTTP %{http_code}\n" -X PATCH \ CODE=$(curl -s -o /dev/null -w '%{http_code}' -X PATCH \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d @"$WORK/payload.json" "$API/releases/$EXIST" -d @"$WORK/payload.json" "$API/releases/$EXIST" || echo 000)
ID="$EXIST"; break echo "release PATCH HTTP $CODE"
if [ "$CODE" = 200 ]; then ID="$EXIST"; break; fi
else
CODE=$(curl -s -o "$WORK/response.json" -w '%{http_code}' -X POST \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d @"$WORK/payload.json" "$API/releases" || echo 000)
echo "release POST attempt $attempt HTTP $CODE"
ID=$(jq -r '.id // empty' "$WORK/response.json" 2>/dev/null || true)
[ -n "$ID" ] && break
fi fi
CODE=$(curl -s -o "$WORK/response.json" -w "%{http_code}" -X POST \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d @"$WORK/payload.json" "$API/releases")
echo "release POST attempt $attempt HTTP $CODE"
ID=$(jq -r '.id // empty' "$WORK/response.json" 2>/dev/null || true)
[ -n "$ID" ] && break
sleep $((attempt * 10)) sleep $((attempt * 10))
done done
if [ -z "$ID" ]; then echo "Could not resolve Codeberg release id after retries." >&2; exit 1; fi if [ -z "$ID" ]; then echo "Could not create or update the Codeberg release." >&2; exit 1; fi
# Attach APKs + checksums, replacing any prior asset of the same name. # Attach APKs + checksums, replacing any prior asset of the same name.
for A in "${ASSETS[@]}"; do for A in "${ASSETS[@]}"; do
OLD=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/$ID/assets" \ OLD=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/$ID/assets" \
| jq -r --arg n "$A" '.[] | select(.name==$n) | .id') | jq -r --arg n "$A" '.[]? | select(.name==$n) | .id' 2>/dev/null || true)
[ -n "$OLD" ] && curl -s -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$ID/assets/$OLD" >/dev/null || true for O in $OLD; do
curl -s -X POST -H "Authorization: token $TOKEN" \ curl -s -o /dev/null -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$ID/assets/$O" || true
-F "attachment=@$WORK/$A" \ done
"$API/releases/$ID/assets?name=$A" -o /dev/null -w "asset $A HTTP %{http_code}\n" CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "Authorization: token $TOKEN" \
-F "attachment=@$WORK/$A" "$API/releases/$ID/assets?name=$A" || echo 000)
echo "asset $A HTTP $CODE"
if [ "$CODE" != 201 ]; then echo "Uploading $A failed." >&2; exit 1; fi
done done
echo "Published $TAG to Codeberg." echo "Published $TAG to Codeberg."
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Decides whether the committed version still needs publishing; prints cut=true|false.
#
# stable: no vX.Y.Z tag on Codeberg yet.
# beta: its Codeberg pre-release doesn't carry the APK yet, so a failed
# publish is redone by the next push. Refused unless it is newer than
# every shipped stable release.
#
# Fails on any lookup error rather than guessing.
set -euo pipefail
cd "$(dirname "$0")/.." # repo root
REPO=${REPO:-jlmakiola/agendula}
VERSION=$(bash scripts/version_info.sh version)
BASE=$(bash scripts/version_info.sh base_version)
CHANNEL=$(bash scripts/version_info.sh channel)
# Exact tag names. The Codeberg git/refs/tags/<name> API matches by prefix.
TAGS=$(git ls-remote --tags --refs "https://codeberg.org/$REPO.git" | sed 's#.*refs/tags/##')
if [ "$CHANNEL" = stable ]; then
if grep -qxF "v$VERSION" <<<"$TAGS"; then
echo "Tag v$VERSION already exists on Codeberg — nothing to release." >&2
echo "cut=false"
else
echo "No tag for v$VERSION on Codeberg yet — cutting the release." >&2
echo "cut=true"
fi
exit 0
fi
LATEST=$(grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' <<<"$TAGS" | sed 's/^v//' | sort -V | tail -n1 || true)
if [ -n "$LATEST" ] && [ "$(printf '%s\n%s\n' "$BASE" "$LATEST" | sort -V | tail -n1)" = "$LATEST" ]; then
echo "Beta $VERSION is not newer than the shipped stable $LATEST — bump the version." >&2
exit 1
fi
ASSET="agendula_v${VERSION}.apk"
BODY=$(mktemp); trap 'rm -f "$BODY"' EXIT
STATUS=$(curl -s -o "$BODY" -w '%{http_code}' "https://codeberg.org/api/v1/repos/$REPO/releases/tags/v$VERSION" || echo 000)
case "$STATUS" in
200)
if grep -qF "\"name\":\"$ASSET\"" "$BODY"; then
echo "Pre-release v$VERSION already carries $ASSET — nothing to do." >&2
echo "cut=false"
else
echo "Pre-release v$VERSION exists without $ASSET — publishing it again." >&2
echo "cut=true"
fi
;;
404)
echo "No pre-release for v$VERSION yet — cutting the beta." >&2
echo "cut=true"
;;
*)
echo "Codeberg release lookup for v$VERSION returned HTTP $STATUS — refusing to guess." >&2
exit 1
;;
esac
+3 -3
View File
@@ -29,12 +29,12 @@ NAME=$(grep -oP 'versionName\s*=\s*"\K[^"]+' "$GRADLE" || true)
COMMITTED=$(grep -oP 'versionCode\s*=\s*\K[0-9]+' "$GRADLE" || true) COMMITTED=$(grep -oP 'versionCode\s*=\s*\K[0-9]+' "$GRADLE" || true)
[ -n "$NAME" ] || { echo "No versionName in $GRADLE" >&2; exit 1; } [ -n "$NAME" ] || { echo "No versionName in $GRADLE" >&2; exit 1; }
if [[ ! "$NAME" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-beta\.([0-9]+))?$ ]]; then if [[ ! "$NAME" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-beta\.([1-9][0-9]*))?$ ]]; then
echo "versionName '$NAME' is neither X.Y.Z nor X.Y.Z-beta.N" >&2 echo "versionName '$NAME' is neither X.Y.Z nor X.Y.Z-beta.N" >&2
exit 1 exit 1
fi fi
MAJOR=$((10#${BASH_REMATCH[1]})); MINOR=$((10#${BASH_REMATCH[2]})); PATCH=$((10#${BASH_REMATCH[3]})) MAJOR=${BASH_REMATCH[1]}; MINOR=${BASH_REMATCH[2]}; PATCH=${BASH_REMATCH[3]}
BETA=${BASH_REMATCH[5]:+$((10#${BASH_REMATCH[5]}))} BETA=${BASH_REMATCH[5]}
BASE="$MAJOR.$MINOR.$PATCH" BASE="$MAJOR.$MINOR.$PATCH"
if [ "$MINOR" -gt 99 ] || [ "$PATCH" -gt 99 ]; then if [ "$MINOR" -gt 99 ] || [ "$PATCH" -gt 99 ]; then