@invarn/cibuild 2.5.9 → 2.6.1
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 +10 -10
- package/dist/src/envman/envstore-round-trip.test.d.ts +27 -0
- package/dist/src/envman/envstore-round-trip.test.d.ts.map +1 -0
- package/dist/src/envman/envstore-round-trip.test.js +130 -0
- package/dist/src/envman/shim.d.ts +16 -0
- package/dist/src/envman/shim.d.ts.map +1 -0
- package/dist/src/envman/shim.js +66 -0
- package/dist/src/runner.d.ts.map +1 -1
- package/dist/src/runner.js +2 -50
- package/dist/src/yaml/steps/cache-pull-daemon.test.d.ts +27 -0
- package/dist/src/yaml/steps/cache-pull-daemon.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/cache-pull-daemon.test.js +203 -0
- package/dist/src/yaml/steps/cache-pull-restore-roots.test.d.ts +42 -0
- package/dist/src/yaml/steps/cache-pull-restore-roots.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/cache-pull-restore-roots.test.js +200 -0
- package/dist/src/yaml/steps/cache-push-daemon.test.js +32 -2
- package/dist/src/yaml/steps/cache.d.ts.map +1 -1
- package/dist/src/yaml/steps/cache.js +259 -30
- package/dist/src/yaml/steps/steps.test.js +20 -9
- package/package.json +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cache-pull` restores every cached path to the root it was recorded against.
|
|
3
|
+
*
|
|
4
|
+
* `cache-push` archives two kinds of entry in one tarball. A `~`- or `/`-rooted
|
|
5
|
+
* path is expanded and tar stores it with the leading `/` stripped
|
|
6
|
+
* (`/home/builder/.gradle/caches` -> `home/builder/.gradle/caches`). A
|
|
7
|
+
* project-relative path (`Pods`, `.gradle`, `node_modules`) is stored as-is,
|
|
8
|
+
* relative to the step's working directory. Every restore then used a single
|
|
9
|
+
* `tar -xf - -C /`, which is right for the first kind and impossible for the
|
|
10
|
+
* second: the build user cannot create `/Pods`.
|
|
11
|
+
*
|
|
12
|
+
* Three shapes, three different failures, all measured before this was fixed:
|
|
13
|
+
*
|
|
14
|
+
* | `cache_paths` | before |
|
|
15
|
+
* |------------------------------------------|---------------------------------|
|
|
16
|
+
* | all `~`-rooted | correct |
|
|
17
|
+
* | mixed (every Gradle/Android pipeline) | `~` half landed, relative half never did, tar exited non-zero, reported **cold** on a build that was warm |
|
|
18
|
+
* | all project-relative (Pods, node_modules)| **nothing** landed — those caches were written on every build and had never restored |
|
|
19
|
+
*
|
|
20
|
+
* So the mixed shape lied and the all-relative shape silently did nothing. Both
|
|
21
|
+
* are asserted here, and the all-`~` shape is asserted too because it already
|
|
22
|
+
* worked and the fix must not cost it anything.
|
|
23
|
+
*
|
|
24
|
+
* ### Why this runs the real `tar`
|
|
25
|
+
*
|
|
26
|
+
* The sibling daemon suite stubs `tar` to `cat > /dev/null`, because the
|
|
27
|
+
* generated script extracts with `-C /` and nothing there is worth writing to
|
|
28
|
+
* the root of the machine running the suite. This suite cannot: *where the
|
|
29
|
+
* bytes land* is the entire question, and a stub answers it by construction.
|
|
30
|
+
*
|
|
31
|
+
* It stays out of the real root by pointing `CIBUILD_USER_HOME` at a temporary
|
|
32
|
+
* directory. `~/.gradle/caches` then expands to an absolute path *inside* that
|
|
33
|
+
* directory, so `-C /` reassembles it exactly where it belongs and writes
|
|
34
|
+
* nothing outside the temp tree. The path is resolved through `realpath` first:
|
|
35
|
+
* on macOS `/var` is a symlink to `/private/var`, and tar refuses to extract
|
|
36
|
+
* through a symlink.
|
|
37
|
+
*
|
|
38
|
+
* `zstd` is stubbed to an identity copy, as in the sibling suite — it is not on
|
|
39
|
+
* every host, and compression is not what is under test.
|
|
40
|
+
*/
|
|
41
|
+
import { describe, test, expect } from '@jest/globals';
|
|
42
|
+
import { execFile } from 'child_process';
|
|
43
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, realpathSync, rmSync, chmodSync, } from 'fs';
|
|
44
|
+
import { join } from 'path';
|
|
45
|
+
import { tmpdir } from 'os';
|
|
46
|
+
import { promisify } from 'util';
|
|
47
|
+
import { CachePullStepExecutor } from './cache.js';
|
|
48
|
+
import { testConfigNoPeer } from './test-config.js';
|
|
49
|
+
const run = promisify(execFile);
|
|
50
|
+
const CACHE_KEY = 'gradle-abc123def456-fingerprint';
|
|
51
|
+
async function restore(shape) {
|
|
52
|
+
// Resolved: see the header — tar will not extract through a symlink, and on
|
|
53
|
+
// macOS the temp root sits behind one.
|
|
54
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'cibuild-restore-roots-')));
|
|
55
|
+
const home = join(root, 'home');
|
|
56
|
+
const checkout = join(root, 'checkout');
|
|
57
|
+
const bin = join(root, 'bin');
|
|
58
|
+
mkdirSync(home, { recursive: true });
|
|
59
|
+
mkdirSync(checkout, { recursive: true });
|
|
60
|
+
mkdirSync(bin, { recursive: true });
|
|
61
|
+
try {
|
|
62
|
+
// Identity stand-in for zstd: copies a named file, or stdin when the
|
|
63
|
+
// daemon path pipes into it.
|
|
64
|
+
writeFileSync(join(bin, 'zstd'), '#!/bin/bash\nfor a in "$@"; do case "$a" in -*) ;; *) exec cat "$a";; esac; done\nexec cat\n', 'utf-8');
|
|
65
|
+
chmodSync(join(bin, 'zstd'), 0o755);
|
|
66
|
+
// Create the content, remembering where each piece has to return to.
|
|
67
|
+
const landings = [];
|
|
68
|
+
const operands = [];
|
|
69
|
+
for (const f of shape.files) {
|
|
70
|
+
const [where, rel] = f.at.split(':', 2);
|
|
71
|
+
const base = where === 'home' ? home : checkout;
|
|
72
|
+
const abs = join(base, rel);
|
|
73
|
+
mkdirSync(join(abs, '..'), { recursive: true });
|
|
74
|
+
writeFileSync(abs, f.body, 'utf-8');
|
|
75
|
+
landings.push({ path: abs, body: f.body });
|
|
76
|
+
}
|
|
77
|
+
// The tar operands are what cache-push would have built: `~` paths
|
|
78
|
+
// expanded to absolute, project-relative paths left alone.
|
|
79
|
+
for (const p of shape.cachePaths) {
|
|
80
|
+
operands.push(p.startsWith('~') ? join(home, p.slice(2)) : p);
|
|
81
|
+
}
|
|
82
|
+
// One archive, mixed operands, created from the checkout — exactly what
|
|
83
|
+
// `tar -cf - "${PATHS_TO_CACHE[@]}"` produces.
|
|
84
|
+
const cacheDir = join(checkout, '.ci-cache');
|
|
85
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
86
|
+
const tarball = join(cacheDir, `${CACHE_KEY}.tar.zst`);
|
|
87
|
+
await run('tar', ['-cf', tarball, ...operands], { cwd: checkout });
|
|
88
|
+
// Remove the originals: "it landed" is only meaningful if it was gone.
|
|
89
|
+
for (const l of landings)
|
|
90
|
+
rmSync(l.path, { force: true });
|
|
91
|
+
const { script } = await new CachePullStepExecutor().execute({ cache_key: CACHE_KEY, cache_paths: shape.cachePaths }, {}, testConfigNoPeer);
|
|
92
|
+
const scriptPath = join(root, '__cache_pull.sh');
|
|
93
|
+
writeFileSync(scriptPath, script, 'utf-8');
|
|
94
|
+
const env = {
|
|
95
|
+
...process.env,
|
|
96
|
+
PATH: `${bin}:${process.env.PATH}`,
|
|
97
|
+
CIBUILD_USER_HOME: home,
|
|
98
|
+
};
|
|
99
|
+
// The filesystem transport is the one under test here; the daemon URL
|
|
100
|
+
// would switch the script to the HTTP path.
|
|
101
|
+
delete env.CIBUILD_CACHE_DAEMON;
|
|
102
|
+
let stdout = '';
|
|
103
|
+
let exitCode = 0;
|
|
104
|
+
try {
|
|
105
|
+
const r = await run('bash', [scriptPath], {
|
|
106
|
+
cwd: checkout,
|
|
107
|
+
encoding: 'utf-8',
|
|
108
|
+
env,
|
|
109
|
+
timeout: 20000,
|
|
110
|
+
});
|
|
111
|
+
stdout = String(r.stdout) + String(r.stderr);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
const err = e;
|
|
115
|
+
stdout = String(err.stdout ?? '') + String(err.stderr ?? '');
|
|
116
|
+
exitCode = err.code ?? 1;
|
|
117
|
+
}
|
|
118
|
+
// Read the landings back while the tree is still alive.
|
|
119
|
+
const observed = landings.map((l) => ({
|
|
120
|
+
path: l.path,
|
|
121
|
+
body: existsSync(l.path) ? readFileSync(l.path, 'utf-8') : '',
|
|
122
|
+
}));
|
|
123
|
+
return { stdout, exitCode, landings: observed };
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
rmSync(root, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
describe('cache-pull restores each class of path to its own root', () => {
|
|
130
|
+
test('all `~`-rooted: keeps working, and still streams', async () => {
|
|
131
|
+
// The shape that was already correct. It is here so the fix cannot buy the
|
|
132
|
+
// other two at its expense.
|
|
133
|
+
const { stdout, exitCode, landings } = await restore({
|
|
134
|
+
cachePaths: ['~/.gradle/caches'],
|
|
135
|
+
files: [{ at: 'home:.gradle/caches/modules/marker', body: 'gradle-cache\n' }],
|
|
136
|
+
});
|
|
137
|
+
expect(exitCode).toBe(0);
|
|
138
|
+
expect(stdout).toContain('CACHE_SOURCE=local');
|
|
139
|
+
expect(stdout).not.toContain('CACHE_SOURCE=cold');
|
|
140
|
+
expect(landings).toEqual([
|
|
141
|
+
{ path: landings[0].path, body: 'gradle-cache\n' },
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
test('mixed: reports warm AND the project-relative half lands in the checkout', async () => {
|
|
145
|
+
// Every Gradle/Android pipeline, including the one the README ships. Before
|
|
146
|
+
// the fix: the `~` half restored (that was the real warmth), `.gradle`
|
|
147
|
+
// could not become `/.gradle`, tar exited non-zero, and the step reported
|
|
148
|
+
// CACHE_SOURCE=cold — on the filesystem transport it ended the step outright.
|
|
149
|
+
const { stdout, exitCode, landings } = await restore({
|
|
150
|
+
cachePaths: ['~/.gradle/caches', '.gradle'],
|
|
151
|
+
files: [
|
|
152
|
+
{ at: 'home:.gradle/caches/modules/marker', body: 'gradle-cache\n' },
|
|
153
|
+
{ at: 'checkout:.gradle/configuration-cache/marker', body: 'project-gradle\n' },
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
expect(exitCode).toBe(0);
|
|
157
|
+
expect(stdout).toContain('CACHE_SOURCE=local');
|
|
158
|
+
expect(stdout).not.toContain('CACHE_SOURCE=cold');
|
|
159
|
+
// Both halves, at their own roots.
|
|
160
|
+
expect(landings.map((l) => l.body)).toEqual(['gradle-cache\n', 'project-gradle\n']);
|
|
161
|
+
});
|
|
162
|
+
test('all project-relative: restores into the checkout instead of nothing', async () => {
|
|
163
|
+
// The iOS/Pods and node shape. Before the fix every member was extracted to
|
|
164
|
+
// `/` and every one failed, so these caches were pushed on every build and
|
|
165
|
+
// had never restored anything — a silent miss that costs a `pod install`
|
|
166
|
+
// rather than failing a build, which is why it went unnoticed.
|
|
167
|
+
const { stdout, exitCode, landings } = await restore({
|
|
168
|
+
cachePaths: ['Pods', 'Podfile.lock'],
|
|
169
|
+
files: [
|
|
170
|
+
{ at: 'checkout:Pods/Alamofire/marker', body: 'pods\n' },
|
|
171
|
+
{ at: 'checkout:Podfile.lock', body: 'lock\n' },
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
expect(exitCode).toBe(0);
|
|
175
|
+
expect(stdout).toContain('CACHE_SOURCE=local');
|
|
176
|
+
expect(stdout).not.toContain('CACHE_SOURCE=cold');
|
|
177
|
+
expect(landings.map((l) => l.body)).toEqual(['pods\n', 'lock\n']);
|
|
178
|
+
});
|
|
179
|
+
test('a project-relative path is never written to the filesystem root', async () => {
|
|
180
|
+
// The direct statement of the bug: `Pods` must not be attempted at `/Pods`.
|
|
181
|
+
// Asserted on the generated script rather than by running it, because the
|
|
182
|
+
// only honest runtime check would be whether `/Pods` appeared on the
|
|
183
|
+
// machine running the suite.
|
|
184
|
+
const { script } = await new CachePullStepExecutor().execute({ cache_key: CACHE_KEY, cache_paths: ['~/.gradle/caches', 'Pods'] }, {}, testConfigNoPeer);
|
|
185
|
+
// No restore pipes into `tar -C /` any more; they all go through the
|
|
186
|
+
// helper, which picks the root per class. The helper keeps a single
|
|
187
|
+
// streaming `tar -xf - -C /` for the all-absolute shape, so the assertion
|
|
188
|
+
// is about the call sites, not about the string appearing anywhere.
|
|
189
|
+
expect(script).not.toContain('| tar -xf - -C /');
|
|
190
|
+
expect(script).toContain('| __ci_cache_extract');
|
|
191
|
+
// `Pods` is declared project-relative, so it can only be extracted
|
|
192
|
+
// relative to the checkout ...
|
|
193
|
+
expect(script).toContain('__ci_cache_rel+=(\'Pods\')');
|
|
194
|
+
// ... and the tilde path is expanded at runtime, then matched against the
|
|
195
|
+
// archive's own top-level names.
|
|
196
|
+
expect(script).toContain('__ci_cache_abs+=("${__ci_p#/}")');
|
|
197
|
+
expect(script).toContain('tar -xf "$__t" -C "$__ci_cache_root"');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
//# sourceMappingURL=cache-pull-restore-roots.test.js.map
|
|
@@ -37,8 +37,11 @@ const CACHE_KEY = 'pods-abc123def456-main';
|
|
|
37
37
|
*
|
|
38
38
|
* @param curlExit exit status the stub returns, and `curlBody` is what it
|
|
39
39
|
* prints — so a rejection can be exercised without a daemon.
|
|
40
|
+
* @param zstdExit exit status the `zstd` stub returns, which is how the
|
|
41
|
+
* *middle* stage is made to fail. 127 is what a shell that
|
|
42
|
+
* cannot find the binary produces, i.e. an image without it.
|
|
40
43
|
*/
|
|
41
|
-
async function runPush({ curlExit = 0, curlBody = '' } = {}) {
|
|
44
|
+
async function runPush({ curlExit = 0, curlBody = '', zstdExit = 0, } = {}) {
|
|
42
45
|
const dir = mkdtempSync(join(tmpdir(), 'cibuild-push-'));
|
|
43
46
|
try {
|
|
44
47
|
// Something real to cache, so the step does not take its empty branch.
|
|
@@ -63,7 +66,11 @@ async function runPush({ curlExit = 0, curlBody = '' } = {}) {
|
|
|
63
66
|
chmodSync(join(bin, 'curl'), 0o755);
|
|
64
67
|
// zstd is not on every host, and without it the pipeline fails before
|
|
65
68
|
// curl is ever reached — which would make these tests pass vacuously.
|
|
66
|
-
|
|
69
|
+
// A failing stub drains stdin before exiting, so tar completes normally
|
|
70
|
+
// and the only non-zero status in the pipeline is the one under test.
|
|
71
|
+
writeFileSync(join(bin, 'zstd'), zstdExit === 0
|
|
72
|
+
? '#!/bin/bash\ncat\n'
|
|
73
|
+
: `#!/bin/bash\ncat > /dev/null\nexit ${zstdExit}\n`, 'utf-8');
|
|
67
74
|
chmodSync(join(bin, 'zstd'), 0o755);
|
|
68
75
|
const { script } = await new CachePushStepExecutor().execute({ cache_key: CACHE_KEY, cache_paths: ['Pods'] }, {}, testConfigNoPeer);
|
|
69
76
|
const scriptPath = join(dir, '__cache_push.sh');
|
|
@@ -128,5 +135,28 @@ describe('cache-push with an explicit key, over the daemon', () => {
|
|
|
128
135
|
});
|
|
129
136
|
expect(stdout).toContain('invalid_cache_key');
|
|
130
137
|
});
|
|
138
|
+
test('a failed compressor is not reported as the daemon saying yes', async () => {
|
|
139
|
+
// What a worker image with no zstd binary actually logged:
|
|
140
|
+
//
|
|
141
|
+
// Warning: failed to upload cache to the daemon — {"ok":true}
|
|
142
|
+
//
|
|
143
|
+
// `--fail-with-body` writes the daemon's response body to curl's *stdout*
|
|
144
|
+
// on success as well as on refusal, and the old code captured that as the
|
|
145
|
+
// failure reason. curl is perfectly happy to upload nothing, so the
|
|
146
|
+
// success body is precisely what a broken middle stage produces — the
|
|
147
|
+
// warning blamed the one stage that worked.
|
|
148
|
+
const { stdout } = await runPush({ zstdExit: 127, curlBody: '{"ok":true}' });
|
|
149
|
+
expect(stdout).toContain('Warning: failed to upload cache to the daemon');
|
|
150
|
+
expect(stdout).not.toContain('{"ok":true}');
|
|
151
|
+
expect(stdout).toContain('zstd is not installed');
|
|
152
|
+
});
|
|
153
|
+
test('a middle stage failing for any other reason still names the stage', async () => {
|
|
154
|
+
// 127 is the case that bit us, but the fix is not about 127: the reason is
|
|
155
|
+
// only the daemon's answer when the daemon is what refused.
|
|
156
|
+
const { stdout } = await runPush({ zstdExit: 1, curlBody: '{"ok":true}' });
|
|
157
|
+
expect(stdout).toContain('Warning: failed to upload cache to the daemon');
|
|
158
|
+
expect(stdout).not.toContain('{"ok":true}');
|
|
159
|
+
expect(stdout).toContain('zstd exited 1');
|
|
160
|
+
});
|
|
131
161
|
});
|
|
132
162
|
//# sourceMappingURL=cache-push-daemon.test.js.map
|
|
@@ -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;AAubF;;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;YAmIzF,iBAAiB;CAkNhC;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;YAqKzF,iBAAiB;CAgJhC"}
|