@@ -0,0 +1,101 @@
1+name: Release
2+
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.
8+on:
9+ push:
10+ branches: [master]
11+
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.
15+concurrency:
16+ group: ${{ github.workflow }}-${{ github.ref }}
17+ cancel-in-progress: false
18+
19+jobs:
20+ release:
21+ runs-on: ubuntu-latest
22+ permissions:
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
26+ steps:
27+ - uses: actions/checkout@v4
28+ with:
29+ # Full history so the versionCode (git rev-list --count HEAD) is
30+ # accurate instead of collapsing to 1 on a shallow clone.
31+ fetch-depth: 0
32+
33+ - name: Compute version from commit date
34+ id: version
35+ run: |
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)"
41+
42+ - uses: actions/setup-java@v4
43+ with:
44+ distribution: temurin
45+ java-version: 17
46+
47+ - uses: android-actions/setup-android@v3
48+
49+ - uses: gradle/actions/setup-gradle@v4
50+
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 != ''
57+ env:
58+ OPENCODE_STORE_BASE64: ${{ secrets.OPENCODE_STORE_BASE64 }}
59+ run: |
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"
63+
64+ - name: Build release APK
65+ env:
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 }}
70+ run: |
71+ ./gradlew assembleRelease \
72+ -PversionName="${{ steps.version.outputs.version }}" \
73+ --no-daemon
74+
75+ # Rename to a stable, version-tagged filename so the release attachment is
76+ # recognizable regardless of the release/ vs release-signed/ output dir.
77+ - name: Stage APK
78+ run: |
79+ mkdir -p staging
80+ APK="$(find app/build/outputs/apk -name '*.apk' | head -n1)"
81+ cp "$APK" "staging/opencode-${{ steps.version.outputs.version }}.apk"
82+
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
91+ with:
92+ subject-path: staging/*.apk
93+
94+ - name: Create release
95+ uses: softprops/action-gh-release@v2
96+ with:
97+ tag_name: ${{ steps.version.outputs.version }}
98+ name: ${{ steps.version.outputs.version }}
99+ make_latest: true
100+ generate_release_notes: true
101+ files: staging/*.apk