Files
DeepSeek-TUI/.github/workflows/pr-issue-link.yml
Hunter Bown e8d2538a9d fix(ci): repair three shared check failures blocking open PRs (#5017)
* fix(ci): skip the PR-issue link gate for bot-authored PRs

The `link` job fails on every dependabot PR (e.g. #5010-#5016): dependabot
bodies are machine-generated dependency bumps that never contain a closing
keyword, so the gate exits 1 ("This PR neither closes an issue nor says why
it doesn't", run 30665868640/job 91272641294).

Skip the gate step when the PR author is a GitHub-verified bot account
(`pull_request.user.type == 'Bot'`, set by GitHub so it cannot be spoofed).
This covers dependabot today and any other automation (renovate, etc.)
without hand-editing every bot body, while keeping the gate strict for every
human PR.

Policy choice: all verified bots, not just dependabot[bot]. Bots cannot
respond to a prompt, and their changes never close issues; the check's
purpose is keeping human work linked to its issue.

* fix(ci): provision the deny container's Rust toolchain explicitly

cargo-deny runs inside EmbarkStudios/cargo-deny-action@v2's alpine container
(host triple x86_64-unknown-linux-musl, rustup default 1.85.0-musl). The
repo's rust-toolchain.toml pins `channel = "stable"` with no target, so the
action's `rustup show` (entrypoint.sh) resolves it to
`stable-x86_64-unknown-linux-musl` — not installed — and every run prints:

  error: override toolchain 'stable-x86_64-unknown-linux-musl' is not
  installed: the toolchain file at '/github/workspace/rust-toolchain.toml'
  specifies an uninstalled toolchain

then auto-installs mid-run (network-dependent ~12s detour; hard failure if
the download stalls; seen in run 30665967039/job 91272951661).

Pass `rust-version: stable` so the entrypoint provisions the channel
(`rustup default stable`, same musl host triple) before cargo-deny touches
the workspace; the toolchain file then resolves to an installed toolchain.

* fix(deps): bump event-listener 5.4.1 -> 5.4.2 (RUSTSEC-2026-0221)

cargo-deny (advisories) fails on every dependabot PR (e.g. run
30665967039/job 91272951661) with the unsound advisory:

  error[unsound]: `event-listener` allows `!Send` tags to cross thread
  boundaries via `StackSlot`  (RUSTSEC-2026-0221)

event-listener 5.4.1 unconditionally implements Send/Sync for StackSlot,
allowing a !Send tag to cross threads via Event::with_tag. Pulled in via
codewhale-tui -> codewhale-workflow-js -> rquickjs -> rquickjs-core ->
async-lock (and event-listener-strategy). Advisory solution: >=5.4.2, the
latest published version.

`cargo update -p event-listener` lands exactly on 5.4.2 (also drops its now
unneeded concurrent-queue dependency). No deny.toml ignore needed.

* fix(ci): fetch the locked graph before the offline runtime-contract check

The Lint job's "Check runtime-contract budget" step fails on every
dependabot PR (run 30665967040/job 91273020141, PR #5016):

  error: failed to download `assert-json-diff v2.0.2`
  Caused by: attempting to make an HTTP request, but --offline was specified
  subprocess.CalledProcessError: ... returned non-zero exit status 101.
  [runtime-contract-budget] ERROR: runtime-contract measurement failed with exit code 1

Root cause: check-runtime-contract-budget.py hardcodes CARGO_NET_OFFLINE=true
and runs `cargo test --locked` on codewhale-tui, which needs the dev-dependency
graph (wiremock -> assert-json-diff). Clippy above builds no test targets, and
Swatinem/rust-cache keys its registry cache on Cargo.lock, so any lock-changing
PR (every dependabot bump) misses the cache and the offline test cannot
download the missing crate. Reproduced locally with the identical error by
removing assert-json-diff from the registry cache and re-running the exact
CI command.

Fix: fetch the full locked graph (`cargo fetch --locked`, dev-deps included)
once in the heavy path before the measurement, so the hermetic offline
measurement is deterministic on every branch. The budget contract itself is
unchanged and still enforced.

* fix(ci): reviewed source-structure budget update for merged LaTeX module

Merging #4981 (LaTeX rendering) added crates/tui/src/tui/history/
latex_render.rs (1734 lines) and grew aggregate owned Rust source to the
measured merged state (644756 lines). The ratchet's contract requires an
explicit reviewed update for new thousand-line modules and aggregate
growth; this commit allows the merged module and raises the ceiling to
the exact CI-measured value. Verified: check-source-structure-budget.py
PASS on the branch; runtime-contract budget still PASS 55/55.
2026-07-31 21:21:07 -07:00

79 lines
3.5 KiB
YAML

name: PR closes an issue
# 342 open issues, 329 of them touched within the month: nothing here is rotting,
# the drain is just clogged. Only 8 of 35 open PRs carried a closing keyword, so
# work ships and its issue stays open, and nobody can tell which of the 342 are
# already done. That is how 121 issues end up on one milestone.
#
# This check asks every PR to either close an issue or say why it doesn't. The
# opt-out is one line, so this is a prompt, not a wall.
on:
pull_request:
types: [opened, edited, reopened, synchronize]
permissions:
contents: read
pull-requests: read
jobs:
link:
runs-on: ubuntu-latest
steps:
# Automated dependency bumps (dependabot and any other GitHub-verified
# bot account) are machine-generated and can never carry a closing
# keyword; failing them here would require hand-editing every bot body,
# which defeats the automation. The gate stays strict for every human
# PR. `user.type` is set by GitHub for verified bot accounts, so a PR
# author cannot spoof it to dodge the check.
- name: Require a closing keyword or an explicit opt-out
if: github.event.pull_request.user.type != 'Bot'
env:
# Fetched live rather than read from the event payload. A rerun
# replays the payload the run started with, so a body-only fix could
# never turn this check green: the obvious operator move — add the
# missing line, rerun the failed check — re-read the old body and
# failed again with no hint why. Reading the current body makes a
# rerun mean what everyone already assumes it means.
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# Through a variable, never interpolated into the script body:
# a PR body is attacker-controlled text.
PR_BODY=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json body --jq '.body // ""')
# Body only, deliberately. GitHub resolves closing keywords from the
# PR description; a "Closes #123" in the title auto-closes nothing.
# Accepting the title here would pass PRs that never close an issue,
# which is the exact false-assurance this check exists to prevent.
text="${PR_BODY:-}"
# GitHub's own closing-keyword set, plus the #N it must attach to.
if grep -qiE '\b(close[sd]?|fix(e[sd])?|resolve[sd]?)\b[[:space:]]*:?[[:space:]]*#[0-9]+' <<<"$text"; then
echo "Closing keyword found — this PR will close its issue on merge."
exit 0
fi
# One-line escape hatch. Anything after the marker is the reason.
if grep -qiE '^[[:space:]]*No-Issue:[[:space:]]*\S' <<<"$text"; then
reason=$(grep -iE '^[[:space:]]*No-Issue:' <<<"$text" | head -1)
echo "Opted out — ${reason}"
exit 0
fi
cat >&2 <<'MSG'
This PR neither closes an issue nor says why it doesn't.
Add one of these to the PR body:
Closes #1234 (or Fixes / Resolves — any of GitHub's keywords)
No-Issue: <one-line why> (chores, docs typos, revert, dependency bump)
Why this is a required check: work here ships faster than issues close,
so an unlinked PR leaves its issue open forever and the backlog stops
reflecting reality. Either line takes five seconds and keeps the
milestone honest.
MSG
exit 1