diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 577b5b45..6e89d541 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -98,6 +98,31 @@ jobs: echo "code=${COMMIT_COUNT}" >> "$GITHUB_OUTPUT" echo "::notice::APK version: ${VERSION_NAME} (code=${COMMIT_COUNT})" + # Checked BEFORE the expensive work, not after it. "Attach APK to gitea + # Release" below resolves the release by tag and fails if it is absent — + # but that is the final step, so a tag pushed without a release built an + # APK for several minutes first and only then discovered it had nowhere to + # put it. Same check, seconds in instead of minutes. + # + # Releases are normally created through the API (which creates the tag and + # the release together, so this passes). A bare `git push origin vX` is the + # case this catches. + - name: Release must exist for this tag + shell: bash + working-directory: ${{ github.workspace }} + env: + CI_TOKEN: ${{ secrets.CI_TOKEN }} + run: | + set -euo pipefail + TAG="${GITHUB_REF#refs/tags/}" + if ! curl -fsSL -o /dev/null \ + -H "Authorization: token ${CI_TOKEN}" \ + "https://git.fabledsword.com/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}"; then + echo "::error::no release exists for ${TAG}. Create the release (which creates the tag) rather than pushing a bare tag — otherwise there is nothing to attach the APK to." + exit 1 + fi + echo "::notice::release found for ${TAG}" + - name: Cache Gradle dirs uses: actions/cache@v4 with: @@ -322,3 +347,79 @@ jobs: docker buildx build \ --build-arg MINSTREL_VERSION="${{ steps.tags.outputs.version }}" \ --push ${{ steps.tags.outputs.args }} . + + # Verifies a tag release actually ended up complete, and names the specific + # thing that's missing if not. + # + # Added 2026-08-07 after v2026.08.07 was re-cut. The android-release job never + # started — no log was written at all — so all eight of its steps reported + # `failure` with none executed and image-release showed `skipped`. The run was + # red, but the *release page rendered fine*, and `main`'s own push build had + # already moved `:latest`, so the code was deployable and nothing looked + # obviously wrong. The release was simply missing its APK and its immutable + # `:vYYYY.MM.DD` image, which is easy to skim past. + # + # This job cannot prevent that (the cause was a runner failing to launch, not + # anything in this file). What it does is turn an incomplete release into an + # explicit, named error instead of eight mystery step failures — so the + # consequence is legible without having to infer it. + # + # `if: always()` is the whole point: it has to report precisely when the jobs + # above did NOT succeed. + verify-release: + name: Verify release artifacts (tag releases only) + needs: [android-release, image-release] + if: ${{ always() && startsWith(github.ref, 'refs/tags/v') }} + runs-on: go-ci + container: + image: git.fabledsword.com/bvandeusen/ci-go:1.26 + + steps: + - name: Release must have an APK attached + shell: bash + env: + CI_TOKEN: ${{ secrets.CI_TOKEN }} + run: | + set -euo pipefail + TAG="${GITHUB_REF#refs/tags/}" + REPO="${GITHUB_REPOSITORY}" + + REL_JSON="$(curl -fsSL \ + -H "Authorization: token ${CI_TOKEN}" \ + "https://git.fabledsword.com/api/v1/repos/${REPO}/releases/tags/${TAG}" || true)" + if [ -z "${REL_JSON}" ]; then + echo "::error::no release found for ${TAG} — the tag exists but nothing was published" + exit 1 + fi + + APK="$(printf '%s' "${REL_JSON}" \ + | grep -oP '"browser_download_url":\s*"\K[^"]+' \ + | grep -E '\.apk$' | head -1 || true)" + if [ -z "${APK}" ]; then + echo "::error::release ${TAG} has NO APK attached — in-app update will offer nothing, and the bundled-APK path on future :latest builds has no source." + echo "::error::Fix by RE-RUNNING this workflow run. Do NOT delete and re-create the tag; if it fails again the runner never started the container, and the evidence is in act_runner on the host (Gitea will hold no job log)." + exit 1 + fi + + echo "::notice::APK attached: ${APK}" + + # The other half. Checking only the APK would report success on a release + # whose image push failed — which is precisely the second thing that was + # missing when v2026.08.07 had to be re-cut. `always()` on this job means + # it runs even when image-release failed, so without this the guard would + # cheerfully verify an incomplete release. + - name: Immutable image tag must exist + shell: bash + run: | + set -euo pipefail + TAG="${GITHUB_REF#refs/tags/}" + IMAGE="git.fabledsword.com/bvandeusen/minstrel" + + echo "${{ secrets.CI_TOKEN }}" \ + | docker login git.fabledsword.com -u "${{ github.actor }}" --password-stdin + + if ! docker manifest inspect "${IMAGE}:${TAG}" > /dev/null 2>&1; then + echo "::error::image ${IMAGE}:${TAG} was never pushed — the release tag has no immutable image, so there is nothing to pin or roll back to. Re-run this workflow run." + exit 1 + fi + echo "::notice::image verified: ${IMAGE}:${TAG}"