# ============================================================================= # PR 构建工作流 # ============================================================================= # 安全模型: # 1. pull_request_target / issue_comment 入口只读取元数据、检查权限、更新评论并触发构建。 # 2. 真正执行 PR 代码的构建流程只在 workflow_dispatch 中运行。 # 3. 构建流程不具备写仓库/写评论/读 secrets 权限,显式清空 GITHUB_TOKEN,不持久化 checkout 凭据。 # 4. 版本号在 Action 中提前生成,通过 NAPCAT_VERSION 传给 Vite 插件,插件无需 GitHub token。 # 5. 发布 Release、触发 Docker 等可信操作只消费构建 artifact,不执行 PR artifact 中的代码。 # ============================================================================= name: Build PR on: pull_request_target: types: [opened, synchronize, reopened] issue_comment: types: [created] workflow_dispatch: inputs: pr_number: description: PR 编号 required: true type: string pr_sha: description: PR HEAD SHA required: true type: string pr_head_repo: description: PR 源仓库 required: true type: string pr_head_ref: description: PR 源分支 required: false type: string permissions: contents: read concurrency: group: pr-build-${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '/build') && github.event.issue.number || github.run_id }} cancel-in-progress: true jobs: # --------------------------------------------------------------------------- # 特权入口:只检查条件、写入“构建中”评论、触发无权限构建 # --------------------------------------------------------------------------- check-build: if: github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: write outputs: should_build: ${{ steps.check.outputs.should_build }} pr_number: ${{ steps.check.outputs.pr_number }} pr_sha: ${{ steps.check.outputs.pr_sha }} pr_head_repo: ${{ steps.check.outputs.pr_head_repo }} pr_head_ref: ${{ steps.check.outputs.pr_head_ref }} steps: - name: Checkout scripts uses: actions/checkout@v5 with: sparse-checkout: .github/scripts sparse-checkout-cone-mode: false persist-credentials: false - name: Setup Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 - name: Check trigger condition id: check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: node --experimental-strip-types .github/scripts/pr-build-check.ts update-comment-building: needs: check-build if: needs.check-build.outputs.should_build == 'true' runs-on: ubuntu-latest permissions: contents: read pull-requests: write issues: write steps: - name: Checkout scripts uses: actions/checkout@v5 with: sparse-checkout: .github/scripts sparse-checkout-cone-mode: false persist-credentials: false - name: Setup Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 - name: Update building comment env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ needs.check-build.outputs.pr_number }} PR_SHA: ${{ needs.check-build.outputs.pr_sha }} run: node --experimental-strip-types .github/scripts/pr-build-building.ts dispatch-build: needs: [check-build, update-comment-building] if: needs.check-build.outputs.should_build == 'true' runs-on: ubuntu-latest permissions: contents: read actions: write steps: - name: Checkout scripts uses: actions/checkout@v5 with: sparse-checkout: .github/scripts sparse-checkout-cone-mode: false persist-credentials: false - name: Setup Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 - name: Dispatch untrusted build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ needs.check-build.outputs.pr_number }} PR_SHA: ${{ needs.check-build.outputs.pr_sha }} PR_HEAD_REPO: ${{ needs.check-build.outputs.pr_head_repo }} PR_HEAD_REF: ${{ needs.check-build.outputs.pr_head_ref }} WORKFLOW_FILE: build-pr.yml DISPATCH_REF: ${{ github.event.repository.default_branch }} run: node --experimental-strip-types .github/scripts/pr-build-dispatch.ts # --------------------------------------------------------------------------- # 无权限构建:只由 workflow_dispatch 执行 PR 代码 # --------------------------------------------------------------------------- build-framework: if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: read outputs: status: ${{ steps.build.outcome }} error: ${{ steps.build.outputs.error }} version: ${{ steps.version.outputs.version }} steps: - name: Checkout scripts from base uses: actions/checkout@v5 with: sparse-checkout: .github/scripts sparse-checkout-cone-mode: false path: _scripts persist-credentials: false - name: Checkout PR code uses: actions/checkout@v5 with: repository: ${{ inputs.pr_head_repo }} ref: ${{ inputs.pr_sha }} path: workspace fetch-depth: 0 persist-credentials: false - name: Setup Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 9 run_install: false - name: Generate Version id: version working-directory: workspace run: | LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0") BASE_VERSION="${LATEST_TAG#v}" SHORT_SHA="${{ inputs.pr_sha }}" SHORT_SHA="${SHORT_SHA::7}" VERSION="${BASE_VERSION}-pr.${{ inputs.pr_number }}.${{ github.run_number }}+${SHORT_SHA}" echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "Latest tag: ${LATEST_TAG}" echo "Build version: ${VERSION}" - name: Build id: build working-directory: workspace env: GITHUB_TOKEN: '' NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }} run: node --experimental-strip-types ../_scripts/.github/scripts/pr-build-run.ts framework continue-on-error: true - name: Upload Artifact if: steps.build.outcome == 'success' uses: actions/upload-artifact@v6 with: name: NapCat.Framework path: workspace/framework-dist retention-days: 5 build-shell: if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: read outputs: status: ${{ steps.build.outcome }} error: ${{ steps.build.outputs.error }} version: ${{ steps.version.outputs.version }} steps: - name: Checkout scripts from base uses: actions/checkout@v5 with: sparse-checkout: .github/scripts sparse-checkout-cone-mode: false path: _scripts persist-credentials: false - name: Checkout PR code uses: actions/checkout@v5 with: repository: ${{ inputs.pr_head_repo }} ref: ${{ inputs.pr_sha }} path: workspace fetch-depth: 0 persist-credentials: false - name: Setup Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 9 run_install: false - name: Generate Version id: version working-directory: workspace run: | LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0") BASE_VERSION="${LATEST_TAG#v}" SHORT_SHA="${{ inputs.pr_sha }}" SHORT_SHA="${SHORT_SHA::7}" VERSION="${BASE_VERSION}-pr.${{ inputs.pr_number }}.${{ github.run_number }}+${SHORT_SHA}" echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "Latest tag: ${LATEST_TAG}" echo "Build version: ${VERSION}" - name: Build id: build working-directory: workspace env: GITHUB_TOKEN: '' NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }} run: node --experimental-strip-types ../_scripts/.github/scripts/pr-build-run.ts shell continue-on-error: true - name: Upload Artifact if: steps.build.outcome == 'success' uses: actions/upload-artifact@v6 with: name: NapCat.Shell path: workspace/shell-dist retention-days: 5 # --------------------------------------------------------------------------- # 可信后处理:只下载/打包/发布 artifact,不执行 artifact 中的代码 # --------------------------------------------------------------------------- publish-pr-release: needs: [build-framework, build-shell] if: always() && github.event_name == 'workflow_dispatch' && (needs.build-framework.outputs.status == 'success' || needs.build-shell.outputs.status == 'success') runs-on: ubuntu-latest permissions: contents: read actions: read outputs: release_url: ${{ steps.publish.outputs.release_url }} release_tag: ${{ steps.publish.outputs.release_tag }} install_url: ${{ steps.publish.outputs.install_url }} publish_status: ${{ steps.publish.outcome }} steps: - name: Checkout scripts uses: actions/checkout@v5 with: sparse-checkout: .github/scripts sparse-checkout-cone-mode: false persist-credentials: false - name: Setup Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 - name: Download Framework artifact if: needs.build-framework.outputs.status == 'success' uses: actions/download-artifact@v6 with: name: NapCat.Framework path: ./artifacts/NapCat.Framework - name: Download Shell artifact if: needs.build-shell.outputs.status == 'success' uses: actions/download-artifact@v6 with: name: NapCat.Shell path: ./artifacts/NapCat.Shell - name: Publish release id: publish continue-on-error: true env: RELEASE_PAT: ${{ secrets.PR_RELEASE_PAT }} PR_NUMBER: ${{ inputs.pr_number }} PR_SHA: ${{ inputs.pr_sha }} NAPCAT_VERSION: ${{ needs.build-framework.outputs.version || needs.build-shell.outputs.version || '' }} ARTIFACTS_DIR: ./artifacts run: | if [ -z "${RELEASE_PAT}" ]; then echo "::warning::PR_RELEASE_PAT secret 未配置,跳过 PR Release 发布" exit 0 fi node --experimental-strip-types .github/scripts/pr-publish-release.ts - name: Upload install script as artifact if: steps.publish.outcome == 'success' uses: actions/upload-artifact@v6 with: name: install-script path: ./release-assets/install.sh retention-days: 5 if-no-files-found: ignore update-comment-result: needs: [build-framework, build-shell, publish-pr-release] if: always() && github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: read actions: read pull-requests: write issues: write steps: - name: Checkout scripts uses: actions/checkout@v5 with: sparse-checkout: .github/scripts sparse-checkout-cone-mode: false persist-credentials: false - name: Setup Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 - name: Update result comment env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ inputs.pr_number }} PR_SHA: ${{ inputs.pr_sha }} RUN_ID: ${{ github.run_id }} NAPCAT_VERSION: ${{ needs.build-framework.outputs.version || needs.build-shell.outputs.version || '' }} FRAMEWORK_STATUS: ${{ needs.build-framework.outputs.status || 'cancelled' }} FRAMEWORK_ERROR: ${{ needs.build-framework.outputs.error }} SHELL_STATUS: ${{ needs.build-shell.outputs.status || 'cancelled' }} SHELL_ERROR: ${{ needs.build-shell.outputs.error }} INSTALL_URL: ${{ needs.publish-pr-release.outputs.install_url || '' }} RELEASE_URL: ${{ needs.publish-pr-release.outputs.release_url || '' }} PUBLISH_STATUS: ${{ needs.publish-pr-release.outputs.publish_status || '' }} run: node --experimental-strip-types .github/scripts/pr-build-result.ts trigger-docker: needs: [build-shell, publish-pr-release] if: github.event_name == 'workflow_dispatch' && needs.build-shell.outputs.status == 'success' && needs.publish-pr-release.outputs.release_tag != '' runs-on: ubuntu-latest permissions: contents: read pull-requests: write issues: write steps: - name: Checkout scripts uses: actions/checkout@v5 with: sparse-checkout: .github/scripts sparse-checkout-cone-mode: false persist-credentials: false - name: Setup Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 - name: Dispatch docker build continue-on-error: true env: DISPATCH_PAT: ${{ secrets.PR_RELEASE_PAT }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ inputs.pr_number }} PR_SHA: ${{ inputs.pr_sha }} PR_HEAD_REPO: ${{ inputs.pr_head_repo }} SOURCE_REPO: ${{ github.repository }} RELEASE_REPO: NapNeko/napcat-pr-release run: | if [ -z "${DISPATCH_PAT}" ]; then echo "::warning::PR_RELEASE_PAT secret 未配置,跳过 Docker 测试镜像构建" exit 0 fi node --experimental-strip-types .github/scripts/pr-docker-dispatch.ts