ci: run CI only on Forgejo and delete the never-run GitHub workflows (WS-D)
The repository has Forgejo and GitLab remotes but no GitHub remote, so nothing under .github/workflows ever ran - including the daily Payple renewal job, which means Payple subscriptions were not being renewed. - Port payple-renew (daily cron + manual dispatch) to Forgejo. Requires the Forgejo secrets SUPABASE_URL and CRON_SECRET. - Port the CI checks (quality incl. version:check and contract:check, build, mobile quality, edge functions, API tests) to .forgejo/workflows/ci.yml. macOS and Android emulator jobs are dropped: no macOS runner here, and the signed Android release stays on GitLab mobile-production-release. - Keep one site deploy workflow (Linux, Cloudflare Pages); remove the manual Windows duplicate. The mobile release boundary self-test runs there. - Release verifiers read the Forgejo/GitLab workflows, fail if .github workflows come back, and check the rewritten site offers no mobile package. Policy: docs/REFACTOR_POLICY.md Wave 3, W3-7 and W3-8.
This commit is contained in:
parent
cd9d199dbf
commit
dc43884e3e
13 changed files with 269 additions and 1370 deletions
188
.forgejo/workflows/ci.yml
Normal file
188
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
name: ci
|
||||||
|
|
||||||
|
# 브랜치·PR 검증. 예전 .github/workflows/ci.yml(GitHub 원격이 없어 한 번도 실행되지 않음)에서
|
||||||
|
# 자체 호스팅 linux-builder 러너로 돌릴 수 있는 잡만 옮겼다.
|
||||||
|
#
|
||||||
|
# 옮기지 않은 잡:
|
||||||
|
# - test-matrix의 windows/macos 칸: macOS 러너가 없고, windows 러너는 태그 릴리스 전용으로 둔다.
|
||||||
|
# - mobile-android APK 빌드 / mobile-emulator-e2e: Android SDK + KVM 에뮬레이터가 필요하다.
|
||||||
|
# GitLab 미러(.gitlab-ci.yml mobile-android, mobile-emulator-e2e)가 계속 담당한다.
|
||||||
|
#
|
||||||
|
# 러너 관례: actions/checkout·setup-* 없이 github.token으로 직접 fetch 한다(다른 .forgejo 워크플로와 동일).
|
||||||
|
# Deno/.NET은 scripts/ci/bootstrap-linux-toolchain.sh 가 체크섬 검증 후 설치한다.
|
||||||
|
# git clean -fdx 로 지워지지 않도록 도구 캐시는 체크아웃 밖(CI_PROJECT_DIR)에 둔다.
|
||||||
|
#
|
||||||
|
# 필요한 시크릿: 없음 (github.token 만 사용)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
- "feature/**"
|
||||||
|
- "fix/**"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
name: 정본·보안·린트·타입·테스트
|
||||||
|
runs-on: linux-builder
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
env: { CI_TOKEN: "${{ github.token }}" }
|
||||||
|
run: |
|
||||||
|
proto="${GITHUB_SERVER_URL%%://*}"; host="${GITHUB_SERVER_URL#*://}"
|
||||||
|
url="$proto://actions:${CI_TOKEN}@${host%/}/${GITHUB_REPOSITORY}.git"
|
||||||
|
[ -d .git ] || git init -q .
|
||||||
|
git remote remove origin 2>/dev/null || true
|
||||||
|
git remote add origin "$url"
|
||||||
|
git fetch -q --depth 1 origin "$GITHUB_REF"
|
||||||
|
git checkout -q -f FETCH_HEAD
|
||||||
|
git clean -qfdx
|
||||||
|
|
||||||
|
- name: 도구 버전
|
||||||
|
run: |
|
||||||
|
echo "node $(node --version) (.nvmrc $(cat .nvmrc)) / npm $(npm --version)"
|
||||||
|
|
||||||
|
- name: 의존성 설치
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: 버전·계약 정본 대조
|
||||||
|
run: |
|
||||||
|
npm run version:check
|
||||||
|
npm run contract:check
|
||||||
|
npm run release:metadata:test
|
||||||
|
|
||||||
|
- name: 하드코딩 자격증명 검사
|
||||||
|
run: |
|
||||||
|
npm run security:secrets:test
|
||||||
|
npm run security:secrets
|
||||||
|
|
||||||
|
- name: 모바일 릴리스 경계·설정 계약
|
||||||
|
run: |
|
||||||
|
npm run release:mobile:boundary:test
|
||||||
|
npm run release:mobile:config:test
|
||||||
|
npm run release:mobile:build-config:test
|
||||||
|
npm run release:play:assets
|
||||||
|
|
||||||
|
- name: 린트
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: 타입 검사
|
||||||
|
run: npm run typecheck
|
||||||
|
|
||||||
|
- name: 테스트 (Vitest)
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
build-validation:
|
||||||
|
name: 워크스페이스 빌드 검증
|
||||||
|
needs: quality
|
||||||
|
runs-on: linux-builder
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
env: { CI_TOKEN: "${{ github.token }}" }
|
||||||
|
run: |
|
||||||
|
proto="${GITHUB_SERVER_URL%%://*}"; host="${GITHUB_SERVER_URL#*://}"
|
||||||
|
url="$proto://actions:${CI_TOKEN}@${host%/}/${GITHUB_REPOSITORY}.git"
|
||||||
|
[ -d .git ] || git init -q .
|
||||||
|
git remote remove origin 2>/dev/null || true
|
||||||
|
git remote add origin "$url"
|
||||||
|
git fetch -q --depth 1 origin "$GITHUB_REF"
|
||||||
|
git checkout -q -f FETCH_HEAD
|
||||||
|
git clean -qfdx
|
||||||
|
|
||||||
|
- name: 의존성 설치
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: 데스크톱 번들 빌드 + 렌더러 번들 검증
|
||||||
|
run: |
|
||||||
|
npm run build --workspace=@d3ro/desktop
|
||||||
|
node scripts/ci/verify-desktop-renderer-bundles.mjs
|
||||||
|
|
||||||
|
- name: 어드민 빌드
|
||||||
|
run: npm run build --workspace=@d3ro/admin
|
||||||
|
|
||||||
|
mobile-quality:
|
||||||
|
name: 모바일 린트·타입·Jest
|
||||||
|
runs-on: linux-builder
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
env: { CI_TOKEN: "${{ github.token }}" }
|
||||||
|
run: |
|
||||||
|
proto="${GITHUB_SERVER_URL%%://*}"; host="${GITHUB_SERVER_URL#*://}"
|
||||||
|
url="$proto://actions:${CI_TOKEN}@${host%/}/${GITHUB_REPOSITORY}.git"
|
||||||
|
[ -d .git ] || git init -q .
|
||||||
|
git remote remove origin 2>/dev/null || true
|
||||||
|
git remote add origin "$url"
|
||||||
|
git fetch -q --depth 1 origin "$GITHUB_REF"
|
||||||
|
git checkout -q -f FETCH_HEAD
|
||||||
|
git clean -qfdx
|
||||||
|
|
||||||
|
- name: 의존성 설치
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm --prefix apps/mobile-rn ci --workspaces=false
|
||||||
|
|
||||||
|
- name: 모바일 검사
|
||||||
|
run: |
|
||||||
|
npm --prefix apps/mobile-rn run lint
|
||||||
|
npm --prefix apps/mobile-rn run typecheck
|
||||||
|
npm --prefix apps/mobile-rn run test:ci
|
||||||
|
|
||||||
|
edge-functions:
|
||||||
|
name: Supabase Edge Functions + Cloudflare Worker
|
||||||
|
runs-on: linux-builder
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
env: { CI_TOKEN: "${{ github.token }}" }
|
||||||
|
run: |
|
||||||
|
proto="${GITHUB_SERVER_URL%%://*}"; host="${GITHUB_SERVER_URL#*://}"
|
||||||
|
url="$proto://actions:${CI_TOKEN}@${host%/}/${GITHUB_REPOSITORY}.git"
|
||||||
|
[ -d .git ] || git init -q .
|
||||||
|
git remote remove origin 2>/dev/null || true
|
||||||
|
git remote add origin "$url"
|
||||||
|
git fetch -q --depth 1 origin "$GITHUB_REF"
|
||||||
|
git checkout -q -f FETCH_HEAD
|
||||||
|
git clean -qfdx
|
||||||
|
|
||||||
|
- name: Deno 2.8.1 설치 후 검사·테스트
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
export CI_PROJECT_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/d3ro-ci"
|
||||||
|
. scripts/ci/bootstrap-linux-toolchain.sh deno
|
||||||
|
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
|
||||||
|
deno check --no-config server/cloudflare-worker/src/push-drain.ts
|
||||||
|
deno test --no-config --allow-read server/cloudflare-worker/src/push-drain.test.ts
|
||||||
|
|
||||||
|
api-server-tests:
|
||||||
|
name: .NET API 서버 테스트
|
||||||
|
runs-on: linux-builder
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
env: { CI_TOKEN: "${{ github.token }}" }
|
||||||
|
run: |
|
||||||
|
proto="${GITHUB_SERVER_URL%%://*}"; host="${GITHUB_SERVER_URL#*://}"
|
||||||
|
url="$proto://actions:${CI_TOKEN}@${host%/}/${GITHUB_REPOSITORY}.git"
|
||||||
|
[ -d .git ] || git init -q .
|
||||||
|
git remote remove origin 2>/dev/null || true
|
||||||
|
git remote add origin "$url"
|
||||||
|
git fetch -q --depth 1 origin "$GITHUB_REF"
|
||||||
|
git checkout -q -f FETCH_HEAD
|
||||||
|
git clean -qfdx
|
||||||
|
|
||||||
|
- name: .NET 10.0.302 설치 후 테스트
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
export CI_PROJECT_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/d3ro-ci"
|
||||||
|
. scripts/ci/bootstrap-linux-toolchain.sh dotnet
|
||||||
|
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,34 +0,0 @@
|
||||||
name: deploy-site-windows
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy-win:
|
|
||||||
runs-on: windows
|
|
||||||
defaults: { run: { shell: pwsh } }
|
|
||||||
steps:
|
|
||||||
- name: checkout
|
|
||||||
env: { CI_TOKEN: "${{ github.token }}" }
|
|
||||||
run: |
|
|
||||||
$u = [Uri]$env:GITHUB_SERVER_URL
|
|
||||||
$url = "$($u.Scheme)://actions:$($env:CI_TOKEN)@$($u.Authority)/$($env:GITHUB_REPOSITORY).git"
|
|
||||||
if (-not (Test-Path .git)) { git init -q . }
|
|
||||||
if (git remote | Select-String -Quiet '^origin$') { git remote set-url origin $url } else { git remote add origin $url }
|
|
||||||
git fetch -q --depth 1 origin $env:GITHUB_REF
|
|
||||||
git checkout -q -f FETCH_HEAD
|
|
||||||
git clean -qfdx
|
|
||||||
|
|
||||||
- name: 사이트 빌드
|
|
||||||
run: |
|
|
||||||
npm ci --prefix site
|
|
||||||
npm run build --prefix site
|
|
||||||
|
|
||||||
- name: Cloudflare Pages 배포
|
|
||||||
env:
|
|
||||||
CLOUDFLARE_API_TOKEN: "${{ secrets.CF_API_TOKEN || secrets.CLOUDFLARE_API_TOKEN }}"
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: "${{ secrets.CF_ACCOUNT_ID || secrets.CLOUDFLARE_ACCOUNT_ID }}"
|
|
||||||
run: |
|
|
||||||
if ($env:CLOUDFLARE_API_TOKEN) {
|
|
||||||
npx --yes wrangler@latest pages deploy site/dist --project-name d3ro --branch main --commit-dirty=true
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
name: deploy-site
|
name: deploy-site
|
||||||
|
|
||||||
|
# 사이트(site/) 배포의 유일한 경로: Cloudflare Pages `d3ro` (d3ro.chanpaca.net).
|
||||||
|
# 예전 deploy-site-windows.yml(수동 전용·배포 검증 없음)과 .github/workflows/deploy-site.yml
|
||||||
|
# (GitHub Pages, 실행된 적 없음)을 이 파일로 합쳤다.
|
||||||
|
#
|
||||||
|
# site/는 저장소 루트의 packages/core/src/*.ts 를 상대 import 하므로 전체 트리를 체크아웃한다
|
||||||
|
# (sparse checkout 금지).
|
||||||
|
#
|
||||||
|
# 필요한 시크릿: CF_API_TOKEN(또는 CLOUDFLARE_API_TOKEN), CF_ACCOUNT_ID(또는 CLOUDFLARE_ACCOUNT_ID)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
@ -21,6 +30,9 @@ jobs:
|
||||||
git checkout -q -f FETCH_HEAD
|
git checkout -q -f FETCH_HEAD
|
||||||
git clean -qfdx
|
git clean -qfdx
|
||||||
|
|
||||||
|
- name: 모바일 릴리스 공개 경계 검사
|
||||||
|
run: node scripts/ci/verify-mobile-release-boundary.mjs --self-test
|
||||||
|
|
||||||
- name: 의존성 설치 및 사이트 빌드
|
- name: 의존성 설치 및 사이트 빌드
|
||||||
run: |
|
run: |
|
||||||
npm ci --prefix site
|
npm ci --prefix site
|
||||||
|
|
|
||||||
47
.forgejo/workflows/payple-renew.yml
Normal file
47
.forgejo/workflows/payple-renew.yml
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
name: payple-renew
|
||||||
|
|
||||||
|
# Payple 정기결제 자동 갱신 트리거. 매일 한 번 Supabase Edge Function `payple-renew`를 호출한다.
|
||||||
|
# 예전 .github/workflows/payple-renew.yml은 GitHub 원격이 없어 한 번도 실행되지 않았다.
|
||||||
|
#
|
||||||
|
# 주의:
|
||||||
|
# - Forgejo schedule은 기본 브랜치(main)에 이 파일이 있을 때만 등록된다.
|
||||||
|
# - cron 시각은 Forgejo 서버 기준으로 해석된다. 서버가 UTC면 01:00 UTC = 10:00 KST.
|
||||||
|
#
|
||||||
|
# 필요한 시크릿:
|
||||||
|
# SUPABASE_URL — https://<project-ref>.supabase.co (끝 슬래시 없이)
|
||||||
|
# CRON_SECRET — Supabase Function 시크릿 CRON_SECRET 과 같은 값
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 1 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
renew:
|
||||||
|
runs-on: linux-builder
|
||||||
|
steps:
|
||||||
|
- name: payple-renew Edge Function 호출
|
||||||
|
env:
|
||||||
|
SUPABASE_URL: "${{ secrets.SUPABASE_URL }}"
|
||||||
|
CRON_SECRET: "${{ secrets.CRON_SECRET }}"
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
if [ -z "${SUPABASE_URL:-}" ] || [ -z "${CRON_SECRET:-}" ]; then
|
||||||
|
echo "SUPABASE_URL / CRON_SECRET 시크릿이 없습니다." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
response=$(curl --silent --show-error --max-time 300 -w "\n%{http_code}" -X POST \
|
||||||
|
"${SUPABASE_URL%/}/functions/v1/payple-renew" \
|
||||||
|
-H "Authorization: Bearer ${CRON_SECRET}" \
|
||||||
|
-H "Content-Type: application/json")
|
||||||
|
|
||||||
|
http_code=$(printf '%s\n' "$response" | tail -n 1)
|
||||||
|
body=$(printf '%s\n' "$response" | sed '$d')
|
||||||
|
|
||||||
|
echo "HTTP $http_code"
|
||||||
|
printf '%s\n' "$body" | jq . 2>/dev/null || printf '%s\n' "$body"
|
||||||
|
|
||||||
|
if [ "$http_code" -ge 400 ]; then
|
||||||
|
echo "갱신 실패: HTTP $http_code" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
name: release
|
name: release
|
||||||
|
|
||||||
# Canonical tag-triggered desktop release built and published on Forgejo.
|
# Canonical tag-triggered desktop release built and published on Forgejo.
|
||||||
# GitLab CI (.gitlab-ci.yml) and GitHub Actions (.github/workflows/release.yml)
|
# GitLab CI (.gitlab-ci.yml) is the legacy mirror builder; both converge on
|
||||||
# remain alternate builders; all three converge on publish-forgejo-release.mjs
|
# publish-forgejo-release.mjs so the Forgejo feed is the single update source.
|
||||||
# so the Forgejo feed is the single update source.
|
|
||||||
#
|
#
|
||||||
# Required repository secrets:
|
# Required repository secrets:
|
||||||
# FORGEJO_TOKEN — PAT with write:package + write:repository
|
# FORGEJO_TOKEN — PAT with write:package + write:repository
|
||||||
|
|
|
||||||
91
.github/workflows/build-mac.yml
vendored
91
.github/workflows/build-mac.yml
vendored
|
|
@ -1,91 +0,0 @@
|
||||||
name: Build macOS
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
notarize:
|
|
||||||
description: 'Apple Notarization 활성화'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- 'false'
|
|
||||||
- 'true'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build & Package (macOS)
|
|
||||||
runs-on: macos-14 # arm64 (Apple Silicon)
|
|
||||||
timeout-minutes: 60
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
arch: [arm64, x64]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Setup Python (sidecar 빌드용)
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Install system deps (sox)
|
|
||||||
run: brew install sox
|
|
||||||
|
|
||||||
- name: Install npm dependencies
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Bundle SoX into resources
|
|
||||||
working-directory: apps/desktop
|
|
||||||
run: bash scripts/install-sox.sh
|
|
||||||
|
|
||||||
- name: Build STT sidecar (PyInstaller)
|
|
||||||
working-directory: apps/desktop
|
|
||||||
run: |
|
|
||||||
# 로컬 전사는 사이드카 번들에 의존한다. 누락된 채 패키징하면 설치본에서
|
|
||||||
# 전사가 전혀 동작하지 않으므로 빌드 후 반드시 검증한다.
|
|
||||||
npm run sidecar:setup
|
|
||||||
npm run sidecar:build
|
|
||||||
node ../../scripts/ci/verify-sidecar-bundle.mjs
|
|
||||||
|
|
||||||
- name: Rebuild native modules for Electron
|
|
||||||
run: npx --yes @electron/rebuild@3 --version=33.4.11
|
|
||||||
|
|
||||||
- name: Build renderer/preload/main
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: electron-builder dist (mac, ${{ matrix.arch }})
|
|
||||||
working-directory: apps/desktop
|
|
||||||
env:
|
|
||||||
# Apple 서명/공증 (notarize=true일 때만 사용)
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
CSC_LINK: ${{ secrets.MAC_CERT_P12_BASE64 }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }}
|
|
||||||
NOTARIZE: ${{ inputs.notarize || 'false' }}
|
|
||||||
run: |
|
|
||||||
if [ "$NOTARIZE" = "true" ] && [ -n "$APPLE_ID" ]; then
|
|
||||||
npx electron-builder --mac --${{ matrix.arch }} -c.mac.notarize=true
|
|
||||||
else
|
|
||||||
npx electron-builder --mac --${{ matrix.arch }} -c.mac.notarize=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: d3ro-voice-mac-${{ matrix.arch }}
|
|
||||||
path: |
|
|
||||||
apps/desktop/release/*.dmg
|
|
||||||
apps/desktop/release/*.zip
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 7
|
|
||||||
333
.github/workflows/ci.yml
vendored
333
.github/workflows/ci.yml
vendored
|
|
@ -1,333 +0,0 @@
|
||||||
# .github/workflows/ci.yml
|
|
||||||
# Continuous Integration Pipeline for D3RO Voice Monorepo
|
|
||||||
|
|
||||||
name: CI Pipeline
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- develop
|
|
||||||
- 'feature/**'
|
|
||||||
- 'fix/**'
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- develop
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
|
||||||
# 1. Code Quality, Linting & Typecheck
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
|
||||||
code-quality:
|
|
||||||
name: Code Quality & Typecheck
|
|
||||||
runs-on: ubuntu-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: Credential Scanner Self-Test
|
|
||||||
run: npm run security:secrets:test
|
|
||||||
|
|
||||||
- name: Hard-Coded Credential Scan
|
|
||||||
run: npm run security:secrets
|
|
||||||
|
|
||||||
- name: Mobile Release Boundary Self-Test
|
|
||||||
run: npm run release:mobile:boundary:test
|
|
||||||
|
|
||||||
- name: Mobile Release Configuration Self-Test
|
|
||||||
run: npm run release:mobile:config:test
|
|
||||||
|
|
||||||
- name: Mobile Build Configuration Self-Test
|
|
||||||
run: npm run release:mobile:build-config:test
|
|
||||||
|
|
||||||
- name: Play Store Asset Contract
|
|
||||||
run: npm run release:play:assets
|
|
||||||
|
|
||||||
- name: Lint Check
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
- name: Typecheck All Workspaces
|
|
||||||
run: npm run typecheck
|
|
||||||
|
|
||||||
api-server-tests:
|
|
||||||
name: .NET API Server Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup .NET 10
|
|
||||||
uses: actions/setup-dotnet@v5
|
|
||||||
with:
|
|
||||||
dotnet-version: '10.0.302'
|
|
||||||
|
|
||||||
- name: Restore API Test Dependencies
|
|
||||||
run: dotnet restore apps/api-server.Tests/D3ROVoice.Api.Tests.csproj
|
|
||||||
|
|
||||||
- name: Run API Authorization and Gateway Tests
|
|
||||||
run: dotnet test apps/api-server.Tests/D3ROVoice.Api.Tests.csproj --configuration Release --no-restore -p:StaticWebAssetsEnabled=false
|
|
||||||
|
|
||||||
edge-functions-quality:
|
|
||||||
name: Supabase Edge Functions Typecheck & Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Deno 2.8.1
|
|
||||||
uses: denoland/setup-deno@v2
|
|
||||||
with:
|
|
||||||
deno-version: v2.8.1
|
|
||||||
|
|
||||||
- name: Check Every Edge Function Entrypoint
|
|
||||||
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
|
|
||||||
|
|
||||||
- name: Run Edge Function Contract Tests
|
|
||||||
run: deno test --config server/supabase/functions/deno.json --allow-read --allow-env server/supabase/functions
|
|
||||||
|
|
||||||
- name: Check Cloudflare Worker Drain
|
|
||||||
run: deno check --no-config server/cloudflare-worker/src/push-drain.ts
|
|
||||||
|
|
||||||
- name: Run Cloudflare Worker Tests
|
|
||||||
run: deno test --no-config --allow-read server/cloudflare-worker/src/push-drain.test.ts
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
|
||||||
# 2. Automated Test Matrix (Windows / macOS / Ubuntu)
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
|
||||||
test-matrix:
|
|
||||||
name: Test Suite (${{ matrix.os }})
|
|
||||||
needs: code-quality
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
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: Run Monorepo Test Suites (Vitest)
|
|
||||||
run: npm test
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
|
||||||
# 3. Build Validation for All Workspaces
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
|
||||||
build-validation:
|
|
||||||
name: Build Validation (${{ matrix.target }})
|
|
||||||
needs: code-quality
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- target: desktop
|
|
||||||
os: windows-latest
|
|
||||||
cmd: npm run build --workspace=@d3ro/desktop
|
|
||||||
- target: admin
|
|
||||||
os: ubuntu-latest
|
|
||||||
cmd: npm run build --workspace=@d3ro/admin
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
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 Target Workspace
|
|
||||||
run: ${{ matrix.cmd }}
|
|
||||||
|
|
||||||
- name: Verify Desktop Renderer Bundles
|
|
||||||
if: matrix.target == 'desktop'
|
|
||||||
run: node scripts/ci/verify-desktop-renderer-bundles.mjs
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
|
||||||
# 4. Android x86_64 artifacts and native dependency gate
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
|
||||||
mobile-android:
|
|
||||||
name: Mobile Android (universal debug + bundled universal E2E)
|
|
||||||
needs: code-quality
|
|
||||||
runs-on: ubuntu-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: 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: 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: Prepare Verified Whisper Model
|
|
||||||
run: node scripts/ci/prepare-whisper-model.mjs
|
|
||||||
|
|
||||||
- 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 Universal Debug, CSPRNG Test, and Bundled Universal E2E APKs
|
|
||||||
working-directory: apps/mobile-rn/android
|
|
||||||
env:
|
|
||||||
D3RO_VERSION_NAME: 0.0.0-e2e.${{ github.run_number }}
|
|
||||||
D3RO_VERSION_CODE: ${{ github.run_number }}
|
|
||||||
run: ./gradlew :app:assembleDebug :app:assembleDebugAndroidTest :app:assembleE2e -PreactNativeArchitectures=arm64-v8a,x86_64 --no-daemon
|
|
||||||
|
|
||||||
- name: Verify BuildConfig and APK Runtime Contracts
|
|
||||||
env:
|
|
||||||
D3RO_VERSION_NAME: 0.0.0-e2e.${{ github.run_number }}
|
|
||||||
D3RO_VERSION_CODE: ${{ github.run_number }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
DEBUG_APK=apps/mobile-rn/android/app/build/outputs/apk/debug/app-debug.apk
|
|
||||||
E2E_APK=apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk
|
|
||||||
test -f "$DEBUG_APK"
|
|
||||||
test -f "$E2E_APK"
|
|
||||||
node scripts/ci/verify-mobile-build-config.mjs debug \
|
|
||||||
| tee apps/mobile-rn/android/app/build/outputs/debug-build-config.json
|
|
||||||
node scripts/ci/verify-mobile-build-config.mjs e2e \
|
|
||||||
| tee apps/mobile-rn/android/app/build/outputs/e2e-build-config.json
|
|
||||||
node scripts/ci/verify-android-artifact.mjs --mode debug --apk "$DEBUG_APK" \
|
|
||||||
| tee apps/mobile-rn/android/app/build/outputs/debug-artifact-evidence.json
|
|
||||||
node scripts/ci/verify-android-artifact.mjs \
|
|
||||||
--mode e2e \
|
|
||||||
--apk "$E2E_APK" \
|
|
||||||
--expected-version-name "$D3RO_VERSION_NAME" \
|
|
||||||
--expected-version-code "$D3RO_VERSION_CODE" \
|
|
||||||
| tee apps/mobile-rn/android/app/build/outputs/e2e-artifact-evidence.json
|
|
||||||
node scripts/ci/verify-android-app-links.mjs \
|
|
||||||
| tee apps/mobile-rn/android/app/build/outputs/app-links-evidence.json
|
|
||||||
sha256sum "$DEBUG_APK" "$E2E_APK" | tee apps/mobile-rn/android/app/build/outputs/android-ci.sha256
|
|
||||||
|
|
||||||
- name: Upload Universal Android Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: d3ro-mobile-android-universal-e2e
|
|
||||||
path: |
|
|
||||||
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
|
|
||||||
apps/mobile-rn/android/app/build/outputs/app-links-evidence.json
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
|
||||||
# 5. Installed bundled APK on a clean API 35 x86_64 emulator
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
|
||||||
mobile-emulator-e2e:
|
|
||||||
name: Mobile Emulator E2E (API 35)
|
|
||||||
needs: mobile-android
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- 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: Download Bundled Android Artifact
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: d3ro-mobile-android-universal-e2e
|
|
||||||
path: mobile-artifact
|
|
||||||
|
|
||||||
- name: Install Verified Maestro CLI 2.7.0
|
|
||||||
run: |
|
|
||||||
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 "$RUNNER_TEMP/maestro"
|
|
||||||
echo "$RUNNER_TEMP/maestro/maestro/bin" >> "$GITHUB_PATH"
|
|
||||||
|
|
||||||
- name: Run Mandatory Clean-room and Optional External-account Journeys
|
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
|
||||||
env:
|
|
||||||
MOBILE_E2E_EMAIL: ${{ secrets.MOBILE_E2E_EMAIL }}
|
|
||||||
MOBILE_E2E_PASSWORD: ${{ secrets.MOBILE_E2E_PASSWORD }}
|
|
||||||
with:
|
|
||||||
api-level: 35
|
|
||||||
target: google_apis
|
|
||||||
arch: x86_64
|
|
||||||
profile: pixel_6
|
|
||||||
disable-animations: true
|
|
||||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
||||||
script: |
|
|
||||||
set -euo pipefail
|
|
||||||
DEBUG_APK="$(find mobile-artifact -path '*/apk/debug/app-debug.apk' -print -quit)"
|
|
||||||
TEST_APK="$(find mobile-artifact -name app-debug-androidTest.apk -print -quit)"
|
|
||||||
E2E_APK="$(find mobile-artifact -name app-e2e.apk -print -quit)"
|
|
||||||
test -n "$DEBUG_APK"
|
|
||||||
test -n "$TEST_APK"
|
|
||||||
test -n "$E2E_APK"
|
|
||||||
maestro --version
|
|
||||||
bash scripts/ci/run-mobile-csprng-instrumentation.sh "$DEBUG_APK" "$TEST_APK"
|
|
||||||
bash scripts/ci/run-mobile-emulator-gate.sh "$E2E_APK"
|
|
||||||
|
|
||||||
- name: Upload Emulator Evidence
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: d3ro-mobile-emulator-e2e
|
|
||||||
path: |
|
|
||||||
apps/mobile-rn/.maestro/*.junit.xml
|
|
||||||
apps/mobile-rn/.maestro-output/
|
|
||||||
if-no-files-found: warn
|
|
||||||
53
.github/workflows/deploy-site.yml
vendored
53
.github/workflows/deploy-site.yml
vendored
|
|
@ -1,53 +0,0 @@
|
||||||
name: Deploy Landing Page
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths: ['site/**']
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: pages
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: site/package-lock.json
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
working-directory: site
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Verify Mobile Release Publication Boundary
|
|
||||||
run: node scripts/ci/verify-mobile-release-boundary.mjs --self-test
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
working-directory: site
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: site/dist
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
steps:
|
|
||||||
- id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
29
.github/workflows/payple-renew.yml
vendored
29
.github/workflows/payple-renew.yml
vendored
|
|
@ -1,29 +0,0 @@
|
||||||
name: Payple Subscription Renewal
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# 매일 01:00 UTC (KST 10:00)
|
|
||||||
- cron: '0 1 * * *'
|
|
||||||
workflow_dispatch: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
renew:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Trigger payple-renew Edge Function
|
|
||||||
run: |
|
|
||||||
response=$(curl -s -w "\n%{http_code}" -X POST \
|
|
||||||
"${{ secrets.SUPABASE_URL }}/functions/v1/payple-renew" \
|
|
||||||
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
|
|
||||||
-H "Content-Type: application/json")
|
|
||||||
|
|
||||||
http_code=$(echo "$response" | tail -1)
|
|
||||||
body=$(echo "$response" | head -n -1)
|
|
||||||
|
|
||||||
echo "HTTP $http_code"
|
|
||||||
echo "$body" | jq . 2>/dev/null || echo "$body"
|
|
||||||
|
|
||||||
if [ "$http_code" -ge 400 ]; then
|
|
||||||
echo "::error::Renewal failed with HTTP $http_code"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
101
.github/workflows/release-signing-ca.yml
vendored
101
.github/workflows/release-signing-ca.yml
vendored
|
|
@ -1,101 +0,0 @@
|
||||||
name: Release & Code Signing CA Pipeline
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-sign-windows:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js 22
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Typecheck & Test
|
|
||||||
run: |
|
|
||||||
npm run typecheck
|
|
||||||
npm run test --workspace=@d3ro/api-client
|
|
||||||
|
|
||||||
# Azure Trusted Signing (Artifact Signing) for SmartScreen Reputation
|
|
||||||
- name: Setup Azure Trusted Signing
|
|
||||||
if: env.AZURE_CLIENT_ID != ''
|
|
||||||
uses: azure/trusted-signing-action@v0.4.1
|
|
||||||
with:
|
|
||||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
|
|
||||||
endpoint: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
|
|
||||||
trusted-signing-account-name: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
|
|
||||||
certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE }}
|
|
||||||
|
|
||||||
- name: Build STT Sidecar (local transcription engine)
|
|
||||||
run: |
|
|
||||||
# Without this bundle the packaged app cannot transcribe at all.
|
|
||||||
npm run sidecar:setup --workspace=@d3ro/desktop
|
|
||||||
npm run sidecar:build --workspace=@d3ro/desktop
|
|
||||||
node scripts/ci/verify-sidecar-bundle.mjs
|
|
||||||
|
|
||||||
- name: Build and Package Windows (NSIS + RFC 3161 TSA)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
RFC3161_TSA_SERVER: "http://timestamp.digicert.com"
|
|
||||||
run: |
|
|
||||||
npm run build --workspace=@d3ro/desktop
|
|
||||||
npx electron-builder --win --config apps/desktop/electron-builder.yml
|
|
||||||
|
|
||||||
- name: Upload Windows Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: d3ro-voice-windows
|
|
||||||
path: apps/desktop/release/*/*.exe
|
|
||||||
|
|
||||||
build-and-sign-macos:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js 22
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Setup Apple Developer ID Certificate
|
|
||||||
if: env.APPLE_CERTIFICATE != ''
|
|
||||||
env:
|
|
||||||
APPLE_CERTIFICATE: ${{ secrets.MAC_CSC_LINK }}
|
|
||||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
|
|
||||||
security create-keychain -p "" build.keychain
|
|
||||||
security default-keychain -s build.keychain
|
|
||||||
security unlock-keychain -p "" build.keychain
|
|
||||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
|
||||||
security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
|
|
||||||
|
|
||||||
- name: Build, Sign, and Notarize macOS (Gatekeeper CA)
|
|
||||||
env:
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
run: |
|
|
||||||
npm run build --workspace=@d3ro/desktop
|
|
||||||
npx electron-builder --mac --config apps/desktop/electron-builder.yml
|
|
||||||
|
|
||||||
- name: Upload macOS Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: d3ro-voice-macos
|
|
||||||
path: apps/desktop/release/*/*.dmg
|
|
||||||
612
.github/workflows/release.yml
vendored
612
.github/workflows/release.yml
vendored
|
|
@ -1,612 +0,0 @@
|
||||||
# .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: Verify Desktop Renderer Bundles
|
|
||||||
run: node scripts/ci/verify-desktop-renderer-bundles.mjs
|
|
||||||
|
|
||||||
- name: Build STT Sidecar (local transcription engine)
|
|
||||||
run: |
|
|
||||||
# Local transcription depends on the faster-whisper sidecar; a release
|
|
||||||
# that ships without it cannot transcribe at all, so build and verify
|
|
||||||
# the bundle before packaging.
|
|
||||||
npm run sidecar:setup --workspace=@d3ro/desktop
|
|
||||||
npm run sidecar:build --workspace=@d3ro/desktop
|
|
||||||
node scripts/ci/verify-sidecar-bundle.mjs
|
|
||||||
|
|
||||||
- name: Package with Electron Builder (NSIS x64)
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
if ([string]::IsNullOrWhiteSpace($env:CSC_LINK)) {
|
|
||||||
throw 'WIN_CSC_LINK must contain the production code-signing PFX path, URL, or base64 payload.'
|
|
||||||
}
|
|
||||||
if ([string]::IsNullOrWhiteSpace($env:CSC_KEY_PASSWORD)) {
|
|
||||||
throw 'WIN_CSC_KEY_PASSWORD is required.'
|
|
||||||
}
|
|
||||||
if ([string]::IsNullOrWhiteSpace($env:WIN_CSC_EXPECTED_SIGNER_SUBJECT)) {
|
|
||||||
throw 'WIN_CSC_EXPECTED_SIGNER_SUBJECT is required.'
|
|
||||||
}
|
|
||||||
if ($env:WIN_CSC_EXPECTED_SIGNER_SUBJECT -match '(?i)Everything2EverythingDev') {
|
|
||||||
throw 'The local Everything2EverythingDev certificate is not a production signing identity.'
|
|
||||||
}
|
|
||||||
$releaseVersion = node -p "require('./release/product-version.json').version"
|
|
||||||
Push-Location apps/desktop
|
|
||||||
try {
|
|
||||||
npx electron-builder --win --x64 --config electron-builder.yml
|
|
||||||
node scripts/ci/verify-native-abi.mjs
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "electron-builder failed with exit code $LASTEXITCODE." }
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
& scripts/ci/verify-windows-release-artifact.ps1 `
|
|
||||||
-ExpectedVersion $releaseVersion `
|
|
||||||
-ExpectedSignerSubject $env:WIN_CSC_EXPECTED_SIGNER_SUBJECT `
|
|
||||||
-ReleaseDirectory "apps/desktop/release/$releaseVersion"
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
|
||||||
WIN_CSC_EXPECTED_SIGNER_SUBJECT: ${{ secrets.WIN_CSC_EXPECTED_SIGNER_SUBJECT }}
|
|
||||||
|
|
||||||
- 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: Verify Desktop Renderer Bundles
|
|
||||||
run: node scripts/ci/verify-desktop-renderer-bundles.mjs
|
|
||||||
|
|
||||||
- name: Build STT Sidecar (local transcription engine)
|
|
||||||
run: |
|
|
||||||
# Local transcription depends on the faster-whisper sidecar; a release
|
|
||||||
# that ships without it cannot transcribe at all, so build and verify
|
|
||||||
# the bundle before packaging.
|
|
||||||
npm run sidecar:setup --workspace=@d3ro/desktop
|
|
||||||
npm run sidecar:build --workspace=@d3ro/desktop
|
|
||||||
node scripts/ci/verify-sidecar-bundle.mjs
|
|
||||||
|
|
||||||
- name: Package with Electron Builder (DMG & ZIP arm64)
|
|
||||||
run: |
|
|
||||||
cd apps/desktop
|
|
||||||
npx electron-builder --mac --arm64 --config electron-builder.yml
|
|
||||||
node scripts/ci/verify-native-abi.mjs
|
|
||||||
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 }}
|
|
||||||
|
|
||||||
- name: Publish to Forgejo Release and Update Feed
|
|
||||||
env:
|
|
||||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
|
||||||
FORGEJO_RELEASE_TAG: ${{ steps.release-identity.outputs.tag }}
|
|
||||||
FORGEJO_RELEASE_DIR: release-dist
|
|
||||||
run: node scripts/ci/publish-forgejo-release.mjs
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { generateKeyPairSync } from 'node:crypto'
|
import { generateKeyPairSync } from 'node:crypto'
|
||||||
import { spawnSync } from 'node:child_process'
|
import { spawnSync } from 'node:child_process'
|
||||||
import { createRequire } from 'node:module'
|
|
||||||
import {
|
import {
|
||||||
existsSync,
|
existsSync,
|
||||||
linkSync,
|
linkSync,
|
||||||
|
|
@ -32,8 +31,6 @@ import {
|
||||||
} from './mobile-release-evidence-lib.mjs'
|
} from './mobile-release-evidence-lib.mjs'
|
||||||
|
|
||||||
const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
||||||
const require = createRequire(import.meta.url)
|
|
||||||
const { assertNoMobileArtifacts } = require('../deploy-site-to-nas.js')
|
|
||||||
const productionAdMobId = 'ca-app-pub-1234567890123456~1234567890'
|
const productionAdMobId = 'ca-app-pub-1234567890123456~1234567890'
|
||||||
const productionSigner = '4fac6924821c50daabed764932a53c486f8c6c5f34b9f18db920aa4099152b54'
|
const productionSigner = '4fac6924821c50daabed764932a53c486f8c6c5f34b9f18db920aa4099152b54'
|
||||||
const versionName = '9.8.7'
|
const versionName = '9.8.7'
|
||||||
|
|
@ -101,15 +98,6 @@ function listScripts(directory) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifySourceContracts() {
|
function verifySourceContracts() {
|
||||||
const legacySync = readWorkspaceFile('scripts/ci/sync-and-publish-forgejo-release.mjs')
|
|
||||||
for (const forbidden of [
|
|
||||||
'app-debug.apk',
|
|
||||||
'd3ro-voice-v1.0.0.apk',
|
|
||||||
'Android Release APK',
|
|
||||||
]) {
|
|
||||||
assert(!legacySync.includes(forbidden), `legacy_sync_contains_${forbidden.replace(/[^a-z0-9]+/gi, '_')}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const retiredCapture = readWorkspaceFile('scripts/capture-pricing.js')
|
const retiredCapture = readWorkspaceFile('scripts/capture-pricing.js')
|
||||||
for (const forbidden of [
|
for (const forbidden of [
|
||||||
'assembleDebug',
|
'assembleDebug',
|
||||||
|
|
@ -125,71 +113,6 @@ function verifySourceContracts() {
|
||||||
}
|
}
|
||||||
assert(retiredCapture.includes('LEGACY_MOBILE_RELEASE_PIPELINE_DISABLED'), 'retired_capture_marker_missing')
|
assert(retiredCapture.includes('LEGACY_MOBILE_RELEASE_PIPELINE_DISABLED'), 'retired_capture_marker_missing')
|
||||||
|
|
||||||
const releaseWorkflow = readWorkspaceFile('.github/workflows/release.yml')
|
|
||||||
for (const required of [
|
|
||||||
'create-mobile-release-evidence.mjs',
|
|
||||||
'prepare-mobile-release-publication.mjs',
|
|
||||||
'ANDROID_RELEASE_EVIDENCE_PRIVATE_KEY_B64',
|
|
||||||
'ANDROID_UPLOAD_CERT_SHA256',
|
|
||||||
'release/android-release-identity.json',
|
|
||||||
'release/mobile-release-evidence-public.pem',
|
|
||||||
'--expected-admob-app-id',
|
|
||||||
'--expected-upload-cert-sha256',
|
|
||||||
'--repository "$GITHUB_REPOSITORY"',
|
|
||||||
'--commit-sha "$GITHUB_SHA"',
|
|
||||||
'--tree-sha "$(git rev-parse',
|
|
||||||
'--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"',
|
|
||||||
'test "$GITHUB_SHA" = "$(git rev-parse HEAD)"',
|
|
||||||
'git status --porcelain --untracked-files=all',
|
|
||||||
'--expected-play-app-signing-cert-sha256',
|
|
||||||
'--forbidden-upload-cert-sha256',
|
|
||||||
'--expected-version-name',
|
|
||||||
'--expected-version-code',
|
|
||||||
'--bundletool',
|
|
||||||
'--snapshot-dir',
|
|
||||||
'a099cfa1543f55593bc2ed16a70a7c67fe54b1747bb7301f37fdfd6d91028e29',
|
|
||||||
'environment: mobile-production-release',
|
|
||||||
'fetch-depth: 0',
|
|
||||||
'git merge-base --is-ancestor',
|
|
||||||
'tag_name: ${{ steps.release-identity.outputs.tag }}',
|
|
||||||
'overwrite_files: false',
|
|
||||||
'name: android-play-console-handoff',
|
|
||||||
'REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }}',
|
|
||||||
'test "$REPOSITORY_VISIBILITY" = "private"',
|
|
||||||
'release-publication/app-release.aab',
|
|
||||||
'release-publication/android-release-evidence.json',
|
|
||||||
'release-publication/android-publication-manifest.json',
|
|
||||||
'release-snapshot/release-artifact-verification.json',
|
|
||||||
'retention-days: 7',
|
|
||||||
]) {
|
|
||||||
assert(releaseWorkflow.includes(required), `release_workflow_missing_${required.replace(/[^a-z0-9]+/gi, '_')}`)
|
|
||||||
}
|
|
||||||
for (const forbidden of [
|
|
||||||
'ANDROID_RELEASE_CERT_SHA256',
|
|
||||||
'--expected-cert-sha256',
|
|
||||||
'ANDROID_RELEASE_EVIDENCE_PUBLIC_KEY_B64',
|
|
||||||
'name: android-release-assets',
|
|
||||||
'path: android-source/',
|
|
||||||
'release-dist/android',
|
|
||||||
]) {
|
|
||||||
assert(!releaseWorkflow.includes(forbidden), `release_workflow_forbidden_${forbidden.replace(/[^a-z0-9]+/gi, '_')}`)
|
|
||||||
}
|
|
||||||
const handoffStep = releaseWorkflow.match(
|
|
||||||
/- name: Upload Restricted Play Console AAB Handoff[\s\S]*?(?=\n\s{6}- name:|\n\s{2}#)/,
|
|
||||||
)?.[0] ?? ''
|
|
||||||
assert(handoffStep.includes('app-release.aab'), 'restricted_handoff_aab_missing')
|
|
||||||
assert(!handoffStep.includes('app-release.apk'), 'restricted_handoff_contains_upload_key_apk')
|
|
||||||
const publicReleaseStep = releaseWorkflow.slice(releaseWorkflow.indexOf('- name: Create GitHub Release'))
|
|
||||||
assert(!publicReleaseStep.includes('.apk'), 'public_release_contains_android_apk')
|
|
||||||
assert(!publicReleaseStep.includes('.aab'), 'public_release_contains_android_aab')
|
|
||||||
assert(!publicReleaseStep.includes('android-play-console-handoff'), 'public_release_contains_android_handoff')
|
|
||||||
assert(releaseWorkflow.includes('test "$GITHUB_SHA" = "$(git rev-parse origin/main)"'), 'dispatch_main_sha_guard_missing')
|
|
||||||
assert(releaseWorkflow.includes('test "$RELEASE_TAG" = "v$VERSION_NAME"'), 'release_tag_identity_guard_missing')
|
|
||||||
|
|
||||||
const gitlabWorkflow = readWorkspaceFile('.gitlab-ci.yml')
|
const gitlabWorkflow = readWorkspaceFile('.gitlab-ci.yml')
|
||||||
const mobileProductionJobStart = gitlabWorkflow.indexOf('mobile-production-release:\n')
|
const mobileProductionJobStart = gitlabWorkflow.indexOf('mobile-production-release:\n')
|
||||||
const publishReleaseJobStart = gitlabWorkflow.indexOf('\npublish-release:\n')
|
const publishReleaseJobStart = gitlabWorkflow.indexOf('\npublish-release:\n')
|
||||||
|
|
@ -361,7 +284,6 @@ function verifySourceContracts() {
|
||||||
for (const safePublicPath of [
|
for (const safePublicPath of [
|
||||||
'site/public/.well-known/assetlinks.json',
|
'site/public/.well-known/assetlinks.json',
|
||||||
'site/public/accept-invite.css',
|
'site/public/accept-invite.css',
|
||||||
'site/public/accept-invite.html',
|
|
||||||
'site/public/accept-invite.js',
|
'site/public/accept-invite.js',
|
||||||
'site/public/accept-invite/index.html',
|
'site/public/accept-invite/index.html',
|
||||||
'site/public/download.html',
|
'site/public/download.html',
|
||||||
|
|
@ -370,26 +292,25 @@ function verifySourceContracts() {
|
||||||
const safePublicSource = readWorkspaceFile(safePublicPath)
|
const safePublicSource = readWorkspaceFile(safePublicPath)
|
||||||
assert(!/(?:d3ro-voice[^"']*\.apk|git\.chanpaca\.net\/attachments\/(?:0b015367-dd8b-488c-8cc0-4db413b51792|d2e1b123-5678-496a-bf74-bc188938c999))/i.test(safePublicSource), `safe_public_mobile_reference_${safePublicPath}`)
|
assert(!/(?:d3ro-voice[^"']*\.apk|git\.chanpaca\.net\/attachments\/(?:0b015367-dd8b-488c-8cc0-4db413b51792|d2e1b123-5678-496a-bf74-bc188938c999))/i.test(safePublicSource), `safe_public_mobile_reference_${safePublicPath}`)
|
||||||
}
|
}
|
||||||
const siteDeployWorkflow = readWorkspaceFile('.github/workflows/deploy-site.yml')
|
const siteDeployWorkflow = readWorkspaceFile('.forgejo/workflows/deploy-site.yml')
|
||||||
assert(siteDeployWorkflow.includes('verify-mobile-release-boundary.mjs --self-test'), 'pages_deploy_boundary_gate_missing')
|
assert(siteDeployWorkflow.includes('verify-mobile-release-boundary.mjs --self-test'), 'pages_deploy_boundary_gate_missing')
|
||||||
const deploySite = readWorkspaceFile('scripts/deploy-site-to-nas.js')
|
// Installers ship only through the Forgejo feed and public pages only from site/,
|
||||||
assert(deploySite.includes('assertNoMobileArtifacts'), 'nas_mobile_artifact_guard_missing')
|
// so the API must not serve static files at all.
|
||||||
assert(!deploySite.includes('sync-and-publish-forgejo-release'), 'nas_legacy_release_sync_enabled')
|
|
||||||
const apiProject = readWorkspaceFile('apps/api-server/D3ROVoice.Api.csproj')
|
|
||||||
assert(apiProject.includes('<Content Remove="wwwroot\\releases\\**\\*" />'), 'api_static_release_exclusion_missing')
|
|
||||||
for (const staleAsset of ['index-D7M5UQvT.js', 'index-JlYFxlAJ.js']) {
|
|
||||||
assert(apiProject.includes(`<Content Remove="wwwroot\\assets\\${staleAsset}" />`), `api_stale_marketing_asset_publishable_${staleAsset}`)
|
|
||||||
}
|
|
||||||
const apiProgram = readWorkspaceFile('apps/api-server/Program.cs')
|
const apiProgram = readWorkspaceFile('apps/api-server/Program.cs')
|
||||||
assert(apiProgram.includes('mobileReleasePath') && apiProgram.indexOf('mobileReleasePath') < apiProgram.indexOf('app.UseStaticFiles()'), 'api_runtime_mobile_release_guard_missing')
|
assert(!apiProgram.includes('UseStaticFiles'), 'api_serves_static_files')
|
||||||
assert(apiProgram.includes('legacyMarketingAsset'), 'api_runtime_legacy_marketing_guard_missing')
|
// 사이트는 Android·iOS 설치 파일을 내려주지 않는다. 다운로드 버튼은 Windows 설치 파일 하나뿐이고
|
||||||
const osHook = readWorkspaceFile('site/src/hooks/useClientOS.ts')
|
// 모바일은 '준비 중' 문구로만 안내한다(site/src/sections/Download.tsx, Hero.tsx).
|
||||||
const androidConfig = osHook.match(/android:\s*\{[\s\S]*?\n\s*\},/)?.[0] ?? ''
|
for (const sitePath of [
|
||||||
assert(androidConfig.includes("downloadUrl: '#download'"), 'android_download_not_unavailable')
|
'site/src/hooks/useClientOS.ts',
|
||||||
assert(!androidConfig.includes('attachments/'), 'android_attachment_link_enabled')
|
'site/src/sections/Hero.tsx',
|
||||||
const downloadUi = readWorkspaceFile('site/src/sections/Download.tsx')
|
'site/src/sections/Download.tsx',
|
||||||
assert(downloadUi.includes('aria-disabled="true"'), 'android_download_ui_not_disabled')
|
'site/src/release.ts',
|
||||||
assert(!/attachments\/[0-9a-f-]+[\s\S]{0,120}\.apk/i.test(downloadUi), 'android_direct_attachment_enabled')
|
]) {
|
||||||
|
const siteSource = readWorkspaceFile(sitePath)
|
||||||
|
assert(!/\.(?:apk|aab)\b/i.test(siteSource), `site_mobile_package_link_${sitePath}`)
|
||||||
|
assert(!/attachments\/[0-9a-f-]+/i.test(siteSource), `site_attachment_link_${sitePath}`)
|
||||||
|
assert(!/play\.google\.com\/store|apps\.apple\.com/i.test(siteSource), `site_store_link_${sitePath}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectFailure(label, operation, expectedCode) {
|
function expectFailure(label, operation, expectedCode) {
|
||||||
|
|
@ -800,14 +721,6 @@ function verifyNegativeAndMaterializationTests() {
|
||||||
expectFailure('hardlink_source', () => hashRegularFileStable(hardlinkPath), 'hardlink_rejected')
|
expectFailure('hardlink_source', () => hashRegularFileStable(hardlinkPath), 'hardlink_rejected')
|
||||||
rmSync(hardlinkPath)
|
rmSync(hardlinkPath)
|
||||||
|
|
||||||
const unsafeSite = join(temporaryRoot, 'unsafe-site')
|
|
||||||
mkdirSync(unsafeSite)
|
|
||||||
writeFileSync(join(unsafeSite, 'legacy.js'), 'location.href="https://git.chanpaca.net/attachments/0b015367-dd8b-488c-8cc0-4db413b51792"')
|
|
||||||
expectFailure('static_link_bypass', () => assertNoMobileArtifacts(unsafeSite), 'Legacy mobile download link blocked')
|
|
||||||
rmSync(join(unsafeSite, 'legacy.js'))
|
|
||||||
writeFileSync(join(unsafeSite, 'unsealed.apk'), 'not a release')
|
|
||||||
expectFailure('static_apk_bypass', () => assertNoMobileArtifacts(unsafeSite), 'Unsealed mobile artifact blocked')
|
|
||||||
|
|
||||||
const outsideRoot = join(temporaryRoot, 'outside')
|
const outsideRoot = join(temporaryRoot, 'outside')
|
||||||
mkdirSync(outsideRoot)
|
mkdirSync(outsideRoot)
|
||||||
const outsideApk = join(outsideRoot, RELEASE_APK_NAME)
|
const outsideApk = join(outsideRoot, RELEASE_APK_NAME)
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,6 @@ function loadSurfaces(readSurface = read) {
|
||||||
publisher: readSurface('scripts/ci/publish-gitlab-release.mjs'),
|
publisher: readSurface('scripts/ci/publish-gitlab-release.mjs'),
|
||||||
forgejoPublisher: readSurface('scripts/ci/publish-forgejo-release.mjs'),
|
forgejoPublisher: readSurface('scripts/ci/publish-forgejo-release.mjs'),
|
||||||
gitlab: readSurface('.gitlab-ci.yml'),
|
gitlab: readSurface('.gitlab-ci.yml'),
|
||||||
github: readSurface('.github/workflows/release.yml'),
|
|
||||||
githubMac: readSurface('.github/workflows/build-mac.yml'),
|
|
||||||
githubSigning: readSurface('.github/workflows/release-signing-ca.yml'),
|
|
||||||
forgejoLinux: readSurface('.forgejo/workflows/deploy-site.yml'),
|
|
||||||
forgejoWindows: readSurface('.forgejo/workflows/deploy-site-windows.yml'),
|
|
||||||
forgejoRelease: readSurface('.forgejo/workflows/release.yml'),
|
forgejoRelease: readSurface('.forgejo/workflows/release.yml'),
|
||||||
changelog: readSurface('CHANGELOG.md'),
|
changelog: readSurface('CHANGELOG.md'),
|
||||||
}
|
}
|
||||||
|
|
@ -208,7 +203,7 @@ function validate(surfaces) {
|
||||||
|
|
||||||
for (const [name, workflow] of [
|
for (const [name, workflow] of [
|
||||||
['gitlab', surfaces.gitlab],
|
['gitlab', surfaces.gitlab],
|
||||||
['github', surfaces.github],
|
['forgejo', surfaces.forgejoRelease],
|
||||||
]) {
|
]) {
|
||||||
fail(workflow.includes('sync-version.mjs'), `${name}_version_gate_missing`)
|
fail(workflow.includes('sync-version.mjs'), `${name}_version_gate_missing`)
|
||||||
fail(workflow.includes('release/product-version.json'), `${name}_product_metadata_missing`)
|
fail(workflow.includes('release/product-version.json'), `${name}_product_metadata_missing`)
|
||||||
|
|
@ -221,10 +216,8 @@ function validate(surfaces) {
|
||||||
fail(/tags:/.test(surfaces.forgejoRelease), 'forgejo_release_workflow_tag_trigger_missing')
|
fail(/tags:/.test(surfaces.forgejoRelease), 'forgejo_release_workflow_tag_trigger_missing')
|
||||||
fail(surfaces.forgejoRelease.includes('sync-version.mjs'), 'forgejo_release_workflow_version_gate_missing')
|
fail(surfaces.forgejoRelease.includes('sync-version.mjs'), 'forgejo_release_workflow_version_gate_missing')
|
||||||
|
|
||||||
fail(!/push:\s*\n\s*tags:/m.test(surfaces.githubMac), 'legacy_mac_tag_trigger_enabled')
|
// CI 정본은 Forgejo 한 벌이다(REFACTOR_POLICY W3-7). GitHub 원격이 없어 .github 워크플로는 실행되지 않는다.
|
||||||
fail(!/push:\s*\n\s*tags:/m.test(surfaces.githubSigning), 'legacy_signing_tag_trigger_enabled')
|
fail(!existsSync(join(root, '.github/workflows')), 'github_workflows_reintroduced')
|
||||||
fail(!surfaces.forgejoLinux.includes('sync-and-publish-forgejo-release'), 'forgejo_linux_legacy_release_sync')
|
|
||||||
fail(!surfaces.forgejoWindows.includes('sync-and-publish-forgejo-release'), 'forgejo_windows_legacy_release_sync')
|
|
||||||
fail(!existsSync(join(root, 'apps/mobile-rn/src/lib/update-manager.ts')), 'unsafe_mobile_update_manager_present')
|
fail(!existsSync(join(root, 'apps/mobile-rn/src/lib/update-manager.ts')), 'unsafe_mobile_update_manager_present')
|
||||||
fail(
|
fail(
|
||||||
existsSync(join(root, `apps/mobile-rn/metadata/android/ko-KR/changelogs/${metadata.androidVersionCode}.txt`)),
|
existsSync(join(root, `apps/mobile-rn/metadata/android/ko-KR/changelogs/${metadata.androidVersionCode}.txt`)),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue