diff --git a/.github/workflows/track-releases.yml b/.github/workflows/track-releases.yml index 17f97d0..9d35770 100644 --- a/.github/workflows/track-releases.yml +++ b/.github/workflows/track-releases.yml @@ -9,11 +9,10 @@ name: Track plugin releases # # - organization: plugins whose repository belongs to the Lithe-IDEA organization, # including first listings. After the archives are verified, the workflow -# merges the pull request itself by fast-forwarding main with the -# STORE_BOT_DEPLOY_KEY deploy key, the only bot credential that the -# "Only owners merge main" ruleset lets through. +# merges the pull request itself with a token of the Lithe Store Bot GitHub +# App, the only bot that the "Only owners merge main" ruleset lets through. # - external: every other plugin. The pull request waits for a maintainer -# review, and this scope never receives the deploy key. +# review, and this scope never receives the app token. on: schedule: - cron: "41 */6 * * *" @@ -57,20 +56,27 @@ jobs: SCOPE: ${{ matrix.scope }} BRANCH: ${{ matrix.branch }} TITLE: ${{ matrix.title }} - # A PR opened with GITHUB_TOKEN does not trigger other workflows. Set the - # STORE_BOT_TOKEN secret (fine-grained PAT: contents + pull requests) so - # `Validate index` runs on the bot PR as well. - GH_TOKEN: ${{ secrets.STORE_BOT_TOKEN || github.token }} steps: + # Only the organization scope may merge into main, so only it asks for the + # app token. Without the app, both scopes fall back to the tokens below. + - name: Create the Lithe Store Bot token + id: bot + if: matrix.scope == 'organization' && vars.STORE_BOT_APP_CLIENT_ID != '' + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.STORE_BOT_APP_CLIENT_ID }} + private-key: ${{ secrets.STORE_BOT_APP_PRIVATE_KEY }} + + # Every step below that pushes or calls gh uses the same token, chosen in + # this order: the app token, then the optional STORE_BOT_TOKEN secret + # (fine-grained PAT: contents + pull requests), then GITHUB_TOKEN. A pull + # request opened with GITHUB_TOKEN does not trigger `Validate index`. - uses: actions/checkout@v7 with: # The latest main, not the commit that triggered the run: the external # scope must start from the main that the organization scope just updated. ref: main - token: ${{ secrets.STORE_BOT_TOKEN || github.token }} - # Git then pushes over SSH with the deploy key. The external scope - # gets an empty key and keeps using the token, which cannot write main. - ssh-key: ${{ matrix.scope == 'organization' && secrets.STORE_BOT_DEPLOY_KEY || '' }} + token: ${{ steps.bot.outputs.token || secrets.STORE_BOT_TOKEN || github.token }} - uses: actions/setup-node@v7 with: node-version: 22 @@ -88,6 +94,8 @@ jobs: - name: Close the obsolete pull request if: steps.track.outputs.added == '0' && inputs.plugin == '' + env: + GH_TOKEN: ${{ steps.bot.outputs.token || secrets.STORE_BOT_TOKEN || github.token }} run: | number=$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number') if [ -n "$number" ]; then @@ -103,6 +111,7 @@ jobs: id: pr if: steps.track.outputs.added != '0' env: + GH_TOKEN: ${{ steps.bot.outputs.token || secrets.STORE_BOT_TOKEN || github.token }} SUMMARY: ${{ steps.track.outputs.summary }} NEW_PLUGINS: ${{ steps.track.outputs.new_plugins }} run: | @@ -142,28 +151,18 @@ jobs: - name: Merge the organization pull request if: matrix.scope == 'organization' && steps.track.outputs.added != '0' env: + # Only the app can pass the ruleset, so this step never falls back to another token. + GH_TOKEN: ${{ steps.bot.outputs.token }} NUMBER: ${{ steps.pr.outputs.number }} - HAS_DEPLOY_KEY: ${{ secrets.STORE_BOT_DEPLOY_KEY != '' }} run: | - if [ "$HAS_DEPLOY_KEY" != true ]; then - echo "::warning::STORE_BOT_DEPLOY_KEY is not set, so pull request #$NUMBER stays open for a maintainer to merge." + if [ -z "$GH_TOKEN" ]; then + echo "::warning::The Lithe Store Bot app is not configured (STORE_BOT_APP_CLIENT_ID, STORE_BOT_APP_PRIVATE_KEY), so pull request #$NUMBER stays open for a maintainer to merge." exit 0 fi - # Fast-forward main to the verified commit. Without --force, git refuses - # the push if main moved after the checkout; the next run then rebuilds - # the branch from the new main. - if ! git push origin "HEAD:main"; then - echo "::warning::Could not fast-forward main, so pull request #$NUMBER stays open for a maintainer to merge." + # --match-head-commit makes GitHub refuse the merge if the branch no + # longer points at the commit that this run verified. + if ! gh pr merge "$NUMBER" --squash --match-head-commit "$(git rev-parse HEAD)"; then + echo "::warning::Could not merge pull request #$NUMBER, so it stays open for a maintainer to merge." exit 0 fi - # GitHub marks the pull request merged once its head commit is on main. - # Delete the branch only after that, or the pull request would show as - # closed instead of merged. - for _ in $(seq 1 12); do - if [ "$(gh pr view "$NUMBER" --json state -q .state)" = MERGED ]; then - git push origin --delete "$BRANCH" - exit 0 - fi - sleep 5 - done - echo "::warning::main contains pull request #$NUMBER, but GitHub has not marked it merged after 60 seconds. Delete $BRANCH once it has." + git push origin --delete "$BRANCH" diff --git a/docs/maintainers.md b/docs/maintainers.md index 1e07990..3e1fe09 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -45,30 +45,35 @@ bot 首次收录的组织插件(组织 PR 描述里的“首次收录”)合 组织插件的自动合入分三步: 1. 开 PR 之前运行 `validate-index.mjs --verify-archives`,与 `Validate index` 对新增归档的检查相同; -2. 用 `STORE_BOT_DEPLOY_KEY` 把 main 快进到 PR 的提交,GitHub 据此把 PR 标为已合并; -3. PR 标为已合并后删除分支。 +2. 用 Lithe Store Bot 的 token 推送分支、开 PR,再 squash 合并。合并时指定本轮校验过的提交 + (`gh pr merge --match-head-commit`),分支在校验之后被改动过,GitHub 就拒绝合并; +3. 合并后删除分支。 -main 在扫描期间被别人更新时快进会失败;没有配置 `STORE_BOT_DEPLOY_KEY` 时工作流不尝试合入。 -这两种情况都只在工作流里给出警告,PR 保持打开,由维护者按审查清单审查后合并,下一轮扫描也会基于新的 main 重建它。 -删除 deploy key 或 secret 即可停止自动合入。 +没有配置 Lithe Store Bot,或者合并失败(例如 main 在扫描期间被更新并与 PR 冲突)时,工作流只给出警告, +PR 保持打开,由维护者按审查清单审查后合并,下一轮扫描也会基于新的 main 重建它。 +删除仓库变量 `STORE_BOT_APP_CLIENT_ID`,或者卸载 Lithe Store Bot,即可停止自动合入。 信任边界:在任一 Lithe-IDEA 组织插件仓库能发布 Release 的人,以及能在组织里创建公开 -`lithe-plugin-*` 仓库的人,都可以不经 Store 维护者审查把插件发布到 Lithe Store。 +`lithe-plugin-*` 仓库的人,都可以不经 Store 维护者审查把插件发布到 Lithe Store。组织允许成员创建公开仓库时 +(Organization settings → Member privileges → Repository creation),每个组织成员都在这个范围内。 授予组织成员身份或插件仓库写权限时按这个标准判断。 需要的仓库设置: 1. **Settings → Actions → General → Workflow permissions**:勾选 “Allow GitHub Actions to create and approve pull requests”。 -2. **Settings → Deploy keys**:一把允许写入的 deploy key - (`lithe-store-bot (track organization releases)`),私钥保存为 `STORE_BOT_DEPLOY_KEY` secret。 - 只有组织插件那一轮的 checkout 会拿到这把 key,组织外那一轮仍用 `GITHUB_TOKEN`,写不了 main。 +2. **Lithe Store Bot**:Lithe-IDEA 组织拥有的私有 GitHub App,只有 Contents 与 Pull requests 读写权限, + 不接收 webhook,只安装在本仓库。仓库变量 `STORE_BOT_APP_CLIENT_ID` 保存它的 Client ID, + secret `STORE_BOT_APP_PRIVATE_KEY` 保存它的私钥。只有组织插件那一轮会生成 app token; + 组织外那一轮用 `STORE_BOT_TOKEN` 或 `GITHUB_TOKEN`,写不了 main。用 app token 开的 PR 会触发 + `Validate index`,合并到 main 也会触发 main 上的 `Validate index` 和 `Sync README`(总览已在 PR 里生成,不会再开同步 PR)。 3. **Settings → Rules → Rulesets**:规则集 “Only owners merge main” 限制 `main` 的更新、删除和强制推送, - 只有组织管理员和 deploy key 可以绕过。GitHub 不允许把 GitHub Actions 加入仓库规则集的绕过名单, - 所以工作流用 deploy key 写 main。这项绕过对仓库的所有 deploy key 生效,新增其他 deploy key 前先评估。 -4. 可选:创建 `STORE_BOT_TOKEN` secret(fine-grained PAT,仅本仓库,Contents 与 Pull requests 读写)。 - 使用默认 `GITHUB_TOKEN` 开的 PR 不会触发 `Validate index`;工作流已在开 PR 前完成同样的校验。 - deploy key 推送到 main 会正常触发 main 上的 `Validate index` 和 `Sync README`。 + 只有组织管理员和 Lithe Store Bot 可以绕过。GitHub 不允许把 GitHub Actions(`GITHUB_TOKEN`)加入仓库规则集的 + 绕过名单,Lithe-IDEA 组织也禁用了 deploy key,所以工作流用组织自己的 GitHub App 写 main。 + 绕过名单按 App 指定,不会放行其他 App 或 deploy key。 +4. 可选:创建 `STORE_BOT_TOKEN` secret(fine-grained PAT,仅本仓库,Contents 与 Pull requests 读写), + 让组织外 PR 和 README 同步 PR 也触发 `Validate index`。使用默认 `GITHUB_TOKEN` 开的 PR 不会触发它; + 工作流已在开 PR 前完成同样的校验。 ## 自动同步 README 总览 @@ -78,7 +83,7 @@ README 的插件总览由 `scripts/render-readme.mjs` 根据索引生成。`main 合并这个 PR 后工作流会再运行一次,发现总览已经一致就结束。总览已经一致时,上一轮没人合并的同步 PR 会被自动关闭。 仓库设置与追踪新版本相同(“Allow GitHub Actions to create and approve pull requests”, -可选的 `STORE_BOT_TOKEN`),同步 PR 不使用 deploy key,始终由维护者合并。没有配置 `STORE_BOT_TOKEN` 时, +可选的 `STORE_BOT_TOKEN`),同步 PR 不使用 Lithe Store Bot,始终由维护者合并。没有配置 `STORE_BOT_TOKEN` 时, 这个 PR 也不会触发 `Validate index`。同步 PR 只应该包含 `README.md`;如果还改了别的文件,不要合并。 ## 下架与撤回