1 Commits
Author SHA1 Message Date
makiolaj b9e29ad46c Release 1.0.1
Re-release of 1.0.0 under a fresh versionCode for the first Play upload.
2026-09-25 13:06:20 +02:00
29 changed files with 341 additions and 1029 deletions
+1 -19
View File
@@ -37,29 +37,11 @@ jobs:
- name: Reproducible-release invariant - name: Reproducible-release invariant
run: bash scripts/check_reproducible_release.sh run: bash scripts/check_reproducible_release.sh
# The committed versionName must parse (X.Y.Z or X.Y.Z-beta.N) and the
# committed versionCode must be the one it derives: the official F-Droid
# repo builds the tag as committed. And a beta must never reach main,
# where the release pipeline would ship it to F-Droid and Play; betas are
# cut from release/* branches (docs/RELEASING.md).
- name: Committed version is well-formed
env:
BASE: ${{ github.base_ref }}
run: |
set -e
bash scripts/version_info.sh --check
if [ "${BASE#refs/heads/}" = "main" ] && [ "$(bash scripts/version_info.sh channel)" = "beta" ]; then
echo "ERROR: versionName $(bash scripts/version_info.sh version) is a beta." >&2
echo "Set the stable version (and its versionCode) before merging into main." >&2
exit 1
fi
# Also cheap, also always-on. Two failures in one: the script exits # Also cheap, also always-on. Two failures in one: the script exits
# non-zero if this version's changelog is over the 500-character limit, # non-zero if this version's changelog is over the 500-character limit,
# and the porcelain check below catches a version with no committed # and the porcelain check below catches a version with no committed
# changelog, which would otherwise ship the CHANGELOG.md section instead # changelog, which would otherwise ship the CHANGELOG.md section instead
# of a hand-written summary. A beta version passes both: it ships no # of a hand-written summary.
# What's New, so the script writes nothing for it.
- name: Changelog fits the stores, and is committed - name: Changelog fits the stores, and is committed
run: | run: |
set -e set -e
-176
View File
@@ -1,176 +0,0 @@
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
# 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.
#
# Deliberately nothing else. A beta never reaches the F-Droid repos or Play:
# Obtainium hides pre-releases unless a user opts in, the official F-Droid
# recipe only picks up `^v[0-9.]+$` tags, and Codeberg's "latest release"
# skips pre-releases. Pushes of a release branch carrying a stable version
# (the usual state) fall through `detect` and do nothing.
#
# Lives in .gitea/workflows next to release.yaml for the same reason: it needs
# the app signing key, which only the self-hosted Gitea instance holds.
on:
push:
branches: ['release/**']
concurrency:
group: beta
cancel-in-progress: false
jobs:
detect:
# Gitea only; see the same guard in release.yaml.
if: github.repository_owner == 'makiolaj'
runs-on: docker
outputs:
is_beta: ${{ steps.v.outputs.is_beta }}
version: ${{ steps.v.outputs.version }}
version_code: ${{ steps.v.outputs.version_code }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Resolve version and whether it still needs publishing
id: v
run: |
set -e
INFO=$(bash scripts/version_info.sh)
echo "$INFO"
echo "$INFO" >> "$GITHUB_OUTPUT"
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
# 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
if: needs.detect.outputs.is_beta == 'true'
runs-on: docker
env:
ANDROID_HOME: /opt/android-sdk
ANDROID_SDK_ROOT: /opt/android-sdk
VERSION: ${{ needs.detect.outputs.version }}
VERSION_CODE: ${{ needs.detect.outputs.version_code }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
packages: ''
- name: Setup Android SDK cache
uses: actions/cache@v4
with:
path: /opt/android-sdk
key: ${{ runner.os }}-android-sdk-37-36.0.0
- name: Install Android SDK packages
run: |
yes | sdkmanager --licenses >/dev/null || true
sdkmanager \
"platform-tools" \
"platforms;android-37.0" \
"build-tools;36.0.0"
- name: Setup Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Install jq
run: |
set -e
SUDO=""
if command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi
if command -v apt-get >/dev/null 2>&1; then
$SUDO apt-get update
$SUDO apt-get install -y jq
elif command -v apk >/dev/null 2>&1; then
$SUDO apk add --no-cache jq
fi
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Pin versionCode to versionName
run: |
set -e
sed -i "s/versionCode = .*/versionCode = $VERSION_CODE/" app/build.gradle.kts
grep -E 'versionName|versionCode' app/build.gradle.kts
- name: Unit tests
run: ./gradlew testDebugUnitTest
# The real app key, same as a stable release: a beta has to update in
# place to the next beta and to the stable version.
- name: Setup Android Keystore
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
run: |
mkdir -p app
echo "$KEYSTORE_BASE64" | base64 --decode > app/upload-keystore.jks
cat > key.properties <<EOF
storePassword=$KEY_PASSWORD
keyPassword=$KEY_PASSWORD
keyAlias=$KEY_ALIAS
storeFile=upload-keystore.jks
EOF
- name: Build release APK
run: ./gradlew assembleRelease
# Notes = a `## [X.Y.Z-beta.N]` section if there is one, else
# `## [Unreleased]`. Gitea creates the tag at this commit.
- name: Create tag + Gitea pre-release
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
SHA: ${{ github.sha }}
run: |
set -e
bash scripts/release_notes.sh "$VERSION" > release-notes.md
cat release-notes.md
TAG="v$VERSION" PRERELEASE=true NOTES_FILE=release-notes.md \
MAPPING=app/build/outputs/mapping/release/mapping.txt \
bash scripts/publish_gitea_release.sh
# The point of the whole workflow, so NOT continue-on-error.
- name: Publish pre-release to Codeberg
env:
TOKEN: ${{ secrets.CODEBERG_RELEASE_TOKEN }}
API: https://codeberg.org/api/v1/repos/jlmakiola/agendula
SHA: ${{ github.sha }}
run: |
set -e
TAG="v$VERSION" PRERELEASE=true NOTES_FILE=release-notes.md \
APK=app/build/outputs/apk/release/app-release.apk \
bash scripts/publish_codeberg_release.sh
+211 -38
View File
@@ -7,9 +7,7 @@ name: Release — F-Droid repo + Gitea/Codeberg release + Play
# the release on Codeberg with the signed APK + a SHA-256 checksum as a # the release on Codeberg with the signed APK + a SHA-256 checksum as a
# direct-download channel — the tag is an output of the pipeline, not its # direct-download channel — the tag is an output of the pipeline, not its
# trigger. Ordinary merges (no version bump) fall through `detect` and do # trigger. Ordinary merges (no version bump) fall through `detect` and do
# nothing. Betas (X.Y.Z-beta.N) never come through here: beta.yaml cuts them # nothing.
# from release/* branches as Codeberg-only pre-releases, and `detect` refuses
# one that reaches main.
# #
# A trailing `play` job then uploads the App Bundle to Google Play. It is last # A trailing `play` job then uploads the App Bundle to Google Play. It is last
# and separate because Play can reject a good build for reasons the pipeline # and separate because Play can reject a good build for reasons the pipeline
@@ -62,32 +60,60 @@ 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 VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
# while MAJOR was 0) all come from the one script beta.yaml uses too. if [ -z "$VERSION" ]; then echo "No versionName in app/build.gradle.kts" >&2; exit 1; fi
INFO=$(bash scripts/version_info.sh) MAJOR=$(echo "$VERSION" | cut -d. -f1); MINOR=$(echo "$VERSION" | cut -d. -f2); PATCH=$(echo "$VERSION" | cut -d. -f3)
echo "$INFO" MAJOR=${MAJOR:-0}; MINOR=${MINOR:-0}; PATCH=${PATCH:-0}
echo "$INFO" >> "$GITHUB_OUTPUT" VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
VERSION=$(echo "$INFO" | sed -n 's/^version=//p') echo "version=$VERSION" >> "$GITHUB_OUTPUT"
CHANNEL=$(echo "$INFO" | sed -n 's/^channel=//p') echo "version_code=$VERSION_CODE" >> "$GITHUB_OUTPUT"
# Pre-1.0 is not stable yet: mark the Gitea release as a pre-release
# while MAJOR is 0. Graduates to a stable release automatically at 1.0.0.
if [ "$MAJOR" = "0" ]; then PRERELEASE=true; else PRERELEASE=false; fi
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
echo "Resolved version $VERSION (code $VERSION_CODE, prerelease=$PRERELEASE)"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "Manual dispatch — re-sign path, not a release." echo "Manual dispatch — re-sign path, not a release."
echo "is_release=false" >> "$GITHUB_OUTPUT" echo "is_release=false" >> "$GITHUB_OUTPUT"
exit 0 exit 0
fi fi
# Betas ship from release/* branches via beta.yaml and must never be # A tag for this version already existing means the release shipped on
# cut here: this path publishes to F-Droid and Play. CI blocks such a # an earlier push; do nothing. Absent => this merge cuts the release.
# PR into main; this is the backstop if one gets through anyway. #
if [ "$CHANNEL" != "stable" ]; then # Anything other than a clean 200/404 is treated as fatal rather than
echo "versionName $VERSION on main is a beta. Set the stable version before merging to main." >&2 # 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 exit 1
fi ;;
# Tags are read from Codeberg, the canonical forge, by exact name: its esac
# 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 # 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.
@@ -170,8 +196,8 @@ jobs:
run: chmod +x ./gradlew run: chmod +x ./gradlew
# The committed versionName is the source of truth. Pin versionCode to the # The committed versionName is the source of truth. Pin versionCode to the
# value scripts/version_info.sh derives from it, so the published APK's # value derived from it so the published APK's code is always
# code follows the scheme even if the committed code was forgotten. # MAJOR*10000 + MINOR*100 + PATCH even if the committed code was forgotten.
- name: Pin versionCode to versionName - name: Pin versionCode to versionName
if: env.IS_RELEASE == 'true' if: env.IS_RELEASE == 'true'
run: | run: |
@@ -306,8 +332,6 @@ jobs:
# Creating it with target_commitish makes Gitea create the vX.Y.Z tag at # Creating it with target_commitish makes Gitea create the vX.Y.Z tag at
# this commit, so the tag only ever marks a fully-shipped release (and a # this commit, so the tag only ever marks a fully-shipped release (and a
# failure before here leaves no tag, so re-running the workflow retries). # failure before here leaves no tag, so re-running the workflow retries).
# Also attaches the R8 mapping (best-effort) so user crash stacktraces
# stay deobfuscatable. Notes = this version's CHANGELOG section.
- name: Create tag + Gitea release - name: Create tag + Gitea release
if: env.IS_RELEASE == 'true' if: env.IS_RELEASE == 'true'
env: env:
@@ -316,17 +340,85 @@ jobs:
SHA: ${{ github.sha }} SHA: ${{ github.sha }}
run: | run: |
set -e set -e
bash scripts/release_notes.sh "$VERSION" > release-notes.md TAG="v$VERSION"
TAG="v$VERSION" NOTES_FILE=release-notes.md \ # Notes = this version's CHANGELOG section.
MAPPING=app/build/outputs/mapping/release/mapping.txt \ awk -v ver="$VERSION" '
bash scripts/publish_gitea_release.sh $0 ~ "^## \\[" ver "\\]" { flag = 1; next }
/^## \[/ { flag = 0 }
flag' CHANGELOG.md > release-notes.md
sed -i -e '/./,$!d' release-notes.md
if [ ! -s release-notes.md ]; then
echo "_No changelog entry for ${VERSION} — see CHANGELOG.md._" > release-notes.md
fi
python3 - "$TAG" "$SHA" "$PRERELEASE" <<'PY' > payload.json
import json, sys
print(json.dumps({
"tag_name": sys.argv[1],
"target_commitish": sys.argv[2],
"name": sys.argv[1],
"body": open("release-notes.md").read(),
"draft": False,
# Pre-1.0 releases are flagged as pre-releases (see detect job).
"prerelease": sys.argv[3] == "true",
}))
PY
# Upsert (re-run safe): PATCH if a release for the tag already exists,
# else POST a new one (which also creates the tag at target_commitish).
curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" > existing.json
ID=$(jq -r '.id // empty' existing.json 2>/dev/null || true)
if [ -n "$ID" ]; then
CODE=$(curl -s -o response.json -w '%{http_code}' -X PATCH \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d @payload.json "$API/releases/$ID")
OK=200
else
CODE=$(curl -s -o response.json -w '%{http_code}' -X POST \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d @payload.json "$API/releases")
OK=201
fi
cat response.json
if [ "$CODE" != "$OK" ]; then
echo "Release upsert failed with HTTP $CODE (expected $OK)" >&2
exit 1
fi
echo "Created/updated release $TAG at $SHA"
# Archive the R8 mapping so user crash stacktraces stay deobfuscatable.
# Attached to the release (it's not an APK, so it fits the no-binaries
# rule). Best-effort: never fail a release over it.
- name: Attach R8 mapping to Gitea release
if: env.IS_RELEASE == 'true'
continue-on-error: true
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
run: |
set -e
MAP="app/build/outputs/mapping/release/mapping.txt"
if [ ! -f "$MAP" ]; then echo "No mapping.txt (R8 off?) — skipping."; exit 0; fi
TAG="v$VERSION"
ASSET="mapping-${VERSION}.txt.gz"
gzip -c "$MAP" > "/tmp/$ASSET"
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
if [ -z "$ID" ]; then echo "Could not resolve release id — skipping."; exit 0; fi
# Replace any prior asset of the same name (re-run safe).
OLD=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/$ID/assets" \
| jq -r --arg n "$ASSET" '.[] | 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=@/tmp/$ASSET" \
"$API/releases/$ID/assets?name=$ASSET" -o /dev/null -w "asset upload HTTP %{http_code}\n"
# Publish the release on Codeberg, which is canonical for tags and # Publish the release on Codeberg, which is canonical for tags and
# releases (see docs/RELEASING.md): push the tag there and attach the # releases (see docs/RELEASING.md). Codeberg push-mirrors branches + tags
# signed APK plus a SHA-256 checksum as the direct-download channel for # to Gitea, but releases aren't git objects and don't sync in either
# users who don't want F-Droid. The APK is identical to the F-Droid one # direction — so this step pushes the tag straight to Codeberg and creates
# (same app key), so this adds no trust surface. Needs the # the release there over the API, attaching the signed APK plus a SHA-256
# CODEBERG_RELEASE_TOKEN secret; skips cleanly if unset. # checksum as the direct-download channel for users who don't want
# F-Droid. The APK is identical to the F-Droid one (same app key), so this
# adds no trust surface. Needs the CODEBERG_RELEASE_TOKEN secret; skips
# cleanly if unset.
- name: Publish release to Codeberg - name: Publish release to Codeberg
if: env.IS_RELEASE == 'true' if: env.IS_RELEASE == 'true'
# NOT continue-on-error: this step reported green through 0.2.1, 0.2.2, # NOT continue-on-error: this step reported green through 0.2.1, 0.2.2,
@@ -339,10 +431,91 @@ jobs:
SHA: ${{ github.sha }} SHA: ${{ github.sha }}
run: | run: |
set -e set -e
[ -s release-notes.md ] || bash scripts/release_notes.sh "$VERSION" > release-notes.md if [ -z "${TOKEN:-}" ]; then
TAG="v$VERSION" NOTES_FILE=release-notes.md \ echo "CODEBERG_RELEASE_TOKEN not set — skipping Codeberg publish."
APK=app/build/outputs/apk/release/app-release.apk \ exit 0
bash scripts/publish_codeberg_release.sh fi
TAG="v$VERSION"
APK="app/build/outputs/apk/release/app-release.apk"
if [ ! -f "$APK" ]; then echo "No release APK found — skipping." >&2; exit 1; fi
ASSET_APK="agendula_v${VERSION}.apk"
ASSET_SUM="${ASSET_APK}.sha256"
cp "$APK" "/tmp/$ASSET_APK"
( cd /tmp && sha256sum "$ASSET_APK" > "$ASSET_SUM" )
# Release notes: reuse the section extracted for the Gitea release,
# fall back to the CHANGELOG entry if that step's file is gone.
if [ ! -s release-notes.md ]; then
awk -v ver="$VERSION" '
$0 ~ "^## \\[" ver "\\]" { flag = 1; next }
/^## \[/ { flag = 0 }
flag' CHANGELOG.md > release-notes.md
sed -i -e '/./,$!d' release-notes.md
fi
[ -s release-notes.md ] || echo "_See CHANGELOG.md for ${VERSION}._" > release-notes.md
# Push the tag to Codeberg ourselves. Under Codeberg-canonical the
# mirror runs Codeberg -> Gitea, so waiting for a tag to arrive here
# from Gitea (what 0.3.2 did) would wait forever. The tag this
# pipeline 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 attaching 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.
git tag -f "$TAG" "$SHA"
git push -f "https://jlmakiola:${TOKEN}@codeberg.org/jlmakiola/agendula.git" \
"refs/tags/$TAG"
python3 - "$TAG" "$PRERELEASE" <<'PY' > cb-payload.json
import json, sys
tag, pre = sys.argv[1:3]
print(json.dumps({
"tag_name": tag,
"name": tag,
"body": open("release-notes.md").read(),
"draft": False,
# Pre-1.0 releases are flagged as pre-releases (see detect job).
"prerelease": pre == "true",
}))
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 we 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, so re-running never disturbs a published release). A 5xx body
# still exits curl 0, so the loop, not `set -e`, controls the flow.
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 \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d @cb-payload.json "$API/releases/$EXIST"
ID="$EXIST"; break
fi
CODE=$(curl -s -o cb-response.json -w "%{http_code}" -X POST \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d @cb-payload.json "$API/releases")
echo "release POST attempt $attempt HTTP $CODE"
ID=$(jq -r '.id // empty' cb-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
# Attach APK + checksum, replacing any prior asset of the same name.
for A in "$ASSET_APK" "$ASSET_SUM"; 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=@/tmp/$A" \
"$API/releases/$ID/assets?name=$A" -o /dev/null -w "asset $A HTTP %{http_code}\n"
done
echo "Published $TAG to Codeberg."
# Play takes an App Bundle, not the APK: a second artifact from the same # Play takes an App Bundle, not the APK: a second artifact from the same
# source and signing config. Play treats the release key only as the # source and signing config. Play treats the release key only as the
+2 -11
View File
@@ -1,17 +1,8 @@
name: Renovate name: Renovate
on: on:
# Every merge to main. Mirror syncs from Codeberg fire push events here (the # Weekly sweep. Mondays 05:00 UTC — this cron owns the cadence; the repo's
# same trigger release.yaml relies on), so a merged Renovate PR is followed # renovate.json5 deliberately has no internal schedule (avoids double-gating).
# within minutes by a run that rebases the sibling PRs it just conflicted —
# most bumps touch gradle/libs.versions.toml. Outside renovate.json5's
# `schedule` window such a run only maintains existing branches
# (updateNotScheduled), it never opens new PRs. Renovate's own rebases push
# to renovate/* branches, not main, so this cannot loop.
push:
branches: [main]
# Weekly sweep for new updates. Mondays 05:00 UTC, inside the schedule window
# in renovate.json5 — keep the two in step.
schedule: schedule:
- cron: '0 5 * * 1' - cron: '0 5 * * 1'
# Manual run for an on-demand sweep from the Actions tab. # Manual run for an on-demand sweep from the Actions tab.
+5
View File
@@ -7,6 +7,11 @@ All notable changes to this project are documented here. The format follows
## [Unreleased] ## [Unreleased]
## [1.0.1] - 2026-09-25
### Changed
- First release on Google Play. No changes to the app since 1.0.0.
## [1.0.0] - 2026-09-21 ## [1.0.0] - 2026-09-21
### Added ### Added
-10
View File
@@ -20,14 +20,6 @@ required.</p>
<a href="https://ko-fi.com/jeanlucmakiola"><img src="https://storage.ko-fi.com/cdn/brandasset/v2/support_me_on_kofi_badge_beige.png" alt="Support me on Ko-fi" height="56"></a> <a href="https://ko-fi.com/jeanlucmakiola"><img src="https://storage.ko-fi.com/cdn/brandasset/v2/support_me_on_kofi_badge_beige.png" alt="Support me on Ko-fi" height="56"></a>
</p> </p>
<p>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/01.png" alt="Home screen: today's progress, overdue and upcoming tasks, and your lists" width="18%">
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/02.png" alt="Onboarding: sync with your own CalDAV server" width="18%">
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/03.png" alt="Upcoming list with a task expanded into its subtasks" width="18%">
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/04.png" alt="Task detail: a monthly recurring task with a reminder and priority" width="18%">
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/05.png" alt="Reminder notification with Done and Snooze actions" width="18%">
</p>
</div> </div>
Agendula is the task-list sibling to [Calendula](https://codeberg.org/jlmakiola/calendula). Agendula is the task-list sibling to [Calendula](https://codeberg.org/jlmakiola/calendula).
@@ -119,8 +111,6 @@ APK and a `.sha256` checksum attached — the same APK the F-Droid repository
serves. For automatic updates from there, use serves. For automatic updates from there, use
**[Obtainium](https://github.com/ImranR98/Obtainium)** and **[Obtainium](https://github.com/ImranR98/Obtainium)** and
**[add Agendula in one tap](https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://codeberg.org/jlmakiola/agendula)**. **[add Agendula in one tap](https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://codeberg.org/jlmakiola/agendula)**.
Betas of upcoming versions are published there too, as pre-releases; to test
them, switch on *Include prereleases* for Agendula in Obtainium.
### Build from source ### Build from source
+6 -8
View File
@@ -26,14 +26,12 @@ android {
// These committed values ARE the source of truth for a release: merging // These committed values ARE the source of truth for a release: merging
// a bumped versionName into main triggers .gitea/workflows/release.yaml, // a bumped versionName into main triggers .gitea/workflows/release.yaml,
// which builds this version and then creates the matching vX.Y.Z tag + // which builds this version and then creates the matching vX.Y.Z tag +
// release itself. A versionName of X.Y.Z-beta.N pushed to a release/* // release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 +
// branch instead cuts a Codeberg-only pre-release (beta.yaml). // PATCH from versionName, e.g. 1.0.0 -> 10000). Releases were flagged as
// versionCode is derived from versionName by scripts/version_info.sh // pre-releases while MAJOR was 0; 1.0.0 is the first stable one, and the
// (1.0.x: 1.0.0 -> 10000; from 1.1.0: 1.1.0-beta.1 -> 1010001, // pipeline graduates it on its own. See docs/RELEASING.md.
// 1.1.0 -> 1010099), and CI fails if the committed one doesn't match. versionCode = 10001
// See docs/RELEASING.md. versionName = "1.0.1"
versionCode = 10000
versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+6 -246
View File
@@ -82,7 +82,7 @@
<string name="lists_header">Listen</string> <string name="lists_header">Listen</string>
<string name="new_task">Neue Aufgabe</string> <string name="new_task">Neue Aufgabe</string>
<string name="lists_failure">Deine Aufgaben konnten nicht gelesen werden.</string> <string name="lists_failure">Deine Aufgaben konnten nicht gelesen werden.</string>
<string name="lists_empty">Keine Aufgaben-Listen bisher.</string> <string name="lists_empty">Keine Aufgaben-Listen bisher. Füge eine in deiner Aufgaben-App hinzu oder mit der + Schaltfläche.</string>
<string name="smart_today">Heute</string> <string name="smart_today">Heute</string>
<string name="smart_overdue">Überfällig</string> <string name="smart_overdue">Überfällig</string>
<string name="smart_upcoming">Anstehend</string> <string name="smart_upcoming">Anstehend</string>
@@ -99,23 +99,23 @@
<string name="reminder_channel_name">Aufgaben Erinnerungen</string> <string name="reminder_channel_name">Aufgaben Erinnerungen</string>
<string name="reminder_channel_desc">Benachrichtigungen für fällige Aufgaben.</string> <string name="reminder_channel_desc">Benachrichtigungen für fällige Aufgaben.</string>
<string name="onboarding_no_provider_title">Eine Task-App wird benötigt</string> <string name="onboarding_no_provider_title">Eine Task-App wird benötigt</string>
<string name="onboarding_no_provider_body">Es ist keine App installiert, in der man Aufgaben speichern könnte. Installieren OpenTasks oder tasks.org oder wechsel zu Agendulas eigenem Speicher.</string> <string name="onboarding_no_provider_body">Agendula zeigt und bearbeitet die Aufgaben, die in OpenTasks oder tasks.org gespeichert sind und per DAVx5 synchronisiert werden. Installiere eine davon, um zu starten.</string>
<string name="onboarding_permission_title">Erlaube Zugriff auf deine Aufgaben</string> <string name="onboarding_permission_title">Erlaube Zugriff auf deine Aufgaben</string>
<string name="onboarding_permission_body">Agendula benötigt die Berechtigung, deine Aufgaben zu lesen und zu schreiben. Das ist die einzige Berechtigung, die es jemals anfragt.</string> <string name="onboarding_permission_body">Agendula benötigt die Berechtigung, deine Aufgaben zu lesen und zu schreiben. Das ist die einzige Berechtigung, die es jemals anfragt.</string>
<string name="onboarding_permission_button">Zugriff gewähren</string> <string name="onboarding_permission_button">Zugriff gewähren</string>
<string name="reminder_onboarding_title">Verpasse nichts, was fällig ist</string> <string name="reminder_onboarding_title">Verpasse nichts, was fällig ist</string>
<string name="reminder_onboarding_body">Agendula benachrichtigt dich, wenn etwas fällig wird, also muss nichts in deinem Kopf leben.</string> <string name="reminder_onboarding_body">Aufgaben-Apps senden selbst keine Erinnerungen, daher liefert Agendula sie für dich. Schalte sie ein, um eine Benachrichtigung zu erhalten, wenn eine Aufgabe fällig ist.</string>
<string name="reminder_onboarding_enable_button">Erinnerungen aktivieren</string> <string name="reminder_onboarding_enable_button">Erinnerungen aktivieren</string>
<string name="reminder_onboarding_skip_button">Nicht jetzt</string> <string name="reminder_onboarding_skip_button">Nicht jetzt</string>
<string name="reminder_benefit_delivery_title">Agendula erinnert dich</string> <string name="reminder_benefit_delivery_title">Agendula erinnert dich</string>
<string name="reminder_benefit_delivery_body">Jede Aufgaben mit Fälligkeitsdatum bekommt seine eigene Benachrichtigung.</string> <string name="reminder_benefit_delivery_body">Es plant eine Benachrichtigung für jede Aufgabe mit einem Fälligkeitsdatum.</string>
<string name="reminder_benefit_timing_title">Auf deinem Zeitplan</string> <string name="reminder_benefit_timing_title">Auf deinem Zeitplan</string>
<string name="reminder_benefit_timing_body">Wähle, wie weit im Voraus du erinnert werden möchtest — für alles oder pro Liste.</string> <string name="reminder_benefit_timing_body">Wähle in den Einstellungen, wie weit im Voraus du erinnert werden möchtest.</string>
<string name="reminder_benefit_reversible_title">Jederzeit änderbar</string> <string name="reminder_benefit_reversible_title">Jederzeit änderbar</string>
<string name="reminder_benefit_reversible_body">Schalte Erinnerungen aus, wann immer du willst — es ist nur ein Schalter.</string> <string name="reminder_benefit_reversible_body">Schalte Erinnerungen aus, wann immer du willst — es ist nur ein Schalter.</string>
<string name="settings_title">Einstellungen</string> <string name="settings_title">Einstellungen</string>
<string name="settings_section_appearance">Darstellung</string> <string name="settings_section_appearance">Darstellung</string>
<string name="settings_appearance_subtitle">Theme, Farbe und Zeitformat</string> <string name="settings_appearance_subtitle">Theme und Farbe</string>
<string name="settings_section_task_form">Aufgaben-Formular</string> <string name="settings_section_task_form">Aufgaben-Formular</string>
<string name="settings_task_form_subtitle">Standard Felder, Liste und Unter-Aufgaben</string> <string name="settings_task_form_subtitle">Standard Felder, Liste und Unter-Aufgaben</string>
<string name="settings_reminders_subtitle">Benachrichtigungen für fällige Aufgaben</string> <string name="settings_reminders_subtitle">Benachrichtigungen für fällige Aufgaben</string>
@@ -169,244 +169,4 @@
</plurals> </plurals>
<string name="settings_translate">Hilf beim Übersetzen</string> <string name="settings_translate">Hilf beim Übersetzen</string>
<string name="settings_translate_hint">Füge eine Sprache auf Weblate hinzu oder verbessere sie</string> <string name="settings_translate_hint">Füge eine Sprache auf Weblate hinzu oder verbessere sie</string>
<string name="task_completed">Fertig</string>
<string name="cd_repeats">Wiederholt</string>
<string name="shortcut_new_task">Neue Aufgabe</string>
<string name="shortcut_today">Heute</string>
<string name="qs_tile_new_task">Neue Aufgabe</string>
<string name="lists_reorder">Liste umsortieren</string>
<string name="lists_reorder_done">Ferig umsortiert</string>
<string name="lists_reorder_handle">Ziehen zum Verschieben</string>
<string name="sort_title">Sortieren nach</string>
<string name="sort_due">Fälligkeitsdatum</string>
<string name="sort_priority">Priorität</string>
<string name="sort_title_alpha">Titel</string>
<string name="sort_created">Kürzlich hinzugefügt</string>
<plurals name="clear_completed_body">
<item quantity="one">%1$d erledigte Aufgabe in dieser Liste wird gelöscht.</item>
<item quantity="other">%1$d erledigte Aufgaben in dieser Liste werden gelöscht.</item>
</plurals>
<string name="clear_completed_confirm">Löschen</string>
<string name="settings_list_notifications">Benachrichtigungen</string>
<string name="settings_list_notifications_hint">Töne, Vibration und Wichtigkeit für Erinnerungen der Liste</string>
<string name="recurrence_generic">Wiederholende Aufgabe</string>
<string name="recurrence_daily">Jeden Tag</string>
<string name="recurrence_weekly">Jede Woche</string>
<string name="recurrence_monthly">Jedem Monat</string>
<string name="recurrence_yearly">Jedes Jahr</string>
<plurals name="recurrence_every_n_days">
<item quantity="one">Jeden Tag</item>
<item quantity="other">Alle %1$d Tage</item>
</plurals>
<plurals name="recurrence_every_n_weeks">
<item quantity="one">Jede Woche</item>
<item quantity="other">Alle %1$d Wochen</item>
</plurals>
<plurals name="recurrence_every_n_months">
<item quantity="one">Jeden Monat</item>
<item quantity="other">Allw %1$d Monate</item>
</plurals>
<plurals name="recurrence_every_n_years">
<item quantity="one">Jedes Jahr</item>
<item quantity="other">Alle %1$d Jahre</item>
</plurals>
<string name="recurrence_on_days">%1$s am %2$s</string>
<string name="recurrence_with_until">%1$s bis %2$s</string>
<string name="recurrence_none">Wiederholt sich nicht</string>
<string name="recurrence_custom">Benutzerdefiniert</string>
<string name="recurrence_unit_days">Tage</string>
<string name="recurrence_unit_weeks">Wochen</string>
<string name="recurrence_unit_months">Monate</string>
<string name="recurrence_unit_years">Jahre</string>
<string name="recurrence_incomplete">Gebe eine Nummer zwischen 1 und 999 ein</string>
<string name="recurrence_end_never">Nie</string>
<string name="recurring_scope_occurrence">Nur diese Aufgabe</string>
<string name="recurring_scope_following">Diese und folgende Aufgaben</string>
<string name="error_recurrence_without_date">Eine sich wiederholende Aufgabe braucht ein Start- oder Fälligkeitsdatum</string>
<string name="edit_location_label">Standort</string>
<string name="edit_url_label">Link</string>
<string name="edit_url_hint">https://…</string>
<string name="edit_discard_title">Änderungen verwerfen?</string>
<string name="edit_discard_body">Deine Änderungen an dieser Aufgabe wurden nicht gespeichert.</string>
<string name="edit_discard_confirm">Verwerfen</string>
<string name="edit_keep_editing">Weiter bearbeiten</string>
<string name="edit_reminder_add">Erinnerung hinzufügen</string>
<string name="edit_reminder_add_first">Erinnerung hinzufügen (sonst wird Standard angewendet)</string>
<string name="edit_reminder_remove">Erinnerung entfernen</string>
<string name="subtask_progress">%1$d / %2$d</string>
<string name="progress_fraction_compact">%1$d/%2$d</string>
<string name="detail_delete_title">Aufgabe löschen?</string>
<string name="detail_delete_body">Die Aufgabe wird gelöscht.</string>
<plurals name="detail_delete_body_subtasks">
<item quantity="one">Diese Aufgabe und ihre Unteraufgabe werden gelöscht.</item>
<item quantity="other">Diese Aufgabe und ihre %1$d Unteraufgaben werden gelöscht.</item>
</plurals>
<string name="detail_status_cancelled">Abgebrochen</string>
<string name="detail_status_in_process">In Bearbeitung</string>
<string name="more_options">Mehr Optionen</string>
<string name="detail_cancel_task">Aufgabe abbrechen</string>
<string name="detail_restore">Aufgabe wiederherstellen</string>
<string name="detail_duplicate">Duplizieren</string>
<string name="lists_empty_action">Liste erstellen</string>
<string name="list_add">Neue Liste</string>
<string name="list_new_title">Neue Liste</string>
<string name="list_edit_title">Liste bearbeiten</string>
<string name="list_name_hint">Listenname</string>
<string name="list_color">Farbe</string>
<string name="list_delete">Liste löschen</string>
<string name="list_delete_confirm_title">Liste löschen?</string>
<string name="list_where_device">Nur auf diesem Gerät</string>
<string name="detail_pane_empty">Wähle eine Aufgabe, um sie hier zu sehen</string>
<string name="percent_complete">%1$d%%</string>
<string name="reminder_on_the_day">Ab dem Tag</string>
<string name="recurrence_next">Als nächstes: %1$s</string>
<string name="recurrence_end_until">An einem Tag</string>
<plurals name="home_today_remaining_count">
<item quantity="one">%1$d übrig</item>
<item quantity="other">%1$d übrig</item>
</plurals>
<string name="reminder_day_today">heute</string>
<string name="reminder_day_tomorrow">morgen</string>
<string name="reminder_day_yesterday">gestern</string>
<string name="reminder_action_done">Fertig</string>
<string name="onboarding_welcome_title">Alles, was du machen wolltest</string>
<string name="onboarding_welcome_body">Ein ruhiger Ort für deine Aufgaben auf deinem Smartphone, das deins bleibt. Lass es uns einrichten, es braucht eine Minute.</string>
<string name="onboarding_welcome_button">Loslegen</string>
<string name="onboarding_welcome_lists_title">Listen und Unteraufgaben</string>
<string name="onboarding_welcome_reminders_title">Erinnerungen, die ankommen</string>
<string name="onboarding_welcome_reminders_body">Eine Benachrichtigung, wenn etwas fällig wird, nach deinem Plan</string>
<string name="onboarding_welcome_sync_body">Synchronisiere über CalDAV mit einem Server, den du wählst, oder behalte alles auf diesem Gerät.</string>
<string name="onboarding_sync_title">Mit deinem eigenen Server synchronisieren lassen?</string>
<string name="onboarding_sync_body">Agendula spricht CalDav — Nextcloud, Radicale, Posteo, Fastmail und alles, was auch CalDAV spricht. Keine Konten bei uns, nie.</string>
<string name="onboarding_sync_connect">Konto verbinden</string>
<string name="onboarding_sync_skip">Nicht jetzt</string>
<string name="onboarding_sync_devices_body">Hake etwas hier ab, und es wird Überfall abgehakt.</string>
<string name="onboarding_sync_copy_body">Deine Aufgaben leben auch auf dem Server, also ist ein verlorenes Gerät nicht verlorene Arbeit.</string>
<string name="onboarding_list_title">Deine erste Liste</string>
<string name="onboarding_list_default_name">Persönlich</string>
<string name="onboarding_list_create">Liste erstellen</string>
<string name="onboarding_backup_title">Eine Sicherung behalten</string>
<string name="onboarding_backup_continue">Fortsetzen</string>
<string name="onboarding_quick_add_title">Wie möchtest du Aufgaben hinzufügen?</string>
<string name="clear_completed_action">Löschen</string>
<string name="clear_completed_title">Erledigte Aufgaben löschen?</string>
<string name="sync_notice_retry">Wiederholen</string>
<string name="sync_notice_cause_server_newer">Durch die neuere Kopie vom Server ersetzt</string>
<string name="sync_sign_in_channel_name">Anmeldung nötig</string>
<string name="sync_sign_in_title">Bei %1$s wieder anmelden</string>
<string name="sync_notification_channel">Synchronisieren</string>
<string name="sync_notification_title">Aufgaben synchronisieren</string>
<string name="add_account_server_label">Server-Adresse</string>
<string name="add_account_server_hint">https://cloud.example.com</string>
<string name="add_account_username_label">Benutzername</string>
<string name="add_account_password_label">Passwort</string>
<string name="add_account_sign_in">Anmelden</string>
<string name="add_account_browser_title">Auf Browser warten</string>
<string name="add_account_browser_body">Bestätige Agendula im Browser, komme dann zurück hierher. Dein Passwort gelangt niemals zu dieser App, der Server vergibt ein seperates App-Passwort, das du jederzeit löschen kannst.</string>
<string name="add_account_browser_use_password">Stattdessen Passwort nutzen</string>
<string name="add_account_browser_retry">Nochmal versuchen</string>
<string name="add_account_browser_open">Öffne die Anmeldeseite</string>
<string name="add_account_browser_insecure">Diese Anmeldeseite ist unverschlüsselt (http). Alles, was du darin schreibst, auch dein Passwort, wird unverschlüsselt versendet.</string>
<string name="add_account_lists_title">Welche Listen sollen synchronisiert werden?</string>
<string name="add_account_lists_shared">Mit dir geteilt</string>
<string name="add_account_save">Konto hinzufügen</string>
<string name="add_account_no_lists_selected">Wähle mindestens eine Liste</string>
<string name="widget_tasks_label">Aufgaben</string>
<string name="widget_empty">Hier ist nichts zu tun</string>
<string name="widget_unavailable">Öffne Agendula und die Einrichtung abzuschließen</string>
<string name="widget_config_title">Im Widget zeigen</string>
<string name="widget_config_smart_lists">Intelligente Listen</string>
<string name="list_color_blue">Blau</string>
<string name="list_color_purple">Lila</string>
<string name="list_color_pink">Rosa</string>
<plurals name="home_today_progress_count">
<item quantity="one">%1$d von %2$d abgeschlossen</item>
<item quantity="other">%1$d von %2$d abgeschlossen</item>
</plurals>
<string name="settings_group_look">Aussehen und Verhalten</string>
<string name="settings_group_data">Daten</string>
<string name="settings_group_app">App</string>
<string name="settings_group_about">Über</string>
<string name="settings_autofocus_title">Fokusiere den Titel bei Erstellung neuer Aufgaben</string>
<plurals name="duration_minutes">
<item quantity="one">%d Minute</item>
<item quantity="other">%d Minuten</item>
</plurals>
<plurals name="duration_hours">
<item quantity="one">%d Stunde</item>
<item quantity="other">%d Stunden</item>
</plurals>
<string name="settings_section_accounts">Konten</string>
<string name="accounts_empty_title">Noch keine Konten</string>
<string name="accounts_add">Konto hinzufügen</string>
<string name="accounts_sync_interval">Automatisch synchronisieren</string>
<plurals name="accounts_sync_every_minutes">
<item quantity="one">Jede %d Minute</item>
<item quantity="other">Jede %d Minuten</item>
</plurals>
<string name="list_save_failed">Liste konnte nicht gespeichert werden.</string>
<string name="list_delete_failed">Liste konnte nicht gelöscht werden.</string>
<string name="list_save_failed_offline">Server konnte nicht erreicht werden. Versuche es bitte erneut, wenn du wieder online bist.</string>
<string name="list_save_failed_unsupported">Dieses Konto lässt Apps keine Aufgabenlisten erstellen.</string>
<string name="list_delete_confirm_message">\"%1$s\" und alle Aufgaben davon werden gelöscht. Das kann nicht rückgängig gemacht werden.</string>
<string name="list_color_red">Rot</string>
<string name="list_color_orange">Orange</string>
<string name="list_color_olive">Olive</string>
<string name="list_color_green">Grün</string>
<string name="list_color_cyan">Cyan</string>
<string name="settings_time_format">Zeitformat</string>
<string name="settings_time_format_auto">Automatisch</string>
<string name="settings_week_start">Woche startet am</string>
<string name="settings_week_start_auto">Automatisch</string>
<string name="settings_week_start_auto_summary">Momentan: %1$s</string>
<string name="settings_licences">Open-Source Lizenzen</string>
<string name="settings_licences_subtitle">Die Projekte, auf denen Agendula aufgebaut ist</string>
<string name="licences_title">Open-Source Lizenzen</string>
<string name="licences_intro">Agendula beinhaltet die unten gelistete Arbeit. Drücke eine, um ihren Quellcode zu erhalten.</string>
<string name="settings_privacy">Datenschutzbestimmungen</string>
<string name="settings_notifications_blocked">Benachrichtigungen sind deaktiviert</string>
<plurals name="import_done">
<item quantity="one">%1$d Aufgabe in %2$s importiert</item>
<item quantity="other">%1$d Aufgaben in %2$s importiert</item>
</plurals>
<plurals name="import_done_skipped_existing">
<item quantity="one">%1$d schon vorhanden. Wird übersprungen.</item>
<item quantity="other">%1$d schon vorhanden. Werden übersprungen.</item>
</plurals>
<string name="import_failed_unreadable">Die Datei konnte nicht als iCalendar eingelesen werden</string>
<string name="import_failed_no_tasks">Die Datei beinhaltet keine Aufgaben</string>
<string name="import_failed_write">Die Aufgabe konnte nicht gespeichert werden</string>
<plurals name="accounts_sync_every_hours">
<item quantity="one">Jede Stunde</item>
<item quantity="other">Alle %d Stunden</item>
</plurals>
<string name="accounts_remove">Konto entfernen</string>
<string name="accounts_generic_provider">CalDAV-Konto</string>
<string name="accounts_sync_now">Jetzt synchronisieren</string>
<string name="accounts_removing">Entfernen…</string>
<string name="accounts_lists">Listen</string>
<string name="add_account_step_of">Schritt %1$d von %2$d</string>
<string name="add_account_provider_other_summary">Baïkal, DAViCal, SOGo, Radicale oder einen, den du selber laufen lässt</string>
<string name="add_account_provider_other_header">Jeglichen CalDAV-Server</string>
<string name="add_account_provider_other">Andere CalDAV-Server</string>
<string name="add_account_email_title">Was ist dort deine Adresse?</string>
<string name="add_account_email_body">Die E-Mail-Adresse, mit der du diesen Dienst nutzt. Agendula findet den Server selbst.</string>
<string name="add_account_email_label">E-Mail-Adresse</string>
<string name="add_account_summary_title">Du bist eingerichtet</string>
<string name="add_account_summary_done">Fertig</string>
<string name="add_account_server_title">Wo sind deine Aufgaben?</string>
<string name="add_account_credentials_title">Anmelden</string>
<string name="add_account_error_not_an_address">Das scheint keine Web-Adresse zu sein. Versuche etwas wie cloud.example.com.</string>
<string name="add_account_working_signing_in">Anmelden</string>
<string name="add_account_working_reading_lists">Aufgaben auslesen</string>
<string name="add_account_working_saving">Konto hinzufügen</string>
<string name="add_account_error_google_unsupported">Google Kalendar unterstützt nach eigener Dokumentation nicht Aufgaben über CalDAV, also kann Agendula nicht damit synchronisieren.</string>
<string name="add_account_error_already_exists">Konto ist schon eingerichtet.</string>
<string name="add_account_error_credentials_rejected">Diser Benutzername oder dieses Passwort wurde nicht akzeptiert.</string>
<string name="add_account_browser_error_maintenance">Der Server befindet sich um Wartungsmodus. Versuche es erneut, wenn er wieder zurück ist.</string>
<string name="add_account_setup_icloud_title">iCloud braucht ein App-spezifisches Passwort</string>
<string name="accounts_sign_in_again">Erneut anmelden</string>
<string name="sync_notice_body">%1$s</string>
<string name="sync_notices_title">Änderungen durch letzte Synchronisierung</string>
<string name="sync_notices_dismiss">Verstanden</string>
</resources> </resources>
+19 -87
View File
@@ -11,12 +11,9 @@ carries no APK assets.
Codeberg is the canonical forge; Gitea is build infrastructure. See Codeberg is the canonical forge; Gitea is build infrastructure. See
[Two forges, one repo](#two-forges-one-repo) for how the two are wired. [Two forges, one repo](#two-forges-one-repo) for how the two are wired.
Before a stable release you can ship **betas** (`X.Y.Z-beta.N`) from the While Agendula is pre-1.0 (`versionName` starts with `0.`), every release is
release branch. They go to **Codeberg only**, flagged as pre-releases; F-Droid flagged as a **pre-release**. This happens automatically and graduates to a
and Play never see them. See [Cutting a beta](#cutting-a-beta). stable release at `1.0.0` — no manual toggling.
While Agendula was pre-1.0 (`versionName` started with `0.`), every release was
flagged as a **pre-release** automatically.
--- ---
@@ -25,30 +22,16 @@ flagged as a **pre-release** automatically.
A release is defined by the `versionName`/`versionCode` committed in A release is defined by the `versionName`/`versionCode` committed in
`app/build.gradle.kts` — **not** by a hand-pushed tag: `app/build.gradle.kts` — **not** by a hand-pushed tag:
- `versionName` = `MAJOR.MINOR.PATCH` (e.g. `1.1.0`), or - `versionName` = `MAJOR.MINOR.PATCH` (e.g. `0.2.0`)
`MAJOR.MINOR.PATCH-beta.N` for a beta (e.g. `1.1.0-beta.2`) - `versionCode` = `MAJOR*10000 + MINOR*100 + PATCH` (`0.2.0` → `200`,
- `versionCode` is derived from it by `scripts/version_info.sh`: `1.3.4` → `10304`)
| `versionName` | `versionCode` | Example | So `MINOR` and `PATCH` each have room for 0–99. The release pipeline reads
| --- | --- | --- | `versionName`, pins `versionCode` to the derived value, builds, publishes, and —
| `X.Y.Z` below 1.1.0 (legacy) | `X*10000 + Y*100 + Z` | `1.0.1` → `10001` | once the APK is live — creates the tag `v<versionName>` at that commit. The tag
| `X.Y.Z-beta.N` (N = 1–98) | `X*1000000 + Y*10000 + Z*100 + N` | `1.1.0-beta.2` → `1010002` | is an **output** of a successful release, not its trigger, so a tag always marks
| `X.Y.Z` from 1.1.0 | `X*1000000 + Y*10000 + Z*100 + 99` | `1.1.0` → `1010099` | a fully-shipped version (and a failure before publish leaves no tag, so
re-running the workflow safely retries).
A beta's code sits below its own stable release and above everything before
it, so a beta install updates in place to the next beta and then to the stable
version. Every 1.0.x keeps the code it already has, and `1.1.0-beta.1`
(`1010001`) is above all of them. `MINOR` and `PATCH` each have room for 0–99.
Run `scripts/version_info.sh` to see what the committed version resolves to;
CI fails a PR whose committed `versionCode` doesn't match, because the official
F-Droid repo builds the tag exactly as committed.
The release pipeline reads `versionName`, pins `versionCode` to the derived
value, builds, publishes, and — once the APK is live — creates the tag
`v<versionName>` at that commit. The tag is an **output** of a successful
release, not its trigger, so a tag always marks a fully-shipped version (and a
failure before publish leaves no tag, so re-running the workflow safely
retries).
--- ---
@@ -61,8 +44,8 @@ retries).
`## [X.Y.Z]` heading (Keep a Changelog format). The text between that heading `## [X.Y.Z]` heading (Keep a Changelog format). The text between that heading
and the next `## [` becomes the Gitea and Codeberg release notes. The and the next `## [` becomes the Gitea and Codeberg release notes. The
heading **must** match the version exactly. heading **must** match the version exactly.
3. **Bump the committed `versionName`** (and `versionCode`, which 3. **Bump the committed `versionName`** (and `versionCode`) in
`scripts/version_info.sh` prints) in `app/build.gradle.kts`. **This bump is what triggers the release** when the `app/build.gradle.kts`. **This bump is what triggers the release** when the
branch merges to `main`. branch merges to `main`.
Then write this version's **"What's New"** by hand: Then write this version's **"What's New"** by hand:
`fastlane/metadata/android/<locale>/changelogs/<versionCode>.txt`, one per `fastlane/metadata/android/<locale>/changelogs/<versionCode>.txt`, one per
@@ -96,48 +79,6 @@ retries).
--- ---
## Cutting a beta
A beta is a test build of the next version, published as a **pre-release on
Codeberg** and nowhere else. It is signed with the real app key, so testers
install it over their stable install and it updates in place to later betas
and to the stable release.
1. **On `release/vX.Y.Z`**, with the features merged in, set
`versionName = "X.Y.Z-beta.1"` and the matching `versionCode`
(`scripts/version_info.sh` prints it; `1.1.0-beta.1` → `1010001`).
No What's New file — betas don't ship to the stores. Notes come from a
`## [X.Y.Z-beta.N]` section of `CHANGELOG.md` if you write one, otherwise
from `## [Unreleased]`.
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 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
[Cutting a release](#cutting-a-release).
Who gets a beta:
- **Obtainium** users only with *Include prereleases* switched on for the app.
That is how a tester opts in; everyone else stays on stable.
- **F-Droid** (self-hosted and official) never: `beta.yaml` doesn't touch the
self-hosted repo, and the official recipe's `UpdateCheckMode: Tags ^v[0-9.]+$`
ignores `-beta` tags.
- **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 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).
---
## What the pipeline does ## What the pipeline does
CI and release are split so a change is built once on its PR and only does CI and release are split so a change is built once on its PR and only does
@@ -153,12 +94,10 @@ release work when a merge actually cuts a release:
`values-*/strings.xml`, so Weblate PRs get fast feedback. Runs on every PR `values-*/strings.xml`, so Weblate PRs get fast feedback. Runs on every PR
without a path filter, so the required `Translations / check` status is always without a path filter, so the required `Translations / check` status is always
reported. reported.
- **`renovate.yml`** (`.gitea/workflows/`, Mondays 05:00 UTC, on push to - **`renovate.yml`** (`.gitea/workflows/`, Mondays 05:00 UTC plus
`main`, plus `workflow_dispatch`, **Gitea**) — the dependency sweep. Runs the `workflow_dispatch`, **Gitea**) — the dependency sweep. Runs the pinned
pinned Renovate image as its job container and targets Codeberg's API. New Renovate image as its job container and targets Codeberg's API; the cadence
PRs only open in the Monday `schedule` window in `renovate.json5` (keep it lives here, not in `renovate.json5`, so the two don't double-gate.
bracketing the cron); the on-merge runs fall outside it and only rebase
existing PRs that the merge left conflicted.
- **`release.yaml`** (`.gitea/workflows/`, on push to `main` plus - **`release.yaml`** (`.gitea/workflows/`, on push to `main` plus
`workflow_dispatch`, **Gitea**) — a cheap `detect` job reads `versionName` and `workflow_dispatch`, **Gitea**) — a cheap `detect` job reads `versionName` and
checks **Codeberg** for a tag for it. Only when there isn't one does the checks **Codeberg** for a tag for it. Only when there isn't one does the
@@ -171,12 +110,6 @@ release work when a merge actually cuts a release:
**Codeberg** with the signed APK + a SHA-256 checksum, and finally builds the **Codeberg** with the signed APK + a SHA-256 checksum, and finally builds the
App Bundle. Ordinary merges with no version bump fall through `detect` and do App Bundle. Ordinary merges with no version bump fall through `detect` and do
nothing. nothing.
- **`beta.yaml`** (`.gitea/workflows/`, on push to `release/**`, **Gitea**) —
the same `detect` idea for betas: only when the committed `versionName` is
`X.Y.Z-beta.N` and Codeberg has no tag for it does the `beta` job run: unit
tests, pin `versionCode`, build & sign the release APK with the **app key**,
create the tag + a Gitea pre-release with the R8 mapping, and publish the
**Codeberg pre-release** with the APK + SHA-256. No F-Droid, no Play.
- **`play` job** (same workflow, after `release`) — uploads the App Bundle and - **`play` job** (same workflow, after `release`) — uploads the App Bundle and
every locale's "What's New" to Google Play. Runs last and separately so a Play every locale's "What's New" to Google Play. Runs last and separately so a Play
rejection can't endanger a release that already shipped; skips cleanly until rejection can't endanger a release that already shipped; skips cleanly until
@@ -190,8 +123,7 @@ Releases aren't git objects and don't sync with the push mirror in either
direction, so the pipeline pushes the `vX.Y.Z` tag straight to Codeberg, creates direction, so the pipeline pushes the `vX.Y.Z` tag straight to Codeberg, creates
the release over the Codeberg API, and attaches `agendula_v<version>.apk` + its the release over the Codeberg API, and attaches `agendula_v<version>.apk` + its
`.sha256`. It's the same APK the F-Droid repo serves (same **app key**), so it `.sha256`. It's the same APK the F-Droid repo serves (same **app key**), so it
adds no trust surface. The logic lives in `scripts/publish_codeberg_release.sh`, adds no trust surface. It skips cleanly if `CODEBERG_RELEASE_TOKEN` is unset,
which `beta.yaml` uses too (with the pre-release flag set). It skips cleanly if `CODEBERG_RELEASE_TOKEN` is unset,
but it is **not** `continue-on-error`: through 0.2.1–0.3.2 this step reported but it is **not** `continue-on-error`: through 0.2.1–0.3.2 this step reported
green while never once publishing, which is how a crash-fix release reached green while never once publishing, which is how a crash-fix release reached
F-Droid but not the Codeberg/Obtainium users who needed it. A broken mirror F-Droid but not the Codeberg/Obtainium users who needed it. A broken mirror
+1 -3
View File
@@ -42,9 +42,7 @@ on 2026-09-24. Its CI rebuilt v1.0.0 and verified it against our published APK
The file here is a copy of the submitted recipe. fdroiddata's copy is the one The file here is a copy of the submitted recipe. fdroiddata's copy is the one
that counts; after the merge F-Droid picks up new `vX.Y.Z` tags on its own that counts; after the merge F-Droid picks up new `vX.Y.Z` tags on its own
(`AutoUpdateMode`), so there is no per-release work there. Beta tags (`AutoUpdateMode`), so there is no per-release work there. Only a change to the
(`vX.Y.Z-beta.N`) don't match `UpdateCheckMode: Tags ^v[0-9.]+$`, so betas stay
off the official repo; keep that pattern if the recipe ever changes. Only a change to the
recipe itself (a new submodule, a build flag) needs an MR, pinned to a full recipe itself (a new submodule, a build flag) needs an MR, pinned to a full
commit hash, never a tag. commit hash, never a tag.
@@ -0,0 +1,5 @@
Agendula gibt es jetzt bei Google Play. An der App hat sich seit 1.0.0 nichts geändert:
- CalDAV-Sync eingebaut: Nextcloud, Radicale, Baïkal und mehr.
- Agendula speichert deine Aufgaben selbst, keine andere App nötig.
- Wiederkehrende Aufgaben, mehrere Erinnerungen, Listen in der App,
iCalendar-Import und -Export, ein Widget und eine Schnelleinstellungs-Kachel.
@@ -0,0 +1,5 @@
Agendula is now on Google Play. Nothing changed in the app since 1.0.0:
- CalDAV sync built in: Nextcloud, Radicale, Baïkal and more.
- Agendula keeps your tasks itself, no other app needed.
- Repeating tasks, several reminders per task, lists managed in the app,
iCalendar import and export, a home-screen widget and a Quick Settings tile.
@@ -0,0 +1,5 @@
Agendula is now on Google Play. Nothing changed in the app since 1.0.0:
- CalDAV sync built in: Nextcloud, Radicale, Baïkal and more.
- Agendula keeps your tasks itself, no other app needed.
- Repeating tasks, several reminders per task, lists managed in the app,
iCalendar import and export, a home-screen widget and a Quick Settings tile.
@@ -0,0 +1,5 @@
O Agendula agora está no Google Play. Nada mudou no app desde a 1.0.0:
- Sincronização CalDAV integrada: Nextcloud, Radicale, Baïkal e outros.
- O Agendula guarda suas tarefas sozinho, sem outro app.
- Tarefas recorrentes, vários lembretes por tarefa, listas no app, importação e
exportação iCalendar, widget e bloco de Configurações rápidas.
+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\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-aarch64-b1163.116.tar.gz toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.FREE_BSD.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.LINUX.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-aarch64-b1163.116.tar.gz toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.LINUX.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/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.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
toolchainUrl.MAC_OS.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-osx-x64-b1163.116.tar.gz toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
toolchainUrl.UNIX.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-aarch64-b1163.116.tar.gz toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.UNIX.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.WINDOWS.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-windows-aarch64-b1163.116.tar.gz toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
toolchainUrl.WINDOWS.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-windows-x64-b1163.116.tar.gz toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
toolchainVendor=JETBRAINS toolchainVendor=JETBRAINS
toolchainVersion=21 toolchainVersion=21
+2 -2
View File
@@ -1,6 +1,6 @@
[versions] [versions]
agp = "9.4.1" agp = "9.2.1"
kotlin = "2.4.20" kotlin = "2.3.21"
ksp = "2.3.11" ksp = "2.3.11"
hilt = "2.60.1" hilt = "2.60.1"
coreKtx = "1.19.0" coreKtx = "1.19.0"
Binary file not shown.
+2 -4
View File
@@ -1,10 +1,8 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=bafd5ce9cfaea0fbccfdc8439a1ac42fbd4cd9c89dc9a988228d8a2639a58e6c distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f
networkTimeout=10000 networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+7 -4
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015 the original authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
############################################################################## ##############################################################################
# #
# gradlew start up script for POSIX generated by Gradle. # Gradle start up script for POSIX generated by Gradle.
# #
# Important for running: # Important for running:
# #
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole # bash, then to run this script, type that shell name before the whole
# command line, like: # command line, like:
# #
# ksh gradlew # ksh Gradle
# #
# Busybox and similar reduced shells will NOT work, because this script # Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features: # requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,6 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -171,6 +172,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -210,6 +212,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
Vendored
+34 -52
View File
@@ -19,39 +19,12 @@
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem gradlew startup script for Windows @rem Gradle startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled @rem Set local scope for the variables with windows NT shell
setlocal EnableExtensions if "%OS%"=="Windows_NT" setlocal
@rem Catch executions from older scripts and ensure they exit cleanly.
@rem This can be removed once we can be reasonably confident that few people
@rem will be migrating directly to this new wrapper.
goto afterSafetyNet
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
goto exitWithErrorLevel
:afterSafetyNet
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@@ -72,14 +45,13 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
1>&2 echo. echo. 1>&2
1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
1>&2 echo. echo. 1>&2
1>&2 echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
1>&2 echo location of your Java installation. echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 goto fail
goto exitWithErrorLevel
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@@ -87,26 +59,36 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
1>&2 echo. echo. 1>&2
1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
1>&2 echo. echo. 1>&2
1>&2 echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
1>&2 echo location of your Java installation. echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 goto fail
goto exitWithErrorLevel
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=
@rem Execute gradlew @rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
@rem This label must not be changed. We rely on old scripts being able to jump to this point. :end
:exitWithErrorLevel @rem End local scope for the variables with windows NT shell
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts if %ERRORLEVEL% equ 0 goto mainEnd
"%COMSPEC%" /c exit %ERRORLEVEL%
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+3 -10
View File
@@ -52,16 +52,9 @@
prConcurrentLimit: 5, prConcurrentLimit: 5,
prHourlyLimit: 0, prHourlyLimit: 0,
// New branches/PRs only in the Monday window (UTC), which brackets the weekly // Cadence is owned by the Gitea Actions cron (.gitea/workflows/renovate.yml,
// cron in .gitea/workflows/renovate.yml with slack for a late start — keep // Mondays) — no internal `schedule` here, so the two don't double-gate and
// the two in step, or the sweep lands outside the window and finds nothing. // silently skip a run.
// The same workflow also runs on every push to main; those runs fall outside
// the window and, via the default updateNotScheduled: true, only maintain
// existing branches — chiefly rebasing PRs a merge just conflicted.
schedule: ["* 4-6 * * 1"],
// Rebase only on conflict, not every time main moves (automerge is off, so
// this matches "auto"; spelled out because the on-merge trigger relies on it).
rebaseWhen: "conflicted",
// Workflows are split by forge and neither directory is .github: CI lives in // Workflows are split by forge and neither directory is .github: CI lives in
// .forgejo/workflows (Codeberg) and the release/renovate jobs in // .forgejo/workflows (Codeberg) and the release/renovate jobs in
+5 -7
View File
@@ -16,7 +16,6 @@ without setup.
""" """
import re import re
import struct import struct
import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@@ -121,11 +120,10 @@ def check_images(locale_dir):
def version_code(): def version_code():
"""The committed version's code, or None for a beta (it ships no What's New).""" gradle = (ROOT / "app/build.gradle.kts").read_text()
def info(key): name = re.search(r'versionName\s*=\s*"([^"]+)"', gradle).group(1)
return subprocess.run(["bash", str(ROOT / "scripts/version_info.sh"), key], parts = (name.split(".") + ["0", "0"])[:3]
check=True, capture_output=True, text=True).stdout.strip() return int(parts[0]) * 10000 + int(parts[1]) * 100 + int(parts[2])
return None if info("channel") == "beta" else int(info("version_code"))
def shipped_languages(): def shipped_languages():
@@ -185,7 +183,7 @@ def main():
err(images / "featureGraphic.png", "required for Play (1024x500, no alpha)") err(images / "featureGraphic.png", "required for Play (1024x500, no alpha)")
if len(list((images / "phoneScreenshots").glob("*"))) < 2: if len(list((images / "phoneScreenshots").glob("*"))) < 2:
err(images / "phoneScreenshots", "Play requires at least 2 phone screenshots") err(images / "phoneScreenshots", "Play requires at least 2 phone screenshots")
for loc in REQUIRED_LOCALES if current is not None else []: for loc in REQUIRED_LOCALES:
if not (META / loc / "changelogs" / f"{current}.txt").exists(): if not (META / loc / "changelogs" / f"{current}.txt").exists():
err(META / loc / "changelogs" / f"{current}.txt", "no What's New for this version") err(META / loc / "changelogs" / f"{current}.txt", "no What's New for this version")
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/env bash
# 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.
#
# Env: TOKEN, API (.../api/v1/repos/<owner>/<repo>), TAG, SHA, PRERELEASE,
# NOTES_FILE, APK
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
: "${API:?}" "${TAG:?}" "${SHA:?}" "${PRERELEASE:?}" "${NOTES_FILE:?}" "${APK:?}"
if [ ! -f "$APK" ]; then echo "No release APK at $APK." >&2; exit 1; fi
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
ASSET_APK="agendula_${TAG}.apk"
ASSET_SUM="${ASSET_APK}.sha256"
cp "$APK" "$WORK/$ASSET_APK"
( cd "$WORK" && sha256sum "$ASSET_APK" > "$ASSET_SUM" )
# 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"
git push -f "https://${REPO%%/*}:${TOKEN}@${HOST}/${REPO}.git" "refs/tags/$TAG"
python3 - "$TAG" "$PRERELEASE" "$NOTES_FILE" <<'PY' > "$WORK/payload.json"
import json, sys
tag, pre, notes = sys.argv[1:4]
print(json.dumps({
"tag_name": tag,
"name": tag,
"body": open(notes).read(),
"draft": False,
"prerelease": pre == "true",
}))
PY
# 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
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" || 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
sleep $((attempt * 10))
done
if [ -z "$ID" ]; then echo "Could not create or update the Codeberg release." >&2; exit 1; fi
for A in "$ASSET_APK" "$ASSET_SUM"; do
OLD=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/$ID/assets" \
| 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."
-76
View File
@@ -1,76 +0,0 @@
#!/usr/bin/env bash
# Create (or update) the release for TAG on the Gitea build instance, the
# changelog of record there, and attach the R8 mapping. Shared by
# .gitea/workflows/release.yaml and beta.yaml.
#
# Creating it with target_commitish makes Gitea create the tag at SHA, so the
# tag only ever marks a fully-shipped version. Upsert, so re-runs are safe.
#
# Env:
# TOKEN, API Gitea token and .../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)
# MAPPING optional path to R8's mapping.txt
set -euo pipefail
: "${TOKEN:?}" "${API:?}" "${TAG:?}" "${SHA:?}" "${PRERELEASE:?}" "${NOTES_FILE:?}"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
python3 - "$TAG" "$SHA" "$PRERELEASE" "$NOTES_FILE" <<'PY' > "$WORK/payload.json"
import json, sys
tag, sha, pre, notes = sys.argv[1:5]
print(json.dumps({
"tag_name": tag,
"target_commitish": sha,
"name": tag,
"body": open(notes).read(),
"draft": False,
"prerelease": pre == "true",
}))
PY
# PATCH if a release for the tag already exists, else POST a new one (which
# also creates the tag at target_commitish).
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty' 2>/dev/null || true)
if [ -n "$ID" ]; then
CODE=$(curl -s -o "$WORK/response.json" -w '%{http_code}' -X PATCH \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d @"$WORK/payload.json" "$API/releases/$ID")
OK=200
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")
OK=201
fi
cat "$WORK/response.json"; echo
if [ "$CODE" != "$OK" ]; then
echo "Gitea release upsert failed with HTTP $CODE (expected $OK)" >&2
exit 1
fi
ID=$(jq -r '.id' "$WORK/response.json")
echo "Created/updated Gitea release $TAG at $SHA"
# The R8 mapping keeps user crash stacktraces deobfuscatable. It's not an APK,
# so it fits the no-binaries rule for this release. Best-effort: never fail a
# release over it.
attach_mapping() {
local asset="mapping-${TAG#v}.txt.gz" old
gzip -c "$MAPPING" > "$WORK/$asset"
# Replace any prior asset of the same name (re-run safe).
old=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/$ID/assets" \
| jq -r --arg n "$asset" '.[] | select(.name==$n) | .id')
if [ -n "$old" ]; then
curl -s -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$ID/assets/$old" >/dev/null
fi
curl -s -X POST -H "Authorization: token $TOKEN" \
-F "attachment=@$WORK/$asset" \
"$API/releases/$ID/assets?name=$asset" -o /dev/null -w "asset $asset HTTP %{http_code}\n"
}
if [ -z "${MAPPING:-}" ] || [ ! -f "$MAPPING" ]; then
echo "No mapping.txt (R8 off?) — skipping."
else
attach_mapping || echo "warning: could not attach the R8 mapping to $TAG" >&2
fi
-59
View File
@@ -1,59 +0,0 @@
#!/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
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
# Print the forge release notes for VERSION: its `## [VERSION]` section of
# CHANGELOG.md. A beta (X.Y.Z-beta.N) normally has no section of its own, so it
# falls back to `## [Unreleased]`, i.e. what the release branch carries so far,
# under a line saying what a beta is.
#
# scripts/release_notes.sh 1.1.0 > release-notes.md
set -euo pipefail
cd "$(dirname "$0")/.." # repo root
VERSION=${1:?usage: release_notes.sh VERSION}
section() {
awk -v ver="$1" '
$0 ~ "^## \\[" ver "\\]" { flag = 1; next }
/^## \[/ { flag = 0 }
flag' CHANGELOG.md | sed -e '/./,$!d'
}
NOTES=$(section "$VERSION")
if [[ "$VERSION" == *-beta.* ]]; then
[ -n "$NOTES" ] || NOTES=$(section Unreleased)
printf '%s\n\n' "**Beta of ${VERSION%%-*}, for testing.** It updates in place to later betas and to the stable release. Obtainium only offers betas with *Include prereleases* switched on; F-Droid and Play never get them."
fi
if [ -n "$NOTES" ]; then
printf '%s\n' "$NOTES"
else
echo "_No changelog entry for ${VERSION} — see CHANGELOG.md._"
fi
+5 -9
View File
@@ -11,15 +11,11 @@
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." # repo root cd "$(dirname "$0")/.." # repo root
VERSION=$(bash scripts/version_info.sh version) VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
VERSION_CODE=$(bash scripts/version_info.sh version_code) [ -n "$VERSION" ] || { echo "No versionName in app/build.gradle.kts" >&2; exit 1; }
MAJOR=${VERSION%%.*}; rest=${VERSION#*.}; MINOR=${rest%%.*}; PATCH=${rest##*.}
# Betas only ship to Codeberg, whose notes come from CHANGELOG.md. A What's New MAJOR=${MAJOR:-0}; MINOR=${MINOR:-0}; PATCH=${PATCH:-0}
# file for one would sit in the tree F-Droid and Play read, so none is wanted. VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
if [ "$(bash scripts/version_info.sh channel)" = beta ]; then
echo "Beta $VERSION: no store What's New (betas only ship to Codeberg)."
exit 0
fi
# Play rejects a longer "What's New" and F-Droid truncates it in-client. # Play rejects a longer "What's New" and F-Droid truncates it in-client.
MAX_CHARS=${MAX_CHARS:-500} MAX_CHARS=${MAX_CHARS:-500}
-87
View File
@@ -1,87 +0,0 @@
#!/usr/bin/env bash
# The one place that turns the committed versionName into a versionCode and a
# release channel. The release and beta pipelines, the changelog sync and the
# store-listing check all read it from here, so the scheme can't drift between
# them.
#
# versionName channel versionCode
# X.Y.Z (< 1.1.0) stable X*10000 + Y*100 + Z (legacy)
# X.Y.Z-beta.N beta X*1000000 + Y*10000 + Z*100 + N (N = 1..98)
# X.Y.Z (>= 1.1.0) stable X*1000000 + Y*10000 + Z*100 + 99
#
# A beta's code sits below its own stable release and above everything before
# it, so a beta install updates in place to the next beta and then to the
# stable version. Every 1.0.x keeps the legacy code it already shipped with;
# the first new-scheme code (1.1.0-beta.1 -> 1010001) is above all of them.
#
# scripts/version_info.sh print version, version_code, base_version,
# channel and prerelease as key=value lines
# (ready for $GITHUB_OUTPUT)
# scripts/version_info.sh <key> print just that value
# scripts/version_info.sh --check also fail unless the committed versionCode
# matches; the official F-Droid repo builds
# the tag as committed, so it must
set -euo pipefail
cd "$(dirname "$0")/.." # repo root
GRADLE="app/build.gradle.kts"
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|[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=${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
echo "versionName '$NAME': MINOR and PATCH each have room for 0-99" >&2
exit 1
fi
LEGACY=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
if [ -n "$BETA" ]; then
if [ "$LEGACY" -lt 10100 ]; then
echo "versionName '$NAME': betas start at 1.1.0; 1.0.x codes have no room for them" >&2
exit 1
fi
if [ "$BETA" -lt 1 ] || [ "$BETA" -gt 98 ]; then
echo "versionName '$NAME': beta number must be 1-98 (99 is the stable release)" >&2
exit 1
fi
CHANNEL=beta
CODE=$(( MAJOR * 1000000 + MINOR * 10000 + PATCH * 100 + BETA ))
elif [ "$LEGACY" -lt 10100 ]; then
CHANNEL=stable
CODE=$LEGACY
else
CHANNEL=stable
CODE=$(( MAJOR * 1000000 + MINOR * 10000 + PATCH * 100 + 99 ))
fi
# Betas are always pre-releases; so was everything before 1.0.0.
if [ "$CHANNEL" = beta ] || [ "$MAJOR" = 0 ]; then PRERELEASE=true; else PRERELEASE=false; fi
case "${1:-}" in
"")
printf 'version=%s\nversion_code=%s\nbase_version=%s\nchannel=%s\nprerelease=%s\n' \
"$NAME" "$CODE" "$BASE" "$CHANNEL" "$PRERELEASE"
;;
--check)
if [ "$COMMITTED" != "$CODE" ]; then
echo "ERROR: $GRADLE has versionCode = ${COMMITTED:-<none>}, but versionName '$NAME' needs $CODE." >&2
echo "Set versionCode = $CODE (see docs/RELEASING.md for the scheme)." >&2
exit 1
fi
echo "OK: versionName $NAME -> versionCode $CODE ($CHANNEL)."
;;
version) echo "$NAME" ;;
version_code) echo "$CODE" ;;
base_version) echo "$BASE" ;;
channel) echo "$CHANNEL" ;;
prerelease) echo "$PRERELEASE" ;;
*) echo "Unknown key '$1'" >&2; exit 2 ;;
esac