Skip to content

actions: add signed-commit — publish a generated file as a GitHub-signed commit - #61

Merged
bdelanghe merged 1 commit into
mainfrom
claude/front-desk-accessibility-ao9466
Jul 28, 2026
Merged

actions: add signed-commit — publish a generated file as a GitHub-signed commit#61
bdelanghe merged 1 commit into
mainfrom
claude/front-desk-accessibility-ao9466

Conversation

@bdelanghe

@bdelanghe bdelanghe commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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 push in a bot workflow produces an unsigned commit. The moment a repo turns on Commits 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.

"Written through the API" does not by itself imply "signed."

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 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 via outputs, with an opt-in fail-if-unsigned for 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:

  1. The first consumer needs this mid-job — after the migration runs, using an App token minted by an earlier step. A reusable workflow cannot be a step.
  2. It needs 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 into repo-standard would push every contents: read caller 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:

  • New files work — a 404 on the existing blob means create, and the Contents API takes no sha in that case. The original always assumed the file existed.
  • Unchanged content is skipped explicitly, by comparing encoded bytes against the blob on the branch, rather than trusting the API to no-op. Whether a PUT with identical content creates an empty commit is not behaviour worth assuming, and an unexplained empty commit is exactly what nobody investigates later.
  • The base ref is resolved in its own statement. Folding it into the create-branch call meant a bad base failed the create and got reported as "branch already exists" — the opposite diagnosis.
  • --head/--base are explicit on gh 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's mirror-migrate is 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.yml to uses: this action once it has a SHA on main.


Generated by Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

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
bdelanghe marked this pull request as ready for review July 28, 2026 16:37
@bdelanghe
bdelanghe merged commit fa35ba8 into main Jul 28, 2026
11 checks passed
@bounded-systems-front-desk bounded-systems-front-desk Bot moved this from Todo to Done in Front Desk Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants