diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..2eb60a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,51 @@ +name: Bug 报告 +description: 可以复现的缺陷 +labels: [kind/bug, needs-triage] +body: + - type: textarea + id: observed + attributes: + label: 现象 + description: 实际看到什么,带上错误信息、日志或截图 + placeholder: | + 命令: + 结果: + validations: + required: true + + - type: textarea + id: expected + attributes: + label: 预期 + description: 你认为应该发生什么,依据是什么 + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: 复现步骤 + description: 从干净环境开始的最小步骤,写清能否稳定复现 + validations: + required: true + + - type: textarea + id: environment + attributes: + label: 环境 + description: 系统、运行时版本、依赖版本 + value: | + - 系统: + - 运行时: + - 依赖: + validations: + required: true + + - type: textarea + id: scope + attributes: + label: 涉及范围 + description: 会动到的模块,是否有其他入口受影响 + placeholder: 无 / 具体描述 + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/epic.yml b/.github/ISSUE_TEMPLATE/epic.yml new file mode 100644 index 0000000..d52ece4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.yml @@ -0,0 +1,39 @@ +name: Epic +description: 需要多个 PR 的大功能,作为跟踪容器,不对应 PR +labels: [kind/epic] +body: + - type: textarea + id: goal + attributes: + label: 目标 + description: 整体要达成什么,为什么现在做 + validations: + required: true + + - type: textarea + id: children + attributes: + label: 切片清单 + description: 列出各片;也可以用原生的 sub-issue 关系挂载 + placeholder: | + - [ ] #12 + - [ ] #13 + validations: + required: true + + - type: textarea + id: done + attributes: + label: 整体完成判定 + description: 满足什么条件这个 epic 可以关闭 + validations: + required: true + + - type: textarea + id: out-of-scope + attributes: + label: 不做的部分 + description: 明确排除的场景;没有写「无」 + placeholder: 无 + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..8ab2ee4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,39 @@ +name: 功能请求 +description: 缺什么能力,先评估再排期 +labels: [kind/feature, needs-triage] +body: + - type: textarea + id: problem + attributes: + label: 要解决的问题 + description: 现在哪里不顺,具体场景是什么,谁受影响 + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: 设想的做法 + description: 已经有想法就写清对外行为;没有就写「暂无」 + placeholder: 暂无 + validations: + required: true + + - type: textarea + id: done + attributes: + label: 完成的判定 + description: 逐条写成可执行的判定:跑哪条命令、看到什么结果 + placeholder: | + - [ ] + validations: + required: true + + - type: textarea + id: out-of-scope + attributes: + label: 明确不做的部分 + description: 这次不覆盖的场景;没有写「无」 + placeholder: 无 + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/refactor.yml b/.github/ISSUE_TEMPLATE/refactor.yml new file mode 100644 index 0000000..dfc2c31 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor.yml @@ -0,0 +1,37 @@ +name: 重构请求 +description: 行为不变的结构调整,可独立合入、独立回滚 +labels: [kind/refactor, ready-for-agent] +body: + - type: textarea + id: current + attributes: + label: 现状问题 + description: 当前结构哪里不合理,依据是什么 + validations: + required: true + + - type: textarea + id: target + attributes: + label: 目标结构 + description: 改成什么样,模块边界在哪里 + validations: + required: true + + - type: textarea + id: unchanged + attributes: + label: 必须保持不变的行为 + description: 对外行为、配置、数据的哪些部分不能变 + validations: + required: true + + - type: textarea + id: acceptance + attributes: + label: 验收标准 + description: 逐条写成可执行判定 + placeholder: | + - [ ] 命令:``,结果: + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 0000000..e96ff18 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,47 @@ +name: 工作切片 +description: 已明确范围、可交给一个会话独立完成的任务 +labels: [ready-for-agent] +body: + - type: input + id: parent + attributes: + label: 上级 Issue + description: 来自哪个 epic 或功能请求,写编号;没有写「无」 + placeholder: 无 / #12 + validations: + required: true + + - type: textarea + id: goal + attributes: + label: 目标 + description: 一段话说清这一片要交付什么 + validations: + required: true + + - type: textarea + id: scope + attributes: + label: 范围 + description: 会动到的文件与模块;明确不做哪些相邻的事 + validations: + required: true + + - type: textarea + id: blocked-by + attributes: + label: 前置依赖 + description: 依赖的 issue 编号,没有写「无」;也可以用原生的 blocked-by 关系挂上 + placeholder: 无 / #12 + validations: + required: true + + - type: textarea + id: acceptance + attributes: + label: 验收标准 + description: 逐条写成可执行判定;没有验收标准不开工 + value: | + - [ ] + validations: + required: true diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..0836959 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,29 @@ +# PR 按改动路径打标签。键是标签名,值是匹配规则。 +# 路径按仓库实际结构改。 + +kind/docs: + - changed-files: + - any-glob-to-any-file: + - "**/*.md" + - "docs/**" + +kind/chore: + - changed-files: + - any-glob-to-any-file: + - ".github/**" + - "scripts/**" + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + - "tsconfig.json" + - "vitest.config.ts" + - "eslint.config.mjs" + - ".prettierrc" + - ".prettierignore" + - ".gitattributes" + - ".gitignore" + +# 所有 PR 先挂待评估标志,与 issue 侧一致 +needs-triage: + - changed-files: + - any-glob-to-any-file: "**" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..e641cbc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## 关联 + +Closes # + +## 做了什么 + +一段话说明这次改动的目标与范围。顺手做的无关改动另开一个 PR。 + +## 验收 + +- [ ] 命令:``,结果: + +## 影响面与回滚 + +会动到的模块、对外行为、配置与数据;出问题时怎么撤回。 + +## 检查清单 + +- [ ] 一个 PR 只对应一个逻辑变更,提交信息形如 `type(scope): 中文描述` +- [ ] 本地跑了完整测试,不只跑改动的那一条 +- [ ] 涉及对外接口或结构的改动已同步到 `docs/`(或仓库里的 `CONTEXT.md`) diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 0000000..4d30fc3 --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,24 @@ +# PR 打开时按改动路径打标签,路径规则在 .github/labeler.yml。 +# +# 用 pull_request_target 而不是 pull_request:前者在默认分支的上下文里运行,不需要 +# checkout 也能读到配置(配置不在 runner 上时 action 会走 API 从源仓库读),而且不会 +# 执行 PR 里的代码。 +# +# sync-labels 关着,只加不摘:路径规则改了以后,已经贴错的标签要人工清掉。 +name: pr-labeler + +on: + pull_request_target: + +permissions: + contents: read + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + # 只读改动路径,不执行 PR 里的代码 + - uses: actions/labeler@v7 + with: + sync-labels: false diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..54df7d3 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,55 @@ +# 长期无活动的条目标记与关闭:issue 60 天打 stale、再过 7 天关闭;PR 是 14 天与 7 天。 +# +# 豁免:带 frozen 的条目,以及带 kind/epic 的 issue(epic 是跟踪容器,本来就不该被 +# 自动关掉,所以它不必再加 frozen)。 +# +# 关闭理由统一是 not planned。要区分「判定不做」与「长期无活动」,看标签:前者带 +# wontfix,后者带 stale。 +# +# 已经带 stale 的条目被补上 frozen 时,remove-stale-when-updated 默认开启,会在下一次 +# 运行把 stale 摘掉。 +name: stale + +on: + schedule: + - cron: "0 2 * * *" + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v11 + with: + # issue:60 天没活动打标记,再过 7 天关闭 + days-before-issue-stale: 60 + days-before-issue-close: 7 + # PR:14 天没活动打标记,再过 7 天关闭 + days-before-pr-stale: 14 + days-before-pr-close: 7 + + stale-issue-label: stale + stale-pr-label: stale + + # frozen 表示长期保留;epic 是跟踪容器,不参与过期 + exempt-issue-labels: frozen,kind/epic + exempt-pr-labels: frozen + + close-issue-reason: not_planned + + stale-issue-message: > + 这条 60 天没有活动。请补齐进展、改成 frozen,或在 7 天内说明保留理由; + 逾期按 not planned 关闭,随时可以重新打开。 + close-issue-message: > + 因长期没有活动按 not planned 关闭。需要继续时重新打开,并补上新的上下文。 + stale-pr-message: > + 这个 PR 14 天没有活动。请更新分支或写明阻塞原因; + 7 天后关闭,代码仍在原分支上。 + close-pr-message: > + 因长期没有活动关闭。需要继续时重新打开,或基于原分支重开一个 PR。 + + operations-per-run: 100 diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml new file mode 100644 index 0000000..b9c3d65 --- /dev/null +++ b/.github/workflows/triage.yml @@ -0,0 +1,49 @@ +# 两件事,对应两个 job: +# +# 1) 新 issue:表单没带状态角色时补 needs-triage。表单自带状态角色的(「重构请求」与 +# 「工作切片」带 ready-for-agent)跳过,避免一条 issue 上出现两个状态角色。 +# 2) 每天 01:00 UTC 调 scripts/sync-missing-labels.sh,给缺 kind/* 或 priority/* 的 +# 开启 issue 补 needs-kind / needs-priority,已经有值的摘掉。 +# +# 两个 job 用 if 按触发事件分工:issues 事件只跑第一个,定时与手动触发只跑第二个。 +name: triage + +on: + issues: + types: [opened] + schedule: + - cron: "0 1 * * *" + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + # 新 issue:表单没带状态角色时才补待评估标志,避免出现两个状态角色 + needs-triage: + if: github.event_name == 'issues' + runs-on: ubuntu-latest + steps: + - run: | + labels=" $(gh issue view ${{ github.event.issue.number }} --json labels --jq '[.labels[].name] | join(" ")') " + for state in needs-triage needs-info ready-for-agent ready-for-human wontfix; do + case "$labels" in + *" $state "*) echo "已有状态角色 $state,不补 needs-triage"; exit 0 ;; + esac + done + gh issue edit ${{ github.event.issue.number }} --add-label needs-triage + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + + # 定时把缺维度的标志标签补齐,有值就摘掉 + missing-dimensions: + if: github.event_name != 'issues' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: bash scripts/sync-missing-labels.sh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} diff --git a/.gitignore b/.gitignore index a704d96..d7209bf 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ playwright-report/ # 临时中间结果 .tmp/ + +# 独立工作区 +.worktrees/ diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md new file mode 100644 index 0000000..f6aa6e9 --- /dev/null +++ b/docs/agents/triage-labels.md @@ -0,0 +1,100 @@ +# Triage 标签 + +本文件回答一个问题:**一条 issue 或 PR 现在该有哪些标签,由谁来打。** 它是标签名称与组合规则的唯一来源;颜色和说明不在本文件里,由 `gh label clone ZouZhao321/repo-template -R / --force` 从模板仓库带入。 + +## 主线 + +一条条目从建立到关闭,标签按下面的顺序出现。前四步要人判断,后四步由流程自己维护。 + +| 阶段 | 该有的标签 | 谁打 | +| ------------ | ----------------------------------------------------- | --------------- | +| 新建 | 表单自带的 `labels` | 表单 | +| 还没人评估 | `needs-triage`(表单没带状态角色时才补) | triage workflow | +| 判定可开工 | `ready-for-agent` + 一个 `kind/*` + 一个 `priority/*` | 维护者 | +| 判定要人做 | `ready-for-human` + 一个 `kind/*` + 一个 `priority/*` | 维护者 | +| 等提交者补充 | `needs-info` | 维护者 | +| 还缺维度 | `needs-kind` / `needs-priority` | 每日同步脚本 | +| 长期保留 | `frozen` | 维护者 | +| 长期无活动 | `stale`,再过 7 天自动关闭 | 过期流程 | +| 判定不做 | `wontfix`,按 `not planned` 关闭 | 维护者 | + +## 1. 状态角色:这条现在归谁 + +五个,互斥,同一时刻只留一个;换的时候先摘掉旧的。 + +| 角色 | 含义 | 可出现的对象 | +| ----------------- | ---------------------------------------------------------------------------- | ------------ | +| `needs-triage` | 还没人评估:类型、优先级、可执行性都未判定 | issue 与 PR | +| `needs-info` | 已评估,缺信息,等提交者回答 | 只 issue | +| `ready-for-agent` | 规格完整(目标、范围、验收标准齐全,不需要人工判断),可交给无人值守的 agent | 只 issue | +| `ready-for-human` | 需要人的判断、凭据或仓库外操作 | 只 issue | +| `wontfix` | 判定不做 | 只 issue | + +`needs-triage` 是唯一也会出现在 PR 上的状态角色,由 PR labeler 自动加。 + +互斥是约定,没有工具强制。唯一有自动化配合的是新 issue:「重构请求」与「工作切片」两个表单自带 `ready-for-agent`,`.github/workflows/triage.yml` 见表单已带状态角色就不再补 `needs-triage`。 + +状态怎么推进: + +| 从 | 到 | 依据 | +| -------------- | ------------------------------- | ---------------------------------------- | +| `needs-triage` | `ready-for-agent` | 目标、范围、验收标准齐全,不需要人工判断 | +| `needs-triage` | `ready-for-human` | 需要人工判断、凭据或仓库外操作 | +| `needs-triage` | `needs-info` | 缺信息,已向提交者提问 | +| `needs-info` | `needs-triage` 或 `ready-for-*` | 提交者补齐后重新判定 | +| 任意 | `wontfix` | 判定不做 | + +## 2. 类型与优先级:这条是什么、多急 + +两边都是人工判定,各自一次只打一个。 + +| 标签 | 含义 | 可出现的对象 | +| -------------------------- | ----------------------------------------- | ------------ | +| `kind/bug` | 行为与预期不符 | issue 与 PR | +| `kind/feature` | 新能力 | issue 与 PR | +| `kind/refactor` | 行为不变的结构调整 | issue 与 PR | +| `kind/chore` | 依赖、配置、构建、脚本 | issue 与 PR | +| `kind/docs` | 文档 | issue 与 PR | +| `kind/epic` | 需要多个 PR 的大功能,跟踪容器,不对应 PR | 只 issue | +| `priority/critical-urgent` | 阻断发布,立刻处理 | issue 与 PR | +| `priority/important-soon` | 本周期内完成 | issue 与 PR | +| `priority/backlog` | 认可但无排期 | issue 与 PR | + +缺 `priority/*` 就是还没评审,不设默认值。三组之间可以自由叠加,`ready-for-agent` + `kind/feature` + `priority/important-soon` 是标准形态。`kind/epic` 不对应 PR,所以也不配 `ready-for-agent` 或 `ready-for-human`。 + +PR 侧只有 `kind/docs` 与 `kind/chore` 两条路径规则:`.github/workflows/pr-labeler.yml` 调用 `.github/labeler.yml`,改文档的 PR 得 `kind/docs`,改依赖与配置的得 `kind/chore`,改源码的不会被自动打上 `kind/*`,这类 PR 的 `kind/*` 由人工补。同一个 PR 可能同时命中两条路径,要人工收敛成一个。 + +## 3. 缺值标志:还没分类的提示 + +| 标签 | 含义 | 可出现的对象 | +| ---------------- | --------------- | ------------ | +| `needs-kind` | 缺 `kind/*` | 只 issue | +| `needs-priority` | 缺 `priority/*` | 只 issue | + +两个都只用于 issue,由 `scripts/sync-missing-labels.sh` 维护,`.github/workflows/triage.yml` 每天 01:00 UTC 调用一次:没有就打上,有了就摘掉。它跳过带 `frozen` 的条目;内部用 `gh issue list --limit 500`,开启 issue 超过 500 条会截断。 + +因为是每天跑一次,「刚手工补上 `kind/*`,`needs-kind` 还挂了一天」是正常现象。 + +## 4. 生命周期与关闭 + +| 标签 | 含义 | 可出现的对象 | +| -------- | ------------------------------------ | ------------ | +| `frozen` | 不参与过期流程 | issue 与 PR | +| `stale` | 长期无活动的信号,只由过期流程打与摘 | issue 与 PR | + +`.github/workflows/stale.yml` 的阈值:issue 60 天无活动打 `stale`、再过 7 天关闭;PR 是 14 天与 7 天。豁免配置里 `exempt-issue-labels` 是 `frozen,kind/epic`,`exempt-pr-labels` 是 `frozen`;被豁免的条目不会被自动关掉,其中 `kind/epic` 是跟踪容器,本来就不该被自动关,所以它不必再加 `frozen`。 + +`frozen` 与 `stale` 互斥:豁免会拦住新打的 `stale`;对已经带着 `stale` 的条目补上 `frozen` 时,`remove-stale-when-updated` 默认开启,下一次运行会把 `stale` 摘掉。 + +关闭理由用原生字段:做完用 `completed`,其余用 `not planned`。`not planned` 同时覆盖「判定不做」与「长期无活动自动关闭」两种情况,区分靠标签——带 `wontfix` 的是前者,带 `stale` 的是后者。 + +## 新仓库前置 + +1. 先跑一次 `gh label clone ZouZhao321/repo-template -R / --force`。不跑的话:表单里的标签会被 GitHub 静默丢弃,每日的缺值同步任务会因为找不到 `needs-kind` / `needs-priority` 而直接失败(脚本会 `exit 1`)。 +2. 把 `.github/labeler.yml` 里的路径换成仓库真实目录,否则第 2 节里 PR 那两条路径规则描述的是模板仓库的目录,不成立。 + +## 维护本文件 + +标签改名或增删时同步改三处:本文件、`.github/ISSUE_TEMPLATE/` 里各表单的 `labels` 字段、以及模板仓库 `ZouZhao321/repo-template` 上的标签本身(`gh label edit` 改名、`gh label create` 新增)。 + +只用于 issue 的标签不要贴到 PR 上;搜索时写清 `is:issue` 或 `is:pr`。 diff --git a/scripts/sync-missing-labels.sh b/scripts/sync-missing-labels.sh new file mode 100755 index 0000000..15d47ec --- /dev/null +++ b/scripts/sync-missing-labels.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# 给缺 kind/* 或 priority/* 的开启 issue 补上 needs-kind / needs-priority, +# 已经有对应维度的就把标志标签摘掉。打了 frozen 的条目不处理。 +# +# 用法: +# GH_REPO=owner/repo bash scripts/sync-missing-labels.sh --dry-run +# GH_REPO=owner/repo bash scripts/sync-missing-labels.sh +# 在 workflow 里由 GITHUB_TOKEN 提供权限。 +# 本地运行需要 bash 能直接调用 gh(Windows 下用 WSL,或把 gh 所在目录加进 PATH)。 +set -euo pipefail + +dry_run=false +if [ "${1:-}" = "--dry-run" ]; then + dry_run=true +fi + +# 标志标签必须先存在,缺了就停下,不要静默跳过 +existing_labels="$(gh label list --limit 100 --json name --jq '.[].name')" +for label in needs-kind needs-priority; do + if ! grep -qx "$label" <<<"$existing_labels"; then + echo "缺少标签 $label,先跑 gh label clone 或 gh label create" >&2 + exit 1 + fi +done + +edit() { + local number="$1" + shift + if [ "$dry_run" = "true" ]; then + echo "gh issue edit $number $*" + else + gh issue edit "$number" "$@" + fi +} + +# --limit 500:开启 issue 超过 500 条的仓库会被截断,届时调大这个数 +# 过滤表达式交给 gh 自带的 --jq 执行,不额外依赖 jq +gh issue list --state open --limit 500 --json number,labels \ + --jq '.[] | [.number, ([.labels[].name] | any(startswith("kind/"))), ([.labels[].name] | any(startswith("priority/"))), ([.labels[].name] | any(. == "frozen"))] | @tsv' \ + | while IFS=$'\t' read -r number has_kind has_priority is_frozen; do + if [ "$is_frozen" = "true" ]; then + continue + fi + + if [ "$has_kind" = "true" ]; then + edit "$number" --remove-label needs-kind + else + edit "$number" --add-label needs-kind + fi + + if [ "$has_priority" = "true" ]; then + edit "$number" --remove-label needs-priority + else + edit "$number" --add-label needs-priority + fi + done