Skip to content

fix(#2489): retry scaffold push on non-fast-forward (auto_init race) - #2491

Merged
rh-hemartin merged 2 commits into
mainfrom
fix/2489-scaffold-non-fast-forward
Jul 2, 2026
Merged

fix(#2489): retry scaffold push on non-fast-forward (auto_init race)#2491
rh-hemartin merged 2 commits into
mainfrom
fix/2489-scaffold-non-fast-forward

Conversation

@rh-hemartin

@rh-hemartin rh-hemartin commented Jun 22, 2026

Copy link
Copy Markdown
Member

Fixes #2489

Summary

Scaffold pushes now retry once when hitting "Update is not a fast forward" errors. Resolves race where GitHub's async auto_init materializes a commit between ref fetch and ref update.

Root cause

CreateRepo sets auto_init: true (line 377 of github.go). GitHub creates initial commit async — can complete after commitFilesTo fetches current ref SHA (line 658) but before updating ref (line 899). Scaffold commit built from stale parent → non-fast-forward rejection.

Impact

Should eliminate most common e2e flake — appeared in nearly every failed run. TestVendorFromSubdirectory and TestAdminInstallUninstall hit this repeatedly.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Retry scaffold ref update with force on GitHub non-fast-forward (422)
🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

Description

• Detect GitHub 422 non-fast-forward ref update failures during scaffold pushes.
• Retry the ref update once with force: true to handle auto_init race conditions.
• Add a focused test asserting the force retry behavior and attempt count.
Diagram

graph TD
A["Scaffold: CommitFiles"] --> B["Fetch ref SHA"] --> C["Create tree/commit"] --> D["PATCH ref"] --> E{"422 error?"}
E -->|"No"| F["Success"]
E -->|"Non-fast-forward"| G["Retry PATCH (force)"] --> F
E -->|"Branch protection"| H["Return ErrBranchProtected"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Refetch head + rebuild commit (no force)
  • ➕ Avoids force-pushing even in newly created repos
  • ➕ Ensures the new commit is a true descendant of the latest head
  • ➖ More API calls and complexity (recompute tree/commit parent) for a rare race
  • ➖ Harder to keep behavior deterministic under concurrent initialization
2. Wait/poll for auto_init completion before scaffolding
  • ➕ Eliminates the race by ensuring the initial commit exists before ref fetch/update
  • ➕ Keeps ref update semantics strictly fast-forward
  • ➖ Adds latency and polling logic to repo creation/scaffold flows
  • ➖ Still needs a timeout/fallback strategy if initialization is delayed
3. Disable auto_init for scaffold-created repos
  • ➕ Removes the underlying source of the asynchronous initial commit race
  • ➕ Simplifies initial push semantics
  • ➖ May change expected repo UX/behavior (no default README/.gitignore/license)
  • ➖ Potentially impacts other flows relying on auto-init defaults

Recommendation: The chosen approach (detect non-fast-forward 422s and retry once with force) is the best tradeoff for reliability vs complexity in the scaffold context. It narrowly targets the known auto_init race, preserves branch-protection failures, and keeps runtime overhead minimal compared to refetch/rebuild or polling solutions.

Files changed (2) +70 / -3

Bug fix (1) +24 / -3
github.goRetry Git ref update with force on non-fast-forward 422 +24/-3

Retry Git ref update with force on non-fast-forward 422

• Extends the branch ref update step to distinguish 422 causes: branch protection remains an immediate failure, while non-fast-forward errors trigger a single retry with 'force: true'. Adds an 'isNonFastForwardError' helper to detect the condition from GitHub error messages.

internal/forge/github/github.go

Tests (1) +46 / -0
github_test.goAdd test covering non-fast-forward force retry on PATCH ref +46/-0

Add test covering non-fast-forward force retry on PATCH ref

• Introduces 'TestCommitFiles_NonFastForwardRetry', using an httptest server to simulate an initial 422 non-fast-forward on ref update followed by a successful forced retry. Asserts the first PATCH omits 'force', the second includes it, and exactly one retry occurs.

internal/forge/github/github_test.go

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

Site preview

Preview: https://dfdcdb6e-site.fullsend-ai.workers.dev

Commit: 5a5d9ed933e044f11478f7ce0d86f2e5068fb075

@qodo-code-review

qodo-code-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 51 rules
✅ Skills: writing-user-docs, writing-adrs

Grey Divider


Action required

1. Unscoped force ref retry ✓ Resolved 📎 Requirement gap ☼ Reliability
Description
commitFilesTo retries a 422 non-fast-forward PATCH to update a git ref by setting force: true
without verifying it is operating on a brand-new scaffold repo/branch, which can overwrite
legitimate concurrent commits in existing repos. Because commitFilesTo is shared by
CommitFiles/CommitFilesToBranch and used by non-scaffold workflows (e.g., vendoring and config
repo updates), this turns a real concurrency signal into a destructive force-update across multiple
call sites.
Code

internal/forge/github/github.go[R802-811]

+			// Non-fast-forward from auto_init race — retry with force.
+			// Safe for freshly-created repos where we own the only commit.
+			if isNonFastForwardError(apiErr) {
+				refPayload["force"] = true
+				retryResp, retryErr := c.patch(ctx, fmt.Sprintf("/repos/%s/%s/git/refs/heads/%s", owner, repo, branch), refPayload)
+				if retryErr != nil {
+					return false, fmt.Errorf("update ref (force): %w", retryErr)
+				}
+				retryResp.Body.Close()
+				return true, nil
Relevance

⭐⭐⭐ High

Team often tightens/limits 422 retry/classification logic to avoid unsafe broad behavior (PRs #816,
#2201).

PR-#816
PR-#2201

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited code changes implement a retry path in commitFilesTo that detects a 422 non-fast-forward
response from PATCH /git/refs/... and then sets refPayload["force"] = true before retrying the
ref update. Since commitFilesTo is the common implementation behind both CommitFiles and
CommitFilesToBranch and is invoked from non-scaffold flows (e.g., vendoring and other repo update
paths), the force retry is not constrained to the brand-new scaffold push scenario required for
safety; therefore, in normal concurrent-update situations the retry can forcibly move the branch ref
and clobber unrelated commits.

Ensure ref update payload/strategy is explicitly safe for brand-new repo scaffold pushes
internal/forge/github/github.go[650-812]
internal/cli/vendor.go[200-213]
internal/forge/github/github.go[641-653]
internal/forge/github/github.go[790-812]
internal/cli/vendor.go[200-214]
internal/layers/harnesswrappers.go[129-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`commitFilesTo` currently retries any 422 non-fast-forward ref update by setting `force: true`, but this behavior is only potentially safe in the narrow “brand-new repo scaffold push” context (if at all) and is unsafe when `CommitFiles`/`CommitFilesToBranch` are used on existing repositories. As implemented, a legitimate concurrency/non-fast-forward signal in non-scaffold workflows (e.g., vendoring and config repo updates) can be “resolved” by forcibly moving the branch ref, overwriting other commits.

## Issue Context
- Compliance requires that any non-fast-forward mitigation be explicitly safe and constrained to the brand-new scaffold pathway, not applied broadly.
- `commitFilesTo` is the shared implementation behind `CommitFiles` and `CommitFilesToBranch`, and it is called from multiple non-scaffold code paths (e.g., vendoring, config repo updates).
- The current retry behavior turns a non-fast-forward (often caused by normal concurrent updates) into a destructive force update.
- Fix should either (a) avoid force by rebuilding the commit on top of the latest ref, or (b) strictly gate force behind an explicit option/parameter enabled only for the known fresh-repo scaffold path.

## Fix Focus Areas
- internal/forge/github/github.go[641-653]
- internal/forge/github/github.go[650-812]
- internal/forge/github/github.go[790-812]
- internal/cli/vendor.go[200-214]
- internal/layers/harnesswrappers.go[129-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Test missing default handler 🐞 Bug ⚙ Maintainability
Description
TestCommitFiles_NonFastForwardRetry’s httptest handler has no default case, so unexpected requests
can silently return an implicit 200/empty body and fail later with less actionable errors. Other
tests in this file fail fast on unexpected requests, improving regression detection.
Code

internal/forge/github/github_test.go[R1808-1840]

+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch {
+		case r.Method == "GET" && r.URL.Path == "/repos/org/repo":
+			json.NewEncoder(w).Encode(map[string]string{"default_branch": "main"})
+		case r.Method == "GET" && r.URL.Path == "/repos/org/repo/git/ref/heads/main":
+			json.NewEncoder(w).Encode(map[string]any{"object": map[string]string{"sha": "commit"}})
+		case r.Method == "GET" && r.URL.Path == "/repos/org/repo/git/commits/commit":
+			json.NewEncoder(w).Encode(map[string]any{"tree": map[string]string{"sha": "tree"}})
+		case r.Method == "GET" && strings.HasPrefix(r.URL.Path, "/repos/org/repo/git/trees/tree"):
+			json.NewEncoder(w).Encode(map[string]any{"tree": []any{}, "truncated": false})
+		case r.Method == "POST" && r.URL.Path == "/repos/org/repo/git/trees":
+			json.NewEncoder(w).Encode(map[string]string{"sha": "newtree"})
+		case r.Method == "POST" && r.URL.Path == "/repos/org/repo/git/commits":
+			json.NewEncoder(w).Encode(map[string]string{"sha": "newcommit"})
+		case r.Method == "PATCH" && r.URL.Path == "/repos/org/repo/git/refs/heads/main":
+			patchCount++
+			var body map[string]any
+			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
+
+			if patchCount == 1 {
+				// First attempt: non-fast-forward
+				assert.Nil(t, body["force"], "first attempt should not force")
+				w.WriteHeader(http.StatusUnprocessableEntity)
+				json.NewEncoder(w).Encode(map[string]any{
+					"message": "Update is not a fast forward",
+				})
+			} else {
+				// Second attempt: with force
+				assert.Equal(t, true, body["force"], "retry should force")
+				w.WriteHeader(http.StatusOK)
+			}
+		}
+	}))
Relevance

⭐⭐ Medium

No direct precedent; similar “make tests stricter” suggestions were rejected as unnecessary in
github_test.go (PR #2342).

PR-#2342

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new test’s server handler only handles known endpoints and has no default error path, while
nearby tests demonstrate the preferred fail-fast pattern with a default case.

internal/forge/github/github_test.go[1806-1840]
internal/forge/github/github_test.go[1738-1778]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The httptest server in `TestCommitFiles_NonFastForwardRetry` does not fail on unexpected requests, reducing test clarity/robustness.

### Issue Context
Other tests in `github_test.go` include a `default:` branch that `t.Errorf/t.Fatalf` and returns 404 for unhandled endpoints.

### Fix Focus Areas
- internal/forge/github/github_test.go[1806-1840]
- internal/forge/github/github_test.go[1738-1778]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread internal/forge/github/github.go Outdated
@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:45 AM UTC · Completed 9:59 AM UTC
Commit: de388e8 · View workflow run →

@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

Looks good to me

Previous run

Looks good to me

Previous run (2)

Looks good to me

Findings

Low

  • [edge-case] internal/layers/commit.go:325 — The retry on non-fast-forward is not scoped exclusively to the auto_init race; any non-fast-forward error triggers one retry. This is harmless since a persistent non-fast-forward will simply fail again and propagate, but the warning message ("auto_init race") could be mildly misleading in other scenarios.

  • [scope-authorization-alignment] internal/layers/commit.go — Issue e2e: TestVendorFromSubdirectory flaky — 422 non-fast-forward on scaffold push #2489 suggested option 2 (auto_init:false) as "likely cleanest," but this PR implements a layer-level retry instead. The retry approach is a valid engineering choice — CommitFiles re-fetches the ref from scratch on each call, so the retry correctly resolves the stale-parent race. A brief code comment noting why retry was chosen over auto_init:false would help future maintainers.

Previous run (3)

Looks good to me

Previous run (4)

Looks good to me

Previous run (5)

Looks good to me

Previous run (6)

Review

Findings

Low

  • [fail-open] internal/forge/github/github.go:983isNonFastForwardError matches on substrings "not a fast forward" and "not a fast-forward". A false positive on an unrelated 422 error message containing these words would trigger an unintended force push. The risk is mitigated by the status-code gate (only 422s reach this check), the prior isBranchProtectionError filter, and the allowForce flag (only CommitFilesToBranch enables it). Consistent with existing string-matching patterns (isBranchProtectionError, isAlreadyExistsError), but the stakes are higher here because the consequence is a force push.

  • [intent-scope-mismatch] internal/forge/github/github.go:638CommitFiles passes allowForce=false, so it does not benefit from the force-retry logic. The root cause (auto_init race) could theoretically affect CommitFiles when called shortly after CreateRepo. In practice the race primarily manifests on the CommitFilesToBranch scaffold path (the e2e failure site), but consider documenting why the default-branch path is not affected or extending the fix if it is.

  • [solution-selection-justification] internal/forge/github/github.go:805 — Issue e2e: TestVendorFromSubdirectory flaky — 422 non-fast-forward on scaffold push #2489 identified option 2 (auto_init:false) as "likely cleanest," but this PR implements option 1 (force:true retry). Both approaches are defensible — option 1 handles the race reactively while option 2 avoids it entirely. A brief code comment or PR note explaining why option 1 was chosen over option 2 would help future maintainers.

Previous run (7)

Review

Findings

Medium

  • [logic-error] internal/forge/github/github.go:804 — The force-push retry in commitFilesTo applies unconditionally to all callers: CommitFiles (used by vendor.go for vendoring binaries and harnesswrappers.go for wrapper commits) and CommitFilesToBranch (used for scaffold branch fallback). The comment says "Safe for freshly-created repos where we own the only commit," but no guard verifies this condition. While all current callers are fullsend-owned automation paths (narrowing the blast radius compared to general collaborative editing), a concurrent fullsend run on the same repo hitting a legitimate non-fast-forward could still result in a silent force-push discarding work.
    Remediation: Gate the force-push retry on a caller-provided flag (e.g., allowForceOnNonFF bool) from the caller that knows the repo was just created, or verify the branch has at most one commit before force-pushing.

  • [error-handling] internal/forge/github/github.go:808 — If the force-push retry itself returns a 422 with a branch-protection message, the error is wrapped as update ref (force): <err> without checking isBranchProtectionError. The caller in layers/commit.go:20 checks forge.IsBranchProtected(err) to decide whether to fall back to a scaffold PR branch. Because the retry error does not wrap forge.ErrBranchProtected, the branch-protection fallback path is unreachable on the forced attempt.
    Remediation: After the force-push retry fails, check isBranchProtectionError on the retry error and wrap with forge.ErrBranchProtected if applicable.

Low

  • [test-inadequate] internal/forge/github/github_test.go:1805 — The test only covers the successful retry path. There is no test for the case where the force-push retry itself fails (e.g., returns a 422 branch-protection error on the forced attempt).
  • [fail-open] internal/forge/github/github.go:972isNonFastForwardError matches on substring "fast forward" or "fast-forward". A false positive on an unrelated error could trigger unintended force push. Consistent with existing patterns (isBranchProtectionError) but stakes are higher due to force-push consequence.
  • [intent-mismatch] internal/forge/github/github.go:805 — The PR implements solution option 1 (force:true on ref update) but issue e2e: TestVendorFromSubdirectory flaky — 422 non-fast-forward on scaffold push #2489 identified option 2 (auto_init:false) as "likely cleanest." The implementation diverges from the issue's suggested direction without explicit justification.
  • [architectural-conflict] internal/forge/github/github.go:805 — The retry logic constructs a new PATCH request rather than wrapping in retryOnRepoRace. This is actually correct since retryOnRepoRace only retries on 404/409 (not 422), but a brief code comment explaining why would address the apparent inconsistency.

Labels: PR fixes a bug in the forge/github install scaffold flow, directly addressing e2e test flakiness

Previous run (8)

Review

Findings

Medium

  • [logic-error] internal/forge/github/github.go:804 — The force-push retry in commitFilesTo applies unconditionally to all callers: CommitFiles (used by vendor.go for vendoring binaries and harnesswrappers.go for wrapper commits) and CommitFilesToBranch (used for scaffold branch fallback). The comment says "Safe for freshly-created repos where we own the only commit," but no guard verifies this condition. While all current callers are fullsend-owned automation paths (narrowing the blast radius compared to general collaborative editing), a concurrent fullsend run on the same repo hitting a legitimate non-fast-forward could still result in a silent force-push discarding work.
    Remediation: Gate the force-push retry on a caller-provided flag (e.g., allowForceOnNonFF bool) from the caller that knows the repo was just created, or verify the branch has at most one commit before force-pushing.

  • [error-handling] internal/forge/github/github.go:808 — If the force-push retry itself returns a 422 with a branch-protection message, the error is wrapped as update ref (force): <err> without checking isBranchProtectionError. The caller in layers/commit.go:20 checks forge.IsBranchProtected(err) to decide whether to fall back to a scaffold PR branch. Because the retry error does not wrap forge.ErrBranchProtected, the branch-protection fallback path is unreachable on the forced attempt.
    Remediation: After the force-push retry fails, check isBranchProtectionError on the retry error and wrap with forge.ErrBranchProtected if applicable.

Low

  • [test-inadequate] internal/forge/github/github_test.go:1805 — The test only covers the successful retry path. There is no test for the case where the force-push retry itself fails (e.g., returns a 422 branch-protection error on the forced attempt).
  • [fail-open] internal/forge/github/github.go:972isNonFastForwardError matches on substring "fast forward" or "fast-forward". A false positive on an unrelated error could trigger unintended force push. Consistent with existing patterns (isBranchProtectionError) but stakes are higher due to force-push consequence.
  • [intent-mismatch] internal/forge/github/github.go:805 — The PR implements solution option 1 (force:true on ref update) but issue e2e: TestVendorFromSubdirectory flaky — 422 non-fast-forward on scaffold push #2489 identified option 2 (auto_init:false) as "likely cleanest." The implementation diverges from the issue's suggested direction without explicit justification.
  • [architectural-conflict] internal/forge/github/github.go:805 — The retry logic constructs a new PATCH request rather than wrapping in retryOnRepoRace. This is actually correct since retryOnRepoRace only retries on 404/409 (not 422), but a brief code comment explaining why would address the apparent inconsistency.
Previous run (9)

Review

Findings

High

  • [logic-error] internal/forge/github/github.go:804 — The force-push retry in commitFilesTo applies unconditionally to all callers: CommitFiles (used by vendor.go for vendoring binaries and harnesswrappers.go for wrapper commits) and CommitFilesToBranch (used for scaffold branch fallback). The comment says "Safe for freshly-created repos where we own the only commit," but no guard verifies this condition. On an established repository with concurrent pushers, a legitimate non-fast-forward rejection would be silently resolved by force-pushing, discarding another developer's commit.
    Remediation: Either (a) scope the force-push retry to only the scaffold code path by passing an option/flag (e.g., allowForceOnNonFF bool) from the caller that knows the repo was just created, or (b) instead of force-pushing, re-fetch the ref and rebuild the commit on top of the new parent.

Medium

  • [error-handling] internal/forge/github/github.go:808 — If the force-push retry itself returns a 422 with a branch protection message, the error is wrapped as update ref (force): <err> without checking isBranchProtectionError. The caller (e.g., layers/commit.go:38 which checks forge.IsBranchProtected) loses the semantic signal, causing misdiagnosis of the failure.
    Remediation: After the force-push retry fails, check isBranchProtectionError on the retry error and wrap with forge.ErrBranchProtected if applicable.

Low

  • [test-inadequate] internal/forge/github/github_test.go:1805 — The test only covers the successful retry path. There is no test for the case where the force-push retry itself fails (e.g., returns a 422 branch-protection error on the forced attempt).
  • [fail-open] internal/forge/github/github.go:972isNonFastForwardError matches on substring "fast forward" or "fast-forward". A false positive on an unrelated error could trigger unintended force push. Consistent with existing patterns (isBranchProtectionError) but stakes are higher due to force-push consequence.
  • [architectural-conflict] internal/forge/github/github.go:814 — The retry logic constructs a new PATCH request rather than wrapping in retryOnRepoRace. This is actually correct since retryOnRepoRace only retries on 404/409 (not 422), but a brief code comment explaining why would address the apparent inconsistency.
  • [pattern-inconsistency] internal/forge/github/github.go:792 — Changed refPayload from map[string]string to map[string]any. This is correct and necessary since the payload now contains a bool (force), but breaks the established pattern for string-only payloads.
  • [code-organization] internal/forge/github/github.go:798 — Error handling now has three levels of nesting. An early return after the branch protection check could flatten this.

Labels: PR fixes a bug in the forge/github install scaffold flow, directly addressing e2e test flakiness

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added type/bug Confirmed defect in existing behavior component/install CLI install and app setup component/e2e End-to-end tests labels Jun 22, 2026
@rh-hemartin
rh-hemartin force-pushed the fix/2489-scaffold-non-fast-forward branch from de388e8 to a2348de Compare June 22, 2026 11:10
@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:14 AM UTC · Completed 11:27 AM UTC
Commit: a2348de · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:34 AM UTC · Completed 11:47 AM UTC
Commit: e17e485 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the ready-for-merge All reviewers approved — ready to merge label Jun 22, 2026
@rh-hemartin rh-hemartin self-assigned this Jun 22, 2026

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this needs a change before we can merge. See inline.

Comment thread internal/forge/github/github.go Outdated

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Multi-Agent Review — 6 agents (Claude, Gemini, Codex)

5 of 6 agents independently confirmed @ralphbean's inline finding: the force-retry path is unreachable in the actual failing scaffold flow. Three additional unique findings below (HIGH, 2x MEDIUM) that are not covered by existing review comments.

Assisted-by: Claude (review), Gemini (review), Codex (review)

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

3 new findings (1 HIGH, 2 MEDIUM) from multi-agent review — unique issues not covered by existing comments. See inline.

Assisted-by: Claude (review), Gemini (review), Codex (review)

Comment thread internal/forge/github/github.go Outdated
Comment thread internal/forge/github/github_test.go
Comment thread internal/forge/github/github.go Outdated
@fullsend-ai-review

fullsend-ai-review Bot commented Jun 29, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:56 AM UTC · Completed 8:08 AM UTC
Commit: 2404f44 · View workflow run →

@rh-hemartin

Copy link
Copy Markdown
Member Author

Applied more feedback before merging. Thanks for the reviews.

@rh-hemartin
rh-hemartin enabled auto-merge July 2, 2026 07:08
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 2, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:09 AM UTC · Completed 7:21 AM UTC
Commit: e1274ea · View workflow run →

@rh-hemartin
rh-hemartin added this pull request to the merge queue Jul 2, 2026
@fullsend-ai-review
fullsend-ai-review Bot dismissed stale reviews from themself July 2, 2026 07:21

Superseded by updated review

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge and removed ready-for-merge All reviewers approved — ready to merge labels Jul 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 2, 2026
@rh-hemartin
rh-hemartin added this pull request to the merge queue Jul 2, 2026
@rh-hemartin
rh-hemartin force-pushed the fix/2489-scaffold-non-fast-forward branch from e1274ea to 8bba718 Compare July 2, 2026 09:58
@rh-hemartin
rh-hemartin removed this pull request from the merge queue due to a manual request Jul 2, 2026
rh-hemartin and others added 2 commits July 2, 2026 11:58
CommitFiles now detects 422 "Update is not a fast forward" errors during
scaffold pushes and retries with `force: true`. Fixes race where GitHub's
async auto_init creates a commit between ref fetch and ref update.

Root cause: CreateRepo sets auto_init: true. GitHub materializes initial
commit async — can complete after commitFilesTo fetches current ref SHA
(line 658) but before updating ref (line 795). Tree built from stale parent
→ non-fast-forward.

Force push safe for scaffold — repo just created, we own only commit.
Separate from branch protection errors (still rejected).

Closes #2489

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Hector Martinez <hemartin@redhat.com>
Patch coverage was below 80% threshold because these new code paths
were only exercised from other packages' tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Hector Martinez <hemartin@redhat.com>
@rh-hemartin
rh-hemartin force-pushed the fix/2489-scaffold-non-fast-forward branch from 8bba718 to 5a5d9ed Compare July 2, 2026 09:58
@rh-hemartin
rh-hemartin enabled auto-merge July 2, 2026 09:58
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 2, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:59 AM UTC · Completed 10:11 AM UTC
Commit: 5a5d9ed · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge and removed ready-for-merge All reviewers approved — ready to merge labels Jul 2, 2026
@rh-hemartin
rh-hemartin added this pull request to the merge queue Jul 2, 2026
Merged via the queue into main with commit 9948cbd Jul 2, 2026
20 of 21 checks passed
@rh-hemartin
rh-hemartin deleted the fix/2489-scaffold-non-fast-forward branch July 2, 2026 10:20
@fullsend-ai-retro

fullsend-ai-retro Bot commented Jul 2, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 10:22 AM UTC · Completed 10:28 AM UTC
Commit: 5a5d9ed · View workflow run →

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #2491 — fix(#2489): retry scaffold push on non-fast-forward

Timeline

This human-authored PR (by rh-hemartin, co-authored with Claude Opus 4.6) fixed a flaky e2e test caused by a race between GitHub's auto_init commit and scaffold push. The PR was open 10 days (2026-06-22 to 2026-07-02) and went through a major architectural pivot after review.

  1. Initial approach (commit 1, 06-22): Added force: true retry inside commitFilesTo at the GitHub API layer.
  2. fullsend-ai-review[bot] (06-22): Ran 3 reviews. Flagged blast radius of unscoped force-push (high→medium) and test gaps (low). On the 3rd review, noted CommitFiles passes allowForce=false as a low "intent-scope-mismatch" — but approved without recognizing this meant the fix was dead code for the actual failing flow.
  3. ralphbean (human, 06-22): Traced the full call chain: CommitScaffoldFilesCommitFiles(allowForce=false) → non-fast-forward 422 ≠ branch-protection → error returned at commit.go:68 without ever reaching CommitFilesToBranch(allowForce=true). The entire force-retry path was unreachable in the failing scenario. Suggested adopting option 2 from the issue (auto_init: false).
  4. waynesun09 multi-agent review (06-22): 5 of 6 agents confirmed ralphbean's finding.
  5. Architectural pivot (06-29–06-30): Author moved retry up to internal/layers/commit.go, retrying the entire CommitFiles() call instead of force-pushing at the API layer.
  6. Final approval and merge (07-01–07-02).

Review quality gap

The review bot had the key clue — it noted allowForce=false — but treated it as a low-severity scope question rather than tracing the call chain to realize the fix was completely unreachable. The human reviewer traced the execution path from the bug report through 3 function calls to prove the fix was dead code.

Existing issues checked

One proposal filed for the distinct gap: verifying fix reachability via call-chain tracing.

Proposals filed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/e2e End-to-end tests component/install CLI install and app setup ready-for-merge All reviewers approved — ready to merge type/bug Confirmed defect in existing behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

e2e: TestVendorFromSubdirectory flaky — 422 non-fast-forward on scaffold push

3 participants