Skip to content

Commit c598234

Browse files
V48 (impl-only): Fix Sandbox.create git clone for branch+SHA deposits
Prefer branch shallow clone over depth:1 + bare commit SHA so Vercel Sandbox create no longer fails with bad_request: git clone failed. Evidence still pins the selected commit via sourceRevision; bare-SHA-only paths omit depth.
1 parent 83c01bd commit c598234

2 files changed

Lines changed: 172 additions & 11 deletions

File tree

uapi/lib/deposit-source-provisioning.ts

Lines changed: 113 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,106 @@ export interface DepositInBoxHostResult {
133133
outcome: PipelineHostRunResult["outcome"];
134134
}
135135

136+
/** Full or abbreviated git object id (7–40 hex). */
137+
const GIT_COMMIT_SHA_RE = /^[0-9a-f]{7,40}$/i;
138+
139+
export type DepositSandboxGitRevisionStrategy =
140+
| "branch-shallow"
141+
| "commit-full"
142+
| "ref-shallow";
143+
144+
/**
145+
* Git source for `Sandbox.create` deposit clones.
146+
*
147+
* Vercel Sandbox performs a single git clone at create time. A common failure
148+
* is `bad_request: git clone failed` when `revision` is a commit SHA with
149+
* `depth: 1` — shallow clones fetch advertised refs (branches/tags), not
150+
* arbitrary SHAs (GitHub often rejects unadvertised objects).
151+
*
152+
* Strategy (mirrors LocalHost intent, adapted for create-time-only clone):
153+
* 1. **Branch present** → shallow clone that branch tip (`depth: 1`).
154+
* 2. **Commit SHA only** → full clone at that revision (omit `depth`).
155+
* 3. **Other ref** (tag / symbolic) → shallow clone that ref.
156+
*
157+
* Evidence still records the exact `commit` via `sourceRevision`; only the
158+
* create-time clone ref is adjusted for Vercel reachability.
159+
*/
160+
export function buildDepositSandboxGitSource(input: {
161+
repositoryFullName: string;
162+
/** Preferred product revision (often the selected commit SHA). */
163+
revision: string;
164+
branch: string | null;
165+
commit: string | null;
166+
token?: string;
167+
}): {
168+
source: {
169+
type: "git";
170+
url: string;
171+
revision: string;
172+
username?: string;
173+
password?: string;
174+
depth?: number;
175+
};
176+
strategy: DepositSandboxGitRevisionStrategy;
177+
/** Ref actually passed to Sandbox.create (branch, SHA, or other). */
178+
cloneRevision: string;
179+
depth: number | null;
180+
} {
181+
const branch = input.branch?.trim() || "";
182+
const commit = (input.commit || "").trim();
183+
const revision = (input.revision || "").trim();
184+
const url = `https://github.com/${input.repositoryFullName}.git`;
185+
const auth =
186+
input.token
187+
? { username: "x-access-token" as const, password: input.token }
188+
: {};
189+
190+
// Prefer a non-SHA branch name even when revision/commit is a full SHA.
191+
if (branch && !GIT_COMMIT_SHA_RE.test(branch)) {
192+
return {
193+
source: {
194+
type: "git",
195+
url,
196+
revision: branch,
197+
depth: 1,
198+
...auth,
199+
},
200+
strategy: "branch-shallow",
201+
cloneRevision: branch,
202+
depth: 1,
203+
};
204+
}
205+
206+
const effective = commit || revision || "HEAD";
207+
if (GIT_COMMIT_SHA_RE.test(effective)) {
208+
// Omit depth: shallow + bare SHA is the create failure mode we hit in prod.
209+
return {
210+
source: {
211+
type: "git",
212+
url,
213+
revision: effective,
214+
...auth,
215+
},
216+
strategy: "commit-full",
217+
cloneRevision: effective,
218+
depth: null,
219+
};
220+
}
221+
222+
return {
223+
source: {
224+
type: "git",
225+
url,
226+
revision: effective,
227+
depth: 1,
228+
...auth,
229+
},
230+
strategy: "ref-shallow",
231+
cloneRevision: effective,
232+
depth: 1,
233+
};
234+
}
235+
136236
/**
137237
* Run the deposit synthesis IN the sandbox box (#25). Builds an asset-pack host in
138238
* DEPOSIT mode (git source for the revision + steering), dispatches it on the sandbox
@@ -160,6 +260,13 @@ export async function runDepositInBoxHost(input: {
160260
shouldAbort?: () => boolean | Promise<boolean>;
161261
hostFactory?: () => Promise<DepositInBoxHost>;
162262
}): Promise<DepositInBoxHostResult> {
263+
const gitSource = buildDepositSandboxGitSource({
264+
repositoryFullName: input.repositoryFullName,
265+
revision: input.revision,
266+
branch: input.branch,
267+
commit: input.commit,
268+
token: input.token,
269+
});
163270
const plan = buildAssetPackSandboxHostPlan({
164271
mode: "asset_pack_pipeline",
165272
synthesizeMode: "deposit",
@@ -176,14 +283,7 @@ export async function runDepositInBoxHost(input: {
176283
branch: input.branch || "main",
177284
commit: input.commit || input.revision,
178285
},
179-
source: {
180-
type: "git",
181-
url: `https://github.com/${input.repositoryFullName}.git`,
182-
revision: input.revision,
183-
username: input.token ? "x-access-token" : undefined,
184-
password: input.token,
185-
depth: 1,
186-
},
286+
source: gitSource.source,
187287
depositSteering: {
188288
obfuscations: input.obfuscations,
189289
forcedExclusions: input.forcedExclusions,
@@ -212,6 +312,11 @@ export async function runDepositInBoxHost(input: {
212312
hasGitSource: Boolean(plan.createOptions.source),
213313
synthesizeMode: plan.manifest.synthesizeMode ?? null,
214314
repositoryFullName: input.repositoryFullName,
315+
gitRevisionStrategy: gitSource.strategy,
316+
gitCloneRevision: gitSource.cloneRevision,
317+
gitDepth: gitSource.depth,
318+
sourceCommit: input.commit || null,
319+
sourceBranch: input.branch || null,
215320
};
216321
bitcodeServerTelemetry("info", "deposit-sandbox-host", "plan-ready", createSummary);
217322

uapi/tests/lib/depositSourceProvisioning.test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @jest-environment node
33
*/
44
import {
5+
buildDepositSandboxGitSource,
56
createDepositLocalHostCloneForRun,
67
provisionDepositCheckout,
78
provisionDepositSourceInventory,
@@ -181,6 +182,51 @@ describe('resolveDepositPipelineHost', () => {
181182
});
182183
});
183184

185+
describe('buildDepositSandboxGitSource', () => {
186+
it('prefers branch shallow clone when branch is present (even if revision is a SHA)', () => {
187+
const built = buildDepositSandboxGitSource({
188+
repositoryFullName: 'advancedengineeredsoftware/Bitcode',
189+
revision: 'f956577ce478e90d629db48c452102e582fa081c',
190+
branch: 'version/v48',
191+
commit: 'f956577ce478e90d629db48c452102e582fa081c',
192+
token: 'ghs_tok',
193+
});
194+
expect(built.strategy).toBe('branch-shallow');
195+
expect(built.source).toMatchObject({
196+
type: 'git',
197+
url: 'https://github.com/advancedengineeredsoftware/Bitcode.git',
198+
revision: 'version/v48',
199+
depth: 1,
200+
username: 'x-access-token',
201+
password: 'ghs_tok',
202+
});
203+
});
204+
205+
it('omits depth for bare commit SHAs (avoid shallow unadvertised-object failure)', () => {
206+
const built = buildDepositSandboxGitSource({
207+
repositoryFullName: 'o/r',
208+
revision: 'f956577ce478e90d629db48c452102e582fa081c',
209+
branch: null,
210+
commit: 'f956577ce478e90d629db48c452102e582fa081c',
211+
});
212+
expect(built.strategy).toBe('commit-full');
213+
expect(built.source.revision).toBe('f956577ce478e90d629db48c452102e582fa081c');
214+
expect(built.source.depth).toBeUndefined();
215+
expect(built.depth).toBeNull();
216+
});
217+
218+
it('shallow-clones non-SHA refs (tags / symbolic)', () => {
219+
const built = buildDepositSandboxGitSource({
220+
repositoryFullName: 'o/r',
221+
revision: 'v1.2.3',
222+
branch: null,
223+
commit: null,
224+
});
225+
expect(built.strategy).toBe('ref-shallow');
226+
expect(built.source).toMatchObject({ revision: 'v1.2.3', depth: 1 });
227+
});
228+
});
229+
184230
describe('runDepositInBoxHost (#25)', () => {
185231
it('dispatches a deposit-mode host and returns evidence.depositOptions', async () => {
186232
let receivedPlan: any;
@@ -199,9 +245,9 @@ describe('runDepositInBoxHost (#25)', () => {
199245
};
200246
const result = await runDepositInBoxHost({
201247
repositoryFullName: 'engineeredsoftware/demo',
202-
revision: 'abc123',
248+
revision: 'abc123def456',
203249
branch: 'main',
204-
commit: 'abc123',
250+
commit: 'abc123def456',
205251
token: 'ghs_tok',
206252
obfuscations: 'hide internal names',
207253
forcedExclusions: ['secret/'],
@@ -215,7 +261,17 @@ describe('runDepositInBoxHost (#25)', () => {
215261
// The dispatched plan ran the deposit lens in-box, with a git source + steering.
216262
expect(receivedPlan.manifest.synthesizeMode).toBe('deposit');
217263
expect(receivedPlan.manifest.depositSteering).toMatchObject({ forcedExclusions: ['secret/'] });
218-
expect(receivedPlan.createOptions.source).toMatchObject({ type: 'git', revision: 'abc123' });
264+
// Clone uses the branch (shallow), not the bare SHA — avoids Sandbox create git-clone 400.
265+
expect(receivedPlan.createOptions.source).toMatchObject({
266+
type: 'git',
267+
revision: 'main',
268+
depth: 1,
269+
});
270+
// Evidence still pins the selected commit.
271+
expect(receivedPlan.manifest.sourceRevision).toMatchObject({
272+
branch: 'main',
273+
commit: 'abc123def456',
274+
});
219275
// Vercel Sandbox v2: persistence is default — deposit must opt out.
220276
expect(receivedPlan.createOptions.persistent).toBe(false);
221277
expect(typeof receivedPlan.createOptions.name).toBe('string');

0 commit comments

Comments
 (0)