Skip to content

Commit 8082a8d

Browse files
authored
ci: pin pnpm store path across runner fleets (#6414)
* ci: pin pnpm store path across runner fleets Nexu ARC resolved the store under _work while hosted/seed used the home store, so actions/cache version hashes diverged and every Nexu job missed. Force $HOME/.pnpm-store and add restore-keys prefix fallback. * test(e2e): lock stable pnpm store path and restore-keys Add regression assertions for the non-persistent home store pin and OS-prefix restore-keys so the cross-fleet cache contract cannot regress silently. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * ci: seed Linux pnpm cache from cache-maintenance Pinning the store path alone cannot hit until a main job saves under the new actions/cache version. Add a single ubuntu-24.04 seed job; the cache is repo-scoped so every Nexu size reuses it.
1 parent 125f373 commit 8082a8d

3 files changed

Lines changed: 75 additions & 10 deletions

File tree

.github/actions/setup-workspace/action.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,17 @@ runs:
4747
echo "enabled=true" >> "$GITHUB_OUTPUT"
4848
echo "path=$store_dir" >> "$GITHUB_OUTPUT"
4949
else
50+
# Force a stable home store path so actions/cache version hashes match
51+
# across fleets (hosted vs Nexu ARC). ARC may copy instead of hardlink;
52+
# that is acceptable for cache hit stability.
53+
store_dir="$HOME/.pnpm-store"
54+
mkdir -p "$store_dir"
55+
{
56+
echo "NPM_CONFIG_STORE_DIR=$store_dir"
57+
echo "npm_config_store_dir=$store_dir"
58+
} >> "$GITHUB_ENV"
5059
echo "enabled=false" >> "$GITHUB_OUTPUT"
60+
echo "path=$store_dir" >> "$GITHUB_OUTPUT"
5161
fi
5262
5363
- name: Setup pnpm
@@ -73,6 +83,8 @@ runs:
7383
with:
7484
path: ${{ steps.pnpm-store.outputs.path }}
7585
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
86+
restore-keys: |
87+
pnpm-store-${{ runner.os }}-
7688
7789
- name: Install dependencies
7890
shell: bash

.github/workflows/cache-maintenance.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@ jobs:
2929
with:
3030
save-pnpm-cache: 'true'
3131

32+
33+
# GHA actions/cache is repo-scoped and shared across hosted + every Nexu
34+
# size (small/medium/large). One Linux seed under the pinned home store path
35+
# is enough for the whole fleet; no per-size matrix required.
36+
seed-pnpm-linux:
37+
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
38+
runs-on: ubuntu-24.04
39+
timeout-minutes: 15
40+
steps:
41+
- name: Checkout main
42+
uses: actions/checkout@v6.0.2
43+
44+
- name: Seed Linux pnpm cache
45+
uses: ./.github/actions/setup-workspace
46+
with:
47+
save-pnpm-cache: 'true'
48+
3249
clean-closed-pr-buildkit:
3350
if: github.event_name == 'pull_request_target' && github.repository == 'nexu-io/open-design'
3451
runs-on: ubuntu-24.04

e2e/tests/actions-cache-workflows.test.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,33 @@ describe("GitHub Actions cache workflows", () => {
4343
action.indexOf("uses: actions/cache/save@v5"),
4444
);
4545

46+
// Non-persistent branch: pin a stable home store before `pnpm store path`
47+
// so actions/cache version hashes match across hosted and Nexu ARC fleets.
48+
const detectStep = sectionBetween(
49+
action,
50+
"- name: Detect persistent pnpm store",
51+
"- name: Setup pnpm",
52+
);
53+
// Isolate the else arm by its home-store pin (not "else"/"fi", which match
54+
// substrings like npm_config).
55+
const homeStoreIndex = detectStep.indexOf('store_dir="$HOME/.pnpm-store"');
56+
expect(homeStoreIndex).toBeGreaterThanOrEqual(0);
57+
const nonPersistentBranch = detectStep.slice(homeStoreIndex);
58+
expect(nonPersistentBranch).toContain('echo "NPM_CONFIG_STORE_DIR=$store_dir"');
59+
expect(nonPersistentBranch).toContain('echo "npm_config_store_dir=$store_dir"');
60+
expect(nonPersistentBranch).toContain('echo "enabled=false"');
61+
expect(action.indexOf('store_dir="$HOME/.pnpm-store"')).toBeLessThan(
62+
action.indexOf('run: echo "path=$(pnpm store path --silent)"'),
63+
);
64+
65+
const restoreStep = sectionBetween(
66+
action,
67+
"- name: Restore pnpm store",
68+
"- name: Install dependencies",
69+
);
70+
expect(restoreStep).toContain("restore-keys: |");
71+
expect(restoreStep).toContain("pnpm-store-${{ runner.os }}-");
72+
4673
const saveStep = action.slice(action.indexOf("- name: Save pnpm store"));
4774
expect(saveStep).toContain("inputs.save-pnpm-cache == 'true'");
4875
expect(saveStep).toContain("steps.persistent-pnpm-store.outputs.enabled != 'true'");
@@ -53,9 +80,10 @@ describe("GitHub Actions cache workflows", () => {
5380
expect(saveStep).toContain("github.event_name == 'schedule'");
5481
});
5582

56-
it("[P1] seeds Windows from main and deletes only closed-PR BuildKit cache families", async () => {
83+
it("[P1] seeds Windows and Linux from main and deletes only closed-PR BuildKit cache families", async () => {
5784
const workflow = await readFile(cacheMaintenanceWorkflow, "utf8");
58-
const seedJob = sectionBetween(workflow, " seed-pnpm-windows:", " clean-closed-pr-buildkit:");
85+
const windowsSeedJob = sectionBetween(workflow, " seed-pnpm-windows:", " seed-pnpm-linux:");
86+
const linuxSeedJob = sectionBetween(workflow, " seed-pnpm-linux:", " clean-closed-pr-buildkit:");
5987
const cleanupJob = workflow.slice(workflow.indexOf(" clean-closed-pr-buildkit:"));
6088

6189
expect(workflow).toContain("pull_request_target:");
@@ -66,13 +94,19 @@ describe("GitHub Actions cache workflows", () => {
6694
expect(workflow).toContain("actions: write");
6795
expect(workflow).toContain("contents: read");
6896

69-
expect(seedJob).toContain("github.event_name == 'push'");
70-
expect(seedJob).toContain("github.event_name == 'workflow_dispatch'");
71-
expect(seedJob).toContain("github.ref == 'refs/heads/main'");
72-
expect(seedJob).toContain("runs-on: windows-latest");
73-
expect(seedJob).toContain("uses: actions/checkout@v6.0.2");
74-
expect(seedJob).toContain("uses: ./.github/actions/setup-workspace");
75-
expect(seedJob).toContain("save-pnpm-cache: 'true'");
97+
for (const seedJob of [windowsSeedJob, linuxSeedJob]) {
98+
expect(seedJob).toContain("github.event_name == 'push'");
99+
expect(seedJob).toContain("github.event_name == 'workflow_dispatch'");
100+
expect(seedJob).toContain("github.ref == 'refs/heads/main'");
101+
expect(seedJob).toContain("uses: actions/checkout@v6.0.2");
102+
expect(seedJob).toContain("uses: ./.github/actions/setup-workspace");
103+
expect(seedJob).toContain("save-pnpm-cache: 'true'");
104+
}
105+
106+
expect(windowsSeedJob).toContain("runs-on: windows-latest");
107+
// One hosted Linux seed is enough: actions/cache is repo-scoped and shared
108+
// by every Nexu size (small/medium/large) once the store path is pinned.
109+
expect(linuxSeedJob).toContain("runs-on: ubuntu-24.04");
76110

77111
expect(cleanupJob).toContain("github.event_name == 'pull_request_target'");
78112
expect(cleanupJob).toContain("refs/pull/${{ github.event.pull_request.number }}/merge");
@@ -85,7 +119,8 @@ describe("GitHub Actions cache workflows", () => {
85119
expect(cleanupJob).not.toContain("github.event.pull_request.head");
86120
});
87121

88-
it("[P1] uses the existing main visual baseline as the Linux pnpm seed", async () => {
122+
123+
it("[P1] keeps visual-baseline as a secondary Linux pnpm seed on main", async () => {
89124
const workflow = await readFile(visualBaselineWorkflow, "utf8");
90125
const setupStep = sectionBetween(workflow, " - name: Setup workspace", " - name: Setup Playwright");
91126

@@ -95,6 +130,7 @@ describe("GitHub Actions cache workflows", () => {
95130
expect(setupStep).toContain("save-pnpm-cache: 'true'");
96131
});
97132

133+
98134
it("[P1] keeps landing preview caches restore-only before merge and seeds them from main", async () => {
99135
const workflow = await readFile(landingPageCiWorkflow, "utf8");
100136

0 commit comments

Comments
 (0)