@invarn/cibuild 1.9.3 → 1.9.5

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;YA4JzF,iBAAiB;CA2IhC"}
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"}
@@ -484,9 +484,13 @@ export class CachePushStepExecutor extends BaseStepExecutor {
484
484
  commands.push(' echo "Paths to cache:"');
485
485
  commands.push(' printf " %s\\n" "${PATHS_TO_CACHE[@]}"');
486
486
  }
487
- // Atomic write: .tmp + rename. No lock needed runs are serialized per runner,
488
- // and cross-writer races resolve to last-writer-wins without corruption.
489
- commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" || true');
487
+ // Atomic write into a per-process-unique temp (CBR-12). The "runs serialized
488
+ // per runner" assumption behind a fixed "$CACHE_FILE.tmp" is false with two VM
489
+ // slots and collides with the host cache daemon mutating this VirtioFS-shared
490
+ // dir; a unique name (PID + $RANDOM) removes the clash. The atomic mv is kept,
491
+ // and a failed compress/mv removes its own temp (no orphan) and stays non-fatal.
492
+ commands.push(' CACHE_TMP="$CACHE_FILE.$$.$RANDOM.tmp"');
493
+ commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_TMP" && mv "$CACHE_TMP" "$CACHE_FILE" || rm -f "$CACHE_TMP"');
490
494
  commands.push(' if [ -f "$CACHE_FILE" ]; then');
491
495
  commands.push(' echo "Cache created successfully"');
492
496
  if (isDebugMode) {
@@ -587,40 +591,25 @@ export class CachePushStepExecutor extends BaseStepExecutor {
587
591
  // build. Atomic .tmp + mv overwrites any prior tarball under this key.
588
592
  commands.push('if [ ${#PATHS_TO_CACHE[@]} -gt 0 ]; then');
589
593
  commands.push(' echo "Caching ${#PATHS_TO_CACHE[@]} path(s)..."');
590
- commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" || true');
594
+ // Per-process-unique temp (CBR-12). The old fixed "$CACHE_FILE.tmp" assumed
595
+ // a single serialized writer — false with two VM slots, and it collides with
596
+ // the host cache daemon mutating this VirtioFS-shared dir, which (a) made the
597
+ // guest's own .tmp transiently vanish at `mv` (silent clobber, masked by
598
+ // `|| true`) and (b) dropped VirtioFS off its async fast path. A unique name
599
+ // removes the collision; the atomic mv into place is preserved, and a failed
600
+ // compress/mv removes its own temp (no orphan) while staying non-fatal.
601
+ commands.push(' CACHE_TMP="$CACHE_FILE.$$.$RANDOM.tmp"');
602
+ commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_TMP" && mv "$CACHE_TMP" "$CACHE_FILE" || rm -f "$CACHE_TMP"');
591
603
  commands.push(' if [ -f "$CACHE_FILE" ]; then');
592
604
  commands.push(' echo "Cache created successfully"');
593
605
  if (isDebugMode) {
594
606
  commands.push(' echo "Cache size: $(du -h "$CACHE_FILE" | cut -f1)"');
595
607
  }
596
- // Retention per-scope: (1) keep N newest by mtime touched on hit in
597
- // cache-pull so this is LRU, not strictly creation-order; (2) delete
598
- // anything past the age cap regardless of position; (3) enforce a size
599
- // budget, oldest-out. Two passes are required: the size pass needs the
600
- // post-cleanup set, otherwise its running sum counts tarballs that the
601
- // count/age pass is about to delete and over-evicts.
602
- commands.push(' __ci_ret_scope="${CACHE_KEY%-*}"');
603
- commands.push(' __ci_ret_count_cap=5');
604
- commands.push(' __ci_ret_age_cap=$((30 * 86400))');
605
- commands.push(' __ci_ret_size_cap_kb="${CIBUILD_CACHE_SCOPE_BUDGET_KB:-10485760}"');
606
- commands.push(' __ci_ret_now=$(date +%s)');
607
- commands.push(' __ci_ret_idx=0');
608
- commands.push(' while IFS= read -r __ci_ret_f; do');
609
- commands.push(' __ci_ret_idx=$((__ci_ret_idx + 1))');
610
- commands.push(' __ci_ret_age=$(( __ci_ret_now - $(stat -f %m "$__ci_ret_f" 2>/dev/null || echo "$__ci_ret_now") ))');
611
- commands.push(' if [ "$__ci_ret_idx" -gt "$__ci_ret_count_cap" ] || [ "$__ci_ret_age" -gt "$__ci_ret_age_cap" ]; then');
612
- commands.push(' rm -f "$__ci_ret_f"');
613
- commands.push(' fi');
614
- commands.push(' done < <(ls -t "$CACHE_DIR"/"$__ci_ret_scope"-*.tar.zst 2>/dev/null)');
615
- commands.push(' __ci_ret_running_kb=0');
616
- commands.push(' while IFS= read -r __ci_ret_f; do');
617
- commands.push(' __ci_ret_size=$(du -k "$__ci_ret_f" 2>/dev/null | cut -f1)');
618
- commands.push(' __ci_ret_size=${__ci_ret_size:-0}');
619
- commands.push(' __ci_ret_running_kb=$((__ci_ret_running_kb + __ci_ret_size))');
620
- commands.push(' if [ "$__ci_ret_running_kb" -gt "$__ci_ret_size_cap_kb" ]; then');
621
- commands.push(' rm -f "$__ci_ret_f"');
622
- commands.push(' fi');
623
- commands.push(' done < <(ls -t "$CACHE_DIR"/"$__ci_ret_scope"-*.tar.zst 2>/dev/null)');
608
+ // Retention is no longer applied here (ADR 0002 / CBR-6). It moved into the
609
+ // long-lived peer cache daemon, which owns ~/cache and — unlike the guest —
610
+ // knows (via the manager) which scopes this runner OWNS and must never evict.
611
+ // A guest-side prune would have to run blind to ownership and could reap the
612
+ // fleet's single durable copy, so the daemon owns the whole policy now.
624
613
  commands.push(' else');
625
614
  commands.push(' echo "Warning: Failed to create cache file"');
626
615
  commands.push(' fi');
@@ -336,6 +336,20 @@ describe('Step Implementations', () => {
336
336
  const result = await executor.execute(inputs, {}, testConfig);
337
337
  expect(result.script).toContain('skipped');
338
338
  });
339
+ test('uses a per-process-unique temp for the atomic write (CBR-12)', async () => {
340
+ const executor = new CachePushStepExecutor();
341
+ const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
342
+ // The old fixed "$CACHE_FILE.tmp" assumed a single writer ("runs
343
+ // serialized per runner") — false with two VM slots, and it collides with
344
+ // the host cache daemon mutating the same VirtioFS-shared dir. A unique
345
+ // temp (PID + $RANDOM) removes the collision; the atomic mv is preserved.
346
+ expect(result.script).not.toContain('> "$CACHE_FILE.tmp"');
347
+ expect(result.script).toContain('CACHE_TMP="$CACHE_FILE.$$.$RANDOM.tmp"');
348
+ expect(result.script).toContain('mv "$CACHE_TMP" "$CACHE_FILE"');
349
+ // A failed compress/mv cleans its own temp instead of leaking orphans,
350
+ // and stays non-fatal (never fails the build).
351
+ expect(result.script).toContain('rm -f "$CACHE_TMP"');
352
+ });
339
353
  test('should require cache_paths', async () => {
340
354
  const executor = new CachePushStepExecutor();
341
355
  const inputs = {
@@ -421,20 +435,24 @@ describe('Step Implementations', () => {
421
435
  test('should use atomic write (tmp + mv) for preset push', async () => {
422
436
  const executor = new CachePushStepExecutor();
423
437
  const result = await executor.execute({ technology: 'gradle' }, {}, testConfig);
424
- expect(result.script).toContain('> "$CACHE_FILE.tmp"');
425
- expect(result.script).toContain('mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
426
- });
427
- test('should sweep retention (count + age + size budget) after successful preset push', async () => {
438
+ expect(result.script).toContain('> "$CACHE_TMP"');
439
+ expect(result.script).toContain('mv "$CACHE_TMP" "$CACHE_FILE"');
440
+ });
441
+ test('should NOT emit guest-side retention (the peer cache daemon owns it, CBR-6)', async () => {
442
+ // ADR 0002 / CBR-6: retention moved into the long-lived peer cache daemon,
443
+ // which knows (via the manager) which scopes this runner owns and must never
444
+ // evict. The guest cache-push no longer prunes — it could only reap an owner's
445
+ // durable copy, having no ownership knowledge of its own.
428
446
  const executor = new CachePushStepExecutor();
429
447
  const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
430
- expect(result.script).toContain('__ci_ret_scope="${CACHE_KEY%-*}"');
431
- expect(result.script).toContain('ls -t "$CACHE_DIR"/"$__ci_ret_scope"-*.tar.zst');
432
- expect(result.script).toContain('__ci_ret_count_cap=5');
433
- expect(result.script).toContain('__ci_ret_age_cap=$((30 * 86400))');
434
- expect(result.script).toContain('CIBUILD_CACHE_SCOPE_BUDGET_KB');
435
- expect(result.script).toContain('rm -f "$__ci_ret_f"');
436
- });
437
- test('should not run retention for manual cache_key push (no scope)', async () => {
448
+ expect(result.script).not.toContain('__ci_ret_scope');
449
+ expect(result.script).not.toContain('__ci_ret_count_cap');
450
+ expect(result.script).not.toContain('__ci_ret_age_cap');
451
+ expect(result.script).not.toContain('CIBUILD_CACHE_SCOPE_BUDGET_KB');
452
+ // The successful-push path itself is unchanged.
453
+ expect(result.script).toContain('Cache created successfully');
454
+ });
455
+ test('should not emit retention for a manual cache_key push either', async () => {
438
456
  const executor = new CachePushStepExecutor();
439
457
  const result = await executor.execute({ cache_key: 'my-key', cache_paths: ['build'] }, {}, testConfig);
440
458
  expect(result.script).not.toContain('__ci_ret_scope');
@@ -443,8 +461,8 @@ describe('Step Implementations', () => {
443
461
  test('should use atomic write (tmp + mv) for manual cache_key push', async () => {
444
462
  const executor = new CachePushStepExecutor();
445
463
  const result = await executor.execute({ cache_key: 'my-key', cache_paths: ['build'] }, {}, testConfig);
446
- expect(result.script).toContain('> "$CACHE_FILE.tmp"');
447
- expect(result.script).toContain('mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
464
+ expect(result.script).toContain('> "$CACHE_TMP"');
465
+ expect(result.script).toContain('mv "$CACHE_TMP" "$CACHE_FILE"');
448
466
  });
449
467
  });
450
468
  describe('XcodeBuildStepExecutor', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "1.9.3",
3
+ "version": "1.9.5",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",