Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41e23b8331 | ||
|
|
9c35712573 | ||
|
|
4342c6ab6d | ||
|
|
57bad5c74c | ||
|
|
ebf833d0bd | ||
|
|
2f24a9878c | ||
|
|
2313671965 | ||
|
|
e1b80d9f89 | ||
|
|
99bb6adf80 | ||
|
|
06ee010f07 |
@@ -37,9 +37,24 @@ jobs:
|
|||||||
- name: Reproducible-release invariant
|
- name: Reproducible-release invariant
|
||||||
run: bash scripts/check_reproducible_release.sh
|
run: bash scripts/check_reproducible_release.sh
|
||||||
|
|
||||||
|
# versionCode must match versionName (the official F-Droid repo builds the
|
||||||
|
# tag as committed), and a beta must never reach main.
|
||||||
|
- 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
|
||||||
|
|
||||||
# Play rejects a "What's New" over 500 characters, which would fail the
|
# Play rejects a "What's New" over 500 characters, which would fail the
|
||||||
# upload after the release had already shipped everywhere else. Cheap, so
|
# upload after the release had already shipped everywhere else. Cheap, so
|
||||||
# it runs on every PR rather than only on the release merge.
|
# it runs on every PR rather than only on the release merge. A beta ships
|
||||||
|
# no What's New, so only the older files are checked for one.
|
||||||
- name: Changelog length invariant
|
- name: Changelog length invariant
|
||||||
run: bash scripts/check_changelog_lengths.sh
|
run: bash scripts/check_changelog_lengths.sh
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
name: Android build environment
|
||||||
|
description: JDK 17, Android SDK + build tools, Gradle cache and jq for the release and beta pipelines.
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- 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
|
||||||
|
shell: bash
|
||||||
|
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
|
||||||
|
shell: bash
|
||||||
|
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
|
||||||
|
shell: bash
|
||||||
|
run: chmod +x ./gradlew
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
name: Beta — Codeberg pre-release
|
||||||
|
|
||||||
|
# Pushing a release/* branch whose versionName is X.Y.Z-beta.N publishes a
|
||||||
|
# Codeberg pre-release (APK + SHA-256) and a Gitea pre-release (R8 mapping).
|
||||||
|
# Betas never reach F-Droid or Play. See docs/RELEASING.md.
|
||||||
|
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 }}
|
||||||
|
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
|
||||||
|
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 }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Android build environment
|
||||||
|
uses: ./.gitea/actions/android-env
|
||||||
|
|
||||||
|
- name: Pin versionCode to versionName
|
||||||
|
run: bash scripts/version_info.sh --pin
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: ./gradlew testDebugUnitTest
|
||||||
|
|
||||||
|
# The real app key, so a beta updates in place to later betas and stable.
|
||||||
|
- name: Setup Android Keystore
|
||||||
|
env:
|
||||||
|
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
|
||||||
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
|
run: bash scripts/write_keystore.sh
|
||||||
|
|
||||||
|
- name: Build release APK
|
||||||
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Publish pre-release to Codeberg
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.CODEBERG_RELEASE_TOKEN }}
|
||||||
|
API: https://codeberg.org/api/v1/repos/jlmakiola/calendula
|
||||||
|
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
|
||||||
+32
-255
@@ -7,7 +7,9 @@ name: Release — F-Droid repo + Gitea/Codeberg release + Play
|
|||||||
# that release to Codeberg with the signed APK + a SHA-256 checksum as a
|
# that release to 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.
|
# nothing. Betas (X.Y.Z-beta.N) never come through here: beta.yaml cuts them
|
||||||
|
# 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 is the only channel that can reject a good build for
|
# and separate because Play is the only channel that can reject a good build for
|
||||||
@@ -54,56 +56,28 @@ 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/calendula
|
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
|
INFO=$(bash scripts/version_info.sh)
|
||||||
if [ -z "$VERSION" ]; then echo "No versionName in app/build.gradle.kts" >&2; exit 1; fi
|
echo "$INFO"
|
||||||
MAJOR=$(echo "$VERSION" | cut -d. -f1); MINOR=$(echo "$VERSION" | cut -d. -f2); PATCH=$(echo "$VERSION" | cut -d. -f3)
|
echo "$INFO" >> "$GITHUB_OUTPUT"
|
||||||
MAJOR=${MAJOR:-0}; MINOR=${MINOR:-0}; PATCH=${PATCH:-0}
|
VERSION=$(echo "$INFO" | sed -n 's/^version=//p')
|
||||||
VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
CHANNEL=$(echo "$INFO" | sed -n 's/^channel=//p')
|
||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "version_code=$VERSION_CODE" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Resolved version $VERSION (code $VERSION_CODE)"
|
|
||||||
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
|
||||||
# A tag for this version already existing means the release shipped on
|
# Backstop for CI's guard: betas ship from release/* via beta.yaml.
|
||||||
# an earlier push; do nothing. Absent => this merge cuts the release.
|
if [ "$CHANNEL" != "stable" ]; then
|
||||||
#
|
echo "versionName $VERSION on main is a beta. Set the stable version before merging to main." >&2
|
||||||
# Anything other than a clean 200/404 is treated as fatal rather than
|
exit 1
|
||||||
# as "no tag". A Codeberg outage or a network blip would otherwise
|
fi
|
||||||
# read as absent and re-cut a release that has already shipped —
|
# Tags are read from Codeberg, the canonical forge: a tag minted here is
|
||||||
# republishing to F-Droid and Play. Failing here is recoverable; a
|
# wiped by the next mirror sync until Codeberg has it. A lookup error is
|
||||||
# duplicate release is not.
|
# fatal, since guessing "no tag" would re-cut a shipped release.
|
||||||
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$TAG_API/git/refs/tags/v$VERSION" || echo 000)
|
GATE=$(bash scripts/release_gate.sh)
|
||||||
case "$STATUS" in
|
echo "is_release=${GATE#cut=}" >> "$GITHUB_OUTPUT"
|
||||||
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
|
|
||||||
|
|
||||||
# Releases: build + sign + publish, then mint the tag and Gitea release.
|
# Releases: build + sign + publish, then mint the tag and Gitea release.
|
||||||
# Also runs on manual dispatch, where it skips the build and just re-signs and
|
# Also runs on manual dispatch, where it skips the build and just re-signs and
|
||||||
@@ -124,65 +98,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Android build environment
|
||||||
uses: actions/setup-java@v4
|
uses: ./.gitea/actions/android-env
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '17'
|
|
||||||
|
|
||||||
- name: Setup Android SDK
|
# The committed versionName is the source of truth; pin the derived code.
|
||||||
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
|
|
||||||
|
|
||||||
# The committed versionName is the source of truth. Pin versionCode to the
|
|
||||||
# value derived from it so the published APK's code is always
|
|
||||||
# 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: bash scripts/version_info.sh --pin
|
||||||
set -e
|
|
||||||
sed -i "s/versionCode = .*/versionCode = $VERSION_CODE/" app/build.gradle.kts
|
|
||||||
grep -E 'versionName|versionCode' app/build.gradle.kts
|
|
||||||
|
|
||||||
# Test the exact commit being shipped (only on a real release).
|
# Test the exact commit being shipped (only on a real release).
|
||||||
- name: Unit tests
|
- name: Unit tests
|
||||||
@@ -195,15 +117,7 @@ jobs:
|
|||||||
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
|
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
|
||||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
run: |
|
run: bash scripts/write_keystore.sh
|
||||||
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
|
- name: Build release APK
|
||||||
if: env.IS_RELEASE == 'true'
|
if: env.IS_RELEASE == 'true'
|
||||||
@@ -312,6 +226,7 @@ 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.
|
||||||
- name: Create tag + Gitea release
|
- name: Create tag + Gitea release
|
||||||
if: env.IS_RELEASE == 'true'
|
if: env.IS_RELEASE == 'true'
|
||||||
env:
|
env:
|
||||||
@@ -320,74 +235,10 @@ jobs:
|
|||||||
SHA: ${{ github.sha }}
|
SHA: ${{ github.sha }}
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
TAG="v$VERSION"
|
bash scripts/release_notes.sh "$VERSION" > release-notes.md
|
||||||
# Notes = this version's CHANGELOG section.
|
TAG="v$VERSION" PRERELEASE=false NOTES_FILE=release-notes.md \
|
||||||
awk -v ver="$VERSION" '
|
MAPPING=app/build/outputs/mapping/release/mapping.txt \
|
||||||
$0 ~ "^## \\[" ver "\\]" { flag = 1; next }
|
bash scripts/publish_gitea_release.sh
|
||||||
/^## \[/ { 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" <<'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,
|
|
||||||
"prerelease": False,
|
|
||||||
}))
|
|
||||||
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"
|
|
||||||
|
|
||||||
# Mirror the release to the Codeberg mirror as a direct-download channel
|
# Mirror the release to the Codeberg mirror as a direct-download channel
|
||||||
# for users who don't want F-Droid. Gitea already push-mirrors branches +
|
# for users who don't want F-Droid. Gitea already push-mirrors branches +
|
||||||
@@ -406,84 +257,10 @@ jobs:
|
|||||||
SHA: ${{ github.sha }}
|
SHA: ${{ github.sha }}
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
if [ -z "${TOKEN:-}" ]; then
|
[ -s release-notes.md ] || bash scripts/release_notes.sh "$VERSION" > release-notes.md
|
||||||
echo "CODEBERG_RELEASE_TOKEN not set — skipping Codeberg publish."
|
TAG="v$VERSION" PRERELEASE=false NOTES_FILE=release-notes.md \
|
||||||
exit 0
|
APK=app/build/outputs/apk/release/app-release.apk \
|
||||||
fi
|
bash scripts/publish_codeberg_release.sh
|
||||||
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="calendula_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
|
|
||||||
# The pipeline creates the tag via the Gitea API, which the push mirror
|
|
||||||
# (sync_on_commit only fires on real git pushes) doesn't propagate
|
|
||||||
# promptly — so a release POST that carries a target_commitish can
|
|
||||||
# outrun the mirror and 500 on a commit/tag Codeberg hasn't received.
|
|
||||||
# Push the tag straight to Codeberg so it's guaranteed present, then
|
|
||||||
# attach the release to that existing tag with NO target_commitish
|
|
||||||
# (which is what triggered the 500).
|
|
||||||
git tag -f "$TAG" "$SHA"
|
|
||||||
git push -f "https://jlmakiola:${TOKEN}@codeberg.org/jlmakiola/calendula.git" \
|
|
||||||
"refs/tags/$TAG"
|
|
||||||
python3 - "$TAG" <<'PY' > cb-payload.json
|
|
||||||
import json, sys
|
|
||||||
print(json.dumps({
|
|
||||||
"tag_name": sys.argv[1],
|
|
||||||
"name": sys.argv[1],
|
|
||||||
"body": open("release-notes.md").read(),
|
|
||||||
"draft": False,
|
|
||||||
"prerelease": False,
|
|
||||||
}))
|
|
||||||
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 kept
|
|
||||||
# failing and skipping the mirror even though the very same call
|
|
||||||
# succeeds seconds later. Retry with backoff, and PATCH in place if a
|
|
||||||
# release already exists (re-run safe). A 5xx body still exits curl 0,
|
|
||||||
# so the loop, not `set -e`, controls the flow.
|
|
||||||
ID=""
|
|
||||||
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, so it is a second artifact from
|
# Play takes an App Bundle, not the APK, so it is a second artifact from
|
||||||
# the same source and the same signing config — not a repackage of the
|
# the same source and the same signing config — not a repackage of the
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
name: Renovate
|
name: Renovate
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Weekly sweep. Mondays 05:00 UTC — this cron owns the cadence; the repo's
|
# Every merge to main. Mirror syncs from Codeberg fire push events here (the
|
||||||
# renovate.json5 deliberately has no internal schedule (avoids double-gating).
|
# same trigger release.yaml relies on), so a merged Renovate PR is followed
|
||||||
|
# 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.
|
||||||
|
|||||||
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.22.3] — 2026-10-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **The store listing is called "Calendula: Calendar" again,** with "Calendar" in each store language. Since 2.22.0 the Play listing comes from the repository, whose title was only "Calendula". The app is unchanged ([#361]).
|
||||||
|
|
||||||
|
## [2.22.2] — 2026-10-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **2.22 reaches Google Play.** The Dutch store listing used the locale code `nl`, which Play rejects, so the Play uploads of 2.22.0 and 2.22.1 failed. It now uses `nl-NL`, and CI checks every store locale against the codes Play accepts. The app is unchanged from 2.22.1 ([#359]).
|
||||||
|
|
||||||
|
## [2.22.1] — 2026-10-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Tapping a link in an event's location opens it in the browser instead of a maps app.** Links are underlined, as in the description, and tapping the rest of the location still opens maps. A link with a capitalised `Https://`, or wrapped in `<…>` the way Outlook and Teams write them, opens correctly too. When no app can open a link, a short message says so instead of the tap doing nothing ([#357]).
|
||||||
|
|
||||||
## [2.22.0] — 2026-10-01
|
## [2.22.0] — 2026-10-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -1817,3 +1832,6 @@ automatically, with zero telemetry and no internet permission.
|
|||||||
[#322]: https://codeberg.org/jlmakiola/calendula/issues/322
|
[#322]: https://codeberg.org/jlmakiola/calendula/issues/322
|
||||||
[#331]: https://codeberg.org/jlmakiola/calendula/issues/331
|
[#331]: https://codeberg.org/jlmakiola/calendula/issues/331
|
||||||
[#336]: https://codeberg.org/jlmakiola/calendula/issues/336
|
[#336]: https://codeberg.org/jlmakiola/calendula/issues/336
|
||||||
|
[#357]: https://codeberg.org/jlmakiola/calendula/issues/357
|
||||||
|
[#359]: https://codeberg.org/jlmakiola/calendula/issues/359
|
||||||
|
[#361]: https://codeberg.org/jlmakiola/calendula/issues/361
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ source "https://rubygems.org"
|
|||||||
# Pinned exactly; Renovate's bundler manager keeps it bumped. No Gemfile.lock is
|
# Pinned exactly; Renovate's bundler manager keeps it bumped. No Gemfile.lock is
|
||||||
# committed on purpose: this resolves an uploader's transitive deps, not the
|
# committed on purpose: this resolves an uploader's transitive deps, not the
|
||||||
# app's, and none of it affects the reproducible release build.
|
# app's, and none of it affects the reproducible release build.
|
||||||
gem "fastlane", "2.237.0"
|
gem "fastlane", "2.240.1"
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ For automatic updates from Codeberg, use
|
|||||||
[add Calendula in one tap](https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://codeberg.org/jlmakiola/calendula),
|
[add Calendula in one tap](https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://codeberg.org/jlmakiola/calendula),
|
||||||
or add it by hand: *Add App*, paste `https://codeberg.org/jlmakiola/calendula`,
|
or add it by hand: *Add App*, paste `https://codeberg.org/jlmakiola/calendula`,
|
||||||
then *Add*. Obtainium then watches the releases and tells you when a new one
|
then *Add*. Obtainium then watches the releases and tells you when a new one
|
||||||
is out.
|
is out. Betas of upcoming versions are published there too, as pre-releases; to
|
||||||
|
test them, switch on *Include prereleases* for Calendula in Obtainium.
|
||||||
|
|
||||||
### Google Play
|
### Google Play
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,14 @@ 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 (versionCode is pinned to MAJOR*10000 + MINOR*100 +
|
// release itself. A versionName of X.Y.Z-beta.N pushed to a release/*
|
||||||
// PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md.
|
// branch instead cuts a Codeberg-only pre-release (beta.yaml).
|
||||||
versionCode = 22200
|
// versionCode is derived from versionName by scripts/version_info.sh
|
||||||
versionName = "2.22.0"
|
// (up to 2.22.3: 2.22.3 -> 22203; from 2.22.4: 2.23.0-beta.1 -> 2230001,
|
||||||
|
// 2.22.4 -> 2220499), and CI fails if the committed one doesn't match.
|
||||||
|
// See docs/RELEASING.md.
|
||||||
|
versionCode = 22203
|
||||||
|
versionName = "2.22.3"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ fun rememberFieldCopier(snackbarHostState: SnackbarHostState): FieldCopier {
|
|||||||
/**
|
/**
|
||||||
* Long-press to copy [text] whole, filed on the clipboard under [label].
|
* Long-press to copy [text] whole, filed on the clipboard under [label].
|
||||||
*
|
*
|
||||||
* [onTap] carries a field's existing tap action through; a field without one
|
* [onTap] carries a field's existing tap action through, announced as
|
||||||
* stays unclickable rather than growing a ripple that leads nowhere, and gets
|
* [onTapLabel]; a field without one stays unclickable rather than growing a
|
||||||
* its long press announced through semantics instead. That branch merges the
|
* ripple that leads nowhere, and gets its long press announced through semantics instead. That branch merges the
|
||||||
* node it sits on, so a screen reader lands on the field itself and finds the
|
* node it sits on, so a screen reader lands on the field itself and finds the
|
||||||
* action there — a bare container is never focused.
|
* action there — a bare container is never focused.
|
||||||
*/
|
*/
|
||||||
@@ -77,11 +77,13 @@ fun Modifier.copyOnLongPress(
|
|||||||
text: String,
|
text: String,
|
||||||
copy: FieldCopier,
|
copy: FieldCopier,
|
||||||
onTap: (() -> Unit)? = null,
|
onTap: (() -> Unit)? = null,
|
||||||
|
onTapLabel: String? = null,
|
||||||
): Modifier {
|
): Modifier {
|
||||||
val actionLabel = stringResource(R.string.field_copy_action)
|
val actionLabel = stringResource(R.string.field_copy_action)
|
||||||
return if (onTap != null) {
|
return if (onTap != null) {
|
||||||
combinedClickable(
|
combinedClickable(
|
||||||
onClick = onTap,
|
onClick = onTap,
|
||||||
|
onClickLabel = onTapLabel,
|
||||||
onLongClickLabel = actionLabel,
|
onLongClickLabel = actionLabel,
|
||||||
onLongClick = { copy(label, text) },
|
onLongClick = { copy(label, text) },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.detail
|
package de.jeanlucmakiola.calendula.ui.detail
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -534,7 +534,7 @@ private fun EventDetailContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Location (conditional, tap → maps).
|
// Location (conditional). URLs open in the browser, the rest in maps.
|
||||||
instance.location?.takeIf { it.isNotBlank() }?.let { location ->
|
instance.location?.takeIf { it.isNotBlank() }?.let { location ->
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val locationLabel = stringResource(R.string.event_detail_location)
|
val locationLabel = stringResource(R.string.event_detail_location)
|
||||||
@@ -544,7 +544,7 @@ private fun EventDetailContent(
|
|||||||
iconContentDescription = locationLabel,
|
iconContentDescription = locationLabel,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = location,
|
text = linkifyUrls(location, MaterialTheme.colorScheme.primary),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -554,6 +554,7 @@ private fun EventDetailContent(
|
|||||||
text = location,
|
text = location,
|
||||||
copy = copyField,
|
copy = copyField,
|
||||||
onTap = { openInMaps(context, location) },
|
onTap = { openInMaps(context, location) },
|
||||||
|
onTapLabel = stringResource(R.string.event_detail_open_in_maps),
|
||||||
)
|
)
|
||||||
.padding(vertical = 2.dp),
|
.padding(vertical = 2.dp),
|
||||||
)
|
)
|
||||||
@@ -846,21 +847,23 @@ private fun foreignTimeZone(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wrap http(s) URLs in [text] as tappable links tinted [linkColor]. */
|
/** Wrap http(s) URLs in [text] as links tinted [linkColor] that open in the browser. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun linkifyUrls(text: String, linkColor: Color): AnnotatedString = remember(text, linkColor) {
|
private fun linkifyUrls(text: String, linkColor: Color): AnnotatedString {
|
||||||
val regex = Regex("""https?://\S+""")
|
val context = LocalContext.current
|
||||||
val styles = TextLinkStyles(
|
return remember(text, linkColor, context) {
|
||||||
style = SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline),
|
val styles = TextLinkStyles(
|
||||||
)
|
style = SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline),
|
||||||
buildAnnotatedString {
|
)
|
||||||
append(text)
|
buildAnnotatedString {
|
||||||
for (match in regex.findAll(text)) {
|
append(text)
|
||||||
// Trim trailing punctuation that commonly abuts a URL in prose.
|
for (link in findWebLinks(text)) {
|
||||||
val raw = match.value
|
addLink(
|
||||||
val url = raw.trimEnd('.', ',', ';', ':', '!', '?', ')', ']', '"', '\'')
|
LinkAnnotation.Url(link.url, styles) { openWebLink(context, link.url) },
|
||||||
val end = match.range.first + url.length
|
link.start,
|
||||||
addLink(LinkAnnotation.Url(url, styles), match.range.first, end)
|
link.end,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -915,21 +918,19 @@ private fun formatWhen(
|
|||||||
private fun Instant.toJavaLocalDateTime(zid: ZoneId): java.time.LocalDateTime =
|
private fun Instant.toJavaLocalDateTime(zid: ZoneId): java.time.LocalDateTime =
|
||||||
java.time.LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(toEpochMilliseconds()), zid)
|
java.time.LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(toEpochMilliseconds()), zid)
|
||||||
|
|
||||||
|
/** Open [url] in a browsable app, or say so when none can take it. */
|
||||||
|
private fun openWebLink(context: Context, url: String) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).addCategory(Intent.CATEGORY_BROWSABLE)
|
||||||
|
if (runCatching { context.startActivity(intent) }.isFailure) {
|
||||||
|
Toast.makeText(context, R.string.link_open_failed, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Open a maps intent for [query]; fall back to a web maps URL if no app handles geo:. */
|
/** Open a maps intent for [query]; fall back to a web maps URL if no app handles geo:. */
|
||||||
private fun openInMaps(context: Context, query: String) {
|
private fun openInMaps(context: Context, query: String) {
|
||||||
val encoded = Uri.encode(query)
|
val encoded = Uri.encode(query)
|
||||||
val geo = Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=$encoded"))
|
val geo = Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=$encoded"))
|
||||||
try {
|
if (runCatching { context.startActivity(geo) }.isFailure) {
|
||||||
context.startActivity(geo)
|
openWebLink(context, "https://www.google.com/maps/search/?api=1&query=$encoded")
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
val web = Intent(
|
|
||||||
Intent.ACTION_VIEW,
|
|
||||||
Uri.parse("https://www.google.com/maps/search/?api=1&query=$encoded"),
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
context.startActivity(web)
|
|
||||||
} catch (e2: ActivityNotFoundException) {
|
|
||||||
// No browser either — nothing sensible to do; swallow.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.detail
|
||||||
|
|
||||||
|
/** An http(s) URL found in free text, spanning [start] until [end] (exclusive). */
|
||||||
|
internal data class WebLink(val start: Int, val end: Int, val url: String)
|
||||||
|
|
||||||
|
private val webLinkRegex = Regex("""https?://[^\s<>"]+""", RegexOption.IGNORE_CASE)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the http(s) URLs in [text]. Trailing prose punctuation is left out of
|
||||||
|
* each match, and the scheme is lower-cased so intent filters match it.
|
||||||
|
*/
|
||||||
|
internal fun findWebLinks(text: String): List<WebLink> = webLinkRegex.findAll(text).mapNotNull { match ->
|
||||||
|
val raw = match.value.trimEnd('.', ',', ';', ':', '!', '?', ')', ']', '\'')
|
||||||
|
val schemeEnd = raw.indexOf(':')
|
||||||
|
if (raw.length <= schemeEnd + 3) return@mapNotNull null
|
||||||
|
val url = raw.substring(0, schemeEnd).lowercase() + raw.substring(schemeEnd)
|
||||||
|
WebLink(match.range.first, match.range.first + raw.length, url)
|
||||||
|
}.toList()
|
||||||
@@ -211,6 +211,8 @@
|
|||||||
<string name="event_detail_calendar_unknown">Unknown calendar</string>
|
<string name="event_detail_calendar_unknown">Unknown calendar</string>
|
||||||
<string name="event_detail_title">Title</string>
|
<string name="event_detail_title">Title</string>
|
||||||
<string name="event_detail_location">Location</string>
|
<string name="event_detail_location">Location</string>
|
||||||
|
<string name="event_detail_open_in_maps">Open in maps</string>
|
||||||
|
<string name="link_open_failed">No app can open this link</string>
|
||||||
<string name="event_detail_description">Description</string>
|
<string name="event_detail_description">Description</string>
|
||||||
<string name="event_detail_attendees">Attendees</string>
|
<string name="event_detail_attendees">Attendees</string>
|
||||||
<string name="event_detail_recurrence">Recurrence</string>
|
<string name="event_detail_recurrence">Recurrence</string>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.detail
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class WebLinksTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a plain address has no links`() {
|
||||||
|
assertThat(findWebLinks("Office B, 12 Main St.")).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a bare meeting link spans the whole text`() {
|
||||||
|
val text = "https://meet.google.com/abc-defg-hij"
|
||||||
|
assertThat(findWebLinks(text)).containsExactly(WebLink(0, text.length, text))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a capitalised scheme is matched and lower-cased`() {
|
||||||
|
val links = findWebLinks("Https://zoom.us/j/123")
|
||||||
|
assertThat(links.single().url).isEqualTo("https://zoom.us/j/123")
|
||||||
|
assertThat(links.single().end).isEqualTo(21)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `angle brackets around a link stay outside it`() {
|
||||||
|
val text = "Microsoft Teams Meeting <https://teams.microsoft.com/l/meetup-join/x>"
|
||||||
|
val link = findWebLinks(text).single()
|
||||||
|
assertThat(link.url).isEqualTo("https://teams.microsoft.com/l/meetup-join/x")
|
||||||
|
assertThat(text.substring(link.start, link.end)).isEqualTo(link.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `trailing punctuation is left out of the link`() {
|
||||||
|
val link = findWebLinks("Office B (https://office.example.com).").single()
|
||||||
|
assertThat(link.url).isEqualTo("https://office.example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `every link in the text is found`() {
|
||||||
|
val links = findWebLinks("Room 3 / https://zoom.us/j/1 / https://meet.google.com/x")
|
||||||
|
assertThat(links.map { it.url })
|
||||||
|
.containsExactly("https://zoom.us/j/1", "https://meet.google.com/x")
|
||||||
|
.inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `other schemes and a bare scheme are not links`() {
|
||||||
|
assertThat(findWebLinks("ftp://example.com mailto:a@b.c https://")).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
-7
@@ -5,34 +5,56 @@ built, signed, and published automatically by `.gitea/workflows/release.yaml`
|
|||||||
when a **bumped `versionName` reaches `main`** — the pipeline then creates the
|
when a **bumped `versionName` reaches `main`** — the pipeline then creates the
|
||||||
matching `vX.Y.Z` tag and Gitea release itself.
|
matching `vX.Y.Z` tag and Gitea release itself.
|
||||||
|
|
||||||
|
Before a stable release you can ship **betas** (`X.Y.Z-beta.N`) from the
|
||||||
|
release branch. They go to **Codeberg only**, flagged as pre-releases; F-Droid
|
||||||
|
and Play never see them. See [Cutting a beta](#cutting-a-beta).
|
||||||
|
|
||||||
## Versioning — the committed version is the source of truth
|
## Versioning — the committed version is the source of truth
|
||||||
|
|
||||||
A release is defined by the `versionName`/`versionCode` committed in
|
A release is defined by the `versionName`/`versionCode` committed in
|
||||||
`app/build.gradle.kts`:
|
`app/build.gradle.kts`:
|
||||||
|
|
||||||
- `versionName` = `MAJOR.MINOR.PATCH` (e.g. `2.1.0`)
|
- `versionName` = `MAJOR.MINOR.PATCH` (e.g. `2.22.4`), or
|
||||||
- `versionCode` = `MAJOR*10000 + MINOR*100 + PATCH` (`2.1.0` → `20100`)
|
`MAJOR.MINOR.PATCH-beta.N` for a beta (e.g. `2.23.0-beta.2`)
|
||||||
|
- `versionCode` is derived from it by `scripts/version_info.sh`:
|
||||||
|
|
||||||
So `MINOR` and `PATCH` each have room for 0–99. The release pipeline reads
|
| `versionName` | `versionCode` | Example |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `X.Y.Z` up to 2.22.3 (legacy) | `X*10000 + Y*100 + Z` | `2.22.3` → `22203` |
|
||||||
|
| `X.Y.Z-beta.N` (N = 1–98) | `X*1000000 + Y*10000 + Z*100 + N` | `2.23.0-beta.2` → `2230002` |
|
||||||
|
| `X.Y.Z` from 2.22.4 | `X*1000000 + Y*10000 + Z*100 + 99` | `2.22.4` → `2220499` |
|
||||||
|
|
||||||
|
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 version up to 2.22.3 keeps the code it already shipped with, and
|
||||||
|
`2.22.4` is the first on the new scheme. `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, and — once the
|
`versionName`, pins `versionCode` to the derived value, builds, and — once the
|
||||||
APK is published — creates the tag `v<versionName>` at that commit. The tag is
|
APK is published — 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
|
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
|
fully-shipped version (and a failure before publish leaves no tag, so re-running
|
||||||
the workflow safely retries).
|
the workflow safely retries).
|
||||||
|
|
||||||
Published version codes so far: `v0.1.0`→100 … `v1.0.0`→10000 … `v2.0.0`→20000.
|
Published version codes so far: `v0.1.0`→100 … `v2.0.0`→20000 …
|
||||||
|
`v2.22.3`→22203, then `v2.22.4`→2220499.
|
||||||
|
|
||||||
## Cutting a release
|
## Cutting a release
|
||||||
|
|
||||||
1. **Assemble the release branch.** Create `release/vX.Y.Z` and merge the
|
1. **Assemble the release branch.** Create `release/vX.Y.Z` and merge the
|
||||||
feature/fix branches that make up this release into it. This branch is the
|
feature/fix branches that make up this release into it. This branch is the
|
||||||
release candidate — everything below happens on it, before it reaches `main`.
|
release candidate — everything below happens on it, before it reaches `main`.
|
||||||
|
To ship betas first, follow [Cutting a beta](#cutting-a-beta) from here and
|
||||||
|
come back to step 2 when going stable.
|
||||||
2. Move the `## [Unreleased]` section of `CHANGELOG.md` under a new
|
2. Move the `## [Unreleased]` section of `CHANGELOG.md` under a new
|
||||||
`## [X.Y.Z] — <date>` heading (Keep a Changelog format). The text between
|
`## [X.Y.Z] — <date>` heading (Keep a Changelog format). The text between
|
||||||
that heading and the next `## [` becomes both the Gitea release notes and
|
that heading and the next `## [` becomes both the Gitea release notes and
|
||||||
the F-Droid per-version changelog.
|
the F-Droid per-version changelog.
|
||||||
3. Bump the committed `versionName` (and `versionCode`) in
|
3. Bump the committed `versionName` (and `versionCode`, which
|
||||||
`app/build.gradle.kts` to the new version. **This bump is what triggers the
|
`scripts/version_info.sh` prints) in `app/build.gradle.kts` to the new version. **This bump is what triggers the
|
||||||
release** when the branch merges to `main`. Then write the per-version
|
release** when the branch merges to `main`. Then write the per-version
|
||||||
"What's New" by hand to
|
"What's New" by hand to
|
||||||
`fastlane/metadata/android/en-US/changelogs/<versionCode>.txt` and commit it.
|
`fastlane/metadata/android/en-US/changelogs/<versionCode>.txt` and commit it.
|
||||||
@@ -89,6 +111,48 @@ Published version codes so far: `v0.1.0`→100 … `v1.0.0`→10000 … `v2.0.0`
|
|||||||
> The `releaseTest` build type exists only for step 4 — it is never published.
|
> The `releaseTest` build type exists only for step 4 — it is never published.
|
||||||
> The pipeline always builds and signs the real `release` variant.
|
> The pipeline always builds and signs the real `release` variant.
|
||||||
|
|
||||||
|
## 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; `2.23.0-beta.1` → `2230001`).
|
||||||
|
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]`. So keep the entries under `## [Unreleased]` while
|
||||||
|
betas are going out and only move them to `## [X.Y.Z]` when going stable:
|
||||||
|
once they're moved, a beta's notes come out empty.
|
||||||
|
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`
|
||||||
|
(`2.23.0` → `2230099`), 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. Keep that pattern if the recipe ever changes.
|
||||||
|
- **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 2.22.4 (the legacy
|
||||||
|
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
|
||||||
@@ -108,6 +172,10 @@ release work when a merge actually cuts a release:
|
|||||||
mirror the release to **Codeberg** with the signed APK + a SHA-256 checksum
|
mirror the release to **Codeberg** with the signed APK + a SHA-256 checksum
|
||||||
(both best-effort). Ordinary merges with no version bump fall through `detect`
|
(both best-effort). Ordinary merges with no version bump fall through `detect`
|
||||||
and do nothing.
|
and do nothing.
|
||||||
|
- **`beta.yaml`** (on push to `release/**`) — when the committed `versionName`
|
||||||
|
is a beta with no tag yet: unit tests, build & sign with the app key, Gitea
|
||||||
|
pre-release with the R8 mapping, Codeberg pre-release with the APK +
|
||||||
|
`.sha256`. Nothing else; see [Cutting a beta](#cutting-a-beta).
|
||||||
- **`play` job** (same workflow, after `release`) — uploads the App Bundle to
|
- **`play` job** (same workflow, after `release`) — uploads the App Bundle to
|
||||||
Google Play. Runs last and separately so a Play rejection can't endanger a
|
Google Play. Runs last and separately so a Play rejection can't endanger a
|
||||||
release that already shipped; skips cleanly until Play is configured.
|
release that already shipped; skips cleanly until Play is configured.
|
||||||
@@ -184,7 +252,7 @@ on every pull request:
|
|||||||
| `images/icon.png` | 512×512, 32-bit PNG |
|
| `images/icon.png` | 512×512, 32-bit PNG |
|
||||||
| `images/featureGraphic.png` | 1024×500, JPEG or 24-bit PNG |
|
| `images/featureGraphic.png` | 1024×500, JPEG or 24-bit PNG |
|
||||||
| `images/*Screenshots/*` | JPEG or 24-bit PNG (no alpha), sides 320–3840, long edge ≤ 2× short, 2–8 per type |
|
| `images/*Screenshots/*` | JPEG or 24-bit PNG (no alpha), sides 320–3840, long edge ≤ 2× short, 2–8 per type |
|
||||||
| locale directories | exactly the ones in `fastlane/store-locales.txt`, which maps every app `values-*` to a Play locale code |
|
| locale directories | exactly the ones in `fastlane/store-locales.txt`, which maps every app `values-*` to a Play locale code; the code must be one Play accepts (`nl-NL`, not `nl`) |
|
||||||
|
|
||||||
A locale without its own images falls back to en-US in both stores, so graphics
|
A locale without its own images falls back to en-US in both stores, so graphics
|
||||||
only need to exist there. Adding an app language means adding its line to
|
only need to exist there. Adding an app language means adding its line to
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
إصلاحات
|
||||||
|
• النقر على رابط في موقع الحدث يفتحه الآن في المتصفح بدلًا من تطبيق الخرائط. النقر على العنوان ما زال يفتح الخرائط.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
لم يصل الإصداران 2.22.0 و2.22.1 إلى Google Play، لذا هذا أول إصدار 2.22 هناك. التطبيق مطابق للإصدار 2.22.1.
|
||||||
|
|
||||||
|
• يتنقّل اليوم والأسبوع والشهر بمُنزلق يتبع إصبعك.
|
||||||
|
• يعرض العرض متعدد الأيام عدداً قابلاً للتخصيص من الأيام جنباً إلى جنب.
|
||||||
|
• الساعات المرئية: يملأ نطاق ساعات تختاره الشاشة.
|
||||||
|
• يبرز اليوم في شبكة الشهر.
|
||||||
|
• يُفتح الرابط في موقع الحدث في المتصفح.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
عاد اسم صفحة المتجر إلى "Calendula: التقويم". لم يتغير شيء في التطبيق.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: التقويم
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Выпраўлена
|
||||||
|
• Націск на спасылку ў месцы падзеі адкрывае яе ў браўзеры, а не ў праграме карт. Націск на адрас па-ранейшаму адкрывае карты.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Вэрсіі 2.22.0 і 2.22.1 не трапілі ў Google Play, таму гэта першая вэрсія 2.22 там. Праграма такая ж, як 2.22.1.
|
||||||
|
|
||||||
|
• Дзень, Тыдзень і Месяц перагортваюцца пэйджарам, які сочыць за пальцам.
|
||||||
|
• Шматдзённы выгляд паказвае наладжвальную колькасць дзён побач.
|
||||||
|
• Бачныя гадзіны: абраны дыяпазон гадзін запаўняе экран.
|
||||||
|
• Сёньня вылучаецца ў сетцы месяца.
|
||||||
|
• Спасылка ў месцы падзеі адкрываецца ў браўзеры.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Старонка ў краме зноў называецца «Calendula: Каляндар». Праграма не змянілася.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Каляндар
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Opraveno
|
||||||
|
• Klepnutí na odkaz v místě události ho otevře v prohlížeči místo v mapách. Klepnutí na adresu dál otevírá mapy.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Verze 2.22.0 a 2.22.1 se na Google Play nedostaly, takže tohle je tam první verze 2.22. Aplikace je stejná jako 2.22.1.
|
||||||
|
|
||||||
|
• Den, Týden a Měsíc listují stránkovačem, který sleduje prst.
|
||||||
|
• Vícedenní zobrazení ukazuje nastavitelný počet dní vedle sebe.
|
||||||
|
• Viditelné hodiny: zvolený rozsah hodin vyplní obrazovku.
|
||||||
|
• Dnes vyniká v mřížce měsíce.
|
||||||
|
• Odkaz v místě události se otevře v prohlížeči.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Stránka v obchodě se opět jmenuje „Calendula: Kalendář“. Aplikace se nezměnila.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Kalendář
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Behoben
|
||||||
|
• Ein Tipp auf einen Link im Ort eines Termins öffnet ihn im Browser statt in einer Karten-App. Ein Tipp auf die Adresse öffnet weiterhin die Karte.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Google Play hat 2.22.0 und 2.22.1 verpasst, deshalb ist das dort die erste 2.22-Version. Die App ist dieselbe wie 2.22.1.
|
||||||
|
|
||||||
|
• Tag, Woche und Monat lassen sich mit einem fingerfolgenden Pager wischen.
|
||||||
|
• Mehrtagesansicht: einstellbare Anzahl Tage nebeneinander.
|
||||||
|
• Sichtbare Stunden: ein gewählter Stundenbereich füllt den Bildschirm.
|
||||||
|
• Heute hebt sich im Monatsraster ab.
|
||||||
|
• Ein Link im Ort eines Termins öffnet sich im Browser.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Der Store-Eintrag heißt wieder „Calendula: Kalender“. An der App hat sich nichts geändert.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Kalender
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Fixed
|
||||||
|
• Tapping a link in an event's location opens it in the browser instead of a maps app. Tapping the address still opens maps.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Google Play missed 2.22.0 and 2.22.1, so this is the first 2.22 release there. The app is the same as 2.22.1.
|
||||||
|
|
||||||
|
• Day, Week and Month follow your finger when you swipe between dates.
|
||||||
|
• A multi-day view shows a set number of days side by side.
|
||||||
|
• Visible hours fits the hours you choose to the screen.
|
||||||
|
• Today stands out in the month grid.
|
||||||
|
• A link in an event's location opens in the browser.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
The store listing is called "Calendula: Calendar" again. Nothing in the app changed.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Calendar
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Corregido
|
||||||
|
• Al tocar un enlace en la ubicación de un evento, se abre en el navegador en lugar de en una app de mapas. Tocar la dirección sigue abriendo los mapas.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
2.22.0 y 2.22.1 no llegaron a Google Play, así que esta es la primera versión 2.22 allí. La app es la misma que 2.22.1.
|
||||||
|
|
||||||
|
• Día, Semana y Mes se deslizan con un paginador que sigue tu dedo.
|
||||||
|
• Vista de varios días: muestra un número configurable de días uno junto a otro.
|
||||||
|
• Horas visibles: un rango de horas elegido llena la pantalla.
|
||||||
|
• Hoy destaca en la cuadrícula del mes.
|
||||||
|
• Un enlace en la ubicación de un evento se abre en el navegador.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
La ficha de la tienda vuelve a llamarse «Calendula: Calendario». La app no cambia.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Calendario
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Corrigé
|
||||||
|
• Toucher un lien dans le lieu d'un événement l'ouvre dans le navigateur et non plus dans une app de cartes. Toucher l'adresse ouvre toujours les cartes.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Les versions 2.22.0 et 2.22.1 ne sont pas arrivées sur Google Play ; voici donc la première version 2.22 sur Play. L'app est identique à la 2.22.1.
|
||||||
|
|
||||||
|
• Jour, Semaine et Mois défilent avec un pager qui suit le doigt.
|
||||||
|
• Vue multi-jours : affiche un nombre de jours réglable, côte à côte.
|
||||||
|
• Heures visibles : une plage d'heures choisie remplit l'écran.
|
||||||
|
• Aujourd'hui ressort dans la grille du mois.
|
||||||
|
• Un lien dans le lieu d'un événement s'ouvre dans le navigateur.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
La fiche du store s'appelle de nouveau « Calendula: Calendrier ». L'app ne change pas.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Calendrier
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Javítva
|
||||||
|
• Az esemény helyszínében lévő linkre koppintva az a böngészőben nyílik meg térképalkalmazás helyett. A címre koppintva továbbra is a térkép nyílik meg.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
A 2.22.0 és a 2.22.1 nem jutott el a Google Playre, így ez az első 2.22-es verzió ott. Az alkalmazás ugyanaz, mint a 2.22.1.
|
||||||
|
|
||||||
|
• A Nap, Hét és Hónap nézet ujjkövető lapozóval lapozható.
|
||||||
|
• A többnapos nézet beállítható számú napot jelenít meg egymás mellett.
|
||||||
|
• Látható órák: a kiválasztott óratartomány kitölti a képernyőt.
|
||||||
|
• A mai nap kiemelkedik a havi rácsban.
|
||||||
|
• Az esemény helyszínében lévő link a böngészőben nyílik meg.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Az áruházi oldal neve ismét „Calendula: Naptár”. Az alkalmazás nem változott.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Naptár
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Corretto
|
||||||
|
• Toccare un link nel luogo di un evento lo apre nel browser invece che in un'app di mappe. Toccare l'indirizzo apre ancora le mappe.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Le versioni 2.22.0 e 2.22.1 non sono arrivate su Google Play, quindi questa è la prima versione 2.22 lì. L'app è la stessa della 2.22.1.
|
||||||
|
|
||||||
|
• Giorno, Settimana e Mese scorrono con un pager che segue il dito.
|
||||||
|
• Vista multi-giorno: mostra un numero configurabile di giorni affiancati.
|
||||||
|
• Ore visibili: un intervallo di ore scelto riempie lo schermo.
|
||||||
|
• Oggi risalta nella griglia del mese.
|
||||||
|
• Un link nel luogo di un evento si apre nel browser.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
La scheda dello store si chiama di nuovo «Calendula: Calendario». L'app non cambia.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Calendario
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
修正
|
||||||
|
• 予定の場所にあるリンクをタップすると、地図アプリではなくブラウザで開くようになりました。住所をタップすると引き続き地図が開きます。
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
2.22.0と2.22.1はGoogle Playに配信されなかったため、これがGoogle Playでの最初の2.22です。アプリの内容は2.22.1と同じです。
|
||||||
|
|
||||||
|
• 日・週・月ビューが指に追従するページャーでめくれます。
|
||||||
|
• マルチデイビューで、設定可能な日数を横に並べて表示します。
|
||||||
|
• 表示時間帯: 選んだ時間の範囲が画面いっぱいに表示されます。
|
||||||
|
• 月表示で今日が目立ちます。
|
||||||
|
• 予定の場所にあるリンクはブラウザで開きます。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ストアの掲載名が再び「Calendula: カレンダー」になりました。アプリ自体に変更はありません。
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: カレンダー
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Opgelost
|
||||||
|
• Tikken op een link in de locatie van een afspraak opent die in de browser in plaats van in een kaarten-app. Tikken op het adres opent nog steeds de kaart.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
2.22.0 en 2.22.1 zijn nooit op Google Play verschenen, dus dit is daar de eerste 2.22-versie. De app is gelijk aan 2.22.1.
|
||||||
|
|
||||||
|
• Dag, Week en Maand blader je door met een pager die je vinger volgt.
|
||||||
|
• Een meerdaagse weergave toont een instelbaar aantal dagen naast elkaar.
|
||||||
|
• Zichtbare uren: een gekozen tijdsbereik vult het scherm.
|
||||||
|
• Vandaag valt op in het maandraster.
|
||||||
|
• Een link in de locatie van een afspraak opent in de browser.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
De winkelvermelding heet weer "Calendula: Agenda". Aan de app is niets veranderd.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Calendula: Agenda
|
||||||
@@ -1 +0,0 @@
|
|||||||
Calendula
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Poprawki
|
||||||
|
• Dotknięcie linku w miejscu wydarzenia otwiera go w przeglądarce zamiast w aplikacji map. Dotknięcie adresu nadal otwiera mapy.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Wersje 2.22.0 i 2.22.1 nie trafiły do Google Play, więc to pierwsza wersja 2.22 w tym sklepie. Aplikacja jest taka sama jak 2.22.1.
|
||||||
|
|
||||||
|
• Dzień, Tydzień i Miesiąc przewijają się pagerem, który podąża za palcem.
|
||||||
|
• Widok wielodniowy pokazuje ustawialną liczbę dni obok siebie.
|
||||||
|
• Widoczne godziny: wybrany zakres godzin wypełnia ekran.
|
||||||
|
• Dziś wyróżnia się w siatce miesiąca.
|
||||||
|
• Link w miejscu wydarzenia otwiera się w przeglądarce.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Strona w sklepie znów nazywa się „Calendula: Kalendarz”. Aplikacja się nie zmieniła.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Kalendarz
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Corrigido
|
||||||
|
• Tocar em um link no local de um evento agora o abre no navegador em vez de em um app de mapas. Tocar no endereço continua abrindo os mapas.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
As versões 2.22.0 e 2.22.1 não chegaram ao Google Play, então esta é a primeira versão 2.22 lá. O app é o mesmo da 2.22.1.
|
||||||
|
|
||||||
|
• Dia, Semana e Mês avançam com um pager que acompanha o dedo.
|
||||||
|
• Visualização de vários dias: mostra um número configurável de dias lado a lado.
|
||||||
|
• Horas visíveis: um intervalo de horas escolhido preenche a tela.
|
||||||
|
• Hoje se destaca na grade do mês.
|
||||||
|
• Um link no local de um evento abre no navegador.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
A página da loja voltou a se chamar "Calendula: Calendário". O app não mudou.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Calendário
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Corrigido
|
||||||
|
• Tocar numa ligação no local de um evento abre-a no navegador em vez de numa app de mapas. Tocar na morada continua a abrir os mapas.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
As versões 2.22.0 e 2.22.1 não chegaram ao Google Play, por isso esta é a primeira versão 2.22 lá. A app é a mesma da 2.22.1.
|
||||||
|
|
||||||
|
• Dia, Semana e Mês deslizam com um pager que acompanha o dedo.
|
||||||
|
• Vista de vários dias: mostra um número configurável de dias lado a lado.
|
||||||
|
• Horas visíveis: um intervalo de horas escolhido preenche o ecrã.
|
||||||
|
• Hoje destaca-se na grelha do mês.
|
||||||
|
• Uma ligação no local de um evento abre no navegador.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
A página da loja volta a chamar-se "Calendula: Calendário". A app não mudou.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Calendário
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Исправлено
|
||||||
|
• Нажатие на ссылку в месте события открывает её в браузере, а не в приложении карт. Нажатие на адрес по-прежнему открывает карты.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Версии 2.22.0 и 2.22.1 не попали в Google Play, поэтому это первая версия 2.22 там. Приложение то же, что и 2.22.1.
|
||||||
|
|
||||||
|
• День, Неделя и Месяц листаются пейджером, который следует за пальцем.
|
||||||
|
• Многодневный вид показывает настраиваемое число дней рядом друг с другом.
|
||||||
|
• Видимые часы: выбранный диапазон часов заполняет экран.
|
||||||
|
• Сегодня выделяется в сетке месяца.
|
||||||
|
• Ссылка в месте события открывается в браузере.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Страница в магазине снова называется «Calendula: Календарь». Приложение не изменилось.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Календарь
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Opravené
|
||||||
|
• Ťuknutie na odkaz v mieste udalosti ho otvorí v prehliadači namiesto v mapách. Ťuknutie na adresu naďalej otvára mapy.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Verzie 2.22.0 a 2.22.1 sa na Google Play nedostali, takže toto je tam prvá verzia 2.22. Aplikácia je rovnaká ako 2.22.1.
|
||||||
|
|
||||||
|
• Deň, Týždeň a Mesiac sa listujú stránkovačom, ktorý sleduje prst.
|
||||||
|
• Viacdňové zobrazenie ukazuje nastaviteľný počet dní vedľa seba.
|
||||||
|
• Viditeľné hodiny: zvolený rozsah hodín vyplní obrazovku.
|
||||||
|
• Dnešok vyniká v mriežke mesiaca.
|
||||||
|
• Odkaz v mieste udalosti sa otvorí v prehliadači.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Stránka v obchode sa opäť volá „Calendula: Kalendár“. Aplikácia sa nezmenila.
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: Kalendár
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
修复
|
||||||
|
• 点按日程地点中的链接现在会在浏览器中打开,而不是地图应用。点按地址仍会打开地图。
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
2.22.0 和 2.22.1 未能发布到 Google Play,因此这是 Google Play 上的第一个 2.22 版本。应用内容与 2.22.1 相同。
|
||||||
|
|
||||||
|
• 日、周、月视图可以用跟手的分页手势滑动切换。
|
||||||
|
• 多日视图并排显示可自定义天数。
|
||||||
|
• 可见时段:选定的时间范围会铺满整个屏幕。
|
||||||
|
• “今天”在月视图网格中突出显示。
|
||||||
|
• 日程地点中的链接会在浏览器中打开。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
商店页面名称恢复为“Calendula: 日历”。应用本身没有变化。
|
||||||
@@ -1 +1 @@
|
|||||||
Calendula
|
Calendula: 日历
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ fr fr-FR
|
|||||||
hu hu-HU
|
hu hu-HU
|
||||||
it it-IT
|
it it-IT
|
||||||
ja ja-JP
|
ja ja-JP
|
||||||
nl nl
|
nl nl-NL
|
||||||
pl pl-PL
|
pl pl-PL
|
||||||
pt pt-PT
|
pt pt-PT
|
||||||
pt-rBR pt-BR
|
pt-rBR pt-BR
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#This file is generated by updateDaemonJvm
|
#This file is generated by updateDaemonJvm
|
||||||
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
toolchainUrl.FREE_BSD.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-aarch64-b1163.116.tar.gz
|
||||||
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
toolchainUrl.FREE_BSD.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz
|
||||||
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
toolchainUrl.LINUX.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-aarch64-b1163.116.tar.gz
|
||||||
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
toolchainUrl.LINUX.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz
|
||||||
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
|
toolchainUrl.MAC_OS.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-osx-aarch64-b1163.116.tar.gz
|
||||||
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
|
toolchainUrl.MAC_OS.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-osx-x64-b1163.116.tar.gz
|
||||||
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
toolchainUrl.UNIX.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-aarch64-b1163.116.tar.gz
|
||||||
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
toolchainUrl.UNIX.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-linux-x64-b1163.116.tar.gz
|
||||||
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
|
toolchainUrl.WINDOWS.AARCH64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-windows-aarch64-b1163.116.tar.gz
|
||||||
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
|
toolchainUrl.WINDOWS.X86_64=https\://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.11-windows-x64-b1163.116.tar.gz
|
||||||
toolchainVendor=JETBRAINS
|
toolchainVendor=JETBRAINS
|
||||||
toolchainVersion=21
|
toolchainVersion=21
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ coreKtx = "1.19.0"
|
|||||||
appcompat = "1.7.1"
|
appcompat = "1.7.1"
|
||||||
lifecycleRuntime = "2.11.0"
|
lifecycleRuntime = "2.11.0"
|
||||||
activityCompose = "1.13.0"
|
activityCompose = "1.13.0"
|
||||||
composeBom = "2026.06.01"
|
composeBom = "2026.09.00"
|
||||||
# Material 3 Expressive APIs currently live only in the 1.5 alpha line.
|
# Material 3 Expressive APIs currently live only in the 1.5 alpha line.
|
||||||
# Pin explicitly to override the BOM (which ships stable 1.4.0).
|
# Pin explicitly to override the BOM (which ships stable 1.4.0).
|
||||||
# Re-evaluate when 1.5.0 stable lands.
|
# Re-evaluate when 1.5.0 stable lands.
|
||||||
material3 = "1.5.0-alpha26"
|
material3 = "1.5.0-alpha29"
|
||||||
datastore = "1.2.1"
|
datastore = "1.2.1"
|
||||||
junit = "6.1.3"
|
junit = "6.1.3"
|
||||||
junitPlatform = "6.1.3"
|
junitPlatform = "6.1.3"
|
||||||
@@ -22,10 +22,10 @@ kotlinxDatetime = "0.7.0"
|
|||||||
kotlinxCoroutines = "1.11.0"
|
kotlinxCoroutines = "1.11.0"
|
||||||
turbine = "1.2.1"
|
turbine = "1.2.1"
|
||||||
hiltNavigationCompose = "1.4.0"
|
hiltNavigationCompose = "1.4.0"
|
||||||
lifecycleCompose = "2.10.0"
|
lifecycleCompose = "2.11.0"
|
||||||
androidxTestRules = "1.7.0"
|
androidxTestRules = "1.7.0"
|
||||||
# Glance: 1.1.1 is the latest stable (1.2.0 is still rc, 1.3.0 alpha).
|
# Glance: 1.1.1 is the latest stable (1.2.0 is still rc, 1.3.0 alpha).
|
||||||
glance = "1.1.1"
|
glance = "1.2.0"
|
||||||
work = "2.11.2"
|
work = "2.11.2"
|
||||||
documentfile = "1.1.0"
|
documentfile = "1.1.0"
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -52,9 +52,16 @@
|
|||||||
prConcurrentLimit: 5,
|
prConcurrentLimit: 5,
|
||||||
prHourlyLimit: 0,
|
prHourlyLimit: 0,
|
||||||
|
|
||||||
// Cadence is owned by the Gitea Actions cron (.gitea/workflows/renovate.yml,
|
// New branches/PRs only in the Monday window (UTC), which brackets the weekly
|
||||||
// Mondays) — no internal `schedule` here, so the two don't double-gate and
|
// cron in .gitea/workflows/renovate.yml with slack for a late start — keep
|
||||||
// silently skip a run.
|
// the two in step, or the sweep lands outside the window and finds nothing.
|
||||||
|
// 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",
|
||||||
|
|
||||||
// Gitea Actions workflows live under .gitea/workflows, not .github — extend
|
// Gitea Actions workflows live under .gitea/workflows, not .github — extend
|
||||||
// the github-actions manager (same syntax) to watch them too.
|
// the github-actions manager (same syntax) to watch them too.
|
||||||
|
|||||||
@@ -28,10 +28,11 @@ LIMIT=500
|
|||||||
STRICT=0
|
STRICT=0
|
||||||
[ "${1:-}" = "--strict" ] && STRICT=1
|
[ "${1:-}" = "--strict" ] && STRICT=1
|
||||||
|
|
||||||
VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
|
VERSION=$(bash scripts/version_info.sh version)
|
||||||
[ -n "$VERSION" ] || { echo "No versionName in app/build.gradle.kts" >&2; exit 1; }
|
VERSION_CODE=$(bash scripts/version_info.sh version_code)
|
||||||
MAJOR=${VERSION%%.*}; rest=${VERSION#*.}; MINOR=${rest%%.*}; PATCH=${rest##*.}
|
# A beta ships no What's New, so only the older files are checked.
|
||||||
VERSION_CODE=$(( ${MAJOR:-0} * 10000 + ${MINOR:-0} * 100 + ${PATCH:-0} ))
|
BETA=0
|
||||||
|
[ "$(bash scripts/version_info.sh channel)" = beta ] && BETA=1
|
||||||
|
|
||||||
fail=0
|
fail=0
|
||||||
warned=0
|
warned=0
|
||||||
@@ -65,7 +66,7 @@ for f in fastlane/metadata/android/*/changelogs/*.txt; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$current" -eq 0 ]; then
|
if [ "$current" -eq 0 ] && [ "$BETA" -eq 0 ]; then
|
||||||
echo "ERROR: no changelog for version $VERSION (code $VERSION_CODE)." >&2
|
echo "ERROR: no changelog for version $VERSION (code $VERSION_CODE)." >&2
|
||||||
echo " Write fastlane/metadata/android/en-US/changelogs/$VERSION_CODE.txt" >&2
|
echo " Write fastlane/metadata/android/en-US/changelogs/$VERSION_CODE.txt" >&2
|
||||||
echo " before releasing — see docs/RELEASING.md step 3." >&2
|
echo " before releasing — see docs/RELEASING.md step 3." >&2
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ fail the upload after the release has shipped everywhere else.
|
|||||||
|
|
||||||
- every locale directory is listed in fastlane/store-locales.txt, and every
|
- every locale directory is listed in fastlane/store-locales.txt, and every
|
||||||
app language (res/values-*) has an entry there and a directory
|
app language (res/values-*) has an entry there and a directory
|
||||||
|
- every store code is a language Play accepts (supply's ALL_LANGUAGES)
|
||||||
- title / short / full description exist, are non-empty and within Play's caps,
|
- title / short / full description exist, are non-empty and within Play's caps,
|
||||||
and the full description isn't hard-wrapped
|
and the full description isn't hard-wrapped
|
||||||
- icon: 512x512 32-bit PNG; featureGraphic: 1024x500 JPEG or 24-bit PNG
|
- icon: 512x512 32-bit PNG; featureGraphic: 1024x500 JPEG or 24-bit PNG
|
||||||
@@ -29,6 +30,16 @@ SCREENSHOT_DIRS = {"phoneScreenshots", "sevenInchScreenshots", "tenInchScreensho
|
|||||||
"tvScreenshots", "wearScreenshots"}
|
"tvScreenshots", "wearScreenshots"}
|
||||||
IMAGE_EXTS = {".png", ".jpg", ".jpeg"}
|
IMAGE_EXTS = {".png", ".jpg", ".jpeg"}
|
||||||
|
|
||||||
|
# fastlane supply/lib/supply/languages.rb, with `_` back to `-`.
|
||||||
|
PLAY_LOCALES = set("""
|
||||||
|
af am ar az-AZ be bg bn-BD ca cs-CZ da-DK de-DE el-GR en-AU en-CA en-GB en-IN
|
||||||
|
en-SG en-US en-ZA es-419 es-ES es-US et eu-ES fa fi-FI fil fr-CA fr-FR gl-ES
|
||||||
|
hi-IN hr hu-HU hy-AM id is-IS it-IT iw-IL ja-JP ka-GE km-KH kn-IN ko-KR ky-KG
|
||||||
|
lo-LA lt lv mk-MK ml-IN mn-MN mr-IN ms ms-MY my-MM ne-NP nl-NL no-NO pl-PL
|
||||||
|
pt-BR pt-PT rm ro ru-RU si-LK sk sl sr sv-SE sw ta-IN te-IN th tr-TR uk vi
|
||||||
|
zh-CN zh-HK zh-TW zu
|
||||||
|
""".split())
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
|
|
||||||
@@ -80,6 +91,10 @@ def check_mapping():
|
|||||||
for q in sorted(mapping.keys() - app_langs):
|
for q in sorted(mapping.keys() - app_langs):
|
||||||
error(f"{rel(MAPPING)} maps '{q}' but the app has no values-{q}")
|
error(f"{rel(MAPPING)} maps '{q}' but the app has no values-{q}")
|
||||||
|
|
||||||
|
for q, store in sorted(mapping.items()):
|
||||||
|
if store not in PLAY_LOCALES:
|
||||||
|
error(f"{rel(MAPPING)} maps '{q}' to '{store}', which Play doesn't accept as a listing language")
|
||||||
|
|
||||||
stores = set(mapping.values())
|
stores = set(mapping.values())
|
||||||
dirs = {d.name for d in META.iterdir() if d.is_dir()}
|
dirs = {d.name for d in META.iterdir() if d.is_dir()}
|
||||||
for d in sorted(dirs - stores):
|
for d in sorted(dirs - stores):
|
||||||
|
|||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/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="calendula_${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."
|
||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Upserts the Gitea release for TAG (creating the tag at SHA) and attaches the
|
||||||
|
# R8 mapping, best-effort.
|
||||||
|
#
|
||||||
|
# Env: TOKEN, API (.../api/v1/repos/<owner>/<repo>), TAG, SHA, PRERELEASE,
|
||||||
|
# NOTES_FILE, MAPPING (optional)
|
||||||
|
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
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
attach_mapping() {
|
||||||
|
local asset="mapping-${TAG#v}.txt.gz" old
|
||||||
|
gzip -c "$MAPPING" > "$WORK/$asset"
|
||||||
|
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
|
||||||
Executable
+59
@@ -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/calendula}
|
||||||
|
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="calendula_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
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Prints the release notes for VERSION from CHANGELOG.md. A beta without its own
|
||||||
|
# section falls back to [Unreleased], under a line saying what a beta is.
|
||||||
|
#
|
||||||
|
# scripts/release_notes.sh 2.22.4 > 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
|
||||||
@@ -19,11 +19,15 @@ cd "$(dirname "$0")/.." # repo root
|
|||||||
|
|
||||||
LIMIT=500
|
LIMIT=500
|
||||||
|
|
||||||
VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
|
VERSION=$(bash scripts/version_info.sh version)
|
||||||
[ -n "$VERSION" ] || { echo "No versionName in app/build.gradle.kts" >&2; exit 1; }
|
VERSION_CODE=$(bash scripts/version_info.sh version_code)
|
||||||
MAJOR=${VERSION%%.*}; rest=${VERSION#*.}; MINOR=${rest%%.*}; PATCH=${rest##*.}
|
|
||||||
MAJOR=${MAJOR:-0}; MINOR=${MINOR:-0}; PATCH=${PATCH:-0}
|
# Betas only ship to Codeberg, whose notes come from CHANGELOG.md. A What's New
|
||||||
VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
# file for one would sit in the tree F-Droid and Play read, so none is wanted.
|
||||||
|
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
|
||||||
|
|
||||||
CL_DIR="fastlane/metadata/android/en-US/changelogs"
|
CL_DIR="fastlane/metadata/android/en-US/changelogs"
|
||||||
mkdir -p "$CL_DIR"
|
mkdir -p "$CL_DIR"
|
||||||
|
|||||||
Executable
+78
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Derives versionCode, channel and pre-release flag from the committed versionName.
|
||||||
|
#
|
||||||
|
# X.Y.Z (<= 2.22.3) 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 (>= 2.22.4) stable X*1000000 + Y*10000 + Z*100 + 99
|
||||||
|
#
|
||||||
|
# scripts/version_info.sh all values as key=value lines
|
||||||
|
# scripts/version_info.sh <key> one value
|
||||||
|
# scripts/version_info.sh --check fail unless the committed versionCode matches
|
||||||
|
# scripts/version_info.sh --pin rewrite the committed versionCode to the derived one
|
||||||
|
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 22204 ]; then
|
||||||
|
echo "versionName '$NAME': betas start at 2.22.4; legacy 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 22204 ]; then
|
||||||
|
CHANNEL=stable
|
||||||
|
CODE=$LEGACY
|
||||||
|
else
|
||||||
|
CHANNEL=stable
|
||||||
|
CODE=$(( MAJOR * 1000000 + MINOR * 10000 + PATCH * 100 + 99 ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$CHANNEL" = beta ]; 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)."
|
||||||
|
;;
|
||||||
|
--pin)
|
||||||
|
sed -i "s/versionCode = .*/versionCode = $CODE/" "$GRADLE"
|
||||||
|
grep -E 'versionName|versionCode' "$GRADLE"
|
||||||
|
;;
|
||||||
|
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
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Writes the app signing keystore and key.properties from CI secrets.
|
||||||
|
#
|
||||||
|
# Env: KEYSTORE_BASE64, KEY_PASSWORD, KEY_ALIAS
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.." # repo root
|
||||||
|
: "${KEYSTORE_BASE64:?}" "${KEY_PASSWORD:?}" "${KEY_ALIAS:?}"
|
||||||
|
|
||||||
|
mkdir -p app
|
||||||
|
echo "$KEYSTORE_BASE64" | base64 --decode > app/upload-keystore.jks
|
||||||
|
cat > key.properties <<PROPS
|
||||||
|
storePassword=$KEY_PASSWORD
|
||||||
|
keyPassword=$KEY_PASSWORD
|
||||||
|
keyAlias=$KEY_ALIAS
|
||||||
|
storeFile=upload-keystore.jks
|
||||||
|
PROPS
|
||||||
Reference in New Issue
Block a user