@invarn/cibuild 1.5.5 → 1.5.6

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;AA8DF;;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;CA8KhC;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;CA6HhC"}
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;AA8DF;;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;CAsLhC;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"}
@@ -252,6 +252,10 @@ export class CachePullStepExecutor extends BaseStepExecutor {
252
252
  commands.push(' echo "Cache file size: $(du -h "$CACHE_FILE" | cut -f1)"');
253
253
  }
254
254
  commands.push(' zstd -dc "$CACHE_FILE" | tar -xf - -C /');
255
+ // LRU bump: lift this tarball to the top of `ls -t` so retention's age
256
+ // cap counts last-used time, not last-written. Without this, a tarball
257
+ // hit daily would still be reaped on its 31st day from creation.
258
+ commands.push(' touch "$CACHE_FILE"');
255
259
  commands.push(' echo "CACHE_SOURCE=local CACHE_KEY=$CACHE_KEY"');
256
260
  // 2. Peer hit — tee to warm local cache while extracting in one pass
257
261
  if (peerCacheDir) {
@@ -301,6 +305,10 @@ export class CachePullStepExecutor extends BaseStepExecutor {
301
305
  commands.push(' echo "Fallback size: $(du -h "$__ci_fb" | cut -f1)"');
302
306
  }
303
307
  commands.push(' zstd -dc "$__ci_fb" | tar -xf - -C /');
308
+ // LRU bump for the fallback tarball — we restored from it, so it earns
309
+ // its place at the top of the retention sort even if the exact-key
310
+ // tarball is the one that gets created in this run's cache-push.
311
+ commands.push(' touch "$__ci_fb"');
304
312
  commands.push(' echo "CACHE_SOURCE=fallback_$__ci_fb_src CACHE_KEY=$CACHE_KEY FALLBACK_KEY=$__ci_fb_key"');
305
313
  commands.push(' else');
306
314
  commands.push(' echo "Cache fallback candidate $__ci_fb_key is ${__ci_fb_age_days}d old, exceeds 30d cap — going cold"');
@@ -541,34 +549,48 @@ export class CachePushStepExecutor extends BaseStepExecutor {
541
549
  commands.push('fi');
542
550
  }
543
551
  commands.push('');
544
- // Skip if cache already exists for this key
545
- commands.push('if [ -f "$CACHE_FILE" ]; then');
546
- commands.push(' echo "Cache already up to date for key: $CACHE_KEY"');
547
- commands.push('elif [ ${#PATHS_TO_CACHE[@]} -gt 0 ]; then');
552
+ // Always re-archive on success: ~/.gradle/caches and friends are
553
+ // write-through accumulators same fingerprint key does NOT imply
554
+ // unchanged contents (Gradle's build-cache-1 keeps appending task
555
+ // outputs as sources change). Skipping here would freeze the tarball
556
+ // at first-mint and let it drift further from current state on every
557
+ // build. Atomic .tmp + mv overwrites any prior tarball under this key.
558
+ commands.push('if [ ${#PATHS_TO_CACHE[@]} -gt 0 ]; then');
548
559
  commands.push(' echo "Caching ${#PATHS_TO_CACHE[@]} path(s)..."');
549
- // Atomic write: compress to .tmp, then mv. No lock needed — runs are
550
- // serialized per runner and cross-writer races resolve to last-writer-wins
551
- // without corruption. shlock was flaky on the virtiofs cache mount.
552
560
  commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" || true');
553
561
  commands.push(' if [ -f "$CACHE_FILE" ]; then');
554
562
  commands.push(' echo "Cache created successfully"');
555
563
  if (isDebugMode) {
556
564
  commands.push(' echo "Cache size: $(du -h "$CACHE_FILE" | cut -f1)"');
557
565
  }
558
- // Retention: keep 5 newest tarballs per <keyPrefix>-<projectId> scope,
559
- // delete older ones to bound disk usage. Runs inline after every push;
560
- // no cron required.
566
+ // Retention per-scope: (1) keep N newest by mtime touched on hit in
567
+ // cache-pull so this is LRU, not strictly creation-order; (2) delete
568
+ // anything past the age cap regardless of position; (3) enforce a size
569
+ // budget, oldest-out. Two passes are required: the size pass needs the
570
+ // post-cleanup set, otherwise its running sum counts tarballs that the
571
+ // count/age pass is about to delete and over-evicts.
561
572
  commands.push(' __ci_ret_scope="${CACHE_KEY%-*}"');
562
- // `|| true` guards against pipefail when ls matches nothing (first push
563
- // for a new scope) — otherwise the step would silently exit non-zero.
564
- commands.push(' __ci_ret_old=$(ls -t "$CACHE_DIR"/"$__ci_ret_scope"-*.tar.zst 2>/dev/null | tail -n +6 || true)');
565
- commands.push(' if [ -n "$__ci_ret_old" ]; then');
566
- commands.push(' __ci_ret_count=$(printf "%s\\n" "$__ci_ret_old" | wc -l | tr -d " ")');
567
- commands.push(' echo "Retention: removing $__ci_ret_count older tarball(s) from scope $__ci_ret_scope"');
568
- commands.push(' printf "%s\\n" "$__ci_ret_old" | while IFS= read -r __ci_ret_f; do');
569
- commands.push(' [ -f "$__ci_ret_f" ] && rm -f "$__ci_ret_f"');
570
- commands.push(' done');
571
- commands.push(' fi');
573
+ commands.push(' __ci_ret_count_cap=5');
574
+ commands.push(' __ci_ret_age_cap=$((30 * 86400))');
575
+ commands.push(' __ci_ret_size_cap_kb="${CIBUILD_CACHE_SCOPE_BUDGET_KB:-10485760}"');
576
+ commands.push(' __ci_ret_now=$(date +%s)');
577
+ commands.push(' __ci_ret_idx=0');
578
+ commands.push(' while IFS= read -r __ci_ret_f; do');
579
+ commands.push(' __ci_ret_idx=$((__ci_ret_idx + 1))');
580
+ commands.push(' __ci_ret_age=$(( __ci_ret_now - $(stat -f %m "$__ci_ret_f" 2>/dev/null || echo "$__ci_ret_now") ))');
581
+ commands.push(' if [ "$__ci_ret_idx" -gt "$__ci_ret_count_cap" ] || [ "$__ci_ret_age" -gt "$__ci_ret_age_cap" ]; then');
582
+ commands.push(' rm -f "$__ci_ret_f"');
583
+ commands.push(' fi');
584
+ commands.push(' done < <(ls -t "$CACHE_DIR"/"$__ci_ret_scope"-*.tar.zst 2>/dev/null)');
585
+ commands.push(' __ci_ret_running_kb=0');
586
+ commands.push(' while IFS= read -r __ci_ret_f; do');
587
+ commands.push(' __ci_ret_size=$(du -k "$__ci_ret_f" 2>/dev/null | cut -f1)');
588
+ commands.push(' __ci_ret_size=${__ci_ret_size:-0}');
589
+ commands.push(' __ci_ret_running_kb=$((__ci_ret_running_kb + __ci_ret_size))');
590
+ commands.push(' if [ "$__ci_ret_running_kb" -gt "$__ci_ret_size_cap_kb" ]; then');
591
+ commands.push(' rm -f "$__ci_ret_f"');
592
+ commands.push(' fi');
593
+ commands.push(' done < <(ls -t "$CACHE_DIR"/"$__ci_ret_scope"-*.tar.zst 2>/dev/null)');
572
594
  commands.push(' else');
573
595
  commands.push(' echo "Warning: Failed to create cache file"');
574
596
  commands.push(' fi');
@@ -379,20 +379,21 @@ describe('Step Implementations', () => {
379
379
  expect(result.script).toContain('> "$CACHE_FILE.tmp"');
380
380
  expect(result.script).toContain('mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
381
381
  });
382
- test('should sweep retention (keep 5 newest per scope) after successful push', async () => {
382
+ test('should sweep retention (count + age + size budget) after successful preset push', async () => {
383
383
  const executor = new CachePushStepExecutor();
384
384
  const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
385
- // Scope = prefix + projectId; retention globs that scope and keeps 5 newest
386
385
  expect(result.script).toContain('__ci_ret_scope="${CACHE_KEY%-*}"');
387
386
  expect(result.script).toContain('ls -t "$CACHE_DIR"/"$__ci_ret_scope"-*.tar.zst');
388
- expect(result.script).toContain('tail -n +6');
387
+ expect(result.script).toContain('__ci_ret_count_cap=5');
388
+ expect(result.script).toContain('__ci_ret_age_cap=$((30 * 86400))');
389
+ expect(result.script).toContain('CIBUILD_CACHE_SCOPE_BUDGET_KB');
389
390
  expect(result.script).toContain('rm -f "$__ci_ret_f"');
390
391
  });
391
392
  test('should not run retention for manual cache_key push (no scope)', async () => {
392
393
  const executor = new CachePushStepExecutor();
393
394
  const result = await executor.execute({ cache_key: 'my-key', cache_paths: ['build'] }, {}, testConfig);
394
395
  expect(result.script).not.toContain('__ci_ret_scope');
395
- expect(result.script).not.toContain('tail -n +6');
396
+ expect(result.script).not.toContain('__ci_ret_count_cap');
396
397
  });
397
398
  test('should use atomic write (tmp + mv) for manual cache_key push', async () => {
398
399
  const executor = new CachePushStepExecutor();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "1.5.5",
3
+ "version": "1.5.6",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",