@invarn/cibuild 1.5.0 → 1.5.2
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;
|
|
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;CA2KhC;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;CA2HhC"}
|
|
@@ -28,9 +28,22 @@ export const CACHE_PRESETS = {
|
|
|
28
28
|
yarn: { lockfile: 'yarn.lock', keyPrefix: 'yarn', paths: ['node_modules'] },
|
|
29
29
|
dart: { lockfile: 'pubspec.lock', keyPrefix: 'dart', paths: ['.dart_tool', '.pub-cache'] },
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Emits bash that derives a stable short project id from the git remote URL
|
|
33
|
+
* (with embedded credentials stripped, so token rotations don't churn the key).
|
|
34
|
+
* Sets $__ci_proj to an 8-char hex. Falls back to "no-remote" if no git remote.
|
|
35
|
+
*/
|
|
36
|
+
function emitProjectScopeCommands() {
|
|
37
|
+
return [
|
|
38
|
+
'__ci_git_remote=$(git config --get remote.origin.url 2>/dev/null | sed -E "s#(://)[^@/]+@#\\1#" || echo "no-remote")',
|
|
39
|
+
'__ci_proj=$(printf "%s" "${__ci_git_remote:-no-remote}" | shasum -a 256 | cut -c1-8)',
|
|
40
|
+
];
|
|
41
|
+
}
|
|
31
42
|
/**
|
|
32
43
|
* Emits bash that discovers fingerprint filenames across the repo, sorts them,
|
|
33
44
|
* and hashes their concatenated names+contents into CACHE_KEY.
|
|
45
|
+
* Key format: <keyPrefix>-<projectId>-<fingerprint>, so tarballs are scoped
|
|
46
|
+
* per-repo and fallback globbing can't cross-contaminate between projects.
|
|
34
47
|
* Used by presets (Gradle, KMM) where no single lockfile captures the build graph.
|
|
35
48
|
*/
|
|
36
49
|
function emitFingerprintKeyCommands(preset) {
|
|
@@ -48,10 +61,10 @@ function emitFingerprintKeyCommands(preset) {
|
|
|
48
61
|
` 2>/dev/null | LC_ALL=C sort)`,
|
|
49
62
|
`if [ -n "$__ci_fp_files" ]; then`,
|
|
50
63
|
` 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}"`,
|
|
64
|
+
` CACHE_KEY="${escape(preset.keyPrefix)}-\${__ci_proj}-\${CHECKSUM}"`,
|
|
52
65
|
`else`,
|
|
53
66
|
` echo "Warning: no fingerprint files found, using fallback cache key"`,
|
|
54
|
-
` CACHE_KEY="${escape(preset.keyPrefix)}-no-fingerprint"`,
|
|
67
|
+
` CACHE_KEY="${escape(preset.keyPrefix)}-\${__ci_proj}-no-fingerprint"`,
|
|
55
68
|
`fi`,
|
|
56
69
|
];
|
|
57
70
|
}
|
|
@@ -181,6 +194,11 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
181
194
|
commands.push(' find . -name "$(basename "$1")" -not -path "*/DerivedData/*" -not -path "*/.build/*" 2>/dev/null | head -1');
|
|
182
195
|
commands.push('}');
|
|
183
196
|
commands.push('');
|
|
197
|
+
// Project scope: include a stable short hash of the git remote in every
|
|
198
|
+
// cache key so different repos on the same runner don't share tarballs
|
|
199
|
+
// (prevents fallback cross-contamination across projects).
|
|
200
|
+
commands.push(...emitProjectScopeCommands());
|
|
201
|
+
commands.push('');
|
|
184
202
|
if (chain.length === 1 && chain[0].fingerprint && chain[0].fingerprint.length > 0) {
|
|
185
203
|
commands.push(...emitFingerprintKeyCommands(chain[0]));
|
|
186
204
|
}
|
|
@@ -189,10 +207,10 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
189
207
|
commands.push(`LOCKFILE=$(__ci_find_lockfile "${this.escapeBash(preset.lockfile)}")`);
|
|
190
208
|
commands.push('if [ -n "$LOCKFILE" ] && [ -f "$LOCKFILE" ]; then');
|
|
191
209
|
commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
|
|
192
|
-
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
|
|
210
|
+
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${__ci_proj}-\${CHECKSUM}"`);
|
|
193
211
|
commands.push('else');
|
|
194
212
|
commands.push(` echo "Warning: ${this.escapeBash(preset.lockfile)} not found, using fallback cache key"`);
|
|
195
|
-
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-no-lockfile"`);
|
|
213
|
+
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${__ci_proj}-no-lockfile"`);
|
|
196
214
|
commands.push('fi');
|
|
197
215
|
}
|
|
198
216
|
else {
|
|
@@ -204,14 +222,14 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
204
222
|
commands.push(`${cond} [ -n "$__ci_lockfile_candidate" ] && [ -f "$__ci_lockfile_candidate" ]; then`);
|
|
205
223
|
commands.push(' LOCKFILE="$__ci_lockfile_candidate"');
|
|
206
224
|
commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
|
|
207
|
-
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
|
|
225
|
+
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${__ci_proj}-\${CHECKSUM}"`);
|
|
208
226
|
if (isDebugMode) {
|
|
209
227
|
commands.push(` echo "Detected: ${this.escapeBash(preset.keyPrefix)} (lockfile: $LOCKFILE)"`);
|
|
210
228
|
}
|
|
211
229
|
}
|
|
212
230
|
commands.push('else');
|
|
213
231
|
commands.push(` echo "Warning: no lockfile found, using fallback cache key"`);
|
|
214
|
-
commands.push(` CACHE_KEY="${this.escapeBash(chain[0].keyPrefix)}-no-lockfile"`);
|
|
232
|
+
commands.push(` CACHE_KEY="${this.escapeBash(chain[0].keyPrefix)}-\${__ci_proj}-no-lockfile"`);
|
|
215
233
|
commands.push('fi');
|
|
216
234
|
}
|
|
217
235
|
commands.push('');
|
|
@@ -247,10 +265,49 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
247
265
|
commands.push(' mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
|
|
248
266
|
commands.push(' echo "CACHE_SOURCE=peer CACHE_KEY=$CACHE_KEY"');
|
|
249
267
|
}
|
|
250
|
-
// 3.
|
|
268
|
+
// 3. Fallback — no exact-key tarball, try the newest prior tarball scoped
|
|
269
|
+
// to the same <keyPrefix>-<projectId> (so fallback stays within this repo
|
|
270
|
+
// and doesn't pick up a different KMM project's cache). Gradle will
|
|
271
|
+
// re-hash task inputs on top of the warm ~/.gradle/caches directory,
|
|
272
|
+
// hitting the unchanged tasks and recompiling only what actually changed.
|
|
273
|
+
// Deliberately does NOT warm $CACHE_FILE — that way cache-push still runs
|
|
274
|
+
// and creates a fresh tarball under the new key for future exact hits.
|
|
251
275
|
commands.push('else');
|
|
252
|
-
commands.push('
|
|
253
|
-
commands.push('
|
|
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)');
|
|
278
|
+
if (peerCacheDir) {
|
|
279
|
+
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)');
|
|
281
|
+
commands.push(' __ci_fb_src=peer');
|
|
282
|
+
commands.push(' else');
|
|
283
|
+
commands.push(' __ci_fb_src=local');
|
|
284
|
+
commands.push(' fi');
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
commands.push(' __ci_fb_src=local');
|
|
288
|
+
}
|
|
289
|
+
commands.push(' if [ -n "$__ci_fb" ] && [ -f "$__ci_fb" ]; then');
|
|
290
|
+
commands.push(' __ci_fb_key=$(basename "$__ci_fb" .tar.zst)');
|
|
291
|
+
// Age cap: a fallback older than 30d likely carries obsolete .konan
|
|
292
|
+
// installs or Gradle entries from a pre-wrapper-bump world. Go cold
|
|
293
|
+
// rather than seed from ancient state. Exact-key hits are unaffected.
|
|
294
|
+
commands.push(' __ci_fb_age_days=$(( ($(date +%s) - $(stat -f %m "$__ci_fb")) / 86400 ))');
|
|
295
|
+
commands.push(' if [ "$__ci_fb_age_days" -le 30 ]; then');
|
|
296
|
+
commands.push(' echo "Cache fallback ($__ci_fb_src): using prior tarball $__ci_fb_key (${__ci_fb_age_days}d old)"');
|
|
297
|
+
if (isDebugMode) {
|
|
298
|
+
commands.push(' echo "Fallback size: $(du -h "$__ci_fb" | cut -f1)"');
|
|
299
|
+
}
|
|
300
|
+
commands.push(' zstd -dc "$__ci_fb" | tar -xf - -C /');
|
|
301
|
+
commands.push(' echo "CACHE_SOURCE=fallback_$__ci_fb_src CACHE_KEY=$CACHE_KEY FALLBACK_KEY=$__ci_fb_key"');
|
|
302
|
+
commands.push(' else');
|
|
303
|
+
commands.push(' echo "Cache fallback candidate $__ci_fb_key is ${__ci_fb_age_days}d old, exceeds 30d cap — going cold"');
|
|
304
|
+
commands.push(' echo "CACHE_SOURCE=cold CACHE_KEY=$CACHE_KEY FALLBACK_REJECTED=$__ci_fb_key"');
|
|
305
|
+
commands.push(' fi');
|
|
306
|
+
commands.push(' else');
|
|
307
|
+
// 4. Cold miss — no exact key, no fallback candidate
|
|
308
|
+
commands.push(' echo "No cache found for key: $CACHE_KEY"');
|
|
309
|
+
commands.push(' echo "CACHE_SOURCE=cold CACHE_KEY=$CACHE_KEY"');
|
|
310
|
+
commands.push(' fi');
|
|
254
311
|
commands.push('fi');
|
|
255
312
|
if (isDebugMode) {
|
|
256
313
|
commands.push('echo "Restored paths:"');
|
|
@@ -424,6 +481,10 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
424
481
|
commands.push(' find . -name "$(basename "$1")" -not -path "*/DerivedData/*" -not -path "*/.build/*" 2>/dev/null | head -1');
|
|
425
482
|
commands.push('}');
|
|
426
483
|
commands.push('');
|
|
484
|
+
// Project scope: include a stable short hash of the git remote in every
|
|
485
|
+
// cache key so different repos on the same runner don't share tarballs.
|
|
486
|
+
commands.push(...emitProjectScopeCommands());
|
|
487
|
+
commands.push('');
|
|
427
488
|
if (chain.length === 1 && chain[0].fingerprint && chain[0].fingerprint.length > 0) {
|
|
428
489
|
commands.push(...emitFingerprintKeyCommands(chain[0]));
|
|
429
490
|
}
|
|
@@ -432,10 +493,10 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
432
493
|
commands.push(`LOCKFILE=$(__ci_find_lockfile "${this.escapeBash(preset.lockfile)}")`);
|
|
433
494
|
commands.push('if [ -n "$LOCKFILE" ] && [ -f "$LOCKFILE" ]; then');
|
|
434
495
|
commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
|
|
435
|
-
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
|
|
496
|
+
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${__ci_proj}-\${CHECKSUM}"`);
|
|
436
497
|
commands.push('else');
|
|
437
498
|
commands.push(` echo "Warning: ${this.escapeBash(preset.lockfile)} not found, using fallback cache key"`);
|
|
438
|
-
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-no-lockfile"`);
|
|
499
|
+
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${__ci_proj}-no-lockfile"`);
|
|
439
500
|
commands.push('fi');
|
|
440
501
|
}
|
|
441
502
|
else {
|
|
@@ -446,11 +507,11 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
446
507
|
commands.push(`${cond} [ -n "$__ci_lockfile_candidate" ] && [ -f "$__ci_lockfile_candidate" ]; then`);
|
|
447
508
|
commands.push(' LOCKFILE="$__ci_lockfile_candidate"');
|
|
448
509
|
commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
|
|
449
|
-
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
|
|
510
|
+
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${__ci_proj}-\${CHECKSUM}"`);
|
|
450
511
|
}
|
|
451
512
|
commands.push('else');
|
|
452
513
|
commands.push(` echo "Warning: no lockfile found, using fallback cache key"`);
|
|
453
|
-
commands.push(` CACHE_KEY="${this.escapeBash(chain[0].keyPrefix)}-no-lockfile"`);
|
|
514
|
+
commands.push(` CACHE_KEY="${this.escapeBash(chain[0].keyPrefix)}-\${__ci_proj}-no-lockfile"`);
|
|
454
515
|
commands.push('fi');
|
|
455
516
|
}
|
|
456
517
|
commands.push('');
|
|
@@ -491,6 +552,18 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
491
552
|
if (isDebugMode) {
|
|
492
553
|
commands.push(' echo "Cache size: $(du -h "$CACHE_FILE" | cut -f1)"');
|
|
493
554
|
}
|
|
555
|
+
// Retention: keep 5 newest tarballs per <keyPrefix>-<projectId> scope,
|
|
556
|
+
// delete older ones to bound disk usage. Runs inline after every push;
|
|
557
|
+
// no cron required.
|
|
558
|
+
commands.push(' __ci_ret_scope="${CACHE_KEY%-*}"');
|
|
559
|
+
commands.push(' __ci_ret_old=$(ls -t "$CACHE_DIR"/"$__ci_ret_scope"-*.tar.zst 2>/dev/null | tail -n +6)');
|
|
560
|
+
commands.push(' if [ -n "$__ci_ret_old" ]; then');
|
|
561
|
+
commands.push(' __ci_ret_count=$(printf "%s\\n" "$__ci_ret_old" | wc -l | tr -d " ")');
|
|
562
|
+
commands.push(' echo "Retention: removing $__ci_ret_count older tarball(s) from scope $__ci_ret_scope"');
|
|
563
|
+
commands.push(' printf "%s\\n" "$__ci_ret_old" | while IFS= read -r __ci_ret_f; do');
|
|
564
|
+
commands.push(' [ -f "$__ci_ret_f" ] && rm -f "$__ci_ret_f"');
|
|
565
|
+
commands.push(' done');
|
|
566
|
+
commands.push(' fi');
|
|
494
567
|
commands.push(' else');
|
|
495
568
|
commands.push(' echo "Warning: Failed to create cache file"');
|
|
496
569
|
commands.push(' fi');
|
|
@@ -218,6 +218,56 @@ describe('Step Implementations', () => {
|
|
|
218
218
|
expect(result.script).toContain('CACHE_SOURCE=peer');
|
|
219
219
|
expect(result.script).toContain('tee "$CACHE_FILE.tmp" < "$PEER_CACHE_FILE"');
|
|
220
220
|
});
|
|
221
|
+
test('should fall back to newest prior tarball scoped to <keyPrefix>-<projectId> on exact-key miss', async () => {
|
|
222
|
+
const executor = new CachePullStepExecutor();
|
|
223
|
+
const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
|
|
224
|
+
// Scope = everything before the last dash (prefix+projectId, not just prefix),
|
|
225
|
+
// so fallback cannot cross-contaminate between different projects
|
|
226
|
+
expect(result.script).toContain('__ci_fb_scope="${CACHE_KEY%-*}"');
|
|
227
|
+
expect(result.script).toContain('ls -t "$CACHE_DIR"/"$__ci_fb_scope"-*.tar.zst');
|
|
228
|
+
expect(result.script).toContain('CACHE_SOURCE=fallback_');
|
|
229
|
+
expect(result.script).toContain('FALLBACK_KEY=');
|
|
230
|
+
// Fallback must NOT warm $CACHE_FILE (otherwise cache-push would skip)
|
|
231
|
+
const fallbackSection = result.script.split('__ci_fb_scope=')[1] ?? '';
|
|
232
|
+
expect(fallbackSection).not.toContain('mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
|
|
233
|
+
});
|
|
234
|
+
test('should try peer dir for fallback when local has no prior tarball', async () => {
|
|
235
|
+
const executor = new CachePullStepExecutor();
|
|
236
|
+
const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
|
|
237
|
+
expect(result.script).toContain('ls -t "$PEER_CACHE_DIR"/"$__ci_fb_scope"-*.tar.zst');
|
|
238
|
+
expect(result.script).toContain('__ci_fb_src=peer');
|
|
239
|
+
});
|
|
240
|
+
test('should skip peer fallback branch when peerCacheDir is not set', async () => {
|
|
241
|
+
const executor = new CachePullStepExecutor();
|
|
242
|
+
const result = await executor.execute({ technology: 'kmm' }, {}, testConfigNoPeer);
|
|
243
|
+
expect(result.script).not.toContain('ls -t "$PEER_CACHE_DIR"');
|
|
244
|
+
// Local fallback still available
|
|
245
|
+
expect(result.script).toContain('ls -t "$CACHE_DIR"/"$__ci_fb_scope"-*.tar.zst');
|
|
246
|
+
});
|
|
247
|
+
test('should include project-scope id ($__ci_proj) in cache key', async () => {
|
|
248
|
+
const executor = new CachePullStepExecutor();
|
|
249
|
+
const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
|
|
250
|
+
// Project id derived from stripped git remote URL, 8-char hash
|
|
251
|
+
expect(result.script).toContain('git config --get remote.origin.url');
|
|
252
|
+
expect(result.script).toContain('__ci_proj=$');
|
|
253
|
+
// Key format: <prefix>-<projid>-<fp>
|
|
254
|
+
expect(result.script).toContain('CACHE_KEY="kmm-${__ci_proj}-${CHECKSUM}"');
|
|
255
|
+
});
|
|
256
|
+
test('should include project-scope id in lockfile-based preset keys', async () => {
|
|
257
|
+
const executor = new CachePullStepExecutor();
|
|
258
|
+
const result = await executor.execute({ technology: 'cocoapods' }, {}, testConfig);
|
|
259
|
+
expect(result.script).toContain('__ci_proj=$');
|
|
260
|
+
expect(result.script).toContain('CACHE_KEY="pods-${__ci_proj}-${CHECKSUM}"');
|
|
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
|
+
});
|
|
221
271
|
});
|
|
222
272
|
describe('CachePushStepExecutor', () => {
|
|
223
273
|
test('should generate cache push script', async () => {
|
|
@@ -329,6 +379,21 @@ describe('Step Implementations', () => {
|
|
|
329
379
|
expect(result.script).toContain('> "$CACHE_FILE.tmp"');
|
|
330
380
|
expect(result.script).toContain('mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
|
|
331
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
|
+
});
|
|
332
397
|
test('should use atomic write (tmp + mv) for manual cache_key push', async () => {
|
|
333
398
|
const executor = new CachePushStepExecutor();
|
|
334
399
|
const result = await executor.execute({ cache_key: 'my-key', cache_paths: ['build'] }, {}, testConfig);
|