# .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: - | $ErrorActionPreference = 'Stop' if ([string]::IsNullOrWhiteSpace($env:WIN_CSC_PFX_FILE) -or -not (Test-Path -LiteralPath $env:WIN_CSC_PFX_FILE -PathType Leaf)) { throw 'WIN_CSC_PFX_FILE must be a protected GitLab file variable containing the production code-signing PFX.' } if ((Get-Item -LiteralPath $env:WIN_CSC_PFX_FILE).Length -le 0) { throw 'WIN_CSC_PFX_FILE is empty.' } if ([string]::IsNullOrWhiteSpace($env:WIN_CSC_KEY_PASSWORD)) { throw 'WIN_CSC_KEY_PASSWORD must be a protected, masked GitLab variable.' } if ([string]::IsNullOrWhiteSpace($env:WIN_CSC_EXPECTED_SIGNER_SUBJECT)) { throw 'WIN_CSC_EXPECTED_SIGNER_SUBJECT must be a protected GitLab variable.' } if ($env:WIN_CSC_EXPECTED_SIGNER_SUBJECT -match '(?i)Everything2EverythingDev') { throw 'The local Everything2EverythingDev certificate is not a production signing identity.' } $env:CSC_LINK = (Resolve-Path -LiteralPath $env:WIN_CSC_PFX_FILE).Path $env:CSC_KEY_PASSWORD = $env:WIN_CSC_KEY_PASSWORD - 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 - cd ../.. - $releaseVersion = node -p "require('./release/product-version.json').version" - '& scripts/ci/verify-windows-release-artifact.ps1 -ExpectedVersion $releaseVersion -ExpectedSignerSubject $env:WIN_CSC_EXPECTED_SIGNER_SUBJECT -ReleaseDirectory "apps/desktop/release/$releaseVersion"' 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. # ────────────────────────────────────────────────────────────────────