@@ -0,0 +1,101 @@
3+# Build a signed production APK and publish a GitHub Release for every
4+# commit that lands on master. The version -- and the git tag / release name --
5+# is derived from the commit's committer date in UTC, formatted as
6+# YYYY.MM.DD.HHMM (e.g. 2026.07.08.1430). It is passed to the Gradle build as
7+# `-PversionName=...` so it is stamped into the APK.
12+# A release run should never be aborted mid-flight by a newer push (a
13+# half-published release with a missing artifact is worse than a slightly stale
14+# one), so the group is per-ref but cancel-in-progress is left off.
16+ group: ${{ github.workflow }}-${{ github.ref }}
17+ cancel-in-progress: false
21+ runs-on: ubuntu-latest
23+ contents: write # required to push the tag and create the release
24+ id-token: write # required for OIDC so GitHub can sign the attestation
25+ attestations: write # required to publish the attestation record
27+ - uses: actions/checkout@v4
29+ # Full history so the versionCode (git rev-list --count HEAD) is
30+ # accurate instead of collapsing to 1 on a shallow clone.
33+ - name: Compute version from commit date
36+ VERSION="$(TZ=UTC git log -1 --format=%cd --date=format-local:%Y.%m.%d.%H%M)"
37+ CODE="$(git rev-list --count HEAD)"
38+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
39+ echo "code=$CODE" >> "$GITHUB_OUTPUT"
40+ echo "Version: $VERSION (code $CODE)"
42+ - uses: actions/setup-java@v4
44+ distribution: temurin
47+ - uses: android-actions/setup-android@v3
49+ - uses: gradle/actions/setup-gradle@v4
51+ # The keystore ships as base64 in a secret so it never touches the repo.
52+ # When the secret isn't configured the Gradle build falls back to an
53+ # unsigned APK (the same behavior as a local build), so this workflow is
54+ # green even before signing is wired up.
55+ - name: Decode signing keystore
56+ if: env.OPENCODE_STORE_BASE64 != ''
58+ OPENCODE_STORE_BASE64: ${{ secrets.OPENCODE_STORE_BASE64 }}
60+ STORE_FILE="$RUNNER_TEMP/release.keystore"
61+ printf '%s' "$OPENCODE_STORE_BASE64" | base64 -d > "$STORE_FILE"
62+ echo "OPENCODE_STORE_FILE=$STORE_FILE" >> "$GITHUB_ENV"
64+ - name: Build release APK
66+ OPENCODE_STORE_FILE: ${{ env.OPENCODE_STORE_FILE }}
67+ OPENCODE_STORE_PASSWORD: ${{ secrets.OPENCODE_STORE_PASSWORD }}
68+ OPENCODE_KEY_ALIAS: ${{ secrets.OPENCODE_KEY_ALIAS }}
69+ OPENCODE_KEY_PASSWORD: ${{ secrets.OPENCODE_KEY_PASSWORD }}
71+ ./gradlew assembleRelease \
72+ -PversionName="${{ steps.version.outputs.version }}" \
75+ # Rename to a stable, version-tagged filename so the release attachment is
76+ # recognizable regardless of the release/ vs release-signed/ output dir.
80+ APK="$(find app/build/outputs/apk -name '*.apk' | head -n1)"
81+ cp "$APK" "staging/opencode-${{ steps.version.outputs.version }}.apk"
83+ # Generate a signed build-provenance attestation for the staged APK and
84+ # store it in GitHub's artifact attestation store, keyed by the file's
85+ # digest. Anyone can later verify the APK's origin/provenance with
86+ # `gh attestation verify` -- its repository & workflow. Run *after*
87+ # staging and *before* the release is published so the attested subject
88+ # is byte-identical to the artifact attached to the release.
89+ - name: Attest build provenance
90+ uses: actions/attest-build-provenance@v2
92+ subject-path: staging/*.apk
94+ - name: Create release
95+ uses: softprops/action-gh-release@v2
97+ tag_name: ${{ steps.version.outputs.version }}
98+ name: ${{ steps.version.outputs.version }}
100+ generate_release_notes: true
101+ files: staging/*.apk