actions: add signed-commit — publish a generated file as a GitHub-signed commit - #61
Merged
Merged
Conversation
Actions hands a job a token, not a signing key, so `git commit` on a runner produces an unsigned commit. Once a repo requires verified signatures, every PR such a workflow opens is blocked — front-desk-scheduler#27 had to be merged with a rule bypass. A commit created through the Contents API is constructed server-side, which is the precondition for GitHub signing it. The part worth extracting is not the API call, it is the CHECK. "Written through the API" does not by itself imply "signed": observed 2026-07-28, a Contents API write came back with no signature at all while GitHub's own squash-merges on the same repo were signed as GitHub <noreply@github.com>. Signing depends on the authenticating identity, not the endpoint. So the action reads .commit.verification.verified back and reports it, with an opt-in fail-if-unsigned for callers where an unsigned commit is worse than none. A composite action rather than a sibling reusable workflow, because the first consumer needs this mid-job — after the migration runs, using an App token from an earlier step — and a reusable workflow cannot be a step. It also keeps repo-standard's contents: read floor intact: this needs contents: write, which per that file's header would force every caller to over-grant (#55). Not a new repository yet. One consumer does not pay for a release cadence; this gets the derive-not-copy property now, and can move out when a second consumer wants its own versioning. Generalisations the front-desk-scheduler original did not need, each of which would otherwise be a latent bug in the second use: a 404 on the existing blob means create-a-new-file (the Contents API takes no `sha` then); unchanged content is skipped by comparing encoded bytes rather than trusting the API to no-op; and the base ref is resolved in its own statement, because folding it into the create-branch call reported a bad base as "branch already exists" — the opposite diagnosis. Untested against a real App installation token. mirror-migrate is the proving ground, and the action's own verification step is what will tell us. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UAAf9rQwNdpEcjjWMY9h6n
bdelanghe
marked this pull request as ready for review
July 28, 2026 16:37
This was referenced Jul 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extracts the generalisable half of bounded-systems/front-desk-scheduler#29 into
.github/actions/signed-commit.The problem it solves
Actions hands a job a token, not a signing key — there is nothing on a runner to sign with. So the usual
git config user.name/git commit/git pushin a bot workflow produces an unsigned commit. The moment a repo turns onCommits must have verified signatures, every PR that workflow opens is blocked. front-desk-scheduler#27 had to be merged with a rule bypass.A commit created through the Contents API is constructed server-side, which is the precondition for GitHub signing it. Same content, same branch, same PR — the difference is who assembles the commit object.
The part actually worth extracting
Not the API call — the check.
Observed 2026-07-28: a Contents API write came back
%G? = N, no signature at all, while GitHub's own squash-merges on the same repo were signed asGitHub <noreply@github.com>. Signing depends on the authenticating identity, not the endpoint. So the action reads.commit.verification.verifiedback and reports it via outputs, with an opt-infail-if-unsignedfor callers where an unsigned commit is worse than no commit.Any repo where a bot writes a generated artifact — schema projections, lockfiles, SBOMs, changelogs, docs — hits this the moment signatures are required.
Why a composite action, not a sibling reusable workflow
Two reasons, both from
repo-standard.yml's own documented constraints:contents: write. Per that file's header, GitHub validates a reusable workflow's permissions as the union of all its jobs at load time, so folding this intorepo-standardwould push everycontents: readcaller above the floor — that was repo-standard.yml @ #54 fails caller startup: scorecard/sbom actions not in the org Actions allowlist #55.Not a new repository yet. One consumer does not pay for a release cadence. This gets the derive-not-copy property now and can move out when a second consumer wants its own versioning.
Generalisations beyond the front-desk-scheduler original
Each would otherwise be a latent bug on second use:
shain that case. The original always assumed the file existed.basefailed the create and got reported as "branch already exists" — the opposite diagnosis.--head/--baseare explicit ongh pr create, since the action never checks the branch out.Status — not yet proven
Untested against a real GitHub App installation token. Signing depends on the authenticating identity, and the only identity I have observed is this session's, which produced an unsigned commit.
front-desk-scheduler'smirror-migrateis the proving ground; the action's own verification step is what will tell us. The README says this rather than implying the mechanism is settled.Deliberately no selftest workflow: unlike the reusable workflows, a composite action declares no permissions of its own, so there is no permission contract for a selftest to pin — and a live test would have to create and delete branches on this repo.
Follow-up (not here): rewire
mirror-migrate.ymltouses:this action once it has a SHA onmain.Generated by Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.