Skip to content

fix(release): support verified squash publication - #33

Merged
myagentdojo merged 3 commits into
mainfrom
codex/release-please-single-parent
Aug 9, 2026
Merged

fix(release): support verified squash publication#33
myagentdojo merged 3 commits into
mainfrom
codex/release-please-single-parent

Conversation

@myagentdojo

@myagentdojo myagentdojo commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

Release publication now supports GitHub's squash path without weakening the existing provenance and immutable-release checks. Ordinary pull requests and Release Please pull requests can stay squashable; repositories no longer need merge-commit-only history to publish safely.

This replaces the one-off recovery used in #31 with the durable release contract. PR #32 remains blocked until this change is merged and the new path is qualified on its exact head.

Design decisions

  • Admit exactly one-parent squash candidates or two-parent merge candidates. Reject rebases and all other topology shapes.
  • Bind the candidate to the push's original base, the merged pull request's base and reviewed head, the GitHub merge result, the exact reviewed changed-path set, and every reviewed blob.
  • Replay current admission immediately before tag or GitHub Release mutation. Historical repair still executes the historical projection policy, then uses current identity and topology validation.
  • Keep the persisted publication record at its existing nine-field shape. This does not introduce a general merge or release framework.
  • Make repository readiness require squash merging plus a no-bypass default-branch ruleset that requires pull requests and blocks force pushes.

Validation

  • bun test: 563 passed, 3 explicitly manual/hosted cases skipped, 0 failed.
  • bun run prove:all: passed from clean commit 99af6ad, including native Claude/Codex install mechanics, runtime custody, deterministic packaging, offline execution, distribution checksums, and DX proof.
  • bun run generate:check, bun run release:validate -- --json, actionlint .github/workflows/release.yml, and git diff --check: passed.
  • Independent correctness, security, YAGNI, and cross-model adversarial review converged with no concerning P0, P1, or P2 findings after the exact changed-path proof was added.

Post-Deploy Monitoring & Validation

  • Owner: Nathan / release maintainer.
  • Window: from this PR's merge through PR chore(main): release 0.2.0 #32 exact-head qualification and the v0.2.0 release.
  • Healthy signal: maintenance updates PR chore(main): release 0.2.0 #32 in place; its exact head passes required checks and Codex review; publication admits one candidate; the tag, Release target, assets, and nine-field record all bind to the same SHA.
  • Failure signal: any identity, topology, projection, replay, ruleset-readiness, proof, or immutable-release check fails. Stop publication and do not merge PR chore(main): release 0.2.0 #32 until the failing authority is understood.

Related: #32

Summary by CodeRabbit

  • Release Process

    • Strengthened validation for one- and two-parent release candidates, including ancestry, reviewed changes, and commit topology.
    • Added safeguards for immutable release targets, trusted bases, publication replay, and unsafe workflow events.
    • Improved repair validation and fail-closed handling for invalid repository configuration.
    • Added checks ensuring published files match reviewed changes.
  • Documentation

    • Updated release setup and publication guidance for supported candidate types and safeguards.
  • Tests

    • Expanded coverage for topology, provenance, projection integrity, dispatch, repair, and repository readiness.

@myagentdojo
myagentdojo temporarily deployed to hosted-canary-qualification August 9, 2026 10:06 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1286ba0f-cf20-4b06-aede-d611f0a94897

📥 Commits

Reviewing files that changed from the base of the PR and between 36fc067 and 4f653a8.

📒 Files selected for processing (3)
  • scripts/release-projection.test.ts
  • scripts/repository-readiness.test.ts
  • scripts/repository-readiness.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • scripts/release-projection.test.ts
  • scripts/repository-readiness.test.ts
  • scripts/repository-readiness.ts

📝 Walkthrough

Walkthrough

Release admission supports verified one-parent and two-parent candidates. It binds reviewed blobs, commit topology, workflow provenance, projection policy, immutable release targets, and repository readiness safeguards.

Changes

Release admission and repository safeguards

Layer / File(s) Summary
Reviewed projection binding
scripts/release-projection.ts, scripts/release-projection.test.ts
Projection validation compares Git-derived changed files and reviewed head blob SHAs with the candidate projection.
Candidate topology admission
scripts/release-validate.ts, scripts/release-validate.test.ts, docs/adr/0003-reviewed-versioned-releases.md
Admission and repair validation use parent SHAs, trusted bases, merged PR bases, reviewed heads, and historical projection rules.
Workflow provenance and replay
.github/workflows/release.yml, scripts/release-validate.test.ts
The workflow persists topology metadata, validates push ancestry, replays admission before mutation, and verifies immutable release targets.
Repository readiness safeguards
scripts/repository-readiness.ts, scripts/repository-readiness.test.ts, README.md
Readiness checks require squash merging and no-bypass default-branch protection with pull-request and force-push safeguards.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubWorkflow
  participant ReleaseValidation
  participant Git
  participant ReleaseTargets
  GitHubWorkflow->>Git: resolve candidate parents, changed files, and blob SHAs
  GitHubWorkflow->>ReleaseValidation: validate topology, provenance, and projection
  ReleaseValidation-->>GitHubWorkflow: return admission result and projection digest
  GitHubWorkflow->>ReleaseValidation: replay admission before mutation
  ReleaseValidation-->>GitHubWorkflow: return proven candidate checkout
  GitHubWorkflow->>ReleaseTargets: verify immutable tag and release target
  ReleaseTargets-->>GitHubWorkflow: return target SHA confirmation
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: verified support for squash-based release publication.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/release-please-single-parent

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
scripts/release-projection.ts (1)

160-171: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Disable Git path quoting when you enumerate changed files.

git diff --name-only quotes paths that contain non-ASCII or special bytes when core.quotePath is enabled, which is the default. A quoted path never equals the GitHub-reported filename, so the comparison at Line 92 fails with a misleading message instead of the real cause. Pass -c core.quotePath=false, or use -z and split on NUL.

♻️ Proposed change
 function gitChangedFiles(base: string, head: string): string[] {
 	const result = Bun.spawnSync({
-		cmd: ["git", "diff", "--name-only", base, head, "--"],
+		cmd: ["git", "-c", "core.quotePath=false", "diff", "-z", "--name-only", base, head, "--"],
 		cwd: process.cwd(),
 		stdout: "pipe",
 		stderr: "pipe",
 	})
 	if (result.exitCode !== 0) {
 		throw new Error(`cannot resolve candidate changed files: ${result.stderr.toString().trim()}`)
 	}
-	return result.stdout.toString().split("\n").filter(Boolean)
+	return result.stdout.toString().split("\0").filter(Boolean)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/release-projection.ts` around lines 160 - 171, Update gitChangedFiles
to disable Git path quoting when invoking git diff, preferably by adding -c
core.quotePath=false before the diff arguments, so returned filenames match
GitHub-reported paths exactly.
scripts/repository-readiness.test.ts (1)

101-152: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add cases for the remaining matchesDefaultBranch patterns.

matchesDefaultBranch accepts four include patterns: ~ALL, ~DEFAULT_BRANCH, the bare branch name, and refs/heads/<branch>. These tests exercise only ~DEFAULT_BRANCH. A regression that drops refs/heads/main from the accepted list would pass.

Two more gaps: a non-empty exclude must fail closed, and an empty-string defaultBranch must return unavailable (line 225 of scripts/repository-readiness.ts).

💚 Suggested additional cases
+	test.each([
+		["~ALL", ["~ALL"]],
+		["bare branch name", ["main"]],
+		["fully qualified ref", ["refs/heads/main"]],
+	] as const)("accepts a ruleset that targets the default branch by %s", (_condition, include) => {
+		expect(
+			classifyDirectPushProtection(
+				[reviewedDefaultBranchRuleset({ conditions: { ref_name: { include, exclude: [] } } })],
+				"main",
+			),
+		).toMatchObject({ status: "ready" })
+	})
+
+	test.each([
+		[
+			"exclusion",
+			[
+				reviewedDefaultBranchRuleset({
+					conditions: { ref_name: { include: ["~DEFAULT_BRANCH"], exclude: ["refs/heads/main"] } },
+				}),
+			],
+			"main",
+		],
+		["empty default branch", [reviewedDefaultBranchRuleset()], ""],
+	] as const)("fails closed for %s", (_condition, rulesets, defaultBranch) => {
+		expect(classifyDirectPushProtection(rulesets, defaultBranch)).toMatchObject({
+			status: "unavailable",
+		})
+	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/repository-readiness.test.ts` around lines 101 - 152, Add test cases
in the “default branch direct-push protection” suite for each remaining accepted
matchesDefaultBranch include pattern: ~ALL, the bare branch name, and
refs/heads/<branch>. Also verify a non-empty conditions.ref_name.exclude fails
closed with status unavailable, and that passing an empty defaultBranch to
classifyDirectPushProtection returns unavailable.
scripts/repository-readiness.ts (3)

312-332: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: the merge-commits check name no longer matches its meaning.

The check now proves squash-merge availability. The name merge-commits and the constant mergeCommitRepair still describe merge commits. Renaming both to squash-merge would align the readiness output with the new contract. The rename touches scripts/repository-readiness.test.ts and the readiness output documented in README.md, so defer it if the output string is treated as a stable contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/repository-readiness.ts` around lines 312 - 332, Optionally rename
the merge-method readiness symbols from mergeCommitRepair and the
"merge-commits" check name to squash-merge equivalents so they accurately
represent squash-merge availability. Update all references in
scripts/repository-readiness.test.ts and the documented readiness output in
README.md; defer the rename if the existing output string is a stable contract.

233-241: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider skipping non-branch rulesets instead of failing closed.

Line 234 returns unavailable for any element whose target is not "branch". The production caller filters branch summaries first, so this path is unreachable today. Any future caller that passes a mixed ruleset list would report unavailable for a correctly protected repository. classifyTagRuleset uses continue for the same condition.

Separating "not a record" (unreadable, fail closed) from "different target" (skip) keeps the two classifiers consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/repository-readiness.ts` around lines 233 - 241, Update the ruleset
iteration in the direct-push classifier to continue past records whose target is
not "branch", matching classifyTagRuleset. Keep the unavailable fail-closed
result for values that are not records, and continue evaluating remaining branch
rulesets.

1050-1062: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: extract the repository payload once.

The guard repositoryResponse.ok && isRecord(repositoryResponse.data) is repeated on lines 1050-1053 and 1058-1060.

♻️ Extract `repositoryData`
-	const squashMergeEnabled =
-		repositoryResponse.ok && isRecord(repositoryResponse.data)
-			? repositoryResponse.data.allow_squash_merge
-			: undefined
+	const repositoryData =
+		repositoryResponse.ok && isRecord(repositoryResponse.data) ? repositoryResponse.data : undefined
 	const checks: ReadinessCheck[] = [
 		checkTagRuleset(repository),
-		checkDirectPushProtection(
-			repository,
-			repositoryResponse.ok && isRecord(repositoryResponse.data)
-				? repositoryResponse.data.default_branch
-				: undefined,
-		),
-		checkMergeHistoryPolicy(repository, squashMergeEnabled),
+		checkDirectPushProtection(repository, repositoryData?.default_branch),
+		checkMergeHistoryPolicy(repository, repositoryData?.allow_squash_merge),
 	]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/repository-readiness.ts` around lines 1050 - 1062, In the
readiness-check setup, extract the guarded repository payload into a single
repositoryData value before defining squashMergeEnabled and checks. Reuse
repositoryData for both allow_squash_merge and default_branch, preserving
undefined when the response is unsuccessful or not a record.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 386: Update the README guidance in the “Protect main” step so the
required checks are configured through classic branch protection, matching the
data source used by bun run readiness; alternatively, update the readiness
implementation to read effective ruleset requirements and retain equivalent
coverage. Ensure the listed Conventional Commit title, Release impact, canary,
Compatibility, and Deterministic package checks remain required.

In `@scripts/release-validate.ts`:
- Around line 297-309: Update admitCandidate’s topology validation to require
every candidate parent, trustedBaseSha, mergedPrBaseSha, and reviewedPrHeadSha
value used in comparisons to be a valid 40-hex commit SHA before performing
equality checks. Reuse the same validation for repair inputs, ensuring empty or
malformed SHAs are rejected even when values match.

---

Nitpick comments:
In `@scripts/release-projection.ts`:
- Around line 160-171: Update gitChangedFiles to disable Git path quoting when
invoking git diff, preferably by adding -c core.quotePath=false before the diff
arguments, so returned filenames match GitHub-reported paths exactly.

In `@scripts/repository-readiness.test.ts`:
- Around line 101-152: Add test cases in the “default branch direct-push
protection” suite for each remaining accepted matchesDefaultBranch include
pattern: ~ALL, the bare branch name, and refs/heads/<branch>. Also verify a
non-empty conditions.ref_name.exclude fails closed with status unavailable, and
that passing an empty defaultBranch to classifyDirectPushProtection returns
unavailable.

In `@scripts/repository-readiness.ts`:
- Around line 312-332: Optionally rename the merge-method readiness symbols from
mergeCommitRepair and the "merge-commits" check name to squash-merge equivalents
so they accurately represent squash-merge availability. Update all references in
scripts/repository-readiness.test.ts and the documented readiness output in
README.md; defer the rename if the existing output string is a stable contract.
- Around line 233-241: Update the ruleset iteration in the direct-push
classifier to continue past records whose target is not "branch", matching
classifyTagRuleset. Keep the unavailable fail-closed result for values that are
not records, and continue evaluating remaining branch rulesets.
- Around line 1050-1062: In the readiness-check setup, extract the guarded
repository payload into a single repositoryData value before defining
squashMergeEnabled and checks. Reuse repositoryData for both allow_squash_merge
and default_branch, preserving undefined when the response is unsuccessful or
not a record.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 607c336e-1f0e-41be-bd72-d332b6698b00

📥 Commits

Reviewing files that changed from the base of the PR and between 478dcc4 and 99af6ad.

📒 Files selected for processing (9)
  • .github/workflows/release.yml
  • README.md
  • docs/adr/0003-reviewed-versioned-releases.md
  • scripts/release-projection.test.ts
  • scripts/release-projection.ts
  • scripts/release-validate.test.ts
  • scripts/release-validate.ts
  • scripts/repository-readiness.test.ts
  • scripts/repository-readiness.ts

Comment thread README.md Outdated
Comment thread scripts/release-validate.ts
@myagentdojo
myagentdojo temporarily deployed to hosted-canary-qualification August 9, 2026 10:23 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
scripts/release-projection.test.ts (1)

163-179: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Strengthen the newline-path regression assertion.

The test creates a newline-containing filename but checks only the generic changed-file-set error. A line-delimited parser would produce the same error and still pass this test.

Use a temporary projection that includes the exact path, then assert that the unsupported path error contains unreviewed-☃\nfile.txt as one value. Alternatively, test the changed-file helper directly.

Also applies to: 192-195

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/release-projection.test.ts` around lines 163 - 179, Strengthen the
newline-path regression test around the projection setup and assertion by using
a temporary projection containing the exact path unreviewed-☃\nfile.txt, then
verify the unsupported path error reports that filename as one value rather than
only asserting the generic changed-file-set error. Apply the same assertion
update to the related case at the second referenced location.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/repository-readiness.ts`:
- Around line 156-158: Update the branch matching logic around matchesBranch to
use GitHub-compatible fnmatch semantics instead of exact string membership, so
wildcard patterns such as refs/heads/* match the corresponding branch refs in
both include and exclude lists. Preserve the existing ~ALL, ~DEFAULT_BRANCH, and
default-branch handling, and add coverage for wildcard include and exclude
cases.

---

Nitpick comments:
In `@scripts/release-projection.test.ts`:
- Around line 163-179: Strengthen the newline-path regression test around the
projection setup and assertion by using a temporary projection containing the
exact path unreviewed-☃\nfile.txt, then verify the unsupported path error
reports that filename as one value rather than only asserting the generic
changed-file-set error. Apply the same assertion update to the related case at
the second referenced location.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 64a4dacf-74f1-4bf4-9173-a0f58cee625f

📥 Commits

Reviewing files that changed from the base of the PR and between 99af6ad and 36fc067.

📒 Files selected for processing (7)
  • README.md
  • scripts/release-projection.test.ts
  • scripts/release-projection.ts
  • scripts/release-validate.test.ts
  • scripts/release-validate.ts
  • scripts/repository-readiness.test.ts
  • scripts/repository-readiness.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • scripts/repository-readiness.test.ts
  • README.md
  • scripts/release-projection.ts
  • scripts/release-validate.ts
  • scripts/release-validate.test.ts

Comment thread scripts/repository-readiness.ts Outdated
@myagentdojo

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36fc067fc3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/repository-readiness.ts Outdated
@myagentdojo
myagentdojo temporarily deployed to hosted-canary-qualification August 9, 2026 10:34 — with GitHub Actions Inactive
@myagentdojo

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 4f653a8e7d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@myagentdojo

Copy link
Copy Markdown
Owner Author

@codex-gate approve 4f653a8 5231078558

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 4f653a8e7d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

2 participants