Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions .github/workflows/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,127 @@ Validates that PR documentation is properly updated:

Uses a shared reusable workflow.

**Skipping this job blocks the PR — it does not just skip a check.**
`docs-check / docs-check` is a required status check in the `main` ruleset,
and that two-part context name exists only because this job calls a
reusable workflow. A job that calls a reusable workflow reports one check
per job _inside_ that workflow, prefixed by the caller's job id. Skip the
caller and there are no inner jobs, so GitHub reports a single check named
`docs-check` — a name nothing requires — and the required
`docs-check / docs-check` context never reports at all. GitHub treats a
required context that never reports as still-pending, so
`mergeable_state` stays `blocked` forever with no failing check to point
at. This is the opposite of the usual case, where a skipped required job
reports `skipped` under its own required name and satisfies the rule.

PR [#121](https://github.com/Uniswap/uniswap-ai/pull/121) sat in exactly
that state: `docs-check` reported `skipped`, `docs-check / docs-check`
never appeared, and the PR could not be merged.

**Bot gating uses the numeric account id, not the login.** The job's `if:`
admits same-repo PRs from any human (`user.type != 'Bot'`) plus the one bot
whose PRs this repo wants documentation-checked, matched as
`github.event.pull_request.user.id == 209825114` — the `claude[bot]`
account, from the Claude GitHub App (app id 1236702). Prefer that id over
`github.actor`, `user.login`, or `contains(login, '[bot]')` in any new
gate: logins are mutable and describe a naming convention, while the
numeric id is assigned by GitHub at account creation, survives renames,
and cannot be chosen by the account holder. `user.type` is GitHub's own
account-kind field, so Dependabot and Renovate stay skipped without
matching on the `[bot]` suffix.

Note this differs from `claude-code-review.yml`, which still gates on
`user.login == 'claude[bot]'`. That workflow is not a required check, so a
mis-fire there skips a review rather than deadlocking the PR; converting
it to the numeric id is worthwhile but is not load-bearing the way this
one is.

**There are two independent bot gates, and only the first is in this
repo.** Clearing the `if:` above is necessary but not sufficient:

1. **The workflow-level `if:`** decided whether the job ran at all. This is
the one that caused the deadlock, and it is fixed here.
2. **`anthropics/claude-code-action`'s own actor check**, reached only once
the job actually starts. It refuses a non-`User` actor unless the actor
is listed in the action's `allowed_bots` input, which defaults to `""`
(allow no bots). The failure reads:

```text
Workflow initiated by non-human actor: claude (type: Bot).
Add bot to allowed_bots list or use '*' to allow all bots.
```

**This cannot be set from this repo, and the value is hardcoded in the
toolkit.** Both ai-toolkit reusable workflows this repo calls decide
`allowed_bots` themselves and expose no input for it:

| Reusable workflow | What it passes | Effect on `claude[bot]` |
| --------------------------- | -------------------------------------- | -------------------------- |
| `_claude-docs-check.yml` | nothing (action default `""`) | rejected — no bots allowed |
| `_generate-pr-metadata.yml` | `allowed_bots: dependabot` (hardcoded) | rejected — not in list |

`_claude-docs-check.yml` declares 14 `workflow_call` inputs, none of them
`allowed_bots`, and neither of its two `claude-code-action` steps forwards
one. Verified against the pinned SHA and against the toolkit's `main` and
`next`, which are byte-identical here — so there is no input to pass and
no newer ref that helps. The fix belongs in ai-toolkit: either add an
`allowed_bots` input and forward it, or extend the hardcoded list to
`dependabot,claude`. Do not reach for `'*'`: it would admit every bot,
including external Apps on a public repo.

This is also why `generate-metadata / generate-metadata` fails on
bot-authored PRs — the same actor check, one line of hardcoded config
away. It is not a required check, so it does not block merges.

**The `@uniswap/review-cli` path is immune to this gate**, which is a
real argument for migrating the docs check to it rather than repairing
the toolkit call. `claude-code-review.yml` invokes the CLI directly and
references `claude-code-action` nowhere, so there is no actor check to
satisfy — and empirically its `Triage` and `AI review` checks both pass
on `claude[bot]`-authored PRs (#121, #131) while the two
`claude-code-action`-based workflows fail on the same PRs. Any workflow
in this repo that wraps `claude-code-action` inherits this gate; the CLI
does not.

Until that lands, a bot-authored PR gets `docs-check / docs-check` →
`failure` from the actor check rather than a real docs verdict. That is
still a strict improvement, because the required context reports at all and
the PR is no longer wedged pending-forever — but treat the red as "the
check could not run", not "the docs are wrong". The tell is in the job's
step list: `Run Claude Docs Check` fails and `Process Results` is
**skipped**, so no verdict was ever produced. (`Set Exit Code` then prints
"✅ Documentation check passed" against an empty `VERDICT`, which is
misleading — the job still fails on the earlier step.)

**The `claude` vs `claude[bot]` spelling does not matter.** The action's
`isAllowedBot` (`src/github/validation/actor.ts`) lowercases and strips a
trailing `[bot]` from _both_ the configured entries and the actor, so
`claude`, `claude[bot]`, and `Claude[bot]` are one entry. The error message
prints the stripped form purely for display; it is not a hint that the
suffixed form was compared and missed.

Note that `allowed_bots` matches on **name**, with no numeric-id
equivalent anywhere in the action's interface. Two properties keep that
acceptable, and one does not:

- The value compared is `github.actor`, which GitHub populates from the
authenticated triggering actor. Nothing in a PR's branch, diff, or title
can influence it.
- The list is consulted **only** for actors GitHub's Users API reports as
non-`User`. A human who registers the account `claude` never reaches the
allow-list, so the name grants them nothing.
- Residual, and unfixable within that interface: if the app were renamed,
or a different app were named `claude`, the name would match. The
action exposes no id-based equivalent.

That is why the gate this repo _does_ control uses the numeric id.

Fork PRs are still skipped (they have no access to secrets) and so are
still subject to the same never-reporting-context deadlock. No fork PR has
needed to merge here, but the durable fix is to drop
`docs-check / docs-check` from the ruleset in favour of a context that
always reports.

### Generate PR Title & Description

**File:** `generate-pr-title-description.yml`
Expand Down
50 changes: 46 additions & 4 deletions .github/workflows/claude-docs-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,58 @@ permissions: {}

jobs:
docs-check:
# Skip if PR is from a bot (e.g., dependabot) or a fork (no secrets available)
# Skip if the PR is from a fork (no secrets available) or from an
# unrelated bot such as Dependabot or Renovate.
#
# `docs-check / docs-check` is a REQUIRED status check in this repo's
# `main` ruleset. Because this job calls a reusable workflow, the
# required context only exists while the job runs: a skipped job
# reports under the bare name `docs-check` instead, so
# `docs-check / docs-check` never reports and the PR stays
# `mergeable_state: blocked` permanently. Any condition here that can
# skip the job on a same-repo PR is therefore a merge deadlock, not
# just a skipped check.
#
# Bot identity is matched on the immutable numeric account id, never on
# the login. `209825114` is the `claude[bot]` account — the Claude
# GitHub App (app id 1236702) that opens this repo's agent PRs, whose
# documentation this check is meant to validate. GitHub assigns that id
# at account creation and it survives renames, whereas a login is
# mutable and `contains(login, '[bot]')` tests a naming convention
# rather than an identity. `user.type` is GitHub's own account-kind
# field, so other bots keep being skipped without depending on the
# `[bot]` login suffix either.
#
# SECOND GATE, NOT FIXABLE HERE: clearing this condition lets the job
# start and report the required context, but the job then fails at
# `anthropics/claude-code-action`'s own actor check with "Workflow
# initiated by non-human actor: claude (type: Bot). Add bot to
# allowed_bots list". That action gates on its `allowed_bots` input,
# which defaults to "" (allow no bots) — and the reusable workflow
# called below neither declares an `allowed_bots` input nor forwards
# one to the action, so there is no value this file can pass. Fixing it
# requires `Uniswap/ai-toolkit` to plumb `allowed_bots` through
# `_claude-docs-check.yml`; then this caller passes `claude[bot]`.
# Until that lands, the check reports `failure` on bot PRs rather than
# a real docs verdict. That is strictly better than before — the
# required context reports at all, so the PR is no longer wedged
# pending-forever — but it is not yet a meaningful verdict.
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
!contains(github.event.pull_request.user.login, '[bot]') &&
github.event.pull_request.head.repo.full_name == github.repository)
github.event.pull_request.head.repo.full_name == github.repository &&
(github.event.pull_request.user.type != 'Bot' ||
github.event.pull_request.user.id == 209825114))
uses: Uniswap/ai-toolkit/.github/workflows/_claude-docs-check.yml@c4820d6e62a9488c831e722aac202733fafab5dd # main
with:
pr_number: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
suggestion_mode: ${{ github.event.inputs.suggestion_mode || 'suggest' }}
# `auto_commit` stays manual-dispatch-only. On a `pull_request` event
# `github.event.inputs` is empty, so this expression is always `false`
# there — which is why the docs check has never auto-committed anything
# on a PR. This workflow deliberately does NOT enable `auto_fix`: the
# check reports a verdict and leaves suggestions, and never writes to a
# contributor's branch.
auto_commit: ${{ github.event.inputs.auto_commit == 'true' }}
# Use sonnet for faster, cheaper checks
model: 'claude-sonnet-5'
Expand All @@ -77,4 +119,4 @@ jobs:
pull-requests: write
issues: read
actions: read
id-token: write
id-token: write
Loading