Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 99 additions & 14 deletions .github/workflows/track-releases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@ name: Track plugin releases

# Scans the GitHub Releases of every listed plugin, and of every unlisted
# public `lithe-plugin-*` repository in the Lithe-IDEA organization except
# `lithe-plugin-docs`, then opens one pull request that adds the versions and
# plugins not yet in the index. Plugin authors only need to publish a release
# that follows docs/publishing.md.
# `lithe-plugin-docs`, and adds the versions and plugins not yet in the index.
# Plugin authors only need to publish a release that follows docs/publishing.md.
#
# The scan runs once per scope, and each scope keeps its own pull request:
#
# - 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.
# - external: every other plugin. The pull request waits for a maintainer
# review, and this scope never receives the deploy key.
on:
schedule:
- cron: "41 */6 * * *"
Expand All @@ -25,18 +34,43 @@ concurrency:

jobs:
track:
name: track (${{ matrix.scope }})
# The workflow writes main's index and may push to main, so it must run the
# workflow and scripts from main. A manual run from another branch does nothing.
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
# One scope at a time, organization first, so the external branch starts
# from the main that the organization scope may have just updated.
max-parallel: 1
fail-fast: false
matrix:
include:
- scope: organization
branch: bot/track-organization-releases
title: "chore(index): 追踪 Lithe-IDEA 组织插件新版本"
- scope: external
branch: bot/track-releases
title: "chore(index): 追踪组织外插件新版本"
env:
BRANCH: bot/track-releases
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:
- 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 || '' }}
- uses: actions/setup-node@v7
with:
node-version: 22
Expand All @@ -48,15 +82,25 @@ jobs:
PLUGIN: ${{ inputs.plugin }}
run: |
cp lithe-plugin-index.json "$RUNNER_TEMP/base-index.json"
args=""
if [ -n "$PLUGIN" ]; then args="--plugin $PLUGIN"; fi
node scripts/track-releases.mjs lithe-plugin-index.json $args
args=(--scope "$SCOPE")
if [ -n "$PLUGIN" ]; then args+=(--plugin "$PLUGIN"); fi
node scripts/track-releases.mjs lithe-plugin-index.json "${args[@]}"

- name: Close the obsolete pull request
if: steps.track.outputs.added == '0' && inputs.plugin == ''
run: |
number=$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number')
if [ -n "$number" ]; then
gh pr close "$number" --delete-branch \
--comment "最新一轮追踪在这个范围内没有发现 main 尚未收录的版本,这个 PR 不再需要。"
fi

- name: Verify the new archives
if: steps.track.outputs.added != '0'
run: node scripts/validate-index.mjs lithe-plugin-index.json --verify-archives --base "$RUNNER_TEMP/base-index.json"

- name: Open or update the pull request
id: pr
if: steps.track.outputs.added != '0'
env:
SUMMARY: ${{ steps.track.outputs.summary }}
Expand All @@ -69,16 +113,57 @@ jobs:
# pull request. Render the overview here so one merge covers both.
node scripts/render-readme.mjs
git add lithe-plugin-index.json README.md
git commit -m "chore(index): track new plugin releases" -m "$SUMMARY"
git commit -m "chore(index): track $SCOPE plugin releases" -m "$SUMMARY"
git push --force origin "$BRANCH"
body=$(printf '自动追踪到新的插件版本:\n\n%s\n\n已校验 SHA-256 与包内 plugin.json(id、version、pluginAPIVersion、signatureRequirement),README 总览已一并重新生成。维护者请按 docs/maintainers.md 的清单审查后合并。' "$(echo "$SUMMARY" | tr ',' '\n' | sed 's/^ */- /')")

list=$(echo "$SUMMARY" | tr ',' '\n' | sed 's/^ */- /')
checks="已校验 SHA-256 与包内 plugin.json(id、version、pluginAPIVersion、signatureRequirement),README 总览已一并重新生成。"
if [ "$SCOPE" = organization ]; then
review="这些插件的仓库属于 Lithe-IDEA 组织,工作流会在校验通过后直接合入 main。如果这个 PR 仍处于打开状态,说明自动合入没有成功(原因见工作流日志),请维护者按 docs/maintainers.md 的清单审查后合并。"
else
review="这些插件来自 Lithe-IDEA 组织以外的仓库,维护者请按 docs/maintainers.md 的清单审查后合并。"
fi
body=$(printf '自动追踪到新的插件版本:\n\n%s\n\n%s%s' "$list" "$checks" "$review")
if [ -n "$NEW_PLUGINS" ]; then
# The next scan rebuilds this branch from main, so metadata fixes belong in a follow-up PR.
body="$body$(printf '\n\n首次收录:%s。条目由插件仓库的简介、许可证、topics 和包内 plugin.json 自动生成,没有图标和中文简介,description 沿用仓库简介的语言。请按 docs/maintainers.md 的首次收录清单审查;合并后另提 PR 补充 icon、localizedDescriptions 和 tags,不要直接改这个分支,下一轮扫描会覆盖它。' "$NEW_PLUGINS")"
body="$body$(printf '\n\n首次收录:%s。条目由插件仓库的简介、许可证、topics 和包内 plugin.json 自动生成,没有图标和中文简介,description 沿用仓库简介的语言。合并后请另提 PR 补充 icon、localizedDescriptions 和 tags,不要直接改这个分支,下一轮扫描会覆盖它。' "$NEW_PLUGINS")"
fi
if gh pr view "$BRANCH" --json state -q .state 2>/dev/null | grep -q OPEN; then
gh pr edit "$BRANCH" --body "$body"

number=$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number')
if [ -n "$number" ]; then
gh pr edit "$number" --title "$TITLE" --body "$body"
else
gh pr create --base main --head "$BRANCH" --title "chore(index): 追踪插件新版本" --body "$body" --label new-version || \
gh pr create --base main --head "$BRANCH" --title "chore(index): 追踪插件新版本" --body "$body"
gh pr create --base main --head "$BRANCH" --title "$TITLE" --body "$body" --label new-version || \
gh pr create --base main --head "$BRANCH" --title "$TITLE" --body "$body"
number=$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number')
fi
echo "number=$number" >> "$GITHUB_OUTPUT"

- name: Merge the organization pull request
if: matrix.scope == 'organization' && steps.track.outputs.added != '0'
env:
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."
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."
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."
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
| 我想… | 怎么做 |
| --- | --- |
| 收录自己的插件 | 读 [docs/publishing.md](docs/publishing.md),提 PR 或开“插件收录申请” issue;Lithe-IDEA 组织的 `lithe-plugin-*` 仓库由 bot 自动收录(见 [首次收录](docs/publishing.md#3-首次收录)) |
| 发布插件新版本 | 在自己仓库发布 Release 即可,bot 会自动开 PR(见 [发布新版本](docs/publishing.md#4-发布新版本)) |
| 发布插件新版本 | 在自己仓库发布 Release 即可,bot 会自动开 PR;Lithe-IDEA 组织插件校验通过后自动合入(见 [发布新版本](docs/publishing.md#4-发布新版本)) |
| 修改插件信息 | 提 PR 修改 `lithe-plugin-index.json` 中自己的条目(含 `icon`) |
| 报告有问题的插件 | 开“报告插件问题” issue;安全问题请勾选“安全问题”,维护者会优先处理 |
| 改进脚本、CI 或文档 | 直接提 PR,并运行 `node --test scripts/store.test.mjs` |
Expand Down Expand Up @@ -57,11 +57,11 @@ Lithe 会把仓库地址 `https://github.com/Lithe-IDEA/Lithe-store` 解析为
```text
lithe-plugin-index.json # 唯一的数据文件,Lithe 客户端读取它
scripts/validate-index.mjs # 与 Lithe Rust Core 同规则的校验 + 下载包校验
scripts/track-releases.mjs # 追踪已收录插件和 Lithe-IDEA 组织 lithe-plugin-* 仓库的新 Release,生成索引条目
scripts/track-releases.mjs # 追踪已收录插件和 Lithe-IDEA 组织 lithe-plugin-* 仓库的新 Release,生成索引条目;--scope 区分组织插件与组织外插件
scripts/render-readme.mjs # 根据索引生成 README 的插件总览
scripts/store.test.mjs # 校验脚本的测试(fixtures 复制自插件契约)
assets/ # README 用到的图片(无图标插件的默认图标)
.github/workflows/ # validate(PR/推送/每周)、track-releases(每 6 小时)、sync-readme(main 更新后开 PR 同步 README)
.github/workflows/ # validate(PR/推送/每周)、track-releases(每 6 小时,组织插件自动合入、组织外插件开 PR 审查)、sync-readme(main 更新后开 PR 同步 README)
```

## 本地校验
Expand Down
58 changes: 45 additions & 13 deletions docs/maintainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,66 @@ CI(`Validate index`)已经自动保证:
- 包内恰好一个匹配的 `plugin.json`,`version`、`pluginAPIVersion` 与索引一致,
`vendor.signatureRequirement` 为 `"none"`,没有不安全的路径。

维护者人工确认:
维护者人工确认(Lithe-IDEA 组织插件的 bot PR 由工作流自动合入,不走这份清单,见[自动追踪新版本](#自动追踪新版本)):

- [ ] `repository` 是公开仓库,有 README、许可证,看得出是该作者在维护;
- [ ] 首次收录:`author` 与仓库所有者对应,名称不冒充其他项目或 Lithe 官方;
- [ ] 插件声明要运行的程序与描述相符(看包内 `plugin.json` 的 `languageServers` 与 `executionProviders`);
- [ ] 描述、标签、图标没有误导内容;
- [ ] 新版本 PR(bot 或作者提交):版本号递增,没有删除旧版本;
- [ ] bot 首次收录的插件(PR 描述里的“首次收录”):仓库简介适合作为 `description`,仓库 topics 适合作为 `tags`。
合并后另提 PR 补充 `icon`、`localizedDescriptions`,必要时修改 `description`、`tags`;
不要直接改 `bot/track-releases`,下一轮扫描会从 main 重建这个分支。
- [ ] 新版本 PR(bot 或作者提交):版本号递增,没有删除旧版本。

bot 首次收录的组织插件(组织 PR 描述里的“首次收录”)合入后,维护者补做两件事:

- 检查仓库简介是否适合作为 `description`,仓库 topics 是否适合作为 `tags`,不合适就另提 PR 修改;
- 另提 PR 补充 `icon`、`localizedDescriptions`。不要直接改 `bot/track-organization-releases`,下一轮扫描会从 main 重建这个分支。

## 自动追踪新版本

`Track plugin releases` 每 6 小时运行一次,也可在 Actions 中手动运行(可只指定一个插件 ID)。
`Track plugin releases` 每 6 小时运行一次,也可在 Actions 中对 `main` 手动运行(可只指定一个插件 ID;从其他分支启动不会执行)。
它扫描已收录插件的 Release,以及 Lithe-IDEA 组织下尚未收录的公开 `lithe-plugin-*` 仓库
(`lithe-plugin-docs` 不发布插件,固定排除),只收录正式 Release;新插件的条目生成规则见
[发布社区插件的首次收录](publishing.md#3-首次收录)。手动运行时指定的插件 ID 可以是已收录插件,
也可以是组织中尚未收录的插件。
它把结果统一提交到 `bot/track-releases` 分支并开一个 PR,PR 里同时包含索引和重新生成的 README 总览(合并后不需要再等 `Sync README`);新的扫描会覆盖这个分支并更新 PR 描述。

扫描按插件仓库的所有者分两轮,先组织插件、后组织外插件,每轮维护一个 bot PR:

| | Lithe-IDEA 组织插件 | 组织外插件 |
| --- | --- | --- |
| 范围 | `repository` 属于 Lithe-IDEA 组织的已收录插件,以及新发现的组织 `lithe-plugin-*` 仓库 | 其他已收录插件;不会被自动首次收录 |
| 分支 | `bot/track-organization-releases` | `bot/track-releases` |
| 合入 | 校验通过后由工作流直接合入 main,不经人工审查 | 维护者按[审查清单](#审查清单)审查后合并 |

两个 PR 都同时包含索引和重新生成的 README 总览,合入后不需要再等 `Sync README`。每轮扫描从 main 重建
对应分支并更新 PR;某一轮没有发现 main 尚未收录的版本时,这一轮遗留的 PR 会被自动关闭
(只指定一个插件的手动运行不会关闭 PR)。

组织插件的自动合入分三步:

1. 开 PR 之前运行 `validate-index.mjs --verify-archives`,与 `Validate index` 对新增归档的检查相同;
2. 用 `STORE_BOT_DEPLOY_KEY` 把 main 快进到 PR 的提交,GitHub 据此把 PR 标为已合并;
3. PR 标为已合并后删除分支。

main 在扫描期间被别人更新时快进会失败;没有配置 `STORE_BOT_DEPLOY_KEY` 时工作流不尝试合入。
这两种情况都只在工作流里给出警告,PR 保持打开,由维护者按审查清单审查后合并,下一轮扫描也会基于新的 main 重建它。
删除 deploy key 或 secret 即可停止自动合入。

信任边界:在任一 Lithe-IDEA 组织插件仓库能发布 Release 的人,以及能在组织里创建公开
`lithe-plugin-*` 仓库的人,都可以不经 Store 维护者审查把插件发布到 Lithe Store。
授予组织成员身份或插件仓库写权限时按这个标准判断。

需要的仓库设置:

1. **Settings → Actions → General → Workflow permissions**:勾选
“Allow GitHub Actions to create and approve pull requests”。
2. 可选但推荐:创建 `STORE_BOT_TOKEN` secret(fine-grained PAT,仅本仓库,
Contents 与 Pull requests 读写)。使用默认 `GITHUB_TOKEN` 开的 PR 不会触发
`Validate index`;工作流已在开 PR 前完成同样的校验,但合并前需要手动运行一次检查才能满足分支保护。
3. 分支保护:`main` 要求 `Validate index / validate` 通过,要求 CODEOWNERS 审查。
2. **Settings → Deploy keys**:一把允许写入的 deploy key
(`lithe-store-bot (track organization releases)`),私钥保存为 `STORE_BOT_DEPLOY_KEY` secret。
只有组织插件那一轮的 checkout 会拿到这把 key,组织外那一轮仍用 `GITHUB_TOKEN`,写不了 main。
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`。

## 自动同步 README 总览

Expand All @@ -46,8 +78,8 @@ README 的插件总览由 `scripts/render-readme.mjs` 根据索引生成。`main
合并这个 PR 后工作流会再运行一次,发现总览已经一致就结束。总览已经一致时,上一轮没人合并的同步 PR 会被自动关闭。

仓库设置与追踪新版本相同(“Allow GitHub Actions to create and approve pull requests”,
可选的 `STORE_BOT_TOKEN`)。没有配置 `STORE_BOT_TOKEN` 时,这个 PR 也不会触发 `Validate index`,
需要手动运行一次检查才能满足分支保护。同步 PR 只应该包含 `README.md`;如果还改了别的文件,不要合并。
可选的 `STORE_BOT_TOKEN`),同步 PR 不使用 deploy key,始终由维护者合并。没有配置 `STORE_BOT_TOKEN` 时,
这个 PR 也不会触发 `Validate index`。同步 PR 只应该包含 `README.md`;如果还改了别的文件,不要合并。

## 下架与撤回

Expand Down
Loading
Loading