fix(push_signed_commits): recover from shallow/partial-clone object failures during rebase#39859
Conversation
…ailures during rebase
When the base branch advances after the agent produced its commits,
pushSignedCommits rebases the commit range onto the current GraphQL
parent. On a shallow + partial (--filter=blob:none) checkout of a large
monorepo, this rebase can fail because git lazily fetches required
objects from the promisor remote and the fetch is rejected
("could not fetch <sha> from promisor remote" / "not our ref").
Previously this aborted and threw immediately, falling back to a review
issue instead of a PR. Now the rebase output is classified: a recoverable
partial-clone object failure triggers a history backfill (git fetch
--unshallow, or a base-ref re-fetch when the repo is already complete)
and the rebase is retried once. Genuine merge conflicts still fail fast.
Closes #39858
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
This PR improves pushSignedCommits’s resilience when rebasing a stale commit range onto an updated base in shallow + partial (--filter=blob:none) checkouts, where git can fail due to missing promisor objects. It adds output-based failure classification, a one-time history backfill + retry path for recoverable object failures, and integration tests covering the recovery vs. genuine merge-conflict behavior.
Changes:
- Add
isPartialCloneObjectFailure()to detect promisor/missing-object failures from git output. - Capture
git rebase --ontooutput, attemptgit fetch --unshallow(or a fallback fetch) and retry the rebase once on recoverable failures. - Add integration tests ensuring recovery occurs only for object failures and not for true merge conflicts.
Show a summary per file
| File | Description |
|---|---|
| actions/setup/js/push_signed_commits.cjs | Adds partial-clone failure detection and a fetch+retry recovery path for rebase failures. |
| actions/setup/js/push_signed_commits.test.cjs | Adds integration tests for the recovery retry behavior and confirms merge conflicts don’t trigger backfill. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 2
This comment has been minimized.
This comment has been minimized.
Replace the uncontrolled 'git fetch --unshallow' recovery in pushSignedCommits with a bounded, monorepo-safe backfill that fetches the full object content of EXACTLY the commits the rebase needs (new GraphQL parent, old replay parent, and branch tip) via 'git fetch --no-filter origin <sha>...'. Add a unified backfillCommitObjects helper to git_helpers.cjs mirroring the targeted fetch-by-SHA strategy already used in ensureFullHistoryForBundle. No --unshallow, no unbounded deepen, so a large monorepo's full history is never downloaded. Update tests to assert the recovery fetches a bounded set of exact commit SHAs and never uses --unshallow/--deepen.
Updated approach: bounded exact-commit backfill (no
|
git rebase --onto can trigger lazy promisor fetches in shallow/partial clones. Without gitAuthEnv those fetches fail in private repos after checkout credentials are scrubbed from .git/config. Pass the same auth env used for other network-touching git operations.
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
✅ Test Quality Sentinel completed test quality analysis. |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
|
✅ PR Code Quality Reviewer completed the code quality review. |
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #39859 does not have the 'implementation' label and has 0 new lines of code in business logic directories (≤100 threshold). |
|
✅ smoke-ci: safeoutputs CLI comment + comment-memory run (27710791912)
|
🧪 Test Quality Sentinel Report✅ Test Quality Score: 100/100 — Excellent
📊 Metrics & Test Classification (2 tests analyzed)
Go: 0 ( Verdict
References:
|
There was a problem hiding this comment.
🔎 Code quality review by PR Code Quality Reviewer
| return ( | ||
| /could not fetch .* from promisor remote/i.test(output) || | ||
| /upload-pack: not our ref/i.test(output) || | ||
| /remote error: upload-pack/i.test(output) || |
There was a problem hiding this comment.
Over-broad pattern silently hijacks non-partial-clone upload-pack errors: /remote error: upload-pack/i matches any remote upload-pack error — auth failures, rate-limiting, server-side packing errors — not only the not our ref partial-clone case this function is meant to detect. A transient auth error would incorrectly trigger the backfill path, waste time on a git fetch --no-filter, and then surface the misleading message \u201cthe required commit objects could not be backfilled\u201d instead of the real error.
\ud83d\udca1 Suggested fix
Pattern 3 is a superset of pattern 2 (/upload-pack: not our ref/i) and adds only false matches. Remove it:
- /remote error: upload-pack/i.test(output) ||Pattern 2 already covers the specific promisor-rejection message; any additional upload-pack sub-errors should be added with their exact suffix.
| /could not fetch .* from promisor remote/i.test(output) || | ||
| /upload-pack: not our ref/i.test(output) || | ||
| /remote error: upload-pack/i.test(output) || | ||
| /(unable to read|object).*(missing|not found)/i.test(output) || |
There was a problem hiding this comment.
object alternation in pattern 4 is too broad — misclassifies genuine merge conflicts: The regex /(unable to read|object).*(missing|not found)/i uses object as a bare alternation. Any git error mentioning the word "object" followed anywhere by "missing" or "not found" matches — including messages from a normal conflict (e.g. error: Your local changes to the following files would be overwritten by merge: ... object or similar). This silently promotes a genuine merge conflict into the partial-clone recovery path, wasting an expensive fetch-and-retry before ultimately failing with a misleading error.
Similarly, /fatal: bad object/i fires for wrong ref names, detached HEAD anomalies, and corrupt local objects — none of which are recoverable via promisor backfill.
💡 Suggested fix
Tighten both patterns to match only the promisor-specific message forms:
- /(unable to read|object).*(missing|not found)/i.test(output) ||
- /fatal: bad object/i.test(output)
+ /unable to read [0-9a-f]{40}/i.test(output) ||
+ /fatal: object [0-9a-f]{40} is unavailable/i.test(output)Including the SHA hex pattern in the match ensures only object-fetch failures triggered by a specific known commit are classified as partial-clone failures.
| } catch { | ||
| // If HEAD cannot be resolved, fall back to the known range anchors. | ||
| } | ||
| const fetchTargets = [firstGraphqlParentOid, firstReplayParentOid, headOid].filter(sha => typeof sha === "string" && sha.length > 0); |
There was a problem hiding this comment.
headOid may be a rebase-internal SHA if --abort failed silently, poisoning the backfill fetch: rev-parse HEAD is called after git rebase --abort whose failure is swallowed. If abort failed (e.g. the rebase-state lock is held), HEAD remains pointing at the cherry-pick-in-progress commit — a local-only SHA that does not exist on origin. Passing that SHA to git fetch --no-filter origin <sha1> <sha2> <bad-sha> causes the entire fetch to fail (git fetch is all-or-nothing for multi-ref fetches), so recovered = false and the caller throws the misleading "could not be backfilled" error rather than attempting recovery with just the two known-good anchor SHAs.
Additionally, the filter at line 449 (sha.length > 0) is weaker than backfillCommitObjects's own guard (/^[0-9a-f]{40}$/i), so the core.warning message may report e.g. "3 anchor commit(s)" while only 2 valid SHAs are actually fetched.
💡 Suggested fix
- Validate
headOidwith the same 40-char hex pattern before including it:
- const fetchTargets = [firstGraphqlParentOid, firstReplayParentOid, headOid].filter(sha => typeof sha === "string" && sha.length > 0);
+ const headOidValid = typeof headOid === "string" && /^[0-9a-f]{40}$/i.test(headOid);
+ const fetchTargets = [firstGraphqlParentOid, firstReplayParentOid, ...(headOidValid ? [headOid] : [])];- Consider using only the two known-good anchor SHAs (
firstGraphqlParentOid,firstReplayParentOid) and omitting the post-abort HEAD entirely.
| } | ||
|
|
||
| if (recovered) { | ||
| rebaseResult = await runRebase(); |
There was a problem hiding this comment.
Fetching only 3 anchor commits does not guarantee all intermediate objects are present — retry will fail with the same promisor error for long ranges: backfillCommitObjects fetches exactly firstGraphqlParentOid, firstReplayParentOid, and HEAD. In a partial clone, git rebase --onto needs the full tree+blob content for every commit in the range firstReplayParentOid..HEAD. For a branch that is tens or hundreds of commits behind, git must lazily fetch intermediate objects for each cherry-picked commit in turn. Fetching only the three endpoints does not pre-materialise those blobs — the retry rebase hits the same could not fetch ... from promisor remote failure on the first intermediate commit.
The error at line 467 then says "failed to rebase ... even after backfilling the required commit objects", implying the objects were backfilled but the rebase still failed — which is misleading; the objects were never fully backfilled.
💡 Suggested fix options
Option A — Fetch the full range: replace the 3-anchor fetch with:
git fetch --no-filter origin firstReplayParentOid..firstGraphqlParentOid(or use git fetch --unshallow origin as a last resort if the repo is shallow).
Option B — At minimum, document the limitation in the JSDoc of backfillCommitObjects and in the core.warning message, so operators know the retry may still fail and why.
Option C — Re-use ensureFullHistoryForBundle's iterative-deepen strategy which already handles this case for bundle apply.
| const fetchTargets = [firstGraphqlParentOid, firstReplayParentOid, headOid].filter(sha => typeof sha === "string" && sha.length > 0); | ||
| core.warning(`pushSignedCommits: rebase failed due to missing objects in a shallow/partial clone; ` + `backfilling object content for ${fetchTargets.length} anchor commit(s) directly from origin by SHA and retrying once`); | ||
| let recovered = false; | ||
| try { |
There was a problem hiding this comment.
Dead outer catch around backfillCommitObjects is unreachable and misleading: backfillCommitObjects catches all errors internally and always returns a boolean — it never throws. The catch (recoveryError) block at this line can therefore never execute, and the core.warning inside it will never fire. This makes the code look like a defensive guard when it is actually dead code, and any future reader will assume backfillCommitObjects can throw (it can't without changing its internal contract).
💡 Suggested fix
Remove the outer try/catch — recovered will correctly be false on any backfill failure:
- let recovered = false;
- try {
- recovered = await backfillCommitObjects(exec, fetchTargets, { cwd, env: { ...process.env, ...(gitAuthEnv || {}) } });
- } catch (recoveryError) {
- core.warning(`pushSignedCommits: targeted object backfill failed: ...`);
- }
+ const recovered = await backfillCommitObjects(exec, fetchTargets, { cwd, env: { ...process.env, ...(gitAuthEnv || {}) } });There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnose, /tdd, and /zoom-out — requesting changes on two classifier precision issues and one missing test branch.
📋 Key Themes & Highlights
Blocking concerns
- Classifier false-positive risk (lines 43–44): two of the five
isPartialCloneObjectFailurepatterns are broader than the documented git promisor error messages. A false positive would cause the code to abort a real error, run a useless targeted fetch, and surface a misleading "even after backfilling" message instead of the original root cause. See inline comments for tighter patterns. - Missing test branch (test line 1964): the path where backfill succeeds but the retry rebase also fails is untested. It has its own error message and
rebase --abortcall — both worth a regression guard.
Non-blocking suggestions
isPartialCloneObjectFailureneeds isolated unit tests (one per pattern); the integration tests cover only two of the five patterns via one happy path.fetchTargetsfilter inconsistency: usessha.length > 0whilebackfillCommitObjectsre-validates to/^[0-9a-f]{40}$/i; preferOID_PATTERNat the call site.backfillCommitObjects: silentfalseon empty targets; add acore.warningto make this debuggable.backfillCommitObjects: nocore.infoon success; operators have no confirmation in run logs that the recovery fetch actually ran.gitAuthEnvthreading is an independent correctness fix (previously missing); worth a line in the PR description.global.execoverwrite in tests: save/restore in afinallyblock for test isolation.
Positive highlights
- ✅ Excellent root-cause analysis in the PR description, with a link to the real failing run
- ✅ Bounded recovery strategy (
git fetch --no-filter origin <sha>...) mirrors the existingensureFullHistoryForBundleapproach — good consistency - ✅ Single-retry guard (
let recovered = false; if (recovered)) prevents infinite retry loops - ✅ Conflict test correctly guards the most important invariant: genuine conflicts must not trigger the backfill path
- ✅
backfillCommitObjectsdefensively validates all SHA inputs at both the helper and call-site levels
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer
| /could not fetch .* from promisor remote/i.test(output) || | ||
| /upload-pack: not our ref/i.test(output) || | ||
| /remote error: upload-pack/i.test(output) || | ||
| /(unable to read|object).*(missing|not found)/i.test(output) || |
There was a problem hiding this comment.
[/diagnose] This regex is too broad and risks false-positives that would suppress real errors.
(unable to read|object).*(missing|not found) can match git output well beyond promisor failures — e.g. a locally corrupted object or working-tree message. A false positive causes the code to abort a healthy rebase failure, run a useless targeted fetch, and surface a confusing "even after backfilling" error instead of the real root cause.
💡 Suggested tighter pattern
Anchor the SHA into the pattern so it only matches specific git lazy-fetch messages:
// before
/(unable to read|object).*(missing|not found)/i.test(output)
// after — require a 40-char SHA adjacent to the keyword
/(?:unable to read|object) [0-9a-f]{40}|[0-9a-f]{40}.*(?:missing|not found)/i.test(output)This keeps coverage for real promisor messages while excluding generic working-tree and corruption messages.
| /upload-pack: not our ref/i.test(output) || | ||
| /remote error: upload-pack/i.test(output) || | ||
| /(unable to read|object).*(missing|not found)/i.test(output) || | ||
| /fatal: bad object/i.test(output) |
There was a problem hiding this comment.
[/diagnose] fatal: bad object is too generic — it fires for invalid/missing revision arguments unrelated to promisor fetches (e.g. a bad branch name, a deleted ref).
A false positive would incorrectly trigger the backfill path for an unrecoverable error, masking the real cause.
💡 Suggested fix
Either remove this pattern entirely (the other four patterns already cover the documented promisor error messages) or anchor it to a SHA:
// before
/fatal: bad object/i.test(output)
// after — only match when a 40-char OID follows the phrase
/fatal: bad object [0-9a-f]{40}/i.test(output)The specific promisor scenario where this fires (the new GraphQL parent OID isn't present locally) is already covered by upload-pack: not our ref and could not fetch ... from promisor remote.
| * @param {string} output - Combined stdout/stderr from the failed git command. | ||
| * @returns {boolean} True when the failure looks like a partial-clone object fetch failure. | ||
| */ | ||
| function isPartialCloneObjectFailure(output) { |
There was a problem hiding this comment.
[/tdd] isPartialCloneObjectFailure is the load-bearing classifier gate for this entire recovery path, yet it has no isolated unit tests — only the integration tests exercise it (implicitly, via two specific strings).
If git ever changes its error message format, or if a new pattern is added, there is no fast-failing unit test to catch the regression.
💡 Suggested test structure
describe('isPartialCloneObjectFailure', () => {
// Positive cases — should return true
it.each([
['could not fetch OID from promisor remote',
'fatal: could not fetch 4f0af081abc from promisor remote'],
['upload-pack: not our ref',
'fatal: remote error: upload-pack: not our ref 0035eb55'],
['remote error: upload-pack',
'error: remote error: upload-pack: filter not supported'],
['unable to read SHA',
'fatal: unable to read tree 1234567890abcdef1234567890abcdef12345678'],
['fatal: bad object SHA',
'fatal: bad object 1234567890abcdef1234567890abcdef12345678'],
])('returns true for: %s', (_, output) => {
expect(isPartialCloneObjectFailure(output)).toBe(true);
});
// Negative cases — should return false
it.each([
['merge conflict', 'CONFLICT (content): Merge conflict in file.txt'],
['empty string', ''],
['null', null],
])('returns false for: %s', (_, output) => {
expect(isPartialCloneObjectFailure(output)).toBe(false);
});
});This also serves as an executable specification of what git messages the function intentionally handles.
| expect(githubClient.graphql).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("should recover from a partial-clone object failure by backfilling the exact commit objects and retrying the rebase", async () => { |
There was a problem hiding this comment.
[/tdd] The new tests cover two of the three branches in the recovery flow, but the third branch — recovered === true yet the retry rebase still fails (lines 460-471 of push_signed_commits.cjs) — is untested. This path produces a distinct error message ("even after backfilling") and has its own rebase --abort call.
💡 Sketch of missing test
it('should throw "even after backfilling" when backfill succeeds but retry rebase also fails', async () => {
// ... set up workDir with a conflicting change ...
let rebaseAttempts = 0;
global.exec = {
getExecOutput: async (program, args, opts = {}) => {
if (program === 'git' && args[0] === 'rebase' && args[1] === '--onto') {
rebaseAttempts++;
// First attempt: simulate promisor failure
if (rebaseAttempts === 1) {
return { exitCode: 128, stdout: '', stderr: 'fatal: could not fetch 0035eb55... from promisor remote' };
}
// Second attempt: simulate a conflict (or another failure)
return { exitCode: 1, stdout: 'CONFLICT (content): Merge conflict in file.txt', stderr: '' };
}
if (program === 'git' && args[0] === 'fetch' && args.includes('--no-filter')) {
return { exitCode: 0, stdout: '', stderr: '' };
}
return realExec.getExecOutput(program, args, opts);
},
exec: realExec.exec,
};
await expect(pushSignedCommits({ ... }))
.rejects.toThrow('even after backfilling the required commit objects');
expect(rebaseAttempts).toBe(2);
});| } catch { | ||
| // If HEAD cannot be resolved, fall back to the known range anchors. | ||
| } | ||
| const fetchTargets = [firstGraphqlParentOid, firstReplayParentOid, headOid].filter(sha => typeof sha === "string" && sha.length > 0); |
There was a problem hiding this comment.
[/diagnose] The fetchTargets filter uses sha.length > 0 while backfillCommitObjects re-validates to /^[0-9a-f]{40}$/i — two different gates for the same invariant.
If headOid ever resolves to an abbreviated SHA (or any non-40-char string), the outer filter passes it through but the inner one discards it silently. If ALL targets end up discarded, backfillCommitObjects returns false with no log and the caller throws a misleading "could not be backfilled" error.
💡 Suggested fix
Reuse the OID_PATTERN already defined at the top of the file for the outer filter so both layers are consistent:
// before
const fetchTargets = [firstGraphqlParentOid, firstReplayParentOid, headOid]
.filter(sha => typeof sha === "string" && sha.length > 0);
// after
const fetchTargets = [firstGraphqlParentOid, firstReplayParentOid, headOid]
.filter(sha => typeof sha === "string" && OID_PATTERN.test(sha));This catches a short/corrupt SHA early, before the backfill attempt, and produces a clearer error if fetchTargets ends up empty.
| async function backfillCommitObjects(execApi, commitShas, options = {}) { | ||
| const targets = [...new Set((commitShas || []).filter(sha => typeof sha === "string" && /^[0-9a-f]{40}$/i.test(sha)))]; | ||
| if (targets.length === 0) { | ||
| return false; |
There was a problem hiding this comment.
[/diagnose] When targets.length === 0, the function returns false silently with no log entry. The caller in push_signed_commits.cjs then throws a "could not be backfilled" error with no indication that the real problem was an empty target list (e.g. all SHAs failed the 40-char hex validation).
💡 Suggested fix
if (targets.length === 0) {
core.warning('backfillCommitObjects: no valid 40-char SHA targets provided; targeted fetch skipped');
return false;
}This makes the empty-list scenario immediately visible in run logs rather than surfacing as a confusing "could not be backfilled" error downstream.
| if (exitCode !== 0) { | ||
| core.warning(`backfillCommitObjects: targeted fetch exited with code ${exitCode}: ${String(stderr || "").trim()}`); | ||
| } | ||
| return exitCode === 0; |
There was a problem hiding this comment.
[/diagnose] There is no log entry on the success path — when the targeted fetch completes with exitCode === 0 the function returns true silently. This makes it hard to confirm in run logs that the backfill actually happened vs. that the recovery path was never triggered.
💡 Suggested fix
if (exitCode !== 0) {
core.warning(`backfillCommitObjects: targeted fetch exited with code ${exitCode}: ${String(stderr || '').trim()}`);
} else {
core.info(`backfillCommitObjects: successfully fetched object content for ${targets.length} anchor commit(s) from origin`);
}
return exitCode === 0;A core.info on success gives operators a clear, searchable confirmation in the Actions log that the recovery fetch ran and succeeded.
| // those on-demand promisor fetches can authenticate in private repos even | ||
| // after checkout credentials have been scrubbed from .git/config. | ||
| const runRebase = async () => { | ||
| const result = await exec.getExecOutput("git", ["rebase", "--onto", firstGraphqlParentOid, firstReplayParentOid, "HEAD"], { cwd, env: { ...process.env, ...(gitAuthEnv || {}) }, ignoreReturnCode: true }); |
There was a problem hiding this comment.
[/zoom-out] Threading gitAuthEnv into the rebase exec looks like a secondary but independent correctness fix — the original code passed no env override, so in private repos where checkout credentials are scrubbed before this point, the rebase's own promisor lazy-fetches would have failed with auth errors that none of the five classifier patterns would match. Worth calling out explicitly in the PR description (and perhaps with a separate commit) so reviewers understand the two orthogonal improvements: (1) recovery from promisor object failures, and (2) ensuring the rebase step itself has auth context for promisor fetches.
No code change needed — just a documentation/narrative suggestion.
| const realExec = makeRealExec(workDir); | ||
| let rebaseAttempts = 0; | ||
| let backfillTargets = null; | ||
| global.exec = { |
There was a problem hiding this comment.
[/tdd] global.exec is overwritten directly and never restored. If an earlier assertion in this test throws (e.g. the pushSignedCommits call rejects unexpectedly), the global remains polluted for subsequent tests in the same suite.
💡 Suggested fix
Save and restore around the mutation:
const originalExec = global.exec;
try {
global.exec = { getExecOutput: ..., exec: realExec.exec };
// ... test body ...
} finally {
global.exec = originalExec;
}Or, if the suite already has a beforeEach/afterEach that resets global.exec to a default mock, a comment documenting that would make the test easier to reason about in isolation.
| expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("backfilling object content")); | ||
| }); | ||
|
|
||
| it("should not attempt history backfill for a genuine merge conflict", async () => { |
There was a problem hiding this comment.
[/tdd] The conflict test verifies backfillAttempted === false for --no-filter, --unshallow, and --deepen fetch variants, which is solid. But the test doesn't directly verify that isPartialCloneObjectFailure returns false for real conflict output — it only proves the integrated behaviour works for this one scenario.
If a future pattern addition to isPartialCloneObjectFailure inadvertently matches merge-conflict text, the integration test would catch it, but only for this specific conflict message. A dedicated unit test for the classifier (see the comment on line 37) would catch regressions more precisely and without needing a full integration run.
fix(push_signed_commits): recover from shallow/partial-clone object failures during rebase
Summary
Fixes a class of transient failures in
push_signed_commitswheregit rebase --ontoexits with object-missing errors in shallow + partial-clone environments (e.g.,partial clone filter,missing object,object file is empty). Previously these errors were either silent or surfaced as opaque rebase failures. This PR introduces a bounded, exact-commit SHA backfill to recover the missing tree/blob objects without triggering an uncontrolled--unshallowor unbounded depth increase, then retries the rebase once before surfacing a human-actionable error for genuine conflicts.Problem
In shallow + partial-clone checkouts,
git rebase --ontocan fail because the rebase engine needs tree or blob objects for anchor commits that were not fetched by the partial clone filter. These failures are recoverable — the missing objects exist on the remote — but the old code had no detection or retry logic, causing the entire rebase to fail with a confusing error. Usinggit fetch --unshallowas a recovery strategy is unsafe because it can pull arbitrarily large history, exceeding time and storage budgets.Solution
Three coordinated changes across the action's JavaScript helpers:
git_helpers.cjs— new exportedbackfillCommitObjects(sha[])function that runsgit fetch --no-filter origin <sha>...targeting exactly the anchor commit SHAs needed, keeping the fetch strictly bounded.push_signed_commits.cjs— newisPartialCloneObjectFailure(stderr)classifier identifies recoverable object errors. Thegit rebase --ontofailure path now:backfillCommitObjectswith the exact anchor SHAs.gitAuthEnvforwarded so on-demand promisor fetches authenticate correctly.push_signed_commits.test.cjs— two new integration tests:Files Changed
actions/setup/js/git_helpers.cjsbackfillCommitObjectsexportactions/setup/js/push_signed_commits.cjsactions/setup/js/push_signed_commits.test.cjsKey Design Decisions
--unshallow: Recovery is scoped to exactly the SHA(s) needed for the current rebase anchor, not the full history. This avoids unbounded fetches.gitAuthEnvis explicitly passed to the retried rebase invocation so on-demand promisor fetches in the retry can authenticate against the remote.isPartialCloneObjectFailurematches only known object-missing stderr patterns specific to shallow/partial-clone failures, so genuine conflict errors are never silently retried.Testing
Commits
c35ea041b85aba19d7bd416d960c7fe9016Risk & Rollout
push_signed_commitsaction; no shared infrastructure or schema changes.