@invarn/cibuild 1.9.7 → 1.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAkBxD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,4HAA4H;IAC5H,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sHAAsH;IACtH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAUrD,CAAC;AAmFF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YA0GzF,iBAAiB;CA8LhC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAgKzF,iBAAiB;CA4HhC"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAkBxD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,4HAA4H;IAC5H,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sHAAsH;IACtH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAUrD,CAAC;AAwFF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YA0GzF,iBAAiB;CAqMhC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAgKzF,iBAAiB;CAmIhC"}
@@ -97,7 +97,12 @@ function resolvePresetChain(technology) {
97
97
  */
98
98
  function emitDaemonPullBranch() {
99
99
  return [
100
- 'elif [ -n "$CIBUILD_CACHE_DAEMON" ] && curl -fsS ${CIBUILD_CACHE_TOKEN:+-H "Authorization: Bearer $CIBUILD_CACHE_TOKEN"} "$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst" | zstd -dc | tar -xf - -C /; then',
100
+ // stderr is silenced on the probe: a cache miss is the normal case and
101
+ // makes curl emit "(22) ... 404" (and the empty body makes zstd print
102
+ // "unexpected end of file"). Those are non-actionable — the branch just
103
+ // falls through to the cold path — so they're pure log noise. Exit
104
+ // status is preserved (pipefail), so the fall-through still works.
105
+ 'elif [ -n "$CIBUILD_CACHE_DAEMON" ] && { curl -fsS ${CIBUILD_CACHE_TOKEN:+-H "Authorization: Bearer $CIBUILD_CACHE_TOKEN"} "$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst" | zstd -dc | tar -xf - -C /; } 2>/dev/null; then',
101
106
  ' echo "Cache found (daemon), extracting..."',
102
107
  // LRU bump the tarball the daemon just persisted to ~/cache (same dir as
103
108
  // the --dir mount); tolerate its absence in case the daemon served it
@@ -237,13 +242,20 @@ export class CachePullStepExecutor extends BaseStepExecutor {
237
242
  commands.push('fi');
238
243
  }
239
244
  else {
240
- // Fallback chain: try each lockfile in order, use the first one found
245
+ // Fallback chain: probe every lockfile up front, then branch on the
246
+ // results. The probes must precede the if/elif — emitting a probe
247
+ // inside a prior preset's `then` body (the old shape) meant the
248
+ // second lockfile was only checked when the FIRST one was found, so
249
+ // an SPM project with no Podfile.lock never reached the Package.resolved
250
+ // branch and fell through to `<first>-no-lockfile`.
251
+ for (let i = 0; i < chain.length; i++) {
252
+ commands.push(`__ci_lockfile_${i}=$(__ci_find_lockfile "${this.escapeBash(chain[i].lockfile)}")`);
253
+ }
241
254
  for (let i = 0; i < chain.length; i++) {
242
255
  const preset = chain[i];
243
256
  const cond = i === 0 ? 'if' : 'elif';
244
- commands.push(`__ci_lockfile_candidate=$(__ci_find_lockfile "${this.escapeBash(preset.lockfile)}")`);
245
- commands.push(`${cond} [ -n "$__ci_lockfile_candidate" ] && [ -f "$__ci_lockfile_candidate" ]; then`);
246
- commands.push(' LOCKFILE="$__ci_lockfile_candidate"');
257
+ commands.push(`${cond} [ -n "$__ci_lockfile_${i}" ] && [ -f "$__ci_lockfile_${i}" ]; then`);
258
+ commands.push(` LOCKFILE="$__ci_lockfile_${i}"`);
247
259
  commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
248
260
  commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${__ci_proj}-\${CHECKSUM}"`);
249
261
  if (isDebugMode) {
@@ -545,12 +557,19 @@ export class CachePushStepExecutor extends BaseStepExecutor {
545
557
  commands.push('fi');
546
558
  }
547
559
  else {
560
+ // Fallback chain: probe every lockfile up front, then branch — same
561
+ // fix as cache-pull. A probe nested in a prior preset's `then` body
562
+ // only ran when the first lockfile was found, so push wrote the
563
+ // `<first>-no-lockfile` key for SPM projects, mismatching what a
564
+ // correct pull would look for.
565
+ for (let i = 0; i < chain.length; i++) {
566
+ commands.push(`__ci_lockfile_${i}=$(__ci_find_lockfile "${this.escapeBash(chain[i].lockfile)}")`);
567
+ }
548
568
  for (let i = 0; i < chain.length; i++) {
549
569
  const preset = chain[i];
550
570
  const cond = i === 0 ? 'if' : 'elif';
551
- commands.push(`__ci_lockfile_candidate=$(__ci_find_lockfile "${this.escapeBash(preset.lockfile)}")`);
552
- commands.push(`${cond} [ -n "$__ci_lockfile_candidate" ] && [ -f "$__ci_lockfile_candidate" ]; then`);
553
- commands.push(' LOCKFILE="$__ci_lockfile_candidate"');
571
+ commands.push(`${cond} [ -n "$__ci_lockfile_${i}" ] && [ -f "$__ci_lockfile_${i}" ]; then`);
572
+ commands.push(` LOCKFILE="$__ci_lockfile_${i}"`);
554
573
  commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
555
574
  commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${__ci_proj}-\${CHECKSUM}"`);
556
575
  }
@@ -3,6 +3,10 @@
3
3
  * Tests each step executor with sample inputs
4
4
  */
5
5
  import { describe, test, expect } from '@jest/globals';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
6
10
  import { GitCloneStepExecutor } from './git-clone.js';
7
11
  import { ScriptStepExecutor } from './script.js';
8
12
  import { CachePullStepExecutor, CachePushStepExecutor } from './cache.js';
@@ -166,6 +170,101 @@ describe('Step Implementations', () => {
166
170
  expect(result.script).toContain('Package.resolved');
167
171
  expect(result.script).toContain('spm-');
168
172
  });
173
+ // A static `toContain` check can't catch a control-flow bug in the
174
+ // fallback chain (the script "contains" both Podfile.lock and
175
+ // Package.resolved either way). So actually execute the generated
176
+ // key-derivation in a fixture repo and read the resolved key back.
177
+ describe('ios fallback chain (executed in a fixture repo)', () => {
178
+ // Run the generated ios cache-pull in a fixture repo and capture
179
+ // BOTH streams (the daemon probe writes its noise to stderr).
180
+ async function runIosPull(build, env = {}) {
181
+ const script = (await new CachePullStepExecutor().execute({ technology: 'ios' }, {}, testConfigNoPeer)).script;
182
+ const dir = mkdtempSync(join(tmpdir(), 'cibuild-cache-'));
183
+ try {
184
+ build(dir);
185
+ const scriptPath = join(dir, '__cache_pull.sh');
186
+ writeFileSync(scriptPath, script);
187
+ let stdout = '';
188
+ let stderr = '';
189
+ try {
190
+ stdout = execFileSync('bash', [scriptPath], {
191
+ cwd: dir,
192
+ encoding: 'utf-8',
193
+ stdio: ['ignore', 'pipe', 'pipe'],
194
+ env: { ...process.env, ...env },
195
+ timeout: 20000,
196
+ });
197
+ }
198
+ catch (e) {
199
+ const err = e;
200
+ stdout = String(err.stdout ?? '');
201
+ stderr = String(err.stderr ?? '');
202
+ }
203
+ return { stdout, stderr, combined: stdout + stderr };
204
+ }
205
+ finally {
206
+ rmSync(dir, { recursive: true, force: true });
207
+ }
208
+ }
209
+ async function resolveIosCacheKey(build) {
210
+ const { combined } = await runIosPull(build);
211
+ const m = combined.match(/CACHE_KEY=(\S+)/);
212
+ if (!m)
213
+ throw new Error(`No CACHE_KEY in output:\n${combined}`);
214
+ return m[1];
215
+ }
216
+ function writeSpmFixture(dir) {
217
+ const swiftpm = join(dir, 'App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm');
218
+ mkdirSync(swiftpm, { recursive: true });
219
+ writeFileSync(join(swiftpm, 'Package.resolved'), '{"pins":[],"version":3}');
220
+ }
221
+ test('a cold daemon miss does not print curl/zstd noise', async () => {
222
+ // Reproduce the runner setup: a configured cache daemon whose
223
+ // probe fails for an uncached key. (We point at a closed port so
224
+ // curl errors instantly with no server handle to leak — the fix
225
+ // silences curl's stderr regardless of the failure mode, exactly
226
+ // as it does for the 404 seen in production.) The probe must fall
227
+ // through to cold quietly: no "curl:" or "unexpected end of file".
228
+ const { stdout, combined } = await runIosPull(writeSpmFixture, {
229
+ CIBUILD_CACHE_DAEMON: 'http://127.0.0.1:1',
230
+ });
231
+ expect(combined).not.toContain('curl:');
232
+ expect(combined).not.toContain('unexpected end of file');
233
+ // Still resolves the SPM key and reports a cold miss.
234
+ expect(stdout).toContain('CACHE_SOURCE=cold');
235
+ expect(stdout).toMatch(/CACHE_KEY=spm-/);
236
+ });
237
+ test('SPM project (Package.resolved, no Podfile.lock) resolves an spm- key', async () => {
238
+ const key = await resolveIosCacheKey((dir) => {
239
+ // Real Xcode SPM location — Package.resolved committed under
240
+ // the shared xcworkspace data, no Podfile.lock anywhere.
241
+ const swiftpm = join(dir, 'App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm');
242
+ mkdirSync(swiftpm, { recursive: true });
243
+ writeFileSync(join(swiftpm, 'Package.resolved'), '{"pins":[],"version":3}');
244
+ });
245
+ expect(key).toMatch(/^spm-/);
246
+ expect(key).not.toContain('no-lockfile');
247
+ });
248
+ test('CocoaPods project (Podfile.lock) resolves a pods- key', async () => {
249
+ const key = await resolveIosCacheKey((dir) => {
250
+ writeFileSync(join(dir, 'Podfile.lock'), 'PODS:\n - Alamofire\n');
251
+ });
252
+ expect(key).toMatch(/^pods-/);
253
+ expect(key).not.toContain('no-lockfile');
254
+ });
255
+ test('Podfile.lock wins over Package.resolved when both exist', async () => {
256
+ const key = await resolveIosCacheKey((dir) => {
257
+ writeFileSync(join(dir, 'Podfile.lock'), 'PODS:\n');
258
+ writeFileSync(join(dir, 'Package.resolved'), '{"pins":[]}');
259
+ });
260
+ expect(key).toMatch(/^pods-/);
261
+ expect(key).not.toContain('no-lockfile');
262
+ });
263
+ test('neither lockfile present falls back to a no-lockfile key', async () => {
264
+ const key = await resolveIosCacheKey(() => { });
265
+ expect(key).toContain('no-lockfile');
266
+ });
267
+ });
169
268
  test('should auto-configure for yarn technology', async () => {
170
269
  const executor = new CachePullStepExecutor();
171
270
  const result = await executor.execute({ technology: 'yarn' }, {}, testConfig);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "1.9.7",
3
+ "version": "1.9.9",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",