d3ro-voice/.github/workflows/release.yml
2026-08-29 18:33:45 +09:00

553 lines
24 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)
run: |
cd apps/desktop
npx electron-builder --win --x64 --config electron-builder.yml
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
- 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 }}