Desktop clients had two competing update sources: the runtime pointed at a legacy GitLab registry while the Forgejo packages were filled in by hardcoded, version-pinned scripts. Operators could not tell which feed was authoritative, and no release could be reproduced from a tag. Auto-update now reads a single canonical Forgejo registry feed, updated by a version-agnostic publisher that runs from the tag on Forgejo, GitLab, and GitHub CI alike. Channel, minimum supported version, forced install, full-versus-delta thresholds, staged rollout, and a remote kill switch come from one policy file the client fetches alongside the feed. Tag creation is gated on a clean tree, matching version surfaces, and a changelog section.
586 lines
26 KiB
YAML
586 lines
26 KiB
YAML
# .github/workflows/release.yml
|
|
# Multi-Platform Automated Release Pipeline for D3RO Voice Desktop & Admin
|
|
|
|
name: Release & Packaging Pipeline
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*.*.*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'Release version (e.g. 1.0.0)'
|
|
required: true
|
|
default: '1.1.0'
|
|
|
|
permissions:
|
|
contents: write
|
|
packages: write
|
|
|
|
jobs:
|
|
release-preflight:
|
|
name: Release Preflight
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout Code
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Node.js 24
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
cache: 'npm'
|
|
|
|
- name: Setup .NET 10
|
|
uses: actions/setup-dotnet@v5
|
|
with:
|
|
dotnet-version: '10.0.302'
|
|
|
|
- name: Setup Deno 2.8.1
|
|
uses: denoland/setup-deno@v2
|
|
with:
|
|
deno-version: v2.8.1
|
|
|
|
- name: Install JavaScript Dependencies
|
|
run: |
|
|
npm ci
|
|
npm --prefix apps/mobile-rn ci --workspaces=false
|
|
|
|
- name: Verify Source, Security, Tests, and Play Assets
|
|
run: |
|
|
npm run version:check
|
|
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
|
node scripts/ci/sync-version.mjs --check --tag "$GITHUB_REF_NAME"
|
|
fi
|
|
npm run release:metadata:test
|
|
npm run security:secrets:test
|
|
npm run security:secrets
|
|
npm run release:mobile:boundary:test
|
|
npm run release:mobile:config:test
|
|
npm run release:mobile:build-config:test
|
|
npm run release:play:assets
|
|
npm run lint
|
|
npm run typecheck
|
|
npm test
|
|
npm --prefix apps/mobile-rn run lint
|
|
npm --prefix apps/mobile-rn run typecheck
|
|
npm --prefix apps/mobile-rn run test:ci
|
|
|
|
- name: Check and Test Every Supabase Edge Function
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
for entrypoint in server/supabase/functions/*/index.ts; do
|
|
deno check --config server/supabase/functions/deno.json "$entrypoint"
|
|
done
|
|
deno test --config server/supabase/functions/deno.json --allow-read --allow-env server/supabase/functions
|
|
|
|
- name: Test .NET API Authorization and Gateway Boundaries
|
|
run: |
|
|
dotnet restore apps/api-server.Tests/D3ROVoice.Api.Tests.csproj
|
|
dotnet test apps/api-server.Tests/D3ROVoice.Api.Tests.csproj --configuration Release --no-restore -p:StaticWebAssetsEnabled=false
|
|
|
|
# ──────────────────────────────────────────────────────────────────
|
|
# 1. Package Windows Installer (.exe & .blockmap & latest.yml)
|
|
# ──────────────────────────────────────────────────────────────────
|
|
package-windows:
|
|
name: Package Windows Desktop App
|
|
needs: release-preflight
|
|
runs-on: windows-latest
|
|
steps:
|
|
- name: Checkout Code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js 24
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
cache: 'npm'
|
|
|
|
- name: Install Dependencies
|
|
run: npm ci
|
|
|
|
- name: Build All Workspaces
|
|
run: |
|
|
npm run version:check
|
|
npm run typecheck
|
|
npm run build --workspace=@d3ro/desktop
|
|
|
|
- name: Package with Electron Builder (NSIS x64)
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
if ([string]::IsNullOrWhiteSpace($env:CSC_LINK)) {
|
|
throw 'WIN_CSC_LINK must contain the production code-signing PFX path, URL, or base64 payload.'
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($env:CSC_KEY_PASSWORD)) {
|
|
throw 'WIN_CSC_KEY_PASSWORD is required.'
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($env:WIN_CSC_EXPECTED_SIGNER_SUBJECT)) {
|
|
throw 'WIN_CSC_EXPECTED_SIGNER_SUBJECT is required.'
|
|
}
|
|
if ($env:WIN_CSC_EXPECTED_SIGNER_SUBJECT -match '(?i)Everything2EverythingDev') {
|
|
throw 'The local Everything2EverythingDev certificate is not a production signing identity.'
|
|
}
|
|
$releaseVersion = node -p "require('./release/product-version.json').version"
|
|
Push-Location apps/desktop
|
|
try {
|
|
npx electron-builder --win --x64 --config electron-builder.yml
|
|
if ($LASTEXITCODE -ne 0) { throw "electron-builder failed with exit code $LASTEXITCODE." }
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
& scripts/ci/verify-windows-release-artifact.ps1 `
|
|
-ExpectedVersion $releaseVersion `
|
|
-ExpectedSignerSubject $env:WIN_CSC_EXPECTED_SIGNER_SUBJECT `
|
|
-ReleaseDirectory "apps/desktop/release/$releaseVersion"
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
|
WIN_CSC_EXPECTED_SIGNER_SUBJECT: ${{ secrets.WIN_CSC_EXPECTED_SIGNER_SUBJECT }}
|
|
|
|
- name: Upload Windows Build Artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: windows-release-assets
|
|
path: |
|
|
apps/desktop/release/*/*.exe
|
|
apps/desktop/release/*/*.blockmap
|
|
apps/desktop/release/*/latest.yml
|
|
|
|
# ──────────────────────────────────────────────────────────────────
|
|
# 2. Package macOS Desktop App (.dmg & .zip & latest-mac.yml)
|
|
# ──────────────────────────────────────────────────────────────────
|
|
package-macos:
|
|
name: Package macOS Desktop App
|
|
needs: release-preflight
|
|
runs-on: macos-latest
|
|
steps:
|
|
- name: Checkout Code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js 24
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
cache: 'npm'
|
|
|
|
- name: Install Dependencies
|
|
run: npm ci
|
|
|
|
- name: Build All Workspaces
|
|
run: |
|
|
npm run version:check
|
|
npm run typecheck
|
|
npm run build --workspace=@d3ro/desktop
|
|
|
|
- name: Package with Electron Builder (DMG & ZIP arm64)
|
|
run: |
|
|
cd apps/desktop
|
|
npx electron-builder --mac --arm64 --config electron-builder.yml
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
|
|
- name: Upload macOS Build Artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: macos-release-assets
|
|
path: |
|
|
apps/desktop/release/*/*.dmg
|
|
apps/desktop/release/*/*.zip
|
|
apps/desktop/release/*/*.blockmap
|
|
apps/desktop/release/*/latest-mac.yml
|
|
|
|
# ──────────────────────────────────────────────────────────────────
|
|
# 3. Package signed Android APK/AAB (arm64, production-only config)
|
|
# ──────────────────────────────────────────────────────────────────
|
|
package-android:
|
|
name: Package Android Mobile App
|
|
needs: release-preflight
|
|
runs-on: ubuntu-latest
|
|
environment: mobile-production-release
|
|
steps:
|
|
- name: Checkout Code
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Establish Trusted Mobile Release Identity
|
|
id: android-version
|
|
shell: bash
|
|
env:
|
|
DISPATCH_VERSION: ${{ inputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
git fetch --no-tags origin main
|
|
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
|
|
test "$GITHUB_REF" = "refs/heads/main"
|
|
test "$GITHUB_SHA" = "$(git rev-parse origin/main)"
|
|
VERSION_NAME="$DISPATCH_VERSION"
|
|
RELEASE_TAG="v$VERSION_NAME"
|
|
else
|
|
[[ "$GITHUB_REF" == refs/tags/v* ]]
|
|
VERSION_NAME="${GITHUB_REF_NAME#v}"
|
|
RELEASE_TAG="$GITHUB_REF_NAME"
|
|
git merge-base --is-ancestor "$GITHUB_SHA" origin/main
|
|
fi
|
|
SSOT_VERSION="$(node -p "require('./release/product-version.json').version")"
|
|
VERSION_CODE="$(node -p "require('./release/product-version.json').androidVersionCode")"
|
|
[[ "$VERSION_NAME" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
|
|
test "$VERSION_NAME" = "$SSOT_VERSION"
|
|
test "$RELEASE_TAG" = "v$VERSION_NAME"
|
|
[[ "$VERSION_CODE" =~ ^[1-9][0-9]{0,9}$ ]]
|
|
test "$VERSION_CODE" -le 2100000000
|
|
printf 'name=%s\n' "$VERSION_NAME" >> "$GITHUB_OUTPUT"
|
|
printf 'code=%s\n' "$VERSION_CODE" >> "$GITHUB_OUTPUT"
|
|
printf 'tag=%s\n' "$RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Verify Immutable Checkout Identity
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
test "$GITHUB_SHA" = "$(git rev-parse HEAD)"
|
|
test -z "$(git status --porcelain --untracked-files=all)"
|
|
git rev-parse "${GITHUB_SHA}^{tree}"
|
|
|
|
- name: Setup Node.js 24
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
cache: 'npm'
|
|
|
|
- name: Verify Mobile Release Boundary Source Contract
|
|
run: node scripts/ci/verify-mobile-release-boundary.mjs --self-test
|
|
|
|
- name: Verify Mobile Release Configuration Contract
|
|
run: npm run release:mobile:config:test
|
|
|
|
- name: Verify Mobile Build Configuration Contract
|
|
run: npm run release:mobile:build-config:test
|
|
|
|
- name: Verify Play Store Asset Contract
|
|
run: npm run release:play:assets
|
|
|
|
- name: Require Restricted AAB Handoff Visibility
|
|
shell: bash
|
|
env:
|
|
REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }}
|
|
run: |
|
|
set -euo pipefail
|
|
test "$REPOSITORY_VISIBILITY" = "private"
|
|
|
|
- name: Setup JDK 17
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
distribution: temurin
|
|
java-version: '17'
|
|
|
|
- name: Setup Android SDK
|
|
uses: android-actions/setup-android@v4
|
|
|
|
- name: Install Pinned Official Bundletool
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
curl --fail --silent --show-error --location \
|
|
--output "$RUNNER_TEMP/bundletool-all-1.18.3.jar" \
|
|
https://github.com/google/bundletool/releases/download/1.18.3/bundletool-all-1.18.3.jar
|
|
printf '%s %s\n' \
|
|
a099cfa1543f55593bc2ed16a70a7c67fe54b1747bb7301f37fdfd6d91028e29 \
|
|
"$RUNNER_TEMP/bundletool-all-1.18.3.jar" | sha256sum --check --strict
|
|
|
|
- name: Setup Gradle
|
|
uses: gradle/actions/setup-gradle@v6
|
|
with:
|
|
cache-provider: basic
|
|
|
|
- name: Install Dependencies
|
|
run: |
|
|
npm ci
|
|
npm --prefix apps/mobile-rn ci --workspaces=false
|
|
|
|
- name: Materialize Release-only Configuration
|
|
shell: bash
|
|
env:
|
|
ANDROID_RELEASE_KEYSTORE_B64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_B64 }}
|
|
ANDROID_GOOGLE_SERVICES_JSON_B64: ${{ secrets.ANDROID_GOOGLE_SERVICES_JSON_B64 }}
|
|
run: |
|
|
set -euo pipefail
|
|
umask 077
|
|
test -n "$ANDROID_RELEASE_KEYSTORE_B64"
|
|
test -n "$ANDROID_GOOGLE_SERVICES_JSON_B64"
|
|
printf '%s' "$ANDROID_RELEASE_KEYSTORE_B64" | base64 --decode > apps/mobile-rn/android/app/release.keystore
|
|
printf '%s' "$ANDROID_GOOGLE_SERVICES_JSON_B64" | base64 --decode > apps/mobile-rn/android/app/google-services.json
|
|
test -s apps/mobile-rn/android/app/release.keystore
|
|
test -s apps/mobile-rn/android/app/google-services.json
|
|
|
|
- name: Prepare Verified Whisper Model
|
|
run: node scripts/ci/prepare-whisper-model.mjs
|
|
|
|
- name: Verify Production Firebase and AdMob Configuration
|
|
env:
|
|
D3RO_FIREBASE_EXPECTED_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }}
|
|
D3RO_FIREBASE_EXPECTED_PROJECT_NUMBER: ${{ secrets.FIREBASE_PROJECT_NUMBER }}
|
|
D3RO_FIREBASE_EXPECTED_MOBILESDK_APP_ID: ${{ secrets.FIREBASE_MOBILESDK_APP_ID }}
|
|
run: |
|
|
export D3RO_ADMOB_APP_ID="$(node -p "require('./release/android-release-identity.json').adMobAppId")"
|
|
export D3RO_ADMOB_BANNER_UNIT_ID="$(node -p "require('./release/android-release-identity.json').adMobBannerUnitId")"
|
|
export D3RO_ADMOB_REWARDED_UNIT_ID="$(node -p "require('./release/android-release-identity.json').adMobRewardedUnitId")"
|
|
npm run release:mobile:config
|
|
|
|
- name: Test Mobile TypeScript and Jest
|
|
run: |
|
|
npm --prefix apps/mobile-rn run lint
|
|
npm --prefix apps/mobile-rn run typecheck
|
|
npm --prefix apps/mobile-rn run test:ci
|
|
|
|
- name: Build Signed arm64 APK and AAB
|
|
working-directory: apps/mobile-rn/android
|
|
env:
|
|
D3RO_RELEASE_STORE_FILE: ${{ github.workspace }}/apps/mobile-rn/android/app/release.keystore
|
|
D3RO_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_STORE_PASSWORD }}
|
|
D3RO_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }}
|
|
D3RO_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }}
|
|
D3RO_VERSION_NAME: ${{ steps.android-version.outputs.name }}
|
|
D3RO_VERSION_CODE: ${{ steps.android-version.outputs.code }}
|
|
run: |
|
|
export D3RO_ADMOB_APP_ID="$(node -p "require('../../../release/android-release-identity.json').adMobAppId")"
|
|
export D3RO_ADMOB_BANNER_UNIT_ID="$(node -p "require('../../../release/android-release-identity.json').adMobBannerUnitId")"
|
|
export D3RO_ADMOB_REWARDED_UNIT_ID="$(node -p "require('../../../release/android-release-identity.json').adMobRewardedUnitId")"
|
|
./gradlew :app:assembleRelease :app:bundleRelease -PreactNativeArchitectures=arm64-v8a --no-daemon
|
|
|
|
- name: Materialize Release Evidence Signing Key
|
|
shell: bash
|
|
env:
|
|
ANDROID_RELEASE_EVIDENCE_PRIVATE_KEY_B64: ${{ secrets.ANDROID_RELEASE_EVIDENCE_PRIVATE_KEY_B64 }}
|
|
run: |
|
|
set -euo pipefail
|
|
umask 077
|
|
test -n "$ANDROID_RELEASE_EVIDENCE_PRIVATE_KEY_B64"
|
|
printf '%s' "$ANDROID_RELEASE_EVIDENCE_PRIVATE_KEY_B64" | base64 --decode > apps/mobile-rn/android/app/release-evidence-private.pem
|
|
test -s apps/mobile-rn/android/app/release-evidence-private.pem
|
|
|
|
- name: Verify Release BuildConfig, Signature, ABI, Bundle, Ads, and Offline Model
|
|
shell: bash
|
|
env:
|
|
D3RO_VERSION_NAME: ${{ steps.android-version.outputs.name }}
|
|
D3RO_VERSION_CODE: ${{ steps.android-version.outputs.code }}
|
|
run: |
|
|
set -euo pipefail
|
|
D3RO_ADMOB_APP_ID="$(node -p "require('./release/android-release-identity.json').adMobAppId")"
|
|
D3RO_ADMOB_BANNER_UNIT_ID="$(node -p "require('./release/android-release-identity.json').adMobBannerUnitId")"
|
|
D3RO_ADMOB_REWARDED_UNIT_ID="$(node -p "require('./release/android-release-identity.json').adMobRewardedUnitId")"
|
|
ANDROID_UPLOAD_CERT_SHA256="$(node -p "require('./release/android-release-identity.json').uploadCertificateSha256")"
|
|
PLAY_APP_SIGNING_CERT_SHA256="$(node -p "require('./release/android-release-identity.json').playAppSigningCertificateSha256")"
|
|
APK=apps/mobile-rn/android/app/build/outputs/apk/release/app-release.apk
|
|
AAB=apps/mobile-rn/android/app/build/outputs/bundle/release/app-release.aab
|
|
test -f "$APK"
|
|
test -f "$AAB"
|
|
node scripts/ci/verify-mobile-build-config.mjs release \
|
|
| tee apps/mobile-rn/android/app/build/outputs/release-build-config.json
|
|
node scripts/ci/create-mobile-release-evidence.mjs \
|
|
--apk "$APK" \
|
|
--aab "$AAB" \
|
|
--bundletool "$RUNNER_TEMP/bundletool-all-1.18.3.jar" \
|
|
--repository "$GITHUB_REPOSITORY" \
|
|
--commit-sha "$GITHUB_SHA" \
|
|
--tree-sha "$(git rev-parse "${GITHUB_SHA}^{tree}")" \
|
|
--git-ref "$GITHUB_REF" \
|
|
--workflow-identity "$GITHUB_WORKFLOW_REF" \
|
|
--run-id "$GITHUB_RUN_ID" \
|
|
--run-attempt "$GITHUB_RUN_ATTEMPT" \
|
|
--runner-identity "$RUNNER_NAME:$RUNNER_OS:$RUNNER_ARCH" \
|
|
--expected-admob-app-id "$D3RO_ADMOB_APP_ID" \
|
|
--expected-upload-cert-sha256 "$ANDROID_UPLOAD_CERT_SHA256" \
|
|
--expected-version-name "$D3RO_VERSION_NAME" \
|
|
--expected-version-code "$D3RO_VERSION_CODE" \
|
|
--private-key apps/mobile-rn/android/app/release-evidence-private.pem \
|
|
--snapshot-dir apps/mobile-rn/android/app/build/outputs/release-snapshot
|
|
VERIFIER_SHA256="$(sha256sum scripts/ci/verify-android-artifact.mjs | awk '{print $1}')"
|
|
BUNDLETOOL_SHA256="a099cfa1543f55593bc2ed16a70a7c67fe54b1747bb7301f37fdfd6d91028e29"
|
|
node scripts/ci/prepare-mobile-release-publication.mjs \
|
|
--source-root apps/mobile-rn/android/app/build/outputs/release-snapshot \
|
|
--apk apps/mobile-rn/android/app/build/outputs/release-snapshot/app-release.apk \
|
|
--aab apps/mobile-rn/android/app/build/outputs/release-snapshot/app-release.aab \
|
|
--evidence apps/mobile-rn/android/app/build/outputs/release-snapshot/release-artifact-evidence.json \
|
|
--public-key release/mobile-release-evidence-public.pem \
|
|
--destination-dir apps/mobile-rn/android/app/build/outputs/release-publication \
|
|
--expected-admob-app-id "$D3RO_ADMOB_APP_ID" \
|
|
--expected-upload-cert-sha256 "$ANDROID_UPLOAD_CERT_SHA256" \
|
|
--expected-version-name "$D3RO_VERSION_NAME" \
|
|
--expected-version-code "$D3RO_VERSION_CODE" \
|
|
--expected-repository "$GITHUB_REPOSITORY" \
|
|
--expected-commit-sha "$GITHUB_SHA" \
|
|
--expected-tree-sha "$(git rev-parse "${GITHUB_SHA}^{tree}")" \
|
|
--expected-git-ref "$GITHUB_REF" \
|
|
--expected-workflow-identity "$GITHUB_WORKFLOW_REF" \
|
|
--expected-run-id "$GITHUB_RUN_ID" \
|
|
--expected-run-attempt "$GITHUB_RUN_ATTEMPT" \
|
|
--expected-runner-identity "$RUNNER_NAME:$RUNNER_OS:$RUNNER_ARCH" \
|
|
--expected-verifier-sha256 "$VERIFIER_SHA256" \
|
|
--expected-bundletool-sha256 "$BUNDLETOOL_SHA256"
|
|
node scripts/ci/verify-android-app-links.mjs \
|
|
--expected-play-app-signing-cert-sha256 "$PLAY_APP_SIGNING_CERT_SHA256" \
|
|
--forbidden-upload-cert-sha256 "$ANDROID_UPLOAD_CERT_SHA256" \
|
|
| tee apps/mobile-rn/android/app/build/outputs/release-app-links-evidence.json
|
|
sha256sum apps/mobile-rn/android/app/build/outputs/release-publication/app-release.apk \
|
|
apps/mobile-rn/android/app/build/outputs/release-publication/app-release.aab \
|
|
| tee apps/mobile-rn/android/app/build/outputs/release-publication/SHA256SUMS.txt
|
|
|
|
- name: Remove Materialized Release Secrets
|
|
if: always()
|
|
shell: bash
|
|
run: rm -f apps/mobile-rn/android/app/release.keystore apps/mobile-rn/android/app/google-services.json apps/mobile-rn/android/app/release-evidence-private.pem
|
|
|
|
- name: Upload Restricted Play Console AAB Handoff
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: android-play-console-handoff
|
|
path: |
|
|
apps/mobile-rn/android/app/build/outputs/release-publication/app-release.aab
|
|
apps/mobile-rn/android/app/build/outputs/release-publication/android-release-evidence.json
|
|
apps/mobile-rn/android/app/build/outputs/release-publication/android-publication-manifest.json
|
|
apps/mobile-rn/android/app/build/outputs/release-publication/SHA256SUMS.txt
|
|
apps/mobile-rn/android/app/build/outputs/release-snapshot/release-artifact-verification.json
|
|
apps/mobile-rn/android/app/build/outputs/release-build-config.json
|
|
apps/mobile-rn/android/app/build/outputs/release-app-links-evidence.json
|
|
retention-days: 7
|
|
if-no-files-found: error
|
|
|
|
# ──────────────────────────────────────────────────────────────────
|
|
# 4. Build & Containerize Admin Dashboard
|
|
# ──────────────────────────────────────────────────────────────────
|
|
package-admin-docker:
|
|
name: Build & Publish Admin Docker Image
|
|
needs: release-preflight
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout Code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: actions/setup-buildx-action@v3
|
|
|
|
- name: Log in to GitHub Container Registry (GHCR)
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Extract metadata (tags, labels) for Docker
|
|
id: meta
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ghcr.io/${{ github.repository }}/admin-console
|
|
|
|
- name: Build and push Docker image
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .
|
|
file: ./apps/admin/Dockerfile
|
|
push: ${{ github.event_name != 'pull_request' }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
|
|
# ──────────────────────────────────────────────────────────────────
|
|
# 5. Create GitHub Release & Upload Checksums
|
|
# ──────────────────────────────────────────────────────────────────
|
|
publish-release:
|
|
name: Publish Official GitHub Release
|
|
needs: [package-windows, package-macos, package-android, package-admin-docker]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout Code
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Re-establish Trusted Release Identity
|
|
id: release-identity
|
|
shell: bash
|
|
env:
|
|
DISPATCH_VERSION: ${{ inputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
git fetch --no-tags origin main
|
|
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
|
|
test "$GITHUB_REF" = "refs/heads/main"
|
|
test "$GITHUB_SHA" = "$(git rev-parse origin/main)"
|
|
VERSION_NAME="$DISPATCH_VERSION"
|
|
RELEASE_TAG="v$VERSION_NAME"
|
|
else
|
|
[[ "$GITHUB_REF" == refs/tags/v* ]]
|
|
VERSION_NAME="${GITHUB_REF_NAME#v}"
|
|
RELEASE_TAG="$GITHUB_REF_NAME"
|
|
git merge-base --is-ancestor "$GITHUB_SHA" origin/main
|
|
fi
|
|
SSOT_VERSION="$(node -p "require('./release/product-version.json').version")"
|
|
[[ "$VERSION_NAME" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
|
|
test "$VERSION_NAME" = "$SSOT_VERSION"
|
|
test "$RELEASE_TAG" = "v$VERSION_NAME"
|
|
printf 'name=%s\n' "$VERSION_NAME" >> "$GITHUB_OUTPUT"
|
|
printf 'tag=%s\n' "$RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Setup Node.js 24
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
|
|
- name: Download Windows Artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: windows-release-assets
|
|
path: release-dist/
|
|
|
|
- name: Download macOS Artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: macos-release-assets
|
|
path: release-dist/
|
|
|
|
- name: Generate SHA-256 Checksums
|
|
run: |
|
|
set -euo pipefail
|
|
find release-dist -type f ! -name SHA256SUMS.txt -print0 \
|
|
| sort -z \
|
|
| xargs -0 sha256sum > release-dist/SHA256SUMS.txt
|
|
cat release-dist/SHA256SUMS.txt
|
|
|
|
- name: Extract Canonical Release Notes
|
|
run: >-
|
|
node scripts/ci/extract-release-notes.mjs
|
|
--version "${{ steps.release-identity.outputs.name }}"
|
|
--output release-notes.md
|
|
|
|
- name: Create GitHub Release
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
files: |
|
|
release-dist/*
|
|
draft: false
|
|
prerelease: false
|
|
body_path: release-notes.md
|
|
generate_release_notes: false
|
|
tag_name: ${{ steps.release-identity.outputs.tag }}
|
|
fail_on_unmatched_files: true
|
|
overwrite_files: false
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Publish to Forgejo Release and Update Feed
|
|
env:
|
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
|
FORGEJO_RELEASE_TAG: ${{ steps.release-identity.outputs.tag }}
|
|
FORGEJO_RELEASE_DIR: release-dist
|
|
run: node scripts/ci/publish-forgejo-release.mjs
|