@@ -0,0 +1,82 @@
1+name: Release
2+
3+# Build the production (release) 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.04.1430). It is passed to the Gradle build as
7+# `-PversionName=...` so it is stamped into the APK and surfaces in-app.
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+ steps:
25+ - uses: actions/checkout@v4
26+ with:
27+ # Full history so the versionCode (git rev-list --count HEAD) is
28+ # accurate instead of collapsing to 1 on a shallow clone.
29+ fetch-depth: 0
30+
31+ - name: Compute version from commit date
32+ id: version
33+ run: |
34+ VERSION="$(TZ=UTC git log -1 --format=%cd --date=format-local:%Y.%m.%d.%H%M)"
35+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
36+ echo "Version: $VERSION"
37+
38+ - uses: actions/setup-java@v4
39+ with:
40+ distribution: temurin
41+ java-version: 17
42+
43+ - uses: android-actions/setup-android@v3
44+
45+ - uses: gradle/actions/setup-gradle@v4
46+
47+ # The keystore ships as base64 in a secret so it never touches the repo.
48+ # When the secret isn't configured the Gradle build falls back to an
49+ # unsigned APK (the same behavior as a local build), so this workflow is
50+ # green even before signing is wired up.
51+ - name: Decode signing keystore
52+ if: secrets.OPENCODE_STORE_BASE64 != ''
53+ env:
54+ OPENCODE_STORE_BASE64: ${{ secrets.OPENCODE_STORE_BASE64 }}
55+ run: |
56+ STORE_FILE="$RUNNER_TEMP/release.keystore"
57+ printf '%s' "$OPENCODE_STORE_BASE64" | base64 -d > "$STORE_FILE"
58+ echo "OPENCODE_STORE_FILE=$STORE_FILE" >> "$GITHUB_ENV"
59+
60+ - name: Build release APK
61+ env:
62+ OPENCODE_STORE_FILE: ${{ env.OPENCODE_STORE_FILE }}
63+ OPENCODE_STORE_PASSWORD: ${{ secrets.OPENCODE_STORE_PASSWORD }}
64+ OPENCODE_KEY_ALIAS: ${{ secrets.OPENCODE_KEY_ALIAS }}
65+ OPENCODE_KEY_PASSWORD: ${{ secrets.OPENCODE_KEY_PASSWORD }}
66+ run: ./gradlew assembleRelease -PversionName="${{ steps.version.outputs.version }}" --no-daemon
67+
68+ # Rename to a stable, version-tagged filename so the release attachment is
69+ # recognizable regardless of the release/ vs release-signed/ output dir.
70+ - name: Stage APK
71+ run: |
72+ mkdir -p staging
73+ APK="$(find app/build/outputs/apk -name '*.apk' | head -n1)"
74+ cp "$APK" "staging/opencode-${{ steps.version.outputs.version }}.apk"
75+
76+ - name: Create release
77+ uses: softprops/action-gh-release@v2
78+ with:
79+ tag_name: ${{ steps.version.outputs.version }}
80+ name: ${{ steps.version.outputs.version }}
81+ generate_release_notes: true
82+ files: staging/*.apk