d3ro-voice/.gitlab-ci.yml
Yun Chan 1af3cf75c7
Some checks failed
deploy-site / deploy (push) Failing after 14m16s
fix(release): stop shipping native modules built for the wrong runtime
The released installer could not start: it carried a better-sqlite3 build for the
host Node runtime instead of Electron, so the app died immediately with a module
version mismatch when it opened its database.

Packaging now proves the Electron build of every runtime-sensitive native module
before an installer or archive exists, and installers are produced only from that
verified tree, so the mistake cannot pass silently. The release pipelines run the
same check.

The default local model also pointed at a retired model: a *.gguf name that
Ollama cannot serve, while the settings, onboarding, and guide screens
recommended an older model. All of them now use the model the service code
already preferred.
2026-09-18 15:45:03 +09:00

509 lines
26 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:
- . scripts/ci/bootstrap-linux-toolchain.sh dotnet
cache:
key: linux-dotnet-sdk-10.0.302
paths:
- .ci-tools/dotnet-sdk-10.0.302/
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:
- . scripts/ci/bootstrap-linux-toolchain.sh deno
cache:
key: linux-deno-2.8.1
paths:
- .ci-tools/deno-2.8.1/
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:
- . scripts/ci/bootstrap-linux-toolchain.sh 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}"
- npm ci
- npm --prefix apps/mobile-rn ci --workspaces=false
cache:
key: linux-android-15859902-jdk-17.0.20.1
paths:
- .ci-tools/android-sdk/
- .ci-tools/microsoft-jdk-17.0.20.1/
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:
- . scripts/ci/bootstrap-linux-toolchain.sh android
cache:
key: linux-android-15859902-jdk-17.0.20.1
paths:
- .ci-tools/android-sdk/
- .ci-tools/microsoft-jdk-17.0.20.1/
script:
- 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"
# 로컬 STT는 faster-whisper 사이드카에 의존한다. 이 번들이 빠지면 설치본에서
# 전사가 전혀 동작하지 않으므로, 패키징 전에 반드시 빌드하고 검증한다.
- npm run sidecar:setup --workspace=@d3ro/desktop
- npm run sidecar:build --workspace=@d3ro/desktop
- node scripts/ci/verify-sidecar-bundle.mjs
- npm run build --workspace=@d3ro/desktop
- cd apps/desktop
- npx electron-builder --win --x64 --config electron-builder.yml
- node scripts/ci/verify-native-abi.mjs
- 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"
# 로컬 STT(faster-whisper 사이드카) 번들 — 누락 시 설치본 전사 불가.
- npm run sidecar:setup --workspace=@d3ro/desktop
- npm run sidecar:build --workspace=@d3ro/desktop
- node scripts/ci/verify-sidecar-bundle.mjs
- npm run build --workspace=@d3ro/desktop
- cd apps/desktop
- npx electron-builder --mac --arm64 --config electron-builder.yml
- node scripts/ci/verify-native-abi.mjs
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 candidate/stable handoff. The main
# branch path only creates restricted artifacts; publish-release remains tag-only.
# 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
. scripts/ci/bootstrap-linux-toolchain.sh 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:refs/remotes/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)"
test "${CI_COMMIT_REF_PROTECTED:-}" = "true"
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")"
if [ -n "${CI_COMMIT_TAG:-}" ]; then
test -z "${CI_COMMIT_BRANCH:-}"
test "$CI_COMMIT_TAG" = "v${VERSION_NAME}"
test "$(git rev-parse "refs/tags/${CI_COMMIT_TAG}^{commit}")" = "$CI_COMMIT_SHA"
RELEASE_GIT_REF="refs/tags/${CI_COMMIT_TAG}"
else
test "${CI_COMMIT_BRANCH:-}" = "main"
test "$CI_COMMIT_SHA" = "$(git rev-parse origin/main)"
RELEASE_GIT_REF="refs/heads/main"
fi
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"
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 "$RELEASE_GIT_REF" \
--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 "$RELEASE_GIT_REF" \
--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
cache:
key: linux-android-15859902-jdk-17.0.20.1
paths:
- .ci-tools/android-sdk/
- .ci-tools/microsoft-jdk-17.0.20.1/
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_REF_PROTECTED == "true" && $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
when: manual
allow_failure: false
- if: '$D3RO_MOBILE_PRODUCTION_RELEASE_ENABLED == "true" && $D3RO_ANDROID_EMULATOR_RUNNER == "true" && $CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_BRANCH == "main"'
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:
# canonical: Forgejo feed + release hub. legacy mirror: GitLab registry.
- node scripts/ci/publish-forgejo-release.mjs
- 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.
# ────────────────────────────────────────────────────────────────────