@invarn/cibuild 1.4.7 → 1.4.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.
@@ -5,11 +5,16 @@ import { BaseStepExecutor } from './base.js';
5
5
  import type { StepDef, CIConfig } from '../../types.js';
6
6
  /**
7
7
  * Built-in cache presets per technology.
8
- * When `technology` is set on a cache step, the lockfile is checksummed
9
- * at runtime to produce the cache key, and the listed paths are cached/restored.
8
+ * When `technology` is set on a cache step, either the single `lockfile` is
9
+ * checksummed, or the list of `fingerprint` filenames is discovered and
10
+ * content-hashed, to produce the cache key. The listed `paths` are then
11
+ * cached/restored under that key.
10
12
  */
11
13
  export interface CachePreset {
12
- lockfile: string;
14
+ /** Single lockfile path to checksum. Use for toolchains with authoritative lockfiles (Podfile.lock, package-lock.json…). */
15
+ lockfile?: string;
16
+ /** Filenames to discover+hash across the repo. Use when no single file captures the full build graph (Gradle/KMM). */
17
+ fingerprint?: string[];
13
18
  keyPrefix: string;
14
19
  paths: string[];
15
20
  /** Optional fallback preset name: if primary lockfile not found, use this preset's lockfile/key/paths */
@@ -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;AAExD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,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;AAoBF;;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;YAoGzF,iBAAiB;CAoGhC;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;YA0JzF,iBAAiB;CAsGhC"}
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;AAgDF;;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;YAsGzF,iBAAiB;CA+HhC;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;YAuKzF,iBAAiB;CAqHhC"}
@@ -2,17 +2,59 @@
2
2
  * Cache step implementations (cache-pull and cache-push)
3
3
  */
4
4
  import { BaseStepExecutor } from './base.js';
5
+ /**
6
+ * Files that together define a Gradle/KMM build graph. Hashing these catches
7
+ * module additions/removals, plugin changes, and version-catalog bumps — things
8
+ * that invalidate reusability of the `~/.gradle/caches` contents across branches.
9
+ * Source file edits do NOT affect this hash; Gradle's task cache inside the
10
+ * tarball handles source-level reuse.
11
+ */
12
+ const GRADLE_FINGERPRINT = [
13
+ 'settings.gradle',
14
+ 'settings.gradle.kts',
15
+ 'build.gradle',
16
+ 'build.gradle.kts',
17
+ 'libs.versions.toml',
18
+ 'gradle-wrapper.properties',
19
+ ];
5
20
  export const CACHE_PRESETS = {
6
21
  ios: { lockfile: 'Podfile.lock', keyPrefix: 'pods', paths: ['Pods'], fallback: 'spm' },
7
22
  cocoapods: { lockfile: 'Podfile.lock', keyPrefix: 'pods', paths: ['Pods'] },
8
23
  carthage: { lockfile: 'Cartfile.resolved', keyPrefix: 'carthage', paths: ['Carthage'] },
9
24
  spm: { lockfile: 'Package.resolved', keyPrefix: 'spm', paths: ['~/Library/Developer/Xcode/DerivedData/*/SourcePackages'] },
10
- gradle: { lockfile: 'gradle/wrapper/gradle-wrapper.properties', keyPrefix: 'gradle', paths: ['~/.gradle/caches', '~/.gradle/wrapper/dists'] },
11
- kmm: { lockfile: 'gradle/wrapper/gradle-wrapper.properties', keyPrefix: 'kmm', paths: ['~/.gradle/caches', '~/.gradle/wrapper/dists', '~/.konan'] },
25
+ gradle: { fingerprint: GRADLE_FINGERPRINT, keyPrefix: 'gradle', paths: ['~/.gradle/caches', '~/.gradle/wrapper/dists'] },
26
+ kmm: { fingerprint: GRADLE_FINGERPRINT, keyPrefix: 'kmm', paths: ['~/.gradle/caches', '~/.gradle/wrapper/dists', '~/.konan'] },
12
27
  npm: { lockfile: 'package-lock.json', keyPrefix: 'npm', paths: ['node_modules'] },
13
28
  yarn: { lockfile: 'yarn.lock', keyPrefix: 'yarn', paths: ['node_modules'] },
14
29
  dart: { lockfile: 'pubspec.lock', keyPrefix: 'dart', paths: ['.dart_tool', '.pub-cache'] },
15
30
  };
31
+ /**
32
+ * Emits bash that discovers fingerprint filenames across the repo, sorts them,
33
+ * and hashes their concatenated names+contents into CACHE_KEY.
34
+ * Used by presets (Gradle, KMM) where no single lockfile captures the build graph.
35
+ */
36
+ function emitFingerprintKeyCommands(preset) {
37
+ const fp = preset.fingerprint ?? [];
38
+ const escape = (s) => s.replace(/'/g, "'\\''");
39
+ const nameArgs = fp
40
+ .map((n, i) => `${i === 0 ? '' : '-o '}-name '${escape(n)}'`)
41
+ .join(' ');
42
+ return [
43
+ `__ci_fp_files=$(find . -type f \\( ${nameArgs} \\) \\`,
44
+ ` -not -path '*/build/*' \\`,
45
+ ` -not -path '*/.gradle/*' \\`,
46
+ ` -not -path '*/node_modules/*' \\`,
47
+ ` -not -path '*/DerivedData/*' \\`,
48
+ ` 2>/dev/null | LC_ALL=C sort)`,
49
+ `if [ -n "$__ci_fp_files" ]; then`,
50
+ ` CHECKSUM=$(echo "$__ci_fp_files" | xargs shasum -a 256 2>/dev/null | shasum -a 256 | cut -c1-16)`,
51
+ ` CACHE_KEY="${escape(preset.keyPrefix)}-\${CHECKSUM}"`,
52
+ `else`,
53
+ ` echo "Warning: no fingerprint files found, using fallback cache key"`,
54
+ ` CACHE_KEY="${escape(preset.keyPrefix)}-no-fingerprint"`,
55
+ `fi`,
56
+ ];
57
+ }
16
58
  /**
17
59
  * Resolves a preset with its fallback chain into a flat list of
18
60
  * {lockfile, keyPrefix, paths} entries to try in order at runtime.
@@ -83,33 +125,34 @@ export class CachePullStepExecutor extends BaseStepExecutor {
83
125
  commands.push('echo "Debug mode enabled"');
84
126
  commands.push('echo "Cache file: $CACHE_FILE"');
85
127
  }
128
+ // Peer cache directory
129
+ const peerCacheDir = config.paths.peerCacheDir || '';
130
+ if (peerCacheDir) {
131
+ commands.push('');
132
+ commands.push(`PEER_CACHE_DIR="${this.escapeBash(peerCacheDir)}"`);
133
+ commands.push(`PEER_CACHE_FILE="$PEER_CACHE_DIR/${this.escapeBash(cacheKey)}.tar.zst"`);
134
+ }
86
135
  // Check if cache file exists
87
136
  commands.push('');
88
137
  commands.push('# Check if cache exists');
89
138
  commands.push('if [ -f "$CACHE_FILE" ]; then');
90
- commands.push(' echo "Cache found, extracting..."');
139
+ commands.push(' echo "Cache found (local), extracting..."');
91
140
  if (isDebugMode) {
92
141
  commands.push(' echo "Cache file size: $(du -h "$CACHE_FILE" | cut -f1)"');
93
142
  }
94
- // Extract cache
95
143
  commands.push(' zstd -dc "$CACHE_FILE" | tar -xf - -C /');
96
- commands.push(' echo "Cache restored successfully"');
97
- // List restored paths if debug mode
98
- if (isDebugMode && Array.isArray(cachePaths) && cachePaths.length > 0) {
99
- commands.push(' echo "Restored paths:"');
100
- for (const path of cachePaths) {
101
- commands.push(` EXPANDED_PATH="${this.escapeBash(path)}"`);
102
- commands.push(' EXPANDED_PATH="${EXPANDED_PATH/#~/${CIBUILD_USER_HOME:-$HOME}}"');
103
- commands.push(' if [ -e "$EXPANDED_PATH" ]; then');
104
- commands.push(' echo " ✓ $EXPANDED_PATH"');
105
- commands.push(' else');
106
- commands.push(' echo " ✗ $EXPANDED_PATH (not found)"');
107
- commands.push(' fi');
108
- }
144
+ commands.push(' echo "CACHE_SOURCE=local"');
145
+ // Peer fallback
146
+ if (peerCacheDir) {
147
+ commands.push('elif [ -f "$PEER_CACHE_FILE" ] 2>/dev/null; then');
148
+ commands.push(' echo "Cache found (peer), extracting..."');
149
+ commands.push(' tee "$CACHE_FILE.tmp" < "$PEER_CACHE_FILE" | zstd -dc | tar -xf - -C /');
150
+ commands.push(' mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
151
+ commands.push(' echo "CACHE_SOURCE=peer"');
109
152
  }
110
153
  commands.push('else');
111
154
  commands.push(` echo "No cache found for key: ${this.escapeBash(cacheKey)}"`);
112
- commands.push(' echo "This is normal for the first build or after cache expiration"');
155
+ commands.push(' echo "CACHE_SOURCE=cold"');
113
156
  commands.push('fi');
114
157
  const script = this.createBashScriptFromCommands(commands, stepName);
115
158
  return this.createScriptStep(script, stepName);
@@ -138,7 +181,10 @@ export class CachePullStepExecutor extends BaseStepExecutor {
138
181
  commands.push(' find . -name "$(basename "$1")" -not -path "*/DerivedData/*" -not -path "*/.build/*" 2>/dev/null | head -1');
139
182
  commands.push('}');
140
183
  commands.push('');
141
- if (chain.length === 1) {
184
+ if (chain.length === 1 && chain[0].fingerprint && chain[0].fingerprint.length > 0) {
185
+ commands.push(...emitFingerprintKeyCommands(chain[0]));
186
+ }
187
+ else if (chain.length === 1) {
142
188
  const preset = chain[0];
143
189
  commands.push(`LOCKFILE=$(__ci_find_lockfile "${this.escapeBash(preset.lockfile)}")`);
144
190
  commands.push('if [ -n "$LOCKFILE" ] && [ -f "$LOCKFILE" ]; then');
@@ -170,35 +216,55 @@ export class CachePullStepExecutor extends BaseStepExecutor {
170
216
  }
171
217
  commands.push('');
172
218
  if (isDebugMode) {
173
- commands.push('echo "Lockfile: $LOCKFILE"');
174
219
  commands.push('echo "Cache key: $CACHE_KEY"');
175
220
  }
176
221
  commands.push('CACHE_FILE="$CACHE_DIR/$CACHE_KEY.tar.zst"');
177
222
  commands.push('');
223
+ // Peer cache directory — set from config, empty string disables peer lookup
224
+ const peerCacheDir = config.paths.peerCacheDir || '';
225
+ if (peerCacheDir) {
226
+ commands.push(`PEER_CACHE_DIR="${this.escapeBash(peerCacheDir)}"`);
227
+ commands.push('PEER_CACHE_FILE="$PEER_CACHE_DIR/$CACHE_KEY.tar.zst"');
228
+ commands.push('');
229
+ }
230
+ // 1. Local hit
178
231
  commands.push('if [ -f "$CACHE_FILE" ]; then');
179
- commands.push(' echo "Cache found, extracting..."');
232
+ commands.push(' echo "Cache found (local), extracting..."');
180
233
  if (isDebugMode) {
181
234
  commands.push(' echo "Cache file size: $(du -h "$CACHE_FILE" | cut -f1)"');
182
235
  }
183
236
  commands.push(' zstd -dc "$CACHE_FILE" | tar -xf - -C /');
184
- commands.push(' echo "Cache restored successfully"');
185
- if (isDebugMode) {
186
- commands.push(' echo "Restored paths:"');
187
- const allPaths = [...new Set(chain.flatMap(p => p.paths))];
188
- for (const p of allPaths) {
189
- commands.push(` EXPANDED="${this.escapeBash(p)}"`);
190
- commands.push(' EXPANDED="${EXPANDED/#~/${CIBUILD_USER_HOME:-$HOME}}"');
191
- commands.push(' if [ -e "$EXPANDED" ]; then');
192
- commands.push(' echo " ✓ $EXPANDED"');
193
- commands.push(' else');
194
- commands.push(' echo " ✗ $EXPANDED (not found)"');
195
- commands.push(' fi');
237
+ commands.push(' echo "CACHE_SOURCE=local CACHE_KEY=$CACHE_KEY"');
238
+ // 2. Peer hit — tee to warm local cache while extracting in one pass
239
+ if (peerCacheDir) {
240
+ commands.push('elif [ -f "$PEER_CACHE_FILE" ] 2>/dev/null; then');
241
+ commands.push(' echo "Cache found (peer), extracting..."');
242
+ if (isDebugMode) {
243
+ commands.push(' echo "Peer cache file size: $(du -h "$PEER_CACHE_FILE" | cut -f1)"');
196
244
  }
245
+ // Single NFS read: tee forks the stream to local cache file AND decompression
246
+ commands.push(' tee "$CACHE_FILE.tmp" < "$PEER_CACHE_FILE" | zstd -dc | tar -xf - -C /');
247
+ commands.push(' mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
248
+ commands.push(' echo "CACHE_SOURCE=peer CACHE_KEY=$CACHE_KEY"');
197
249
  }
250
+ // 3. Cold miss
198
251
  commands.push('else');
199
252
  commands.push(' echo "No cache found for key: $CACHE_KEY"');
200
- commands.push(' echo "This is normal for the first build or after cache expiration"');
253
+ commands.push(' echo "CACHE_SOURCE=cold CACHE_KEY=$CACHE_KEY"');
201
254
  commands.push('fi');
255
+ if (isDebugMode) {
256
+ commands.push('echo "Restored paths:"');
257
+ const allPaths = [...new Set(chain.flatMap(p => p.paths))];
258
+ for (const p of allPaths) {
259
+ commands.push(`EXPANDED="${this.escapeBash(p)}"`);
260
+ commands.push('EXPANDED="${EXPANDED/#~/${CIBUILD_USER_HOME:-$HOME}}"');
261
+ commands.push('if [ -e "$EXPANDED" ]; then');
262
+ commands.push(' echo " ✓ $EXPANDED"');
263
+ commands.push('else');
264
+ commands.push(' echo " ✗ $EXPANDED (not found)"');
265
+ commands.push('fi');
266
+ }
267
+ }
202
268
  const script = this.createBashScriptFromCommands(commands, stepName);
203
269
  return this.createScriptStep(script, stepName);
204
270
  }
@@ -320,7 +386,20 @@ export class CachePushStepExecutor extends BaseStepExecutor {
320
386
  commands.push(' echo "Paths to cache:"');
321
387
  commands.push(' printf " %s\\n" "${PATHS_TO_CACHE[@]}"');
322
388
  }
389
+ // Acquire lock with shlock
390
+ commands.push(' LOCK_FILE="$CACHE_FILE.lock"');
391
+ commands.push(' __ci_lock_tries=0');
392
+ commands.push(' while ! shlock -f "$LOCK_FILE" -p $$; do');
393
+ commands.push(' __ci_lock_tries=$((__ci_lock_tries + 1))');
394
+ commands.push(' if [ "$__ci_lock_tries" -ge 30 ]; then');
395
+ commands.push(' echo "Warning: cache push lock timeout, skipping"');
396
+ commands.push(' rm -f "$LOCK_FILE"');
397
+ commands.push(' exit 0');
398
+ commands.push(' fi');
399
+ commands.push(' sleep 1');
400
+ commands.push(' done');
323
401
  commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" || true');
402
+ commands.push(' rm -f "$LOCK_FILE"');
324
403
  commands.push(' if [ -f "$CACHE_FILE" ]; then');
325
404
  commands.push(' echo "Cache created successfully"');
326
405
  if (isDebugMode) {
@@ -356,7 +435,10 @@ export class CachePushStepExecutor extends BaseStepExecutor {
356
435
  commands.push(' find . -name "$(basename "$1")" -not -path "*/DerivedData/*" -not -path "*/.build/*" 2>/dev/null | head -1');
357
436
  commands.push('}');
358
437
  commands.push('');
359
- if (chain.length === 1) {
438
+ if (chain.length === 1 && chain[0].fingerprint && chain[0].fingerprint.length > 0) {
439
+ commands.push(...emitFingerprintKeyCommands(chain[0]));
440
+ }
441
+ else if (chain.length === 1) {
360
442
  const preset = chain[0];
361
443
  commands.push(`LOCKFILE=$(__ci_find_lockfile "${this.escapeBash(preset.lockfile)}")`);
362
444
  commands.push('if [ -n "$LOCKFILE" ] && [ -f "$LOCKFILE" ]; then');
@@ -384,7 +466,6 @@ export class CachePushStepExecutor extends BaseStepExecutor {
384
466
  }
385
467
  commands.push('');
386
468
  if (isDebugMode) {
387
- commands.push('echo "Lockfile: $LOCKFILE"');
388
469
  commands.push('echo "Cache key: $CACHE_KEY"');
389
470
  }
390
471
  commands.push('CACHE_FILE="$CACHE_DIR/$CACHE_KEY.tar.zst"');
@@ -412,7 +493,21 @@ export class CachePushStepExecutor extends BaseStepExecutor {
412
493
  commands.push(' echo "Cache already up to date for key: $CACHE_KEY"');
413
494
  commands.push('elif [ ${#PATHS_TO_CACHE[@]} -gt 0 ]; then');
414
495
  commands.push(' echo "Caching ${#PATHS_TO_CACHE[@]} path(s)..."');
496
+ // Acquire lock with shlock (macOS-native, stale-lock safe via PID check)
497
+ commands.push(' LOCK_FILE="$CACHE_FILE.lock"');
498
+ commands.push(' __ci_lock_tries=0');
499
+ commands.push(' while ! shlock -f "$LOCK_FILE" -p $$; do');
500
+ commands.push(' __ci_lock_tries=$((__ci_lock_tries + 1))');
501
+ commands.push(' if [ "$__ci_lock_tries" -ge 30 ]; then');
502
+ commands.push(' echo "Warning: cache push lock timeout, skipping"');
503
+ commands.push(' rm -f "$LOCK_FILE"');
504
+ commands.push(' exit 0');
505
+ commands.push(' fi');
506
+ commands.push(' sleep 1');
507
+ commands.push(' done');
508
+ // Atomic write: compress to .tmp, then mv (peers never see half-written files)
415
509
  commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" || true');
510
+ commands.push(' rm -f "$LOCK_FILE"');
416
511
  commands.push(' if [ -f "$CACHE_FILE" ]; then');
417
512
  commands.push(' echo "Cache created successfully"');
418
513
  if (isDebugMode) {
@@ -6,7 +6,7 @@ import { describe, test, expect } from '@jest/globals';
6
6
  import { GitCloneStepExecutor } from './git-clone.js';
7
7
  import { ScriptStepExecutor } from './script.js';
8
8
  import { CachePullStepExecutor, CachePushStepExecutor } from './cache.js';
9
- import { testConfig } from './test-config.js';
9
+ import { testConfig, testConfigNoPeer } from './test-config.js';
10
10
  import { XcodeBuildStepExecutor, XcodeTestStepExecutor } from './xcode.js';
11
11
  import { SetJavaVersionStepExecutor, GradleBuildStepExecutor, AndroidLintStepExecutor, AndroidUnitTestStepExecutor, } from './android.js';
12
12
  describe('Step Implementations', () => {
@@ -138,7 +138,8 @@ describe('Step Implementations', () => {
138
138
  test('should auto-configure for gradle technology', async () => {
139
139
  const executor = new CachePullStepExecutor();
140
140
  const result = await executor.execute({ technology: 'gradle' }, {}, testConfig);
141
- expect(result.script).toContain('gradle/wrapper/gradle-wrapper.properties');
141
+ expect(result.script).toContain("-name 'gradle-wrapper.properties'");
142
+ expect(result.script).toContain("-name 'build.gradle.kts'");
142
143
  expect(result.script).toContain('gradle-');
143
144
  });
144
145
  test('should auto-configure for npm technology', async () => {
@@ -174,7 +175,10 @@ describe('Step Implementations', () => {
174
175
  test('should auto-configure for kmm technology', async () => {
175
176
  const executor = new CachePullStepExecutor();
176
177
  const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
177
- expect(result.script).toContain('gradle/wrapper/gradle-wrapper.properties');
178
+ expect(result.script).toContain("-name 'gradle-wrapper.properties'");
179
+ expect(result.script).toContain("-name 'settings.gradle'");
180
+ expect(result.script).toContain("-name 'build.gradle.kts'");
181
+ expect(result.script).toContain("-name 'libs.versions.toml'");
178
182
  expect(result.script).toContain('kmm-');
179
183
  });
180
184
  test('should use zstd compression format', async () => {
@@ -189,6 +193,31 @@ describe('Step Implementations', () => {
189
193
  const executor = new CachePullStepExecutor();
190
194
  await expect(executor.execute({ technology: 'unknown' }, {}, testConfig)).rejects.toThrow("Unknown cache technology 'unknown'");
191
195
  });
196
+ test('should include peer cache fallback when peerCacheDir is set', async () => {
197
+ const executor = new CachePullStepExecutor();
198
+ const result = await executor.execute({ technology: 'cocoapods' }, {}, testConfig);
199
+ expect(result.script).toContain('PEER_CACHE_DIR=');
200
+ expect(result.script).toContain('PEER_CACHE_FILE=');
201
+ expect(result.script).toContain('CACHE_SOURCE=local');
202
+ expect(result.script).toContain('CACHE_SOURCE=peer');
203
+ expect(result.script).toContain('CACHE_SOURCE=cold');
204
+ // tee pattern: single read from peer, simultaneous extract + local warm
205
+ expect(result.script).toContain('tee "$CACHE_FILE.tmp" < "$PEER_CACHE_FILE"');
206
+ });
207
+ test('should skip peer fallback when peerCacheDir is not set', async () => {
208
+ const executor = new CachePullStepExecutor();
209
+ const result = await executor.execute({ technology: 'cocoapods' }, {}, testConfigNoPeer);
210
+ expect(result.script).not.toContain('PEER_CACHE_DIR=');
211
+ expect(result.script).not.toContain('PEER_CACHE_FILE=');
212
+ expect(result.script).not.toContain('CACHE_SOURCE=peer');
213
+ });
214
+ test('should include peer fallback for manual cache_key pull', async () => {
215
+ const executor = new CachePullStepExecutor();
216
+ const result = await executor.execute({ cache_key: 'my-key' }, {}, testConfig);
217
+ expect(result.script).toContain('PEER_CACHE_DIR=');
218
+ expect(result.script).toContain('CACHE_SOURCE=peer');
219
+ expect(result.script).toContain('tee "$CACHE_FILE.tmp" < "$PEER_CACHE_FILE"');
220
+ });
192
221
  });
193
222
  describe('CachePushStepExecutor', () => {
194
223
  test('should generate cache push script', async () => {
@@ -241,7 +270,8 @@ describe('Step Implementations', () => {
241
270
  test('should auto-configure for gradle technology', async () => {
242
271
  const executor = new CachePushStepExecutor();
243
272
  const result = await executor.execute({ technology: 'gradle' }, {}, testConfig);
244
- expect(result.script).toContain('gradle/wrapper/gradle-wrapper.properties');
273
+ expect(result.script).toContain("-name 'gradle-wrapper.properties'");
274
+ expect(result.script).toContain("-name 'build.gradle.kts'");
245
275
  expect(result.script).toContain('~/.gradle/caches');
246
276
  });
247
277
  test('should auto-configure for npm technology', async () => {
@@ -260,7 +290,9 @@ describe('Step Implementations', () => {
260
290
  test('should auto-configure for kmm technology', async () => {
261
291
  const executor = new CachePushStepExecutor();
262
292
  const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
263
- expect(result.script).toContain('gradle/wrapper/gradle-wrapper.properties');
293
+ expect(result.script).toContain("-name 'gradle-wrapper.properties'");
294
+ expect(result.script).toContain("-name 'settings.gradle.kts'");
295
+ expect(result.script).toContain("-name 'libs.versions.toml'");
264
296
  expect(result.script).toContain('kmm-');
265
297
  expect(result.script).toContain('~/.gradle/caches');
266
298
  expect(result.script).toContain('~/.gradle/wrapper/dists');
@@ -278,6 +310,28 @@ describe('Step Implementations', () => {
278
310
  const executor = new CachePushStepExecutor();
279
311
  await expect(executor.execute({ technology: 'unknown' }, {}, testConfig)).rejects.toThrow("Unknown cache technology 'unknown'");
280
312
  });
313
+ test('should use shlock locking for preset push', async () => {
314
+ const executor = new CachePushStepExecutor();
315
+ const result = await executor.execute({ technology: 'cocoapods' }, {}, testConfig);
316
+ expect(result.script).toContain('shlock -f "$LOCK_FILE" -p $$');
317
+ expect(result.script).toContain('LOCK_FILE="$CACHE_FILE.lock"');
318
+ expect(result.script).toContain('rm -f "$LOCK_FILE"');
319
+ // Lock timeout after 30 retries
320
+ expect(result.script).toContain('__ci_lock_tries');
321
+ expect(result.script).toContain('-ge 30');
322
+ });
323
+ test('should use shlock locking for manual cache_key push', async () => {
324
+ const executor = new CachePushStepExecutor();
325
+ const result = await executor.execute({ cache_key: 'my-key', cache_paths: ['build'] }, {}, testConfig);
326
+ expect(result.script).toContain('shlock -f "$LOCK_FILE" -p $$');
327
+ expect(result.script).toContain('rm -f "$LOCK_FILE"');
328
+ });
329
+ test('should use atomic write (tmp + mv) for preset push', async () => {
330
+ const executor = new CachePushStepExecutor();
331
+ const result = await executor.execute({ technology: 'gradle' }, {}, testConfig);
332
+ expect(result.script).toContain('> "$CACHE_FILE.tmp"');
333
+ expect(result.script).toContain('mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
334
+ });
281
335
  });
282
336
  describe('XcodeBuildStepExecutor', () => {
283
337
  test('should generate xcodebuild script', async () => {
@@ -1,3 +1,5 @@
1
1
  import type { CIConfig } from '../../types.js';
2
2
  export declare const testConfig: CIConfig;
3
+ /** Config without peer cache (no NFS peer runner) */
4
+ export declare const testConfigNoPeer: CIConfig;
3
5
  //# sourceMappingURL=test-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"test-config.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/test-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,eAAO,MAAM,UAAU,EAAE,QAexB,CAAC"}
1
+ {"version":3,"file":"test-config.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/test-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,eAAO,MAAM,UAAU,EAAE,QAexB,CAAC;AAEF,qDAAqD;AACrD,eAAO,MAAM,gBAAgB,EAAE,QAM9B,CAAC"}
@@ -10,8 +10,16 @@ export const testConfig = {
10
10
  paths: {
11
11
  buildsDir: '.ci-builds',
12
12
  cacheDir: '.ci-cache',
13
- peerCacheDir: 'peer',
13
+ peerCacheDir: '/Volumes/My Shared Files/cibuild-peer-cache',
14
14
  derivedDataDir: '~/Library/Developer/Xcode/DerivedData',
15
15
  },
16
16
  };
17
+ /** Config without peer cache (no NFS peer runner) */
18
+ export const testConfigNoPeer = {
19
+ ...testConfig,
20
+ paths: {
21
+ ...testConfig.paths,
22
+ peerCacheDir: undefined,
23
+ },
24
+ };
17
25
  //# sourceMappingURL=test-config.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "1.4.7",
3
+ "version": "1.4.9",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",