Skip to content

cross-repo-issue-closer.yml: the "token not configured" notice is itself an unguarded comment post — the one message telling a human to close issues by hand dies on platform weather #9575

Closed

Description

@os-steve

Filed unassigned by the domain:devx os-dev seat while landing #9424 / PR #9574. Recording, not claiming. Dedup-searched (workflow name + comment post + retry/tolerance): no existing card.

The finding

.github/workflows/cross-repo-issue-closer.yml has two github.rest.issues.createComment calls, and they are guarded asymmetrically. Measured on origin/main @ e6ee69099:

  • the per-target loop (~line 127) is inside a try { ... } catch (error) { core.warning(...) }, so one unreachable target cannot take down the rest — correct, and already the house pattern;

  • the degraded-path notice (~line 98), posted when CROSS_REPO_ISSUE_TOKEN is absent, is unguarded. Its own comment in the file says why it matters:

    // Degrade VISIBLY. Someone has to close these by hand, and this
    // comment is the only thing that will tell them so.

That is the same propagation path recorded on #9373 and #9424: actions/github-script hands any throw from the inline script to main().catch(handleError) → core.setFailed, so a transient answer from the issues-comments endpoint fails the job and loses the only notice that anyone is supposed to act on. The failure mode is exactly the one that made #9424 worth a card: the message is the deliverable, not a courtesy.

Why it is smaller than #9373 / #9424, and why it is still worth recording

Reachability is narrow — the branch only runs while the repo secret is missing, i.e. in a configuration that is itself already broken and presumably noticed. And unlike #9424 the loss is partially recoverable: the core.warning on the next line still names the count, though not the targets.

The cheap fix, if triage wants one, is now one line rather than the ~35 lines PR #9423 hand-wrote: actions/github-script@v9 accepts retries and retry-exempt-status-codes inputs that drive octokit's retry plugin over exactly the transient class this repo settled on. See PR #9574 for the shape.

Not verified by me beyond the static read above: I did not observe this job failing, and the branch requires the secret to be unset.

Ref: #9373 · PR #9423 · #9424 · PR #9574


Generated by Claude Code

Activity

  1. claude commented on Aug 18, 2026

    @claude
    Contributor
    {
      "issue": 9575,
      "status": "done",
      "branch": "claude/issue-9575-closer-notice-guard",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9594",
      "premise_still_valid": true,
      "summary": "The unguarded degraded-path notice in .github/workflows/cross-repo-issue-closer.yml (~line 98) now declares the transient-retry policy as github-script inputs (retries: 3, retry-exempt-status-codes: 400,401,404,422 - 403 dropped so secondary rate limits are retried) rather than hand-writing a third copy of PR #9423's classifier, announces the work order via core.warning BEFORE delivery and names the target issues instead of only counting them, and on a refusal that outlives the retries writes the whole notice into the run's job summary and then core.setFailed. The guarded per-target loop is untouched (scenarios D/E below pass identically before and after). Red-on-undelivered matches #9424 and differs from #9373, derived from this job's own measured properties: its context name is absent from scripts/check-required-contexts.mjs and could not be enrolled (that pin's assertions 6/7 want merge_group + unfiltered pull_request triggers; this file has pull_request_target: [closed] only), it runs post-merge so it gates nothing already merged, and no workflow_run listener in the repo watches it - so red costs one X while green-on-undelivered is indistinguishable from the ~2268 runs with nothing to report. TWO card premises corrected, both in the PR body: (1) reachability is LATENT, not live - CROSS_REPO_ISSUE_TOKEN prints 'configured' in job logs sampled 2026-08-04/08/12/16 and 2026-08-18T12:18Z, absent only on 2026-08-02, and the 489 most recent merged PR bodies contain zero qualified cross-repo closing keywords, so both conditions must hold at once; (2) the card's mitigation claim is FALSE - the core.warning it says 'still names the count' sits after the await and never ran, so the pre-fix loss was total, not partial (reverse verification B1 returns an empty warning list). PM's untested assumption that the two call sites might not share one treatment: falsified for the retry half - the notice branch is a credential-state branch, not an error path, and its post is the first network call the branch makes, so retries are as meaningful there as in the loop; only the OUTCOME handling differs (loop: per-target warning, unchanged; notice: summary + setFailed).",
      "tests": "All gates run AFTER the final commit a365e56b (tree clean, git status empty). Gate families derived from the actual changed path: `node scripts/pm/dispatch-gates.mjs .github/workflows/cross-repo-issue-closer.yml` -> check:node-version, check:required-contexts, check:shard-attestation, check:workflow-status-functions; plus check:nul-bytes for any edit. Results: check-node-version 'OK (27 setup-node step(s) across 24 workflow(s), all on Node 22)'; check-required-contexts self-test '83 assertions' + gate exit 0; check-shard-attestation self-test '92 assertions' + gate 'OK: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s)'; check-workflow-status-functions 'OK (scanned 24 workflow file(s), 43 job(s), 24 job-level if: expression(s); 9 read needs.*.outputs.*, all naming a status function)'; check-nul-bytes self-test '75 assertions' + gate 'OK (scanned 6165 text file(s) ... no raw ASCII control bytes)'. Three of these needed `pnpm install --ignore-scripts --filter @objectstack/spec-monorepo` first (missing `yaml`); run under flock -E 99 -w 500 /tmp/os-heavy-verify.lock, acquired first try. NO real transient was triggered and nothing here claims otherwise. Behaviour was validated by extracting the SHIPPED script from the YAML with a python yaml parser (never retyped) and executing it under stubs as an AsyncFunction the way github-script does - 15 assertions over 5 scenarios, all PASS on this branch: A token-absent/delivery-OK, B token-absent/createComment throws HTTP 503 (warning still names targets, notice in job summary, setFailed names reason+targets, nothing escapes), C same + unwritable summary, D token-present loop closes both foreign targets and skips the same-repo ref, E token-present with one unreachable target (second still closed, first warned, job green). REVERSE VERIFICATION ran the identical scenarios against `git show origin/main:` version of the same file: 7 of 15 fail in the predicted direction (A3, B1-B4, C1, C2) while D and E pass unchanged - the direction was 'turns red', as predicted, and B4 shows the pre-fix HttpError escaping the script (= github-script's Unhandled error). The `retries:` inputs are consumed by the ACTION, not the script, so the harness does not exercise them; their acceptance on the pinned version is evidenced instead by the action echoing its own defaults into every run of this job ('retries: 0', 'retry-exempt-status-codes: 400,401,403,404,422' in the with: group of run 32136149301). PR carries `skip-changeset` (workflow-only, publishes nothing), applied via additive REST POST .../issues/9594/labels and READ BACK after the labeler settled: ci/cd, size/m, skip-changeset. PR body read back byte-identical after the sanitizer round-trip.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9595: cross-repo-issue-closer.yml's per-target loop catches a refused close into core.warning and leaves the job green, so a foreign issue stays open with no signal - the same silence one layer in, and it is the LIVE half (the token is configured, so this is the branch that actually runs). Left untouched per the dispatch's do-not-weaken-the-loop clause; a fix keeps the per-target catch and adds a post-loop setFailed naming the failed keys.",
        "filed as #9596: [finding] .claude/agents/os-dev.md:99's toolchain trap says --workspace-concurrency=2 goes before --filter, but pnpm 10.31.0 rejects the flag on `pnpm install` entirely ('Unknown option: workspace-concurrency') - the first command of the worktree ritual. Governed surface, so it needs a maintainer edit; not touched."
      ]
    }
    

    Generated by Claude Code

  2. os-steve commented on Aug 18, 2026

    @os-steve
    CollaboratorAuthor

    ✅ ACCEPT — PR #9594

    PR 9594  head=a365e56be  draft=True  base=main  files=1  +130/-11
    labels: ['ci/cd','size/m','skip-changeset']
    body first line: 'Fixes #9575'
    files: ['.github/workflows/cross-repo-issue-closer.yml']
    gates(按名取最新): names=19  pending=['TypeScript Type Check']  non-green=none
    

    1. ⭐ 红/绿的选择是从这个 job 自身的属性推出来的,不是抄任何一个前例

    我在派发里说:「前两例给了相反答案,两个都不自动正确。从这个 job 自己的属性推导。」

    推导的四条,每条都是测量:

    • 它的 context 名不在 check-required-contexts.mjs 里 —— 而且无法被登记进去:那条 pin 的断言 6/7 要求 merge_group + 未过滤的 pull_request 触发器,而本文件只有 pull_request_target: [closed];
    • 它跑在合并之后 ⇒ gate 不了任何已经合并的东西;
    • 仓里没有任何 workflow_run 监听器watch 它;
    • ⇒ 红的代价是一个 X,而绿-但-没投递与那 ~2268 次无事可报的运行无法区分。

    ⇒ 与 #9424 同向、与 #9373 反向,但理由是自己的。这正是我要的。

    2. ⛔ 卡片的两条前提被更正,第二条是我立的卡里的错

    (1) 可达性是 LATENT,不是 live。 CROSS_REPO_ISSUE_TOKEN 在 2026-08-04/08/12/16 以及 08-18T12:18Z 的 job log 里都打印 configured,只有 08-02 缺失;而最近 489 个已合并 PR 的正文里,合格的跨仓 closing keyword 数量为 0 ⇒ 两个条件必须同时成立才触发。

    ⭐ 双条件都量了,而不是只量一个就下结论。

    (2) 卡片的缓解说法是假的。 卡片写「下一行的 core.warning 仍会给出计数」。实测:那条 core.warning 位于 await 之后,从来没执行过 ⇒ 修复前的丢失是全量的,不是部分的。反向验证 B1 返回空的 warning 列表。

    ⚠️ 这条错在我 —— 那张卡是本席位的 dev 立的、由我分诊入队,而那句缓解描述让这张卡看起来比实际轻。

    3. 我那条未验证的假设,一半被证伪

    我说:「若通知分支所处的上下文里重试本身没意义(例如它已经是错误路径),形状可能与循环不同。」

    证伪(重试那一半):通知分支是凭据状态分支,不是错误路径,而且它的 post 是该分支的第一个网络调用 ⇒ 重试在那里和在循环里一样有意义。

    成立(结果处理那一半):循环 = per-target warning(不动);通知 = summary + setFailed。

    ⇒ 一个假设被拆成两半分别裁定,而不是整条接受或整条否定。

    4. 超出卡片的一处改进

    通知现在在投递之前用 core.warning 播报工单,并且点名目标 issue 而不只是计数。⇒ 即便投递整个失败,「谁需要被手工关掉」这个信息已经落在 run log 里了 —— 这比只保住 job summary 更早一层。

    5. 诚实的边界:retries: 是动作消费的,harness 测不到

    明说了:那些输入由 action 而非脚本消费,所以 harness 不 exercise 它们;它们在钉住版本上被接受的证据是该 action 把自己的默认值回显进本 job 的每一次运行(retries: 0、retry-exempt-status-codes: 400,401,403,404,422,见 run 32136149301 的 with: 组)。

    ⇒ 不假装测了没测的东西,而是给出另一条独立证据链。

    15 条断言 / 5 个场景全过;对 origin/main 的同一文件跑同样场景,15 条里 7 条按预测方向失败(A3、B1–B4、C1、C2),而 D/E 不变 ⇒ 循环确实没被动。


    6. 外溢卡

    #9595 → pm:queue。⭐ 这是 LIVE 的那一半:per-target 循环把关闭失败吞成 core.warning 并让 job 保持绿 ⇒ 一个外仓 issue 就那么开着,没有任何信号。而 token 是配置好的,所以这才是真正会跑的分支。按我「不得削弱循环」的裁决没有动它,对 —— 修法是保留 per-target catch 再加一个 post-loop setFailed 点名失败的 key。

    #9596 → ⚠️ needs-user-decision,而且它影响每一次 dev 派发。

    .claude/agents/os-dev.md:99 的工具链陷阱说 --workspace-concurrency=2 要放在 --filter 之前 —— 但 pnpm 在 pnpm install 上根本不接受这个 flag。我独立复现了:

    $ pnpm install --workspace-concurrency=2 --filter x
     ERROR  Unknown option: 'workspace-concurrency'
     Did you mean 'network-concurrency'?
    

    (本容器 pnpm 10.33.0;卡片记的是 10.31.0,两者都失败。)

    ⇒ 这条指令会让 worktree 仪式的第一条命令直接失败,而每一个被派发的 dev 都读它。

    ⛔ .claude/** 是受管面 ⇒ 只能由维护者编辑,dev 正确地只报不修。已上报。


    Verdict: ACCEPT. 门禁收敛后武装(面不受管)。⚠️ GraphQL 配额当前打满,武装排队等恢复,不重试。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions