diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68a6fe9d96..c7d57aa87b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,13 +40,35 @@ jobs: cache: pnpm registry-url: https://registry.npmjs.org - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # 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. + # + # 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. + # + # `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 shared scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # 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-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -196,6 +218,17 @@ 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 — `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.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 # cannot: jsdom has no layout engine, so a column that grew to 1024px or a @@ -229,13 +262,35 @@ jobs: node-version-file: .nvmrc cache: pnpm - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # 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. + # + # 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. + # + # `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 shared scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # 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-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -270,6 +325,17 @@ 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 — `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.job }}-${{ 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..e81f0528f0 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -90,13 +90,35 @@ jobs: node-version-file: .nvmrc cache: pnpm - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # 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. + # + # 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. + # + # `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 shared scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # 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-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -135,6 +157,17 @@ 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 — `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.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 # the postgres/mysql images it would not connect to, which removes its only @@ -161,13 +194,35 @@ jobs: node-version-file: .nvmrc cache: pnpm - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # 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. + # + # 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. + # + # `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 shared scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # 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-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -181,3 +236,14 @@ 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 — `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.job }}-${{ github.sha }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 130b81f40f..42611acd2c 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -44,13 +44,35 @@ jobs: node-version-file: .nvmrc cache: pnpm - - name: Cache Turbo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + # 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. + # + # 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. + # + # `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 shared scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # 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-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -69,3 +91,14 @@ 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 — `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.job }}-${{ github.sha }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e65d10948..e408d49095 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,13 +67,35 @@ 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 + # 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. + # + # 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. + # + # `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 shared scheme in one + # step. Cache keys are immutable and `restore-keys` prefers the newest match, + # 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-${{ runner.os }}-${{ github.sha }} + key: turbo-v2-${{ runner.os }}-${{ github.job }}-${{ github.sha }} restore-keys: | - turbo-${{ runner.os }}- + turbo-v2-${{ runner.os }}-${{ github.job }}- - name: Install dependencies run: pnpm install --frozen-lockfile @@ -209,3 +231,14 @@ 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 — `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.job }}-${{ github.sha }}