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
|
||||
|
||||
# 사이트(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:
|
||||
push:
|
||||
branches: [main]
|
||||
|
|
@ -21,6 +30,9 @@ jobs:
|
|||
git checkout -q -f FETCH_HEAD
|
||||
git clean -qfdx
|
||||
|
||||
- name: 모바일 릴리스 공개 경계 검사
|
||||
run: node scripts/ci/verify-mobile-release-boundary.mjs --self-test
|
||||
|
||||
- name: 의존성 설치 및 사이트 빌드
|
||||
run: |
|
||||
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
|
||||
|
||||
# Canonical tag-triggered desktop release built and published on Forgejo.
|
||||
# GitLab CI (.gitlab-ci.yml) and GitHub Actions (.github/workflows/release.yml)
|
||||
# remain alternate builders; all three converge on publish-forgejo-release.mjs
|
||||
# so the Forgejo feed is the single update source.
|
||||
# GitLab CI (.gitlab-ci.yml) is the legacy mirror builder; both converge on
|
||||
# publish-forgejo-release.mjs so the Forgejo feed is the single update source.
|
||||
#
|
||||
# Required repository secrets:
|
||||
# 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 { spawnSync } from 'node:child_process'
|
||||
import { createRequire } from 'node:module'
|
||||
import {
|
||||
existsSync,
|
||||
linkSync,
|
||||
|
|
@ -32,8 +31,6 @@ import {
|
|||
} from './mobile-release-evidence-lib.mjs'
|
||||
|
||||
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 productionSigner = '4fac6924821c50daabed764932a53c486f8c6c5f34b9f18db920aa4099152b54'
|
||||
const versionName = '9.8.7'
|
||||
|
|
@ -101,15 +98,6 @@ function listScripts(directory) {
|
|||
}
|
||||
|
||||
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')
|
||||
for (const forbidden of [
|
||||
'assembleDebug',
|
||||
|
|
@ -125,71 +113,6 @@ function verifySourceContracts() {
|
|||
}
|
||||
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 mobileProductionJobStart = gitlabWorkflow.indexOf('mobile-production-release:\n')
|
||||
const publishReleaseJobStart = gitlabWorkflow.indexOf('\npublish-release:\n')
|
||||
|
|
@ -361,7 +284,6 @@ function verifySourceContracts() {
|
|||
for (const safePublicPath of [
|
||||
'site/public/.well-known/assetlinks.json',
|
||||
'site/public/accept-invite.css',
|
||||
'site/public/accept-invite.html',
|
||||
'site/public/accept-invite.js',
|
||||
'site/public/accept-invite/index.html',
|
||||
'site/public/download.html',
|
||||
|
|
@ -370,26 +292,25 @@ function verifySourceContracts() {
|
|||
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}`)
|
||||
}
|
||||
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')
|
||||
const deploySite = readWorkspaceFile('scripts/deploy-site-to-nas.js')
|
||||
assert(deploySite.includes('assertNoMobileArtifacts'), 'nas_mobile_artifact_guard_missing')
|
||||
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}`)
|
||||
}
|
||||
// Installers ship only through the Forgejo feed and public pages only from site/,
|
||||
// so the API must not serve static files at all.
|
||||
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('legacyMarketingAsset'), 'api_runtime_legacy_marketing_guard_missing')
|
||||
const osHook = readWorkspaceFile('site/src/hooks/useClientOS.ts')
|
||||
const androidConfig = osHook.match(/android:\s*\{[\s\S]*?\n\s*\},/)?.[0] ?? ''
|
||||
assert(androidConfig.includes("downloadUrl: '#download'"), 'android_download_not_unavailable')
|
||||
assert(!androidConfig.includes('attachments/'), 'android_attachment_link_enabled')
|
||||
const downloadUi = readWorkspaceFile('site/src/sections/Download.tsx')
|
||||
assert(downloadUi.includes('aria-disabled="true"'), 'android_download_ui_not_disabled')
|
||||
assert(!/attachments\/[0-9a-f-]+[\s\S]{0,120}\.apk/i.test(downloadUi), 'android_direct_attachment_enabled')
|
||||
assert(!apiProgram.includes('UseStaticFiles'), 'api_serves_static_files')
|
||||
// 사이트는 Android·iOS 설치 파일을 내려주지 않는다. 다운로드 버튼은 Windows 설치 파일 하나뿐이고
|
||||
// 모바일은 '준비 중' 문구로만 안내한다(site/src/sections/Download.tsx, Hero.tsx).
|
||||
for (const sitePath of [
|
||||
'site/src/hooks/useClientOS.ts',
|
||||
'site/src/sections/Hero.tsx',
|
||||
'site/src/sections/Download.tsx',
|
||||
'site/src/release.ts',
|
||||
]) {
|
||||
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) {
|
||||
|
|
@ -800,14 +721,6 @@ function verifyNegativeAndMaterializationTests() {
|
|||
expectFailure('hardlink_source', () => hashRegularFileStable(hardlinkPath), 'hardlink_rejected')
|
||||
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')
|
||||
mkdirSync(outsideRoot)
|
||||
const outsideApk = join(outsideRoot, RELEASE_APK_NAME)
|
||||
|
|
|
|||
|
|
@ -33,11 +33,6 @@ function loadSurfaces(readSurface = read) {
|
|||
publisher: readSurface('scripts/ci/publish-gitlab-release.mjs'),
|
||||
forgejoPublisher: readSurface('scripts/ci/publish-forgejo-release.mjs'),
|
||||
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'),
|
||||
changelog: readSurface('CHANGELOG.md'),
|
||||
}
|
||||
|
|
@ -208,7 +203,7 @@ function validate(surfaces) {
|
|||
|
||||
for (const [name, workflow] of [
|
||||
['gitlab', surfaces.gitlab],
|
||||
['github', surfaces.github],
|
||||
['forgejo', surfaces.forgejoRelease],
|
||||
]) {
|
||||
fail(workflow.includes('sync-version.mjs'), `${name}_version_gate_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(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')
|
||||
fail(!/push:\s*\n\s*tags:/m.test(surfaces.githubSigning), 'legacy_signing_tag_trigger_enabled')
|
||||
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')
|
||||
// CI 정본은 Forgejo 한 벌이다(REFACTOR_POLICY W3-7). GitHub 원격이 없어 .github 워크플로는 실행되지 않는다.
|
||||
fail(!existsSync(join(root, '.github/workflows')), 'github_workflows_reintroduced')
|
||||
fail(!existsSync(join(root, 'apps/mobile-rn/src/lib/update-manager.ts')), 'unsafe_mobile_update_manager_present')
|
||||
fail(
|
||||
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