@invarn/cibuild 1.5.4 → 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.
- package/dist/cli.cjs +1 -1
- package/dist/src/shared/prompts.d.ts +9 -5
- package/dist/src/shared/prompts.d.ts.map +1 -1
- package/dist/src/shared/prompts.js +15 -5
- package/dist/src/yaml/step-validator.d.ts +12 -0
- package/dist/src/yaml/step-validator.d.ts.map +1 -1
- package/dist/src/yaml/step-validator.js +19 -2
- package/dist/src/yaml/steps/cache.d.ts.map +1 -1
- package/dist/src/yaml/steps/cache.js +42 -20
- package/dist/src/yaml/steps/steps.test.js +5 -4
- package/package.json +1 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { YAMLPipeline } from "../yaml/types.js";
|
|
2
2
|
import type { CIConfig } from "../types.js";
|
|
3
3
|
/**
|
|
4
|
-
* True only when stdin AND stdout are real TTYs
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* True only when stdin AND stdout are real TTYs AND the caller has not
|
|
5
|
+
* explicitly opted out of interactivity.
|
|
6
|
+
*
|
|
7
|
+
* `process.stdin.isTTY` is unreliable as a sole signal: Tart guest
|
|
8
|
+
* sessions opened via `tart exec` attach a pseudo-TTY, so the check
|
|
9
|
+
* passes and the prompt runs — only for nobody to ever answer it. The
|
|
10
|
+
* runner sets `CIBUILD_NON_INTERACTIVE=1` (and the generic `CI=1` env
|
|
11
|
+
* is also honored) to make the override unambiguous regardless of
|
|
12
|
+
* whether the shell looks interactive.
|
|
9
13
|
*/
|
|
10
14
|
export declare function isInteractiveTTY(): boolean;
|
|
11
15
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../../src/shared/prompts.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C
|
|
1
|
+
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../../src/shared/prompts.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAU1C;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0BxF;AAED;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,OAAO,CAAC,CAwFlB"}
|
|
@@ -3,13 +3,23 @@ import { StepValidator, formatValidationResult } from "../yaml/step-validator.js
|
|
|
3
3
|
import { MissingEnvHandler } from "../yaml/missing-env-handler.js";
|
|
4
4
|
import { MissingEnvironmentVariableError } from "../yaml/env-resolver.js";
|
|
5
5
|
/**
|
|
6
|
-
* True only when stdin AND stdout are real TTYs
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* True only when stdin AND stdout are real TTYs AND the caller has not
|
|
7
|
+
* explicitly opted out of interactivity.
|
|
8
|
+
*
|
|
9
|
+
* `process.stdin.isTTY` is unreliable as a sole signal: Tart guest
|
|
10
|
+
* sessions opened via `tart exec` attach a pseudo-TTY, so the check
|
|
11
|
+
* passes and the prompt runs — only for nobody to ever answer it. The
|
|
12
|
+
* runner sets `CIBUILD_NON_INTERACTIVE=1` (and the generic `CI=1` env
|
|
13
|
+
* is also honored) to make the override unambiguous regardless of
|
|
14
|
+
* whether the shell looks interactive.
|
|
11
15
|
*/
|
|
12
16
|
export function isInteractiveTTY() {
|
|
17
|
+
if (process.env.CIBUILD_NON_INTERACTIVE === '1' ||
|
|
18
|
+
process.env.CIBUILD_NON_INTERACTIVE === 'true' ||
|
|
19
|
+
process.env.CI === '1' ||
|
|
20
|
+
process.env.CI === 'true') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
13
23
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
14
24
|
}
|
|
15
25
|
/**
|
|
@@ -34,12 +34,24 @@ export declare class StepValidator {
|
|
|
34
34
|
private validateRequirement;
|
|
35
35
|
/**
|
|
36
36
|
* Extracts all $VAR and ${VAR} variable names referenced in an object recursively.
|
|
37
|
+
*
|
|
38
|
+
* `${{ secrets.X }}` / `${{ vars.X }}` references are deliberately ignored:
|
|
39
|
+
* those are resolved by the runner-side interpolator before cibuild sees
|
|
40
|
+
* the YAML (or by the dashboard's secrets-resolver on dispatch). Without
|
|
41
|
+
* this filter the inner regex would capture `{ secrets.X ` as if it were
|
|
42
|
+
* a `${VAR}` reference, producing garbled variable names in the
|
|
43
|
+
* missing-env error output.
|
|
37
44
|
*/
|
|
38
45
|
private extractVariableReferences;
|
|
39
46
|
/**
|
|
40
47
|
* Replaces all unresolved $VAR and ${VAR} references with empty string.
|
|
41
48
|
* Used as a lenient fallback when full interpolation fails, so that
|
|
42
49
|
* getValidationRequirements can detect truly-missing variables correctly.
|
|
50
|
+
*
|
|
51
|
+
* `${{ ... }}` refs are left intact — they belong to the runner-side
|
|
52
|
+
* interpolator / dashboard secrets-resolver layer, not to shell-level
|
|
53
|
+
* variable substitution. Stripping them here would destroy the YAML for
|
|
54
|
+
* any downstream consumer that still expects to see them.
|
|
43
55
|
*/
|
|
44
56
|
private stripUnresolvedVars;
|
|
45
57
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"step-validator.d.ts","sourceRoot":"","sources":["../../../src/yaml/step-validator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAGV,wBAAwB,EAGzB,MAAM,uBAAuB,CAAC;AAwB/B;;;;GAIG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,YAAY,CAAC,CAAS;IAG9B,OAAO,CAAC,gBAAgB,CAAoE;IAG5F,OAAO,CAAC,mBAAmB,CAA6B;gBAGtD,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,YAAY,CAAC,EAAE,MAAM;IAwBvB;;;OAGG;IACG,gBAAgB,IAAI,OAAO,CAAC,wBAAwB,CAAC;IA8M3D;;OAEG;YACW,mBAAmB;IAsDjC
|
|
1
|
+
{"version":3,"file":"step-validator.d.ts","sourceRoot":"","sources":["../../../src/yaml/step-validator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAGV,wBAAwB,EAGzB,MAAM,uBAAuB,CAAC;AAwB/B;;;;GAIG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,YAAY,CAAC,CAAS;IAG9B,OAAO,CAAC,gBAAgB,CAAoE;IAG5F,OAAO,CAAC,mBAAmB,CAA6B;gBAGtD,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,YAAY,CAAC,EAAE,MAAM;IAwBvB;;;OAGG;IACG,gBAAgB,IAAI,OAAO,CAAC,wBAAwB,CAAC;IA8M3D;;OAEG;YACW,mBAAmB;IAsDjC;;;;;;;;;OASG;IACH,OAAO,CAAC,yBAAyB;IAsBjC;;;;;;;;;OASG;IACH,OAAO,CAAC,mBAAmB;IAoB3B;;OAEG;IACH,OAAO,CAAC,SAAS;CA8BlB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CA4D/E"}
|
|
@@ -285,13 +285,22 @@ export class StepValidator {
|
|
|
285
285
|
}
|
|
286
286
|
/**
|
|
287
287
|
* Extracts all $VAR and ${VAR} variable names referenced in an object recursively.
|
|
288
|
+
*
|
|
289
|
+
* `${{ secrets.X }}` / `${{ vars.X }}` references are deliberately ignored:
|
|
290
|
+
* those are resolved by the runner-side interpolator before cibuild sees
|
|
291
|
+
* the YAML (or by the dashboard's secrets-resolver on dispatch). Without
|
|
292
|
+
* this filter the inner regex would capture `{ secrets.X ` as if it were
|
|
293
|
+
* a `${VAR}` reference, producing garbled variable names in the
|
|
294
|
+
* missing-env error output.
|
|
288
295
|
*/
|
|
289
296
|
extractVariableReferences(obj) {
|
|
290
297
|
const vars = new Set();
|
|
291
298
|
if (typeof obj === 'string') {
|
|
299
|
+
// Strip `${{ ... }}` refs first so they can't be mis-matched as `${VAR}`.
|
|
300
|
+
const sanitized = obj.replace(/\$\{\{[^}]*\}\}/g, '');
|
|
292
301
|
const pattern = /\$\{([^}]+)\}|\$([A-Z_][A-Z_0-9]*)/g;
|
|
293
302
|
let match;
|
|
294
|
-
while ((match = pattern.exec(
|
|
303
|
+
while ((match = pattern.exec(sanitized)) !== null) {
|
|
295
304
|
vars.add(match[1] || match[2]);
|
|
296
305
|
}
|
|
297
306
|
}
|
|
@@ -313,10 +322,18 @@ export class StepValidator {
|
|
|
313
322
|
* Replaces all unresolved $VAR and ${VAR} references with empty string.
|
|
314
323
|
* Used as a lenient fallback when full interpolation fails, so that
|
|
315
324
|
* getValidationRequirements can detect truly-missing variables correctly.
|
|
325
|
+
*
|
|
326
|
+
* `${{ ... }}` refs are left intact — they belong to the runner-side
|
|
327
|
+
* interpolator / dashboard secrets-resolver layer, not to shell-level
|
|
328
|
+
* variable substitution. Stripping them here would destroy the YAML for
|
|
329
|
+
* any downstream consumer that still expects to see them.
|
|
316
330
|
*/
|
|
317
331
|
stripUnresolvedVars(obj) {
|
|
318
332
|
if (typeof obj === 'string') {
|
|
319
|
-
|
|
333
|
+
// `(?!\{)` prevents `\$\{[^}]+\}` from matching a leading `${` of
|
|
334
|
+
// `${{ ... }}` and stripping the inner content. Plain `$VAR` and
|
|
335
|
+
// `${VAR}` are still stripped as before.
|
|
336
|
+
return obj.replace(/\$\{(?!\{)[^}]+\}|\$[A-Z_][A-Z_0-9]*/g, '');
|
|
320
337
|
}
|
|
321
338
|
if (Array.isArray(obj)) {
|
|
322
339
|
return obj.map((item) => this.stripUnresolvedVars(item));
|
|
@@ -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;
|
|
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
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
|
559
|
-
//
|
|
560
|
-
//
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
commands.push('
|
|
565
|
-
commands.push('
|
|
566
|
-
commands.push('
|
|
567
|
-
commands.push('
|
|
568
|
-
commands.push('
|
|
569
|
-
commands.push('
|
|
570
|
-
commands.push('
|
|
571
|
-
commands.push('
|
|
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 (
|
|
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('
|
|
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('
|
|
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();
|