Packaging silently tolerates a missing resource directory: electron-builder prints "file source doesn't exist" and continues, which is how installers that could not transcribe were published. Every pipeline that packages the desktop app now builds the sidecar and fails when the engine or its VAD data is absent, so a release cannot ship without local transcription.
604 lines
26 KiB
YAML
604 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: Build STT Sidecar (local transcription engine)
|
|
run: |
|
|
# Local transcription depends on the faster-whisper sidecar; a release
|
|
# that ships without it cannot transcribe at all, so build and verify
|
|
# the bundle before packaging.
|
|
npm run sidecar:setup --workspace=@d3ro/desktop
|
|
npm run sidecar:build --workspace=@d3ro/desktop
|
|
node scripts/ci/verify-sidecar-bundle.mjs
|
|
|
|
- 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: Build STT Sidecar (local transcription engine)
|
|
run: |
|
|
# Local transcription depends on the faster-whisper sidecar; a release
|
|
# that ships without it cannot transcribe at all, so build and verify
|
|
# the bundle before packaging.
|
|
npm run sidecar:setup --workspace=@d3ro/desktop
|
|
npm run sidecar:build --workspace=@d3ro/desktop
|
|
node scripts/ci/verify-sidecar-bundle.mjs
|
|
|
|
- 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
|