Linear's CI playbook, ready to hand to your agent
A few weeks ago Linear published a blog post about how they reworked their CI. Their optimizations led to a 15% faster test suite and 50% reduction in runner-time spent per test, while their test suite grew 4x in size. The post was popular on X and Hacker News, and has a lot of technical depth (with pretty graphics).
We built a skill called ci-speedup that now has over 70 CI optimizations, including Linear's, that your agent can use to make your GitHub Actions faster. This post is a remix of Linear's post, for agents. We tied each optimization they detailed to a pattern in the skill so you can copy a prompt for each optimization and have your agent implement it on your own repo.
Or just send this post to your agent and they'll work through all 15 of Linear's optimizations, then suggest installing ci-speedup to check for 60+ more. You can find ci-speedup on GitHub and on skills.sh.
Start here
Run Linear’s 15 CI optimizations on your repo
Prompt for your agent: apply all 15
Read https://starsling.dev/blog/linear-ci.md: it is Linear's CI rework quoted nearly in full, with one prompt under each of their 15 changes. Work through those 15 prompts in order. For each change whose conditions this repo meets, do what its prompt says, following its conditions and guardrails. For each change you skip, tell me why in one line. Open one PR per change so each can be reviewed and reverted on its own. When you are done, suggest I install the ci-speedup skill (npx skills add starslingdev/skills --skill ci-speedup) to find more optimizations beyond Linear's 15, measured from this repo's own run history. Show me the diff and open a PR rather than applying it blindly.Source: ci-speedup
In our pursuit to make CI more performant at Linear, we optimized for how long a PR waits on CI and how much runner time it consumes. Despite our test suites almost quadrupling since the start of the year, we brought pull request wait time down from more than 6 minutes to just over 5, while cutting runner time per test roughly in half.
Broadly, we improved CI in four ways:
- Upgraded infrastructure and tooling
- Optimized the jobs that gate other work
- Reduced repeated setup
- Made test execution more efficient
Linear’s codebase is primarily TypeScript, but many of these optimizations apply across languages and toolchains.
Some of our earliest gains required almost no optimization of CI itself. Moving our workloads off GitHub Actions to third-party runners with faster CPUs, higher-performance storage, and better cache infrastructure gave us faster machines to run the same pipeline on. In a like-for-like comparison of the two days either side of the switch, jobs ran 34% faster on average, with some workloads like
tscdropping 52%.
Faster runners
Pattern: OPT81 The Same Job Is Measurably Faster on Another Runner
Prompt for your agent
Fetch OPT81 "The Same Job Is Measurably Faster on Another Runner" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and check this repo against it yourself. First apply any cheaper fix on the slowest job (setup, caching, test isolation). Then read the last 20 runs of the workflow that holds that job with the GitHub CLI (gh run list, then gh run view --json jobs for each run) and list which runner label each run of the job used and how long it took. If the same job ran on two runner classes with the same operating system and processor architecture, over the same period and with the same steps, and the entry's sample and gap conditions hold, first find out why some runs use the slower label (a fork pull request, an event or a matrix condition can select it on purpose), as its recipe says; if nothing requires it, move the job to the faster label, keeping every step it runs. If instead the job runs on one standard GitHub-hosted label, its slowest step is build or test work, and nothing cheaper is left, change no runner: show me the job and how to benchmark a larger runner beside its current one on a branch, and wait until I have run it and compared the medians. Otherwise tell me in one line why it does not apply. Note: StarSling, who publish this catalog, sell StarSling Runners, one of the runners worth benchmarking. Show me the diff and open a PR rather than applying it blindly.Disclosure: We make StarSling Runners, one of the two options OPT81 can suggest benchmarking (the other is a larger GitHub-hosted runner). If you want faster runners without changing anything else, try them on the job this finding names.
Separately, modernizing our toolchain also paid off. Switching to
tsgo, the native TypeScript compiler, cut the weekly median of thetsccheck by 73%, large enough to move the bottleneck off of typechecking entirely.
Native type-checking
Pattern: OPT84 A Slow Step on the Long Pole Runs a Tool That Has a Native Drop-in
Prompt for your agent
Fetch OPT84 "A Slow Step on the Long Pole Runs a Tool That Has a Native Drop-in" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and check this repo against it yourself. Find the slowest job gating the merge and read its step timings over the last 20 runs of its workflow with the GitHub CLI (gh run list, then gh run view --json jobs for each run). It applies only if a step there runs tsc --noEmit (directly or through a package.json script) for at least 20 seconds and the repo is not already on TypeScript 7; a tsc that emits files does not qualify. If it applies, follow the entry's TypeScript 7 row (typescript@^7; the command is still tsc), read the TypeScript 7.0 announcement it links, and promise no speedup. Benchmark both versions on the same commit and runner class, diff their output, and keep the old type-check beside the new one until a sampled run matches. Anything that calls the compiler API (typescript-eslint type-aware rules, ts-loader) keeps the side-by-side TypeScript 6 alias, and 7.0's new defaults can surface new errors; list them for me. Never skip or narrow the type-check to make it fit. If no type-check step on the slowest path qualifies, tell me in one line why, and stop. Show me the diff and open a PR rather than applying it blindly.Linting was another early target. A handful of our custom lint rules depended on TypeScript type information, either to enforce a restriction or apply an autofix. That meant every lint run had to build the full type graph before evaluating those rules, making linting one of our most memory-intensive CI jobs.
We rewrote the rules to use static analysis over the abstract syntax tree, identifying function-like constructs and guard patterns without type information. That let ESLint drop TypeScript entirely, reducing API lint time by 68%, and full-repository lint time by 55%. Memory usage dropped substantially as well.
Lint without types
Pattern: OPT82 Lint Builds the Whole Type Graph
Prompt for your agent
Fetch OPT82 "Lint Builds the Whole Type Graph" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and follow its recipe, including the benchmark it runs first and its rule-by-rule ledger. Read this repo's ESLint config and list every rule that needs type information (parserOptions.project / projectService, or typescript-eslint's type-checked sets). For each, decide: rewrite it as an AST-only rule, or move it into a separate type-aware lint job. That job may check changed files only if a merge queue or another required pass still runs it over the whole tree; otherwise it stays a required check over the whole tree. Never disable a rule to make lint faster. Show me the diff and open a PR rather than applying it blindly.Removing the dependency on type information also made our later move to Oxlint much easier because rules that operate purely on syntax are straightforward to port. Oxlint itself reduced the CI runner-minutes spent on linting.
Oxlint
Pattern: OPT84 A Slow Step on the Long Pole Runs a Tool That Has a Native Drop-in
Prompt for your agent
Fetch OPT84 "A Slow Step on the Long Pole Runs a Tool That Has a Native Drop-in" and OPT82 "Lint Builds the Whole Type Graph" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and check this repo against them yourself. Find the slowest job gating the merge and read its step timings over the last 20 runs of its workflow with the GitHub CLI (gh run list, then gh run view --json jobs for each run). OPT84's ESLint to oxlint row applies only if a step there runs eslint (directly or through a package.json script) for at least 20 seconds and the repo does not already run oxlint. If that lint step also qualifies for OPT82, do the OPT82 work first. Read the ESLint config: if it has custom local rules, the tailwindcss plugin, a framework plugin or any other plugin oxlint does not carry, a react-hooks/exhaustive-deps rule, type-aware rules, or a shared config you cannot follow, use the entry's run-both shape: run oxlint across the tree and keep ESLint scoped to the rules oxlint lacks. Follow the oxlint migration guide the entry links, and promise no speedup. Never drop, narrow or disable a rule, plugin or file set to make the swap fit. Migrate the config first, then benchmark both tools on the same commit and runner class and diff their output before removing anything. One tool per PR. If no lint step on the slowest path qualifies, tell me in one line why, and stop. Show me the diff and open a PR rather than applying it blindly.With the underlying infrastructure and individual checks running faster, we zoomed out to look at CI as a system. That drew our attention to the small jobs that sat in front of everything else. Every run starts by checking which paths a PR touched and whether these tests have already passed for the same inputs. We gate on those checks at the job level so skipped work never reserves a runner, but that also puts them directly on the critical path. None of the eight API test shards can start until they finish, making even small delays disproportionately important.
Several of our workflows start with a change-detection job that decides what runs next; for instance, it checks whether a diff contains a database migration and outputs a signal used to schedule the relevant database CI checks. These jobs were checking out the full working tree even though they needed only a small subset of it. We capped the fetch depth, which took the slowest of these gates from 94 seconds to 20, and removed checkout entirely from the jobs that never needed a working tree, reducing time spent on those from 27 seconds to 7. For commit push and merge-queue events, where we do have to diff paths, we found that a sparse, blobless checkout with limited history was enough, saving another 11 odd seconds.
Lighter checkouts
Pattern: OPT28 Full Git History Checkout
Prompt for your agent
Fetch OPT28 "Full Git History Checkout" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and follow its recipe. For every actions/checkout step in .github/workflows: delete the checkout only if no step in that job reads a file from it, directly or indirectly: check for a local action (uses: ./...) and open its definition, hashFiles() in a cache key, a run: that calls git or a repo script, and a gh command that relies on the current repo (it then needs --repo or GH_REPO); a missing file fails the job, so delete only when every step is accounted for; if it reads only a few paths, use sparse-checkout so only those paths are written; use a shallow depth where nothing needs history. For a diff gate that needs history but not every file's contents, use filter: blob:none with fetch-depth: 0, which fetches file contents on demand. Keep the base branch reachable for jobs that diff against it (turbo --filter=...[origin/main], nx affected, vitest --changed), and keep fetch-depth: 0 where something walks history or tags. Show me the diff and open a PR rather than applying it blindly.After we swapped the underlying runner infrastructure, we noticed that our checkout times (with
actions/checkout) in our jobs had gotten longer and would sometimes hang. Because the third-party runners sit outside GitHub’s network, they rely on a direct IP link to reach GitHub. The provider traced the hangs to intermittent degradation on that link. Several of our workflows begin with a checkout, so a stalled fetch could delay the entire CI run.To be resilient to the network instability, we replaced
actions/checkoutwith a composite action of our own that retried with backoff, and setsGIT_HTTP_LOW_SPEED_LIMITandGIT_HTTP_LOW_SPEED_TIMEso a stalled connection aborts after about 30 seconds instead of hanging and also uses the checkout cache, which keeps a persistent git mirror on a sticky disk. The result was far fewer runs where a critical-path job sat idle waiting for checkout to finish.
Resilient checkout
Pattern: OPT80 Checkout Stalls on the Tail
Prompt for your agent
Fetch OPT80 "Checkout Stalls on the Tail" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and check this repo against it yourself. For each job's checkout step, read its duration over the last 20 runs of the workflow with the GitHub CLI (gh run list, then gh run view --json jobs for each run). Where a few runs take far longer than the rest, read those runs' checkout logs (gh run view --log): the entry applies only when the git fetch stood still, with the same Receiving objects percentage on both sides of a pause, not when the fetch was merely slow or the checkout is simply large. If it applies, follow its recipe in its order: wrap checkout in a composite action that retries with backoff and sets GIT_HTTP_LOW_SPEED_LIMIT and GIT_HTTP_LOW_SPEED_TIME so a stalled fetch aborts instead of hanging, shipped together, never the abort without the retry. The composite action lives in this repo and is referenced by path; pin every third-party action it uses, actions/checkout included, to a full commit SHA. If no run shows a stalled fetch, tell me in one line why, and stop. Show me the diff and open a PR rather than applying it blindly.Not every job on the critical path needed to be there. We were writing cache markers as part of the final check before merging, which meant a pull request could sit in the merge queue even after its tests had passed. We moved that write into a job that runs once the test shards finish but gates nothing, shaving 42 seconds from the merge path for every API pull request and merge-queue entry.
Together, these changes took roughly a minute off the required check for API pull requests on cache misses, while also reducing runner starts.
Off the critical path
Pattern: OPT71 Expensive Non-Required Check on the Critical Path
Prompt for your agent
This move is done by reading the workflows. In every job that is a required check, list the steps that only produce something for later runs (a cache save, a marker write, an artifact no later job in this run reads). Move each into a separate job that needs the test jobs and is not a required check, so the merge path no longer waits on it. Do not move anything a later job in the same run depends on, and do not use a post step: it still runs inside the required job. Then fetch OPT71 "Expensive Non-Required Check on the Critical Path" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and check this repo against it yourself: read the last 20 runs of each pull-request workflow with the GitHub CLI (gh run list, then gh run view --json jobs for each run) to find the slowest checks, and read the required checks (gh api repos/OWNER/REPO/rulesets and the branch protection's required_status_checks). If one of the slowest checks is not required and, once you have listed everything that consumes its result, is genuinely advisory (a comment, a preview, a non-blocking annotation), apply the least aggressive option its recipe gives. If it runs real tests or validates a build, speed it up instead of de-scoping it, and if the required checks cannot be read, de-scope nothing. If nothing qualifies, tell me in one line why. Show me the diff and open a PR rather than applying it blindly.From there, we turned to the setup cost repeated across every job, like booting a runner, installing packages, and provisioning build dependencies. That overhead means a job that does only seconds of useful work can end up consuming whole minutes of infrastructure time. Here are a few steps we took to work around that issue:
Our API test shards each spent 7 to 8 seconds installing the same Postgres client with apt on every run. We moved it into a small CI base image containing Node and the client, so each shard could start from an environment that was ready to run. We later added the required native build headers to the image after discovering that downloading them during setup could occasionally hang, shortening the tail.
Preinstall in the CI image
Patterns: OPT73 Shared Sub-Step Across Critical-Path Jobs (cluster-floor lever), OPT1 Unnecessary Tool Install
Prompt for your agent
Fetch OPT1 "Unnecessary Tool Install" and OPT73 "Shared Sub-Step Across Critical-Path Jobs" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and check this repo against them yourself. Handle each on its own. OPT1: read the workflows and delete installs of tools a job never uses; if a shared setup action does the install, make it conditional or split the action. OPT73: read step timings over the last 20 runs of each pull-request workflow with the GitHub CLI (gh run list, then gh run view --json jobs for each run) and look for a setup step (apt-get, a toolchain download, a dependency install) that recurs with real cost in two or more of the jobs that finish near the slowest one. If one does, follow its recipe: make that step cheap in every job that runs it, choosing between a cache keyed on the step's real inputs and a prebuilt base image, and never move it into one upstream job the others need. If you propose a container image, first list what changes for services, Docker steps and paths, before switching any job. If neither applies, tell me in one line why. Show me the diff and open a PR rather than applying it blindly.Linear’s codebase is a monorepo managed as a pnpm workspace. Our API test workflow was installing the entire workspace even though it only needed the API package and its dependencies. Restricting the install to our API package cut pnpm install from 44-73 seconds to 16-18 seconds. We applied the same pattern to API-adjacent jobs, which were each installing the full repository and uploading a dependency cache that later runs almost never hit.
Filtered installs
Pattern: OPT54 Full-Repo pnpm -r Where Package Filters Are Possible
Prompt for your agent
Fetch OPT54 "Full-Repo `pnpm -r` Where Package Filters Are Possible" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and scope recursive pnpm commands in .github/workflows with --filter as it describes. Then apply the same idea to installs: for each job, work out which workspace packages its later steps use and install with pnpm install --frozen-lockfile --filter "<package>..." (the trailing ... includes that package's dependencies). Confirm nothing in the job imports a package outside the filter. Show me the diff and open a PR rather than applying it blindly.We also tested caching
node_modulesand found it was faster to rebuild. The cache key depended on a frequently changing lockfile, and even a cache hit took about 28 seconds to restore, compared with roughly 7.5 seconds for a filtered install. The cache was adding save time and variability without giving us any discernible advantage.Together, these three changes reduced per-shard setup time by roughly 44%, from 110-140 seconds to 67-73 seconds.
Beyond this, there were other forms of repeated setup we could avoid altogether.
Drop a costly cache
Patterns: OPT79 A Cache That Costs More Than It Saves, OPT6 Cache Key Entropy Too High or Unstable
Prompt for your agent
Fetch OPT79 "A Cache That Costs More Than It Saves" and OPT6 "Cache Key Entropy Too High or Unstable" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and check this repo against them yourself. OPT79: for a job with exactly one dependency cache, read the last 20 runs of its workflow with the GitHub CLI (gh run list, then gh run view --json jobs for each run), plus the cache and dependency steps' logs (gh run view --log), and compare restore plus dependency setup on cache hits against the same steps on misses, on the same runner label. It applies only when a hit is measured slower than a miss. If it applies, work in the order its fix gives: a restore that is slow on a hit means the archive is too big, so first cache the package manager's store (for pnpm, the pnpm store) instead of node_modules, or scope the cache and the dependency step to the package this job needs (OPT54's filtered install, OPT8's key granularity), then re-measure. Remove the cache only if a filtered install is still faster than any restore, and never drop the step the cache feeds or skip it on some runs. OPT6 is a separate problem, a cache that misses: if a cache key includes github.sha, github.run_id, a date or another value that changes every run, fix the key as OPT6 says. If neither applies, keep the cache and tell me in one line why. Show me the diff and open a PR rather than applying it blindly.Some setup work only needs to be repeated when its inputs change. Our API containers, for example, were replaying the full database migration history on every run, even when a PR hadn’t changed the schema. For those cases, we switched to loading a generated schema snapshot and bootstrap file instead, cutting database setup from roughly 12 seconds to 1-2 seconds per container.
Schema snapshots
Prompt for your agent
No catalog pattern covers this one. Find the step in .github/workflows that prepares the test database (migration replay, seed, or a script named migrate/setup-db). Time it over the last 20 runs of its workflow with the GitHub CLI (gh run list, then gh run view --json jobs for each run). If it is a material share of the job, propose replacing replay with loading a generated schema snapshot: add a script that dumps the schema at the current migration head, commit the snapshot or generate it in a cached step, and load it in CI. Keep one job that still replays migrations end to end, so migration bugs are still caught. Show me the diff and open a PR rather than applying it blindly.Seven independent checks were each starting a runner, checking out the repository, and installing dependencies before doing only seconds of useful work. We consolidated them into two jobs, and then ran the seven tasks concurrently inside them. That reduced the number of times we paid the same setup overhead from seven to two. Based on June usage, the change saved roughly 87,000 runner-minutes per month, equivalent to 11.8% of our total CI usage.
Batch short checks
Patterns: OPT77 Repeated Fixed Setup Across Independent Small Jobs, OPT83 Independent Steps on the Long Pole Run One After Another
Prompt for your agent
Fetch OPT77 "Repeated Fixed Setup Across Independent Small Jobs" and OPT83 "Independent Steps on the Long Pole Run One After Another" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and check this repo against them yourself. Read step timings over the last 20 runs of each pull-request workflow with the GitHub CLI (gh run list, then gh run view --json jobs for each run). OPT77: look for small independent jobs on the same runner label that repeat the same setup steps (checkout, setup actions, dependency install) and spend at least as long on that setup as on their own work. If such a group exists, merge only those jobs into one, run their tasks concurrently inside it (a parallel: step group), and keep the two conditions its recipe names: no job outside the group may wait on a member, and the merged job must stay shorter than the tallest job that remains. The merged job still runs every check and fails when any of them fails. OPT83: on the slowest job gating the merge, look for adjacent compute steps that each take at least 20 seconds and at least 15% of the job; if the entry's independence checklist holds for all of them, rewrite only those steps as one GitHub Actions parallel: group, and benchmark it before keeping it. If either part does not qualify, leave it alone and tell me in one line why. Show me the diff and open a PR rather than applying it blindly.With the fixed cost of each test shard down, we could afford to parallelize the API suite more aggressively. It was the largest and one of the most frequently executed parts of our workflow, so improvements there had an outsized effect on merge time.
Vitest, the test runner we use for our TypeScript test suites, distributes work by file rather than by the duration of individual tests. That meant a few unusually large test files could dominate a shard and effectively hold up completion of the entire suite, even when the other shards finished much earlier.
We split those large files into smaller, more focused files while preserving the structure of the tests, then evaluated different shard and runner configurations. We had already gone from three to four shards earlier in the year; moving to eight made the critical job roughly 19% faster and 19% cheaper in our initial benchmark. A week after the change, the slowest shard dropped from 5.25 minutes to 4.33 minutes.
Balance the shards
Patterns: OPT25 Shard Imbalance, OPT24 Long Test Job Without Sharding
Prompt for your agent
Fetch OPT24 "Long Test Job Without Sharding" and OPT25 "Shard Imbalance" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md. If the slowest test job is not sharded, follow OPT24. If it is, get per-file runtimes (the test runner's own timing output, or the logs of the last 20 runs, read with gh run view --log) and list shard durations. If one shard is consistently longer because of a few slow files, split those files along their existing describe blocks before adding shards. If shards are uneven without one dominant file, use timing-based splitting only where the runner supports it (OPT25 names pytest-split and nextest timing data; Vitest's --shard splits by file and has no timing mode). Propose a higher shard count only once shards are balanced. Show me the diff and open a PR rather than applying it blindly.Vitest normally isolates every test file, which for us meant rebuilding the entity, GraphQL, and decorator graph in each test shard. We introduced an opt-in vitest project with
isolate: false, allowing safe files to share a module registry within each worker.This was our largest single performance improvement, worth roughly 17% in monthly savings at our volume. The slowest shard fell from roughly 300-379 seconds to about 195 seconds, while total API-shard runner time dropped from about 32.8 to 22 minutes per run.
It was also the optimization with the highest correctness risk. We made eligibility explicit with an opt-in comment on every file, and added the necessary teardown for shared state. A handful of files used fake timers or shared state in ways we couldn’t untangle safely, so we left them in the isolated project. And because agents now write the majority of our tests, we updated our respective agent skills to account for this performance opt-in as well, so generated tests follow the same constraints by default.
Share module state
Pattern: OPT78 Per-File Test Isolation Rebuilding Shared Module State
Prompt for your agent
Fetch OPT78 "Per-File Test Isolation Rebuilding Shared Module State" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md and check this repo against it yourself; it covers vitest only. Read the logs of the slowest test job over the last 20 runs of its workflow (gh run list, then gh run view --log) and find vitest's Duration line. It applies only when import plus transform exceeds tests there, import is above 30 seconds, and the vitest config keeps isolation on (no isolate: false anywhere, no vm pool). If it applies, read the config's history first, then follow its recipe and guardrail exactly; never set isolate: false globally. Add a second vitest project with isolate: false. Files join it only through an explicit include list or a dedicated filename suffix, and only files with no module-level mutable state and no fake timers. Write teardown for any shared state they touch, enable sequence.shuffle there, and start in shadow mode (files run in both projects; the shared one is not required). If it does not apply, tell me in one line why, and stop. Show me the diff and open a PR rather than applying it blindly.Further sharding only pays off when the fixed cost per shard is low, since doubling the shard count also doubles the workflow time spent on setup. The setup optimizations we referred to earlier are what made eight shards practical. At 110-140 seconds per shard, eight shards would have spent 15-19 minutes of runner time on setup alone, more than the tests themselves. Setup is now around 40 seconds, so eight shards spend less total setup time than four did before, while parallelizing the tests twice as far.
Setup bounds sharding
Patterns: OPT73 Shared Sub-Step Across Critical-Path Jobs (cluster-floor lever), OPT54 Full-Repo pnpm -r Where Package Filters Are Possible, OPT79 A Cache That Costs More Than It Saves, OPT25 Shard Imbalance
This change follows from three earlier ones, “Preinstall shared dependencies in the CI image”, “Install only the dependencies each job needs” and “Don’t cache when it’s faster to rebuild”: once each shard’s setup shrinks, more shards pay.
Prompt for your agent
Fetch OPT73 "Shared Sub-Step Across Critical-Path Jobs", OPT54 "Full-Repo `pnpm -r` Where Package Filters Are Possible", OPT79 "A Cache That Costs More Than It Saves" and OPT25 "Shard Imbalance" from https://raw.githubusercontent.com/starslingdev/skills/main/skills/ci-speedup/references/optimization-patterns.md. For the slowest test job, read its step timings over the last 20 runs of its workflow with the GitHub CLI (gh run list, then gh run view --json jobs for each run) and measure the setup prefix (checkout, dependencies, containers, migrations) against the test step. Tell me, in seconds, what each added shard costs in setup today, and check this repo against OPT73, OPT54 and OPT79 yourself to see which would shrink it; apply each only where its own conditions hold, and do those first. Propose a higher shard count only in a later PR, once the setup changes are merged and new runs show the smaller setup. Show me the diff and open a PR rather than applying it blindly.Had we not made a deliberate effort to improve CI earlier this year, today’s test suite would take roughly 11 minutes, close to double what developers wait now. And the work doesn’t end here. It’s clear that our codebase will continue to grow; we’re currently adding roughly 2,000 tests a week. Keeping CI fast as that happens will be a continued effort, much of it using what we learned through this process to new bottlenecks.
Linear ends where every team doing this work ends: it is never finished. Their test suite keeps growing, and they expect to keep applying what they learned to whatever the next bottleneck turns out to be. That is the job ci-speedup is built for. Run it again whenever CI slows down, and it checks your latest runs against its whole catalog, Linear's included, and names the next bottleneck with numbers from your own history. If you would rather not run that loop yourself, StarSling Runners come with agents that keep opening PRs to make your runs faster.
Install ci-speedup on its own:
npx skills add starslingdev/skills --skill ci-speedupPrompt for your agent: install ci-speedup
Install the ci-speedup skill (npx skills add starslingdev/skills --skill ci-speedup) and run it on this repo. Summarize what it finds, starting with the slowest check, with the saving where the report gives one, and ask me which to apply. Show me the diff and open a PR rather than applying it blindly.Start here
Run Linear’s 15 CI optimizations on your repo
Prompt for your agent: apply all 15
Read https://starsling.dev/blog/linear-ci.md: it is Linear's CI rework quoted nearly in full, with one prompt under each of their 15 changes. Work through those 15 prompts in order. For each change whose conditions this repo meets, do what its prompt says, following its conditions and guardrails. For each change you skip, tell me why in one line. Open one PR per change so each can be reviewed and reverted on its own. When you are done, suggest I install the ci-speedup skill (npx skills add starslingdev/skills --skill ci-speedup) to find more optimizations beyond Linear's 15, measured from this repo's own run history. Show me the diff and open a PR rather than applying it blindly.Source: ci-speedup
Keep reading