From 3e794e123e749c1b0c9fe078676239cbcb363934 Mon Sep 17 00:00:00 2001 From: gdz66601 Date: Wed, 23 Sep 2026 01:22:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(update):=20add=20marked=20forced=20updates?= =?UTF-8?q?=20/=20=E6=96=B0=E5=A2=9E=E6=A0=87=E8=AE=B0=E5=BC=8F=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 13 +++++ .github/workflows/force-update.yml | 84 ++++++++++++++++++++++++++++++ DEPLOYMENT.md | 12 +++++ README.md | 6 ++- tests/deployment.test.ts | 18 +++++++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/force-update.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 79ef2af..6b27449 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,6 +10,16 @@ on: description: Cloudflare 模式的管理员邮箱(也可保存为 ADMIN_EMAIL 变量;GitHub 模式不填) required: false type: string + workflow_call: + inputs: + admin_email: + description: Cloudflare 模式的管理员邮箱 + required: false + type: string + deployment_ref: + description: 需要检出并部署的提交 SHA + required: false + type: string concurrency: group: production-deploy @@ -40,6 +50,9 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v4 + with: + # 强制更新通过可复用工作流传入目标提交,避免推送后仍部署触发任务的旧版本。 + ref: ${{ inputs.deployment_ref || github.sha }} - name: Set up Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/force-update.yml b/.github/workflows/force-update.yml new file mode 100644 index 0000000..34dfc7b --- /dev/null +++ b/.github/workflows/force-update.yml @@ -0,0 +1,84 @@ +name: Force Update + +on: + schedule: + - cron: "17 * * * *" + workflow_dispatch: + +concurrency: + group: upstream-auto-update + cancel-in-progress: false + +permissions: + contents: write + +env: + OFFICIAL_REPOSITORY: aozorae/EdgeSSH + OFFICIAL_BRANCH: main + +jobs: + update: + name: Check official release markers + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + target_sha: ${{ steps.update.outputs.target_sha }} + updated: ${{ steps.update.outputs.updated }} + steps: + - name: Check out user repository + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + + - name: Find marked commit and update main + id: update + shell: bash + run: | + git remote add official "https://github.com/${OFFICIAL_REPOSITORY}.git" + git fetch --no-tags official "${OFFICIAL_BRANCH}" + + current_sha="$(git rev-parse refs/remotes/origin/main)" + official_sha="$(git rev-parse "refs/remotes/official/${OFFICIAL_BRANCH}")" + target_sha="" + + # rev-list 按拓扑从新到旧扫描,只接受完整 Git trailer,防止正文中的同名文字误触发更新。 + while read -r sha; do + marker="$(git show -s --format='%(trailers:key=EdgeSSH-Auto-Update,valueonly,unfold)' "${sha}")" + if [[ "${marker}" == "true" ]]; then + target_sha="${sha}" + break + fi + done < <(git rev-list --topo-order "refs/remotes/official/${OFFICIAL_BRANCH}" --not "${current_sha}") + + if [[ -z "${target_sha}" ]]; then + echo "updated=false" >> "${GITHUB_OUTPUT}" + { + echo "## Force Update" + echo + echo "No commit marked \`EdgeSSH-Auto-Update: true\` exists between \`${current_sha}\` and \`${official_sha}\`." + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + # lease 保证检测期间 main 若被人工更新则停止,不覆盖刚产生的新提交;下次定时任务会重新计算。 + git push origin "${target_sha}:refs/heads/main" \ + --force-with-lease="refs/heads/main:${current_sha}" + + echo "target_sha=${target_sha}" >> "${GITHUB_OUTPUT}" + echo "updated=true" >> "${GITHUB_OUTPUT}" + { + echo "## Force Update" + echo + echo "Updated \`main\` from \`${current_sha}\` to marked commit \`${target_sha}\`." + echo "The deployment job will deploy that exact commit in this run." + } >> "${GITHUB_STEP_SUMMARY}" + + deploy: + name: Deploy marked commit + needs: update + if: needs.update.outputs.updated == 'true' + uses: ./.github/workflows/deploy.yml + with: + deployment_ref: ${{ needs.update.outputs.target_sha }} + secrets: inherit diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index c9bd9b7..8659dc3 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -75,6 +75,18 @@ API Token 只存 GitHub Secret,不放普通变量、代码或命令行输入 设置自定义域名时关闭备用 `workers.dev` 入口,所有部署关闭 preview URL。Cloudflare 模式校验 Access JWT;GitHub 模式使用 state、PKCE 和签名 HttpOnly Cookie。缺少认证不降级为匿名 SSH。 +## 官方强制更新 + +Fork 中的 `Force Update` 工作流每小时第 17 分钟运行,也支持从 Actions 页面手动运行。它会获取官方 `aozorae/EdgeSSH` 的 `main`,扫描 Fork 当前 `main` 到官方最新版本之间的提交,并查找以下完整 Git trailer: + +```text +EdgeSSH-Auto-Update: true +``` + +不存在标记时,工作流成功结束且不改代码、不部署。存在标记时,它选择拓扑顺序中最新的标记提交,以 `force-with-lease` 将 Fork 的 `main` 精确更新到该 SHA,然后直接调用 `Deploy` 的可复用部署任务并检出同一个 SHA。部署不依赖这次推送再次触发工作流,因此不会受 GitHub 防递归机制影响。 + +此能力用于维护者发布必须尽快应用的安全或兼容性更新。精确同步会移除 Fork 在 `main` 上独有的提交;需要长期维护的自定义改动应放在其他分支。若检测期间 `main` 又被人工更新,lease 会让本次任务停止,下一次运行会基于新版本重新检查。仓库或组织策略还必须允许工作流使用 `contents: write`,否则无法更新分支。 + ## 其他可选配置 | 名称 | GitHub 位置 | 默认/示例 | diff --git a/README.md b/README.md index 0cd2824..3977d1c 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,11 @@ npm ci `ENCRYPTION_KEY` 由部署流程管理并持久保存在 **Cloudflare Worker Secrets**;Cloudflare 模式另存 `ACCESS_TEAM_DOMAIN`、`ACCESS_AUD`,GitHub 模式同步 `GITHUB_CLIENT_SECRET`。不需要用户复制自动生成的值回 GitHub。加密密钥只在首次部署生成,后续保留,即使 GitHub 留有旧值也不会覆盖线上密钥。不要删除 Worker 或其加密密钥;Cloudflare 不提供密钥明文读回,丢失后无法解密已有资料。 -推送 `main` 或手动运行 `Deploy` 都会执行检查和部署。只在首次 Run workflow 输入邮箱也可以:后续未提供邮箱时保留已有认证配置。需要更换域名或重新自动配置 Access 时,请再次提供邮箱。完整权限、旧版迁移及 GitHub 登录扩展说明见[部署指南](DEPLOYMENT.md);手工维护见 [Zero Trust 指南](docs/ZERO_TRUST.md)。 +推送 `main` 或手动运行 `Deploy` 都会执行检查和部署。只在首次 Run workflow 输入邮箱也可以:后续未提供邮箱时保留已有认证配置。需要更换域名或重新自动配置 Access 时,请再次提供邮箱。 + +Fork 启用 Actions 后,`Force Update` 每小时检查一次官方 `aozorae/EdgeSSH` 的 `main`。只有当前版本与官方最新版本之间出现带有独立 Git trailer `EdgeSSH-Auto-Update: true` 的提交时,工作流才会将 Fork 的 `main` 精确同步到最新一个标记提交,并在同一次运行中直接部署该 SHA;普通提交不会触发同步。也可从 Actions 页面手动运行检查。该流程会覆盖 Fork 在 `main` 上的自定义提交,自定义开发请保留在其他分支。 + +完整权限、自动更新、旧版迁移及 GitHub 登录扩展说明见[部署指南](DEPLOYMENT.md);手工维护见 [Zero Trust 指南](docs/ZERO_TRUST.md)。 ## 本地开发 diff --git a/tests/deployment.test.ts b/tests/deployment.test.ts index e0dbfa6..37e82fc 100644 --- a/tests/deployment.test.ts +++ b/tests/deployment.test.ts @@ -202,3 +202,21 @@ test('Actions asks for email, keeps Token private and uses the shared deploy ent assert.ok(workflow.includes('run: npm run deploy\n')); assert.equal(workflow.includes('wrangler d1 migrations apply'), false); }); + +test('Force Update selects a marked official commit and deploys the same SHA directly', async () => { + const deploy = await readFile(new URL('../.github/workflows/deploy.yml', import.meta.url), 'utf8'); + const update = await readFile(new URL('../.github/workflows/force-update.yml', import.meta.url), 'utf8'); + + assert.ok(deploy.includes('workflow_call:')); + assert.ok(deploy.includes('deployment_ref:')); + assert.ok(deploy.includes("ref: ${{ inputs.deployment_ref || github.sha }}")); + + assert.ok(update.includes('OFFICIAL_REPOSITORY: aozorae/EdgeSSH')); + assert.ok(update.includes('fetch-depth: 0')); + assert.ok(update.includes("trailers:key=EdgeSSH-Auto-Update,valueonly,unfold")); + assert.ok(update.includes('--not "${current_sha}"')); + assert.ok(update.includes('--force-with-lease="refs/heads/main:${current_sha}"')); + assert.ok(update.includes('uses: ./.github/workflows/deploy.yml')); + assert.ok(update.includes('deployment_ref: ${{ needs.update.outputs.target_sha }}')); + assert.equal(update.includes('npm run deploy'), false); +});