From 6a9608c447f42dd51fbc51c42554402959191995 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sat, 29 Aug 2026 19:19:19 +0900 Subject: [PATCH] ci: bootstrap Linux release toolchains --- .gitignore | 1 + .gitlab-ci.yml | 42 ++++++-- apps/desktop/tests/setup.ts | 24 +++++ docs/deployment/release-guide.md | 1 + scripts/ci/bootstrap-linux-toolchain.sh | 135 ++++++++++++++++++++++++ 5 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 scripts/ci/bootstrap-linux-toolchain.sh diff --git a/.gitignore b/.gitignore index 31aad85..fbc6053 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ release/ !/release/android-release-identity.json !/release/mobile-release-evidence-public.pem *.unpacked +/.ci-tools/ # Build *.tsbuildinfo diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 112a7fd..efafcb9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -63,7 +63,12 @@ test-unit: api-server-tests: stage: test image: mcr.microsoft.com/dotnet/sdk:10.0.302-noble - before_script: [] + 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 @@ -75,7 +80,12 @@ api-server-tests: edge-functions-quality: stage: test image: denoland/deno:2.8.1 - before_script: [] + 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 @@ -104,8 +114,7 @@ mobile-android: needs: - mobile-quality before_script: - - export ANDROID_HOME=/opt/android - - export ANDROID_SDK_ROOT=/opt/android + - . 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 @@ -114,9 +123,13 @@ mobile-android: - 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 + 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 @@ -151,10 +164,14 @@ mobile-emulator-e2e: needs: - job: mobile-android artifacts: true - before_script: [] + 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: - - 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 & @@ -296,8 +313,7 @@ mobile-production-release: script: - | set -euo pipefail - export ANDROID_HOME=/opt/android - export ANDROID_SDK_ROOT=/opt/android + . 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 @@ -331,7 +347,6 @@ mobile-production-release: 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 @@ -414,6 +429,11 @@ mobile-production-release: 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: diff --git a/apps/desktop/tests/setup.ts b/apps/desktop/tests/setup.ts index be599a0..5cff54f 100644 --- a/apps/desktop/tests/setup.ts +++ b/apps/desktop/tests/setup.ts @@ -76,6 +76,30 @@ vi.mock('electron', () => ({ decryptString: vi.fn((b: Buffer) => b.toString('utf-8')), }, Notification: vi.fn(), + globalShortcut: { + register: vi.fn(() => true), + unregister: vi.fn(), + unregisterAll: vi.fn(), + isRegistered: vi.fn(() => false), + }, +})) + +// Unit tests must not load the native global keyboard hook. Linux shell runners +// intentionally do not provide an X11 session or libXrandr, and product-level +// hook behaviour is covered by runtime/E2E validation instead. +vi.mock('uiohook-napi', () => ({ + uIOhook: { + on: vi.fn(), + removeListener: vi.fn(), + start: vi.fn(), + stop: vi.fn(), + }, + UiohookKey: new Proxy>({}, { + get: (_target, property) => Array.from(String(property)).reduce( + (value, character) => ((value * 31) + character.charCodeAt(0)) & 0x7fffffff, + 17, + ), + }), })) // WindowManager 모킹 — VoiceModeService가 import하므로 필요 diff --git a/docs/deployment/release-guide.md b/docs/deployment/release-guide.md index 427f8cf..22f22c0 100644 --- a/docs/deployment/release-guide.md +++ b/docs/deployment/release-guide.md @@ -41,6 +41,7 @@ authoritative release commit - Windows installer와 `latest.yml`은 필수다. macOS 산출물이 없는 Windows-only release를 의도했다면 그 판단을 release record에 남긴다. - `package-windows`는 external public-trust code-signing PFX를 protected file variable `WIN_CSC_PFX_FILE`로, 암호와 exact certificate subject를 protected `WIN_CSC_KEY_PASSWORD`, `WIN_CSC_EXPECTED_SIGNER_SUBJECT`로 받아야 한다. GitHub Release도 `WIN_CSC_LINK`, `WIN_CSC_KEY_PASSWORD`, `WIN_CSC_EXPECTED_SIGNER_SUBJECT`가 모두 없으면 실패한다. - `scripts/ci/verify-windows-release-artifact.ps1`는 installer와 unpacked app의 Authenticode `Valid`, exact signer subject, non-self-signed code-signing EKU, PE version, `latest.yml` path/size/SHA-512를 검증한다. 로컬 self-signed `Everything2EverythingDev`는 production 신뢰 인증서가 아니며 gate에서 명시적으로 거부한다. +- 현재 GitLab Linux runner는 Shell executor이므로 job의 `image:`를 툴체인 근거로 간주하지 않는다. `.gitlab-ci.yml`은 `scripts/ci/bootstrap-linux-toolchain.sh`를 source해 .NET 10.0.302, Deno 2.8.1, Microsoft OpenJDK 17.0.20.1, Android command-line tools 15859902를 고정 해시로 검증·캐시한 뒤 실행한다. ## 자동 업데이트 계약 diff --git a/scripts/ci/bootstrap-linux-toolchain.sh b/scripts/ci/bootstrap-linux-toolchain.sh new file mode 100644 index 0000000..8e99935 --- /dev/null +++ b/scripts/ci/bootstrap-linux-toolchain.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash + +# GitLab's current Linux runner uses the Shell executor, so job-level `image:` +# declarations do not provide their advertised SDKs. Source this file from a +# job to install a pinned, checksum-verified toolchain inside CI_PROJECT_DIR. + +set -euo pipefail + +mode="${1:-}" +if [[ -z "$mode" ]]; then + echo 'bootstrap-linux-toolchain: mode is required (dotnet|deno|android)' >&2 + return 64 2>/dev/null || exit 64 +fi + +: "${CI_PROJECT_DIR:?CI_PROJECT_DIR is required}" +tools_root="$CI_PROJECT_DIR/.ci-tools" +mkdir -p "$tools_root" + +download_verified() { + local url="$1" + local expected_checksum="$2" + local destination="$3" + + curl --fail --silent --show-error --location "$url" --output "$destination" + case "${#expected_checksum}" in + 64) printf '%s %s\n' "$expected_checksum" "$destination" | sha256sum --check --status ;; + 128) printf '%s %s\n' "$expected_checksum" "$destination" | sha512sum --check --status ;; + *) + echo 'bootstrap-linux-toolchain: unsupported checksum length' >&2 + return 65 + ;; + esac +} + +bootstrap_dotnet() { + export DOTNET_ROOT="$tools_root/dotnet-sdk-10.0.302" + if [[ ! -x "$DOTNET_ROOT/dotnet" ]]; then + local archive + archive="$(mktemp "${TMPDIR:-/tmp}/d3ro-dotnet.XXXXXX.tar.gz")" + download_verified \ + 'https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.302/dotnet-sdk-10.0.302-linux-x64.tar.gz' \ + '10069bec8783596484a610332f090d562802a41b9b40e3327a5a5688b572e10c296ae300f940d40461f23c157ed1b0843c2f8e6b3f20d8d8d9d83432d8143bac' \ + "$archive" + mkdir -p "$DOTNET_ROOT" + tar --extract --gzip --file "$archive" --directory "$DOTNET_ROOT" + rm -f "$archive" + fi + export DOTNET_CLI_TELEMETRY_OPTOUT=1 + export DOTNET_NOLOGO=1 + export PATH="$DOTNET_ROOT:$PATH" + [[ "$(dotnet --version)" == '10.0.302' ]] +} + +bootstrap_deno() { + local deno_root="$tools_root/deno-2.8.1" + if [[ ! -x "$deno_root/deno" ]]; then + local archive + archive="$(mktemp "${TMPDIR:-/tmp}/d3ro-deno.XXXXXX.zip")" + download_verified \ + 'https://github.com/denoland/deno/releases/download/v2.8.1/deno-x86_64-unknown-linux-gnu.zip' \ + '2d7bb6195226ac832e0bf7109a115f0af65ee69ac797a4bbde5b27a06cc242d9' \ + "$archive" + mkdir -p "$deno_root" + unzip -q "$archive" -d "$deno_root" + rm -f "$archive" + fi + export PATH="$deno_root:$PATH" + local deno_version + deno_version="$(deno --version)" + [[ "$deno_version" == deno\ 2.8.1* ]] +} + +bootstrap_android() { + export JAVA_HOME="$tools_root/microsoft-jdk-17.0.20.1" + if [[ ! -x "$JAVA_HOME/bin/java" ]]; then + local jdk_archive + jdk_archive="$(mktemp "${TMPDIR:-/tmp}/d3ro-jdk.XXXXXX.tar.gz")" + download_verified \ + 'https://download.visualstudio.microsoft.com/download/pr/a9672056-0a4d-425d-8488-f481ee56319c/5897054114dd5c0acab8489cd815dad9/microsoft-jdk-17.0.20.1-linux-x64.tar.gz' \ + 'd00e5b04e9726b63d915706c7049e5297c9f40239ce8a12fcc68b7267fa91ad2' \ + "$jdk_archive" + mkdir -p "$JAVA_HOME" + tar --extract --gzip --file "$jdk_archive" --directory "$JAVA_HOME" --strip-components=1 + rm -f "$jdk_archive" + fi + export PATH="$JAVA_HOME/bin:$PATH" + local java_version + java_version="$(java -version 2>&1)" + [[ "$java_version" == *'17.0.20.1'* ]] + + export ANDROID_HOME="$tools_root/android-sdk" + export ANDROID_SDK_ROOT="$ANDROID_HOME" + local sdkmanager_path="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" + if [[ ! -x "$sdkmanager_path" ]]; then + local cli_archive cli_stage + cli_archive="$(mktemp "${TMPDIR:-/tmp}/d3ro-android-cli.XXXXXX.zip")" + cli_stage="$(mktemp -d "${TMPDIR:-/tmp}/d3ro-android-cli.XXXXXX")" + download_verified \ + 'https://dl.google.com/android/repository/commandlinetools-linux-15859902_latest.zip' \ + '4e4c464f145a7512b57d088ac6c278c03c9eea610886b35a5e0804e74eedf583' \ + "$cli_archive" + unzip -q "$cli_archive" -d "$cli_stage" + mkdir -p "$ANDROID_HOME/cmdline-tools" + mv "$cli_stage/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest" + rmdir "$cli_stage" + rm -f "$cli_archive" + fi + export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH" + sdkmanager --version >/dev/null + + if [[ ! -f "$ANDROID_HOME/platforms/android-36/android.jar" \ + || ! -x "$ANDROID_HOME/build-tools/36.0.0/aapt2" \ + || ! -x "$ANDROID_HOME/platform-tools/adb" ]]; then + set +o pipefail + yes | sdkmanager --licenses >/dev/null + local license_status=$? + set -o pipefail + [[ "$license_status" -eq 0 ]] + sdkmanager 'platform-tools' 'platforms;android-36' 'build-tools;36.0.0' >/dev/null + fi + + [[ -f "$ANDROID_HOME/platforms/android-36/android.jar" ]] + [[ -x "$ANDROID_HOME/build-tools/36.0.0/aapt2" ]] + [[ -x "$ANDROID_HOME/platform-tools/adb" ]] +} + +case "$mode" in + dotnet) bootstrap_dotnet ;; + deno) bootstrap_deno ;; + android) bootstrap_android ;; + *) + echo "bootstrap-linux-toolchain: unsupported mode: $mode" >&2 + return 64 2>/dev/null || exit 64 + ;; +esac