Skip to content

ci(content): sign the content bump so it stops publishing unverified commits - #2729

Merged
Hugo0 merged 2 commits into
mainfrom
ci/sign-content-bump
Aug 18, 2026
Merged

ci(content): sign the content bump so it stops publishing unverified commits#2729
Hugo0 merged 2 commits into
mainfrom
ci/sign-content-bump

Conversation

@Hugo0

@Hugo0 Hugo0 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Every Update content submodule to latest main commit since Feb 2026 has been unsigned, and they surface as a wall of "Unverified" whenever a maindev back-merge carries them across (most recently #2724).

The step built its commit with POST /git/commits, added in fab5971 under the title "use GitHub API for signed commits". That endpoint does not sign — it only attaches a signature the caller computes and passes in. The title has been wrong for six months.

Nothing caught it because required_signatures is active on the org All branches ruleset, but CONTENT_BOT_TOKEN belongs to an org admin and admins hold bypass_mode: always. So ref creation succeeded quietly instead of failing.

createCommitOnBranch — the one write path GitHub signs for you — is not an option here. Its FileAddition input carries path and contents only, with no file mode, so it cannot write a 160000 gitlink. Confirmed against the live schema. A real commit signed on the runner is the only route to a Verified submodule bump.

This PR: signs the bump with an SSH signing key (CONTENT_BOT_SIGNING_KEY), keeping the exact identity GitHub already stamped so the only change is that the commit is now signed. Then it asks GitHub whether the signature actually verified and fails the step if it did not — a missing key or a rejected signature now stops the publish instead of shipping another unverified commit.

Task

TASK-21002 — Prevent unverified commits from Hugo's account

Why base main, not dev

repository_dispatch runs workflows from the default branch, which is main. The fix has no effect until it is on main. The regular maindev back-merge carries it to dev afterwards.

Prerequisites already in place

  • CONTENT_BOT_SIGNING_KEY set on peanut-ui (2026-08-18).
  • Matching public key registered as a Signing Key on the Hugo0 account, fingerprint SHA256:U6N8EsIfBAZKtLGFT9CJkYOCiJwMMFUBdD85PFdaXlY. It is a signing key only — it is not in Authentication keys and grants no repo access.
  • h@hugomontenegro.com confirmed Primary + Verified on that account, which is what makes GitHub accept the signature.

Design notes / accepted trade-offs

  • The signing key lives in peanut-ui Actions secrets and carries Hugo's identity. Anything that can read those secrets can produce commits showing Verified · Hugo Montenegro. Accepted because CONTENT_BOT_TOKEN already sits in the same store with repo write and can push as him outright — the key adds no meaningful blast radius. A dedicated bot account is the stronger alternative and was deliberately deferred: it costs an org seat and a replacement PAT.
  • workflow_dispatch makes this workflow reachable from any ref, so anyone with write access can run a modified copy of it with these secrets. That means the signing key can be read, and commits can be signed as Hugo. Flagged rather than fixed, because it is pre-existing and strictly smaller than what is already exposed: CONTENT_BOT_TOKEN, an org-admin PAT, sits in the same secret store behind the same trigger. Worth addressing on its own (bot account, or drop workflow_dispatch), not by holding up this fix.
  • The verification gate is a hard failure, not a warning. Publishing an unverified commit is the bug this step exists to prevent, so a silent fallback would defeat the change.

Risk

Low, and CI-only — no app code, no runtime behavior. Worst case is that the content publish pipeline fails loudly and content stops auto-reaching peanut.me until fixed, which is the intended failure mode and strictly better than the current silent-unsigned one. No cross-repo deploy ordering.

QA

  • Signing mechanics proven locally with this exact key and the same command sequence: gpgsig -----BEGIN SSH SIGNATURE----- present, git verify-commit returns Good "git" signature, %G? = G.
  • YAML parses; the embedded shell passes bash -n; the BODY heredoc terminator sits at column 0 after YAML dedent.
  • prettier clean, 229/229 jest suites pass (unaffected — no TS touched).
  • Real proof lands post-merge: the next content publish must open a PR whose bump commit shows Verified. If the key or secret were wrong, the step now fails instead of publishing.

Screenshots: N/A (no visible change — CI workflow only).

Review

/code-review medium raised four findings; CodeRabbit raised none.

  • Fixed — push used the persisted GITHUB_TOKEN, not the PAT. A GITHUB_TOKEN push raises no workflow events, so Tests would run for pull_request only. content-publish-automerge.yml is written for two ci-success runs per commit and merges on all-green-of-N, so at N=1 one concurrency cancellation gives N=0 and the publish stalls silently. It also dropped the bypass ref creation had. Now pushes over CONTENT_BOT_TOKEN.
  • Fixed — a failed verify left an orphan branch. The gate ran after the push, and the superseded-PR sweep only walks branches that have an open PR, so an abandoned auto/update-content-* would never be collected. It is now deleted on that path.
  • Checked, no change needed — the push does not actually depend on a bypass today. The "All branches" ruleset carries only restrict-deletions, restrict-force-push and require-signed-commits, and the bump branch matches none of the PR or status-check rulesets, so a signed push passes on its own. The PAT is belt and braces.
  • Flagged, not fixed — the workflow_dispatch exposure above. Pre-existing, and smaller than the org-admin PAT already reachable the same way.

Summary by CodeRabbit

  • Chores
    • Improved the security and verification of automated content updates.
    • Content-update commits are now signed and verified before the workflow completes.
    • Failed verification now stops the process and removes the temporary branch.

…commits

Every content bump since Feb 2026 has been unsigned. The step built its
commit with `POST /git/commits`, added in fab5971 under the title "use
GitHub API for signed commits" — but that endpoint does not sign. It only
attaches a signature the caller computes and passes in.

Nothing caught it. `required_signatures` is active on the org "All branches"
ruleset, but CONTENT_BOT_TOKEN belongs to an org admin and admins hold
`bypass_mode: always`, so ref creation succeeded quietly for six months. The
commits then reach `dev` through the main->dev back-merge, which is where
they get noticed (#2724).

createCommitOnBranch, the one write path GitHub signs for you, is not an
option here: its FileAddition input carries `path` and `contents` only, with
no file mode, so it cannot write a 160000 gitlink. A real commit signed on
the runner is the only route to a Verified submodule bump.

Sign with an SSH signing key (CONTENT_BOT_SIGNING_KEY), keeping the identity
GitHub already stamped so the sole change is that the commit is now signed.
Then ask GitHub whether the signature verified and fail the step if it did
not — a missing key or a rejected signature now stops the publish instead of
shipping another unverified commit.
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 18, 2026 2:03pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c18f2786-def1-4d6b-ac6e-f7b190683360

📥 Commits

Reviewing files that changed from the base of the PR and between 3159f3b and 35914a6.

📒 Files selected for processing (1)
  • .github/workflows/update-content.yml

📝 Walkthrough

Walkthrough

The content update workflow now requires a signing key, creates signed commits locally, pushes them, and verifies their status through GitHub. The previous Git Data API commit and reference flow was removed.

Changes

Content update signing

Layer / File(s) Summary
Signing setup and local commit
.github/workflows/update-content.yml
The workflow validates CONTENT_BOT_SIGNING_KEY, configures SSH commit signing, creates the submodule update as a signed local commit, and pushes a new branch.
Pushed commit verification
.github/workflows/update-content.yml
The workflow queries GitHub for commit verification and deletes the branch when verification fails.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: signing content bump commits to prevent unverified commits from being published.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/sign-content-bump

Comment @coderabbitai help to get the list of available commands.

@Hugo0

Hugo0 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2934 ran, 0 failed, 0 skipped, 49.0s

📊 Coverage (unit)

metric %
statements 66.3%
branches 51.3%
functions 56.4%
lines 67.0%
⏱ 10 slowest test cases
time test
3.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.2s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.6s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/hooks/__tests__/useCrispTokenId.test.ts › retries then stays undefined when the endpoint keeps failing (no fallback token)
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/utils/__tests__/auth-token.test.ts › ignores the guarded marker and falls back to the plain token
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7095.78 → 7095.78 (0)
Findings: 0 net (+0 new, -0 resolved)

Review findings on the first commit.

The push inherited actions/checkout's persisted GITHUB_TOKEN, not the PAT the
old ref-creation call used. Two consequences. A GITHUB_TOKEN push raises no
workflow events, so `Tests` would fire for the `pull_request` event alone —
content-publish-automerge.yml is written for two `ci-success` runs per commit
and merges on all-green-of-N, so at N=1 one concurrency cancellation makes
N=0 and the publish stalls with no error. It also dropped the bypass the ref
creation had, leaving the pipeline exposed to any rule added to the "All
branches" ruleset later. Push over CONTENT_BOT_TOKEN instead.

The verify gate also ran after the push, so a rejected signature left an
`auto/update-content-*` branch with no PR. The superseded-PR sweep only walks
branches that have an open PR, so nothing would ever collect it. Delete the
branch on that path.

Checked and not changed: the "All branches" ruleset carries only restrict-
deletions, restrict-force-push and require-signed-commits, and the bump branch
matches none of the PR or status-check rulesets, so a signed push needs no
bypass today. The PAT is belt and braces.
@Hugo0
Hugo0 marked this pull request as ready for review August 18, 2026 14:03
@Hugo0
Hugo0 merged commit 042fc1f into main Aug 18, 2026
27 of 28 checks passed
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.

1 participant