From 4fba9b0a97f42bc839b40033260b3841ea26109c Mon Sep 17 00:00:00 2001 From: Mobeen Abdullah Date: Tue, 11 Aug 2026 17:03:39 +0500 Subject: [PATCH 1/2] ci(root): stop a cancelled run from publishing a half-built turbo cache --- .github/workflows/ci.yml | 76 +++++++++++++++++++++++++++---- .github/workflows/integration.yml | 76 +++++++++++++++++++++++++++---- .github/workflows/preview.yml | 38 ++++++++++++++-- .github/workflows/release.yml | 38 ++++++++++++++-- 4 files changed, 204 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68a6fe9d96..0ad1aa96b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,13 +40,33 @@ jobs: cache: pnpm registry-url: https://registry.npmjs.org - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # Restore only. The matching save runs at the end of the job, gated on the + # job having succeeded. + # + # `actions/cache` saves in a post step that runs however the job ended, so a + # run cancelled mid-build — which `cancel-in-progress` above makes routine — + # can publish a `.turbo` holding a task entry whose recorded outputs are + # incomplete. A later run restores it, reports `cache hit, replaying logs`, + # and lays down a `dist/` missing files the rest of the build imports. The + # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package + # nobody changed. + # + # Re-running cannot clear it: the key is the commit SHA, so a second attempt + # scores an exact hit on the same entry. Splitting restore from save is the + # supported way to make the save conditional (`save-always` was deprecated + # for this). + # + # The `v2-` prefix retires every entry written under the old scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # so entries already poisoned would otherwise keep being selected until they + # aged out. + - name: Restore Turbo cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -196,6 +216,16 @@ jobs: env: TURBO_CACHE_DIR: .turbo + # Only a job that finished its work publishes what it learned. A cancelled or + # failed run leaves the cache as it found it, so nothing it half-built can be + # restored by a later one. + - name: Save Turbo cache + if: success() + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .turbo + key: turbo-v2-${{ runner.os }}-${{ github.sha }} + # The admin, in a real browser, against a real server and a real database. # Covers the class of failure the unit and integration suites structurally # cannot: jsdom has no layout engine, so a column that grew to 1024px or a @@ -229,13 +259,33 @@ jobs: node-version-file: .nvmrc cache: pnpm - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # Restore only. The matching save runs at the end of the job, gated on the + # job having succeeded. + # + # `actions/cache` saves in a post step that runs however the job ended, so a + # run cancelled mid-build — which `cancel-in-progress` above makes routine — + # can publish a `.turbo` holding a task entry whose recorded outputs are + # incomplete. A later run restores it, reports `cache hit, replaying logs`, + # and lays down a `dist/` missing files the rest of the build imports. The + # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package + # nobody changed. + # + # Re-running cannot clear it: the key is the commit SHA, so a second attempt + # scores an exact hit on the same entry. Splitting restore from save is the + # supported way to make the save conditional (`save-always` was deprecated + # for this). + # + # The `v2-` prefix retires every entry written under the old scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # so entries already poisoned would otherwise keep being selected until they + # aged out. + - name: Restore Turbo cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -270,6 +320,16 @@ jobs: path: e2e/.playwright/report retention-days: 7 + # Only a job that finished its work publishes what it learned. A cancelled or + # failed run leaves the cache as it found it, so nothing it half-built can be + # restored by a later one. + - name: Save Turbo cache + if: success() + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .turbo + key: turbo-v2-${{ runner.os }}-${{ github.sha }} + # Cross-platform smoke test for the scaffolder. Catches Windows path # separator bugs and macOS-specific issues that only surface in fresh # installs. Runs after the main `ci` job to avoid wasting minutes when diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index b372a83be3..0221e8f8b2 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -90,13 +90,33 @@ jobs: node-version-file: .nvmrc cache: pnpm - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # Restore only. The matching save runs at the end of the job, gated on the + # job having succeeded. + # + # `actions/cache` saves in a post step that runs however the job ended, so a + # run cancelled mid-build — which `cancel-in-progress` above makes routine — + # can publish a `.turbo` holding a task entry whose recorded outputs are + # incomplete. A later run restores it, reports `cache hit, replaying logs`, + # and lays down a `dist/` missing files the rest of the build imports. The + # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package + # nobody changed. + # + # Re-running cannot clear it: the key is the commit SHA, so a second attempt + # scores an exact hit on the same entry. Splitting restore from save is the + # supported way to make the save conditional (`save-always` was deprecated + # for this). + # + # The `v2-` prefix retires every entry written under the old scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # so entries already poisoned would otherwise keep being selected until they + # aged out. + - name: Restore Turbo cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -135,6 +155,16 @@ jobs: TEST_MYSQL_URL: mysql://root:nextly@127.0.0.1:3306/nextly_test TURBO_CACHE_DIR: .turbo + # Only a job that finished its work publishes what it learned. A cancelled or + # failed run leaves the cache as it found it, so nothing it half-built can be + # restored by a later one. + - name: Save Turbo cache + if: success() + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .turbo + key: turbo-v2-${{ runner.os }}-${{ github.sha }} + # sqlite runs in-process against a throwaway file, so this job declares no # `services:` at all. Keeping it out of the matrix above means it never pulls # the postgres/mysql images it would not connect to, which removes its only @@ -161,13 +191,33 @@ jobs: node-version-file: .nvmrc cache: pnpm - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # Restore only. The matching save runs at the end of the job, gated on the + # job having succeeded. + # + # `actions/cache` saves in a post step that runs however the job ended, so a + # run cancelled mid-build — which `cancel-in-progress` above makes routine — + # can publish a `.turbo` holding a task entry whose recorded outputs are + # incomplete. A later run restores it, reports `cache hit, replaying logs`, + # and lays down a `dist/` missing files the rest of the build imports. The + # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package + # nobody changed. + # + # Re-running cannot clear it: the key is the commit SHA, so a second attempt + # scores an exact hit on the same entry. Splitting restore from save is the + # supported way to make the save conditional (`save-always` was deprecated + # for this). + # + # The `v2-` prefix retires every entry written under the old scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # so entries already poisoned would otherwise keep being selected until they + # aged out. + - name: Restore Turbo cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -181,3 +231,13 @@ jobs: pnpm turbo test:integration --filter=@nextlyhq/adapter-sqlite --filter=nextly --filter=@nextlyhq/plugin-page-builder env: TURBO_CACHE_DIR: .turbo + + # Only a job that finished its work publishes what it learned. A cancelled or + # failed run leaves the cache as it found it, so nothing it half-built can be + # restored by a later one. + - name: Save Turbo cache + if: success() + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .turbo + key: turbo-v2-${{ runner.os }}-${{ github.sha }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 130b81f40f..bfb63adc6b 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -44,13 +44,33 @@ jobs: node-version-file: .nvmrc cache: pnpm - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # Restore only. The matching save runs at the end of the job, gated on the + # job having succeeded. + # + # `actions/cache` saves in a post step that runs however the job ended, so a + # run cancelled mid-build — which `cancel-in-progress` above makes routine — + # can publish a `.turbo` holding a task entry whose recorded outputs are + # incomplete. A later run restores it, reports `cache hit, replaying logs`, + # and lays down a `dist/` missing files the rest of the build imports. The + # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package + # nobody changed. + # + # Re-running cannot clear it: the key is the commit SHA, so a second attempt + # scores an exact hit on the same entry. Splitting restore from save is the + # supported way to make the save conditional (`save-always` was deprecated + # for this). + # + # The `v2-` prefix retires every entry written under the old scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # so entries already poisoned would otherwise keep being selected until they + # aged out. + - name: Restore Turbo cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -69,3 +89,13 @@ jobs: # push to the same PR. - name: Publish to pkg.pr.new run: pnpm dlx pkg-pr-new publish --pnpm --commentWithSha './packages/*' + + # Only a job that finished its work publishes what it learned. A cancelled or + # failed run leaves the cache as it found it, so nothing it half-built can be + # restored by a later one. + - name: Save Turbo cache + if: success() + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .turbo + key: turbo-v2-${{ runner.os }}-${{ github.sha }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e65d10948..a91f0fa0b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,13 +67,33 @@ jobs: - name: Upgrade npm to >=11.5.1 for trusted publishing run: npm install -g npm@11.18.0 - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # Restore only. The matching save runs at the end of the job, gated on the + # job having succeeded. + # + # `actions/cache` saves in a post step that runs however the job ended, so a + # run cancelled mid-build — which `cancel-in-progress` above makes routine — + # can publish a `.turbo` holding a task entry whose recorded outputs are + # incomplete. A later run restores it, reports `cache hit, replaying logs`, + # and lays down a `dist/` missing files the rest of the build imports. The + # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package + # nobody changed. + # + # Re-running cannot clear it: the key is the commit SHA, so a second attempt + # scores an exact hit on the same entry. Splitting restore from save is the + # supported way to make the save conditional (`save-always` was deprecated + # for this). + # + # The `v2-` prefix retires every entry written under the old scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # so entries already poisoned would otherwise keep being selected until they + # aged out. + - name: Restore Turbo cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -209,3 +229,13 @@ jobs: >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY cat "$NOTES_FILE" >> $GITHUB_STEP_SUMMARY + + # Only a job that finished its work publishes what it learned. A cancelled or + # failed run leaves the cache as it found it, so nothing it half-built can be + # restored by a later one. + - name: Save Turbo cache + if: success() + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .turbo + key: turbo-v2-${{ runner.os }}-${{ github.sha }} From 1dee8275733d66c59f002ba6f9db08f2887993a5 Mon Sep 17 00:00:00 2001 From: Mobeen Abdullah Date: Tue, 11 Aug 2026 17:08:35 +0500 Subject: [PATCH 2/2] ci(root): give each job its own turbo cache lineage --- .github/workflows/ci.yml | 90 ++++++++++++++++--------------- .github/workflows/integration.yml | 90 ++++++++++++++++--------------- .github/workflows/preview.yml | 45 ++++++++-------- .github/workflows/release.yml | 45 ++++++++-------- 4 files changed, 144 insertions(+), 126 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ad1aa96b7..c7d57aa87b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,33 +40,35 @@ jobs: cache: pnpm registry-url: https://registry.npmjs.org - # Restore only. The matching save runs at the end of the job, gated on the - # job having succeeded. + # Scoped to the JOB, which is the part that matters. Six jobs across four + # workflows cache `.turbo`, they run concurrently on the same commit, and + # they build different filter sets — this one builds every package and app, + # `preview` builds only packages, `integration` builds five of them. Keyed on + # the commit alone they all wrote one entry, so whichever finished first + # decided what every later reader got. # - # `actions/cache` saves in a post step that runs however the job ended, so a - # run cancelled mid-build — which `cancel-in-progress` above makes routine — - # can publish a `.turbo` holding a task entry whose recorded outputs are - # incomplete. A later run restores it, reports `cache hit, replaying logs`, - # and lays down a `dist/` missing files the rest of the build imports. The - # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package - # nobody changed. + # A reader then restores a `.turbo` shaped by someone else's build, reports + # `cache hit, replaying logs`, and lays down a `dist/` that does not contain + # what its own tests import. The symptom is `Cannot find module + # '.../dist/chunk-.mjs'` in a package the pull request never touched. + # Re-running cannot clear it: the key is the commit, so a second attempt + # scores an exact hit on the same foreign entry. # - # Re-running cannot clear it: the key is the commit SHA, so a second attempt - # scores an exact hit on the same entry. Splitting restore from save is the - # supported way to make the save conditional (`save-always` was deprecated - # for this). + # `github.job` gives each job its own lineage. Nothing is lost — a job's own + # previous runs are what its cache was ever useful for, and turbo entries are + # content-addressed, so a narrower cache is a slower run and never a wrong + # one. # - # The `v2-` prefix retires every entry written under the old scheme in one + # The `v2-` prefix retires every entry written under the shared scheme in one # step. Cache keys are immutable and `restore-keys` prefers the newest match, - # so entries already poisoned would otherwise keep being selected until they - # aged out. + # so the entries already written would otherwise keep being selected. - name: Restore Turbo cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-v2-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -217,14 +219,15 @@ jobs: TURBO_CACHE_DIR: .turbo # Only a job that finished its work publishes what it learned. A cancelled or - # failed run leaves the cache as it found it, so nothing it half-built can be - # restored by a later one. + # failed run leaves the cache as it found it — `actions/cache` saves in a post + # step that runs however the job ended, and splitting restore from save is the + # supported way to make that conditional (`save-always` was deprecated for it). - name: Save Turbo cache if: success() uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} # The admin, in a real browser, against a real server and a real database. # Covers the class of failure the unit and integration suites structurally @@ -259,33 +262,35 @@ jobs: node-version-file: .nvmrc cache: pnpm - # Restore only. The matching save runs at the end of the job, gated on the - # job having succeeded. + # Scoped to the JOB, which is the part that matters. Six jobs across four + # workflows cache `.turbo`, they run concurrently on the same commit, and + # they build different filter sets — this one builds every package and app, + # `preview` builds only packages, `integration` builds five of them. Keyed on + # the commit alone they all wrote one entry, so whichever finished first + # decided what every later reader got. # - # `actions/cache` saves in a post step that runs however the job ended, so a - # run cancelled mid-build — which `cancel-in-progress` above makes routine — - # can publish a `.turbo` holding a task entry whose recorded outputs are - # incomplete. A later run restores it, reports `cache hit, replaying logs`, - # and lays down a `dist/` missing files the rest of the build imports. The - # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package - # nobody changed. + # A reader then restores a `.turbo` shaped by someone else's build, reports + # `cache hit, replaying logs`, and lays down a `dist/` that does not contain + # what its own tests import. The symptom is `Cannot find module + # '.../dist/chunk-.mjs'` in a package the pull request never touched. + # Re-running cannot clear it: the key is the commit, so a second attempt + # scores an exact hit on the same foreign entry. # - # Re-running cannot clear it: the key is the commit SHA, so a second attempt - # scores an exact hit on the same entry. Splitting restore from save is the - # supported way to make the save conditional (`save-always` was deprecated - # for this). + # `github.job` gives each job its own lineage. Nothing is lost — a job's own + # previous runs are what its cache was ever useful for, and turbo entries are + # content-addressed, so a narrower cache is a slower run and never a wrong + # one. # - # The `v2-` prefix retires every entry written under the old scheme in one + # The `v2-` prefix retires every entry written under the shared scheme in one # step. Cache keys are immutable and `restore-keys` prefers the newest match, - # so entries already poisoned would otherwise keep being selected until they - # aged out. + # so the entries already written would otherwise keep being selected. - name: Restore Turbo cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-v2-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -321,14 +326,15 @@ jobs: retention-days: 7 # Only a job that finished its work publishes what it learned. A cancelled or - # failed run leaves the cache as it found it, so nothing it half-built can be - # restored by a later one. + # failed run leaves the cache as it found it — `actions/cache` saves in a post + # step that runs however the job ended, and splitting restore from save is the + # supported way to make that conditional (`save-always` was deprecated for it). - name: Save Turbo cache if: success() uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} # Cross-platform smoke test for the scaffolder. Catches Windows path # separator bugs and macOS-specific issues that only surface in fresh diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 0221e8f8b2..e81f0528f0 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -90,33 +90,35 @@ jobs: node-version-file: .nvmrc cache: pnpm - # Restore only. The matching save runs at the end of the job, gated on the - # job having succeeded. + # Scoped to the JOB, which is the part that matters. Six jobs across four + # workflows cache `.turbo`, they run concurrently on the same commit, and + # they build different filter sets — this one builds every package and app, + # `preview` builds only packages, `integration` builds five of them. Keyed on + # the commit alone they all wrote one entry, so whichever finished first + # decided what every later reader got. # - # `actions/cache` saves in a post step that runs however the job ended, so a - # run cancelled mid-build — which `cancel-in-progress` above makes routine — - # can publish a `.turbo` holding a task entry whose recorded outputs are - # incomplete. A later run restores it, reports `cache hit, replaying logs`, - # and lays down a `dist/` missing files the rest of the build imports. The - # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package - # nobody changed. + # A reader then restores a `.turbo` shaped by someone else's build, reports + # `cache hit, replaying logs`, and lays down a `dist/` that does not contain + # what its own tests import. The symptom is `Cannot find module + # '.../dist/chunk-.mjs'` in a package the pull request never touched. + # Re-running cannot clear it: the key is the commit, so a second attempt + # scores an exact hit on the same foreign entry. # - # Re-running cannot clear it: the key is the commit SHA, so a second attempt - # scores an exact hit on the same entry. Splitting restore from save is the - # supported way to make the save conditional (`save-always` was deprecated - # for this). + # `github.job` gives each job its own lineage. Nothing is lost — a job's own + # previous runs are what its cache was ever useful for, and turbo entries are + # content-addressed, so a narrower cache is a slower run and never a wrong + # one. # - # The `v2-` prefix retires every entry written under the old scheme in one + # The `v2-` prefix retires every entry written under the shared scheme in one # step. Cache keys are immutable and `restore-keys` prefers the newest match, - # so entries already poisoned would otherwise keep being selected until they - # aged out. + # so the entries already written would otherwise keep being selected. - name: Restore Turbo cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-v2-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -156,14 +158,15 @@ jobs: TURBO_CACHE_DIR: .turbo # Only a job that finished its work publishes what it learned. A cancelled or - # failed run leaves the cache as it found it, so nothing it half-built can be - # restored by a later one. + # failed run leaves the cache as it found it — `actions/cache` saves in a post + # step that runs however the job ended, and splitting restore from save is the + # supported way to make that conditional (`save-always` was deprecated for it). - name: Save Turbo cache if: success() uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} # sqlite runs in-process against a throwaway file, so this job declares no # `services:` at all. Keeping it out of the matrix above means it never pulls @@ -191,33 +194,35 @@ jobs: node-version-file: .nvmrc cache: pnpm - # Restore only. The matching save runs at the end of the job, gated on the - # job having succeeded. + # Scoped to the JOB, which is the part that matters. Six jobs across four + # workflows cache `.turbo`, they run concurrently on the same commit, and + # they build different filter sets — this one builds every package and app, + # `preview` builds only packages, `integration` builds five of them. Keyed on + # the commit alone they all wrote one entry, so whichever finished first + # decided what every later reader got. # - # `actions/cache` saves in a post step that runs however the job ended, so a - # run cancelled mid-build — which `cancel-in-progress` above makes routine — - # can publish a `.turbo` holding a task entry whose recorded outputs are - # incomplete. A later run restores it, reports `cache hit, replaying logs`, - # and lays down a `dist/` missing files the rest of the build imports. The - # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package - # nobody changed. + # A reader then restores a `.turbo` shaped by someone else's build, reports + # `cache hit, replaying logs`, and lays down a `dist/` that does not contain + # what its own tests import. The symptom is `Cannot find module + # '.../dist/chunk-.mjs'` in a package the pull request never touched. + # Re-running cannot clear it: the key is the commit, so a second attempt + # scores an exact hit on the same foreign entry. # - # Re-running cannot clear it: the key is the commit SHA, so a second attempt - # scores an exact hit on the same entry. Splitting restore from save is the - # supported way to make the save conditional (`save-always` was deprecated - # for this). + # `github.job` gives each job its own lineage. Nothing is lost — a job's own + # previous runs are what its cache was ever useful for, and turbo entries are + # content-addressed, so a narrower cache is a slower run and never a wrong + # one. # - # The `v2-` prefix retires every entry written under the old scheme in one + # The `v2-` prefix retires every entry written under the shared scheme in one # step. Cache keys are immutable and `restore-keys` prefers the newest match, - # so entries already poisoned would otherwise keep being selected until they - # aged out. + # so the entries already written would otherwise keep being selected. - name: Restore Turbo cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-v2-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -233,11 +238,12 @@ jobs: TURBO_CACHE_DIR: .turbo # Only a job that finished its work publishes what it learned. A cancelled or - # failed run leaves the cache as it found it, so nothing it half-built can be - # restored by a later one. + # failed run leaves the cache as it found it — `actions/cache` saves in a post + # step that runs however the job ended, and splitting restore from save is the + # supported way to make that conditional (`save-always` was deprecated for it). - name: Save Turbo cache if: success() uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index bfb63adc6b..42611acd2c 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -44,33 +44,35 @@ jobs: node-version-file: .nvmrc cache: pnpm - # Restore only. The matching save runs at the end of the job, gated on the - # job having succeeded. + # Scoped to the JOB, which is the part that matters. Six jobs across four + # workflows cache `.turbo`, they run concurrently on the same commit, and + # they build different filter sets — this one builds every package and app, + # `preview` builds only packages, `integration` builds five of them. Keyed on + # the commit alone they all wrote one entry, so whichever finished first + # decided what every later reader got. # - # `actions/cache` saves in a post step that runs however the job ended, so a - # run cancelled mid-build — which `cancel-in-progress` above makes routine — - # can publish a `.turbo` holding a task entry whose recorded outputs are - # incomplete. A later run restores it, reports `cache hit, replaying logs`, - # and lays down a `dist/` missing files the rest of the build imports. The - # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package - # nobody changed. + # A reader then restores a `.turbo` shaped by someone else's build, reports + # `cache hit, replaying logs`, and lays down a `dist/` that does not contain + # what its own tests import. The symptom is `Cannot find module + # '.../dist/chunk-.mjs'` in a package the pull request never touched. + # Re-running cannot clear it: the key is the commit, so a second attempt + # scores an exact hit on the same foreign entry. # - # Re-running cannot clear it: the key is the commit SHA, so a second attempt - # scores an exact hit on the same entry. Splitting restore from save is the - # supported way to make the save conditional (`save-always` was deprecated - # for this). + # `github.job` gives each job its own lineage. Nothing is lost — a job's own + # previous runs are what its cache was ever useful for, and turbo entries are + # content-addressed, so a narrower cache is a slower run and never a wrong + # one. # - # The `v2-` prefix retires every entry written under the old scheme in one + # The `v2-` prefix retires every entry written under the shared scheme in one # step. Cache keys are immutable and `restore-keys` prefers the newest match, - # so entries already poisoned would otherwise keep being selected until they - # aged out. + # so the entries already written would otherwise keep being selected. - name: Restore Turbo cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-v2-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -91,11 +93,12 @@ jobs: run: pnpm dlx pkg-pr-new publish --pnpm --commentWithSha './packages/*' # Only a job that finished its work publishes what it learned. A cancelled or - # failed run leaves the cache as it found it, so nothing it half-built can be - # restored by a later one. + # failed run leaves the cache as it found it — `actions/cache` saves in a post + # step that runs however the job ended, and splitting restore from save is the + # supported way to make that conditional (`save-always` was deprecated for it). - name: Save Turbo cache if: success() uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a91f0fa0b8..e408d49095 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,33 +67,35 @@ jobs: - name: Upgrade npm to >=11.5.1 for trusted publishing run: npm install -g npm@11.18.0 - # Restore only. The matching save runs at the end of the job, gated on the - # job having succeeded. + # Scoped to the JOB, which is the part that matters. Six jobs across four + # workflows cache `.turbo`, they run concurrently on the same commit, and + # they build different filter sets — this one builds every package and app, + # `preview` builds only packages, `integration` builds five of them. Keyed on + # the commit alone they all wrote one entry, so whichever finished first + # decided what every later reader got. # - # `actions/cache` saves in a post step that runs however the job ended, so a - # run cancelled mid-build — which `cancel-in-progress` above makes routine — - # can publish a `.turbo` holding a task entry whose recorded outputs are - # incomplete. A later run restores it, reports `cache hit, replaying logs`, - # and lays down a `dist/` missing files the rest of the build imports. The - # symptom is `Cannot find module '.../dist/chunk-.mjs'` in a package - # nobody changed. + # A reader then restores a `.turbo` shaped by someone else's build, reports + # `cache hit, replaying logs`, and lays down a `dist/` that does not contain + # what its own tests import. The symptom is `Cannot find module + # '.../dist/chunk-.mjs'` in a package the pull request never touched. + # Re-running cannot clear it: the key is the commit, so a second attempt + # scores an exact hit on the same foreign entry. # - # Re-running cannot clear it: the key is the commit SHA, so a second attempt - # scores an exact hit on the same entry. Splitting restore from save is the - # supported way to make the save conditional (`save-always` was deprecated - # for this). + # `github.job` gives each job its own lineage. Nothing is lost — a job's own + # previous runs are what its cache was ever useful for, and turbo entries are + # content-addressed, so a narrower cache is a slower run and never a wrong + # one. # - # The `v2-` prefix retires every entry written under the old scheme in one + # The `v2-` prefix retires every entry written under the shared scheme in one # step. Cache keys are immutable and `restore-keys` prefers the newest match, - # so entries already poisoned would otherwise keep being selected until they - # aged out. + # so the entries already written would otherwise keep being selected. - name: Restore Turbo cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-v2-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -231,11 +233,12 @@ jobs: cat "$NOTES_FILE" >> $GITHUB_STEP_SUMMARY # Only a job that finished its work publishes what it learned. A cancelled or - # failed run leaves the cache as it found it, so nothing it half-built can be - # restored by a later one. + # failed run leaves the cache as it found it — `actions/cache` saves in a post + # step that runs however the job ended, and splitting restore from save is the + # supported way to make that conditional (`save-always` was deprecated for it). - name: Save Turbo cache if: success() uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .turbo - key: turbo-v2-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }}