Skip to content

Commit 6d6cf4f

Browse files
authored
Merge pull request #5 from Rafau-18/claude/versioned-release-pipeline
feat: versioned release pipeline — tag-triggered gated web deploy + APK GitHub Release
2 parents 5874e37 + 5a2125e commit 6d6cf4f

9 files changed

Lines changed: 897 additions & 4 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Android release: on a v* tag, build the stably-signed APK with tag-derived
2+
# versionName/versionCode and publish the GitHub Release for the tag with the
3+
# APK attached. Ungated — runs to completion automatically on tag push.
4+
#
5+
# Secrets (repo-level, public-by-design build credentials; lessons.md §37):
6+
# SUPABASE_URL, SUPABASE_ANON_KEY, GOOGLE_SERVER_CLIENT_ID
7+
#
8+
# Release creation uses the default GITHUB_TOKEN (contents: write) — no PAT,
9+
# and a Release is not a push to main, so the main-pr-gate ruleset is untouched.
10+
# This workflow triggers on tags only — never pull_request — so it cannot enter
11+
# the required-check set (contract-surfaces.md §7).
12+
name: Android release
13+
14+
on:
15+
push:
16+
tags: ["v*"]
17+
18+
permissions:
19+
contents: write
20+
21+
jobs:
22+
android-release:
23+
name: Build APK + publish Release
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 30
26+
defaults:
27+
run:
28+
working-directory: SmartChessboard
29+
steps:
30+
- name: Checkout
31+
uses: actions/checkout@v5
32+
33+
- name: Set up JDK 21
34+
uses: actions/setup-java@v5
35+
with:
36+
distribution: temurin
37+
java-version: "21"
38+
39+
- name: Set up Gradle
40+
uses: gradle/actions/setup-gradle@v4
41+
42+
# versionName = tag minus the leading "v" (pre-release suffix kept);
43+
# versionCode = MAJOR*10000 + MINOR*100 + PATCH from the core semver
44+
# (suffix stripped). Constraint: MINOR and PATCH must stay < 100 so the
45+
# code is monotonic across releases. Two pre-releases of the same core
46+
# version share a versionCode — acceptable for RC testing.
47+
- name: Derive version from tag
48+
id: version
49+
run: |
50+
TAG="${GITHUB_REF_NAME}"
51+
NAME="${TAG#v}"
52+
CORE="${NAME%%-*}"
53+
IFS=. read -r MAJOR MINOR PATCH <<< "$CORE"
54+
for part in "$MAJOR" "$MINOR" "$PATCH"; do
55+
case "$part" in
56+
''|*[!0-9]*) echo "::error::Tag '$TAG' is not v<MAJOR>.<MINOR>.<PATCH>[-suffix]"; exit 1 ;;
57+
esac
58+
done
59+
if [ "$MINOR" -ge 100 ] || [ "$PATCH" -ge 100 ]; then
60+
echo "::error::MINOR and PATCH must be < 100 for the versionCode scheme"; exit 1
61+
fi
62+
CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
63+
echo "APP_VERSION_NAME=$NAME" >> "$GITHUB_ENV"
64+
echo "APP_VERSION_CODE=$CODE" >> "$GITHUB_ENV"
65+
echo "versionName=$NAME versionCode=$CODE"
66+
67+
# Functional release APK: Supabase/Google creds injected via BuildKonfig;
68+
# signing uses the committed debug.keystore (stable signature → releases
69+
# install update-in-place).
70+
- name: Assemble release APK
71+
run: >
72+
./gradlew :androidApp:assembleDebug --console=plain
73+
-PappVersionName="$APP_VERSION_NAME"
74+
-PappVersionCode="$APP_VERSION_CODE"
75+
-PSUPABASE_URL='${{ secrets.SUPABASE_URL }}'
76+
-PSUPABASE_ANON_KEY='${{ secrets.SUPABASE_ANON_KEY }}'
77+
-PGOOGLE_SERVER_CLIENT_ID='${{ secrets.GOOGLE_SERVER_CLIENT_ID }}'
78+
79+
- name: Publish GitHub Release with APK
80+
uses: softprops/action-gh-release@v2
81+
with:
82+
tag_name: ${{ github.ref_name }}
83+
generate_release_notes: true
84+
files: SmartChessboard/androidApp/build/outputs/apk/debug/*.apk
85+
fail_on_unmatched_files: true

.github/workflows/web-deploy.yml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Web release deploy: on a v* tag, build the WasmJS bundle (ungated) and deploy
2+
# it to the existing Cloudflare Worker `smart-chessboard-web` behind the
3+
# `production` Environment approval gate. Only the live-site mutation waits for
4+
# the human "Approve" click — the build runs immediately on tag push.
5+
#
6+
# Secrets:
7+
# repo-level — SUPABASE_URL, SUPABASE_ANON_KEY, GOOGLE_SERVER_CLIENT_ID
8+
# (public-by-design build credentials; see lessons.md §37)
9+
# production — CLOUDFLARE_API_TOKEN (Workers Scripts edit, this project only),
10+
# CLOUDFLARE_ACCOUNT_ID
11+
#
12+
# This workflow triggers on tags only — it never runs on pull_request, so it
13+
# cannot enter the main-pr-gate required-check set (contract-surfaces.md §7).
14+
name: Web deploy
15+
16+
on:
17+
push:
18+
tags: ["v*"]
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
web-build:
25+
name: Build web bundle
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 30
28+
defaults:
29+
run:
30+
working-directory: SmartChessboard
31+
steps:
32+
- name: Checkout
33+
uses: actions/checkout@v5
34+
35+
- name: Set up JDK 21
36+
uses: actions/setup-java@v5
37+
with:
38+
distribution: temurin
39+
java-version: "21"
40+
41+
- name: Set up Gradle
42+
uses: gradle/actions/setup-gradle@v4
43+
44+
# Functional release bundle: Supabase/Google creds injected via BuildKonfig
45+
# (-P project properties). Only the RLS-protected anon key reaches the
46+
# bundle — never a service-role key (lessons.md §37).
47+
- name: Build wasm production bundle
48+
run: >
49+
./gradlew :webApp:wasmJsBrowserDistribution --console=plain
50+
-PSUPABASE_URL='${{ secrets.SUPABASE_URL }}'
51+
-PSUPABASE_ANON_KEY='${{ secrets.SUPABASE_ANON_KEY }}'
52+
-PGOOGLE_SERVER_CLIENT_ID='${{ secrets.GOOGLE_SERVER_CLIENT_ID }}'
53+
54+
- name: Upload web bundle
55+
uses: actions/upload-artifact@v4
56+
with:
57+
name: web-release-bundle
58+
path: SmartChessboard/webApp/build/dist/wasmJs/productionExecutable/
59+
retention-days: 14
60+
if-no-files-found: error
61+
62+
web-deploy:
63+
name: Deploy to Cloudflare (gated)
64+
needs: web-build
65+
runs-on: ubuntu-latest
66+
timeout-minutes: 15
67+
# The approval gate: the `production` Environment requires a reviewer, so
68+
# this job pauses in the Actions UI until a human clicks Approve.
69+
environment: production
70+
steps:
71+
# Checkout only for wrangler.toml — the bundle comes from the artifact.
72+
- name: Checkout
73+
uses: actions/checkout@v5
74+
75+
- name: Download web bundle
76+
uses: actions/download-artifact@v4
77+
with:
78+
name: web-release-bundle
79+
path: SmartChessboard/webApp/build/dist/wasmJs/productionExecutable/
80+
81+
# COOP/COEP guard: without _headers the deployed site silently loses
82+
# SharedArrayBuffer → OPFS persistence breaks (lessons.md §27).
83+
- name: Assert _headers survived into the bundle
84+
run: |
85+
test -f SmartChessboard/webApp/build/dist/wasmJs/productionExecutable/_headers \
86+
|| { echo "::error::_headers missing from productionExecutable/ — COOP/COEP would be lost"; exit 1; }
87+
88+
- name: Deploy with wrangler
89+
uses: cloudflare/wrangler-action@v3
90+
with:
91+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
92+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
93+
command: deploy

SmartChessboard/androidApp/build.gradle.kts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,21 @@ android {
4040
libs.versions.android.targetSdk
4141
.get()
4242
.toInt()
43-
versionCode = 1
44-
versionName = "1.0"
43+
// Release CI injects tag-derived values (-PappVersionName/-PappVersionCode);
44+
// local and PR-gate builds fall back to the historical constants.
45+
versionCode = (project.findProperty("appVersionCode") as String?)?.toInt() ?: 1
46+
versionName = (project.findProperty("appVersionName") as String?) ?: "1.0"
47+
}
48+
signingConfigs {
49+
// Committed throwaway keystore (standard Android debug conventions,
50+
// password "android" — non-secret by design) so every CI runner signs
51+
// with the same key and release APKs install update-in-place.
52+
getByName("debug") {
53+
storeFile = file("debug.keystore")
54+
storePassword = "android"
55+
keyAlias = "androiddebugkey"
56+
keyPassword = "android"
57+
}
4558
}
4659
packaging {
4760
resources {
2.6 KB
Binary file not shown.

context/changes/github-ci-and-distribution/change.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ change_id: github-ci-and-distribution
33
title: Public GitHub + CI — web→Cloudflare and Android→Firebase App Distribution
44
status: planned
55
created: 2026-07-02
6-
updated: 2026-07-02
6+
updated: 2026-07-05
77
archived_at: null
88
---
99

1010
## Notes
1111

12+
> **SUPERSEDED (2026-07-05) by [`versioned-release-pipeline`](../versioned-release-pipeline/change.md).**
13+
> Never implemented. Its go-public + history-scrub half is now owned by `public-repo-and-pr-gate`
14+
> (S-14); its web→Cloudflare `wrangler deploy` + Android-artifact half is replaced by
15+
> `versioned-release-pipeline` (tag-triggered, gated deploy + downloadable APK GitHub Release
16+
> instead of Firebase App Distribution). Firebase App Distribution is carried over as an
17+
> **optional final phase** of the new change. Left here as a planning record; may be `/10x-archive`d.
18+
1219
Infrastructure change (CI/CD + distribution) — **not** a vertical product slice, so it is intentionally not an `S-NN` in the roadmap (no PRD FR/US trace). It **un-parks** two roadmap `## Parked` items — "CI pipeline (build/test/deploy workflows)" and the CI half of "Store/TestFlight mobile distribution" (Firebase App Distribution only, not app stores). Builds on an earlier local CI/distribution planning sketch (not published) and the web-host decision in `context/foundation/infrastructure.md` (Cloudflare Workers Static Assets).
1320

1421
### Decisions locked (2026-07-02)

0 commit comments

Comments
 (0)