d3ro-voice/.gitlab-ci.yml
2026-08-29 18:33:45 +09:00

440 lines
22 KiB
YAML

# .gitlab-ci.yml
# GitLab CI/CD Pipeline for D3RO Voice
stages:
- validate
- test
- build
- e2e
- package
- publish
- deploy
variables:
NODE_VERSION: "24.19.0"
PACKAGE_NAME: "d3ro-voice"
default:
image: node:24.19.0-bookworm
tags:
- build-linux-x64
before_script:
- npm ci
# ────────────────────────────────────────────────────────────────────
# Validate & Lint
# ────────────────────────────────────────────────────────────────────
lint-and-typecheck:
stage: validate
script:
- npm run version:check
- if [ -n "${CI_COMMIT_TAG:-}" ]; then node scripts/ci/sync-version.mjs --check --tag "$CI_COMMIT_TAG"; 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
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"'
- if: '$CI_COMMIT_TAG'
# ────────────────────────────────────────────────────────────────────
# Unit & Integration Tests
# ────────────────────────────────────────────────────────────────────
test-unit:
stage: test
script:
- npm test
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
when: always
reports:
junit: junit.xml
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"'
- if: '$CI_COMMIT_TAG'
api-server-tests:
stage: test
image: mcr.microsoft.com/dotnet/sdk:10.0.302-noble
before_script: []
script:
- 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
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"'
- if: '$CI_COMMIT_TAG'
edge-functions-quality:
stage: test
image: denoland/deno:2.8.1
before_script: []
script:
- 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
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"'
- if: '$CI_COMMIT_TAG'
mobile-quality:
stage: test
before_script:
- npm ci
- npm --prefix apps/mobile-rn ci --workspaces=false
script:
- npm --prefix apps/mobile-rn run lint
- npm --prefix apps/mobile-rn run typecheck
- npm --prefix apps/mobile-rn run test:ci
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"'
- if: '$CI_COMMIT_TAG'
mobile-android:
stage: build
image: reactnativecommunity/react-native-android@sha256:24ca7ab5a70ec0b78a81bdc5eeea5924c2531531d53971b6f2321aff08446c36
needs:
- mobile-quality
before_script:
- export ANDROID_HOME=/opt/android
- export ANDROID_SDK_ROOT=/opt/android
- curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" -o /tmp/node.tar.xz
- echo '14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 /tmp/node.tar.xz' | sha256sum -c -
- mkdir -p /tmp/node24
- tar -xJf /tmp/node.tar.xz -C /tmp/node24 --strip-components=1
- export PATH="/tmp/node24/bin:$PATH"
- node --version | grep -Fx "v${NODE_VERSION}"
- export D3RO_VERSION_NAME="0.0.0-e2e.${CI_PIPELINE_IID}"
- export D3RO_VERSION_CODE="${CI_PIPELINE_IID}"
- sdkmanager "platforms;android-36" "build-tools;36.0.0" >/dev/null
- npm ci
- npm --prefix apps/mobile-rn ci --workspaces=false
script:
- node scripts/ci/prepare-whisper-model.mjs
- cd apps/mobile-rn/android
- ./gradlew :app:assembleDebug :app:assembleDebugAndroidTest :app:assembleE2e -PreactNativeArchitectures=arm64-v8a,x86_64 --no-daemon
- cd "$CI_PROJECT_DIR"
- node scripts/ci/verify-mobile-build-config.mjs debug > apps/mobile-rn/android/app/build/outputs/debug-build-config.json
- node scripts/ci/verify-mobile-build-config.mjs e2e > apps/mobile-rn/android/app/build/outputs/e2e-build-config.json
- node scripts/ci/verify-android-artifact.mjs --mode debug --apk apps/mobile-rn/android/app/build/outputs/apk/debug/app-debug.apk > apps/mobile-rn/android/app/build/outputs/debug-artifact-evidence.json
- node scripts/ci/verify-android-artifact.mjs --mode e2e --apk apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk --expected-version-name "$D3RO_VERSION_NAME" --expected-version-code "$D3RO_VERSION_CODE" > apps/mobile-rn/android/app/build/outputs/e2e-artifact-evidence.json
- sha256sum apps/mobile-rn/android/app/build/outputs/apk/debug/app-debug.apk apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk > apps/mobile-rn/android/app/build/outputs/android-ci.sha256
artifacts:
when: always
expire_in: 1 day
paths:
- apps/mobile-rn/android/app/build/outputs/apk/debug/app-debug.apk
- apps/mobile-rn/android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
- apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk
- apps/mobile-rn/android/app/build/outputs/android-ci.sha256
- apps/mobile-rn/android/app/build/outputs/*-build-config.json
- apps/mobile-rn/android/app/build/outputs/*-artifact-evidence.json
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"'
- if: '$CI_COMMIT_TAG'
mobile-emulator-e2e:
stage: e2e
image: reactnativecommunity/react-native-android@sha256:24ca7ab5a70ec0b78a81bdc5eeea5924c2531531d53971b6f2321aff08446c36
tags:
- android-emulator
- kvm
needs:
- job: mobile-android
artifacts: true
before_script: []
script:
- export ANDROID_HOME=/opt/android
- export ANDROID_SDK_ROOT=/opt/android
- sdkmanager "platform-tools" "emulator" "platforms;android-35" "system-images;android-35;google_apis;x86_64" >/dev/null
- echo no | avdmanager create avd --force --name d3ro_ci_api35 --package "system-images;android-35;google_apis;x86_64"
- emulator -avd d3ro_ci_api35 -no-window -noaudio -no-boot-anim -gpu swiftshader_indirect -camera-back none &
- adb wait-for-device
- timeout 180 bash -c 'until [[ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d "\r")" == "1" ]]; do sleep 2; done'
- curl -fsSL https://github.com/mobile-dev-inc/maestro/releases/download/cli-2.7.0/maestro.zip -o /tmp/maestro.zip
- echo 'a4ccab6b604617e7aef6db4f885666056eabe5cfa32befaa3bc994041b8fcbb5 /tmp/maestro.zip' | sha256sum -c -
- unzip -q /tmp/maestro.zip -d /tmp/maestro
- export PATH="/tmp/maestro/maestro/bin:$PATH"
- bash scripts/ci/run-mobile-csprng-instrumentation.sh apps/mobile-rn/android/app/build/outputs/apk/debug/app-debug.apk apps/mobile-rn/android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
- bash scripts/ci/run-mobile-emulator-gate.sh apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk
artifacts:
when: always
expire_in: 7 days
reports:
junit: apps/mobile-rn/.maestro/*.junit.xml
paths:
- apps/mobile-rn/.maestro/*.junit.xml
- apps/mobile-rn/.maestro-output/
rules:
- if: '$D3RO_ANDROID_EMULATOR_RUNNER == "true" && $CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$D3RO_ANDROID_EMULATOR_RUNNER == "true" && ($CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop")'
- if: '$D3RO_ANDROID_EMULATOR_RUNNER == "true" && $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
# ────────────────────────────────────────────────────────────────────
# Build Workspaces
# ────────────────────────────────────────────────────────────────────
build-workspaces:
stage: build
script:
- npm run build --workspace=@d3ro/desktop
- npm run build --workspace=@d3ro/admin
artifacts:
paths:
- apps/desktop/out/
- apps/admin/.next/
expire_in: 1 day
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_TAG'
# ────────────────────────────────────────────────────────────────────
# Package Windows (Windows Runner)
# ────────────────────────────────────────────────────────────────────
package-windows:
stage: package
tags:
- build-win-x64
before_script:
- npm ci
script:
- node scripts/ci/sync-version.mjs --check --tag "$CI_COMMIT_TAG"
- npm run build --workspace=@d3ro/desktop
- cd apps/desktop
- npx electron-builder --win --x64 --config electron-builder.yml
artifacts:
name: "d3ro-voice-windows-$CI_COMMIT_TAG"
paths:
- apps/desktop/release/*/*.exe
- apps/desktop/release/*/*.blockmap
- apps/desktop/release/*/latest.yml
expire_in: 7 days
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+.*$/'
# ────────────────────────────────────────────────────────────────────
# Package macOS (macOS Runner)
# ────────────────────────────────────────────────────────────────────
package-macos:
stage: package
tags:
- build-mac-arm64
before_script:
- npm ci
script:
- node scripts/ci/sync-version.mjs --check --tag "$CI_COMMIT_TAG"
- npm run build --workspace=@d3ro/desktop
- cd apps/desktop
- npx electron-builder --mac --arm64 --config electron-builder.yml
artifacts:
name: "d3ro-voice-macos-$CI_COMMIT_TAG"
paths:
- apps/desktop/release/*/*.dmg
- apps/desktop/release/*/*.zip
- apps/desktop/release/*/*.blockmap
- apps/desktop/release/*/latest-mac.yml
expire_in: 7 days
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+.*$/'
# Protected, manually approved Play Console handoff. Configure the three
# *_FILE variables below as protected GitLab file variables scoped to the
# mobile-production-release environment. All other credentials must be
# protected, masked, and hidden project/group variables.
mobile-production-release:
stage: package
image: reactnativecommunity/react-native-android@sha256:24ca7ab5a70ec0b78a81bdc5eeea5924c2531531d53971b6f2321aff08446c36
needs:
- job: lint-and-typecheck
artifacts: false
- job: test-unit
artifacts: false
- job: api-server-tests
artifacts: false
- job: edge-functions-quality
artifacts: false
- job: mobile-quality
artifacts: false
- job: mobile-android
artifacts: false
- job: mobile-emulator-e2e
artifacts: false
environment:
name: mobile-production-release
action: prepare
before_script: []
script:
- |
set -euo pipefail
export ANDROID_HOME=/opt/android
export ANDROID_SDK_ROOT=/opt/android
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" -o /tmp/node.tar.xz
echo '14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 /tmp/node.tar.xz' | sha256sum -c -
mkdir -p /tmp/node24
tar -xJf /tmp/node.tar.xz -C /tmp/node24 --strip-components=1
export PATH="/tmp/node24/bin:$PATH"
node --version | grep -Fx "v${NODE_VERSION}"
git fetch --no-tags origin main
git merge-base --is-ancestor "$CI_COMMIT_SHA" origin/main
test "$CI_COMMIT_SHA" = "$(git rev-parse HEAD)"
test -z "$(git status --porcelain --untracked-files=all)"
SOURCE_TREE_SHA="$(git rev-parse "${CI_COMMIT_SHA}^{tree}")"
VERSION_NAME="$(node -p "require('./release/product-version.json').version")"
VERSION_CODE="$(node -p "require('./release/product-version.json').androidVersionCode")"
ANDROID_UPLOAD_CERT_SHA256="$(node -p "require('./release/android-release-identity.json').uploadCertificateSha256")"
ADMOB_APP_ID="$(node -p "require('./release/android-release-identity.json').adMobAppId")"
ADMOB_BANNER_UNIT_ID="$(node -p "require('./release/android-release-identity.json').adMobBannerUnitId")"
ADMOB_REWARDED_UNIT_ID="$(node -p "require('./release/android-release-identity.json').adMobRewardedUnitId")"
test "$CI_COMMIT_TAG" = "v${VERSION_NAME}"
echo "$VERSION_NAME" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
echo "$VERSION_CODE" | grep -Eq '^[1-9][0-9]{0,9}$'
test "$VERSION_CODE" -le 2100000000
test -s "$ANDROID_RELEASE_KEYSTORE_FILE"
test -s "$ANDROID_GOOGLE_SERVICES_JSON_FILE"
test -s "$ANDROID_RELEASE_EVIDENCE_PRIVATE_KEY_FILE"
install -m 600 "$ANDROID_RELEASE_KEYSTORE_FILE" apps/mobile-rn/android/app/release.keystore
install -m 600 "$ANDROID_GOOGLE_SERVICES_JSON_FILE" apps/mobile-rn/android/app/google-services.json
install -m 600 "$ANDROID_RELEASE_EVIDENCE_PRIVATE_KEY_FILE" apps/mobile-rn/android/app/release-evidence-private.pem
test -n "$ANDROID_RELEASE_STORE_PASSWORD"
test -n "$ANDROID_RELEASE_KEY_ALIAS"
test -n "$ANDROID_RELEASE_KEY_PASSWORD"
test -n "$D3RO_FIREBASE_EXPECTED_PROJECT_ID"
test -n "$D3RO_FIREBASE_EXPECTED_PROJECT_NUMBER"
test -n "$D3RO_FIREBASE_EXPECTED_MOBILESDK_APP_ID"
sdkmanager "platforms;android-36" "build-tools;36.0.0" >/dev/null
npm ci
npm --prefix apps/mobile-rn ci --workspaces=false
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 --prefix apps/mobile-rn run lint
npm --prefix apps/mobile-rn run typecheck
npm --prefix apps/mobile-rn run test:ci
node scripts/ci/prepare-whisper-model.mjs
curl --fail --silent --show-error --location --output /tmp/bundletool.jar https://github.com/google/bundletool/releases/download/1.18.3/bundletool-all-1.18.3.jar
echo 'a099cfa1543f55593bc2ed16a70a7c67fe54b1747bb7301f37fdfd6d91028e29 /tmp/bundletool.jar' | sha256sum -c -
export D3RO_RELEASE_STORE_FILE="$CI_PROJECT_DIR/apps/mobile-rn/android/app/release.keystore"
export D3RO_RELEASE_STORE_PASSWORD="$ANDROID_RELEASE_STORE_PASSWORD"
export D3RO_RELEASE_KEY_ALIAS="$ANDROID_RELEASE_KEY_ALIAS"
export D3RO_RELEASE_KEY_PASSWORD="$ANDROID_RELEASE_KEY_PASSWORD"
export D3RO_ADMOB_APP_ID="$ADMOB_APP_ID"
export D3RO_ADMOB_BANNER_UNIT_ID="$ADMOB_BANNER_UNIT_ID"
export D3RO_ADMOB_REWARDED_UNIT_ID="$ADMOB_REWARDED_UNIT_ID"
export D3RO_VERSION_NAME="$VERSION_NAME"
export D3RO_VERSION_CODE="$VERSION_CODE"
PLAY_APP_SIGNING_CERT_SHA256="$(node -p "require('./release/android-release-identity.json').playAppSigningCertificateSha256")"
npm run release:mobile:config
cd apps/mobile-rn/android
./gradlew :app:assembleRelease :app:bundleRelease -PreactNativeArchitectures=arm64-v8a --no-daemon
cd "$CI_PROJECT_DIR"
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 > apps/mobile-rn/android/app/build/outputs/release-build-config.json
node scripts/ci/create-mobile-release-evidence.mjs \
--apk "$APK" \
--aab "$AAB" \
--bundletool /tmp/bundletool.jar \
--repository "$CI_PROJECT_PATH" \
--commit-sha "$CI_COMMIT_SHA" \
--tree-sha "$SOURCE_TREE_SHA" \
--git-ref "refs/tags/$CI_COMMIT_TAG" \
--workflow-identity "gitlab-ci/mobile-production-release" \
--run-id "$CI_JOB_ID" \
--run-attempt "1" \
--runner-identity "$CI_RUNNER_ID:$CI_RUNNER_REVISION" \
--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 "$CI_PROJECT_PATH" \
--expected-commit-sha "$CI_COMMIT_SHA" \
--expected-tree-sha "$SOURCE_TREE_SHA" \
--expected-git-ref "refs/tags/$CI_COMMIT_TAG" \
--expected-workflow-identity "gitlab-ci/mobile-production-release" \
--expected-run-id "$CI_JOB_ID" \
--expected-run-attempt "1" \
--expected-runner-identity "$CI_RUNNER_ID:$CI_RUNNER_REVISION" \
--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" \
> 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 \
> apps/mobile-rn/android/app/build/outputs/release-publication/SHA256SUMS.txt
after_script:
- 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
artifacts:
access: maintainer
expire_in: 7 days
paths:
- 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
manual_confirmation: '검증된 production AAB를 생성해 Maintainer 전용 Play Console handoff로 보낼까요?'
rules:
- if: '$D3RO_MOBILE_PRODUCTION_RELEASE_ENABLED == "true" && $D3RO_ANDROID_EMULATOR_RUNNER == "true" && $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
when: manual
allow_failure: false
- when: never
# ────────────────────────────────────────────────────────────────────
# Publish Release (GitLab Package Registry + Release Page)
# ────────────────────────────────────────────────────────────────────
publish-release:
stage: publish
image: node:24.19.0-bookworm
needs:
- job: package-windows
artifacts: true
- job: package-macos
artifacts: true
optional: true
- job: mobile-production-release
artifacts: false
optional: true
script:
- node scripts/ci/publish-gitlab-release.mjs
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+.*$/'
# ────────────────────────────────────────────────────────────────────
# Admin NAS deployment remains disabled until an immutable image, authenticated
# remote target, health check, and rollback path are configured. Do not revive
# the former DinD job: it only changed an ephemeral CI daemon and referenced a
# non-existent compose file, so it never deployed the NAS.
# ────────────────────────────────────────────────────────────────────