From db371be93cdfc3bc24b48c3ef5ef7fbc3c0852b6 Mon Sep 17 00:00:00 2001 From: RuoJi6 <79234113+RuoJi6@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:47:03 +0800 Subject: [PATCH] feat(demo): deploy published releases to Vercel (#15) --- .github/workflows/demo.yml | 189 ++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 11 ++ .gitignore | 4 +- README.md | 36 +++++++ web/vercel.json | 9 ++ 5 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/demo.yml create mode 100644 web/vercel.json diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml new file mode 100644 index 0000000..a9f5724 --- /dev/null +++ b/.github/workflows/demo.yml @@ -0,0 +1,189 @@ +name: deploy-demo + +on: + # 网页手工发布的正式版;Actions 自己发布的版本由 workflow_call 接入。 + release: + types: [published] + workflow_call: + inputs: + tag: + required: true + type: string + commit: + required: true + type: string + secrets: + VERCEL_TOKEN: + required: true + workflow_dispatch: + inputs: + tag: + description: "已发布的最新正式版标签;留空自动选择最新正式版" + required: false + type: string + +permissions: + contents: read + +# 不中断正在上传的部署;旧版本在构建前和上线前都会检查是否仍为 latest。 +concurrency: + group: demo-production + cancel-in-progress: false + +jobs: + deploy: + if: github.event_name != 'release' || github.event.release.prerelease == false + runs-on: ubuntu-latest + timeout-minutes: 25 + env: + VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }} + DEMO_URL: ${{ vars.DEMO_URL }} + NEXT_PUBLIC_MOCK: "1" + NEXT_TELEMETRY_DISABLED: "1" + steps: + - name: Select a published, complete, latest release + id: release + uses: actions/github-script@v7 + env: + REQUESTED_TAG: ${{ inputs.tag || github.event.release.tag_name }} + with: + script: | + const { owner, repo } = context.repo; + const { data: latest } = await github.rest.repos.getLatestRelease({ owner, repo }); + const tag = process.env.REQUESTED_TAG || latest.tag_name; + if (!/^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(tag)) { + throw new Error('只允许 v主版本.次版本.修订号 的正式标签'); + } + const { data: release } = await github.rest.repos.getReleaseByTag({ owner, repo, tag }); + if (release.draft || release.prerelease) throw new Error('草稿和预发布版本不可部署'); + const version = tag.slice(1); + const assets = new Set(release.assets.map(asset => asset.name)); + for (const platform of ['linux-amd64', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'windows-amd64']) { + if (!assets.has(`artex-${version}-${platform}.zip`)) throw new Error(`缺少 ${platform} 发布包`); + } + if (!assets.has('SHA256SUMS')) throw new Error('缺少发布校验文件'); + core.setOutput('tag', tag); + core.setOutput('current', String(tag === latest.tag_name)); + if (tag !== latest.tag_name) { + core.notice(`${tag} 已被 ${latest.tag_name} 替代,跳过以免覆盖新版 Demo`); + } + + - name: Check deployment configuration + if: steps.release.outputs.current == 'true' + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + for name in VERCEL_ORG_ID VERCEL_PROJECT_ID VERCEL_TOKEN DEMO_URL; do + if [ -z "${!name}" ]; then + echo "::error::请先在仓库 Actions 中配置 $name,参见 README 的在线 Demo 部署说明" + exit 1 + fi + done + + - uses: actions/checkout@v4 + if: steps.release.outputs.current == 'true' + with: + ref: refs/tags/${{ steps.release.outputs.tag }} + persist-credentials: false + + - name: Verify the released commit and write deployment metadata + if: steps.release.outputs.current == 'true' + env: + EXPECTED_COMMIT: ${{ inputs.commit }} + RELEASE_TAG: ${{ steps.release.outputs.tag }} + run: | + commit=$(git rev-parse HEAD) + if [ -n "$EXPECTED_COMMIT" ] && [ "$commit" != "$EXPECTED_COMMIT" ]; then + echo "::error::发布标签已移动,拒绝部署与 Release 构建不同的提交" + exit 1 + fi + mkdir -p web/public + jq -n --arg tag "$RELEASE_TAG" --arg commit "$commit" \ + --arg release "https://github.com/$GITHUB_REPOSITORY/releases/tag/$RELEASE_TAG" \ + '{tag: $tag, commit: $commit, release: $release, mock: true}' > web/public/demo-version.json + # 兼容首次部署尚未含此配置的历史正式版;CLI 部署不受此开关影响。 + jq -n '{"$schema":"https://openapi.vercel.sh/vercel.json", framework:"nextjs", installCommand:"npm ci", buildCommand:"npm run build", git:{deploymentEnabled:false}}' > web/vercel.json + + - uses: actions/setup-node@v4 + if: steps.release.outputs.current == 'true' + with: + node-version: "22" + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Install Vercel CLI + if: steps.release.outputs.current == 'true' + run: npm install --global vercel@64.2.0 + + - name: Configure the dedicated demo project + if: steps.release.outputs.current == 'true' + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + # CLI 以 web/ 为项目根;设置与 runner 一致的 Node 主版本。 + curl --fail-with-body --silent --show-error --request PATCH \ + --header "Authorization: Bearer $VERCEL_TOKEN" --header 'Content-Type: application/json' \ + --data '{"framework":"nextjs","nodeVersion":"22.x","rootDirectory":null,"installCommand":"npm ci","buildCommand":"npm run build"}' \ + "https://api.vercel.com/v9/projects/$VERCEL_PROJECT_ID?teamId=$VERCEL_ORG_ID" \ + --output /tmp/demo-project.json + curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $VERCEL_TOKEN" --header 'Content-Type: application/json' \ + --data '{"key":"NEXT_PUBLIC_MOCK","value":"1","type":"plain","target":["production"]}' \ + "https://api.vercel.com/v10/projects/$VERCEL_PROJECT_ID/env?teamId=$VERCEL_ORG_ID&upsert=true" \ + --output /tmp/demo-env.json + jq -e '(.failed // []) | length == 0' /tmp/demo-env.json + + - name: Pull production project settings + if: steps.release.outputs.current == 'true' + working-directory: web + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: vercel pull --yes --environment=production --token="$VERCEL_TOKEN" + + - name: Build the mock demo + if: steps.release.outputs.current == 'true' + working-directory: web + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: vercel build --prod --token="$VERCEL_TOKEN" + + - name: Check latest release again before publishing + if: steps.release.outputs.current == 'true' + id: publish + uses: actions/github-script@v7 + env: + RELEASE_TAG: ${{ steps.release.outputs.tag }} + with: + script: | + const { data: latest } = await github.rest.repos.getLatestRelease(context.repo); + const current = latest.tag_name === process.env.RELEASE_TAG; + core.setOutput('current', String(current)); + if (!current) core.notice('构建期间已有更新的正式版发布,保留线上版本,等待新版部署'); + + - name: Deploy the prebuilt demo to production + if: steps.publish.outputs.current == 'true' + id: deploy + working-directory: web + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + url=$(vercel deploy --prebuilt --prod --yes --token="$VERCEL_TOKEN") + # 空项目首次部署也明确绑定固定域名,后续版本沿用同一个网址。 + domain="${DEMO_URL#https://}" + domain="${domain%/}" + vercel alias set "$url" "$domain" --token="$VERCEL_TOKEN" + echo "url=$url" >> "$GITHUB_OUTPUT" + echo "### Demo 已部署" >> "$GITHUB_STEP_SUMMARY" + echo "- 正式版本:${{ steps.release.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY" + echo "- 部署地址:$url" >> "$GITHUB_STEP_SUMMARY" + + - name: Verify production version metadata + if: steps.publish.outputs.current == 'true' + env: + RELEASE_TAG: ${{ steps.release.outputs.tag }} + run: | + # 检查公开的生产域名;Vercel 可单独保护部署专属的随机域名。 + curl --fail --silent --show-error --retry 5 --retry-all-errors --retry-delay 3 \ + "${DEMO_URL%/}/demo-version.json" -o /tmp/demo-version.json + jq -e --arg tag "$RELEASE_TAG" '.tag == $tag and .mock == true' /tmp/demo-version.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c139df2..a38b0b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -181,6 +181,17 @@ jobs: - name: Publish complete release run: gh release edit "$RELEASE_TAG" --draft=false --latest + # GITHUB_TOKEN 发布的 Release 不会再次触发 release: published 工作流。 + # 直接依赖发布 job,保证 ZIP、校验文件和镜像全部发布成功后才更新 Demo。 + demo: + needs: [prepare, release] + uses: ./.github/workflows/demo.yml + with: + tag: ${{ needs.prepare.outputs.tag }} + commit: ${{ needs.prepare.outputs.commit }} + secrets: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + # ④ 在原生 AMD64 / ARM64 runner 上构建和测试,再推送架构镜像。 docker: needs: [prepare, binaries] diff --git a/.gitignore b/.gitignore index a01eacc..c9a7436 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ web/node_modules/ web/.next/ web/tsconfig.tsbuildinfo web/next-env.d.ts +.vercel/ +web/public/demo-version.json *.local # development tools @@ -57,4 +59,4 @@ Thumbs.db # bench 基准测试相关(本地产物,不提交) /bench/ /Dockerfile.bench -/Dockerfile.bench.v2 \ No newline at end of file +/Dockerfile.bench.v2 diff --git a/README.md b/README.md index b5c1fa7..470457f 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,42 @@ AI 自主渗透测试系统(Go 后端 + Next.js 前端) --- +## 部署自己的在线 Demo(随正式 Release 自动更新) + +Demo 只运行 `web/` 前端,构建时启用 `NEXT_PUBLIC_MOCK=1`,不需要 Go、PostgreSQL 或模型密钥。界面使用模拟数据,不执行真实扫描。 + +发布链路:正式版本的程序包、`SHA256SUMS` 和 Docker 镜像全部成功发布 → `release.yml` 的 `demo` job 调用 `demo.yml` → 检出该版本标签的提交 → 构建 Mock 前端 → 部署至 Vercel 的固定生产域名。普通 main 推送、仅创建标签、失败的发布、草稿和预发布版本都不会更新 Demo。新版本功能的演示数据仍需由维护者同步维护 `web/src/lib/mock/`。 + +### 首次配置 Vercel + +1. 在自己的 Vercel 账号创建空项目,设置名称(例如 `artex-demo-ruoji6`)。保持 **Git 未连接**,避免每次提交都自动部署。`web/vercel.json` 也禁用了 Git 自动部署。 +2. 在 **Settings → Build and Deployment** 中选择 **Next.js**、Node.js **22.x**,安装命令可覆盖为 `npm ci`,构建命令为 `npm run build`,Output Directory 保持默认。**Root Directory 留空**:此工作流从检出的 `web/` 目录调用 Vercel CLI,不能再填一层 `web`。 +3. 在项目的 **Environment Variables** 中添加 `NEXT_PUBLIC_MOCK=1`,至少应用于 **Production**。不要设置 `NEXT_EXPORT=1`,也不要填写数据库连接或模型密钥。 +4. 在项目 **Settings → General** 复制 **Project ID**;在团队 **Settings → General** 复制 **Team ID**(对应 `VERCEL_ORG_ID`)。确认 **Domains** 中有准备使用的生产域名。 +5. 在 Vercel **Account Settings → Tokens** 创建部署 Token,范围选择项目所在团队,并设置适当有效期。将其直接保存到 GitHub 仓库 **Settings → Secrets and variables → Actions → Secrets**,名称为 `VERCEL_TOKEN`。令牌不要写入仓库、README 或聊天;到期后更新这个 Secret。 + +在同一 GitHub 设置页的 **Variables** 中添加: + +| 变量 | 值 | +| --- | --- | +| `VERCEL_ORG_ID` | Vercel Team ID | +| `VERCEL_PROJECT_ID` | Vercel Project ID | +| `DEMO_URL` | 固定公开生产网址,含 `https://`,例如 `https://artex-demo-ruoji6.vercel.app` | + +### 首次部署与重试 + +将配置合并到默认分支后,打开 **Actions → deploy-demo → Run workflow**,分支选 `main`,tag 留空或填写当前最新正式版(例如 `v0.3.21`)。工作流只接受已发布且包含五个平台程序包和校验文件的最新正式版,也能部署尚未包含此工作流的历史正式版。无需重新发 Release 或移动已有标签。 + +工作流会将这个专用 Demo 项目的框架设为 Next.js、Node 设为 22.x、根目录设为空、安装命令设为 `npm ci`、构建命令设为 `npm run build`,并确保 Production 环境的 `NEXT_PUBLIC_MOCK=1`,随后绑定 `DEMO_URL` 指定的域名。请使用独立项目;后续部署会继续维护这些配置。 + +成功后访问生产域名,并检查 `/demo-version.json` 中的 `tag`、`commit` 和 `mock: true`。确认可访问后,将 README 顶部和截图区域的旧 Demo 链接一起改为自己的生产域名。 + +此后按正常流程发布更高版本即可自动更新 Demo。若 Demo 部署失败,已发布的程序包和镜像仍然有效,旧 Demo 通常继续提供服务;修复设置后单独重跑 `deploy-demo`,不必重新发布整个 Release。两个版本同时发布时,部署串行执行,并在构建前、上线前检查 latest,避免已过期的构建覆盖新版。 + +这里采用发布流程直接调用部署 job,是因为使用 `GITHUB_TOKEN` 创建的 Release 不会继续触发另一个 `release: published` 工作流。`demo.yml` 也监听网页手工发布的 Release,经过同样的正式版、完整发布包和 latest 检查后部署;手工重试则运行 `deploy-demo`。 + +--- + ## 安装 > 依赖数据库 **PostgreSQL**;探索需配置 **LLM**(`ANTHROPIC_API_KEY` 或 `OPENAI_API_KEY`,也可在 UI 里配)。 diff --git a/web/vercel.json b/web/vercel.json new file mode 100644 index 0000000..906739a --- /dev/null +++ b/web/vercel.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "nextjs", + "installCommand": "npm ci", + "buildCommand": "npm run build", + "git": { + "deploymentEnabled": false + } +}