Skip to content

pr-signoff: Add reusable workflow for /signoff PR commands - #2

Draft
cgwalters-bot wants to merge 1 commit into
mainfrom
bot/pr-signoff
Draft

cgwalters-bot wants to merge 1 commit into
mainfrom
bot/pr-signoff

Conversation

@cgwalters-bot

@cgwalters-bot cgwalters-bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Adds pr-signoff, a reusable workflow for two PR comment commands. With it, each repository only needs a small stub: cgwalters-forge/infra#8 syncs that stub org-wide, and cgwalters-forge/bootc#16 adds it to bootc first. This supersedes bootc-dev/infra#261. It is plain Actions with no LLM, runs on ubuntu-24.04, and handles all of a PR's commits.

/signoff [<sha>] adds the commenter's Signed-off-by to each PR commit that lacks it.

  • Each commit is recreated with git hash-object in a bare repository.
  • Tree, parent base, author line and message stay byte for byte (including non-UTF-8 encodings and trailing whitespace). Only the trailer and the committer change.
  • dco-2 accepts a sign-off that matches the committer, and the commenter becomes the committer.
  • Needs triage, or write on fork PRs.

/rebase-signoff [<sha>] does the same onto the target tip, using git merge-tree (no worktree). Commits that become empty are dropped, and a conflict is refused. Needs write.

Which head gets signed off. Only a head that GitHub's activity log shows was pushed before the comment. A SHA (12+ hex digits) must also match it. The push is --force-with-lease against that head.

Refused, with a reply:

  • fork PRs without "Allow edits by maintainers";
  • merge commits;
  • heads that are the default branch, protected, or under a non_fast_forward/update rule.

Ignored silently: bots, and people without read access.

Security.

  • Nothing from the PR is checked out or run. Git has hooks off, system/global config ignored, * merge=text -filter in info/attributes, https only, and no submodules.
  • Untrusted text only reaches the script through env:.
  • The App token is scoped to one repository.
  • The script is checked out from the workflow's own commit (job.workflow_sha). Callers pin that commit, since they pass the App key.
  • create-github-app-token and checkout are SHA-pinned.

Runner cost. GitHub can't filter issue_comment on the comment body, so every comment creates a run. The stub's job-level if: skips the job before a runner is requested, so other comments cost nothing.

Existing tools. Per bootc-dev/infra#260 I looked for existing ones before writing this:

  • DCO checkers (cncf/dcochecker, tisonkun/actions-dco, KineticCafe/actions-dco) only report.
  • dco-2's third-party remediation commits (.github/dco.yml) avoid a force-push, but someone still has to write and push a remediation commit per PR, and it leaves extra commits.
  • peter-evans/slash-command-dispatch only adds a repository_dispatch hop (and needs a PAT) where a job-level if: suffices.
  • I found no maintained comment-triggered sign-off action; the closest, GeorgLink/test_dco_signoff_action, is an abandoned 2019 experiment.

The command matching and role logic follow gh-aw's slash_command and check_membership.

Testing

pr-signoff/test.sh has offline, data-driven tests and runs in this repo's PR CI:

  • command parsing, role mapping and ranking, SHA length, and required roles;
  • the rewrite in a scratch repo: byte-exact messages including Latin-1 and trailing whitespace, trees, authors, idempotence, rebase drop/keep-empty, and conflicts.

They pass on a 4-core devspace (cgwalters-devspace-36074104378, git 2.52). Mutating the rank table, the SHA length, the fork rule, addIfDifferent or the encoding copy makes them fail. shellcheck is clean. actionlint 1.7.12 is clean apart from not knowing the job.workflow_* contexts (-ignore for those two).

Real runs on cgwalters-forge/infra, with the stub pinned to this commit and the GITHUB_TOKEN fallback:

  • A plain comment produced a skipped run, with no runner.
  • /signoff without a SHA on 2 unsigned commits worked (run). One commit's message has trailing whitespace and the other is Latin-1 with an encoding header. Afterwards the trees and author lines were identical, each message began with the original bytes followed by the trailer, the encoding header was kept, and the commenter was the committer.
  • /signoff <12-hex head> on the result replied "nothing to do" (run).
  • /rebase-signoff <12-hex head> after the base moved put both commits on the new tip, each with one trailer (run).
  • These were refused:
    • a 7-hex SHA, with the usage message (run);
    • a stale SHA (run);
    • a head branch with a non_fast_forward ruleset (run);
    • a head that is the default branch (run);
    • a fork PR without maintainer edits (run);
    • a fork PR with maintainer edits, which got as far as needing an App token (run).
  • Reactions were 👀 then 🚀 or 😕 on every command.

Credentials: in bootc-dev, the bootc-bot App's GH_AW_APP_CLIENT_ID/GH_AW_APP_PRIVATE_KEY are installed org-wide. For composefs this is unconfirmed: it needs that org variable and secret set, or its stubs fall back to GITHUB_TOKEN.

Untested and required before relying on this: the App-token path. That covers minting with permission-workflows, pushing commits that touch .github/workflows, and pushing to fork PRs. The forge has no App, so this must get one real run on an org with the bootc-bot App. Also untested:

  • a commenter with triage only, or without access (the bot is admin wherever it can test);
  • a bot commenter;
  • dco-2 turning green (it isn't installed on the forge).

That an App push to a fork PR skips the "approve and run" gate for fork CI is an assumption I couldn't check. It is why fork PRs require write.

Generated-by: https://github.com/cgwalters/#llms


Review draft in cgwalters-forge, not upstream yet. This section is removed when the PR is opened upstream.

  • Upstream: bootc-dev/actions, base main
  • Board item: PVTI_lADOE9oHIs4BlJLczg9kERg
  • Fork CI: off; the devspace testing described above is this PR's CI, and upstream CI runs once it is opened there

To review:

  • Approve, or comment /promote on a line of its own, to open it upstream, ready for review. Either covers only the commits pushed so far.
  • If upstream requires DCO, approving also signs off: promote adds Signed-off-by: Colin Walters <walters@verbum.org> to the commits lacking it (the bot's and yours; anyone else's only if you ask), with you as committer.
  • Add a /draft line (in the same comment or before) to open it upstream as a draft (/ready undoes that).
  • Close to drop it.
  • Edit the title and description freely: they become the upstream PR's. Review comments are squashed into the commits they concern, with a reply here.

DCO is a required check across bootc-dev, and PRs from contributors who
forgot to sign off (or from the bot, which never does) stay blocked
until someone rewrites them by hand. This lets a maintainer comment
`/signoff` to add their own Signed-off-by to every commit of the PR,
keeping the commits otherwise byte for byte as they are, or
`/rebase-signoff` to also move them onto the target branch.

It lives here as a reusable workflow so every repository, in any org,
only needs a small stub that filters comments with a job-level `if:` (so
other comments never get a runner) and passes the App credentials. It is
plain Actions with no LLM; the command matching and role check follow
gh-aw's slash-command semantics. The logic is a script run from the
workflow's own commit, so it can have offline tests.

Since it force-pushes PR branches, including forks, with an App token,
it never checks out or runs anything from the PR (commits are recreated
with git plumbing in a bare repository), signs off only a head GitHub
recorded being pushed before the comment (and the one named, if a SHA is
given), and pushes with --force-with-lease against that head.

Related: bootc-dev/infra#260
Generated-by: AI
@cgwalters-bot cgwalters-bot moved this to Draft in Workstream Sep 30, 2026
@cgwalters-bot cgwalters-bot moved this from Draft to In Progress in Workstream Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant