diff --git a/.gitea/workflows/beta.yaml b/.gitea/workflows/beta.yaml index 5dd9d44..a9933a4 100644 --- a/.gitea/workflows/beta.yaml +++ b/.gitea/workflows/beta.yaml @@ -3,7 +3,7 @@ name: Beta — Codeberg pre-release # 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 # 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 # mapping) and publishes a Codeberg pre-release with the APK + SHA-256. # @@ -36,53 +36,23 @@ jobs: - name: Checkout 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 - env: - # Codeberg, not Gitea: see the same lookup in release.yaml. - TAG_API: https://codeberg.org/api/v1/repos/jlmakiola/agendula run: | set -e INFO=$(bash scripts/version_info.sh) echo "$INFO" echo "$INFO" >> "$GITHUB_OUTPUT" - VERSION=$(echo "$INFO" | sed -n 's/^version=//p') - BASE=$(echo "$INFO" | sed -n 's/^base_version=//p') - if [ "$(echo "$INFO" | sed -n 's/^channel=//p')" != "beta" ]; then - echo "versionName $VERSION is not a beta — nothing to do." + if [ "$(bash scripts/version_info.sh channel)" != beta ]; then + echo "Not a beta — nothing to do." echo "is_beta=false" >> "$GITHUB_OUTPUT" exit 0 fi - # Fatal on anything but a clean 200/404, as in release.yaml: guessing - # "no tag" during an outage would re-cut a published beta. - tag_status() { - curl -s -o /dev/null -w '%{http_code}' "$TAG_API/git/refs/tags/$1" || echo 000 - } - # 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 + # Done only once its Codeberg pre-release carries the APK, so a failed + # publish is redone by the next push; refused unless newer than the + # latest stable release. + GATE=$(bash scripts/release_gate.sh) + echo "is_beta=${GATE#cut=}" >> "$GITHUB_OUTPUT" beta: needs: detect diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 21927b8..ec1c16d 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -62,17 +62,6 @@ jobs: - name: Resolve version and whether it is a new release 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: | set -e # 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 exit 1 fi - # A tag for this version already existing means the release shipped on - # an earlier push; do nothing. Absent => this merge cuts the release. - # - # Anything other than a clean 200/404 is treated as fatal rather than - # as "no tag". A Codeberg outage or a network blip would otherwise - # 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 + # Tags are read from Codeberg, the canonical forge, by exact name: its + # 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". + GATE=$(bash scripts/release_gate.sh) + echo "is_release=${GATE#cut=}" >> "$GITHUB_OUTPUT" # 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. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index b84ca5a..04f233f 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -124,9 +124,11 @@ and to the stable release. 2. **Optionally verify it on a device** with `scripts/verify-release.sh`, as for a stable release. 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, - creates the `vX.Y.Z-beta.1` tag + a Gitea pre-release (with the R8 mapping) - and publishes the **Codeberg pre-release** with the APK + `.sha256`. + beta whose Codeberg pre-release doesn't carry its APK yet, runs the unit + tests, builds and signs the APK, creates the `vX.Y.Z-beta.1` tag + a Gitea + 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. 5. **Going stable:** set `versionName = "X.Y.Z"` and its `versionCode` (`1.1.0` → `1010099`), then continue from step 2 of @@ -142,8 +144,8 @@ Who gets a beta: - **Play** never. 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 -that already shipped stable, and betas start at 1.1.0 (the 1.0.x codes have no +`detect` refuses one as a backstop), `beta.yaml` refuses a beta that isn't +newer than the latest stable release, and betas start at 1.1.0 (the 1.0.x codes have no room below them). --- diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties index baa28d1..3503532 100644 --- a/gradle/gradle-daemon-jvm.properties +++ b/gradle/gradle-daemon-jvm.properties @@ -1,13 +1,13 @@ #This file is generated by updateDaemonJvm -toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect -toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect -toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect -toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect -toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect -toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect -toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect -toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect -toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect -toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/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\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz +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\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz +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\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-osx-x64-b1163.116.tar.gz +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\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz +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\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-windows-x64-b1163.116.tar.gz toolchainVendor=JETBRAINS toolchainVersion=21 diff --git a/scripts/publish_codeberg_release.sh b/scripts/publish_codeberg_release.sh index 62a4b11..7d626ac 100755 --- a/scripts/publish_codeberg_release.sh +++ b/scripts/publish_codeberg_release.sh @@ -1,24 +1,16 @@ #!/usr/bin/env bash -# Publish TAG on Codeberg, which is canonical for tags and releases, with the -# signed APK and its SHA-256 checksum attached: the direct-download channel -# that Obtainium follows. Shared by .gitea/workflows/release.yaml (stable) and -# beta.yaml (pre-release). Run from the repo checkout: it pushes the tag. +# Publishes TAG on Codeberg with the signed APK and its SHA-256 attached. +# Upserts, so re-runs are safe. A missing TOKEN skips a stable release but fails +# a pre-release, where Codeberg is the only channel. # -# Codeberg push-mirrors branches and tags to Gitea, but releases aren't git -# objects and don't sync in either direction, so this pushes the tag straight -# 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// -# 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) +# Env: TOKEN, API (.../api/v1/repos//), TAG, SHA, PRERELEASE, +# NOTES_FILE, APK, optional APK_OFFLINE (the offline flavor, #39) set -euo pipefail 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." exit 0 fi @@ -42,16 +34,8 @@ if [ -n "${APK_OFFLINE:-}" ]; then ASSETS+=("$ASSET_OFFLINE" "$ASSET_OFFLINE.sha256") fi -# Push the tag to Codeberg ourselves. Under Codeberg-canonical the mirror runs -# Codeberg -> Gitea, so waiting for a tag to arrive from Gitea (what 0.3.2 did) -# 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. +# Push the tag first and create the release without target_commitish: a POST +# naming a commit Codeberg hasn't received yet 500s. HOST=${API#https://}; HOST=${HOST%%/*} REPO=${API#*/repos/} git tag -f "$TAG" "$SHA" @@ -69,38 +53,38 @@ print(json.dumps({ })) PY -# Create (or update) the release. Codeberg 500s on a POST/GET against a tag it -# 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. +# Codeberg 500s on a freshly pushed tag for a few seconds, hence the retries. ID="" 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) 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" \ - -d @"$WORK/payload.json" "$API/releases/$EXIST" - ID="$EXIST"; break + -d @"$WORK/payload.json" "$API/releases/$EXIST" || echo 000) + 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 - 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)) 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. for A in "${ASSETS[@]}"; do OLD=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/$ID/assets" \ - | jq -r --arg n "$A" '.[] | select(.name==$n) | .id') - [ -n "$OLD" ] && curl -s -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$ID/assets/$OLD" >/dev/null || true - curl -s -X POST -H "Authorization: token $TOKEN" \ - -F "attachment=@$WORK/$A" \ - "$API/releases/$ID/assets?name=$A" -o /dev/null -w "asset $A HTTP %{http_code}\n" + | jq -r --arg n "$A" '.[]? | select(.name==$n) | .id' 2>/dev/null || true) + for O in $OLD; do + curl -s -o /dev/null -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$ID/assets/$O" || true + done + 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 echo "Published $TAG to Codeberg." diff --git a/scripts/release_gate.sh b/scripts/release_gate.sh new file mode 100755 index 0000000..b449665 --- /dev/null +++ b/scripts/release_gate.sh @@ -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/ 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 diff --git a/scripts/version_info.sh b/scripts/version_info.sh index 54858ea..2b96e87 100755 --- a/scripts/version_info.sh +++ b/scripts/version_info.sh @@ -29,12 +29,12 @@ NAME=$(grep -oP 'versionName\s*=\s*"\K[^"]+' "$GRADLE" || true) COMMITTED=$(grep -oP 'versionCode\s*=\s*\K[0-9]+' "$GRADLE" || true) [ -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 exit 1 fi -MAJOR=$((10#${BASH_REMATCH[1]})); MINOR=$((10#${BASH_REMATCH[2]})); PATCH=$((10#${BASH_REMATCH[3]})) -BETA=${BASH_REMATCH[5]:+$((10#${BASH_REMATCH[5]}))} +MAJOR=${BASH_REMATCH[1]}; MINOR=${BASH_REMATCH[2]}; PATCH=${BASH_REMATCH[3]} +BETA=${BASH_REMATCH[5]} BASE="$MAJOR.$MINOR.$PATCH" if [ "$MINOR" -gt 99 ] || [ "$PATCH" -gt 99 ]; then