Skip to content

Commit ca24a22

Browse files
Clone from snapshot (#582)
* Clone from snapshot * Update cloud.mdx * Split new step * Update clone-repo.ts * Update v6.mdx * Misc fixes * Update create-sandbox.ts * Update configure-git.ts * Update clone-repo.ts
1 parent c150f77 commit ca24a22

File tree

9 files changed

+161
-161
lines changed

9 files changed

+161
-161
lines changed

apps/app/app/api/cron/lint/lint-repo.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { configureGit } from "@/lib/steps/configure-git";
77
import { createBranch } from "@/lib/steps/create-branch";
88
import { createLintRun } from "@/lib/steps/create-lint-run";
99
import { createPullRequest } from "@/lib/steps/create-pr";
10+
import { cloneRepo } from "@/lib/steps/clone-repo";
1011
import { createSandbox } from "@/lib/steps/create-sandbox";
1112
import { extendSandbox } from "@/lib/steps/extend-sandbox";
1213
import { fixLint } from "@/lib/steps/fix-lint";
@@ -74,12 +75,14 @@ export async function lintRepoWorkflow(params: LintRepoParams): Promise<void> {
7475
// Get GitHub access token
7576
const token = await getGitHubToken(installationId);
7677

77-
// Create sandbox with the repo (returns sandbox ID for serialization)
78-
const sandboxId = await createSandbox(repoFullName, token);
78+
// Create sandbox (returns sandbox ID for serialization)
79+
const sandboxId = await createSandbox();
7980

8081
let result: LintStepResult;
8182

8283
try {
84+
// Clone the repo into the sandbox
85+
await cloneRepo(sandboxId, repoFullName, token);
8386
// Install dependencies
8487
await installDependencies(sandboxId);
8588

apps/app/app/api/github/webhooks/review-pr.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { checkPushAccess } from "@/lib/steps/check-push-access";
55
import { checkoutBranch } from "@/lib/steps/checkout-branch";
66
import { commitAndPush } from "@/lib/steps/commit-and-push";
77
import { configureGit } from "@/lib/steps/configure-git";
8+
import { cloneRepo } from "@/lib/steps/clone-repo";
89
import { createSandbox } from "@/lib/steps/create-sandbox";
910
import { extendSandbox } from "@/lib/steps/extend-sandbox";
1011
import { fixLint } from "@/lib/steps/fix-lint";
@@ -81,10 +82,12 @@ Please ensure the Ultracite app has write access to this repository and branch.
8182
// Get GitHub access token
8283
const token = await getGitHubToken(installationId);
8384

84-
// Create sandbox with the repo (returns sandbox ID for serialization)
85-
const sandboxId = await createSandbox(repoFullName, token);
85+
// Create sandbox (returns sandbox ID for serialization)
86+
const sandboxId = await createSandbox();
8687

8788
try {
89+
// Clone the repo into the sandbox
90+
await cloneRepo(sandboxId, repoFullName, token);
8891
// Checkout the PR branch
8992
await checkoutBranch(sandboxId, prBranch);
9093

apps/app/lib/steps/clone-repo.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Sandbox } from "@vercel/sandbox";
2+
import { parseError } from "@/lib/error";
3+
4+
export async function cloneRepo(
5+
sandboxId: string,
6+
repoFullName: string,
7+
token: string
8+
): Promise<void> {
9+
"use step";
10+
11+
let sandbox: Sandbox | null = null;
12+
13+
try {
14+
sandbox = await Sandbox.get({ sandboxId });
15+
} catch (error) {
16+
throw new Error(
17+
`[cloneRepo] Failed to get sandbox: ${parseError(error)}`
18+
);
19+
}
20+
21+
try {
22+
const result = await sandbox.runCommand("git", [
23+
"clone",
24+
"--depth",
25+
"1",
26+
`https://x-access-token:${token}@github.com/${repoFullName}`,
27+
".",
28+
]);
29+
30+
if (result.exitCode !== 0) {
31+
const output = await result.output("both");
32+
const sanitized = output.replaceAll(token, "***");
33+
throw new Error(
34+
`git clone failed with exit code ${result.exitCode}: ${sanitized.trim()}`
35+
);
36+
}
37+
} catch (error) {
38+
throw new Error(`Failed to clone repo: ${parseError(error)}`);
39+
}
40+
}

apps/app/lib/steps/configure-git.ts

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@ export async function configureGit(
1818
);
1919
}
2020

21-
const name = "Ultracite";
22-
const email = "ultracite@users.noreply.github.com";
23-
24-
// Configure remote URL with authentication token
21+
// Git user.name, user.email, and core.hooksPath are pre-configured in the sandbox snapshot.
22+
// The authenticated remote URL and local hooks override are per-run.
2523
const authenticatedUrl = `https://x-access-token:${token}@github.com/${repoFullName}.git`;
2624

2725
try {
@@ -31,27 +29,16 @@ export async function configureGit(
3129
"origin",
3230
authenticatedUrl,
3331
]);
34-
} catch (error) {
35-
throw new Error(`Failed to set remote URL: ${parseError(error)}`);
36-
}
37-
38-
try {
39-
await sandbox.runCommand("git", ["config", "user.email", email]);
40-
} catch (error) {
41-
throw new Error(`Failed to configure git email: ${parseError(error)}`);
42-
}
4332

44-
try {
45-
await sandbox.runCommand("git", ["config", "user.name", name]);
46-
} catch (error) {
47-
throw new Error(`Failed to configure git name: ${parseError(error)}`);
48-
}
49-
50-
// Disable all git hooks in the sandbox to prevent TTY-dependent hooks
51-
// (like prepare-commit-msg) from failing in non-interactive environments
52-
try {
53-
await sandbox.runCommand("git", ["config", "core.hooksPath", "/dev/null"]);
33+
// Disable hooks locally to override any hooks installed by package managers
34+
// (e.g. husky's prepare script sets core.hooksPath to .husky/)
35+
await sandbox.runCommand("git", [
36+
"config",
37+
"--local",
38+
"core.hooksPath",
39+
"/dev/null",
40+
]);
5441
} catch (error) {
55-
throw new Error(`Failed to disable git hooks: ${parseError(error)}`);
42+
throw new Error(`Failed to configure git: ${parseError(error)}`);
5643
}
5744
}

apps/app/lib/steps/create-sandbox.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,16 @@ import { parseError } from "@/lib/error";
33

44
const FIVE_MINUTES_MS = 5 * 60 * 1000;
55

6-
export async function createSandbox(
7-
repoFullName: string,
8-
token: string
9-
): Promise<string> {
6+
export async function createSandbox(): Promise<string> {
107
"use step";
118

129
let sandbox: Sandbox | null = null;
1310

1411
try {
1512
sandbox = await Sandbox.create({
1613
source: {
17-
type: "git",
18-
url: `https://github.com/${repoFullName}`,
19-
username: "x-access-token",
20-
password: token,
21-
depth: 1,
14+
type: "snapshot",
15+
snapshotId: "snap_C7bRk0eKocK3L8QqZsNIz54Cwwk7",
2216
},
2317
timeout: FIVE_MINUTES_MS,
2418
});

apps/app/lib/steps/install-dependencies.ts

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,49 +14,8 @@ export async function installDependencies(sandboxId: string): Promise<void> {
1414
);
1515
}
1616

17-
// Install ni and Claude Code separately so one failure doesn't block the other
18-
for (const pkg of ["@antfu/ni", "@anthropic-ai/claude-code"]) {
19-
const result = await sandbox
20-
.runCommand("npm", ["install", "-g", pkg])
21-
.catch((error: unknown) => {
22-
throw new Error(`Failed to install ${pkg}: ${parseError(error)}`);
23-
});
24-
25-
if (result.exitCode !== 0) {
26-
const output = await result.output("both");
27-
throw new Error(
28-
`Failed to install ${pkg} (exit code ${result.exitCode}): ${output.trim()}`
29-
);
30-
}
31-
}
32-
33-
// Detect the package manager using `ni -v`
34-
const result = await sandbox
35-
.runCommand("ni", ["-v"])
36-
.catch((error: unknown) => {
37-
throw new Error(`Failed to detect package manager: ${parseError(error)}`);
38-
});
39-
40-
const output = await result.stdout();
41-
42-
// Parse output to find which package manager is used (pnpm, yarn, or bun)
43-
const packageManagers = ["pnpm", "yarn", "bun"];
44-
const detectedManager = packageManagers.find((pm) =>
45-
output.split("\n").some((line) => line.startsWith(pm))
46-
);
47-
48-
// Install the detected package manager if needed (npm is already available)
49-
if (detectedManager) {
50-
try {
51-
await sandbox.runCommand("npm", ["install", "-g", detectedManager]);
52-
} catch (error) {
53-
throw new Error(
54-
`Failed to install ${detectedManager}: ${parseError(error)}`
55-
);
56-
}
57-
}
58-
59-
// Use `ni` to install project dependencies
17+
// Global tools (ni, claude-code, package managers) are pre-installed
18+
// in the sandbox snapshot. Only project dependencies need installing.
6019
try {
6120
await sandbox.runCommand("ni", []);
6221
} catch (error) {

apps/docs/cloud.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ In the dashboard, enable the repositories you want Ultracite to lint. Once enabl
6060

6161
All linting and fixes run in isolated sandbox environments. Your code is cloned, processed, and the sandbox is destroyed — we never store your source code.
6262

63+
The sandbox is created from a snapshot with a simple, pre-configured environment which helps saves a few precious seconds on each run.
64+
65+
```sh
66+
# Global tools
67+
npm install -g @antfu/ni @anthropic-ai/claude-code pnpm yarn bun
68+
69+
# Git config (global so it applies to all cloned repos)
70+
git config --global user.name "Ultracite"
71+
git config --global user.email "ultracite@users.noreply.github.com"
72+
git config --global core.hooksPath /dev/null
73+
```
74+
6375
### AI-Powered Fixes
6476

6577
When `ultracite fix` can't auto-fix an issue, Ultracite uses Claude Code to understand the context and apply intelligent fixes. This handles complex issues like:

apps/docs/upgrade/v6.mdx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ Ultracite v6 introduces framework-specific presets that give you more control ov
99

1010
v6 introduces [framework-specific presets](/configuration#framework-specific-configurations):
1111

12-
- [`ultracite/core`](/preset/core) - Base JavaScript/TypeScript rules
13-
- [`ultracite/react`](/preset/react) - React-specific rules
14-
- [`ultracite/next`](/preset/next) - Next.js-specific rules
15-
- [`ultracite/solid`](/preset/solid) - Solid.js rules
16-
- [`ultracite/vue`](/preset/vue) - Vue.js rules
17-
- [`ultracite/svelte`](/preset/svelte) - Svelte rules
18-
- [`ultracite/qwik`](/preset/qwik) - Qwik rules
19-
- [`ultracite/angular`](/preset/angular) - Angular HTML parser
20-
- [`ultracite/remix`](/preset/remix) - File-based routing support
12+
- [`ultracite/core`](/configuration#framework-presets) - Base JavaScript/TypeScript rules
13+
- [`ultracite/react`](/configuration#framework-presets) - React-specific rules
14+
- [`ultracite/next`](/configuration#framework-presets) - Next.js-specific rules
15+
- [`ultracite/solid`](/configuration#framework-presets) - Solid.js rules
16+
- [`ultracite/vue`](/configuration#framework-presets) - Vue.js rules
17+
- [`ultracite/svelte`](/configuration#framework-presets) - Svelte rules
18+
- [`ultracite/qwik`](/configuration#framework-presets) - Qwik rules
19+
- [`ultracite/angular`](/configuration#framework-presets) - Angular HTML parser
20+
- [`ultracite/remix`](/configuration#framework-presets) - File-based routing support
2121

2222
## Migration
2323

0 commit comments

Comments
 (0)