Skip to content

fix(standing-pr): validate the manifest against source before publishing - #587

Merged
goosewobbler merged 1 commit into
mainfrom
fix/standing-pr-publish-hardening
Jul 20, 2026
Merged

goosewobbler merged 1 commit into
mainfrom
fix/standing-pr-publish-hardening

Conversation

@goosewobbler

@goosewobbler goosewobbler commented Jul 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

Hardens the standing-PR publish path, which previously trusted the <!-- releasekit-manifest --> comment verbatim — a repo write-access actor could edit the comment's hidden base64 to forge versionOutput and drive arbitrary git tags and registry publishes under the project's identity (an insider/confused-deputy supply-chain path). The draft flow already recomputed-and-compared; standing-pr did not.

New trust model — what is now recomputed / validated / authenticated

Before publishFromManifest creates any tag or touches any registry:

Before: createReleaseTags/runPublishStep ran on manifest.versionOutput as-is; the only guards were who merged, ancestry (warn-only), and override-label staleness — never the version/tag/target content, and no schema or author check. After: the package set, versions and tags are re-derived from and cross-checked against the merged source, the manifest is schema-validated, and the manifest comment must come from the bot.

DoS hardening (#558)

Release-notes length was unbounded and the body cap never shed the notes region, so inflated notes could push the PR body past GitHub's limit and 422 every standing-PR update. Now per-package notes are truncated at a documented bound (…(truncated)), and the notes region is part of the body-cap shed order — an over-long body collapses/drops notes instead of failing the update.

Test plan

Closes #556
Closes #558

🤖 Generated with Claude Code

https://claude.ai/code/session_01BT6fVBZrmExXZj7vQ29881

Greptile Summary

This PR hardens the standing-PR publish path against insider/confused-deputy supply-chain attacks where a write-access actor could edit the hidden base64 in the release manifest comment to drive arbitrary git tags and registry publishes. It also caps per-package release notes to prevent inflated notes from wedging the standing PR via GitHub's 65 536-char body limit.

  • Manifest integrity (three layers): assertManifestMatchesSource recomputes on-disk package versions and validates every tag in the manifest against them before any publish operation; parseManifest now schema-validates via Zod and requires an exact known schemaVersion (closing the undefined bypass in the old range); and findComment/upsertMarkerComment now prefer bot-authored matches and refuse to adopt human-authored marker comments.
  • Author-binding: publishFromManifest hard-rejects a manifest comment not authored by the bot; fetchStandingPRSnapshot applies the same check so a forged manifest isn't surfaced in the preview either.
  • DoS hardening (Unbounded release-notes length wedges standing-PR updates (body-cap DoS) #558): truncatePackageNotes caps each package's notes at 8 000 chars; the progressive body-shedding logic collapses/drops the notes region when even a fully-shed changelog still overflows.

Confidence Score: 5/5

Safe to merge — the validation guards are well-structured and every new code path has a dedicated test.

All three hardening layers are correctly implemented. The tag-boundary regex blocks digit-suffix and hyphen/plus-suffix version-embedding bypasses. The schema version membership check closes the undefined-bypass. No correctness issues found.

No files require special attention.

Important Files Changed

Filename Overview
packages/release/src/standing-pr/standing-pr.ts Core publish hardening: schema validation, author-binding, baseSha ancestry guard upgraded to hard error, and assertManifestMatchesSource added. Tag validation regex correctly uses (?![\d.+-]) to block version-embedding and -suffix bypasses.
packages/forge/src/types.ts Adds CommentAuthor interface and isBotComment helper; fail-open on absent user is intentional and documented for legacy/fake paths.
packages/forge/src/github.ts findComment now prefers bot-authored matches over human ones; upsertMarkerComment only adopts bot-authored existing comments.
packages/version/src/workspace.ts New getWorkspacePackageVersions reads on-disk versions across all ecosystems; silently skips packages without name/version (safe).
packages/release/src/standing-pr/notes-region.ts Adds truncatePackageNotes at 8000 chars/package with correct budget accounting for the marker string.
packages/release/test/unit/standing-pr.spec.ts Comprehensive new tests covering all attack paths: forged version, extra package, rogue tag, version-embedding tag, scoped-package mismatch, non-bot manifest, baseSha non-ancestor, and absent schemaVersion.

Comments Outside Diff (1)

  1. packages/release/src/standing-pr/standing-pr.ts, line 607-624 (link)

    P2 fetchStandingPRSnapshot does not author-check the manifest comment

    findManifestComment now returns a human-authored comment as a fallback when no bot-authored one exists. fetchStandingPRSnapshot parses that comment without calling isBotComment, so a pre-seeded forged manifest would be surfaced in the preview. This is a display-only path (it feeds preview, not publishFromManifest), so the impact is showing misleading version information rather than triggering a publish — but it is an inconsistency with the rest of the hardening.

    Fix in Claude Code Fix in Cursor

Reviews (7): Last reviewed commit: "fix(standing-pr): validate the manifest ..." | Re-trigger Greptile

@goosewobbler goosewobbler added this to the 0.42.0 milestone Jul 19, 2026
@github-actions

github-actions Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Standing release PR: #582 · v0.41.1 queued (4 packages) · open 3h 2m · ✅ ready to merge

Release Preview — this PR contributes no changes

No bump label detected.
Note: Add bump:patch, bump:minor, or bump:major to trigger a release.

Currently queued for release

Package Current Next
monorepo 0.41.0 0.41.1

Updated automatically by ReleaseKit

Comment thread packages/release/src/standing-pr/standing-pr.ts Outdated
@goosewobbler
goosewobbler force-pushed the fix/standing-pr-publish-hardening branch from 3bdd5f3 to ac9c026 Compare July 19, 2026 22:36
Comment thread packages/release/src/standing-pr/standing-pr.ts
@goosewobbler
goosewobbler force-pushed the fix/standing-pr-publish-hardening branch 4 times, most recently from be37871 to 88ac24c Compare July 19, 2026 23:51
The standing-PR publish path trusted the release manifest comment verbatim: a
write-access actor could edit the comment's base64 to forge `versionOutput` and
drive arbitrary git tags and registry publishes under the project's identity
(the draft flow already recomputed; standing-pr did not). Harden the publish
boundary with three layers, and cap release-notes length so an inflated body
can't wedge standing-PR updates.

#556 — before creating any tag or touching a registry, `publishFromManifest` now:
- Recompute-and-compare: reads each package's actual version from the merged
  commit (the reviewed-and-merged source of truth) and refuses a manifest that
  names an unknown package, a version that doesn't match the source, or a
  consumer tag encoding no published version; the baseSha ancestry check is
  upgraded from a warning to a hard refusal.
- Schema-validate: parseManifest runs the decoded payload through a Zod schema,
  requires an exact schemaVersion (an absent version no longer slips through),
  and rejects oversized/malformed manifests.
- Author-bind: ForgeComment surfaces the comment author; findComment prefers a
  bot-authored match, upsertMarkerComment only adopts a bot-authored comment,
  and the publish refuses a human-authored manifest comment.

#558 — per-package release notes are truncated at a documented bound with a
"…(truncated)" marker, and the notes region joins the standing-PR body-cap shed
order so an over-long body degrades gracefully instead of hitting GitHub's 422.

Closes #556
Closes #558

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BT6fVBZrmExXZj7vQ29881
@goosewobbler
goosewobbler force-pushed the fix/standing-pr-publish-hardening branch from 88ac24c to 1e0645d Compare July 20, 2026 00:09
@goosewobbler

Copy link
Copy Markdown
Owner Author

@greptileai review plz

@goosewobbler
goosewobbler merged commit ff2bb72 into main Jul 20, 2026
30 of 31 checks passed
@goosewobbler
goosewobbler deleted the fix/standing-pr-publish-hardening branch July 20, 2026 08:02
@github-actions github-actions Bot mentioned this pull request Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant