@invarn/cibuild 1.5.1 → 1.5.3

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;CAkKhC;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;CA+GhC"}
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"}
@@ -274,10 +274,13 @@ export class CachePullStepExecutor extends BaseStepExecutor {
274
274
  // and creates a fresh tarball under the new key for future exact hits.
275
275
  commands.push('else');
276
276
  commands.push(' __ci_fb_scope="${CACHE_KEY%-*}"');
277
- commands.push(' __ci_fb=$(ls -t "$CACHE_DIR"/"$__ci_fb_scope"-*.tar.zst 2>/dev/null | head -1)');
277
+ // `|| true` defuses pipefail+errexit when ls finds no matches: bare
278
+ // assignment would abort the whole cache-pull step silently on an empty
279
+ // cache dir, flipping any cold miss into a non-zero exit.
280
+ commands.push(' __ci_fb=$(ls -t "$CACHE_DIR"/"$__ci_fb_scope"-*.tar.zst 2>/dev/null | head -1 || true)');
278
281
  if (peerCacheDir) {
279
282
  commands.push(' if [ -z "$__ci_fb" ] || [ ! -f "$__ci_fb" ]; then');
280
- commands.push(' __ci_fb=$(ls -t "$PEER_CACHE_DIR"/"$__ci_fb_scope"-*.tar.zst 2>/dev/null | head -1)');
283
+ commands.push(' __ci_fb=$(ls -t "$PEER_CACHE_DIR"/"$__ci_fb_scope"-*.tar.zst 2>/dev/null | head -1 || true)');
281
284
  commands.push(' __ci_fb_src=peer');
282
285
  commands.push(' else');
283
286
  commands.push(' __ci_fb_src=local');
@@ -288,14 +291,23 @@ export class CachePullStepExecutor extends BaseStepExecutor {
288
291
  }
289
292
  commands.push(' if [ -n "$__ci_fb" ] && [ -f "$__ci_fb" ]; then');
290
293
  commands.push(' __ci_fb_key=$(basename "$__ci_fb" .tar.zst)');
291
- commands.push(' echo "Cache fallback ($__ci_fb_src): using prior tarball $__ci_fb_key"');
294
+ // Age cap: a fallback older than 30d likely carries obsolete .konan
295
+ // installs or Gradle entries from a pre-wrapper-bump world. Go cold
296
+ // rather than seed from ancient state. Exact-key hits are unaffected.
297
+ commands.push(' __ci_fb_age_days=$(( ($(date +%s) - $(stat -f %m "$__ci_fb")) / 86400 ))');
298
+ commands.push(' if [ "$__ci_fb_age_days" -le 30 ]; then');
299
+ commands.push(' echo "Cache fallback ($__ci_fb_src): using prior tarball $__ci_fb_key (${__ci_fb_age_days}d old)"');
292
300
  if (isDebugMode) {
293
- commands.push(' echo "Fallback size: $(du -h "$__ci_fb" | cut -f1)"');
294
- }
295
- commands.push(' zstd -dc "$__ci_fb" | tar -xf - -C /');
296
- commands.push(' echo "CACHE_SOURCE=fallback_$__ci_fb_src CACHE_KEY=$CACHE_KEY FALLBACK_KEY=$__ci_fb_key"');
301
+ commands.push(' echo "Fallback size: $(du -h "$__ci_fb" | cut -f1)"');
302
+ }
303
+ commands.push(' zstd -dc "$__ci_fb" | tar -xf - -C /');
304
+ commands.push(' echo "CACHE_SOURCE=fallback_$__ci_fb_src CACHE_KEY=$CACHE_KEY FALLBACK_KEY=$__ci_fb_key"');
305
+ commands.push(' else');
306
+ commands.push(' echo "Cache fallback candidate $__ci_fb_key is ${__ci_fb_age_days}d old, exceeds 30d cap — going cold"');
307
+ commands.push(' echo "CACHE_SOURCE=cold CACHE_KEY=$CACHE_KEY FALLBACK_REJECTED=$__ci_fb_key"');
308
+ commands.push(' fi');
297
309
  commands.push(' else');
298
- // 4. Cold miss — no exact key, no fallback
310
+ // 4. Cold miss — no exact key, no fallback candidate
299
311
  commands.push(' echo "No cache found for key: $CACHE_KEY"');
300
312
  commands.push(' echo "CACHE_SOURCE=cold CACHE_KEY=$CACHE_KEY"');
301
313
  commands.push(' fi');
@@ -543,6 +555,20 @@ export class CachePushStepExecutor extends BaseStepExecutor {
543
555
  if (isDebugMode) {
544
556
  commands.push(' echo "Cache size: $(du -h "$CACHE_FILE" | cut -f1)"');
545
557
  }
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.
561
+ 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');
546
572
  commands.push(' else');
547
573
  commands.push(' echo "Warning: Failed to create cache file"');
548
574
  commands.push(' fi');
@@ -259,6 +259,15 @@ describe('Step Implementations', () => {
259
259
  expect(result.script).toContain('__ci_proj=$');
260
260
  expect(result.script).toContain('CACHE_KEY="pods-${__ci_proj}-${CHECKSUM}"');
261
261
  });
262
+ test('should enforce 30-day age cap on fallback, going cold when older', async () => {
263
+ const executor = new CachePullStepExecutor();
264
+ const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
265
+ // Computes age in days from stat mtime, rejects if over 30
266
+ expect(result.script).toContain('__ci_fb_age_days=$(( ($(date +%s) - $(stat -f %m "$__ci_fb")) / 86400 ))');
267
+ expect(result.script).toContain('if [ "$__ci_fb_age_days" -le 30 ]');
268
+ expect(result.script).toContain('exceeds 30d cap — going cold');
269
+ expect(result.script).toContain('FALLBACK_REJECTED=');
270
+ });
262
271
  });
263
272
  describe('CachePushStepExecutor', () => {
264
273
  test('should generate cache push script', async () => {
@@ -370,6 +379,21 @@ describe('Step Implementations', () => {
370
379
  expect(result.script).toContain('> "$CACHE_FILE.tmp"');
371
380
  expect(result.script).toContain('mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
372
381
  });
382
+ test('should sweep retention (keep 5 newest per scope) after successful push', async () => {
383
+ const executor = new CachePushStepExecutor();
384
+ const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
385
+ // Scope = prefix + projectId; retention globs that scope and keeps 5 newest
386
+ expect(result.script).toContain('__ci_ret_scope="${CACHE_KEY%-*}"');
387
+ expect(result.script).toContain('ls -t "$CACHE_DIR"/"$__ci_ret_scope"-*.tar.zst');
388
+ expect(result.script).toContain('tail -n +6');
389
+ expect(result.script).toContain('rm -f "$__ci_ret_f"');
390
+ });
391
+ test('should not run retention for manual cache_key push (no scope)', async () => {
392
+ const executor = new CachePushStepExecutor();
393
+ const result = await executor.execute({ cache_key: 'my-key', cache_paths: ['build'] }, {}, testConfig);
394
+ expect(result.script).not.toContain('__ci_ret_scope');
395
+ expect(result.script).not.toContain('tail -n +6');
396
+ });
373
397
  test('should use atomic write (tmp + mv) for manual cache_key push', async () => {
374
398
  const executor = new CachePushStepExecutor();
375
399
  const result = await executor.execute({ cache_key: 'my-key', cache_paths: ['build'] }, {}, testConfig);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",