@invarn/cibuild 1.4.7 → 1.4.9
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/yaml/steps/cache.d.ts +8 -3
- package/dist/src/yaml/steps/cache.d.ts.map +1 -1
- package/dist/src/yaml/steps/cache.js +131 -36
- package/dist/src/yaml/steps/steps.test.js +59 -5
- package/dist/src/yaml/steps/test-config.d.ts +2 -0
- package/dist/src/yaml/steps/test-config.d.ts.map +1 -1
- package/dist/src/yaml/steps/test-config.js +9 -1
- package/package.json +1 -1
|
@@ -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;YAuKzF,iBAAiB;CAqHhC"}
|
|
@@ -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.
|
|
@@ -83,33 +125,34 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
83
125
|
commands.push('echo "Debug mode enabled"');
|
|
84
126
|
commands.push('echo "Cache file: $CACHE_FILE"');
|
|
85
127
|
}
|
|
128
|
+
// Peer cache directory
|
|
129
|
+
const peerCacheDir = config.paths.peerCacheDir || '';
|
|
130
|
+
if (peerCacheDir) {
|
|
131
|
+
commands.push('');
|
|
132
|
+
commands.push(`PEER_CACHE_DIR="${this.escapeBash(peerCacheDir)}"`);
|
|
133
|
+
commands.push(`PEER_CACHE_FILE="$PEER_CACHE_DIR/${this.escapeBash(cacheKey)}.tar.zst"`);
|
|
134
|
+
}
|
|
86
135
|
// Check if cache file exists
|
|
87
136
|
commands.push('');
|
|
88
137
|
commands.push('# Check if cache exists');
|
|
89
138
|
commands.push('if [ -f "$CACHE_FILE" ]; then');
|
|
90
|
-
commands.push(' echo "Cache found, extracting..."');
|
|
139
|
+
commands.push(' echo "Cache found (local), extracting..."');
|
|
91
140
|
if (isDebugMode) {
|
|
92
141
|
commands.push(' echo "Cache file size: $(du -h "$CACHE_FILE" | cut -f1)"');
|
|
93
142
|
}
|
|
94
|
-
// Extract cache
|
|
95
143
|
commands.push(' zstd -dc "$CACHE_FILE" | tar -xf - -C /');
|
|
96
|
-
commands.push(' echo "
|
|
97
|
-
//
|
|
98
|
-
if (
|
|
99
|
-
commands.push('
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
commands.push(' echo " ✓ $EXPANDED_PATH"');
|
|
105
|
-
commands.push(' else');
|
|
106
|
-
commands.push(' echo " ✗ $EXPANDED_PATH (not found)"');
|
|
107
|
-
commands.push(' fi');
|
|
108
|
-
}
|
|
144
|
+
commands.push(' echo "CACHE_SOURCE=local"');
|
|
145
|
+
// Peer fallback
|
|
146
|
+
if (peerCacheDir) {
|
|
147
|
+
commands.push('elif [ -f "$PEER_CACHE_FILE" ] 2>/dev/null; then');
|
|
148
|
+
commands.push(' echo "Cache found (peer), extracting..."');
|
|
149
|
+
commands.push(' tee "$CACHE_FILE.tmp" < "$PEER_CACHE_FILE" | zstd -dc | tar -xf - -C /');
|
|
150
|
+
commands.push(' mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
|
|
151
|
+
commands.push(' echo "CACHE_SOURCE=peer"');
|
|
109
152
|
}
|
|
110
153
|
commands.push('else');
|
|
111
154
|
commands.push(` echo "No cache found for key: ${this.escapeBash(cacheKey)}"`);
|
|
112
|
-
commands.push(' echo "
|
|
155
|
+
commands.push(' echo "CACHE_SOURCE=cold"');
|
|
113
156
|
commands.push('fi');
|
|
114
157
|
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
115
158
|
return this.createScriptStep(script, stepName);
|
|
@@ -138,7 +181,10 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
138
181
|
commands.push(' find . -name "$(basename "$1")" -not -path "*/DerivedData/*" -not -path "*/.build/*" 2>/dev/null | head -1');
|
|
139
182
|
commands.push('}');
|
|
140
183
|
commands.push('');
|
|
141
|
-
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) {
|
|
142
188
|
const preset = chain[0];
|
|
143
189
|
commands.push(`LOCKFILE=$(__ci_find_lockfile "${this.escapeBash(preset.lockfile)}")`);
|
|
144
190
|
commands.push('if [ -n "$LOCKFILE" ] && [ -f "$LOCKFILE" ]; then');
|
|
@@ -170,35 +216,55 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
170
216
|
}
|
|
171
217
|
commands.push('');
|
|
172
218
|
if (isDebugMode) {
|
|
173
|
-
commands.push('echo "Lockfile: $LOCKFILE"');
|
|
174
219
|
commands.push('echo "Cache key: $CACHE_KEY"');
|
|
175
220
|
}
|
|
176
221
|
commands.push('CACHE_FILE="$CACHE_DIR/$CACHE_KEY.tar.zst"');
|
|
177
222
|
commands.push('');
|
|
223
|
+
// Peer cache directory — set from config, empty string disables peer lookup
|
|
224
|
+
const peerCacheDir = config.paths.peerCacheDir || '';
|
|
225
|
+
if (peerCacheDir) {
|
|
226
|
+
commands.push(`PEER_CACHE_DIR="${this.escapeBash(peerCacheDir)}"`);
|
|
227
|
+
commands.push('PEER_CACHE_FILE="$PEER_CACHE_DIR/$CACHE_KEY.tar.zst"');
|
|
228
|
+
commands.push('');
|
|
229
|
+
}
|
|
230
|
+
// 1. Local hit
|
|
178
231
|
commands.push('if [ -f "$CACHE_FILE" ]; then');
|
|
179
|
-
commands.push(' echo "Cache found, extracting..."');
|
|
232
|
+
commands.push(' echo "Cache found (local), extracting..."');
|
|
180
233
|
if (isDebugMode) {
|
|
181
234
|
commands.push(' echo "Cache file size: $(du -h "$CACHE_FILE" | cut -f1)"');
|
|
182
235
|
}
|
|
183
236
|
commands.push(' zstd -dc "$CACHE_FILE" | tar -xf - -C /');
|
|
184
|
-
commands.push(' echo "
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
commands.push('
|
|
191
|
-
commands.push(' if [ -e "$EXPANDED" ]; then');
|
|
192
|
-
commands.push(' echo " ✓ $EXPANDED"');
|
|
193
|
-
commands.push(' else');
|
|
194
|
-
commands.push(' echo " ✗ $EXPANDED (not found)"');
|
|
195
|
-
commands.push(' fi');
|
|
237
|
+
commands.push(' echo "CACHE_SOURCE=local CACHE_KEY=$CACHE_KEY"');
|
|
238
|
+
// 2. Peer hit — tee to warm local cache while extracting in one pass
|
|
239
|
+
if (peerCacheDir) {
|
|
240
|
+
commands.push('elif [ -f "$PEER_CACHE_FILE" ] 2>/dev/null; then');
|
|
241
|
+
commands.push(' echo "Cache found (peer), extracting..."');
|
|
242
|
+
if (isDebugMode) {
|
|
243
|
+
commands.push(' echo "Peer cache file size: $(du -h "$PEER_CACHE_FILE" | cut -f1)"');
|
|
196
244
|
}
|
|
245
|
+
// Single NFS read: tee forks the stream to local cache file AND decompression
|
|
246
|
+
commands.push(' tee "$CACHE_FILE.tmp" < "$PEER_CACHE_FILE" | zstd -dc | tar -xf - -C /');
|
|
247
|
+
commands.push(' mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
|
|
248
|
+
commands.push(' echo "CACHE_SOURCE=peer CACHE_KEY=$CACHE_KEY"');
|
|
197
249
|
}
|
|
250
|
+
// 3. Cold miss
|
|
198
251
|
commands.push('else');
|
|
199
252
|
commands.push(' echo "No cache found for key: $CACHE_KEY"');
|
|
200
|
-
commands.push(' echo "
|
|
253
|
+
commands.push(' echo "CACHE_SOURCE=cold CACHE_KEY=$CACHE_KEY"');
|
|
201
254
|
commands.push('fi');
|
|
255
|
+
if (isDebugMode) {
|
|
256
|
+
commands.push('echo "Restored paths:"');
|
|
257
|
+
const allPaths = [...new Set(chain.flatMap(p => p.paths))];
|
|
258
|
+
for (const p of allPaths) {
|
|
259
|
+
commands.push(`EXPANDED="${this.escapeBash(p)}"`);
|
|
260
|
+
commands.push('EXPANDED="${EXPANDED/#~/${CIBUILD_USER_HOME:-$HOME}}"');
|
|
261
|
+
commands.push('if [ -e "$EXPANDED" ]; then');
|
|
262
|
+
commands.push(' echo " ✓ $EXPANDED"');
|
|
263
|
+
commands.push('else');
|
|
264
|
+
commands.push(' echo " ✗ $EXPANDED (not found)"');
|
|
265
|
+
commands.push('fi');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
202
268
|
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
203
269
|
return this.createScriptStep(script, stepName);
|
|
204
270
|
}
|
|
@@ -320,7 +386,20 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
320
386
|
commands.push(' echo "Paths to cache:"');
|
|
321
387
|
commands.push(' printf " %s\\n" "${PATHS_TO_CACHE[@]}"');
|
|
322
388
|
}
|
|
389
|
+
// Acquire lock with shlock
|
|
390
|
+
commands.push(' LOCK_FILE="$CACHE_FILE.lock"');
|
|
391
|
+
commands.push(' __ci_lock_tries=0');
|
|
392
|
+
commands.push(' while ! shlock -f "$LOCK_FILE" -p $$; do');
|
|
393
|
+
commands.push(' __ci_lock_tries=$((__ci_lock_tries + 1))');
|
|
394
|
+
commands.push(' if [ "$__ci_lock_tries" -ge 30 ]; then');
|
|
395
|
+
commands.push(' echo "Warning: cache push lock timeout, skipping"');
|
|
396
|
+
commands.push(' rm -f "$LOCK_FILE"');
|
|
397
|
+
commands.push(' exit 0');
|
|
398
|
+
commands.push(' fi');
|
|
399
|
+
commands.push(' sleep 1');
|
|
400
|
+
commands.push(' done');
|
|
323
401
|
commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" || true');
|
|
402
|
+
commands.push(' rm -f "$LOCK_FILE"');
|
|
324
403
|
commands.push(' if [ -f "$CACHE_FILE" ]; then');
|
|
325
404
|
commands.push(' echo "Cache created successfully"');
|
|
326
405
|
if (isDebugMode) {
|
|
@@ -356,7 +435,10 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
356
435
|
commands.push(' find . -name "$(basename "$1")" -not -path "*/DerivedData/*" -not -path "*/.build/*" 2>/dev/null | head -1');
|
|
357
436
|
commands.push('}');
|
|
358
437
|
commands.push('');
|
|
359
|
-
if (chain.length === 1) {
|
|
438
|
+
if (chain.length === 1 && chain[0].fingerprint && chain[0].fingerprint.length > 0) {
|
|
439
|
+
commands.push(...emitFingerprintKeyCommands(chain[0]));
|
|
440
|
+
}
|
|
441
|
+
else if (chain.length === 1) {
|
|
360
442
|
const preset = chain[0];
|
|
361
443
|
commands.push(`LOCKFILE=$(__ci_find_lockfile "${this.escapeBash(preset.lockfile)}")`);
|
|
362
444
|
commands.push('if [ -n "$LOCKFILE" ] && [ -f "$LOCKFILE" ]; then');
|
|
@@ -384,7 +466,6 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
384
466
|
}
|
|
385
467
|
commands.push('');
|
|
386
468
|
if (isDebugMode) {
|
|
387
|
-
commands.push('echo "Lockfile: $LOCKFILE"');
|
|
388
469
|
commands.push('echo "Cache key: $CACHE_KEY"');
|
|
389
470
|
}
|
|
390
471
|
commands.push('CACHE_FILE="$CACHE_DIR/$CACHE_KEY.tar.zst"');
|
|
@@ -412,7 +493,21 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
412
493
|
commands.push(' echo "Cache already up to date for key: $CACHE_KEY"');
|
|
413
494
|
commands.push('elif [ ${#PATHS_TO_CACHE[@]} -gt 0 ]; then');
|
|
414
495
|
commands.push(' echo "Caching ${#PATHS_TO_CACHE[@]} path(s)..."');
|
|
496
|
+
// Acquire lock with shlock (macOS-native, stale-lock safe via PID check)
|
|
497
|
+
commands.push(' LOCK_FILE="$CACHE_FILE.lock"');
|
|
498
|
+
commands.push(' __ci_lock_tries=0');
|
|
499
|
+
commands.push(' while ! shlock -f "$LOCK_FILE" -p $$; do');
|
|
500
|
+
commands.push(' __ci_lock_tries=$((__ci_lock_tries + 1))');
|
|
501
|
+
commands.push(' if [ "$__ci_lock_tries" -ge 30 ]; then');
|
|
502
|
+
commands.push(' echo "Warning: cache push lock timeout, skipping"');
|
|
503
|
+
commands.push(' rm -f "$LOCK_FILE"');
|
|
504
|
+
commands.push(' exit 0');
|
|
505
|
+
commands.push(' fi');
|
|
506
|
+
commands.push(' sleep 1');
|
|
507
|
+
commands.push(' done');
|
|
508
|
+
// Atomic write: compress to .tmp, then mv (peers never see half-written files)
|
|
415
509
|
commands.push(' tar -cf - "${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" || true');
|
|
510
|
+
commands.push(' rm -f "$LOCK_FILE"');
|
|
416
511
|
commands.push(' if [ -f "$CACHE_FILE" ]; then');
|
|
417
512
|
commands.push(' echo "Cache created successfully"');
|
|
418
513
|
if (isDebugMode) {
|
|
@@ -6,7 +6,7 @@ import { describe, test, expect } from '@jest/globals';
|
|
|
6
6
|
import { GitCloneStepExecutor } from './git-clone.js';
|
|
7
7
|
import { ScriptStepExecutor } from './script.js';
|
|
8
8
|
import { CachePullStepExecutor, CachePushStepExecutor } from './cache.js';
|
|
9
|
-
import { testConfig } from './test-config.js';
|
|
9
|
+
import { testConfig, testConfigNoPeer } from './test-config.js';
|
|
10
10
|
import { XcodeBuildStepExecutor, XcodeTestStepExecutor } from './xcode.js';
|
|
11
11
|
import { SetJavaVersionStepExecutor, GradleBuildStepExecutor, AndroidLintStepExecutor, AndroidUnitTestStepExecutor, } from './android.js';
|
|
12
12
|
describe('Step Implementations', () => {
|
|
@@ -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 () => {
|
|
@@ -189,6 +193,31 @@ describe('Step Implementations', () => {
|
|
|
189
193
|
const executor = new CachePullStepExecutor();
|
|
190
194
|
await expect(executor.execute({ technology: 'unknown' }, {}, testConfig)).rejects.toThrow("Unknown cache technology 'unknown'");
|
|
191
195
|
});
|
|
196
|
+
test('should include peer cache fallback when peerCacheDir is set', async () => {
|
|
197
|
+
const executor = new CachePullStepExecutor();
|
|
198
|
+
const result = await executor.execute({ technology: 'cocoapods' }, {}, testConfig);
|
|
199
|
+
expect(result.script).toContain('PEER_CACHE_DIR=');
|
|
200
|
+
expect(result.script).toContain('PEER_CACHE_FILE=');
|
|
201
|
+
expect(result.script).toContain('CACHE_SOURCE=local');
|
|
202
|
+
expect(result.script).toContain('CACHE_SOURCE=peer');
|
|
203
|
+
expect(result.script).toContain('CACHE_SOURCE=cold');
|
|
204
|
+
// tee pattern: single read from peer, simultaneous extract + local warm
|
|
205
|
+
expect(result.script).toContain('tee "$CACHE_FILE.tmp" < "$PEER_CACHE_FILE"');
|
|
206
|
+
});
|
|
207
|
+
test('should skip peer fallback when peerCacheDir is not set', async () => {
|
|
208
|
+
const executor = new CachePullStepExecutor();
|
|
209
|
+
const result = await executor.execute({ technology: 'cocoapods' }, {}, testConfigNoPeer);
|
|
210
|
+
expect(result.script).not.toContain('PEER_CACHE_DIR=');
|
|
211
|
+
expect(result.script).not.toContain('PEER_CACHE_FILE=');
|
|
212
|
+
expect(result.script).not.toContain('CACHE_SOURCE=peer');
|
|
213
|
+
});
|
|
214
|
+
test('should include peer fallback for manual cache_key pull', async () => {
|
|
215
|
+
const executor = new CachePullStepExecutor();
|
|
216
|
+
const result = await executor.execute({ cache_key: 'my-key' }, {}, testConfig);
|
|
217
|
+
expect(result.script).toContain('PEER_CACHE_DIR=');
|
|
218
|
+
expect(result.script).toContain('CACHE_SOURCE=peer');
|
|
219
|
+
expect(result.script).toContain('tee "$CACHE_FILE.tmp" < "$PEER_CACHE_FILE"');
|
|
220
|
+
});
|
|
192
221
|
});
|
|
193
222
|
describe('CachePushStepExecutor', () => {
|
|
194
223
|
test('should generate cache push script', async () => {
|
|
@@ -241,7 +270,8 @@ describe('Step Implementations', () => {
|
|
|
241
270
|
test('should auto-configure for gradle technology', async () => {
|
|
242
271
|
const executor = new CachePushStepExecutor();
|
|
243
272
|
const result = await executor.execute({ technology: 'gradle' }, {}, testConfig);
|
|
244
|
-
expect(result.script).toContain('gradle
|
|
273
|
+
expect(result.script).toContain("-name 'gradle-wrapper.properties'");
|
|
274
|
+
expect(result.script).toContain("-name 'build.gradle.kts'");
|
|
245
275
|
expect(result.script).toContain('~/.gradle/caches');
|
|
246
276
|
});
|
|
247
277
|
test('should auto-configure for npm technology', async () => {
|
|
@@ -260,7 +290,9 @@ describe('Step Implementations', () => {
|
|
|
260
290
|
test('should auto-configure for kmm technology', async () => {
|
|
261
291
|
const executor = new CachePushStepExecutor();
|
|
262
292
|
const result = await executor.execute({ technology: 'kmm' }, {}, testConfig);
|
|
263
|
-
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'");
|
|
264
296
|
expect(result.script).toContain('kmm-');
|
|
265
297
|
expect(result.script).toContain('~/.gradle/caches');
|
|
266
298
|
expect(result.script).toContain('~/.gradle/wrapper/dists');
|
|
@@ -278,6 +310,28 @@ describe('Step Implementations', () => {
|
|
|
278
310
|
const executor = new CachePushStepExecutor();
|
|
279
311
|
await expect(executor.execute({ technology: 'unknown' }, {}, testConfig)).rejects.toThrow("Unknown cache technology 'unknown'");
|
|
280
312
|
});
|
|
313
|
+
test('should use shlock locking for preset push', async () => {
|
|
314
|
+
const executor = new CachePushStepExecutor();
|
|
315
|
+
const result = await executor.execute({ technology: 'cocoapods' }, {}, testConfig);
|
|
316
|
+
expect(result.script).toContain('shlock -f "$LOCK_FILE" -p $$');
|
|
317
|
+
expect(result.script).toContain('LOCK_FILE="$CACHE_FILE.lock"');
|
|
318
|
+
expect(result.script).toContain('rm -f "$LOCK_FILE"');
|
|
319
|
+
// Lock timeout after 30 retries
|
|
320
|
+
expect(result.script).toContain('__ci_lock_tries');
|
|
321
|
+
expect(result.script).toContain('-ge 30');
|
|
322
|
+
});
|
|
323
|
+
test('should use shlock locking for manual cache_key push', async () => {
|
|
324
|
+
const executor = new CachePushStepExecutor();
|
|
325
|
+
const result = await executor.execute({ cache_key: 'my-key', cache_paths: ['build'] }, {}, testConfig);
|
|
326
|
+
expect(result.script).toContain('shlock -f "$LOCK_FILE" -p $$');
|
|
327
|
+
expect(result.script).toContain('rm -f "$LOCK_FILE"');
|
|
328
|
+
});
|
|
329
|
+
test('should use atomic write (tmp + mv) for preset push', async () => {
|
|
330
|
+
const executor = new CachePushStepExecutor();
|
|
331
|
+
const result = await executor.execute({ technology: 'gradle' }, {}, testConfig);
|
|
332
|
+
expect(result.script).toContain('> "$CACHE_FILE.tmp"');
|
|
333
|
+
expect(result.script).toContain('mv "$CACHE_FILE.tmp" "$CACHE_FILE"');
|
|
334
|
+
});
|
|
281
335
|
});
|
|
282
336
|
describe('XcodeBuildStepExecutor', () => {
|
|
283
337
|
test('should generate xcodebuild script', async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-config.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/test-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,eAAO,MAAM,UAAU,EAAE,QAexB,CAAC"}
|
|
1
|
+
{"version":3,"file":"test-config.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/test-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,eAAO,MAAM,UAAU,EAAE,QAexB,CAAC;AAEF,qDAAqD;AACrD,eAAO,MAAM,gBAAgB,EAAE,QAM9B,CAAC"}
|
|
@@ -10,8 +10,16 @@ export const testConfig = {
|
|
|
10
10
|
paths: {
|
|
11
11
|
buildsDir: '.ci-builds',
|
|
12
12
|
cacheDir: '.ci-cache',
|
|
13
|
-
peerCacheDir: 'peer',
|
|
13
|
+
peerCacheDir: '/Volumes/My Shared Files/cibuild-peer-cache',
|
|
14
14
|
derivedDataDir: '~/Library/Developer/Xcode/DerivedData',
|
|
15
15
|
},
|
|
16
16
|
};
|
|
17
|
+
/** Config without peer cache (no NFS peer runner) */
|
|
18
|
+
export const testConfigNoPeer = {
|
|
19
|
+
...testConfig,
|
|
20
|
+
paths: {
|
|
21
|
+
...testConfig.paths,
|
|
22
|
+
peerCacheDir: undefined,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
17
25
|
//# sourceMappingURL=test-config.js.map
|