@invarn/cibuild 1.9.7 → 1.9.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.
|
@@ -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;AAwFF;;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;YA0GzF,iBAAiB;CAqMhC;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;YAgKzF,iBAAiB;CAmIhC"}
|
|
@@ -97,7 +97,12 @@ function resolvePresetChain(technology) {
|
|
|
97
97
|
*/
|
|
98
98
|
function emitDaemonPullBranch() {
|
|
99
99
|
return [
|
|
100
|
-
|
|
100
|
+
// stderr is silenced on the probe: a cache miss is the normal case and
|
|
101
|
+
// makes curl emit "(22) ... 404" (and the empty body makes zstd print
|
|
102
|
+
// "unexpected end of file"). Those are non-actionable — the branch just
|
|
103
|
+
// falls through to the cold path — so they're pure log noise. Exit
|
|
104
|
+
// status is preserved (pipefail), so the fall-through still works.
|
|
105
|
+
'elif [ -n "$CIBUILD_CACHE_DAEMON" ] && { curl -fsS ${CIBUILD_CACHE_TOKEN:+-H "Authorization: Bearer $CIBUILD_CACHE_TOKEN"} "$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst" | zstd -dc | tar -xf - -C /; } 2>/dev/null; then',
|
|
101
106
|
' echo "Cache found (daemon), extracting..."',
|
|
102
107
|
// LRU bump the tarball the daemon just persisted to ~/cache (same dir as
|
|
103
108
|
// the --dir mount); tolerate its absence in case the daemon served it
|
|
@@ -237,13 +242,20 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
237
242
|
commands.push('fi');
|
|
238
243
|
}
|
|
239
244
|
else {
|
|
240
|
-
// Fallback chain:
|
|
245
|
+
// Fallback chain: probe every lockfile up front, then branch on the
|
|
246
|
+
// results. The probes must precede the if/elif — emitting a probe
|
|
247
|
+
// inside a prior preset's `then` body (the old shape) meant the
|
|
248
|
+
// second lockfile was only checked when the FIRST one was found, so
|
|
249
|
+
// an SPM project with no Podfile.lock never reached the Package.resolved
|
|
250
|
+
// branch and fell through to `<first>-no-lockfile`.
|
|
251
|
+
for (let i = 0; i < chain.length; i++) {
|
|
252
|
+
commands.push(`__ci_lockfile_${i}=$(__ci_find_lockfile "${this.escapeBash(chain[i].lockfile)}")`);
|
|
253
|
+
}
|
|
241
254
|
for (let i = 0; i < chain.length; i++) {
|
|
242
255
|
const preset = chain[i];
|
|
243
256
|
const cond = i === 0 ? 'if' : 'elif';
|
|
244
|
-
commands.push(
|
|
245
|
-
commands.push(
|
|
246
|
-
commands.push(' LOCKFILE="$__ci_lockfile_candidate"');
|
|
257
|
+
commands.push(`${cond} [ -n "$__ci_lockfile_${i}" ] && [ -f "$__ci_lockfile_${i}" ]; then`);
|
|
258
|
+
commands.push(` LOCKFILE="$__ci_lockfile_${i}"`);
|
|
247
259
|
commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
|
|
248
260
|
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${__ci_proj}-\${CHECKSUM}"`);
|
|
249
261
|
if (isDebugMode) {
|
|
@@ -545,12 +557,19 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
545
557
|
commands.push('fi');
|
|
546
558
|
}
|
|
547
559
|
else {
|
|
560
|
+
// Fallback chain: probe every lockfile up front, then branch — same
|
|
561
|
+
// fix as cache-pull. A probe nested in a prior preset's `then` body
|
|
562
|
+
// only ran when the first lockfile was found, so push wrote the
|
|
563
|
+
// `<first>-no-lockfile` key for SPM projects, mismatching what a
|
|
564
|
+
// correct pull would look for.
|
|
565
|
+
for (let i = 0; i < chain.length; i++) {
|
|
566
|
+
commands.push(`__ci_lockfile_${i}=$(__ci_find_lockfile "${this.escapeBash(chain[i].lockfile)}")`);
|
|
567
|
+
}
|
|
548
568
|
for (let i = 0; i < chain.length; i++) {
|
|
549
569
|
const preset = chain[i];
|
|
550
570
|
const cond = i === 0 ? 'if' : 'elif';
|
|
551
|
-
commands.push(
|
|
552
|
-
commands.push(
|
|
553
|
-
commands.push(' LOCKFILE="$__ci_lockfile_candidate"');
|
|
571
|
+
commands.push(`${cond} [ -n "$__ci_lockfile_${i}" ] && [ -f "$__ci_lockfile_${i}" ]; then`);
|
|
572
|
+
commands.push(` LOCKFILE="$__ci_lockfile_${i}"`);
|
|
554
573
|
commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
|
|
555
574
|
commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${__ci_proj}-\${CHECKSUM}"`);
|
|
556
575
|
}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* Tests each step executor with sample inputs
|
|
4
4
|
*/
|
|
5
5
|
import { describe, test, expect } from '@jest/globals';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
6
10
|
import { GitCloneStepExecutor } from './git-clone.js';
|
|
7
11
|
import { ScriptStepExecutor } from './script.js';
|
|
8
12
|
import { CachePullStepExecutor, CachePushStepExecutor } from './cache.js';
|
|
@@ -166,6 +170,101 @@ describe('Step Implementations', () => {
|
|
|
166
170
|
expect(result.script).toContain('Package.resolved');
|
|
167
171
|
expect(result.script).toContain('spm-');
|
|
168
172
|
});
|
|
173
|
+
// A static `toContain` check can't catch a control-flow bug in the
|
|
174
|
+
// fallback chain (the script "contains" both Podfile.lock and
|
|
175
|
+
// Package.resolved either way). So actually execute the generated
|
|
176
|
+
// key-derivation in a fixture repo and read the resolved key back.
|
|
177
|
+
describe('ios fallback chain (executed in a fixture repo)', () => {
|
|
178
|
+
// Run the generated ios cache-pull in a fixture repo and capture
|
|
179
|
+
// BOTH streams (the daemon probe writes its noise to stderr).
|
|
180
|
+
async function runIosPull(build, env = {}) {
|
|
181
|
+
const script = (await new CachePullStepExecutor().execute({ technology: 'ios' }, {}, testConfigNoPeer)).script;
|
|
182
|
+
const dir = mkdtempSync(join(tmpdir(), 'cibuild-cache-'));
|
|
183
|
+
try {
|
|
184
|
+
build(dir);
|
|
185
|
+
const scriptPath = join(dir, '__cache_pull.sh');
|
|
186
|
+
writeFileSync(scriptPath, script);
|
|
187
|
+
let stdout = '';
|
|
188
|
+
let stderr = '';
|
|
189
|
+
try {
|
|
190
|
+
stdout = execFileSync('bash', [scriptPath], {
|
|
191
|
+
cwd: dir,
|
|
192
|
+
encoding: 'utf-8',
|
|
193
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
194
|
+
env: { ...process.env, ...env },
|
|
195
|
+
timeout: 20000,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
const err = e;
|
|
200
|
+
stdout = String(err.stdout ?? '');
|
|
201
|
+
stderr = String(err.stderr ?? '');
|
|
202
|
+
}
|
|
203
|
+
return { stdout, stderr, combined: stdout + stderr };
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
rmSync(dir, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function resolveIosCacheKey(build) {
|
|
210
|
+
const { combined } = await runIosPull(build);
|
|
211
|
+
const m = combined.match(/CACHE_KEY=(\S+)/);
|
|
212
|
+
if (!m)
|
|
213
|
+
throw new Error(`No CACHE_KEY in output:\n${combined}`);
|
|
214
|
+
return m[1];
|
|
215
|
+
}
|
|
216
|
+
function writeSpmFixture(dir) {
|
|
217
|
+
const swiftpm = join(dir, 'App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm');
|
|
218
|
+
mkdirSync(swiftpm, { recursive: true });
|
|
219
|
+
writeFileSync(join(swiftpm, 'Package.resolved'), '{"pins":[],"version":3}');
|
|
220
|
+
}
|
|
221
|
+
test('a cold daemon miss does not print curl/zstd noise', async () => {
|
|
222
|
+
// Reproduce the runner setup: a configured cache daemon whose
|
|
223
|
+
// probe fails for an uncached key. (We point at a closed port so
|
|
224
|
+
// curl errors instantly with no server handle to leak — the fix
|
|
225
|
+
// silences curl's stderr regardless of the failure mode, exactly
|
|
226
|
+
// as it does for the 404 seen in production.) The probe must fall
|
|
227
|
+
// through to cold quietly: no "curl:" or "unexpected end of file".
|
|
228
|
+
const { stdout, combined } = await runIosPull(writeSpmFixture, {
|
|
229
|
+
CIBUILD_CACHE_DAEMON: 'http://127.0.0.1:1',
|
|
230
|
+
});
|
|
231
|
+
expect(combined).not.toContain('curl:');
|
|
232
|
+
expect(combined).not.toContain('unexpected end of file');
|
|
233
|
+
// Still resolves the SPM key and reports a cold miss.
|
|
234
|
+
expect(stdout).toContain('CACHE_SOURCE=cold');
|
|
235
|
+
expect(stdout).toMatch(/CACHE_KEY=spm-/);
|
|
236
|
+
});
|
|
237
|
+
test('SPM project (Package.resolved, no Podfile.lock) resolves an spm- key', async () => {
|
|
238
|
+
const key = await resolveIosCacheKey((dir) => {
|
|
239
|
+
// Real Xcode SPM location — Package.resolved committed under
|
|
240
|
+
// the shared xcworkspace data, no Podfile.lock anywhere.
|
|
241
|
+
const swiftpm = join(dir, 'App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm');
|
|
242
|
+
mkdirSync(swiftpm, { recursive: true });
|
|
243
|
+
writeFileSync(join(swiftpm, 'Package.resolved'), '{"pins":[],"version":3}');
|
|
244
|
+
});
|
|
245
|
+
expect(key).toMatch(/^spm-/);
|
|
246
|
+
expect(key).not.toContain('no-lockfile');
|
|
247
|
+
});
|
|
248
|
+
test('CocoaPods project (Podfile.lock) resolves a pods- key', async () => {
|
|
249
|
+
const key = await resolveIosCacheKey((dir) => {
|
|
250
|
+
writeFileSync(join(dir, 'Podfile.lock'), 'PODS:\n - Alamofire\n');
|
|
251
|
+
});
|
|
252
|
+
expect(key).toMatch(/^pods-/);
|
|
253
|
+
expect(key).not.toContain('no-lockfile');
|
|
254
|
+
});
|
|
255
|
+
test('Podfile.lock wins over Package.resolved when both exist', async () => {
|
|
256
|
+
const key = await resolveIosCacheKey((dir) => {
|
|
257
|
+
writeFileSync(join(dir, 'Podfile.lock'), 'PODS:\n');
|
|
258
|
+
writeFileSync(join(dir, 'Package.resolved'), '{"pins":[]}');
|
|
259
|
+
});
|
|
260
|
+
expect(key).toMatch(/^pods-/);
|
|
261
|
+
expect(key).not.toContain('no-lockfile');
|
|
262
|
+
});
|
|
263
|
+
test('neither lockfile present falls back to a no-lockfile key', async () => {
|
|
264
|
+
const key = await resolveIosCacheKey(() => { });
|
|
265
|
+
expect(key).toContain('no-lockfile');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
169
268
|
test('should auto-configure for yarn technology', async () => {
|
|
170
269
|
const executor = new CachePullStepExecutor();
|
|
171
270
|
const result = await executor.execute({ technology: 'yarn' }, {}, testConfig);
|