Merge pull request #1119 from torlando-tech/fix/1079-review-pass #182
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Tagged Release/Prerelease | |
| on: | |
| push: | |
| tags: | |
| - 'v*' # Trigger on version tags (e.g., v1.0.0, v1.2.3-beta1) | |
| jobs: | |
| build-and-release: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # Required to create releases and upload assets | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 # Fetch all history for version tagging | |
| - name: Set up Python | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.11' | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v6.3.0 | |
| with: | |
| cache-read-only: false | |
| - name: Extract version from tag | |
| id: version | |
| run: | | |
| TAG_NAME=${GITHUB_REF#refs/tags/} | |
| VERSION=${TAG_NAME#v} | |
| echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| # Base URL for release asset downloads. GitHub serves every asset at a | |
| # predictable path, so we can link directly to APKs from the body | |
| # instead of making people expand the Assets accordion. Note: these | |
| # URLs 404 while the release is a draft (the tag isn't public yet) and | |
| # start resolving the moment it's published. | |
| echo "dl=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/download/${TAG_NAME}" >> $GITHUB_OUTPUT | |
| echo "Building version: $VERSION from tag: $TAG_NAME" | |
| - name: Build Release APK | |
| # 40m to match build-prerelease-apk.yml: assembleRelease builds the two | |
| # pythonBackend flavors, which bundle a Python runtime per ABI via | |
| # Chaquopy and overrun the old 20m budget. | |
| timeout-minutes: 40 | |
| env: | |
| KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} | |
| KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} | |
| KEY_ALIAS: ${{ secrets.KEY_ALIAS }} | |
| KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ secrets.SENTRY_ORG }} | |
| run: | | |
| # Python backend first, then Kotlin backend; each in sentry / noSentry | |
| # telemetry variants. Four flavor combos × ABI splits + universal. | |
| ./gradlew \ | |
| :app:assembleSentryPythonBackendRelease \ | |
| :app:assembleNoSentryPythonBackendRelease \ | |
| :app:assembleSentryKotlinBackendRelease \ | |
| :app:assembleNoSentryKotlinBackendRelease \ | |
| --no-daemon --stacktrace | |
| echo "✓ Release APKs built successfully" | |
| - name: Build Release AAB (Play Store) | |
| # Reuses compiled classes/resources from the assemble step above, so | |
| # this is fast (~1m) despite the multi-flavor project. Only the | |
| # pythonBackend flavor is built: it's the appId (network.columba.app) | |
| # registered with the Play Console listing — kotlinBackend (`.kt` | |
| # suffix) isn't published there. | |
| timeout-minutes: 15 | |
| env: | |
| KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} | |
| KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} | |
| KEY_ALIAS: ${{ secrets.KEY_ALIAS }} | |
| KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ secrets.SENTRY_ORG }} | |
| run: | | |
| ./gradlew :app:bundleSentryPythonBackendRelease --no-daemon --stacktrace | |
| echo "✓ Release AAB built successfully" | |
| - name: Extract signing certificate fingerprints | |
| id: cert | |
| env: | |
| KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} | |
| KEY_ALIAS: ${{ secrets.KEY_ALIAS }} | |
| run: | | |
| CERT_INFO=$(keytool -list -v -keystore app/build/keystore/release.keystore \ | |
| -alias "$KEY_ALIAS" -storepass "$KEYSTORE_PASSWORD") | |
| SHA256=$(echo "$CERT_INFO" | grep "SHA256:" | awk '{print $2}') | |
| SHA1=$(echo "$CERT_INFO" | grep "SHA1:" | awk '{print $2}') | |
| if [ -z "$SHA256" ] || [ -z "$SHA1" ]; then | |
| echo "::error::Failed to extract certificate fingerprints" | |
| exit 1 | |
| fi | |
| echo "sha256=$SHA256" >> $GITHUB_OUTPUT | |
| echo "sha1=$SHA1" >> $GITHUB_OUTPUT | |
| echo "✓ Extracted certificate fingerprints" | |
| - name: Rename APKs with version and architecture | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| # Two flavor dimensions (telemetry × rnsImpl) mean Gradle writes to | |
| # app/build/outputs/apk/<telemetry><Backend>/release/, e.g. | |
| # sentryPythonBackend/release/. Output filenames: | |
| # columba-{version}-{backend}-{abi}[-no-sentry].apk | |
| # Backend is python first, then kotlin. | |
| APK_DIR="app/build/outputs/apk" | |
| # "<dir-infix>:<filename-token>" | |
| for backend in "Python:official-rns-py" "Kotlin:EXPERIMENTAL-reticulum-kt"; do | |
| dir_backend="${backend%%:*}" | |
| name_backend="${backend##*:}" | |
| for telemetry in sentry noSentry; do | |
| sentry_suffix="" | |
| [ "$telemetry" = "noSentry" ] && sentry_suffix="-no-sentry" | |
| variant_dir="${telemetry}${dir_backend}Backend" | |
| for apk in "$APK_DIR/$variant_dir/release/"*.apk; do | |
| [ -e "$apk" ] || continue | |
| filename=$(basename "$apk") | |
| if echo "$filename" | grep -q "armeabi-v7a"; then | |
| cp "$apk" "columba-${VERSION}-${name_backend}-armeabi-v7a${sentry_suffix}.apk" | |
| elif echo "$filename" | grep -q "arm64-v8a"; then | |
| cp "$apk" "columba-${VERSION}-${name_backend}-arm64-v8a${sentry_suffix}.apk" | |
| elif echo "$filename" | grep -q "x86_64"; then | |
| cp "$apk" "columba-${VERSION}-${name_backend}-x86_64${sentry_suffix}.apk" | |
| elif echo "$filename" | grep -q "universal"; then | |
| cp "$apk" "columba-${VERSION}-${name_backend}-universal${sentry_suffix}.apk" | |
| fi | |
| done | |
| done | |
| done | |
| ls -la columba-*.apk | |
| echo "✓ Renamed APKs" | |
| - name: Rename AAB with version | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| cp app/build/outputs/bundle/sentryPythonBackendRelease/app-sentry-pythonBackend-release.aab \ | |
| "columba-${VERSION}-official-rns-py.aab" | |
| sha256sum "columba-${VERSION}-official-rns-py.aab" > "columba-${VERSION}-official-rns-py.aab.sha256" | |
| echo "✓ Renamed AAB" | |
| - name: Generate SHA256 checksums | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| for apk in columba-*.apk; do | |
| sha256sum "$apk" > "${apk}.sha256" | |
| done | |
| echo "✓ Generated SHA256 checksums" | |
| - name: Read SHA256 checksums | |
| id: sha256 | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| CHECKSUMS="" | |
| for checksum_file in columba-*.apk.sha256; do | |
| CHECKSUMS="${CHECKSUMS}$(cat "$checksum_file")\n" | |
| done | |
| echo "checksums<<EOF" >> $GITHUB_OUTPUT | |
| echo -e "$CHECKSUMS" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| # When this release bumps reticulum-kt / LXMF-kt / LXST-kt vs the previous | |
| # tag, fetch the commit log between the old and new lib versions and inject | |
| # it into the release notes. Lib repos publish tags but not GitHub Releases, | |
| # so we synthesize from `compare` API output instead. | |
| - name: Build library-changelog injection | |
| id: libchangelog | |
| env: | |
| TAG_NAME: ${{ steps.version.outputs.tag }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # Find the tag that immediately precedes this one in semver order. | |
| PREV_TAG=$(git tag --sort=-v:refname \ | |
| | awk -v t="$TAG_NAME" 'BEGIN{found=0} { if (found) { print; exit } if ($0==t) { found=1 } }') | |
| echo "previous tag: ${PREV_TAG:-(none — first release)}" | |
| extract_pin() { | |
| # Match `<var> = "<version>"` or `<var>="<version>"`. Allow: | |
| # - prereleases like v0.0.16-rc1 | |
| # - a leading-`v` or no-prefix pin (`"0.0.16"`) | |
| # - any whitespace (or none) around `=` | |
| git show "$1:gradle/libs.versions.toml" 2>/dev/null \ | |
| | sed -n -E "s/^[[:space:]]*$2[[:space:]]*=[[:space:]]*\"([^\"]+)\".*/\\1/p" \ | |
| | head -1 | |
| } | |
| NOTES="" | |
| # libdef format: "<libs.versions.toml var>:<github repo>" | |
| for libdef in "reticulumKt:reticulum-kt" "lxmfKt:LXMF-kt" "lxstKt:LXST-kt"; do | |
| var="${libdef%%:*}" | |
| repo="${libdef##*:}" | |
| old="" | |
| if [ -n "$PREV_TAG" ]; then | |
| old=$(extract_pin "$PREV_TAG" "$var" || true) | |
| fi | |
| new=$(extract_pin "$TAG_NAME" "$var" || true) | |
| # Skip if not pinned in this release, or if unchanged. | |
| if [ -z "$new" ]; then | |
| echo "skip $repo: no pin in $TAG_NAME" | |
| continue | |
| fi | |
| if [ "$old" = "$new" ]; then | |
| echo "skip $repo: unchanged at $new" | |
| continue | |
| fi | |
| echo "$repo: ${old:-(unpinned)} -> $new" | |
| NOTES+=$'\n#### `'"$repo"$'` updated: `'"${old:-unpinned}"$'` → `'"$new"$'`\n\n' | |
| if [ -n "$old" ]; then | |
| # Compare link only makes sense when there's a prior version | |
| # to diff against — otherwise we'd render a self-compare | |
| # (empty diff) which is confusing. | |
| NOTES+="See https://github.com/torlando-tech/$repo/compare/$old...$new"$'\n\n' | |
| # Pull commit messages between old and new lib tags. Filter to | |
| # noteworthy commits and drop Conventional-Commit + Sentence- | |
| # Case noise (chore/ci/test/review/revert/build/style/refactor/ | |
| # docs). | |
| # The two-stage filter does: | |
| # ALLOW IF subject matches: | |
| # - feat:/fix:/perf: with optional scope and breaking-! prefix | |
| # - leading "BREAKING" marker | |
| # - sentence-case lead with a curated allowlist of action verbs | |
| # (Add, Fix, Improve, Support, Implement, Update, Enable, | |
| # Allow, Avoid, Prevent, Make, Switch, Move, Drop, Remove, | |
| # Replace, Expose, Wire, Bump, Persist, Cache, Stabilize, | |
| # Optimize) — handles subjects with or without a colon | |
| # AND DENY IF the leading word is a sentence-case form of a | |
| # filtered conventional-commit type (Refactor/Test/Build/ | |
| # Style/Chore/Revert/Ci/Doc(s)?/Review). | |
| # This catches the failure modes from greploop on PR #908: | |
| # 1. `Add resource: dedup duplicate RESOURCE_ADV` no longer | |
| # dropped by the colon-rejecting `[^:]+$` of the prior pass. | |
| # 2. `Refactor LXMRouter dispatch` no longer slipping through | |
| # the sentence-case fallback that the comment claimed | |
| # filtered it. | |
| # Errexit suppression: the `if log=$(...)` test context disables | |
| # `set -e` for the inner command. We log stderr instead of | |
| # swallowing it so failures surface in the Actions log. | |
| if log=$(gh api --paginate "repos/torlando-tech/$repo/compare/$old...$new" \ | |
| --jq '.commits[] | |
| | select(.commit.message | test("^Merge ") | not) | |
| | (.commit.message | split("\n")[0]) as $subject | |
| | select( | |
| ($subject | test("^(feat|fix|perf)(\\([^)]+\\))?!?:|^BREAKING")) | |
| or | |
| ( | |
| ($subject | test("^(Add|Fix|Improve|Support|Implement|Update|Enable|Allow|Avoid|Prevent|Make|Switch|Move|Drop|Remove|Replace|Expose|Wire|Bump|Persist|Cache|Stabilize|Optimize)\\b")) | |
| and | |
| ($subject | test("^(Refactor|Test|Build|Style|Chore|Revert|Ci|Docs?|Review)\\b") | not) | |
| ) | |
| ) | |
| | "- " + $subject + " (`" + (.sha[0:7]) + "`)"'); then | |
| if [ -n "$log" ]; then | |
| NOTES+="$log"$'\n' | |
| else | |
| NOTES+="_(no user-facing feat/fix/perf commits — only chores/tests/reviews; see compare link for the full diff.)_"$'\n' | |
| fi | |
| else | |
| NOTES+="_(failed to fetch compare; see Actions log for the underlying error and the GitHub link above for a manual review)_"$'\n' | |
| fi | |
| else | |
| NOTES+="_First inclusion of this library — no prior version to diff against._"$'\n' | |
| fi | |
| done | |
| # If anything got injected, wrap with a section header. Empty otherwise. | |
| if [ -n "$NOTES" ]; then | |
| FINAL=$'\n### Reticulum / LXMF / LXST updates\n\nThis release picks up the following library updates:'"$NOTES" | |
| else | |
| FINAL="" | |
| fi | |
| # Use a random per-run delimiter so the heredoc can't be closed | |
| # early by a literal line in a fetched commit message. GitHub | |
| # Actions docs recommend this pattern: | |
| # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings | |
| delim="LIBCHANGELOG_$(openssl rand -hex 16)" | |
| { | |
| printf '%s<<%s\n' "notes" "$delim" | |
| printf '%s\n' "$FINAL" | |
| printf '%s\n' "$delim" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| # The .aab isn't linked from the body below (users can't sideload | |
| # it — it's Play Console upload material only) but still rides | |
| # along as a release asset: those don't count against Actions | |
| # storage quota the way workflow artifacts do. | |
| files: | | |
| columba-*.apk | |
| columba-*.apk.sha256 | |
| columba-*.aab | |
| columba-*.aab.sha256 | |
| draft: true | |
| prerelease: ${{ contains(steps.version.outputs.version, '-') }} | |
| generate_release_notes: true | |
| body: | | |
| ## Columba ${{ steps.version.outputs.version }} | |
| ### Installation | |
| Pick a **backend**, then download the APK matching your device's CPU architecture. | |
| - **Python backend, RECOMMENDED** — ships Mark Qvist's official python reference implementations of Reticulum and LXMF via Chaquopy. | |
| - **Kotlin backend** — ships Torlando's EXPERIMENTAL, AI-GENERATED native reticulum-kt / lxmf-kt / lxst-kt stack. | |
| The python version is highly recommended. The kotlin version is a work in progress, and is not yet verified to be completely safe. | |
| The kotlin version may yield better battery life depending on your device, and comes in a smaller apk due to not requiring a python runtime. | |
| #### Python backend (`official-rns-py`) | |
| | APK | Architecture | Devices | | |
| |-----|-------------|---------| | |
| | [`columba-${{ steps.version.outputs.version }}-official-rns-py-armeabi-v7a.apk`](${{ steps.version.outputs.dl }}/columba-${{ steps.version.outputs.version }}-official-rns-py-armeabi-v7a.apk) | armeabi-v7a | Older 32-bit ARM phones & tablets | | |
| | [`columba-${{ steps.version.outputs.version }}-official-rns-py-arm64-v8a.apk`](${{ steps.version.outputs.dl }}/columba-${{ steps.version.outputs.version }}-official-rns-py-arm64-v8a.apk) | arm64-v8a | Most modern Android phones & tablets | | |
| | [`columba-${{ steps.version.outputs.version }}-official-rns-py-x86_64.apk`](${{ steps.version.outputs.dl }}/columba-${{ steps.version.outputs.version }}-official-rns-py-x86_64.apk) | x86_64 | Chromebooks, emulators, some tablets | | |
| | [`columba-${{ steps.version.outputs.version }}-official-rns-py-universal.apk`](${{ steps.version.outputs.dl }}/columba-${{ steps.version.outputs.version }}-official-rns-py-universal.apk) | All | Universal fallback (larger download) | | |
| #### Kotlin backend (`EXPERIMENTAL-reticulum-kt`) | |
| | APK | Architecture | Devices | | |
| |-----|-------------|---------| | |
| | [`columba-${{ steps.version.outputs.version }}-EXPERIMENTAL-reticulum-kt-armeabi-v7a.apk`](${{ steps.version.outputs.dl }}/columba-${{ steps.version.outputs.version }}-EXPERIMENTAL-reticulum-kt-armeabi-v7a.apk) | armeabi-v7a | Older 32-bit ARM phones & tablets | | |
| | [`columba-${{ steps.version.outputs.version }}-EXPERIMENTAL-reticulum-kt-arm64-v8a.apk`](${{ steps.version.outputs.dl }}/columba-${{ steps.version.outputs.version }}-EXPERIMENTAL-reticulum-kt-arm64-v8a.apk) | arm64-v8a | Most modern Android phones & tablets | | |
| | [`columba-${{ steps.version.outputs.version }}-EXPERIMENTAL-reticulum-kt-x86_64.apk`](${{ steps.version.outputs.dl }}/columba-${{ steps.version.outputs.version }}-EXPERIMENTAL-reticulum-kt-x86_64.apk) | x86_64 | Chromebooks, emulators, some tablets | | |
| | [`columba-${{ steps.version.outputs.version }}-EXPERIMENTAL-reticulum-kt-universal.apk`](${{ steps.version.outputs.dl }}/columba-${{ steps.version.outputs.version }}-EXPERIMENTAL-reticulum-kt-universal.apk) | All | Universal fallback (larger download) | | |
| **Telemetry variants:** | |
| Each APK above also has a `-no-sentry` variant without crash reporting for maximum privacy — append `-no-sentry` to any download link above (e.g. the most common builds: [Python arm64-v8a no-sentry](${{ steps.version.outputs.dl }}/columba-${{ steps.version.outputs.version }}-official-rns-py-arm64-v8a-no-sentry.apk) · [Kotlin arm64-v8a no-sentry](${{ steps.version.outputs.dl }}/columba-${{ steps.version.outputs.version }}-EXPERIMENTAL-reticulum-kt-arm64-v8a-no-sentry.apk)). Sentry collects stack traces when Columba encounters errors | |
| and provides helpful information to the developer to improve Columba, but is hosted at sentry.io, and is thus not the maximum privacy option. | |
| > **Not sure which to pick?** Most Android phones use **arm64-v8a**. If unsure, use the **universal** APK. | |
| ### Verification | |
| See [SECURITY.md](https://github.com/torlando-tech/columba/blob/main/SECURITY.md) for verification instructions. | |
| **Signing Certificate Fingerprints:** | |
| ``` | |
| SHA-256: ${{ steps.cert.outputs.sha256 }} | |
| SHA-1: ${{ steps.cert.outputs.sha1 }} | |
| ``` | |
| **SHA256 Checksums:** | |
| ``` | |
| ${{ steps.sha256.outputs.checksums }} | |
| ``` | |
| ${{ steps.libchangelog.outputs.notes }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload APKs as workflow artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: columba-${{ steps.version.outputs.version }}-apks | |
| path: columba-*.apk | |
| # Short retention: these duplicate the Release assets (which don't count | |
| # toward Actions storage). 1 day keeps them handy for a quick download | |
| # (e.g. sideloading to a phone) without piling up against the quota. | |
| retention-days: 1 |