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)