From 8fb42300c1ae77beabaa52f00da8ab8eb293af49 Mon Sep 17 00:00:00 2001 From: Zyy Date: Mon, 28 Sep 2026 16:54:13 +0800 Subject: [PATCH] ci: streamline release checks and protect contribution rules Reuse test binaries and frontend builds, reduce redundant cache writes, and overlap release preparation with quality checks. Protect skills, contribution rules, and GitHub configuration, and simplify workflow names. Co-authored-by: Codex --- .github/actions/build-frontend/action.yml | 41 ++++++++++++ .github/workflows/_container.yml | 25 +++++--- .github/workflows/_quality.yml | 64 ++++++++----------- .github/workflows/ci.yml | 7 ++ .github/workflows/release.yml | 37 ++++++++--- .../{agents-md-guard.yml => rules.yml} | 33 ++++++---- .../{security-scan.yml => security.yml} | 4 +- CONTRIBUTING.md | 17 ++++- 8 files changed, 155 insertions(+), 73 deletions(-) create mode 100644 .github/actions/build-frontend/action.yml rename .github/workflows/{agents-md-guard.yml => rules.yml} (55%) rename .github/workflows/{security-scan.yml => security.yml} (92%) diff --git a/.github/actions/build-frontend/action.yml b/.github/actions/build-frontend/action.yml new file mode 100644 index 000000000..1f8b9403c --- /dev/null +++ b/.github/actions/build-frontend/action.yml @@ -0,0 +1,41 @@ +name: Build frontend +description: Validate and build the frontend, optionally uploading release assets +inputs: + upload-artifact: + description: Upload frontend-dist for downstream release jobs + default: 'false' +runs: + using: composite + steps: + - name: Setup pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + package_json_file: frontend/package.json + + - name: Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + + - name: Install frontend dependencies + shell: bash + run: pnpm --dir frontend install --frozen-lockfile + + - name: Frontend format + shell: bash + run: pnpm --dir frontend format:check + + - name: Frontend build + shell: bash + run: pnpm --dir frontend build + + - name: Upload frontend artifact + if: inputs.upload-artifact == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: frontend-dist + path: frontend/dist + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/_container.yml b/.github/workflows/_container.yml index 47f5696fc..7de06319b 100644 --- a/.github/workflows/_container.yml +++ b/.github/workflows/_container.yml @@ -1,4 +1,4 @@ -name: Reusable Container +name: Container on: workflow_call: @@ -31,17 +31,26 @@ jobs: uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Cache Cargo mounts + if: github.event_name == 'push' && github.ref == 'refs/heads/main' id: cargo-cache uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ runner.temp }}/container-cargo - key: container-cargo-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('backend/Cargo.lock', 'backend/**/Cargo.toml', 'deploy/Dockerfile') }}-${{ inputs.ref }} + # 同一平台和依赖复用快照,源码变化由 Cargo 指纹判定是否重编译 + key: container-cargo-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('backend/Cargo.lock', 'backend/rust-toolchain.toml', 'backend/**/Cargo.toml', 'backend/.cargo/config.toml', 'deploy/Dockerfile') }} restore-keys: | - container-cargo-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('backend/Cargo.lock', 'backend/**/Cargo.toml', 'deploy/Dockerfile') }}- - container-cargo-v1-${{ runner.os }}-${{ runner.arch }}- + container-cargo-v2-${{ runner.os }}-${{ runner.arch }}- - # The GHA layer exporter does not persist RUN --mount=type=cache contents. - # Extraction runs before actions/cache saves its directories in post steps. + - name: Restore Cargo mounts for pull requests + if: github.event_name != 'push' || github.ref != 'refs/heads/main' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ runner.temp }}/container-cargo + key: container-cargo-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('backend/Cargo.lock', 'backend/rust-toolchain.toml', 'backend/**/Cargo.toml', 'backend/.cargo/config.toml', 'deploy/Dockerfile') }} + restore-keys: | + container-cargo-v2-${{ runner.os }}-${{ runner.arch }}- + + # GHA 层缓存不含 cache mount,只有主分支缺少当前快照时才回收并保存 - name: Restore Cargo mounts to BuildKit uses: reproducible-containers/buildkit-cache-dance@5422eac04292c961a382e0f584ea0f03ad9da723 # v3.4.0 with: @@ -53,7 +62,7 @@ jobs: "${{ runner.temp }}/container-cargo/git": "/usr/local/cargo/git", "${{ runner.temp }}/container-cargo/target": "/app/backend/target" } - skip-extraction: ${{ steps.cargo-cache.outputs.cache-hit }} + skip-extraction: ${{ github.event_name != 'push' || github.ref != 'refs/heads/main' || steps.cargo-cache.outputs.cache-hit == 'true' }} - name: Build runtime image uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 @@ -67,7 +76,7 @@ jobs: runtime-base runtime cache-from: type=gha,scope=runtime-ci - cache-to: type=gha,mode=max,scope=runtime-ci + cache-to: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'type=gha,mode=max,scope=runtime-ci' || '' }} - name: Verify runtime image uses: ./.github/actions/verify-container diff --git a/.github/workflows/_quality.yml b/.github/workflows/_quality.yml index 5eee456c7..fe923ddfa 100644 --- a/.github/workflows/_quality.yml +++ b/.github/workflows/_quality.yml @@ -1,4 +1,4 @@ -name: Reusable Quality +name: Quality on: workflow_call: @@ -7,11 +7,6 @@ on: description: Exact commit SHA to validate required: true type: string - upload_frontend: - description: Upload the built frontend for release jobs - required: false - default: false - type: boolean run_backend: description: Run backend quality checks required: false @@ -111,6 +106,7 @@ jobs: uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: backend -> target + save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - name: Rust format working-directory: backend @@ -120,6 +116,19 @@ jobs: working-directory: backend run: cargo clippy --all-targets --all-features --locked -- -D warnings + - name: Setup nextest + uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 + with: + tool: cargo-nextest@0.9.143 + fallback: none + + - name: Build Rust tests once + working-directory: backend + run: | + cargo metadata --format-version 1 --locked > "$RUNNER_TEMP/nextest-cargo-metadata.json" + cargo nextest list --test main --locked --list-type binaries-only --message-format json \ + > "$RUNNER_TEMP/nextest-binaries.json" + - name: Configure PostgreSQL and Redis test authentication shell: bash run: | @@ -144,7 +153,11 @@ jobs: - name: Rust tests working-directory: backend - run: cargo test --test main --locked + run: | + cargo nextest run \ + --cargo-metadata "$RUNNER_TEMP/nextest-cargo-metadata.json" \ + --binaries-metadata "$RUNNER_TEMP/nextest-binaries.json" \ + --no-tests fail - name: Store tests with Redis ACL authentication working-directory: backend @@ -159,7 +172,11 @@ jobs: docker exec --env REDISCLI_AUTH="$redis_acl_password" "${{ job.services.redis.id }}" \ redis-cli -e --user cpr_acl ACL SETUSER default off >/dev/null export CPR_TEST_REDIS_URL="redis://cpr_acl:${redis_acl_password}@127.0.0.1:6379" - cargo test -p gateway-store --test main --locked + # 从同一份构建清单选择 Redis 测试,保留 workspace 特性图且不重复运行 PostgreSQL + cargo nextest run \ + --cargo-metadata "$RUNNER_TEMP/nextest-cargo-metadata.json" \ + --binaries-metadata "$RUNNER_TEMP/nextest-binaries.json" \ + --no-tests fail -E 'package(=gateway-store) & test(/^redis::/)' frontend: if: inputs.run_frontend @@ -173,35 +190,8 @@ jobs: with: ref: ${{ inputs.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - package_json_file: frontend/package.json - - - name: Setup Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: frontend/pnpm-lock.yaml - - - name: Install frontend dependencies - run: pnpm --dir frontend install --frozen-lockfile - - - name: Frontend format - run: pnpm --dir frontend format:check - - - name: Frontend build - run: pnpm --dir frontend build - - - name: Upload frontend artifact - if: inputs.upload_frontend - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: frontend-dist - path: frontend/dist - if-no-files-found: error - retention-days: 1 + - name: Validate and build frontend + uses: ./.github/actions/build-frontend workflow-lint: runs-on: ubuntu-latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 860a5cecb..966e0eb87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,14 +38,19 @@ jobs: id: filter uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 with: + # 目录规则取并集,再统一排除说明文档,保留隐藏配置与子模块指针 + predicate-quantifier: some-with-excludes filters: | backend: - 'backend/**' - 'release/version.yaml' - '.github/workflows/**' + - '!**/*.md' frontend: - 'frontend/**' + - '.github/actions/build-frontend/**' - '.github/workflows/**' + - '!**/*.md' source: - '.gitmodules' - 'modules/ui' @@ -53,6 +58,7 @@ jobs: - 'frontend/**' - 'backend/crates/gateway-plugin/sdk/**' - '.github/workflows/ci.yml' + - '!**/*.md' container: - 'backend/**' - 'frontend/**' @@ -62,6 +68,7 @@ jobs: - '.github/actions/**' - '.github/workflows/_container.yml' - '.github/workflows/ci.yml' + - '!**/*.md' quality: needs: changes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 616fd73e8..618622fe2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,7 +170,24 @@ jobs: uses: ./.github/workflows/_quality.yml with: ref: ${{ needs.release-info.outputs.git_sha }} - upload_frontend: true + run_frontend: false + + frontend: + needs: release-info + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + CI: 'true' + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.release-info.outputs.git_sha }} + + - name: Validate and build frontend + uses: ./.github/actions/build-frontend + with: + upload-artifact: 'true' build-docker-binary: name: build-docker-binary (${{ matrix.os }}-${{ matrix.arch }}) @@ -194,10 +211,10 @@ jobs: ~/.cargo/registry ~/.cargo/git backend/target - key: cargo-v2-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock', 'backend/rust-toolchain.toml', 'backend/**/Cargo.toml', 'backend/.cargo/config.toml') }}-${{ needs.release-info.outputs.git_sha }} + # 按目标、工具链和依赖保存,不为每个发行提交复制整个 target + key: cargo-v3-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock', 'backend/rust-toolchain.toml', 'backend/**/Cargo.toml', 'backend/.cargo/config.toml', '.github/workflows/release.yml') }} restore-keys: | - cargo-v2-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock', 'backend/rust-toolchain.toml', 'backend/**/Cargo.toml', 'backend/.cargo/config.toml') }} - cargo-v2-${{ runner.os }}-${{ matrix.target }}- + cargo-v3-${{ runner.os }}-${{ matrix.target }}- - name: Build binary shell: bash @@ -260,10 +277,9 @@ jobs: ~/.cargo/registry ~/.cargo/git backend/target - key: cargo-v2-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock', 'backend/rust-toolchain.toml', 'backend/**/Cargo.toml', 'backend/.cargo/config.toml') }}-${{ needs.release-info.outputs.git_sha }} + key: cargo-v3-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock', 'backend/rust-toolchain.toml', 'backend/**/Cargo.toml', 'backend/.cargo/config.toml', '.github/workflows/release.yml') }} restore-keys: | - cargo-v2-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock', 'backend/rust-toolchain.toml', 'backend/**/Cargo.toml', 'backend/.cargo/config.toml') }} - cargo-v2-${{ runner.os }}-${{ matrix.target }}- + cargo-v3-${{ runner.os }}-${{ matrix.target }}- - name: Build binary shell: bash @@ -287,7 +303,7 @@ jobs: retention-days: 1 package-assets: - needs: [release-info, quality, build-docker-binary, build-binary] + needs: [release-info, frontend, build-docker-binary, build-binary] runs-on: ubuntu-latest timeout-minutes: 20 permissions: @@ -443,7 +459,7 @@ jobs: build-docker-image: name: build-docker-image (${{ matrix.dockerPlatform }}) - needs: [release-info, quality, build-docker-binary, package-assets] + needs: [release-info, build-docker-binary, package-assets] strategy: fail-fast: false matrix: ${{ fromJson(needs.release-info.outputs.docker_matrix) }} @@ -601,7 +617,8 @@ jobs: retention-days: 1 publish: - needs: [release-info, package-assets, build-docker-image] + # 产物准备可与测试重叠,版本标签和 GitHub Release 必须等全部质量与镜像验证通过 + needs: [release-info, quality, package-assets, build-docker-image] runs-on: ubuntu-latest timeout-minutes: 20 permissions: diff --git a/.github/workflows/agents-md-guard.yml b/.github/workflows/rules.yml similarity index 55% rename from .github/workflows/agents-md-guard.yml rename to .github/workflows/rules.yml index 54c55b054..4c3a407a7 100644 --- a/.github/workflows/agents-md-guard.yml +++ b/.github/workflows/rules.yml @@ -1,4 +1,4 @@ -name: Agent Instructions Guard +name: Rules Guard on: pull_request_target: @@ -13,7 +13,7 @@ permissions: pull-requests: write concurrency: - group: agents-md-guard-${{ github.event.pull_request.number }} + group: rules-${{ github.event.pull_request.number }} cancel-in-progress: false jobs: @@ -22,7 +22,7 @@ jobs: timeout-minutes: 5 steps: # 只查询 PR 元数据,禁止检出或执行贡献分支中的代码。 - - name: Enforce agent instructions policy + - name: Check protected files uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | @@ -33,12 +33,15 @@ jobs: ...params, per_page: 100, }); - const isInstructions = path => typeof path === 'string' - && (path === 'AGENTS.md' || path.endsWith('/AGENTS.md')); + const isProtectedRule = path => typeof path === 'string' + && (path === 'AGENTS.md' || path.endsWith('/AGENTS.md') + || path === 'CONTRIBUTING.md' + || path === '.agents/skills' || path.startsWith('.agents/skills/') + || path === '.github' || path.startsWith('.github/')); // 同时检查重命名前后的路径,避免通过移走规则文件绕过检查。 - if (!files.some(file => isInstructions(file.filename) || isInstructions(file.previous_filename))) { - core.info('No agent instruction changes detected.'); + if (!files.some(file => isProtectedRule(file.filename) || isProtectedRule(file.previous_filename))) { + core.info('No protected contribution rule changes detected.'); return; } @@ -49,27 +52,31 @@ jobs: return; } + // 通知标记保持稳定,以便复用机器人已有评论,避免重复提醒 const marker = ''; const body = [ marker, - '此 PR 修改了 `AGENTS.md`,已按贡献规则关闭。', + '佬友,我看到这个 PR 涉及受保护的协作规则文件,按仓库约定需要先关闭', '', - 'PR 不接受根目录或子目录中 `AGENTS.md` 的新增、修改、删除或重命名。', - '请撤销相关变更后重新打开 PR;如需调整协作规则,请先提交 Issue 说明原因和建议,由维护者处理。', + '保护范围包括根目录或子目录的 `AGENTS.md`、根目录的 `CONTRIBUTING.md`,以及 `.agents/skills/` 和 `.github/` 中的所有文件,涵盖新增、修改、删除和重命名', + '', + '麻烦先撤销这些变更,再重新打开 PR;如果希望调整协作规则,可以先提个 Issue 说明原因和建议,交由维护者处理,感谢理解', ].join('\n'); try { const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100, }); - const notified = comments.some(comment => comment.user?.login === 'github-actions[bot]' + const notification = comments.find(comment => comment.user?.login === 'github-actions[bot]' && comment.body?.includes(marker)); - if (!notified) { + if (!notification) { await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body }); + } else if (notification.body !== body) { + await github.rest.issues.updateComment({ owner, repo, comment_id: notification.id, body }); } } catch (error) { core.warning(`Unable to post policy explanation: ${error.message}`); } await github.rest.pulls.update({ ...params, state: 'closed' }); - core.setFailed('Pull requests must not change AGENTS.md.'); + core.setFailed('Pull requests must not change protected contribution rules (AGENTS.md, CONTRIBUTING.md, .agents/skills/, .github/).'); diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security.yml similarity index 92% rename from .github/workflows/security-scan.yml rename to .github/workflows/security.yml index acc317558..f750d5af9 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security.yml @@ -1,4 +1,4 @@ -name: Security Scan +name: Security on: push: @@ -14,7 +14,7 @@ permissions: contents: read concurrency: - group: security-scan-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }} + group: security-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true jobs: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7636631f..822ebcf11 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,14 @@ PR 正文说明提交者是否使用 AI。使用 AI 参与方案、代码、文 ### 规则文件保护 -PR 不接受根目录或子目录中 `AGENTS.md` 的新增、修改、删除或重命名。调整这些文件时先提交 Issue 说明原因和建议,由维护者处理;[规则保护工作流](.github/workflows/agents-md-guard.yml) 会提示并关闭包含此类变更的 PR。撤销相关变更后可以重新打开 PR,继续正常检查 +PR 不接受以下协作规则文件的新增、修改、删除或重命名: + +- 根目录及任意子目录的 `AGENTS.md` +- 根目录的 `CONTRIBUTING.md` +- `.agents/skills/` 中的所有文件,包括技能入口、引用文档、配置和脚本 +- `.github/` 中的所有文件,包括工作流、Actions、Issue/PR 模板和依赖更新配置 + +调整这些文件时先提交 Issue 说明原因和建议,由维护者处理;[规则保护工作流](.github/workflows/rules.yml) 会提示并关闭包含此类变更的 PR。撤销相关变更后可以重新打开 PR,继续正常检查 源码变更遵守现有架构边界与迁移冻结规则,由对应 CI 检查执行;不额外设立整块业务目录的 PR 禁改清单。构建、测试和安全扫描复用现有工作流 @@ -184,13 +191,17 @@ git diff --check `build` 已包含类型检查;不维护独立前端测试代码。后端的工具链、命令与线程栈设置统一见 [修改与验收](docs/architecture.md#12-修改与验收) -依赖 PostgreSQL / Redis 的测试使用 [专用测试环境](backend/migrations/README.md#本地测试库),未配置导致跳过时明确记录。CI 中已有对应服务和检查,见 [质量工作流](.github/workflows/_quality.yml) 与 [安全扫描](.github/workflows/security-scan.yml) +依赖 PostgreSQL / Redis 的测试使用 [专用测试环境](backend/migrations/README.md#本地测试库),未配置导致跳过时明确记录。CI 中已有对应服务和检查,见 [质量工作流](.github/workflows/_quality.yml) 与 [安全扫描](.github/workflows/security.yml) 集成变更应验证实际业务路径及结果,不能只凭健康检查或探测成功认定全链路正常。协议和网络改动按需核对认证、证书、代理、流式交付及客户端结果;记录影响复现的环境条件。缺少运行条件时说明缺口,不用模拟结果代替真实集成结论 ## 自动检查与合并条件 -[CI](.github/workflows/ci.yml) 已按变更范围调用现有质量检查。新增自动规则优先使用现有工具,要求依据明确、结果稳定、误报可控;无法可靠判定的设计与视觉问题保留审查判断,避免把具体源码写法锁成测试 +[CI](.github/workflows/ci.yml) 按变更范围调用质量检查,Markdown 不触发后端、前端、容器和源码联调任务,工作流语法检查独立执行 + +后端 CI 使用 nextest 执行 `main` 集成测试,默认 Redis 密码模式执行完整测试集,ACL 用户模式复用同一批构建产物执行 Store 的 Redis 测试;本地验证命令见 [架构文档](docs/architecture.md#验证命令) + +新增自动规则优先使用现有工具,要求依据明确、结果稳定、误报可控;无法可靠判定的设计与视觉问题保留审查判断,避免把具体源码写法锁成测试 AI 自动审查需要在所用工具中单独启用。其支持的检查范围和优先级过滤以工具说明为准;即使能读取 `AGENTS.md`,也不能保证所有可维护性和视觉问题都会被报告。配置参考 [Codex GitHub 审查文档](https://learn.chatgpt.com/docs/third-party/github)