31ad650329
android / Build + lint + test (push) Successful in 4m54s
Previous design: android.yml's release job and release.yml ran in parallel on every tag push. release.yml polled the gitea release- download URL for up to 15 min waiting for the APK to appear. In practice the polling step was completing in 11s — either following a gitea redirect to a 200 page and writing HTML into the bundled APK file, or returning empty content silently. Either way the resulting image shipped without a working APK and the web Settings page's in-app-update section never rendered. Fix the race architecturally: - Move the signed-release-build + Attach-APK steps out of android.yml and into a new android-release job in release.yml. - release.yml's image-release job declares needs: [android-release], so on tag pushes the image cannot start building until the APK is guaranteed-attached. - Pass the APK between jobs via actions/upload-artifact@v3 + download-artifact@v3 (the v2 backend isn't supported on Gitea). This removes the polling loop entirely — the image-release job just downloads the artifact, renames to minstrel.apk, writes the .version sidecar, and continues to docker buildx. - For main pushes android-release is skipped via its if: condition. image-release uses so it still runs (the skipped predecessor doesn't poison the chain) and the download/stage steps gate themselves on the tag context. Main images ship without an APK by design, same as before. android.yml is now testing-only: lint + detekt + unit tests on every push, debug APK artifact on main. Independent of release CI as requested.
223 lines
8.5 KiB
YAML
223 lines
8.5 KiB
YAML
name: release
|
|
|
|
# Builds and pushes the minstrel container image to the Gitea registry.
|
|
#
|
|
# push to main → :main and :latest (no APK bundled)
|
|
# push tag vYYYY.MM.DD → :vYYYY.MM.DD and :latest (APK bundled)
|
|
# workflow_dispatch → manual trigger (same rules based on the ref)
|
|
#
|
|
# Release model: per-day CalVer tags (no trailing patch digit). The day's
|
|
# tag is intentionally mutable — if a second release happens the same day,
|
|
# move the tag with `git push -f origin vYYYY.MM.DD` and the image tag of
|
|
# the same name gets overwritten. :latest is updated by every main push
|
|
# AND every tag push, so it always reflects the newest blessed image.
|
|
#
|
|
# APK pipeline: on tag pushes the android-release job builds + signs the
|
|
# Android APK and uploads it as a workflow artifact. The image-release
|
|
# job declares `needs: android-release`, so the docker image cannot
|
|
# start building until the APK is guaranteed-ready — no polling, no
|
|
# race, no silent-failure mode. Asset attachment to the gitea Release
|
|
# happens in the same android-release job, so the Release-page download
|
|
# link and the in-image bundled APK are both populated atomically.
|
|
#
|
|
# Android testing (lint + detekt + unit tests, debug APK upload on main)
|
|
# lives in android.yml and runs independently on every push.
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
tags: ['v*']
|
|
paths-ignore:
|
|
- 'docs/**'
|
|
- '**/*.md'
|
|
workflow_dispatch:
|
|
|
|
# Force-moving the per-day tag (or rapidly re-pushing to main) should
|
|
# supersede the in-flight build — the operator explicitly wants the
|
|
# later commit to win.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
android-release:
|
|
name: Build signed APK (tag releases only)
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
runs-on: flutter-ci
|
|
container:
|
|
image: git.fabledsword.com/bvandeusen/ci-android:36
|
|
|
|
defaults:
|
|
run:
|
|
working-directory: android
|
|
|
|
env:
|
|
JAVA_TOOL_OPTIONS: "--enable-native-access=ALL-UNNAMED"
|
|
# PKCS12 keystores collapse store + key password into a single
|
|
# value; both env vars map to one secret. build.gradle reads them
|
|
# separately to stay format-agnostic.
|
|
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Cache Gradle dirs
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
~/.kotlin
|
|
key: gradle-${{ runner.os }}-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/gradle/libs.versions.toml', 'android/**/*.gradle.kts') }}
|
|
restore-keys: |
|
|
gradle-${{ runner.os }}-
|
|
|
|
- name: Make gradlew executable
|
|
run: chmod +x ./gradlew
|
|
|
|
- name: Decode signing keystore
|
|
env:
|
|
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
|
|
shell: bash
|
|
run: |
|
|
if [ -z "${ANDROID_KEYSTORE_B64}" ]; then
|
|
echo "::error::ANDROID_KEYSTORE_B64 missing"; exit 1
|
|
fi
|
|
KEYSTORE_PATH="${RUNNER_TEMP}/minstrel-release.keystore"
|
|
echo "${ANDROID_KEYSTORE_B64}" | base64 -d > "${KEYSTORE_PATH}"
|
|
echo "ANDROID_KEYSTORE_PATH=${KEYSTORE_PATH}" >> "${GITHUB_ENV}"
|
|
|
|
- name: Build release APK
|
|
run: ./gradlew assembleRelease
|
|
|
|
- name: Upload APK as workflow artifact
|
|
# @v3 because Gitea Actions emulates GHES and the v2 artifact
|
|
# backend used by upload-artifact@v4 errors with GHESNotSupportedError.
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: minstrel-apk
|
|
path: android/app/build/outputs/apk/release/app-release.apk
|
|
|
|
- name: Attach APK to gitea Release
|
|
shell: bash
|
|
env:
|
|
CI_TOKEN: ${{ secrets.CI_TOKEN }}
|
|
run: |
|
|
set -euxo pipefail
|
|
TAG="${GITHUB_REF#refs/tags/}"
|
|
REPO="${GITHUB_REPOSITORY}"
|
|
APK_PATH="app/build/outputs/apk/release/app-release.apk"
|
|
ls -lh "${APK_PATH}"
|
|
|
|
RELEASE_JSON="$(curl -fsSL \
|
|
-H "Authorization: token ${CI_TOKEN}" \
|
|
"https://git.fabledsword.com/api/v1/repos/${REPO}/releases/tags/${TAG}")"
|
|
RELEASE_ID="$(printf '%s' "${RELEASE_JSON}" | grep -oP '"id":\s*\K[0-9]+' | head -1)"
|
|
if [ -z "${RELEASE_ID}" ]; then
|
|
echo "::error::release for ${TAG} not found"; exit 1
|
|
fi
|
|
echo "release_id=${RELEASE_ID}"
|
|
|
|
UPLOAD_HTTP=$(curl -sS -L -o /tmp/upload.out -w '%{http_code}' \
|
|
-H "Authorization: token ${CI_TOKEN}" \
|
|
-F "attachment=@${APK_PATH}" \
|
|
"https://git.fabledsword.com/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=minstrel-${TAG}.apk")
|
|
echo "upload_http=${UPLOAD_HTTP}"
|
|
cat /tmp/upload.out || true
|
|
echo
|
|
if [ "${UPLOAD_HTTP}" -lt 200 ] || [ "${UPLOAD_HTTP}" -ge 300 ]; then
|
|
echo "::error::APK upload returned HTTP ${UPLOAD_HTTP}"
|
|
exit 1
|
|
fi
|
|
|
|
image-release:
|
|
name: Build + push container image
|
|
# `needs:` waits for android-release. For tag pushes android-release
|
|
# runs and must succeed before this job starts — guaranteeing the
|
|
# APK artifact is present. For main pushes android-release is
|
|
# skipped; the `if: ...` below lets this job run anyway and the
|
|
# download/copy steps gate themselves on the tag context.
|
|
needs: [android-release]
|
|
if: ${{ !failure() && !cancelled() }}
|
|
runs-on: go-ci
|
|
container:
|
|
image: git.fabledsword.com/bvandeusen/ci-go:1.26
|
|
|
|
env:
|
|
IMAGE: git.fabledsword.com/bvandeusen/minstrel
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Detect buildable project
|
|
id: guard
|
|
shell: bash
|
|
run: |
|
|
if [ -f Dockerfile ] && [ -f go.mod ]; then
|
|
echo "ready=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "ready=false" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::No Dockerfile + go.mod yet — release build skipped"
|
|
fi
|
|
|
|
- name: Compute image tags
|
|
id: tags
|
|
if: steps.guard.outputs.ready == 'true'
|
|
shell: bash
|
|
run: |
|
|
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
|
VERSION="${GITHUB_REF#refs/tags/}"
|
|
echo "args=-t ${IMAGE}:${VERSION} -t ${IMAGE}:latest" >> "$GITHUB_OUTPUT"
|
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::Release build: ${VERSION} + latest"
|
|
else
|
|
# Main is the protected, post-PR-merge branch. Treat it as the
|
|
# rolling stable channel — every main push moves :latest.
|
|
# Pinned consumers can target :vYYYY.MM.DD; everyone else
|
|
# gets the newest main.
|
|
echo "args=-t ${IMAGE}:main -t ${IMAGE}:latest" >> "$GITHUB_OUTPUT"
|
|
echo "version=main" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::Main-branch build: :main + :latest"
|
|
fi
|
|
|
|
- name: Registry login
|
|
if: steps.guard.outputs.ready == 'true'
|
|
shell: bash
|
|
run: |
|
|
echo "${{ secrets.CI_TOKEN }}" \
|
|
| docker login git.fabledsword.com -u "${{ github.actor }}" --password-stdin
|
|
|
|
- name: Download signed APK artifact
|
|
# Tag pushes only — android-release just produced this. Main
|
|
# pushes skip and the image ships with empty client/ (the
|
|
# /api/client/version endpoint then returns 404 by design).
|
|
if: steps.guard.outputs.ready == 'true' && startsWith(github.ref, 'refs/tags/v')
|
|
uses: actions/download-artifact@v3
|
|
with:
|
|
name: minstrel-apk
|
|
path: client/
|
|
|
|
- name: Stage bundled APK + version sidecar
|
|
if: steps.guard.outputs.ready == 'true' && startsWith(github.ref, 'refs/tags/v')
|
|
shell: bash
|
|
run: |
|
|
set -euxo pipefail
|
|
TAG="${GITHUB_REF#refs/tags/}"
|
|
# The artifact lands as `app-release.apk` (the original Gradle
|
|
# output name). The Dockerfile COPYs client/* into /app/client/
|
|
# and the server reads minstrel.apk + minstrel.apk.version.
|
|
mv client/app-release.apk client/minstrel.apk
|
|
echo "${TAG}" > client/minstrel.apk.version
|
|
ls -lh client/
|
|
|
|
- name: Build and push
|
|
if: steps.guard.outputs.ready == 'true'
|
|
run: |
|
|
docker buildx build \
|
|
--build-arg MINSTREL_VERSION="${{ steps.tags.outputs.version }}" \
|
|
--push ${{ steps.tags.outputs.args }} .
|