mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Run generated worktree cleanup during automatic dev maintenance (#33277)
This commit is contained in:
@@ -60,7 +60,7 @@ export async function runReclaimCommand(
|
||||
'Add --tests-stopped to include old test caches after stopping cross-version tests.\n'
|
||||
)
|
||||
output.write(
|
||||
'Manual cleanup includes old ignored dependencies and build output in sibling worktrees. Stop their apps, agents, installs, builds and tests before applying. Shared pnpm stores are retained.\n'
|
||||
'Cleanup includes old ignored dependencies and build output in sibling worktrees, including during automatic dev cleanup. Shared pnpm stores are retained.\n'
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export async function runReclaimCommand(
|
||||
}
|
||||
if (!options.automatic) {
|
||||
output.write(
|
||||
'Manual cleanup includes old dependency installs and build output in sibling worktrees. Stop their apps, agents, installs, builds and tests before applying.\n'
|
||||
'Cleanup includes old dependency installs and build output in sibling worktrees. Shared pnpm stores are retained.\n'
|
||||
)
|
||||
}
|
||||
await reclaim(options)
|
||||
|
||||
@@ -137,7 +137,6 @@ async function makeDirectoriesWritable(root) {
|
||||
export async function reclaimWorktreeGeneratedFiles({
|
||||
repoRoot = process.cwd(),
|
||||
apply = false,
|
||||
automatic = false,
|
||||
generatedMinAgeDays = 30,
|
||||
excludeWorktree,
|
||||
report = console.log
|
||||
@@ -145,9 +144,6 @@ export async function reclaimWorktreeGeneratedFiles({
|
||||
if (!Number.isFinite(generatedMinAgeDays) || generatedMinAgeDays < 0) {
|
||||
throw new Error('--generated-min-age-days requires a non-negative number')
|
||||
}
|
||||
if (automatic) {
|
||||
throw new Error('Generated dependency cleanup is only available in the manual reclaim command')
|
||||
}
|
||||
const protectedPaths = await Promise.all(
|
||||
[repoRoot, process.cwd(), ...(excludeWorktree ? [excludeWorktree] : [])].map((p) => realpath(p))
|
||||
)
|
||||
|
||||
@@ -132,15 +132,6 @@ it('retains recent directories, tracked output, and ignored files outside recogn
|
||||
}
|
||||
})
|
||||
|
||||
it('never permits automatic dependency cleanup', async () => {
|
||||
const f = await fixture()
|
||||
const file = await generated(f.sibling)
|
||||
await expect(
|
||||
reclaimWorktreeGeneratedFiles({ repoRoot: f.repoRoot, apply: true, automatic: true })
|
||||
).rejects.toThrow('manual reclaim command')
|
||||
expect(await readFile(file, 'utf8')).toBe('generated bytes')
|
||||
})
|
||||
|
||||
it('protects tracked files under package names containing Git pathspec metacharacters', async () => {
|
||||
const f = await fixture()
|
||||
const root = join(f.sibling, 'native', 'addon[1]')
|
||||
@@ -304,28 +295,42 @@ it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)(
|
||||
}
|
||||
)
|
||||
|
||||
it('includes generated cleanup in the default manual CLI preview and apply', async () => {
|
||||
const f = await fixture()
|
||||
const file = await generated(f.sibling, 'mobile/node_modules')
|
||||
const run = (args) =>
|
||||
runProcess({
|
||||
program: process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath,
|
||||
args: [
|
||||
resolve(import.meta.dirname, 'reclaim-worktree-space.mjs'),
|
||||
'--repo',
|
||||
f.repoRoot,
|
||||
...args
|
||||
],
|
||||
cwd: tmpdir(),
|
||||
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
|
||||
timeoutMs: 30_000,
|
||||
terminationBarrier: true
|
||||
})
|
||||
const preview = await run([])
|
||||
expect(preview.code, preview.stderr).toBe(0)
|
||||
expect(preview.stdout).toContain('would remove generated directory')
|
||||
expect(await readFile(file, 'utf8')).toBe('generated bytes')
|
||||
const applied = await run(['--apply'])
|
||||
expect(applied.code, applied.stderr).toBe(0)
|
||||
await expect(lstat(file)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
})
|
||||
it.each(['manual', 'automatic'])(
|
||||
'includes generated cleanup in %s CLI preview and apply',
|
||||
async (mode) => {
|
||||
const f = await fixture()
|
||||
const protectedFile = await generated(f.repoRoot)
|
||||
const file = await generated(f.sibling, 'mobile/node_modules')
|
||||
const store = join(f.root, 'global-pnpm-store')
|
||||
await mkdir(store)
|
||||
const shared = join(store, 'package.js')
|
||||
await writeFile(shared, 'shared package')
|
||||
await link(shared, join(dirname(file), 'package.js'))
|
||||
await symlink(store, join(dirname(file), 'shared-package'), 'junction')
|
||||
await utimes(dirname(file), new Date(0), new Date(0))
|
||||
const run = (args) =>
|
||||
runProcess({
|
||||
program: process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath,
|
||||
args: [
|
||||
resolve(import.meta.dirname, 'reclaim-worktree-space.mjs'),
|
||||
'--repo',
|
||||
f.repoRoot,
|
||||
...(mode === 'automatic' ? ['--automatic'] : []),
|
||||
...args
|
||||
],
|
||||
cwd: tmpdir(),
|
||||
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
|
||||
timeoutMs: 30_000,
|
||||
terminationBarrier: true
|
||||
})
|
||||
const preview = await run([])
|
||||
expect(preview.code, preview.stderr).toBe(0)
|
||||
expect(preview.stdout).toContain('would remove generated directory')
|
||||
expect(await readFile(file, 'utf8')).toBe('generated bytes')
|
||||
const applied = await run(['--apply'])
|
||||
expect(applied.code, applied.stderr).toBe(0)
|
||||
await expect(lstat(file)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
expect(await readFile(protectedFile, 'utf8')).toBe('generated bytes')
|
||||
expect(await readFile(shared, 'utf8')).toBe('shared package')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -50,14 +50,12 @@ export async function reclaimWorktreeSpace({
|
||||
const scripts = [
|
||||
'reclaim-electron-dists.mjs',
|
||||
'reclaim-dev-electron-bundles.mjs',
|
||||
'reclaim-vite-caches.mjs'
|
||||
'reclaim-vite-caches.mjs',
|
||||
'reclaim-worktree-generated-files.mjs'
|
||||
]
|
||||
if (!automatic && testsStopped) {
|
||||
scripts.unshift('reclaim-release-test-caches.mjs')
|
||||
}
|
||||
if (!automatic) {
|
||||
scripts.push('reclaim-worktree-generated-files.mjs')
|
||||
}
|
||||
for (const script of scripts) {
|
||||
const args = [resolve(import.meta.dirname, script), '--repo', repoRoot]
|
||||
if (apply) {
|
||||
|
||||
@@ -52,7 +52,7 @@ it('applies Electron, Vite and generated cleanup while retaining unacknowledged
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves historical caches out of automatic invocations', async () => {
|
||||
it('includes generated cleanup and leaves historical test caches out of automatic invocations', async () => {
|
||||
const options = {
|
||||
...(await fixture()),
|
||||
automatic: true,
|
||||
@@ -61,11 +61,11 @@ it('leaves historical caches out of automatic invocations', async () => {
|
||||
excludeWorktree: '/dev-worktree'
|
||||
}
|
||||
await reclaimWorktreeSpace(options)
|
||||
expect(options.execute).toHaveBeenCalledTimes(3)
|
||||
expect(options.execute).toHaveBeenCalledTimes(4)
|
||||
expect(options.execute.mock.calls.flatMap(([spec]) => spec.args).join(' ')).not.toContain(
|
||||
'reclaim-release-test-caches'
|
||||
)
|
||||
expect(options.execute.mock.calls.flatMap(([spec]) => spec.args).join(' ')).not.toContain(
|
||||
expect(options.execute.mock.calls.flatMap(([spec]) => spec.args).join(' ')).toContain(
|
||||
'reclaim-worktree-generated-files'
|
||||
)
|
||||
for (const [spec] of options.execute.mock.calls) {
|
||||
|
||||
@@ -23,7 +23,7 @@ It shares complete Electron distributions through the repository cache and remov
|
||||
`out/electron-dev` bundles, which development rebuilds on demand. It also removes tracked
|
||||
`.cache/vite` dependency caches unused for 30 days. With `--tests-stopped`, it also
|
||||
removes stamped historical test checkouts and shared seeds older than 30 days; tests regenerate them when needed. It leaves source files,
|
||||
worktree registrations and branches intact. The manual command also includes old dependency
|
||||
worktree registrations and branches intact. Manual and automatic cleanup also include old dependency
|
||||
installs and build output as described below. Reported sizes are
|
||||
logical file data; APFS clones and hardlinks mean actual disk savings can be smaller.
|
||||
|
||||
@@ -47,22 +47,22 @@ anything on SSH hosts. Run maintenance on the execution host when maintaining re
|
||||
|
||||
## Old dependency installs and build output
|
||||
|
||||
Old generated files are included in the normal manual command. Stop apps, agents, package
|
||||
installs, builds and tests in the repository's sibling worktrees before applying:
|
||||
Old generated files are included in the normal manual command and automatic dev cleanup:
|
||||
|
||||
```sh
|
||||
pnpm reclaim-space
|
||||
pnpm reclaim-space --apply
|
||||
```
|
||||
|
||||
This manual pass retains the selected repository worktree, the invoking worktree, and any
|
||||
This pass retains the selected repository worktree, the invoking worktree, and any
|
||||
`--exclude-worktree` directory. It removes ignored, completely untracked `node_modules`, `out`,
|
||||
`dist`, and `build` directories older than 30 days. Recognized package roots are the repository
|
||||
root, `mobile`, `native/*`, `src/packages/*`, `cloud/apps/*`, and `cloud/packages/*`, each with a
|
||||
regular `package.json`. It does not search arbitrary folders or the user's home directory.
|
||||
Use `--generated-min-age-days 14` to shorten retention, or `0` to include recent generated
|
||||
directories. Age is the directory modification time, not proof that an install is unused;
|
||||
stop sibling work before applying even when its dependencies were installed long ago.
|
||||
directories. Age is the directory modification time, not proof that an install is unused.
|
||||
Old sibling installs and output can be removed while sibling work is running; reinstall
|
||||
dependencies or rebuild in that worktree if needed. Shared pnpm package data is retained.
|
||||
|
||||
Preview leaves files in place. Apply rechecks Git tracking, parent paths, directory identity
|
||||
and modification time before deleting. Symlinked directories, symlinked package parents, nested
|
||||
@@ -72,7 +72,7 @@ Source files, local changes, worktree registrations, branches, and shared pnpm s
|
||||
intact. Generated files are recreated by installing dependencies or rebuilding in that worktree.
|
||||
APFS clones and hardlinks mean directory sizes can greatly overstate the actual space reclaimed.
|
||||
|
||||
The automatic worker never runs this pass. Machine-wide caches, Docker images/volumes, downloads and simulator data remain
|
||||
Machine-wide caches, Docker images/volumes, downloads and simulator data remain
|
||||
outside this repository-scoped command.
|
||||
|
||||
## Keeping installs shared
|
||||
@@ -82,10 +82,12 @@ without waiting for an Electron upgrade. The manual command migrates older insta
|
||||
Read-only Electron package directories are retained and reported as skipped. Cache publication
|
||||
or copying failures are also reported as skipped rather than silently omitted.
|
||||
|
||||
`pnpm dev` and `pnpm dev-stable-name` start an automatic repository-wide Electron and Vite cleanup worker
|
||||
`pnpm dev` and `pnpm dev-stable-name` start an automatic repository-wide cleanup worker for
|
||||
Electron, Vite caches, and old dependency installs and generated output
|
||||
in the background after the dev process starts. It never prompts or waits before launching the app.
|
||||
The worktree being launched is excluded from Electron sharing, dev bundle removal, and Vite cleanup; active
|
||||
sibling apps and recent unfinished builds remain protected by the existing bundle checks.
|
||||
The worktree being launched is excluded from every cleanup pass. Existing bundle checks protect
|
||||
active sibling Electron bundles and recent unfinished bundles from the bundle-specific pass;
|
||||
the generated-directory pass uses its 30-day age cutoff and filesystem/Git protections.
|
||||
Background sweep output is suppressed; worker start or exit failures are reported without stopping dev. Set `ORCA_SKIP_DEV_STORAGE_RECLAIM=1` to opt out.
|
||||
The worker finishes independently if the dev app exits, and each cleanup operation has a ten-minute limit.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user