ci: adopt the modern calendula pipeline + Codeberg mirror
Port Calendula's current CI/release pipeline: - ci.yaml: pull_request-triggered, change-scope classification (docs/metadata-only PRs skip the Android build but still report a green CI), and a reproducible-release invariant guard. - release.yaml: the committed versionName is the source of truth — a bump reaching main triggers the release, which builds, signs, publishes to the F-Droid repo, then mints the vX.Y.Z tag + Gitea release and mirrors it to Codeberg with the signed APK + SHA-256 checksum. workflow_dispatch runs the re-sign-only recovery path. - Gitea releases are flagged as pre-releases while MAJOR is 0. - build.gradle.kts: reproducible-release invariants (vcsInfo, dependenciesInfo) + a releaseTest variant for the on-device gate. - fastlane/ becomes the single source of truth for store metadata; the localized F-Droid layout is generated from it at release time. - Port scripts/, .gitea/ISSUE_TEMPLATE/, and rewrite docs/RELEASING.md for the versionName-in-main model; fix stale references elsewhere. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
71
scripts/check_reproducible_release.sh
Executable file
71
scripts/check_reproducible_release.sh
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproducibility guard for the official F-Droid repo (de.jeanlucmakiola.agendula).
|
||||
#
|
||||
# F-Droid only republishes OUR signed binary if a from-source build reproduces it
|
||||
# byte-for-byte and the binary carries no extra signing blocks. If any invariant
|
||||
# below regresses, the official repo silently stalls on the last good version
|
||||
# (fails safe — but you'd be stuck on an old release without noticing). So fail
|
||||
# loudly here, on every PR.
|
||||
#
|
||||
# Each invariant guards against a known fdroiddata CI rejection cause (learned on
|
||||
# the sibling Calendula repo's official-repo submission):
|
||||
# 1. vcsInfo { include = false } — else AGP embeds env-dependent git
|
||||
# metadata (META-INF/version-control-info.textproto) -> not reproducible.
|
||||
# 2. no foojay toolchain resolver — F-Droid's offline source scanner
|
||||
# rejects org.gradle.toolchains.foojay-resolver (it can fetch a JDK over
|
||||
# the network at build time).
|
||||
# 3. dependenciesInfo { includeInApk = false } — else AGP embeds a "Dependency
|
||||
# metadata" block (id 0x504b4453) in the APK Signing Block, which F-Droid's
|
||||
# binary scanner rejects as an extra signing block.
|
||||
set -euo pipefail
|
||||
|
||||
APP="app/build.gradle.kts"
|
||||
SETTINGS="settings.gradle.kts"
|
||||
fail=0
|
||||
|
||||
# 1. AGP VCS-info must be disabled on the release build. -z reads the whole file
|
||||
# as one record so the match can span newlines; [^}] keeps it inside the block.
|
||||
if grep -Pzoq 'vcsInfo\s*\{[^}]*include\s*=\s*false' "$APP"; then
|
||||
echo "OK: vcsInfo { include = false } — no env-dependent VCS metadata embedded."
|
||||
else
|
||||
echo "ERROR: '$APP' release build is missing 'vcsInfo { include = false }'." >&2
|
||||
echo " AGP would embed version-control-info.textproto, breaking reproducibility." >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# 2. The foojay toolchain resolver must not be present in any Gradle script.
|
||||
# This includes the floret-kit submodule: it's an included build (composite
|
||||
# build via `includeBuild`), so F-Droid evaluates its Gradle scripts too when
|
||||
# building from source — the same offline-scanner bar applies to it.
|
||||
gradle_files=("$SETTINGS" "$APP")
|
||||
[ -f build.gradle.kts ] && gradle_files+=(build.gradle.kts)
|
||||
if [ -d floret-kit ]; then
|
||||
while IFS= read -r f; do gradle_files+=("$f"); done \
|
||||
< <(find floret-kit -name '*.gradle.kts' -not -path '*/build/*')
|
||||
fi
|
||||
if grep -qi 'foojay' "${gradle_files[@]}"; then
|
||||
echo "ERROR: foojay toolchain resolver found in: $(grep -li foojay "${gradle_files[@]}" | tr '\n' ' ')" >&2
|
||||
echo " F-Droid's source scanner rejects org.gradle.toolchains.foojay-resolver" >&2
|
||||
echo " (it can fetch a JDK over the network). Remove the plugin." >&2
|
||||
fail=1
|
||||
else
|
||||
echo "OK: no foojay toolchain resolver — offline build scanner stays happy."
|
||||
fi
|
||||
|
||||
# 3. AGP dependency-metadata block must not be embedded in the APK.
|
||||
if grep -Pzoq 'dependenciesInfo\s*\{[^}]*includeInApk\s*=\s*false' "$APP"; then
|
||||
echo "OK: dependenciesInfo { includeInApk = false } — no extra APK signing block."
|
||||
else
|
||||
echo "ERROR: '$APP' is missing 'dependenciesInfo { includeInApk = false }'." >&2
|
||||
echo " AGP would embed a 'Dependency metadata' block (0x504b4453) in the APK" >&2
|
||||
echo " Signing Block, which F-Droid's binary scanner rejects." >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo >&2
|
||||
echo "Reproducible-release invariant(s) violated — official F-Droid publishing would" >&2
|
||||
echo "stall. Fix the above before merging." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "All reproducible-release invariants hold."
|
||||
48
scripts/fastlane_to_fdroid_localized.sh
Executable file
48
scripts/fastlane_to_fdroid_localized.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Single source of truth: fastlane/metadata/android/<locale>/ feeds BOTH the
|
||||
# official F-Droid repo (harvested from source automatically) and the
|
||||
# self-hosted repo. This script transforms the fastlane layout into the F-Droid
|
||||
# "localized" layout that the self-hosted `fdroid update` consumes, so we don't
|
||||
# maintain two copies.
|
||||
#
|
||||
# usage: fastlane_to_fdroid_localized.sh <fastlane_android_dir> <out_localized_dir>
|
||||
# e.g. scripts/fastlane_to_fdroid_localized.sh \
|
||||
# fastlane/metadata/android \
|
||||
# fdroid/metadata/de.jeanlucmakiola.agendula
|
||||
#
|
||||
# Mapping (fastlane -> F-Droid repo localized):
|
||||
# short_description.txt -> summary.txt
|
||||
# full_description.txt -> description.txt
|
||||
# title.txt -> name.txt
|
||||
# images/icon.png -> icon.png
|
||||
# images/phoneScreenshots/* -> phoneScreenshots/*
|
||||
# changelogs/<versionCode>.txt -> changelogs/<versionCode>.txt
|
||||
# (changelogs are seeded into the fastlane tree by
|
||||
# scripts/sync_changelog_to_fastlane.sh.)
|
||||
set -euo pipefail
|
||||
|
||||
SRC="${1:?need fastlane android dir, e.g. fastlane/metadata/android}"
|
||||
OUT="${2:?need output localized dir, e.g. fdroid/metadata/<appid>}"
|
||||
|
||||
shopt -s nullglob
|
||||
for locdir in "$SRC"/*/; do
|
||||
loc="$(basename "$locdir")"
|
||||
dst="$OUT/$loc"
|
||||
mkdir -p "$dst"
|
||||
[ -f "$locdir/short_description.txt" ] && cp "$locdir/short_description.txt" "$dst/summary.txt"
|
||||
[ -f "$locdir/full_description.txt" ] && cp "$locdir/full_description.txt" "$dst/description.txt"
|
||||
[ -f "$locdir/title.txt" ] && cp "$locdir/title.txt" "$dst/name.txt"
|
||||
[ -f "$locdir/images/icon.png" ] && cp "$locdir/images/icon.png" "$dst/icon.png"
|
||||
if [ -d "$locdir/images/phoneScreenshots" ]; then
|
||||
mkdir -p "$dst/phoneScreenshots"
|
||||
cp "$locdir"images/phoneScreenshots/* "$dst/phoneScreenshots/"
|
||||
fi
|
||||
# Per-version changelogs live in the same fastlane tree (see
|
||||
# scripts/sync_changelog_to_fastlane.sh) and map straight across.
|
||||
if [ -d "$locdir/changelogs" ]; then
|
||||
mkdir -p "$dst/changelogs"
|
||||
cp "$locdir"changelogs/* "$dst/changelogs/"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Built F-Droid localized metadata in '$OUT' from '$SRC'"
|
||||
41
scripts/sync_changelog_to_fastlane.sh
Executable file
41
scripts/sync_changelog_to_fastlane.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# Write the current version's CHANGELOG.md section into the fastlane changelog
|
||||
# file that F-Droid harvests: fastlane/metadata/android/en-US/changelogs/<code>.txt
|
||||
# (en-US is F-Droid's fallback locale, so it covers every language).
|
||||
#
|
||||
# Run this when cutting a release (after editing CHANGELOG.md and bumping
|
||||
# versionName in app/build.gradle.kts) and COMMIT the result, so the OFFICIAL
|
||||
# F-Droid repo — which reads the changelog from the tagged source tree — shows
|
||||
# this version's "What's New". The self-hosted release pipeline also runs it so
|
||||
# its changelog never depends on the file having been committed. Idempotent.
|
||||
#
|
||||
# Extraction matches the awk used for the Gitea release notes so all three
|
||||
# (release notes, self-hosted changelog, official changelog) stay in sync.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.." # repo root
|
||||
|
||||
VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
|
||||
[ -n "$VERSION" ] || { echo "No versionName in app/build.gradle.kts" >&2; exit 1; }
|
||||
MAJOR=${VERSION%%.*}; rest=${VERSION#*.}; MINOR=${rest%%.*}; PATCH=${rest##*.}
|
||||
MAJOR=${MAJOR:-0}; MINOR=${MINOR:-0}; PATCH=${PATCH:-0}
|
||||
VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
||||
|
||||
CL_DIR="fastlane/metadata/android/en-US/changelogs"
|
||||
mkdir -p "$CL_DIR"
|
||||
OUT="$CL_DIR/${VERSION_CODE}.txt"
|
||||
|
||||
awk -v ver="$VERSION" '
|
||||
$0 ~ "^## \\[" ver "\\]" { flag = 1; next }
|
||||
/^## \[/ { flag = 0 }
|
||||
flag' CHANGELOG.md > "$OUT"
|
||||
# Trim leading blank lines (same as the pipeline did).
|
||||
sed -i -e '/./,$!d' "$OUT"
|
||||
if [ ! -s "$OUT" ]; then
|
||||
echo "See CHANGELOG.md for $VERSION." > "$OUT"
|
||||
fi
|
||||
|
||||
CHARS=$(wc -m < "$OUT" | tr -d ' ')
|
||||
echo "Wrote $OUT (version $VERSION, code $VERSION_CODE, ${CHARS} chars)"
|
||||
if [ "$CHARS" -gt 500 ]; then
|
||||
echo " note: >500 chars — F-Droid may truncate this changelog in-client." >&2
|
||||
fi
|
||||
49
scripts/verify-release.sh
Executable file
49
scripts/verify-release.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Build the release-candidate APK and install it on a connected device for the
|
||||
# mandatory pre-release on-device check (see docs/RELEASING.md).
|
||||
#
|
||||
# It builds the `releaseTest` variant: the same R8 shrinking + obfuscation and
|
||||
# resource shrinking as the published `release` build, but debug-signed and
|
||||
# with a `.releasetest` applicationId suffix so it installs alongside the
|
||||
# production and debug apps. This is what surfaces release-only breakage (R8
|
||||
# stripping) and first-run states (permission not yet granted) that the
|
||||
# unminified debug build — or a device that already holds the permission —
|
||||
# silently hides.
|
||||
#
|
||||
# Usage: scripts/verify-release.sh
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
PKG="de.jeanlucmakiola.agendula.releasetest"
|
||||
APK="app/build/outputs/apk/releaseTest/app-releaseTest.apk"
|
||||
|
||||
echo "==> Building release-candidate APK (releaseTest, R8 minified)…"
|
||||
./gradlew :app:assembleReleaseTest
|
||||
|
||||
echo "==> Installing $PKG …"
|
||||
adb install -r "$APK"
|
||||
|
||||
echo "==> Resetting to a first-run state (revoking tasks + notification permissions)…"
|
||||
# Force the permission-not-granted state so the permission gate / onboarding is
|
||||
# exercised every time — R8-only breakage and first-run crashes never show up in
|
||||
# the unminified debug build, nor on a device that already holds the permission.
|
||||
# Both tasks-provider permission sets are declared; revoke each so whichever the
|
||||
# device's provider uses starts ungranted.
|
||||
adb shell pm revoke "$PKG" org.dmfs.permission.READ_TASKS 2>/dev/null || true
|
||||
adb shell pm revoke "$PKG" org.dmfs.permission.WRITE_TASKS 2>/dev/null || true
|
||||
adb shell pm revoke "$PKG" org.tasks.permission.READ_TASKS 2>/dev/null || true
|
||||
adb shell pm revoke "$PKG" org.tasks.permission.WRITE_TASKS 2>/dev/null || true
|
||||
adb shell pm revoke "$PKG" android.permission.POST_NOTIFICATIONS 2>/dev/null || true
|
||||
|
||||
echo
|
||||
echo "Installed and reset. Now verify ON THE DEVICE before releasing:"
|
||||
echo " 1. Launch from a clean state — the permission screen must appear (no crash)."
|
||||
echo " 2. Grant tasks access — the task list must load."
|
||||
echo " 3. Create a task with a due reminder and confirm the notification fires."
|
||||
echo " 4. Exercise the release's headline changes end to end."
|
||||
echo
|
||||
echo "Watch for crashes with: adb logcat -b crash"
|
||||
echo "Only merge the release branch to main once all of the above pass on a device"
|
||||
echo "(the merge is what publishes the release — see docs/RELEASING.md)."
|
||||
Reference in New Issue
Block a user