From 6a60b743fa3fc100a61a124f9ec51f359863a081 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 00:03:11 +0000 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=E4=B8=A4=E4=B8=AA=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E5=86=99=E5=85=A5=20job=20=E4=B8=8D=E5=86=8D=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E3=80=81=E4=B9=9F=E4=B8=8D=E5=86=8D=E8=A2=AB=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E9=87=8D=E5=A4=8D=E8=A7=A6=E5=8F=91=20(#5649?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Check PR Size` 与 `Auto Label` 都用**整集 PUT**(`PUT /issues/{n}/labels`) 写标签,不是新增 POST。源码实测(非文档推断): - `codelytv/pr-size-labeler@v1.10.4` `src/github.sh:68-91`:GET 读 PR 标签, grep 掉自己那套 size 家族,追加新 size 标签,然后 `curl -X PUT`。 - `actions/labeler@v7.0.0` `src/labeler.ts:56,111-133` + `src/api/set-labels.ts`: run 开始时快照 `preexistingLabels`,并入 config 命中项,回读一次实时标签, 再 `client.rest.issues.setLabels` —— 就是 PUT。 两者都没有把写入改成新增的输入项;`sync-labels` 也不是那个开关 —— 它只管 「config 自己拥有的标签在 glob 不再命中时是否删掉」(`labeler.ts:81-83`), 缺省已是 false。本 PR 仍把它显式写出,只为防升级漂移,不是本缺陷的修复。 整集 PUT 只在「别人的写入落在读→PUT 窗口内」时才有破坏性,所以本文件能修的是 **重叠**: 1. 两个写入方不再并发(`auto-label` needs `pr-size`)。原先由同一事件同时拉起、 窗口完全重合:PR #5650 run 31051251795(`opened`)里 `Add size label` 22:03:47->22:03:49、`Label based on changed files` 22:03:47->22:03:49, labeler 的 PUT 在 22:03:49 发出 `unlabeled size/s` —— 抹掉的是它不管的标签。 2. 两个写入方不再被 `labeled`/`unlabeled` 触发。它们唯一的输入是 diff,标签事件 改不了 diff,这种 run 只能把同一个集合再 PUT 一遍 —— 零新信息,多一次互抹 机会。同 PR run 31051273625(由标签事件拉起):`Auto Label` 重算后没写, `Check PR Size` 在 22:04:22 又 PUT 了一次。两个事件类型保留在 `on:` 里, 因为 `changeset-check` 确实需要(#5580)。 未被本 PR 关闭、并写进文件注释而非留给下一个读者踩:**工作流之外**的写入方 (`gh pr create` 后几秒挂标签的 agent 或人,正好落在这两个 job 运行期间) 仍可能落进 PUT 窗口被抹 —— #5533 的 `skip-changeset` 豁免只活一秒就是这样丢的 (15:46:44 挂上,15:46:45 被 labeler 的 `{size/m, tests}` PUT 抹掉)。关掉这半 需要写入本身变成新增语义,不是排序问题,任何本文件的配置都替代不了。 `changeset-check` 的实时读与计数逻辑(#5580/#5625)、`allow-major`(#5620)一字未动。 --- .github/workflows/pr-automation.yml | 72 +++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml index b45b38c62a..7e36a0152b 100644 --- a/.github/workflows/pr-automation.yml +++ b/.github/workflows/pr-automation.yml @@ -5,8 +5,59 @@ on: types: [opened, synchronize, reopened, labeled, unlabeled] jobs: + # =========================================================================== + # Both label-writing jobs below write this PR's label set with a WHOLE-SET PUT + # (`PUT /issues/{n}/labels`), never an additive POST. Read out of the pinned + # sources rather than inferred from the docs (#5649): + # + # * codelytv/pr-size-labeler@v1.10.4 -- src/github.sh:68-91 + # (`github::add_label_to_pr`): GETs the PR, greps its OWN size family out + # of the result, appends the new size label, then + # `curl -X PUT .../issues/$pr_number/labels` with the whole set. + # * actions/labeler@v7.0.0 -- src/labeler.ts:56,111-133 plus + # src/api/set-labels.ts: snapshots `preexistingLabels` at run start, + # unions in the config matches, re-reads the live label list once, then + # calls `client.rest.issues.setLabels` -- which IS the PUT. + # + # Neither action exposes an input that makes its write additive, and + # `sync-labels` is NOT that input: it only decides whether a label the CONFIG + # owns is dropped once its globs stop matching (labeler.ts:81-83). It is + # pinned explicitly below for upgrade-drift protection only. It does not, and + # cannot, stop the clobbering described here. + # + # A whole-set PUT only destroys someone else's label when that label lands + # inside the window between the writer's read and its PUT. What this file can + # therefore fix is the OVERLAP, and two changes below do exactly that: + # + # 1. The two writers no longer run concurrently -- `auto-label` needs + # `pr-size`. They used to be started by the same event and overlapped + # exactly. Live specimen, PR #5650 run 31051251795 (the `opened` run): + # `Add size label` ran 22:03:47->22:03:49 and + # `Label based on changed files` ran 22:03:47->22:03:49, and the + # labeler's PUT emitted `unlabeled size/s` at 22:03:49 -- one second + # after the size job added it, for a label the labeler does not manage. + # 2. Neither writer runs on `labeled`/`unlabeled` any more. Their only input + # is the diff, which a label event cannot change, so such a run could + # only ever re-PUT the same set -- one more chance to erase a concurrent + # writer in exchange for no new information. Same PR, run 31051273625 + # (started by a label event): `Auto Label` recomputed and wrote nothing, + # `Check PR Size` re-PUT at 22:04:22. The two event types stay in `on:` + # because `changeset-check` genuinely needs them (#5580). + # + # NOT closed by either change, and deliberately recorded rather than implied: + # a writer OUTSIDE this workflow -- an agent or a human labelling the PR + # seconds after `gh pr create`, i.e. exactly while these jobs run -- can still + # land inside a PUT window and be erased. That is how #5533 lost its + # `skip-changeset` exemption for one second (15:46:44 applied, 15:46:45 erased + # by the labeler's PUT of `{size/m, tests}`). Closing that half needs the + # writes themselves to become additive, not merely better ordered; it is the + # open half of #5649 and no configuration here can stand in for it. + # =========================================================================== pr-size: name: Check PR Size + # A `labeled`/`unlabeled` event cannot change this job's input (the diff), + # so running it there buys nothing and costs one whole-set PUT. See above. + if: github.event.action != 'labeled' && github.event.action != 'unlabeled' runs-on: ubuntu-latest permissions: pull-requests: write @@ -31,6 +82,19 @@ jobs: auto-label: name: Auto Label + # ORDERING ONLY, not a dependency: this job wants `pr-size`'s PUT to be + # already done, so that the label set this one reads includes the size + # label and its own PUT carries it forward. `!cancelled()` is written out + # because GitHub would otherwise wrap this `if:` in an implicit `success()` + # -- a failed or skipped size job must not silently stop path labelling. + # (Same reasoning the check-workflow-status-functions gate exists to make + # explicit; that gate scans only `needs.*.outputs.*` reads, so this one is + # out of its scope and has to state its intent by hand.) + needs: pr-size + if: >- + !cancelled() + && github.event.action != 'labeled' + && github.event.action != 'unlabeled' runs-on: ubuntu-latest permissions: contents: read @@ -45,6 +109,14 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} configuration-path: .github/labeler.yml + # Pinned at the value it already defaults to (action.yml), because a + # default is not a decision: an upgrade may move it, and `true` would + # make this step REMOVE a label of its own config whenever the globs + # stop matching -- on a `synchronize` that reverts a docs file, for + # instance. Pinning it is upgrade-drift protection and nothing more: + # `sync-labels` never governed foreign labels, so it is NOT the fix + # for the clobbering documented at the top of this file (#5649). + sync-labels: false changeset-check: name: Check Changeset