Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/deployment-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Deployment Gate

on:
workflow_dispatch:
inputs:
target:
description: staging 或 production
required: true
default: staging
type: choice
options:
- staging
- production
base_url:
description: TypeTwo Server HTTPS URL
required: true
type: string
expected_environment:
description: /health environment 預期值
required: true
default: production
type: string
skip_smoke:
description: 只檢查 /health,不執行會寫入資料的 smoke
required: true
default: false
type: boolean

permissions:
contents: read

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
deployment-gate:
runs-on: windows-latest
timeout-minutes: 20
environment: ${{ inputs.target }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Run deployment gate
shell: powershell
env:
ADMIN_EMAIL: ${{ secrets.TYPETWO_ADMIN_EMAIL }}
ADMIN_PASSWORD: ${{ secrets.TYPETWO_ADMIN_PASSWORD }}
run: |
$args = @(
'-BaseUrl', '${{ inputs.base_url }}',
'-ExpectedEnvironment', '${{ inputs.expected_environment }}',
'-EvidencePath', 'deployment-evidence-${{ inputs.target }}.json'
)

if ('${{ inputs.skip_smoke }}' -eq 'true') {
$args += '-SkipSmoke'
}

.\scripts\test_typetwo_deployment_gate.ps1 @args

- name: Upload deployment evidence
uses: actions/upload-artifact@v4
if: always()
with:
name: deployment-evidence-${{ inputs.target }}
path: deployment-evidence-${{ inputs.target }}.json
if-no-files-found: warn
13 changes: 13 additions & 0 deletions COMPLETION_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ M0、M2、M3、M4 與 M5 的程式、文件、測試與本機 production-like
- `master` CI run `26617148862`:`backend-quality` 與 `windows-quality` 皆通過。
- GitHub Release `v1.0.18`:release workflow run `26617247786` 通過,已附加 `setup_typetwo.exe`、`setup_typetwo.exe.sha256`、`typetwo-glossary-api-v1.0.18.tar`、`typetwo-glossary-api-v1.0.18.tar.sha256`。
- GitHub Release `v1.0.18` checksum:release assets 已下載後重新計算,installer 與 backend image checksum 皆吻合。
- 已新增 `scripts/test_typetwo_deployment_gate.ps1`,可對 staging/production URL 執行 `/health`、smoke、備份還原驗證並輸出 evidence JSON。
- 已新增 GitHub Actions `Deployment Gate` workflow,設定 environment secrets 後可手動對 staging/production URL 產出 deployment evidence artifact。
- 本機 Docker 已實測 `test_typetwo_deployment_gate.ps1 -AllowHttp -ExpectedEnvironment development`,`health` 與 `smoke` 均通過,cleanup 成功;production/staging 仍需真實 HTTPS URL 重跑。

## 外部環境待驗證

Expand All @@ -45,6 +48,7 @@ M0、M2、M3、M4 與 M5 的程式、文件、測試與本機 production-like
| 正式 HTTPS domain | 需要真實 DNS、TLS 憑證與可從公司外連線的主機 | `Invoke-RestMethod https://<domain>/health` 回傳 `ok=true`、`db=ok`、`environment=production` |
| staging smoke | 需要 staging domain 與 staging DB | 對 staging URL 執行 `.\scripts\smoke_typetwo_glossary_api.ps1 -BaseUrl https://<staging-domain>` |
| production smoke | 需要 production domain 與 production 帳號策略 | 對 production URL 執行 smoke,確認 cleanup 後沒有測試詞彙與可登入測試帳號 |
| staging/production evidence | 已有腳本與 workflow,仍需要真實 URL 與 admin secrets | 執行 `test_typetwo_deployment_gate.ps1` 或 GitHub Actions `Deployment Gate` |
| GitHub Actions 綠燈 | 已在 PR 與 `master` 驗證通過 | `CI / backend-quality` 與 `CI / windows-quality` 皆通過 |
| Release artifact 可下載 | 已在 GitHub Release `v1.0.18` 驗證通過 | Release 內有 `setup_typetwo.exe`、`setup_typetwo.exe.sha256`、`typetwo-glossary-api-v1.0.18.tar`、`typetwo-glossary-api-v1.0.18.tar.sha256` |
| production 備份還原演練 | 需要 production DB 備份檔與非 production 還原環境 | 用 `.\scripts\verify_typetwo_postgres_backup.ps1 -BackupZip <production-backup.zip>` 驗證成功 |
Expand All @@ -60,3 +64,12 @@ M0、M2、M3、M4 與 M5 的程式、文件、測試與本機 production-like
5. GitHub Release event 產出 installer、backend image tar 與各自 checksum,並附加到 GitHub Release。
6. production 備份已還原到非 production volume 驗證。
7. rollback runbook 至少演練一次並記錄結果。

## 仍需提供的外部設定

repo 目前沒有部署 secrets、environment variables、遠端主機或 DNS 設定。若要讓 GitHub Actions `Deployment Gate` 直接完成 staging/production evidence,需先在 GitHub environment `staging` 與 `production` 設定:

- `TYPETWO_ADMIN_EMAIL`
- `TYPETWO_ADMIN_PASSWORD`

並準備可從 GitHub runner 連線的 HTTPS URL,例如 `https://typetwo-glossary.company.com`。
30 changes: 30 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build

smoke script 會建立臨時 approved 詞彙、臨時 user、pending 建議詞,驗證 approve 後可進 approved 詞彙包,最後刪除 smoke 詞彙、停用 smoke user,並確認既有 token 已失效。

也可以用 deployment gate 統一產出證據檔:

```powershell
.\scripts\test_typetwo_deployment_gate.ps1 `
-BaseUrl "https://staging.example.com" `
-ExpectedEnvironment "production" `
-AdminEmail $env:ADMIN_EMAIL `
-AdminPassword $env:ADMIN_PASSWORD `
-EvidencePath ".\evidence\staging-deployment-gate.json"
```

GitHub Actions 也提供手動 workflow `Deployment Gate`。使用前需在對應 environment 設定 secrets:

- `TYPETWO_ADMIN_EMAIL`
- `TYPETWO_ADMIN_PASSWORD`

執行 workflow 時輸入 staging 或 production URL;workflow 會上傳 `deployment-evidence-<target>.json` artifact。

## Production 部署

1. 備份 production PostgreSQL。
Expand Down Expand Up @@ -79,6 +97,18 @@ docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
-AdminPassword $env:ADMIN_PASSWORD
```

建議正式驗收改用 deployment gate,同時輸出 evidence:

```powershell
.\scripts\test_typetwo_deployment_gate.ps1 `
-BaseUrl $env:PUBLIC_BASE_URL `
-ExpectedEnvironment "production" `
-AdminEmail $env:ADMIN_EMAIL `
-AdminPassword $env:ADMIN_PASSWORD `
-BackupZip ".\backups\typetwo_<timestamp>.sql.zip" `
-EvidencePath ".\evidence\production-deployment-gate.json"
```

## 備份與還原驗證

建立備份:
Expand Down
2 changes: 2 additions & 0 deletions RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
- staging 備份已用 `verify_typetwo_postgres_backup.ps1` 還原到臨時 volume 驗證。
- Alembic migration 已在 staging 成功升級到 head。
- staging smoke 通過。
- `test_typetwo_deployment_gate.ps1` 或 GitHub Actions `Deployment Gate` 已產出 staging evidence JSON。
- Flutter App 可連 staging TypeTwo Server 並完成登入、同步、pending review、匯入預覽。

## Production
Expand All @@ -62,6 +63,7 @@
- Alembic migration 已執行。
- service 已啟動,`/health` 為 `ok=true`、`db=ok`、`environment=production`。
- production smoke 通過且 cleanup 驗證通過。
- `test_typetwo_deployment_gate.ps1` 或 GitHub Actions `Deployment Gate` 已產出 production evidence JSON。
- rollback owner 與 rollback artifact 已確認。

## Release notes
Expand Down
8 changes: 8 additions & 0 deletions plan-20260526.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,14 @@ docker compose -f docker-compose.yml -f docker-compose.prod.yml config
- 已建立 GitHub Release `v1.0.18`,release workflow run `26617247786` 已通過,並成功附加 installer、backend image tar 與各自 SHA256。
- 已下載 GitHub Release `v1.0.18` assets 並重新計算 SHA256,確認 release 內附 checksum 皆吻合。

### 2026-05-29:部署 evidence gate 增量

- 已新增 `scripts/test_typetwo_deployment_gate.ps1`,可對 staging/production URL 統一執行 `/health`、smoke、備份還原驗證,並輸出 JSON evidence。
- 已新增 GitHub Actions `Deployment Gate` workflow,可手動輸入 staging/production URL,使用 environment secrets 執行遠端 smoke 並上傳 evidence artifact。
- `DEPLOYMENT.md`、`RELEASE_CHECKLIST.md` 與 `COMPLETION_AUDIT.md` 已補上 deployment gate 使用方式與完成條件。
- 已用本機 Docker 實測 deployment gate,`health` 與 `smoke` 皆通過,並確認 cleanup 成功。
- repo 目前沒有 staging/production 主機、DNS、GitHub Secrets 或 Variables;最後外部 gate 仍需真實 HTTPS URL 與 admin credentials 才能執行。

## 下一步建議

下一輪優先補齊外部環境證據:
Expand Down
149 changes: 149 additions & 0 deletions scripts/test_typetwo_deployment_gate.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
param(
[Parameter(Mandatory = $true)][string]$BaseUrl,
[string]$AdminEmail = $env:ADMIN_EMAIL,
[string]$AdminPassword = $env:ADMIN_PASSWORD,
[string]$ExpectedEnvironment = "",
[string]$BackupZip = "",
[string]$EvidencePath = ".\deployment-evidence.json",
[switch]$AllowHttp,
[switch]$SkipSmoke,
[switch]$SkipBackupVerify
)

$ErrorActionPreference = "Stop"

function Add-Step {
param(
[System.Collections.Generic.List[object]]$Steps,
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][string]$Status,
[object]$Details = $null
)

$Steps.Add([pscustomobject]@{
name = $Name
status = $Status
details = $Details
checkedAt = (Get-Date).ToUniversalTime().ToString("o")
})
}

function Assert-Url {
param([string]$Value)

try {
$uri = [Uri]$Value
} catch {
throw "BaseUrl 必須是有效 URL:$Value"
}

if (-not $AllowHttp -and $uri.Scheme -ne "https") {
throw "BaseUrl 必須使用 https://;本機或臨時測試才可加 -AllowHttp"
}

if ($uri.AbsolutePath -ne "/") {
return $Value.TrimEnd("/")
}

return "$($uri.Scheme)://$($uri.Authority)"
}

function Wait-TypeTwoHealth {
param(
[Parameter(Mandatory = $true)][string]$Url,
[int]$TimeoutSeconds = 90
)

$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$lastError = $null

do {
try {
$body = Invoke-RestMethod -Method Get -Uri "$Url/health"
if ($body.ok -eq $true -and $body.db -eq "ok") {
return $body
}
$lastError = "/health 回傳異常:$($body | ConvertTo-Json -Compress -Depth 8)"
} catch {
$lastError = $_.Exception.Message
}

Start-Sleep -Seconds 2
} while ((Get-Date) -lt $deadline)

throw "API healthcheck 未在期限內通過:$lastError"
}

$normalizedBaseUrl = Assert-Url -Value $BaseUrl
$steps = New-Object "System.Collections.Generic.List[object]"
$summary = [ordered]@{
ok = $false
baseUrl = $normalizedBaseUrl
expectedEnvironment = $ExpectedEnvironment
startedAt = (Get-Date).ToUniversalTime().ToString("o")
completedAt = $null
steps = $steps
}

try {
$health = Wait-TypeTwoHealth -Url $normalizedBaseUrl

if (-not [string]::IsNullOrWhiteSpace($ExpectedEnvironment) -and
$health.environment -ne $ExpectedEnvironment) {
throw "/health environment 不符合預期:expected=$ExpectedEnvironment actual=$($health.environment)"
}

Add-Step -Steps $steps -Name "health" -Status "passed" -Details $health

if ($SkipSmoke) {
Add-Step -Steps $steps -Name "smoke" -Status "skipped" -Details "已使用 -SkipSmoke"
} else {
if ([string]::IsNullOrWhiteSpace($AdminEmail) -or [string]::IsNullOrWhiteSpace($AdminPassword)) {
throw "執行 smoke 需要 AdminEmail/AdminPassword,或使用 -SkipSmoke"
}

$smokeJson = & "$PSScriptRoot\smoke_typetwo_glossary_api.ps1" `
-BaseUrl $normalizedBaseUrl `
-AdminEmail $AdminEmail `
-AdminPassword $AdminPassword
if ($LASTEXITCODE -ne 0) {
throw "smoke script 失敗"
}

$smokeResult = $smokeJson | ConvertFrom-Json
if ($smokeResult.ok -ne $true) {
throw "smoke result 不符合預期:$($smokeResult | ConvertTo-Json -Compress -Depth 8)"
}

Add-Step -Steps $steps -Name "smoke" -Status "passed" -Details $smokeResult
}

if ($SkipBackupVerify) {
Add-Step -Steps $steps -Name "backup-restore" -Status "skipped" -Details "已使用 -SkipBackupVerify"
} elseif ([string]::IsNullOrWhiteSpace($BackupZip)) {
Add-Step -Steps $steps -Name "backup-restore" -Status "skipped" -Details "未提供 BackupZip"
} else {
& "$PSScriptRoot\verify_typetwo_postgres_backup.ps1" -BackupZip $BackupZip
if ($LASTEXITCODE -ne 0) {
throw "備份還原驗證失敗"
}

Add-Step -Steps $steps -Name "backup-restore" -Status "passed" -Details @{
backupZip = (Resolve-Path -LiteralPath $BackupZip).Path
}
}

$summary.ok = $true
} catch {
Add-Step -Steps $steps -Name "deployment-gate" -Status "failed" -Details $_.Exception.Message
throw
} finally {
$summary.completedAt = (Get-Date).ToUniversalTime().ToString("o")
$evidenceDir = Split-Path -Parent $EvidencePath
if (-not [string]::IsNullOrWhiteSpace($evidenceDir)) {
New-Item -ItemType Directory -Force -Path $evidenceDir | Out-Null
}
[pscustomobject]$summary | ConvertTo-Json -Depth 12 |
Set-Content -Encoding utf8 -Path $EvidencePath
Write-Output "部署 gate 證據已寫入:$EvidencePath"
}
Loading