@invarn/cibuild 1.4.8 → 1.5.0
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.
|
@@ -5,11 +5,16 @@ import { BaseStepExecutor } from './base.js';
|
|
|
5
5
|
import type { StepDef, CIConfig } from '../../types.js';
|
|
6
6
|
/**
|
|
7
7
|
* Built-in cache presets per technology.
|
|
8
|
-
* When `technology` is set on a cache step, the lockfile is
|
|
9
|
-
*
|
|
8
|
+
* When `technology` is set on a cache step, either the single `lockfile` is
|
|
9
|
+
* checksummed, or the list of `fingerprint` filenames is discovered and
|
|
10
|
+
* content-hashed, to produce the cache key. The listed `paths` are then
|
|
11
|
+
* cached/restored under that key.
|
|
10
12
|
*/
|
|
11
13
|
export interface CachePreset {
|
|
12
|
-
lockfile
|
|
14
|
+
/** Single lockfile path to checksum. Use for toolchains with authoritative lockfiles (Podfile.lock, package-lock.json…). */
|
|
15
|
+
lockfile?: string;
|
|
16
|
+
/** Filenames to discover+hash across the repo. Use when no single file captures the full build graph (Gradle/KMM). */
|
|
17
|
+
fingerprint?: string[];
|
|
13
18
|
keyPrefix: string;
|
|
14
19
|
paths: string[];
|
|
15
20
|
/** Optional fallback preset name: if primary lockfile not found, use this preset's lockfile/key/paths */
|
|
@@ -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;
|
|
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;AAgDF;;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;CA+HhC;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;CA0GhC"}
|
|
@@ -2,17 +2,59 @@
|
|
|
2
2
|
* Cache step implementations (cache-pull and cache-push)
|
|
3
3
|
*/
|
|
4
4
|
import { BaseStepExecutor } from './base.js';
|
|
5
|
+
/**
|
|
6
|
+
* Files that together define a Gradle/KMM build graph. Hashing these catches
|
|
7
|
+
* module additions/removals, plugin changes, and version-catalog bumps — things
|
|
8
|
+
* that invalidate reusability of the `~/.gradle/caches` contents across branches.
|
|
9
|
+
* Source file edits do NOT affect this hash; Gradle's task cache inside the
|
|
10
|
+
* tarball handles source-level reuse.
|
|
11
|
+
*/
|
|
12
|
+
const GRADLE_FINGERPRINT = [
|
|
13
|
+
'settings.gradle',
|
|
14
|
+
'settings.gradle.kts',
|
|
15
|
+
'build.gradle',
|
|
16
|
+
'build.gradle.kts',
|
|
17
|
+
'libs.versions.toml',
|
|
18
|
+
'gradle-wrapper.properties',
|
|
19
|
+
];
|
|
5
20
|
export const CACHE_PRESETS = {
|
|
6
21
|
ios: { lockfile: 'Podfile.lock', keyPrefix: 'pods', paths: ['Pods'], fallback: 'spm' },
|
|
7
22
|
cocoapods: { lockfile: 'Podfile.lock', keyPrefix: 'pods', paths: ['Pods'] },
|
|
8
23
|
carthage: { lockfile: 'Cartfile.resolved', keyPrefix: 'carthage', paths: ['Carthage'] },
|
|
9
24
|
spm: { lockfile: 'Package.resolved', keyPrefix: 'spm', paths: ['~/Library/Developer/Xcode/DerivedData/*/SourcePackages'] },
|
|
10
|
-
gradle: {
|
|
11
|
-
kmm: {
|
|
25
|
+
gradle: { fingerprint: GRADLE_FINGERPRINT, keyPrefix: 'gradle', paths: ['~/.gradle/caches', '~/.gradle/wrapper/dists'] },
|
|
26
|
+
kmm: { fingerprint: GRADLE_FINGERPRINT, keyPrefix: 'kmm', paths: ['~/.gradle/caches', '~/.gradle/wrapper/dists', '~/.konan'] },
|
|
12
27
|
npm: { lockfile: 'package-lock.json', keyPrefix: 'npm', paths: ['node_modules'] },
|
|
13
28
|
yarn: { lockfile: 'yarn.lock', keyPrefix: 'yarn', paths: ['node_modules'] },
|
|
14
29
|
dart: { lockfile: 'pubspec.lock', keyPrefix: 'dart', paths: ['.dart_tool', '.pub-cache'] },
|
|
15
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Emits bash that discovers fingerprint filenames across the repo, sorts them,
|
|
33
|
+
* and hashes their concatenated names+contents into CACHE_KEY.
|
|
34
|
+
* Used by presets (Gradle, KMM) where no single lockfile captures the build graph.
|
|
35
|
+
*/
|
|
36
|
+
function emitFingerprintKeyCommands(preset) {
|
|
37
|
+
const fp = preset.fingerprint ?? [];
|
|
38
|
+
const escape = (s) => s.replace(/'/g, "'\\''");
|
|
39
|
+
const nameArgs = fp
|
|
40
|
+
.map((n, i) => `${i === 0 ? '' : '-o '}-name '${escape(n)}'`)
|
|
41
|
+
.join(' ');
|
|
42
|
+
return [
|
|
43
|
+
`__ci_fp_files=$(find . -type f \\( ${nameArgs} \\) \\`,
|
|
44
|
+
` -not -path '*/build/*' \\`,
|
|
45
|
+
` -not -path '*/.gradle/*' \\`,
|
|
46
|
+
` -not -path '*/node_modules/*' \\`,
|
|
47
|
+
` -not -path '*/DerivedData/*' \\`,
|
|
48
|
+
` 2>/dev/null | LC_ALL=C sort)`,
|
|
49
|
+
`if [ -n "$__ci_fp_files" ]; then`,
|
|
50
|
+
` 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}"`,
|
|
52
|
+
`else`,
|
|
53
|
+
` echo "Warning: no fingerprint files found, using fallback cache key"`,
|
|
54
|
+
` CACHE_KEY="${escape(preset.keyPrefix)}-no-fingerprint"`,
|
|
55
|
+
`fi`,
|
|
56
|
+
];
|
|
57
|
+
}
|
|
16
58
|
/**
|
|
17
59
|
* Resolves a preset with its fallback chain into a flat list of
|
|
18
60
|
* {lockfile, keyPrefix, paths} entries to try in order at runtime.
|
|
@@ -139,7 +181,10 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
139
181
|
commands.push(' find . -name "$(basename "$1")" -not -path "*/DerivedData/*" -not -path "*/.build/*" 2>/dev/null | head -1');
|
|
140
182
|
commands.push('}');
|
|
141
183
|
commands.push('');
|
|
142
|
-
if (chain.length === 1) {
|
|
184
|
+
if (chain.length === 1 && chain[0].fingerprint && chain[0].fingerprint.length > 0) {
|
|
185
|
+
commands.push(...emitFingerprintKeyCommands(chain[0]));
|
|
186
|
+
}
|
|
187
|
+
else if (chain.length === 1) {
|
|
143
188
|
const preset = chain[0];
|
|
144
189
|
commands.push(`LOCKFILE=$(__ci_find_lockfile "${this.escapeBash(preset.lockfile)}")`);
|
|
145
190
|
commands.push('if [ -n "$LOCKFILE" ] && [ -f "$LOCKFILE" ]; then');
|
|
@@ -171,7 +216,6 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
171
216
|
}
|
|
172
217
|
commands.push('');
|
|
173
218
|
if (isDebugMode) {
|
|
174
|
-
commands.push('echo "Lockfile: $LOCKFILE"');
|
|
175
219
|
commands.push('echo "Cache key: $CACHE_KEY"');
|
|
176
220
|
}
|
|
177
221
|
commands.push('CACHE_FILE="$CACHE_DIR/$CACHE_KEY.tar.zst"');
|
|
@@ -342,20 +386,9 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
342
386
|
commands.push(' echo "Paths to cache:"');
|
|
343
387
|
commands.push(' printf " %s\\n" "${PATHS_TO_CACHE[@]}"');
|
|
344
388
|
}
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
commands.push(' __ci_lock_tries=0');
|
|
348
|
-
commands.push(' while ! shlock -f "$LOCK_FILE" -p $$; do');
|
|
349
|
-
commands.push(' __ci_lock_tries=$((__ci_lock_tries + 1))');
|
|
350
|
-
commands.push(' if [ "$__ci_lock_tries" -ge 30 ]; then');
|
|
351
|
-
commands.push(' echo "Warning: cache push lock timeout, skipping"');
|
|
352
|
-
commands.push(' rm -f "$LOCK_FILE"');
|
|
353
|
-
commands.push(' exit 0');
|
|
354
|
-
commands.push(' fi');
|
|
355
|
-
commands.push(' sleep 1');
|
|
356
|
-
commands.push(' done');
|
|
389
|
+
// Atomic write: .tmp + rename. No lock needed — runs are serialized per runner,
|
|
390
|
+
// and cross-writer races resolve to last-writer-wins without corruption.
|
|
357
391
|
commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" || true');
|
|
358
|
-
commands.push(' rm -f "$LOCK_FILE"');
|
|
359
392
|
commands.push(' if [ -f "$CACHE_FILE" ]; then');
|
|
360
393
|
commands.push(' echo "Cache created successfully"');
|
|
361
394
|
if (isDebugMode) {
|
|
@@ -391,7 +424,10 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
391
424
|
commands.push(' find . -name "$(basename "$1")" -not -path "*/DerivedData/*" -not -path "*/.build/*" 2>/dev/null | head -1');
|
|
392
425
|
commands.push('}');
|
|
393
426
|
commands.push('');
|
|
394
|
-
if (chain.length === 1) {
|
|
427
|
+
if (chain.length === 1 && chain[0].fingerprint && chain[0].fingerprint.length > 0) {
|
|
428
|
+
commands.push(...emitFingerprintKeyCommands(chain[0]));
|
|
429
|
+
}
|
|
430
|
+
else if (chain.length === 1) {
|
|
395
431
|
const preset = chain[0];
|
|
396
432
|
commands.push(`LOCKFILE=$(__ci_find_lockfile "${this.escapeBash(preset.lockfile)}")`);
|
|
397
433
|
commands.push('if [ -n "$LOCKFILE" ] && [ -f "$LOCKFILE" ]; then');
|
|
@@ -419,7 +455,6 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
419
455
|
}
|
|
420
456
|
commands.push('');
|
|
421
457
|
if (isDebugMode) {
|
|
422
|
-
commands.push('echo "Lockfile: $LOCKFILE"');
|
|
423
458
|
commands.push('echo "Cache key: $CACHE_KEY"');
|
|
424
459
|
}
|
|
425
460
|
commands.push('CACHE_FILE="$CACHE_DIR/$CACHE_KEY.tar.zst"');
|
|
@@ -447,21 +482,10 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
447
482
|
commands.push(' echo "Cache already up to date for key: $CACHE_KEY"');
|
|
448
483
|
commands.push('elif [ ${#PATHS_TO_CACHE[@]} -gt 0 ]; then');
|
|
449
484
|
commands.push(' echo "Caching ${#PATHS_TO_CACHE[@]} path(s)..."');
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
commands.push(' while ! shlock -f "$LOCK_FILE" -p $$; do');
|
|
454
|
-
commands.push(' __ci_lock_tries=$((__ci_lock_tries + 1))');
|
|
455
|
-
commands.push(' if [ "$__ci_lock_tries" -ge 30 ]; then');
|
|
456
|
-
commands.push(' echo "Warning: cache push lock timeout, skipping"');
|
|
457
|
-
commands.push(' rm -f "$LOCK_FILE"');
|
|
458
|
-
commands.push(' exit 0');
|
|
459
|
-
commands.push(' fi');
|
|
460
|
-
commands.push(' sleep 1');
|
|
461
|
-
commands.push(' done');
|
|
462
|
-
// Atomic write: compress to .tmp, then mv (peers never see half-written files)
|
|
485
|
+
// Atomic write: compress to .tmp, then mv. No lock needed — runs are
|
|
486
|
+
// serialized per runner and cross-writer races resolve to last-writer-wins
|
|
487
|
+
// without corruption. shlock was flaky on the virtiofs cache mount.
|
|
463
488
|
commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" || true');
|
|
464
|
-
commands.push(' rm -f "$LOCK_FILE"');
|
|
465
489
|
commands.push(' if [ -f "$CACHE_FILE" ]; then');
|
|
466
490
|
commands.push(' echo "Cache created successfully"');
|
|
467
491
|
if (isDebugMode) {
|
|
@@ -138,7 +138,8 @@ describe('Step Implementations', () => {
|
|
|
138
138
|
test('should auto-configure for gradle technology', async () => {
|
|
139
139
|
const executor = new CachePullStepExecutor();
|
|
140
140
|
const result = await executor.execute({ technology: 'gradle' }, {}, testConfig);
|
|
141
|
-
expect(result.script).toContain('gradle
|
|
141
|
+
expect(result.script).toContain("-name 'gradle-wrapper.properties'");
|
|
142
|
+
expect(result.script).toContain("-name 'build.gradle.kts'");
|
|
142
143
|
expect(result.script).toContain('gradle-');
|
|
143
144
|
});
|
|
144
145
|
test('should auto-configure for npm technology', async () => {
|
|
@@ -174,7 +175,10 @@ describe('Step Implementations', () => {
|
|
|
174
175
|
test('should auto-configure for kmm technology', async () => {
|
|
175
176
|
const executor = new CachePullStepExecutor();
|
|
176
177
|
const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
|
|
177
|
-
expect(result.script).toContain('gradle
|
|
178
|
+
expect(result.script).toContain("-name 'gradle-wrapper.properties'");
|
|
179
|
+
expect(result.script).toContain("-name 'settings.gradle'");
|
|
180
|
+
expect(result.script).toContain("-name 'build.gradle.kts'");
|
|
181
|
+
expect(result.script).toContain("-name 'libs.versions.toml'");
|
|
178
182
|
expect(result.script).toContain('kmm-');
|
|
179
183
|
});
|
|
180
184
|
test('should use zstd compression format', async () => {
|
|
@@ -266,7 +270,8 @@ describe('Step Implementations', () => {
|
|
|
266
270
|
test('should auto-configure for gradle technology', async () => {
|
|
267
271
|
const executor = new CachePushStepExecutor();
|
|
268
272
|
const result = await executor.execute({ technology: 'gradle' }, {}, testConfig);
|
|
269
|
-
expect(result.script).toContain('gradle
|
|
273
|
+
expect(result.script).toContain("-name 'gradle-wrapper.properties'");
|
|
274
|
+
expect(result.script).toContain("-name 'build.gradle.kts'");
|
|
270
275
|
expect(result.script).toContain('~/.gradle/caches');
|
|
271
276
|
});
|
|
272
277
|
test('should auto-configure for npm technology', async () => {
|
|
@@ -285,7 +290,9 @@ describe('Step Implementations', () => {
|
|
|
285
290
|
test('should auto-configure for kmm technology', async () => {
|
|
286
291
|
const executor = new CachePushStepExecutor();
|
|
287
292
|
const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
|
|
288
|
-
expect(result.script).toContain('gradle
|
|
293
|
+
expect(result.script).toContain("-name 'gradle-wrapper.properties'");
|
|
294
|
+
expect(result.script).toContain("-name 'settings.gradle.kts'");
|
|
295
|
+
expect(result.script).toContain("-name 'libs.versions.toml'");
|
|
289
296
|
expect(result.script).toContain('kmm-');
|
|
290
297
|
expect(result.script).toContain('~/.gradle/caches');
|
|
291
298
|
expect(result.script).toContain('~/.gradle/wrapper/dists');
|
|
@@ -303,21 +310,18 @@ describe('Step Implementations', () => {
|
|
|
303
310
|
const executor = new CachePushStepExecutor();
|
|
304
311
|
await expect(executor.execute({ technology: 'unknown' }, {}, testConfig)).rejects.toThrow("Unknown cache technology 'unknown'");
|
|
305
312
|
});
|
|
306
|
-
test('should use shlock
|
|
313
|
+
test('should not use shlock (flaky on virtiofs cache mount) for preset push', async () => {
|
|
307
314
|
const executor = new CachePushStepExecutor();
|
|
308
315
|
const result = await executor.execute({ technology: 'cocoapods' }, {}, testConfig);
|
|
309
|
-
expect(result.script).toContain('shlock
|
|
310
|
-
expect(result.script).toContain('LOCK_FILE
|
|
311
|
-
expect(result.script).toContain('
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
expect(result.script).toContain('-ge 30');
|
|
315
|
-
});
|
|
316
|
-
test('should use shlock locking for manual cache_key push', async () => {
|
|
316
|
+
expect(result.script).not.toContain('shlock');
|
|
317
|
+
expect(result.script).not.toContain('LOCK_FILE');
|
|
318
|
+
expect(result.script).not.toContain('__ci_lock_tries');
|
|
319
|
+
});
|
|
320
|
+
test('should not use shlock for manual cache_key push', async () => {
|
|
317
321
|
const executor = new CachePushStepExecutor();
|
|
318
322
|
const result = await executor.execute({ cache_key: 'my-key', cache_paths: ['build'] }, {}, testConfig);
|
|
319
|
-
expect(result.script).toContain('shlock
|
|
320
|
-
expect(result.script).toContain('
|
|
323
|
+
expect(result.script).not.toContain('shlock');
|
|
324
|
+
expect(result.script).not.toContain('LOCK_FILE');
|
|
321
325
|
});
|
|
322
326
|
test('should use atomic write (tmp + mv) for preset push', async () => {
|
|
323
327
|
const executor = new CachePushStepExecutor();
|
|
@@ -325,6 +329,12 @@ describe('Step Implementations', () => {
|
|
|
325
329
|
expect(result.script).toContain('> "$CACHE_FILE.tmp"');
|
|
326
330
|
expect(result.script).toContain('mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
|
|
327
331
|
});
|
|
332
|
+
test('should use atomic write (tmp + mv) for manual cache_key push', async () => {
|
|
333
|
+
const executor = new CachePushStepExecutor();
|
|
334
|
+
const result = await executor.execute({ cache_key: 'my-key', cache_paths: ['build'] }, {}, testConfig);
|
|
335
|
+
expect(result.script).toContain('> "$CACHE_FILE.tmp"');
|
|
336
|
+
expect(result.script).toContain('mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
|
|
337
|
+
});
|
|
328
338
|
});
|
|
329
339
|
describe('XcodeBuildStepExecutor', () => {
|
|
330
340
|
test('should generate xcodebuild script', async () => {
|