Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Every npm install in g1t's workflows starts from npm's downloads for the whole lockfile, rather than from Wrangler's alone. All of them restored and saved one key, npm-Linux-<lockfile hash>, and the first job to finish on main (Deploy's check, which installs only the root) saved its 73 MB under it; a key is written once, so every later job restored those 73 MB and downloaded the rest of its install through the runner's egress, where downloads were breaking off. The key is now npm-full-, and only jobs that install the whole lockfile save it: CI's TypeScript and Build jobs, and the desktop release's Linux builds. Every job that installs part of it (Deploy's check, migrate and stages, the mobile, CLI and desktop publish jobs, the runner release) only restores. On main, Deploy's new npm-cache job keeps it: it looks the key up without downloading, and when this lockfile has none it starts from the last lockfile's, downloads every package with scripts off and saves them; nothing waits for it, and a failed download leaves the cache as it was and the run green. A test holds every workflow to one npm key and to saving it only from whole installs.

syntaqxcommitted Parent1bb793aBrowse files
8 files+127−380/8 viewed
+4−4
7979 uses: actions/cache@v4
8080 with:
8181 path: ~/.npm
82− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
83− restore-keys: npm-${{ runner.os }}-
82+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
83+ restore-keys: npm-full-${{ runner.os }}-
8484 - name: Install
8585 run: npm ci --prefer-offline --no-audit --no-fund
8686 - name: Type checks
110110 uses: actions/cache@v4
111111 with:
112112 path: ~/.npm
113− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
114− restore-keys: npm-${{ runner.os }}-
113+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
114+ restore-keys: npm-full-${{ runner.os }}-
115115 - name: Install
116116 run: npm ci --prefer-offline --no-audit --no-fund
117117 - name: The site, sudo and the docs
+3−3
7474 node scripts/cli-release.mjs sign
7575 node scripts/cli-release.mjs verify
7676 - name: Cache npm's downloads
77− uses: actions/cache@v4
77+ uses: actions/cache/restore@v4
7878 with:
7979 path: ~/.npm
80− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
81− restore-keys: npm-${{ runner.os }}-
80+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
81+ restore-keys: npm-full-${{ runner.os }}-
8282 - name: Install Wrangler
8383 run: npm ci --prefer-offline --workspaces=false --no-audit --no-fund
8484 - name: Publish to g1t.sh/downloads/cli
+55−14
7070 timeout-minutes: 20
7171 steps:
7272 - uses: actions/checkout@v5
73− # npm's downloads, kept between runs: an install reads them first
74− # and asks the registry only for what changed, so a dropped
75− # connection to it rarely stops the job.
73+ # npm's downloads for the whole lockfile, which the npm-cache job
74+ # keeps: an install reads them first and asks the registry only for
75+ # what changed, so a dropped connection to it rarely stops the job.
76+ # Only restored: a job that installs part of the lockfile never
77+ # saves, or its part would stand in for the whole under that key.
7678 - name: Cache npm's downloads
77− uses: actions/cache@v4
79+ uses: actions/cache/restore@v4
7880 with:
7981 path: ~/.npm
80− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
81− restore-keys: npm-${{ runner.os }}-
82+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
83+ restore-keys: npm-full-${{ runner.os }}-
8284 - name: Install Wrangler
8385 run: npm ci --prefer-offline --workspaces=false --no-audit --no-fund
8486 - name: The manifest matches every wrangler.jsonc
8688 - name: The deploy tool's tests
8789 run: npm run test:deploy
8890
91+ # npm's downloads for the whole lockfile, saved once for each lockfile
92+ # on main, which every install of this and the other workflows starts
93+ # from. Before, every job saved its own part under one key, and the
94+ # first to finish (Wrangler alone, 73 MB) stood in for the rest: each
95+ # stage then downloaded hundreds of MB through the runner's egress,
96+ # 50 MB tarballs among them. Nothing waits for this job, and a failed
97+ # download only leaves the cache as it was.
98+ npm-cache:
99+ name: npm's downloads
100+ runs-on: ubuntu-latest
101+ timeout-minutes: 20
102+ steps:
103+ - uses: actions/checkout@v5
104+ - name: Look for this lockfile's downloads
105+ id: saved
106+ uses: actions/cache/restore@v4
107+ with:
108+ path: ~/.npm
109+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
110+ lookup-only: true
111+ - name: Start from the last lockfile's
112+ if: ${{ steps.saved.outputs.cache-hit != 'true' }}
113+ uses: actions/cache/restore@v4
114+ with:
115+ path: ~/.npm
116+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
117+ restore-keys: npm-full-${{ runner.os }}-
118+ - name: Download every package
119+ id: download
120+ if: ${{ steps.saved.outputs.cache-hit != 'true' }}
121+ continue-on-error: true
122+ run: npm ci --prefer-offline --ignore-scripts --no-audit --no-fund
123+ - name: Save them
124+ if: ${{ steps.saved.outputs.cache-hit != 'true' && steps.download.outcome == 'success' }}
125+ uses: actions/cache/save@v4
126+ with:
127+ path: ~/.npm
128+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
129+
89130 # Runs beside check: nothing deploys until both have succeeded.
90131 plan:
91132 name: Plan
138179 # and asks the registry only for what changed, so a dropped
139180 # connection to it rarely stops the job.
140181 - name: Cache npm's downloads
141− uses: actions/cache@v4
182+ uses: actions/cache/restore@v4
142183 with:
143184 path: ~/.npm
144− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
145− restore-keys: npm-${{ runner.os }}-
185+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
186+ restore-keys: npm-full-${{ runner.os }}-
146187 - name: Install Wrangler
147188 run: npm ci --prefer-offline --workspaces=false --no-audit --no-fund
148189 - name: Apply pending migrations
246287 - name: sccache
247288 if: ${{ matrix.rust || matrix.image }}
248289 run: bash scripts/sccache.sh install
249− # npm's downloads, kept between runs as the check and migrate jobs
250− # keep them: the install reads them first (--prefer-offline, in
290+ # npm's downloads for the whole lockfile, only restored, as the
291+ # check job restores them: the install reads them first (--prefer-offline, in
251292 # scripts/deploy/stack.mjs) and asks the registry only for what
252293 # changed, so a connection it drops mid-install rarely stops a stage.
253294 - name: Cache npm's downloads
254− uses: actions/cache@v4
295+ uses: actions/cache/restore@v4
255296 with:
256297 path: ~/.npm
257− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
258− restore-keys: npm-${{ runner.os }}-
298+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
299+ restore-keys: npm-full-${{ runner.os }}-
259300 - name: Install
260301 run: node scripts/deploy.mjs install --only "${{ matrix.units }}"
261302 - name: Deploy ${{ matrix.units }}
+7−7
9797 uses: actions/cache@v4
9898 with:
9999 path: ~/.npm
100− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
101− restore-keys: npm-${{ runner.os }}-
100+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
101+ restore-keys: npm-full-${{ runner.os }}-
102102 - name: Install
103103 run: npm ci --prefer-offline --no-audit --no-fund
104104 - name: Build and sign
212212 uses: actions/cache@v4
213213 with:
214214 path: ~/.npm
215− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
216− restore-keys: npm-${{ runner.os }}-
215+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
216+ restore-keys: npm-full-${{ runner.os }}-
217217 - name: Install
218218 run: npm ci --prefer-offline --no-audit --no-fund
219219 - name: Build and sign
288288 # and asks the registry only for what changed, so a dropped
289289 # connection to it rarely stops the job.
290290 - name: Cache npm's downloads
291− uses: actions/cache@v4
291+ uses: actions/cache/restore@v4
292292 with:
293293 path: ~/.npm
294− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
295− restore-keys: npm-${{ runner.os }}-
294+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
295+ restore-keys: npm-full-${{ runner.os }}-
296296 - name: Install Wrangler
297297 run: npm ci --prefer-offline --workspaces=false --no-audit --no-fund
298298 - name: The manifest
+3−3
5757 steps:
5858 - uses: actions/checkout@v5
5959 - name: Cache npm's downloads
60− uses: actions/cache@v4
60+ uses: actions/cache/restore@v4
6161 with:
6262 path: ~/.npm
63− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
64− restore-keys: npm-${{ runner.os }}-
63+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
64+ restore-keys: npm-full-${{ runner.os }}-
6565 # EAS reads app.config.ts through the app's own `expo`.
6666 - name: Install the app's dependencies
6767 run: npm ci --prefer-offline --no-audit --no-fund -w @g1t/mobile
+3−3
6464 # and asks the registry only for what changed, so a dropped
6565 # connection to it rarely stops the job.
6666 - name: Cache npm's downloads
67− uses: actions/cache@v4
67+ uses: actions/cache/restore@v4
6868 with:
6969 path: ~/.npm
70− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
71− restore-keys: npm-${{ runner.os }}-
70+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
71+ restore-keys: npm-full-${{ runner.os }}-
7272 # Wrangler, from the root, and the app's own (EAS reads app.config.ts
7373 # through its `expo`).
7474 - name: Install Wrangler and the app
+3−3
245245 # and asks the registry only for what changed, so a dropped
246246 # connection to it rarely stops the job.
247247 - name: Cache npm's downloads
248− uses: actions/cache@v4
248+ uses: actions/cache/restore@v4
249249 with:
250250 path: ~/.npm
251− key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
252− restore-keys: npm-${{ runner.os }}-
251+ key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
252+ restore-keys: npm-full-${{ runner.os }}-
253253 - name: Install Wrangler
254254 run: npm ci --prefer-offline --workspaces=false --no-audit --no-fund
255255 - name: Publish to g1t.sh/downloads/runner
+49−1
125125 let concurrency = deploy.concurrency.as_ref().unwrap();
126126 assert_eq!(concurrency.group, "deploy-production");
127127 assert_eq!(concurrency.cancel_in_progress, json!(false));
128− assert_eq!(deploy.job_order(), ["check", "plan", "migrate", "core", "edge", "front", "smoke"]);
128+ assert_eq!(deploy.job_order(), ["check", "npm-cache", "plan", "migrate", "core", "edge", "front", "smoke"]);
129+ // Nothing waits for npm's downloads to be saved.
130+ assert!(deploy.jobs.iter().all(|job| !job.needs.iter().any(|n| n == "npm-cache")));
129131 // The stages share their steps (a YAML alias), and read the token only
130132 // where they deploy.
131133 let steps = |id: &str| deploy.jobs.iter().find(|j| j.id == id).unwrap().steps.len();
135137 assert!(!source.contains("cancel-in-progress: true"));
136138 }
137139
140+/// Whether a step's `run` installs the whole lockfile: `npm ci` with no
141+/// workspace narrowing it, and not the deploy tool's install of some units.
142+fn installs_everything(run: &str) -> bool {
143+ run.split('\n').any(|line| {
144+ let line = line.trim();
145+ line.starts_with("npm ci") && !line.contains(" -w ") && !line.contains("--workspace")
146+ })
147+}
148+
149+#[test]
150+fn npm_downloads_are_cached_whole_and_only_whole_installs_save_them() {
151+ // Every job's npm cache is the whole lockfile's, under one key. A job
152+ // that installs part of it only restores: had it saved, its part would
153+ // stand in for the whole under that key (the first job to finish used
154+ // to save Wrangler alone, and every later install downloaded the rest).
155+ let mut saving = Vec::new();
156+ for entry in std::fs::read_dir(workflows_dir()).unwrap() {
157+ let path = entry.unwrap().path();
158+ if path.extension().and_then(|e| e.to_str()) != Some("yml") {
159+ continue;
160+ }
161+ let name = path.file_name().unwrap().to_string_lossy().to_string();
162+ for job in read(&name).jobs {
163+ let whole = job.steps.iter().any(|s| s.run.as_deref().is_some_and(installs_everything));
164+ for step in &job.steps {
165+ let Some(uses) = step.uses.as_deref() else { continue };
166+ if !uses.starts_with("actions/cache") || step.raw["with"]["path"] != json!("~/.npm") {
167+ continue;
168+ }
169+ let key = step.raw["with"]["key"].as_str().unwrap_or_default();
170+ assert_eq!(key, "npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}", "{name} {}", job.id);
171+ if let Some(prefix) = step.raw["with"]["restore-keys"].as_str() {
172+ assert_eq!(prefix, "npm-full-${{ runner.os }}-", "{name} {}", job.id);
173+ }
174+ let saves = uses.starts_with("actions/cache@") || uses.starts_with("actions/cache/save@");
175+ assert!(!saves || whole, "{name} {}: saves npm's cache but installs only part of the lockfile", job.id);
176+ if saves {
177+ saving.push(format!("{name} {}", job.id));
178+ }
179+ }
180+ }
181+ }
182+ // main's whole cache is kept by the deploy workflow, beside its check.
183+ assert!(saving.contains(&"deploy.yml npm-cache".to_owned()), "{saving:?}");
184+}
185+
138186 #[test]
139187 fn deploy_stages_follow_one_another() {
140188 let deploy = read("deploy.yml");