diff --git a/.changeset/clean-artifacts-script.md b/.changeset/clean-artifacts-script.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/clean-artifacts-script.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/AGENTS.md b/AGENTS.md index 1119e1ed8e..2f6905d455 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,8 +58,38 @@ pnpm typecheck # Clean build artifacts pnpm clean + +# Scrub ALL gitignored build artifacts (see "Branch-switch hygiene" below) +pnpm clean:artifacts ``` +### Branch-switch hygiene + +Build outputs are gitignored, so they survive `git checkout` across branches +whose layouts disagree (e.g. `main` vs `stable`). The mismatched leftovers +(stale `packages/*/dist`, generated sources from the other branch, stale +`.tsbuildinfo`, and a Turbo cache that may have captured polluted outputs) +wedge the repo in ways that look like unrelated breakage: `pnpm install` +hanging or erroring in workbench `prepare` scripts (`Could not resolve +"workflow/internal/private"`, SWC transform errors on valid workflow code), +`tsc` failing on generated files the branch doesn't have, or cleaned files +reappearing on every build (Turbo restoring a polluted cache entry). + +After switching between divergent branches, or whenever the workspace shows +any of those symptoms, run: + +```bash +pnpm clean:artifacts # add --dry-run to preview, --all to also drop node_modules + Cargo target/ +pnpm install +pnpm build +``` + +The script removes only files git ignores (candidates come from `git +ls-files -o -i`, filtered through a keep-list that preserves `node_modules`, +`.env*`/`*.local`, `.vercel`, Cargo `target/`, and local agent state), so +untracked work-in-progress files are never touched, and it works even when +the workspace is too broken to run turbo or pnpm scripts. + ### Core Package Testing ```bash diff --git a/package.json b/package.json index 1347105d1e..a6fb68a986 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "build": "turbo build --filter='./packages/*'", "test": "turbo test", "clean": "turbo clean", + "clean:artifacts": "bash scripts/clean-artifacts.sh", "typecheck": "turbo typecheck", "test:e2e": "vitest run packages/core/e2e/e2e.test.ts packages/core/e2e/e2e-agent.test.ts", "test:e2e:event-log-race-repro": "vitest run packages/core/e2e/event-log-race-repro.test.ts", diff --git a/scripts/clean-artifacts.sh b/scripts/clean-artifacts.sh new file mode 100755 index 0000000000..1aa288596f --- /dev/null +++ b/scripts/clean-artifacts.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# +# clean-artifacts.sh: scrub every gitignored build artifact so the workspace +# rebuilds from a pristine state. Run it after switching branches (or whenever +# the repo is behaving strangely); see "Why this exists" below. +# +# Usage: +# pnpm clean:artifacts # remove ignored artifacts, keep node_modules +# pnpm clean:artifacts --dry-run # print what would be removed +# pnpm clean:artifacts --all # also remove node_modules and the +# # Cargo target/ dir (slower reinstall + +# # Rust rebuild, but maximally pristine) +# +# Why this exists +# --------------- +# Build outputs (`packages/*/dist`, generated sources like +# `src/version.ts` / `quickjs-assets.generated.ts`, `tsc` incremental state, +# the Turbo cache, workbench state like `.next` / `.svelte-kit` / `.output`) +# are all gitignored, so they SURVIVE `git checkout` across branches whose +# layouts disagree (main vs stable, or any two branches spanning a +# refactor). The mismatched leftovers then wedge the repo in ways that look +# like unrelated breakage: +# +# * `pnpm install` hangs or errors: workbench `prepare` scripts (e.g. +# sveltekit's `svelte-kit sync`) load vite configs that resolve +# `workflow` to a stale `packages/workflow/dist`, whose imports no +# longer match the checked-out `package.json` exports map +# ("Could not resolve \"workflow/internal/private\""), or compile the +# checked-out workflows with a stale SWC plugin build that predates +# their syntax ("Functions marked with \"use step\" must be async"). +# * `tsc` builds fail on generated files from the other branch (e.g. +# `quickjs-assets.generated.ts` on a branch without QuickJS), or emit +# nothing because a stale `.tsbuildinfo` claims outputs are fresh. +# * Worst: a build that RUNS while dist is polluted lets Turbo capture +# the polluted dist as that task's cached outputs, after which the +# cache faithfully restores the pollution on every rebuild, and no +# amount of `rm -rf dist` alone fixes it. +# +# Design notes +# ------------ +# The candidate list comes from `git ls-files -o -i --exclude-standard +# --directory`, which lists ONLY files git ignores. Untracked-but-not-ignored +# files (your work in progress) are never touched. `git clean -x` was +# deliberately avoided (it removes untracked files too), and so was +# `git clean -X -e ` (exclude patterns modify the ignore rules, +# which under -X can ADD to the removal set instead of protecting, and +# negations leak into subdirectories). Filtering a listing in shell is +# boring and predictable. +# +# What is deliberately KEPT: +# * node_modules: reinstall is slow and pnpm's store makes staleness +# here rare (pass --all when you want it gone too); +# note the Turbo cache lives at .turbo/, which IS +# removed. +# * .env*, *.local: local secrets/config (e.g. VERCEL_OIDC_TOKEN pulled +# via `vercel env pull`). +# * .vercel: Vercel project links; losing them forces relinking. +# * target/: Cargo's build cache; unlike the JS tooling above, +# Cargo fingerprints inputs correctly across branch +# switches, and a cold Rust rebuild of the SWC plugin +# costs minutes (pass --all to remove it anyway). +# * .opencode, .claude, .agents: local agent tooling state. +set -euo pipefail + +cd "$(dirname "$0")/.." + +DRY_RUN=0 +ALL=0 +for arg in "$@"; do + case "$arg" in + --dry-run | -n) DRY_RUN=1 ;; + --all) ALL=1 ;; + --help | -h) + # Print the header comment block (everything between the shebang and + # the first non-comment line), stripped of the leading "# ". + awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' "$0" + exit 0 + ;; + *) + echo "Unknown argument: $arg (try --help)" >&2 + exit 1 + ;; + esac +done + +# Whether an ignored path should be preserved. Paths are repo-relative, +# directories carry a trailing slash (--directory). +keep() { + local p="${1%/}" + case "$p" in + # Local secrets/config. + .env* | */.env* | *.local) return 0 ;; + # Vercel project links. + .vercel | */.vercel | .vercel/* | */.vercel/*) return 0 ;; + # Local agent tooling state. + .opencode | .opencode/* | .claude | .claude/* | .agents | .agents/*) return 0 ;; + esac + if [[ "$ALL" -ne 1 ]]; then + case "$p" in + node_modules | node_modules/* | */node_modules | */node_modules/*) return 0 ;; + target | target/* | */target | */target/*) return 0 ;; + esac + fi + return 1 +} + +removed=0 +kept=0 +while IFS= read -r path; do + [[ -z "$path" ]] && continue + if keep "$path"; then + kept=$((kept + 1)) + continue + fi + # A parent directory earlier in the listing may have removed it already. + [[ -e "$path" || -L "$path" ]] || continue + if [[ "$DRY_RUN" -eq 1 ]]; then + echo "Would remove $path" + else + echo "Removing $path" + rm -rf -- "$path" + fi + removed=$((removed + 1)) +done < <(git ls-files -o -i --exclude-standard --directory) + +echo +if [[ "$DRY_RUN" -eq 1 ]]; then + echo "Dry run: nothing removed ($removed candidates, $kept kept). Re-run without --dry-run to clean." +else + echo "Cleaned $removed artifacts ($kept kept). Next steps:" + echo ' pnpm install' + echo ' pnpm build' +fi