Skip to content

Own the CAA gate: a reusable workflow and a Go program in this repository - #2

Draft
gafferongames wants to merge 1 commit into
mainfrom
caa-reusable-workflow
Draft

Own the CAA gate: a reusable workflow and a Go program in this repository#2
gafferongames wants to merge 1 commit into
mainfrom
caa-reusable-workflow

Conversation

@gafferongames

@gafferongames gafferongames commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Replaces the archived third-party CAA action with a reusable workflow and a Go
program this organization owns.

The gate ran on contributor-assistant/github-action@v2.6.1. That repository is
archived, v2.6.1 (2024-09-26) is its last release, and it holds a personal
access token with write on every library repository. This is the owner half of
that: same behavior, code we own, and a credential that reaches one repository.

What lands

  • .github/workflows/caa.yml, on: workflow_call. Every library repository
    calls it from its own cla.yml. All inputs have defaults, so a caller passes
    only the secret.
  • tools/caa, the logic, run as go run ./tools/caa. Go 1.26 via setup-go,
    pinned by SHA, as is checkout. No dependencies outside the standard library.
  • .github/workflows/ci.yml, running gofmt, go vet and go test ./....
  • A README that says where the ledger is and how to record a signature by hand.

Behavior

Event Condition What the run does
pull_request_target author allowlisted, or a bot status success
pull_request_target author in the ledger status success
pull_request_target author not in the ledger posts the ask to sign, once per pull request; status failure
issue_comment on a pull request body is the sentence, from the pull request's author, not yet in the ledger appends the signature; status success
issue_comment on a pull request body is the sentence, from anyone else records nothing; status failure
issue_comment on a pull request body is the sentence, author already in the ledger records nothing; status success
issue_comment on a pull request any other comment records nothing; restates the status from the ledger
issue_comment on a plain issue any nothing at all; the job does not start

A signature is the whole comment body, ignoring surrounding whitespace and
capitalization. A body carrying any other text is not a signature, which is what
the old action did too.

Three things worth reading before you approve.

  1. The gate name moves from an Actions check to a commit status. The
    organization ruleset Require CAA signature requires a status check named
    caa on the default branch of eleven repositories: netcode, reliable,
    serialize, yojimbo, fixed3d, netcode.go, netcode.rs,
    reliable.go, reliable.rs, serialize.go and serialize.rs. That name
    is satisfied today by the Actions job check. A reusable workflow names its
    check after the caller, so that check becomes caa / caa. The program
    therefore sets a commit status named caa on every run, whatever the
    verdict. A required check name matches a commit status as readily as a check
    run, so the ruleset needs no change and must not be touched. schema is not
    in that ruleset, which is why it is the right first caller: if the status
    does not appear as expected, nothing is blocked while we find out.
  2. The gate is on the pull request's author, not on every commit author. The
    old action listed every commit author on the pull request. This is the
    narrower rule you specified. A pull request carrying a commit authored by
    somebody else passes on the author's signature alone.
  3. Any comment on a pull request re-evaluates it. That replaces the
    recheck magic word: after a signature is recorded by hand, any comment
    turns the status green. Nothing else re-evaluates an open pull request.

The credential

One fine-grained personal access token, created once:

  • Resource owner: mas-bandwidth
  • Repository access: only select repositories, mas-bandwidth/.github
  • Repository permissions: Contents: Read and write. Nothing else. Not
    metadata beyond the mandatory read, not pull requests, not issues, not
    actions.
  • Organization permissions: none
  • Expiry: 1 year, calendar reminder to rotate

Store it once, as an organization secret named CAA_LEDGER_TOKEN, with
repository access granted to the calling repositories. No per-repository secret
is created. The caller passes it through explicitly rather than with
secrets: inherit, so the reusable workflow sees that one secret and no other.

Everything else uses the calling repository's GITHUB_TOKEN: pull-requests: write to post the ask, statuses: write to set the gate, contents: read for
checkout to fetch this repository's tools/caa at the called workflow's own
commit. The pull request's head is never checked out, and no value out of an
event payload reaches a command line.

If an organization-installed GitHub App with contents write on this repository
alone is easier for you to manage than a PAT, it fits the same slot: the
workflow only needs a token in CAA_LEDGER_TOKEN that can write
signatures/caa.json.

Rollout

  1. Create the PAT and set the organization secret CAA_LEDGER_TOKEN, granting
    it to schema and to the eleven repositories in the ruleset above.
  2. Merge this pull request. Note its merge commit SHA.
  3. Merge Call the organization's own CAA workflow schema#500, the caller, after repointing its uses: line
    to that SHA. Watch one pull request through the gate.
  4. Move the eleven ruleset repositories the same way, yojimbo first, one at a
    time, confirming the caa status appears on a pull request in each before
    moving to the next.
  5. After the last caller moves, revoke CLA_SIGNATURES_TOKEN and delete the
    secret from every repository and from the organization.

Not in this pass

  • cla.yml in the other eleven repositories: netcode, reliable,
    serialize, yojimbo, fixed3d, netcode.go, netcode.rs, reliable.go,
    reliable.rs, serialize.go and serialize.rs. All eleven carry the same
    file, byte for byte, and differ from schema's only in that schema pins the
    archived action by SHA. Each is a one file follow-up once schema is proven.
  • Gating on every commit author rather than the pull request's author.
  • Recording a signature posted on an issue. It is still recorded by hand, and
    the README says how.

Testing

go test ./... covers ledger parsing, a byte for byte round trip of the real
ledger so existing signatures stay valid, the exact sentence match, the author
check, the idempotent append, and the negative control that the sentence from
another user signs nothing. Each of those was confirmed to go red when the rule
it guards is removed. The program was also run end to end against a stub API for
all four event shapes, confirming the two tokens never cross.

🤖 Generated with Claude Code

…tory

The gate ran on contributor-assistant/github-action, a third-party action whose
repository is archived at v2.6.1. This replaces it with an organization-owned
reusable workflow that every library repository calls.

.github/workflows/caa.yml holds a pull request until its author appears in the
signature ledger, asks an unsigned author to sign, records the signature when
the author posts the exact sentence, and sets a commit status named caa on
every run so branch protection has one whatever the verdict. A reusable
workflow necessarily names its Actions check after the caller's job, so the
name branch protection watches is now the commit status rather than the check.

tools/caa carries the logic. The ledger keeps the shape it already has, so
every signature recorded before this still counts, and a round trip through the
program is byte for byte the file it read. Two tokens, never crossed: the
ledger write uses CAA_LEDGER_TOKEN, which needs contents read and write on this
repository and nothing else; every other call uses the calling repository's
GITHUB_TOKEN with pull-requests write, statuses write, and contents read. The
pull request's head is never checked out, and no value from an event payload
reaches a command line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants