diff --git a/.changeset/12255-governed-workflows.md b/.changeset/12255-governed-workflows.md new file mode 100644 index 0000000000..b86af3e4e3 --- /dev/null +++ b/.changeset/12255-governed-workflows.md @@ -0,0 +1,4 @@ +--- +--- + +objectui#12255: `.github/workflows/**` joins the governed surfaces in `scripts/check-governed-queue-guard.mjs`, `AGENTS.md` and the CI guide. Repository tooling and docs only; no published package changes. diff --git a/AGENTS.md b/AGENTS.md index 1c16aa522b..1fd345e0c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -527,13 +527,14 @@ ls /* | wc -l # 两个数不等 ⇒ 工 > 同意 -**本仓的受管面 —— 五项,已逐条对本仓实际目录核实:** +**本仓的受管面 —— 六项,已逐条对本仓实际目录核实:** - `AGENTS.md` —— **你正在读的这个文件本身就是受管面**。 - `CLAUDE.md` —— 仓根一个。 - `.claude/**` —— **整棵树**:hooks、settings、launch 配置、内部 skills,一个不落,**不是只有 skills**。 - `skills/**` —— 仓根**发布给使用者**的那棵 skills 树(如 `skills/objectui/`),guard 里的 `skills-catalog` 一项 —— 见下方 ⚠️,这一项本段曾经写反。 - `docs/adr/**` —— 本仓**确实有**这个目录;它和上面四项同级,不因为篇数少而降级。 +- `.github/workflows/**` —— CI、部署与发版 workflow 决定每次 push 跑什么、拿什么门禁挡合并队列,改它们由维护者批准(维护者 2026-10-12 裁决,原文照录:「两仓加治理行」,objectui#12255)。guard 自己的 `governed-surface-guard.yml` 也在内;Dependabot 的 `github-actions` 升级同样受管 —— `dependabot-auto-merge.yml` 照常批准并入队,`merge_group` 腿照样拒绝,它等的也是一条获授权的批准。`.github/` 下其余文件(`dependabot.yml`、`labeler.yml`、`prompts/`)**不受管**。 ⚠️ **两棵 skills 树都受管 —— 别把它们和 skill 的安装位置弄混:** @@ -559,7 +560,7 @@ sha pin **退休**之后这条**更重、不是更轻**(维护者 2026-09-04 裁 - **CI 全绿、已 review 都不构成例外。** 这类文件是后续每一次 dispatch 读的操作规程,绿灯说明不了它该不该成为规程。 - **发现自己已经挂上了怎么办**:把 PR 转回 **draft** 是唯一能可靠退出合并队列的动作 —— 只调 `disable_pr_auto_merge` 会摘掉 auto-merge 但**不取消队列成员资格**,两个都要做。⚠️ 只回收**你自己**挂上的:本仓多 agent 共用同一 GitHub 身份,不是你设置的状态就属于别的 actor —— 去问、去报告,别替他回退。 -**本仓的机械兜底只有一件,它的两条腿意思不同:`merge_group` 腿拦得住合并队列(2026-09-27 起,见下),`pull_request` 腿只告警 —— 别把 PR 页上的绿读成不受管,也别再读成「什么都没有」。** 本仓仍然没有 CODEOWNERS(核实:仓内不存在该文件),受管面上也没有钩子;但 `.github/workflows/governed-surface-guard.yml`(check 名 `Governed Surface Queue Guard`,判定逻辑在 `scripts/check-governed-queue-guard.mjs`)**是活的**:`pull_request` 腿是早期告警、**故意 exit 0**(受管 PR 停在 draft 正是健康终态,所以**绿不等于不受管**),`merge_group` 腿才是会拒绝的那条 —— 它要求 `GOVERNED_APPROVERS`(`os-zhuang` / `hotlong`)里某个账号的一条 latest-decisive APPROVED review,**留在哪个 commit 上都算**;DISMISSED 与被顶掉的批准(同一 reviewer 后续给了 CHANGES_REQUESTED)不算,该集合之外账号的 APPROVED 不算,review 列表为空或读不到则 fail closed —— 判定读的是**有没有一条人工批准记录**,不问它是对哪些字节给的(维护者 2026-09-04 裁,逐字未译:「你的门禁有问题,只需要有人工批准记录就行,不需要卡最新的提交。」;sha pin 是**退休**不是放宽,守卫里已没有任何判定读 `commit_id`)。⚠️ 已接受的代价:批准之后的 push 不再被这道门重审,一个已批准的受管 PR 可以带着批准者没读过的字节落地 —— 维护者接受这一点,而这道拒绝先印的补救仍是转回 draft、交人类合并。⚠️ 而这条拒绝腿上的判据**不止上面那一条** —— #9018(PR #9212)给 `merge_group` 腿加了**第二条、与受管面完全无关的**判据:它枚举该 merge group 要落地的**每一个** PR(逐 commit 分解,不是只读 `merge_group.head_ref`),读每个 PR 的 label,其中任何一个仍挂着 `needs:contract-review` 就拒绝(**exit 6**);label 读不到、或该 group 指不出任何 PR,同样拒绝(**exit 7**)。⚠️ 这条判据读的是**挂在 PR 上的 label**,不读任何路径、不问 diff 命中了什么 —— ⇒ **一个 diff 完全没碰受管面的 merge group,现在也可能被这道 check 拒绝**;`pull_request` 腿不受影响,不读 label。⚠️ 它拦得住,是因为这个 check 名进了 ruleset 的 required 集合 —— 下面是一段有日期的历史,不是本段替你复核的现状:ruleset 开关只有维护者能翻;2026-09-14 的读数里它还不在集合中,那时这条拒绝腿只报告、不阻止队列;ruleset 的最后一次编辑在 2026-09-27,三分钟后 objectui#6596 以 completed 关闭,2026-09-28 实测它已在集合里。**在集合里,它的拒绝(上面的 exit 6 / exit 7 同样)就会把 PR 踢出合并队列。** 它现在还在不在,跑 `node scripts/check-required-check-set.mjs` 读,⛔ 别把它的答案抄进本段。⚠️ 这是按配置推出的,不是观察到的:登记后到 2026-09-28 的 `merge_group` 运行全部通过,拒绝路径还没在队列里真正触发过一次。事后一侧:`../objectstack` 的 report-only 合并后审计(`scripts/pm/check-governed-merges.mjs`)自 objectstack#9619 起**已覆盖本仓**(四个受管仓一次扫完),它把受管面的合并列出来,但它不阻止任何事。⇒ 一个没有获授权批准记录的受管 PR 经合并队列落不了地,这一半队列替你拦;**拦不住的是批准本身**:席位共用一个 GitHub 身份,守卫分不出那条 APPROVED 是不是人留的,绕过 ruleset 的人也不经这道门(谁能绕过,见上面 ruleset 那一条)—— 所以第五条禁令的效力仍然全在于你读到了它并照做。**⛔ 别再把本段当成兜底工具的完整清单** —— 覆盖面以脚本自己的 `GOVERNED_SURFACES` 为准(它随树变化,本段不会);⚠️ 该清单曾与上面的受管面清单**并不一致**(脚本的集合含已发布 `skills/**`),这一分歧**已裁**:维护者第 5 场决裁批 #7 采 **Option A**(#6866 评论 5469339478)—— 已发布 `skills/**` **受管**,守卫的读法才是裁定的那个;该裁决**已随本段上方的清单落地**(#6866):上面五项已含 `skills/**`,与脚本的 `GOVERNED_SURFACES` 一致,曾经那条「仓根 `skills/**` 不受管、自行入队」的豁免**已作废**,⛔ 别再照它行事。 +**本仓的机械兜底只有一件,它的两条腿意思不同:`merge_group` 腿拦得住合并队列(2026-09-27 起,见下),`pull_request` 腿只告警 —— 别把 PR 页上的绿读成不受管,也别再读成「什么都没有」。** 本仓仍然没有 CODEOWNERS(核实:仓内不存在该文件),受管面上也没有钩子;但 `.github/workflows/governed-surface-guard.yml`(check 名 `Governed Surface Queue Guard`,判定逻辑在 `scripts/check-governed-queue-guard.mjs`)**是活的**:`pull_request` 腿是早期告警、**故意 exit 0**(受管 PR 停在 draft 正是健康终态,所以**绿不等于不受管**),`merge_group` 腿才是会拒绝的那条 —— 它要求 `GOVERNED_APPROVERS`(`os-zhuang` / `hotlong`)里某个账号的一条 latest-decisive APPROVED review,**留在哪个 commit 上都算**;DISMISSED 与被顶掉的批准(同一 reviewer 后续给了 CHANGES_REQUESTED)不算,该集合之外账号的 APPROVED 不算,review 列表为空或读不到则 fail closed —— 判定读的是**有没有一条人工批准记录**,不问它是对哪些字节给的(维护者 2026-09-04 裁,逐字未译:「你的门禁有问题,只需要有人工批准记录就行,不需要卡最新的提交。」;sha pin 是**退休**不是放宽,守卫里已没有任何判定读 `commit_id`)。⚠️ 已接受的代价:批准之后的 push 不再被这道门重审,一个已批准的受管 PR 可以带着批准者没读过的字节落地 —— 维护者接受这一点,而这道拒绝先印的补救仍是转回 draft、交人类合并。⚠️ 而这条拒绝腿上的判据**不止上面那一条** —— #9018(PR #9212)给 `merge_group` 腿加了**第二条、与受管面完全无关的**判据:它枚举该 merge group 要落地的**每一个** PR(逐 commit 分解,不是只读 `merge_group.head_ref`),读每个 PR 的 label,其中任何一个仍挂着 `needs:contract-review` 就拒绝(**exit 6**);label 读不到、或该 group 指不出任何 PR,同样拒绝(**exit 7**)。⚠️ 这条判据读的是**挂在 PR 上的 label**,不读任何路径、不问 diff 命中了什么 —— ⇒ **一个 diff 完全没碰受管面的 merge group,现在也可能被这道 check 拒绝**;`pull_request` 腿不受影响,不读 label。⚠️ 它拦得住,是因为这个 check 名进了 ruleset 的 required 集合 —— 下面是一段有日期的历史,不是本段替你复核的现状:ruleset 开关只有维护者能翻;2026-09-14 的读数里它还不在集合中,那时这条拒绝腿只报告、不阻止队列;ruleset 的最后一次编辑在 2026-09-27,三分钟后 objectui#6596 以 completed 关闭,2026-09-28 实测它已在集合里。**在集合里,它的拒绝(上面的 exit 6 / exit 7 同样)就会把 PR 踢出合并队列。** 它现在还在不在,跑 `node scripts/check-required-check-set.mjs` 读,⛔ 别把它的答案抄进本段。⚠️ 这是按配置推出的,不是观察到的:登记后到 2026-09-28 的 `merge_group` 运行全部通过,拒绝路径还没在队列里真正触发过一次。事后一侧:`../objectstack` 的 report-only 合并后审计(`scripts/pm/check-governed-merges.mjs`)自 objectstack#9619 起**已覆盖本仓**(四个受管仓一次扫完),它把受管面的合并列出来,但它不阻止任何事。⇒ 一个没有获授权批准记录的受管 PR 经合并队列落不了地,这一半队列替你拦;**拦不住的是批准本身**:席位共用一个 GitHub 身份,守卫分不出那条 APPROVED 是不是人留的,绕过 ruleset 的人也不经这道门(谁能绕过,见上面 ruleset 那一条)—— 所以第五条禁令的效力仍然全在于你读到了它并照做。**⛔ 别再把本段当成兜底工具的完整清单** —— 覆盖面以脚本自己的 `GOVERNED_SURFACES` 为准(它随树变化,本段不会);⚠️ 该清单曾与上面的受管面清单**并不一致**(脚本的集合含已发布 `skills/**`),这一分歧**已裁**:维护者第 5 场决裁批 #7 采 **Option A**(#6866 评论 5469339478)—— 已发布 `skills/**` **受管**,守卫的读法才是裁定的那个;该裁决**已随本段上方的清单落地**(#6866):上面六项已含 `skills/**`,与脚本的 `GOVERNED_SURFACES` 一致,曾经那条「仓根 `skills/**` 不受管、自行入队」的豁免**已作废**,⛔ 别再照它行事。 ### 服务纪律(本仓库与 `../objectstack` 多 agent 并行开发) diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index f992b7ae1c..66e8a450d3 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -1788,8 +1788,10 @@ each *free* package is already imported by. **Blocks a PR?** Not on the pull request, by design. On a merge-queue build it refuses. The **governed surface** is a fixed list — `AGENTS.md`, `CLAUDE.md`, `.claude/**`, `skills/**`, -`docs/adr/**` — and the rule about it is that a change to any of them is merged by a human, not by -the queue. That rule used to live only in prose. On +`docs/adr/**` and, since the maintainer's ruling on +[#12255](https://github.com/objectstack-ai/objectui/issues/12255), `.github/workflows/**` — and +the rule about it is that a change to any of them is merged by a human, not by the queue. That rule +used to live only in prose. On [#6183](https://github.com/objectstack-ai/objectui/pull/6183) an `AGENTS.md` change was correctly parked as a draft; a GitHub MCP `update_pull_request` call passing only `reviewers` silently also set `draft: false`; the pull request entered the merge queue and landed with no human approval, and @@ -1857,8 +1859,13 @@ the sweeper copy whose H31 compared the gate's two carriers was retired carriers either. The `CLEAR` rendering says so out loud, so a green is never read as "the review happened". -**What it deliberately does not do.** It does not govern its own workflow or CI configuration -generally: that would be a larger rule than the one that was ruled. It cannot stop a maintainer +**What it deliberately does not do.** It does not govern the rest of `.github/` — `.github/dependabot.yml`, +`.github/labeler.yml`, `.github/prompts/` — because the ruling named the workflows, not their +directory. It *does* +govern its own workflow file, and a Dependabot `github-actions` bump with it: that bump edits +`.github/workflows/**`, so `dependabot-auto-merge.yml` still approves and enqueues it, the queue leg +refuses it, and it waits for an authorized approval like any other workflow change. It +cannot stop a maintainer merging a governed pull request by hand, and does not try — under this regime the human merge *is* the review record. And it does not make itself required: that is a branch-protection setting only the maintainer can flip. Until it is flipped, the queue leg reports without stopping anything. What diff --git a/scripts/__tests__/check-governed-queue-guard.test.ts b/scripts/__tests__/check-governed-queue-guard.test.ts index 6ca6506e02..a97f137c9c 100644 --- a/scripts/__tests__/check-governed-queue-guard.test.ts +++ b/scripts/__tests__/check-governed-queue-guard.test.ts @@ -146,14 +146,17 @@ describe('check-governed-queue-guard is wired, not merely present', () => { * nothing, and the reverse (a surface silently dropped) is the rule quietly * shrinking. */ -describe('the governed surface is the 2026-08-18 definition, and it is real', () => { - it('declares exactly the five ruled surfaces', () => { +describe('the governed surface is the ruled definition, and it is real', () => { + it('declares exactly the six ruled surfaces', () => { + // The five of the 2026-08-18 definition, then `.github/workflows/**`, which + // the maintainer's ruling on objectui#12255 added (「两仓加治理行」). expect(GOVERNED_SURFACES.map((s) => s.glob)).toEqual([ 'docs/adr/**', '.claude/**', 'skills/**', 'AGENTS.md', 'CLAUDE.md', + '.github/workflows/**', ]); }); @@ -174,12 +177,21 @@ describe('the governed surface is the 2026-08-18 definition, and it is real', () } }); - it('does not govern its own workflow, or CI configuration generally', () => { - // The widening deliberately NOT taken. Promoting CI config to a governed - // surface is a strictly larger rule than the one ruled, and taking it in an - // implementation would be a governance gate acquiring policy nobody agreed - // to. Pinned so a later edit has to be a decision. - expect(governedPathsIn([WORKFLOW, GATE, '.github/workflows/ci.yml'])).toEqual([]); + it('governs every workflow, its own included, and not the script that workflow runs', () => { + // This case used to pin the opposite ("does not govern its own workflow, or + // CI configuration generally") as the widening deliberately NOT taken. The + // maintainer took it on objectui#12255, so the pin is reversed, not deleted: + // a later edit that drops the row still has to be a decision. + const hit = governedPathsIn([WORKFLOW, GATE, '.github/workflows/ci.yml']); + expect(hit.map((s) => s.id)).toEqual(['workflows']); + expect(hit[0].files).toEqual([WORKFLOW, '.github/workflows/ci.yml']); + }); + + it('does not govern the rest of .github/ — the ruling named the workflows, not their directory', () => { + // YAML paths only: a markdown literal here would read, to + // `markdown-test-inputs.mjs`, as a document this test consumes. The self-test + // carries the `.github/prompts/**` negative. + expect(governedPathsIn(['.github/dependabot.yml', '.github/ISSUE_TEMPLATE/config.yml', '.github/labeler.yml'])).toEqual([]); }); it('the two root rows are EXACT — a vendored copy is ordinary source', () => { diff --git a/scripts/check-governed-queue-guard.mjs b/scripts/check-governed-queue-guard.mjs index 2457db3fe3..81a9eef604 100644 --- a/scripts/check-governed-queue-guard.mjs +++ b/scripts/check-governed-queue-guard.mjs @@ -283,17 +283,33 @@ export const EVENT_MERGE_GROUP = 'merge_group'; export const EVENT_PULL_REQUEST = 'pull_request'; /** - * The governed surface, verbatim from the 2026-08-18 definition the card - * restates: `AGENTS.md`, `CLAUDE.md`, `.claude/**`, `skills/**`, `docs/adr/**`. + * The governed surface: the 2026-08-18 definition the card restated — + * `AGENTS.md`, `CLAUDE.md`, `.claude/**`, `skills/**`, `docs/adr/**` — plus + * `.github/workflows/**`, which the maintainer ruled in on objectui#12255 + * (verbatim, untranslated: 「两仓加治理行」). * * ⛔ This list is the WHOLE scope and widening it is a maintainer decision, not - * an implementation detail. Two omissions are deliberate and worth naming so a - * future reader does not read them as oversights: - * - * - `.github/workflows/**` is NOT governed, so this guard does not govern its - * own workflow file. Making CI configuration governed is a strictly larger - * rule than the one ruled, and taking it here would be widening a - * governance gate past its own ruling. + * an implementation detail. The workflows row is that rule working, not an + * exception to it: until the ruling this comment named `.github/workflows/**` as + * a widening deliberately NOT taken, and the row arrived by the maintainer's + * word rather than by an implementation's reading. What it costs, named so a + * future reader does not read it as an accident: + * + * - this guard now governs its OWN workflow file, so a change to how the + * refusal is wired waits for the same authorized approval the refusal + * enforces. The script itself stays ordinary source (`scripts/**` is not a + * row). + * - a Dependabot `github-actions` bump edits `.github/workflows/**`, so it is + * governed like any other workflow change: `dependabot-auto-merge.yml` still + * approves and enqueues it, and the `merge_group` leg refuses it until an + * approval by an account in `GOVERNED_APPROVERS` is on record. + * + * Two omissions are deliberate and worth naming so a future reader does not read + * them as oversights: + * + * - the rest of `.github/` stays out (`dependabot.yml`, `labeler.yml`, + * `prompts/**`, an `ISSUE_TEMPLATE/**` tree): the ruling named the + * workflows, not the directory that holds them. * - `examples/**` and any vendored template copy of `AGENTS.md` stay out: the * two root rows are EXACT matches, so `examples/AGENTS.md` is ordinary * source. @@ -307,6 +323,7 @@ export const GOVERNED_SURFACES = Object.freeze([ Object.freeze({ id: 'skills-catalog', prefix: 'skills/', glob: 'skills/**', what: 'the published skills catalog' }), Object.freeze({ id: 'agents-md', exact: 'AGENTS.md', glob: 'AGENTS.md', what: 'the repo-root agent instruction file' }), Object.freeze({ id: 'claude-md', exact: 'CLAUDE.md', glob: 'CLAUDE.md', what: 'the repo-root Claude instruction file' }), + Object.freeze({ id: 'workflows', prefix: '.github/workflows/', glob: '.github/workflows/**', what: 'CI, deploy and release workflows: what runs on every push, what deploys, what gates the queue' }), ]); /** @@ -871,8 +888,11 @@ export function carrierPullsInGroup(rows, namedPull = null) { * was before this predicate existed. An early warning on the PR leg would be * useful and is deliberately NOT taken here: the ruling this leg implements is * about ENQUEUE, and widening a governance gate past its own ruling is how gates - * acquire policy nobody agreed to — the same restraint this file already applies - * to `.github/workflows/**`. Widening it is a one-line maintainer decision. + * acquire policy nobody agreed to — the same restraint this file applies to + * `GOVERNED_SURFACES`, whose rows arrive by a maintainer's ruling and never by an + * implementation (`.github/workflows/**` was named as a widening NOT taken until + * the maintainer ruled it in on objectui#12255). Widening it is a one-line + * maintainer decision. */ export function carrierVerdict({ event, pulls = [], readings = new Map(), apiCalls = 0 }) { const base = { event, entries: [], apiCalls, legName: CARRIER_LEG_NAME, label: CONTRACT_REVIEW_LABEL }; @@ -1341,13 +1361,14 @@ export async function selfTest() { // -- the governed surface IS the ruled definition, in both directions ------ // - // The card restates the 2026-08-18 definition as five surfaces. Pinning the - // membership BOTH ways is what stops a sixth being added here instead of by a - // maintainer, and a fifth being dropped by an edit nobody reads. - assert('the-register-declares-exactly-the-five-ruled-surfaces', GOVERNED_SURFACES.length === 5, String(GOVERNED_SURFACES.length)); + // The card restated the 2026-08-18 definition as five surfaces, and the + // maintainer's ruling on objectui#12255 added the sixth, `.github/workflows/**`. + // Pinning the membership BOTH ways is what stops a seventh being added here + // instead of by a maintainer, and one being dropped by an edit nobody reads. + assert('the-register-declares-exactly-the-six-ruled-surfaces', GOVERNED_SURFACES.length === 6, String(GOVERNED_SURFACES.length)); assert( 'and-they-are-the-ruled-globs-verbatim', - GOVERNED_SURFACES.map((s) => s.glob).join() === 'docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md', + GOVERNED_SURFACES.map((s) => s.glob).join() === 'docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md,.github/workflows/**', GOVERNED_SURFACES.map((s) => s.glob).join(), ); for (const surface of GOVERNED_SURFACES) { @@ -1358,10 +1379,23 @@ export async function selfTest() { // The two exact rows are EXACT: a vendored or example copy is ordinary source. assert('an-example-copy-of-AGENTS.md-is-not-the-governed-one', governedPathsIn(['examples/AGENTS.md']).length === 0); assert('a-near-miss-adr-directory-is-not-governed', governedPathsIn(['docs/adrs/z.md', 'docs/adr-notes.md']).length === 0); - // ⛔ The widening this guard deliberately does NOT take: it does not govern - // its own workflow, and pinning that keeps the scope honest against a future - // edit that quietly promotes CI config to a governed surface. - assert('this-guards-own-workflow-is-NOT-governed', governedPathsIn([`.github/workflows/${CHECK_WORKFLOW}`, 'scripts/check-governed-queue-guard.mjs']).length === 0); + // ⭐ This case used to pin the OPPOSITE — "this guard's own workflow is NOT + // governed" — as the widening deliberately not taken. The maintainer took it + // (objectui#12255), so it is reversed rather than deleted: the guard governs + // its own workflow file, and the script that file runs stays ordinary source. + const ownWiring = governedPathsIn([`.github/workflows/${CHECK_WORKFLOW}`, 'scripts/check-governed-queue-guard.mjs']); + assert( + 'this-guards-own-workflow-IS-governed-and-its-script-is-not', + ownWiring.length === 1 && ownWiring[0].id === 'workflows' && ownWiring[0].files.join() === `.github/workflows/${CHECK_WORKFLOW}`, + JSON.stringify(ownWiring.map((s) => [s.id, s.files])), + ); + // ⛔ The widening this guard still does NOT take: the ruling named the + // workflows, not the `.github/` directory that holds them. + assert( + 'the-rest-of-.github-is-NOT-governed', + governedPathsIn(['.github/dependabot.yml', '.github/ISSUE_TEMPLATE/config.yml', '.github/labeler.yml', '.github/prompts/component.prompt.md']).length === 0, + ); + assert('a-near-miss-workflows-path-is-not-governed', governedPathsIn(['.github/workflow/ci.yml', '.github/workflows.md']).length === 0); assert('ordinary-source-is-not-governed', governedPathsIn(['packages/core/src/index.ts', 'content/docs/guide/x.md', 'package.json']).length === 0); // -- the exit contract as a table ------------------------------------------ @@ -1521,6 +1555,16 @@ export async function selfTest() { new Map([[11, authorizedPass()], [12, authorizedApprovalVerdict([])]]), ); assert('one-approved-pr-does-NOT-carry-an-unapproved-sibling-through-the-same-group', partial.exitCode === EXIT_REFUSED_UNAPPROVED); + // The workflows row's consequence for Dependabot, pinned as the ruling's and + // not as a defect: a `github-actions` bump edits `.github/workflows/**`, and the + // approval `dependabot-auto-merge.yml` leaves is by `github-actions[bot]`, an + // account outside GOVERNED_APPROVERS — so the queue refuses it, and the PR leg + // only warns, exactly as for any other governed pull request. + const bumpRows = [row(4242, ['.github/workflows/ci.yml', '.github/workflows/lint.yml'])]; + const bumpQueued = run('merge_group', bumpRows, new Map([[4242, authorizedApprovalVerdict(approved('github-actions[bot]'))]])); + assert('a-github-actions-bump-approved-only-by-the-bot-is-REFUSED-at-the-queue-with-code-3', bumpQueued.conclusion === 'refused' && bumpQueued.exitCode === EXIT_REFUSED_UNAPPROVED); + const bumpWarned = run('pull_request', bumpRows, new Map([[4242, approvalVerdict(approved('github-actions[bot]'))]])); + assert('and-the-same-bump-is-not-reddened-on-its-pull-request', bumpWarned.exitCode === EXIT_CLEAR); // -- the pull_request leg is an EARLY WARNING and never reddens ------------- const warnedV = run('pull_request', [row(6183, ['AGENTS.md'])], new Map([[6183, approvalVerdict([])]])); @@ -2183,7 +2227,7 @@ export async function selfTest() { } console.log( `OK check-governed-queue-guard self-test: ${checked} cases pass ` + - '(the five ruled surfaces pinned in both directions including the widenings NOT taken, the queue/PR ' + + '(the six ruled surfaces pinned in both directions including the widenings NOT taken, the queue/PR ' + 'event split, latest-decisive approval reduction, the authorized-approval-record predicate on the queue ' + 'leg per the 2026-09-04 ruling — counts on an OLDER commit, on no commit_id and on an unparsable one; ' + 'never for an unauthorized, dismissed, superseded, non-decisive or absent review — with the PR leg ' + diff --git a/scripts/dependabot-merge-gate.mjs b/scripts/dependabot-merge-gate.mjs index 61276245dc..cd9da0a1e5 100644 --- a/scripts/dependabot-merge-gate.mjs +++ b/scripts/dependabot-merge-gate.mjs @@ -161,9 +161,15 @@ import { isEntrypoint } from './invoked-as.mjs'; * `merge_group` leg. Listing it here is still correct and still load-bearing: * this list is what `merge-queue-reporting.test.ts` derives the `merge_group` * subscription floor from, and a guard whose whole point is the queue build must - * be inside that floor. It also costs a Dependabot bump nothing — such a diff - * touches no governed path, so the check reports CLEAR without making a single - * API call. + * be inside that floor. What it costs a Dependabot bump depends on the + * ecosystem. An `npm` bump touches no governed path, so the check reports CLEAR + * without making a single API call. A `github-actions` bump edits + * `.github/workflows/**`, which is governed since objectui#12255: on the pull + * request the check is still green (an early warning, one review read), so this + * gate still goes green and the workflow still approves and enqueues — but the + * approval is `github-actions[bot]`'s, outside the guard's GOVERNED_APPROVERS, + * so the `merge_group` leg refuses the bump until an authorized approval is on + * record. That wait is the ruling's consequence, not a defect of this gate. * * The four shards are spelled out individually on purpose. A single `Test` * entry, or any pattern match, would be satisfied by whichever shard happened