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
251 changes: 251 additions & 0 deletions .github/workflows/sync-live-state.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# 业务源码仓 liveState 同步入口。
# deploy 仓完成部署并生成 runtime facts 后,可通过 workflow_dispatch 或
# repository_dispatch 触发本 workflow。业务仓只消费非敏感 facts,不生成运行事实。

name: sync-live-state

on:
workflow_dispatch:
inputs:
source_repository:
description: Repository that uploaded runtime facts, for example owner/rtnn-deploy.
required: true
type: string
source_run_id:
description: Deploy workflow run id that uploaded runtime facts.
required: true
type: string
runtime_facts_artifact:
description: Runtime facts artifact name.
required: false
default: rtnn-runtime-facts
type: string
runtime_facts_file:
description: Runtime facts JSON path inside downloaded artifact.
required: false
default: runtime-facts.json
type: string
client_artifacts_artifact:
description: Optional client release artifacts name.
required: false
default: ""
type: string
environment:
description: Environment to check or sync. Empty means all environments in facts.
required: false
default: ""
type: string
mode:
description: Run read-only status or prepare a liveState-only PR.
required: false
default: status
type: choice
options:
- status
- prepare-pr
base_branch:
description: PR base branch.
required: false
default: main
type: string
create_pr:
description: Create PR when mode=prepare-pr and liveState changed.
required: false
default: true
type: boolean
repository_dispatch:
types:
- sync-rtnn-live-state

permissions:
actions: read
contents: write
pull-requests: write

concurrency:
group: sync-live-state-${{ github.event.client_payload.environment || inputs.environment || github.ref }}
cancel-in-progress: false

jobs:
sync-live-state:
name: Sync Live State
if: ${{ vars.RTNN_BUSINESS_SOURCE_WORKFLOWS_ENABLED == 'true' }}
runs-on: ${{ vars.RTNN_RELEASE_EXECUTION_MODE == 'github-hosted' && 'ubuntu-latest' || 'self-hosted' }}
timeout-minutes: 20

steps:
- name: Resolve inputs
id: input
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_SOURCE_RUN_ID: ${{ inputs.source_run_id }}
INPUT_SOURCE_REPOSITORY: ${{ inputs.source_repository }}
INPUT_RUNTIME_ARTIFACT: ${{ inputs.runtime_facts_artifact }}
INPUT_RUNTIME_FILE: ${{ inputs.runtime_facts_file }}
INPUT_CLIENT_ARTIFACT: ${{ inputs.client_artifacts_artifact }}
INPUT_ENVIRONMENT: ${{ inputs.environment }}
INPUT_MODE: ${{ inputs.mode }}
INPUT_BASE_BRANCH: ${{ inputs.base_branch }}
INPUT_CREATE_PR: ${{ inputs.create_pr }}
PAYLOAD_SOURCE_RUN_ID: ${{ github.event.client_payload.source_run_id }}
PAYLOAD_SOURCE_REPOSITORY: ${{ github.event.client_payload.source_repository }}
PAYLOAD_RUNTIME_ARTIFACT: ${{ github.event.client_payload.runtime_facts_artifact }}
PAYLOAD_RUNTIME_FILE: ${{ github.event.client_payload.runtime_facts_file }}
PAYLOAD_CLIENT_ARTIFACT: ${{ github.event.client_payload.client_artifacts_artifact }}
PAYLOAD_ENVIRONMENT: ${{ github.event.client_payload.environment }}
PAYLOAD_MODE: ${{ github.event.client_payload.mode }}
PAYLOAD_BASE_BRANCH: ${{ github.event.client_payload.base_branch }}
PAYLOAD_CREATE_PR: ${{ github.event.client_payload.create_pr }}
run: |
if [[ "${EVENT_NAME}" == "repository_dispatch" ]]; then
source_run_id="${PAYLOAD_SOURCE_RUN_ID}"
source_repository="${PAYLOAD_SOURCE_REPOSITORY}"
runtime_artifact="${PAYLOAD_RUNTIME_ARTIFACT:-rtnn-runtime-facts}"
runtime_file="${PAYLOAD_RUNTIME_FILE:-runtime-facts.json}"
client_artifact="${PAYLOAD_CLIENT_ARTIFACT}"
environment="${PAYLOAD_ENVIRONMENT}"
mode="${PAYLOAD_MODE:-status}"
base_branch="${PAYLOAD_BASE_BRANCH:-main}"
create_pr="${PAYLOAD_CREATE_PR:-true}"
else
source_run_id="${INPUT_SOURCE_RUN_ID}"
source_repository="${INPUT_SOURCE_REPOSITORY}"
runtime_artifact="${INPUT_RUNTIME_ARTIFACT:-rtnn-runtime-facts}"
runtime_file="${INPUT_RUNTIME_FILE:-runtime-facts.json}"
client_artifact="${INPUT_CLIENT_ARTIFACT}"
environment="${INPUT_ENVIRONMENT}"
mode="${INPUT_MODE:-status}"
base_branch="${INPUT_BASE_BRANCH:-main}"
create_pr="${INPUT_CREATE_PR:-true}"
fi

[[ -n "${source_run_id}" ]] || { echo "source_run_id is required"; exit 1; }
[[ -n "${source_repository}" ]] || { echo "source_repository is required"; exit 1; }

{
echo "source_run_id=${source_run_id}"
echo "source_repository=${source_repository}"
echo "runtime_artifact=${runtime_artifact}"
echo "runtime_file=${runtime_file}"
echo "client_artifact=${client_artifact}"
echo "environment=${environment}"
echo "mode=${mode}"
echo "base_branch=${base_branch}"
echo "create_pr=${create_pr}"
} >> "${GITHUB_OUTPUT}"

- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
version: 10.17.0

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 20
cache: pnpm
cache-dependency-path: |
package.json
pnpm-workspace.yaml
apps/admin/package.json
apps/app/package.json
apps/backend/package.json
apps/weapp/package.json
clients/*/package.json
packages/*/package.json

- name: Install dependencies
run: pnpm install --no-frozen-lockfile

- name: Download runtime facts
uses: actions/download-artifact@v4
with:
run-id: ${{ steps.input.outputs.source_run_id }}
repository: ${{ steps.input.outputs.source_repository }}
github-token: ${{ secrets.DEPLOY_REPOSITORY_READ_TOKEN || secrets.DEPLOY_REPOSITORY_DISPATCH_TOKEN || secrets.GITHUB_TOKEN }}
name: ${{ steps.input.outputs.runtime_artifact }}
path: artifacts/runtime-facts

- name: Download client release artifacts
if: ${{ steps.input.outputs.client_artifact != '' }}
uses: actions/download-artifact@v4
with:
run-id: ${{ steps.input.outputs.source_run_id }}
repository: ${{ steps.input.outputs.source_repository }}
github-token: ${{ secrets.DEPLOY_REPOSITORY_READ_TOKEN || secrets.DEPLOY_REPOSITORY_DISPATCH_TOKEN || secrets.GITHUB_TOKEN }}
name: ${{ steps.input.outputs.client_artifact }}
path: artifacts/client-release

- name: Release status
id: status
continue-on-error: ${{ steps.input.outputs.mode == 'prepare-pr' }}
shell: bash
env:
RUNTIME_FILE: artifacts/runtime-facts/${{ steps.input.outputs.runtime_file }}
CLIENT_ARTIFACT: ${{ steps.input.outputs.client_artifact }}
ENVIRONMENT: ${{ steps.input.outputs.environment }}
run: |
args=(--facts-file "${RUNTIME_FILE}" --output-dir artifacts/release-status)
if [[ -n "${ENVIRONMENT}" ]]; then
args+=(--environment "${ENVIRONMENT}")
fi
if [[ -n "${CLIENT_ARTIFACT}" ]]; then
args+=(--client-artifacts-dir artifacts/client-release)
fi

node scripts/release/run-release-status-ci.mjs "${args[@]}"

- name: Block invalid release facts before PR
if: ${{ steps.input.outputs.mode == 'prepare-pr' && (steps.status.outputs.status == 'blocked' || steps.status.outputs.status == '') }}
shell: bash
run: |
echo "release status is blocked; refusing to prepare liveState PR"
echo "code=${{ steps.status.outputs.code }}"
exit 1

- name: Prepare liveState PR
id: live_state_pr
if: ${{ steps.input.outputs.mode == 'prepare-pr' && steps.status.outputs.status != 'blocked' && steps.status.outputs.status != '' }}
shell: bash
env:
RUNTIME_FILE: artifacts/runtime-facts/${{ steps.input.outputs.runtime_file }}
CLIENT_ARTIFACT: ${{ steps.input.outputs.client_artifact }}
ENVIRONMENT: ${{ steps.input.outputs.environment }}
BASE_BRANCH: ${{ steps.input.outputs.base_branch }}
CREATE_PR: ${{ steps.input.outputs.create_pr }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
args=(--facts-file "${RUNTIME_FILE}" --output-dir artifacts/live-state-pr --base-branch "${BASE_BRANCH}")
if [[ -n "${ENVIRONMENT}" ]]; then
args+=(--environment "${ENVIRONMENT}")
fi
if [[ -n "${CLIENT_ARTIFACT}" ]]; then
args+=(--client-artifacts-dir artifacts/client-release)
fi
if [[ "${CREATE_PR}" == "true" ]]; then
args+=(--create-pr)
else
args+=(--no-push)
fi

node scripts/release/run-live-state-pr-ci.mjs "${args[@]}"

- name: Upload release status
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: rtnn-release-status
path: artifacts/release-status

- name: Upload liveState PR summary
if: ${{ always() && steps.input.outputs.mode == 'prepare-pr' }}
uses: actions/upload-artifact@v4
with:
name: rtnn-live-state-pr
path: artifacts/live-state-pr
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ pnpm run template:init -- --project-id=acme --brand-name=ACME --rewrite-source -
- 业务仓手动执行 `promote-production` 发起正式发布
- 上游模板仓 `rtnn` 默认不直接拥有任何业务环境发布权
- `rtnn` 保留 `release-images / promote-production` 作为模板源码的一部分,但只有 `project.role=business-source` 的业务仓会真正执行它们
- deploy 仓生成运行事实报告,业务仓用 `pnpm run release:sync-live-state` 校验或写回 `.rtnn/project.json liveState`
- deploy 仓生成运行事实报告,业务仓用 `pnpm run release:status` 只读校验线上状态,并用显式 sync/PR 准备命令写回 `.rtnn/project.json liveState`

## 验收入口

Expand All @@ -112,8 +112,13 @@ pnpm run check:quick
pnpm run check:backend-release
pnpm run check:template-bootstrap
pnpm run check:template-derivation
pnpm run check:client-release
pnpm run profile:doctor
pnpm run check:release-candidate
pnpm run release:status -- --facts-file /tmp/rtnn-runtime-facts.json
pnpm run release:status -- --facts-file /tmp/rtnn-runtime-facts.json --summary-md --output /tmp/rtnn-release-status.json
pnpm run release:status:ci -- --facts-file /tmp/rtnn-runtime-facts.json --output-dir /tmp/rtnn-release-status
pnpm run release:prepare-live-state-pr:ci -- --facts-file /tmp/rtnn-runtime-facts.json --environment testing --no-push
pnpm run release:check-runtime-freshness -- --facts-file /tmp/rtnn-runtime-facts.json
pnpm run smoke:admin
pnpm run check
Expand All @@ -122,8 +127,14 @@ pnpm run check
- `check:quick`:不主动启动 PostgreSQL,覆盖 lint、typecheck、admin UI 规则、模板中立性与契约漂移。
- `check:backend-release`:会在本地数据库配置下启动 PostgreSQL,执行测试 schema 残留预检、backend 发布基线、integration/e2e 并行隔离检查与测试后残留审计。
- `check:release-candidate`:发布候选入口,覆盖模板派生、契约、backend 发布门禁;设置 `RTNN_RUN_UI_SMOKE=true` 后追加多端 UI smoke。
- `check:client-release`:客户端发布链路统一检查入口,覆盖 release facts 解析、client liveState、release status、liveState PR 准备、surface gate 与相关脚本测试。
- `profile:doctor`:读取 `.rtnn/project.json` 和 project profile,输出启用服务、客户端构建目标、警告与接入风险,适合业务仓初次接入或配置回归检查。
- `release:check-runtime-freshness`:读取部署仓 runtime facts,判断 `.rtnn/project.json liveState` 是否代表线上实际版本;只读不写,写回仍使用 `release:sync-live-state`。
- `release:status`:回答“线上是否最新”的只读统一入口,输出稳定 `status/code/findings`,支持 `--summary-md` 和 `--output` 供 CI/PR comment 消费;不写回 `.rtnn/project.json`。
- `release:check-runtime-freshness`:底层 runtime freshness gate,读取部署仓 runtime facts,判断 `.rtnn/project.json liveState` 是否代表线上实际版本;只读不写,写回仍使用 `release:sync-live-state`。
- `release:prepare-live-state-pr`:CI 用 liveState-only PR 准备入口,只允许改写 `.rtnn/project.json liveState`,不提交、不推送、不创建 PR。
- `release:status:ci`:GitHub Actions / deploy 回调入口,运行 `release:status` 并写出 JSON、Markdown、GitHub outputs 和 step summary。
- `release:prepare-live-state-pr:ci`:liveState-only PR 编排入口,负责 branch/commit/push/可选 PR;写回逻辑仍由 `release:prepare-live-state-pr` 控制。
- `.github/workflows/sync-live-state.yml`:从 deploy 仓 workflow artifact 下载 runtime facts,并选择只读检查或准备 liveState-only PR;状态判断必须使用机器字段 `status/code`,code 语义见 `docs/operations/release-status-codes.md`。
- `smoke:*:ui`:CI 中使用 Playwright Chromium;Codex App 普通本地页面核验优先使用内置 Browser,不为普通本地验收安装 Chromium;显式设置 `RTNN_RUN_UI_SMOKE=true` 时缺浏览器会失败。
- `check`:本地完整质量门禁,覆盖静态检查、模板派生、契约、backend 发布门禁与多端构建。

Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/common/i18n/backend-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export function getBackendMessageFromCode(
code: string,
locale: SupportedLocale,
): string | undefined {
return codeMessages[locale][code as ApiErrorCode];
return codeMessages[locale][code];
}

export function getBackendMessageCode(message: string): string | undefined {
Expand Down
29 changes: 29 additions & 0 deletions docs/architecture/template-deployment-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,37 @@ backend 必须公开以下无鉴权探活/版本端点:
业务源码仓负责执行:

```bash
pnpm run release:status -- --facts-file /tmp/rtnn-runtime-facts.json
pnpm run release:status -- --facts-file /tmp/rtnn-runtime-facts.json --summary-md --output /tmp/rtnn-release-status.json
pnpm run release:sync-live-state -- --facts-file /tmp/rtnn-runtime-facts.json --check
pnpm run release:sync-live-state -- --facts-file /tmp/rtnn-runtime-facts.json --write
pnpm run release:prepare-live-state-pr -- --facts-file /tmp/rtnn-runtime-facts.json --summary-md /tmp/live-state-pr.md --json
pnpm run release:status:ci -- --facts-file /tmp/rtnn-runtime-facts.json --output-dir /tmp/rtnn-release-status
pnpm run release:prepare-live-state-pr:ci -- --facts-file /tmp/rtnn-runtime-facts.json --environment testing --no-push
```

`liveState` 是业务仓的非敏感事实,不是 deploy 仓的发布决策来源。
`release:status` 是只读入口,用来回答线上 runtime facts 是否与业务仓
`liveState` 一致;写回必须显式使用 sync 命令,或由 CI 使用
`release:prepare-live-state-pr` 准备 liveState-only PR。

业务源码仓还提供 `.github/workflows/sync-live-state.yml`。部署仓可在完成
deploy/smoke 后上传 `rtnn-runtime-facts` artifact,并以
`repository_dispatch` 的 `sync-rtnn-live-state` 事件触发业务仓:

```json
{
"event_type": "sync-rtnn-live-state",
"client_payload": {
"source_repository": "owner/rtnn-deploy",
"source_run_id": "1234567890",
"runtime_facts_artifact": "rtnn-runtime-facts",
"runtime_facts_file": "runtime-facts.json",
"environment": "testing",
"mode": "prepare-pr"
}
}
```

`mode=status` 只产出 release status artifact;`mode=prepare-pr` 会在业务仓
准备 liveState-only PR。CI 判断必须读取 `status/code`,不要解析人类文案。
6 changes: 4 additions & 2 deletions docs/operations/client-distribution-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ The release center should reflect runtime facts rather than invent a parallel
release truth. When a deploy executor syncs client release facts, the backend
stores accepted package metadata and the business repository can mirror
non-sensitive client state into `.rtnn/project.json liveState.<env>.clients`.
Use `release:sync-client-live-state` for the write-back/check flow and
`release:check-runtime-freshness` for environment freshness.
Use `release:status` for the read-only operator answer, optionally passing
`--client-artifacts-dir` to include client release facts. Use
`release:sync-client-live-state` only for the explicit write-back/check flow, or
`release:prepare-live-state-pr` when CI should prepare a liveState-only PR.

## Build Operations

Expand Down
Loading