improvement(ci): collect idle node_modules sticky disks at 3 days - #7215
improvement(ci): collect idle node_modules sticky disks at 3 days#7215waleedlatif1 wants to merge 1 commit into
Conversation
node_modules sticky disks are keyed on hashFiles('bun.lock') by design, so a
lockfile bump orphans the old disk. Blacksmith already evicts after 7 days of
inactivity, so this is not a leak — it is a retention window far too generous for
a key that churns ~4.7 disks/day. Measured across the 39 live disks, the median
one is USED for 0.16 days and then billed for another 7, so the retention tail is
almost the whole cost. Collecting at 3 days takes the family from ~236 GB to
~111 GB steady-state.
Age-based, never PR-triggered. The key holds no PR identifier, so every open PR
whose checkout has the same bun.lock mounts the same disk — with ~180 open PRs
over roughly 20 distinct lockfile hashes, sharing is the common case, and a
delete on PR close would destroy a disk dozens of other open PRs are using.
Two independent guards on what may be deleted, because the blast radius of a
wrong key is a cache every CI job depends on: a server-side --search, then a
local regex re-proving the full key shape. The event segment is [a-z_]+ rather
than an enumerated push|pull_request — the key interpolates github.event_name,
and a workflow_dispatch disk already exists that an enumerated list would have
skipped forever. Verified against the live account: matches all 39 node_modules
disks and none of the 19 bun/turbo/Docker disks.
Runs on a GitHub-hosted runner so collection still works during a Blacksmith
outage or a CI_PROVIDER break-glass switch, which is exactly when disks idle and
still bill. The CLI is pinned by version and SHA256 rather than piped from a
remote installer, since the job holds an org-wide token, and auto-update is
disabled so the pin holds. Deletes fail the job rather than continue-on-error, so
a revoked token cannot silently revert us to 7-day billing.
Requires a BLACKSMITH_CLI_TOKEN repository secret; run once with dry_run first.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds a scheduled and manually dispatchable workflow that installs a pinned Blacksmith CLI and deletes node_modules sticky disks idle beyond a configurable retention window.
Confidence Score: 2/5This PR should not merge until the destructive retention input is bounded and the collector correctly handles the existing fork-key namespace. An authorized manual run can purge recently used shared caches through an unchecked cutoff, while normal fork-PR cache keys are systematically excluded from the intended three-day collection policy. Files Needing Attention: .github/workflows/stickydisk-gc.yml
|
| Filename | Overview |
|---|---|
| .github/workflows/stickydisk-gc.yml | Adds the complete sticky-disk collection workflow, but lacks a safe retention bound, omits fork-key variants, and processes only one result page. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
Schedule[Daily schedule] --> Input[Retention defaults to 3 days]
Dispatch[Manual dispatch] --> Input
Input --> List[List matching sticky disks]
List --> Guard[Validate key shape]
Guard --> Group[Group architecture variants]
Group --> Cutoff[Compare newest use with cutoff]
Cutoff --> DryRun{Dry run?}
DryRun -->|Yes| Report[Report candidates]
DryRun -->|No| Delete[Delete each matching key]
Delete --> Report
Reviews (1): Last reviewed commit: "improvement(ci): collect idle node_modul..." | Re-trigger Greptile
| jq -r --arg repo "$TARGET_REPO" --argjson days "$RETENTION_DAYS" ' | ||
| (now - ($days * 86400)) as $cutoff |
There was a problem hiding this comment.
When a manual run supplies zero or a negative retention_days, the cutoff selects recently used or every matching shared disk, causing active CI caches to be deleted. Reject non-positive values before calculating the cutoff. How this was verified: The unconstrained dispatch input is parsed by --argjson, used directly in the cutoff, and every resulting key reaches the authenticated delete command.
| jq -r --arg repo "$TARGET_REPO" --argjson days "$RETENTION_DAYS" ' | |
| (now - ($days * 86400)) as $cutoff | |
| jq -e -n --argjson days "$RETENTION_DAYS" '$days > 0' > /dev/null | |
| jq -r --arg repo "$TARGET_REPO" --argjson days "$RETENTION_DAYS" ' | |
| (now - ($days * 86400)) as $cutoff |
| (now - ($days * 86400)) as $cutoff | ||
| | .entries | ||
| | map(select(.type == "stickydisk")) | ||
| | map(select(.key | test("^" + ($repo | gsub("/"; "\\/")) + "-node-modules-[a-z_]+-[0-9a-f]{64}$"))) |
There was a problem hiding this comment.
For fork pull requests, the existing key producer emits pull_request-fork, but [a-z_]+ rejects the hyphenated suffix. Those node_modules disks are never selected by this collector and therefore retain the vendor's seven-day inactivity window instead of the configured three days.
| | map(select(.key | test("^" + ($repo | gsub("/"; "\\/")) + "-node-modules-[a-z_]+-[0-9a-f]{64}$"))) | |
| | map(select(.key | test("^" + ($repo | gsub("/"; "\\/")) + "-node-modules-[a-z_]+(-fork)?-[0-9a-f]{64}$"))) |
| --repo "$TARGET_REPO" \ | ||
| --search '-node-modules-' \ | ||
| --per-page 100 \ | ||
| --format json > disks.json | ||
|
|
There was a problem hiding this comment.
If the node_modules disk family exceeds 100 entries after an interruption in collection, this single list request processes only the first page. Entries outside that page remain billed until Blacksmith's longer vendor eviction window, so the workflow should iterate through all pages or fail explicitly when results are truncated.
Summary
node_modulessticky disks are keyed onhashFiles('bun.lock')by design (see the load-bearing comment intest-build.yml), so every lockfile bump orphans the old disk. That design is correct and is preserved here.This is not a leak. Blacksmith already evicts any sticky disk after 7 days of inactivity — confirmed in their docs and corroborated by the data: the oldest idle disk in the account is 7.50 days, with nothing beyond it. What we have is a retention window far too generous for a key that churns ~4.7 disks/day.
Measured across the 39 live disks: the median disk is used for 0.16 days, then billed for another 7. The retention tail is essentially the entire cost.
3 days rather than 2 so a long weekend still hits a warm disk: a PR whose last CI run was Friday afternoon and that gets pushed Monday morning is ~2.5 days idle. The extra day costs ~$16/mo and is worth it.
Why age-based and never PR-triggered
The key contains no PR identifier. Every open PR whose checkout has the same
bun.lockmounts the same disk — there are ~180 open PRs over roughly 20 distinct lockfile hashes, so sharing is the common case, not an edge case. Apull_request: [closed]hook would destroy a disk dozens of other open PRs are actively using, and would do so most aggressively for the most-shared disk. The existingci-cache-cleanup.ymlis safe only because its legacy key was${{ github.head_ref }}— genuinely per-branch.Safety
Two independent guards, because the blast radius of a wrong key is a cache every CI job depends on:
--search '-node-modules-'.The event segment is
[a-z_]+rather than an enumeratedpush|pull_request— the key interpolates${{ github.event_name }}, and aworkflow_dispatchdisk already exists (14 GB) that an enumerated list would have skipped forever. Caught by testing the selection against live data rather than reasoning about it.Verified against the account: matches all 39 node_modules disks and none of the 19 bun/turbo/Docker disks (which are mounted every run, never idle, and must survive). Dry run selects 25 disks / 129 GB.
Other choices worth defending in review:
CI_PROVIDERbreak-glass switch — exactly when disks idle and still bill.curl https://get.blacksmith.sh | sh; the job holds an org-wide token and must not execute unpinned remote shell. Checksum verified against the vendor's published.sha256sidecar.BLACKSMITH_DISABLE_AUTO_UPDATE=1so the pin holds.permissions: {}and no checkout — the job reads nothing from the repo.deletewithout--archremoves every arch variant.Worst case of a wrong delete is one cold
bun install— and the bun cache disk is not lockfile-keyed, so it never idles and never gets collected, meaning a refill links from a warm local store with no network fetch.Setup required before this does anything
blacksmith org-token create --label ci-stickydisk-gc --organization simstudioai(prints once).BLACKSMITH_CLI_TOKEN.main—scheduleonly runs the default-branch copy.node-modules-<hash>one.Type of Change
Testing
actionlintclean including its shellcheck pass. Selection logic dry-run against the live account (25 candidates / 129 GB) and cross-checked to exclude all 19 non-node_modules disks. CLI URL returns 200 and its SHA256 matches the published sidecar.bun run lintand all 38check:auditspass.Checklist