diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml new file mode 100644 index 000000000..a6844ad08 --- /dev/null +++ b/.gitea/workflows/project-ci.yml @@ -0,0 +1,280 @@ +name: Project CI + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + CI: 'true' + CARGO_INCREMENTAL: '0' + CARGO_TERM_COLOR: always + RUSTC_WRAPPER: '' + CARGO_BUILD_RUSTC_WRAPPER: '' + +jobs: + repository-checks: + name: Repository checks + runs-on: ubuntu-latest + steps: + - name: Checkout full history + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install base tools + shell: bash + run: | + set -euo pipefail + command -v apt-get >/dev/null 2>&1 || { + echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 + exit 1 + } + sudo_command='' + if command -v sudo >/dev/null 2>&1; then + sudo_command='sudo' + fi + ${sudo_command} apt-get update + ${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + curl + + - name: Set up Node.js 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22' + + - name: Resolve comparison base + shell: bash + run: | + set -euo pipefail + base_ref="$(node -e ' + const fs = require("node:fs"); + const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); + process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? ""); + ')" + if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then + git cat-file -e "${base_ref}^{commit}" 2>/dev/null || { + echo "comparison base commit is unavailable: ${base_ref}" >&2 + exit 1 + } + else + base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)" + fi + if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \ + && ! git merge-base --is-ancestor "${base_ref}" HEAD; then + echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2 + exit 1 + fi + echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}" + + - name: Set up repository Rust toolchain + shell: bash + run: | + set -euo pipefail + if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain none + fi + echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" + export PATH="${HOME}/.cargo/bin:${PATH}" + toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" + test -n "${toolchain}" + rustup toolchain install "${toolchain}" --profile minimal --component rustfmt + rustc --version + cargo --version + rustfmt --version + + - name: Install npm dependencies + run: npm ci + + - name: Run repository lint gates + run: npm run lint + + - name: Run frontend and script tests + run: npm run test + + - name: Build web applications + run: npm run build + + - name: Validate content data + run: npm run check:content + + - name: Check committed whitespace + shell: bash + run: | + set -euo pipefail + base_ref="${SPACETIME_SCHEMA_BASE_REF:-}" + test -n "${base_ref}" + git cat-file -e "${base_ref}^{commit}" + git diff --check "${base_ref}"...HEAD + + backend-checks: + name: Backend checks + runs-on: ubuntu-latest + steps: + - name: Checkout full history + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install backend build dependencies + shell: bash + run: | + set -euo pipefail + command -v apt-get >/dev/null 2>&1 || { + echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 + exit 1 + } + sudo_command='' + if command -v sudo >/dev/null 2>&1; then + sudo_command='sudo' + fi + ${sudo_command} apt-get update + ${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + clang \ + cmake \ + curl \ + libclang-dev \ + libcurl4-openssl-dev \ + libssl-dev \ + lld \ + pkg-config + + - name: Set up Node.js 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22' + + - name: Resolve comparison base + shell: bash + run: | + set -euo pipefail + base_ref="$(node -e ' + const fs = require("node:fs"); + const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); + process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? ""); + ')" + if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then + git cat-file -e "${base_ref}^{commit}" 2>/dev/null || { + echo "comparison base commit is unavailable: ${base_ref}" >&2 + exit 1 + } + else + base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)" + fi + if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \ + && ! git merge-base --is-ancestor "${base_ref}" HEAD; then + echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2 + exit 1 + fi + echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}" + + - name: Set up repository Rust toolchain + shell: bash + run: | + set -euo pipefail + if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain none + fi + echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" + export PATH="${HOME}/.cargo/bin:${PATH}" + toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" + test -n "${toolchain}" + rustup toolchain install "${toolchain}" --profile minimal --component rustfmt + rustc --version + cargo --version + rustfmt --version + + - name: Install npm dependencies + run: npm ci + + - name: Check server-rs boundaries + run: npm run check:server-rs-ddd + + - name: Check api-server targets + run: cargo check --locked -p api-server --all-targets --manifest-path server-rs/Cargo.toml + + - name: Check SpacetimeDB module + run: cargo check --locked -p spacetime-module --manifest-path server-rs/Cargo.toml + + native-shell-checks: + name: Native shell checks + runs-on: ubuntu-latest + steps: + - name: Checkout full history + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install native shell build dependencies + shell: bash + run: | + set -euo pipefail + command -v apt-get >/dev/null 2>&1 || { + echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2 + exit 1 + } + sudo_command='' + if command -v sudo >/dev/null 2>&1; then + sudo_command='sudo' + fi + ${sudo_command} apt-get update + ${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + clang \ + cmake \ + curl \ + file \ + libayatana-appindicator3-dev \ + libssl-dev \ + libwebkit2gtk-4.1-dev \ + libxdo-dev \ + librsvg2-dev \ + lld \ + patchelf \ + pkg-config \ + wget + + - name: Set up Node.js 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22' + + - name: Set up repository Rust toolchain + shell: bash + run: | + set -euo pipefail + if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain none + fi + echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}" + export PATH="${HOME}/.cargo/bin:${PATH}" + toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" + test -n "${toolchain}" + rustup toolchain install "${toolchain}" --profile minimal --component rustfmt + rustc --version + cargo --version + rustfmt --version + + - name: Install npm dependencies + run: npm ci + + - name: Run native shell gates + run: npm run check:native-shells + + - name: Ensure native lockfile is unchanged + run: git diff --exit-code -- apps/desktop-shell/src-tauri/Cargo.lock diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index 5630daee9..535861061 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -262,6 +262,15 @@ DDD 边界检查: npm run check:server-rs-ddd ``` +## Gitea CI 与 PR 检查 + +- 仓库 CI 入口是 `.gitea/workflows/project-ci.yml`,向 `master` 推送和所有 PR 创建、更新时必须运行,也允许手工触发。 +- CI 固定拆分为 `Repository checks`、`Backend checks`、`Native shell checks` 三个 required job;对应 PR context 完整名称是 `Project CI / Repository checks (pull_request)`、`Project CI / Backend checks (pull_request)`、`Project CI / Native shell checks (pull_request)`,首次运行后仍须从 Gitea 最近一周 context 表复核。前两项覆盖仓库/Web 与后端边界/编译,原生壳验收单独运行以便定位重型构建失败。 +- 三个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。 +- checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commit,workflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。 +- 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。 +- Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发和 crates.io;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述三个 job 全部成功。 + ## 后端相关默认验证 后端修改后,按 DDD 文档中的验收命令执行。涉及 API smoke 时: diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 24cd48b95..4b9cf53cd 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -1,6 +1,6 @@ # 本地开发验证与生产运维 -更新时间:`2026-07-17` +更新时间:`2026-07-21` ## 标准开发流程 @@ -198,6 +198,22 @@ npm run check `npm run build` 由 `scripts/build-gate.mjs` 串行构建主站和后台;该门禁会把 Vite warning 当成失败处理。若看到 `Build gate failed because warnings were emitted`,先看 warning 原文,例如 chunk 体积超过 `vite.config.ts` / `apps/admin-web/vite.config.ts` 的 `chunkSizeWarningLimit`,不要先按 Rust 编译失败排查。 +### Gitea Actions PR 门禁 + +仓库级 Gitea Actions 工作流固定为 `.gitea/workflows/project-ci.yml`,在向 `master` 推送、创建或更新 PR,以及手工触发时运行。工作流拆成三个必须通过的 job: + +- `Repository checks`:执行 `npm run lint`、完整 Vitest、主站与后台生产构建、内容数据检查和提交差异空白检查。 +- `Backend checks`:执行 `npm run check:server-rs-ddd`、`api-server --all-targets` 编译和 `spacetime-module` 编译。 +- `Native shell checks`:独立执行 `npm run check:native-shells`,覆盖微信壳、Expo 和 Tauri 的完整验收,并确认 Tauri `Cargo.lock` 没有被构建过程改写,避免把重型原生壳或依赖锁漂移隐藏在基础检查末尾。 + +三个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 server-rs DDD 与现役后端编译门禁。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。 + +PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME_SCHEMA_BASE_REF`。`check:spacetime-schema` 依赖该基线识别已有表字段删除、改名、重排和改类型;事件给出的基线缺失或本地不可解析时必须直接失败,不能退化为空差异检查。Gitea 的 PR checkout 是 PR head,不是与目标分支的预合并 commit,因此 workflow 还会验证 PR head 包含事件中的最新 base commit;分支保护必须继续开启“PR 过期禁止合并”,过期分支先更新再重跑。向 `master` 直接推送时使用 push before SHA,手工触发时回退到 `origin/master`。 + +启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld 和 Tauri Linux 依赖;受控 runner 镜像应预装 rustup,fallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发和 crates.io。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。Gitea 1.26 不执行 workflow 的 `timeout-minutes`,任务最长运行时间在 runner 全局配置收口,不能只在 YAML 写一个不会生效的超时值。 + +workflow 首次成功运行后,在 Gitea `master` 分支保护中把 `Project CI / Repository checks (pull_request)`、`Project CI / Backend checks (pull_request)`、`Project CI / Native shell checks (pull_request)` 三个完整 context 都设为合并必需检查,并从最近一周已上报 context 表复核名称后再保存。不能只填裸 job 名,否则无法匹配 Gitea 实际上报的 ` / ()`。只提交 workflow 文件不会自动创建 runner,也不会自动修改分支保护;如果 Actions 长时间停留在等待状态,先到仓库或组织的 Actions runner 页面确认存在在线、带 `ubuntu-latest` 标签的 runner。 + 视觉小说负向扫描与验收门禁: ```bash