@invarn/cibuild 2.5.8 → 2.6.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.
- 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/base.d.ts.map +1 -1
- package/dist/src/yaml/steps/base.js +11 -4
- 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 +183 -0
- package/dist/src/yaml/steps/cache-push-daemon.test.d.ts +27 -0
- package/dist/src/yaml/steps/cache-push-daemon.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/cache-push-daemon.test.js +132 -0
- package/dist/src/yaml/steps/cache.d.ts.map +1 -1
- package/dist/src/yaml/steps/cache.js +131 -19
- package/dist/src/yaml/steps/envstore-loader.test.d.ts +15 -0
- package/dist/src/yaml/steps/envstore-loader.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/envstore-loader.test.js +143 -0
- package/dist/src/yaml/steps/steps.test.js +20 -9
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A build whose HEAD commit has a multi-line message completes every step.
|
|
3
|
+
*
|
|
4
|
+
* This is the regression that started the whole thread. On 2026-08-30 the first
|
|
5
|
+
* real `invarn setup` proof died of it: `git-clone` publishes the commit body,
|
|
6
|
+
* and every step after it failed loading the envstore —
|
|
7
|
+
*
|
|
8
|
+
* [step_2] Running: cache-pull
|
|
9
|
+
* …: line 59: export: `value nothing provides.': not a valid identifier
|
|
10
|
+
* ✗ Pipeline failed: Step "Install CocoaPods" failed with exit code 1
|
|
11
|
+
*
|
|
12
|
+
* `src/yaml/steps/envstore-loader.test.ts` drives the *reader* with hostile
|
|
13
|
+
* content written by hand, which is the right shape for the defect but stops
|
|
14
|
+
* short of the thing that actually happened: the value came out of a real
|
|
15
|
+
* repository, through a real producer. This closes that gap — a real
|
|
16
|
+
* `git commit`, the real `git-clone` step reading it with `git log -1
|
|
17
|
+
* --format=%b`, the real `envman` writer (`envmanShellFunction`, the same
|
|
18
|
+
* bytes the runner prepends to every script step), and the real reader in the
|
|
19
|
+
* next step's preamble.
|
|
20
|
+
*
|
|
21
|
+
* The fixture's commit message carries both consequences the PRD names, because
|
|
22
|
+
* they need different bytes to surface: a line that is *not* a valid assignment
|
|
23
|
+
* (which killed the step) and a line that *is* one (which let the commit's
|
|
24
|
+
* author set `PATH` for every step after it).
|
|
25
|
+
*/
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=envstore-round-trip.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envstore-round-trip.test.d.ts","sourceRoot":"","sources":["../../../src/envman/envstore-round-trip.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A build whose HEAD commit has a multi-line message completes every step.
|
|
3
|
+
*
|
|
4
|
+
* This is the regression that started the whole thread. On 2026-08-30 the first
|
|
5
|
+
* real `invarn setup` proof died of it: `git-clone` publishes the commit body,
|
|
6
|
+
* and every step after it failed loading the envstore —
|
|
7
|
+
*
|
|
8
|
+
* [step_2] Running: cache-pull
|
|
9
|
+
* …: line 59: export: `value nothing provides.': not a valid identifier
|
|
10
|
+
* ✗ Pipeline failed: Step "Install CocoaPods" failed with exit code 1
|
|
11
|
+
*
|
|
12
|
+
* `src/yaml/steps/envstore-loader.test.ts` drives the *reader* with hostile
|
|
13
|
+
* content written by hand, which is the right shape for the defect but stops
|
|
14
|
+
* short of the thing that actually happened: the value came out of a real
|
|
15
|
+
* repository, through a real producer. This closes that gap — a real
|
|
16
|
+
* `git commit`, the real `git-clone` step reading it with `git log -1
|
|
17
|
+
* --format=%b`, the real `envman` writer (`envmanShellFunction`, the same
|
|
18
|
+
* bytes the runner prepends to every script step), and the real reader in the
|
|
19
|
+
* next step's preamble.
|
|
20
|
+
*
|
|
21
|
+
* The fixture's commit message carries both consequences the PRD names, because
|
|
22
|
+
* they need different bytes to surface: a line that is *not* a valid assignment
|
|
23
|
+
* (which killed the step) and a line that *is* one (which let the commit's
|
|
24
|
+
* author set `PATH` for every step after it).
|
|
25
|
+
*/
|
|
26
|
+
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
|
|
27
|
+
import { execFileSync, spawnSync } from 'child_process';
|
|
28
|
+
import { mkdtempSync, readFileSync, mkdirSync, rmSync, writeFileSync, existsSync } from 'fs';
|
|
29
|
+
import { join } from 'path';
|
|
30
|
+
import { tmpdir } from 'os';
|
|
31
|
+
import { envmanShellFunction } from './shim.js';
|
|
32
|
+
import { GitCloneStepExecutor } from '../yaml/steps/git-clone.js';
|
|
33
|
+
import { ScriptStepExecutor } from '../yaml/steps/script.js';
|
|
34
|
+
import { testConfigNoPeer } from '../yaml/steps/test-config.js';
|
|
35
|
+
const SUBJECT = 'Add Invarn pipeline';
|
|
36
|
+
/**
|
|
37
|
+
* Three lines and a blank one. Line 2 is not a valid assignment; line 4 is.
|
|
38
|
+
* `git log --format=%b` emits a trailing newline that `$( )` strips, so the
|
|
39
|
+
* expected value below carries none.
|
|
40
|
+
*/
|
|
41
|
+
const BODY = [
|
|
42
|
+
'cibuild sets CIBUILD_GIT_BRANCH, so pre-execution validation asked for a',
|
|
43
|
+
'value nothing provides.',
|
|
44
|
+
'',
|
|
45
|
+
'PATH=/tmp/attacker-controlled',
|
|
46
|
+
].join('\n');
|
|
47
|
+
describe('a multi-line commit message, from the repository to the next step', () => {
|
|
48
|
+
let repoDir;
|
|
49
|
+
let envstorePath;
|
|
50
|
+
let captureDir;
|
|
51
|
+
beforeAll(() => {
|
|
52
|
+
repoDir = mkdtempSync(join(tmpdir(), 'envstore-round-trip-'));
|
|
53
|
+
envstorePath = join(repoDir, '.ci', '.envstore.json');
|
|
54
|
+
captureDir = join(repoDir, 'captured');
|
|
55
|
+
mkdirSync(captureDir, { recursive: true });
|
|
56
|
+
writeFileSync(join(repoDir, 'README.md'), 'fixture\n', 'utf-8');
|
|
57
|
+
const git = (...args) => {
|
|
58
|
+
execFileSync('git', args, { cwd: repoDir, stdio: 'ignore' });
|
|
59
|
+
};
|
|
60
|
+
git('init', '-b', 'main');
|
|
61
|
+
git('config', 'user.name', 'CI Build Tests');
|
|
62
|
+
git('config', 'user.email', 'cibuild-tests@example.com');
|
|
63
|
+
git('config', 'commit.gpgsign', 'false');
|
|
64
|
+
git('add', '.');
|
|
65
|
+
git('commit', '-m', `${SUBJECT}\n\n${BODY}`);
|
|
66
|
+
});
|
|
67
|
+
afterAll(() => {
|
|
68
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* Runs one generated step the way the runner does: the envman shell function
|
|
72
|
+
* prepended, bash, the envstore path in the environment so it survives the
|
|
73
|
+
* `cd` the step does.
|
|
74
|
+
*/
|
|
75
|
+
function runStep(script) {
|
|
76
|
+
const scriptPath = join(repoDir, '__step.sh');
|
|
77
|
+
writeFileSync(scriptPath, envmanShellFunction() + script, 'utf-8');
|
|
78
|
+
const proc = spawnSync('bash', [scriptPath], {
|
|
79
|
+
cwd: repoDir,
|
|
80
|
+
encoding: 'utf-8',
|
|
81
|
+
env: {
|
|
82
|
+
...process.env,
|
|
83
|
+
ENVMAN_ENVSTORE_PATH: envstorePath,
|
|
84
|
+
CAPTURE_DIR: captureDir,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return { status: proc.status, stdout: proc.stdout ?? '', stderr: proc.stderr ?? '' };
|
|
88
|
+
}
|
|
89
|
+
function captured(name) {
|
|
90
|
+
const file = join(captureDir, name);
|
|
91
|
+
return existsSync(file) ? readFileSync(file, 'utf-8') : undefined;
|
|
92
|
+
}
|
|
93
|
+
test('git-clone publishes the commit body without dying on it', async () => {
|
|
94
|
+
const { script } = await new GitCloneStepExecutor().execute({}, {}, testConfigNoPeer);
|
|
95
|
+
const run = runStep(script);
|
|
96
|
+
expect(run.status).toBe(0);
|
|
97
|
+
expect(run.stdout).toContain(`Message: ${SUBJECT}`);
|
|
98
|
+
// The envstore is what the next step reads, so it has to exist and to hold
|
|
99
|
+
// the body as data rather than as three more records.
|
|
100
|
+
const store = JSON.parse(readFileSync(envstorePath, 'utf-8'));
|
|
101
|
+
expect(store.envs.find(e => e.key === 'GIT_CLONE_COMMIT_MESSAGE_BODY')?.value).toBe(BODY);
|
|
102
|
+
expect(store.envs.map(e => e.key)).not.toContain('PATH');
|
|
103
|
+
});
|
|
104
|
+
test('the next step loads it, completes, and is not reconfigured by it', async () => {
|
|
105
|
+
// Runs after git-clone above: the envstore on disk is the real one that
|
|
106
|
+
// step wrote.
|
|
107
|
+
const { script } = await new ScriptStepExecutor().execute({
|
|
108
|
+
content: [
|
|
109
|
+
`printf '%s' "\${GIT_CLONE_COMMIT_MESSAGE_BODY-}" > "$CAPTURE_DIR/body"`,
|
|
110
|
+
`printf '%s' "\${GIT_CLONE_COMMIT_MESSAGE_SUBJECT-}" > "$CAPTURE_DIR/subject"`,
|
|
111
|
+
`printf '%s' "$PATH" > "$CAPTURE_DIR/path"`,
|
|
112
|
+
'echo step-completed',
|
|
113
|
+
].join('\n'),
|
|
114
|
+
}, {}, testConfigNoPeer);
|
|
115
|
+
const run = runStep(script);
|
|
116
|
+
// "Completes every step" — the whole acceptance criterion, and what the
|
|
117
|
+
// production build could not do.
|
|
118
|
+
expect(run.status).toBe(0);
|
|
119
|
+
expect(run.stdout).toContain('step-completed');
|
|
120
|
+
expect(run.stderr).not.toContain('not a valid identifier');
|
|
121
|
+
// Correctness: byte for byte, internal blank line and all.
|
|
122
|
+
expect(captured('body')).toBe(BODY);
|
|
123
|
+
expect(captured('subject')).toBe(SUBJECT);
|
|
124
|
+
// Security: the commit's author does not get to choose PATH. Compared
|
|
125
|
+
// against what the parent process held, which is what "unchanged" means.
|
|
126
|
+
expect(captured('path')).toBe(process.env.PATH ?? '');
|
|
127
|
+
expect(captured('path')).not.toContain('/tmp/attacker-controlled');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
//# sourceMappingURL=envstore-round-trip.test.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `envman` shell function prepended to every script step.
|
|
3
|
+
*
|
|
4
|
+
* Steps call `envman add --key K --value V` to publish a value to the steps
|
|
5
|
+
* after them. This is a bash function rather than a call to the `ci` binary
|
|
6
|
+
* because pkg-built binaries mishandle subcommands — so the writer half of the
|
|
7
|
+
* envstore contract is generated bash, exactly like the reader half in
|
|
8
|
+
* `createBashScript`.
|
|
9
|
+
*
|
|
10
|
+
* Lives in its own module so a test can run the *same bytes production runs*
|
|
11
|
+
* against a real producer, rather than a copy that can drift from it. The
|
|
12
|
+
* reader is driven directly in `src/yaml/steps/envstore-loader.test.ts`; the
|
|
13
|
+
* pair is exercised end to end in `src/envman/envstore-round-trip.test.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function envmanShellFunction(): string;
|
|
16
|
+
//# sourceMappingURL=shim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shim.d.ts","sourceRoot":"","sources":["../../../src/envman/shim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAkD5C"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `envman` shell function prepended to every script step.
|
|
3
|
+
*
|
|
4
|
+
* Steps call `envman add --key K --value V` to publish a value to the steps
|
|
5
|
+
* after them. This is a bash function rather than a call to the `ci` binary
|
|
6
|
+
* because pkg-built binaries mishandle subcommands — so the writer half of the
|
|
7
|
+
* envstore contract is generated bash, exactly like the reader half in
|
|
8
|
+
* `createBashScript`.
|
|
9
|
+
*
|
|
10
|
+
* Lives in its own module so a test can run the *same bytes production runs*
|
|
11
|
+
* against a real producer, rather than a copy that can drift from it. The
|
|
12
|
+
* reader is driven directly in `src/yaml/steps/envstore-loader.test.ts`; the
|
|
13
|
+
* pair is exercised end to end in `src/envman/envstore-round-trip.test.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export function envmanShellFunction() {
|
|
16
|
+
return `envman() {
|
|
17
|
+
local cmd="$1"; shift
|
|
18
|
+
local store="\${ENVMAN_ENVSTORE_PATH:-.ci/.envstore.json}"
|
|
19
|
+
case "$cmd" in
|
|
20
|
+
add)
|
|
21
|
+
local key="" value="" valuefile="" sensitive=false skipifempty=false append=false
|
|
22
|
+
while [ $# -gt 0 ]; do
|
|
23
|
+
case "$1" in
|
|
24
|
+
--key|-k) key="$2"; shift 2 ;;
|
|
25
|
+
--value|-v) value="$2"; shift 2 ;;
|
|
26
|
+
--valuefile|-f) valuefile="$2"; shift 2 ;;
|
|
27
|
+
--sensitive|-s) sensitive=true; shift ;;
|
|
28
|
+
--skip-if-empty) skipifempty=true; shift ;;
|
|
29
|
+
--append|-a) append=true; shift ;;
|
|
30
|
+
*) shift ;;
|
|
31
|
+
esac
|
|
32
|
+
done
|
|
33
|
+
[ -z "$key" ] && { echo "envman add: --key required" >&2; return 1; }
|
|
34
|
+
if [ -n "$valuefile" ]; then
|
|
35
|
+
value="$(cat "$valuefile")"
|
|
36
|
+
elif [ -z "$value" ] && ! [ -t 0 ]; then
|
|
37
|
+
value="$(cat)"
|
|
38
|
+
fi
|
|
39
|
+
[ "$skipifempty" = true ] && [ -z "$value" ] && return 0
|
|
40
|
+
node -e '
|
|
41
|
+
const fs=require("fs");
|
|
42
|
+
const f=process.argv[1], k=process.argv[2], v=process.argv[3];
|
|
43
|
+
const s=process.argv[4]==="true", a=process.argv[5]==="true";
|
|
44
|
+
let store={envs:[]};
|
|
45
|
+
try{store=JSON.parse(fs.readFileSync(f,"utf-8"))}catch{}
|
|
46
|
+
const idx=store.envs.findIndex(e=>e.key===k);
|
|
47
|
+
if(a && idx>=0){store.envs[idx].value+=v}
|
|
48
|
+
else if(idx>=0){store.envs[idx].value=v;store.envs[idx].sensitive=s}
|
|
49
|
+
else{store.envs.push({key:k,value:v,sensitive:s})}
|
|
50
|
+
fs.mkdirSync(require("path").dirname(f),{recursive:true});
|
|
51
|
+
fs.writeFileSync(f,JSON.stringify(store,null,2));
|
|
52
|
+
' "$store" "$key" "$value" "$sensitive" "$append"
|
|
53
|
+
echo "envman: $key set"
|
|
54
|
+
;;
|
|
55
|
+
init)
|
|
56
|
+
node -e 'const fs=require("fs");const f=process.argv[1];fs.mkdirSync(require("path").dirname(f),{recursive:true});fs.writeFileSync(f,JSON.stringify({envs:[]},null,2))' "$store"
|
|
57
|
+
;;
|
|
58
|
+
*)
|
|
59
|
+
echo "envman: unknown command: $cmd" >&2; return 1
|
|
60
|
+
;;
|
|
61
|
+
esac
|
|
62
|
+
}
|
|
63
|
+
export -f envman
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=shim.js.map
|
package/dist/src/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAOjE,qBAAa,kBAAmB,SAAQ,KAAK;IAElC,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,MAAM;IAChB,QAAQ,EAAE,MAAM;gBAFhB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;CAK1B;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,QAAQ;IAE9B,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiHhE,OAAO,CAAC,cAAc;IAYtB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAwB7C,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAczD,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA2CpG"}
|
package/dist/src/runner.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { unlinkSync, existsSync, writeFileSync, chmodSync, mkdtempSync, rmSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
+
import { envmanShellFunction } from "./envman/shim.js";
|
|
5
6
|
import { formatStepMarker, shouldEmitStepMarker } from "./step-marker.js";
|
|
6
7
|
// Use absolute path for envstore so it works regardless of cd in scripts
|
|
7
8
|
const ENVMAN_ENVSTORE_PATH = resolve(process.cwd(), ".ci", ".envstore.json");
|
|
@@ -54,56 +55,7 @@ export class PipelineRunner {
|
|
|
54
55
|
tmpScriptPath = join(tmpDir, `step-${step.id || 'script'}.sh`);
|
|
55
56
|
// Prepend envman shell function that writes directly to the envstore JSON file.
|
|
56
57
|
// This avoids calling the ci binary (pkg binaries have issues with subcommands).
|
|
57
|
-
|
|
58
|
-
local cmd="\$1"; shift
|
|
59
|
-
local store="\${ENVMAN_ENVSTORE_PATH:-.ci/.envstore.json}"
|
|
60
|
-
case "\$cmd" in
|
|
61
|
-
add)
|
|
62
|
-
local key="" value="" valuefile="" sensitive=false skipifempty=false append=false
|
|
63
|
-
while [ \$# -gt 0 ]; do
|
|
64
|
-
case "\$1" in
|
|
65
|
-
--key|-k) key="\$2"; shift 2 ;;
|
|
66
|
-
--value|-v) value="\$2"; shift 2 ;;
|
|
67
|
-
--valuefile|-f) valuefile="\$2"; shift 2 ;;
|
|
68
|
-
--sensitive|-s) sensitive=true; shift ;;
|
|
69
|
-
--skip-if-empty) skipifempty=true; shift ;;
|
|
70
|
-
--append|-a) append=true; shift ;;
|
|
71
|
-
*) shift ;;
|
|
72
|
-
esac
|
|
73
|
-
done
|
|
74
|
-
[ -z "\$key" ] && { echo "envman add: --key required" >&2; return 1; }
|
|
75
|
-
if [ -n "\$valuefile" ]; then
|
|
76
|
-
value="\$(cat "\$valuefile")"
|
|
77
|
-
elif [ -z "\$value" ] && ! [ -t 0 ]; then
|
|
78
|
-
value="\$(cat)"
|
|
79
|
-
fi
|
|
80
|
-
[ "\$skipifempty" = true ] && [ -z "\$value" ] && return 0
|
|
81
|
-
node -e '
|
|
82
|
-
const fs=require("fs");
|
|
83
|
-
const f=process.argv[1], k=process.argv[2], v=process.argv[3];
|
|
84
|
-
const s=process.argv[4]==="true", a=process.argv[5]==="true";
|
|
85
|
-
let store={envs:[]};
|
|
86
|
-
try{store=JSON.parse(fs.readFileSync(f,"utf-8"))}catch{}
|
|
87
|
-
const idx=store.envs.findIndex(e=>e.key===k);
|
|
88
|
-
if(a && idx>=0){store.envs[idx].value+=v}
|
|
89
|
-
else if(idx>=0){store.envs[idx].value=v;store.envs[idx].sensitive=s}
|
|
90
|
-
else{store.envs.push({key:k,value:v,sensitive:s})}
|
|
91
|
-
fs.mkdirSync(require("path").dirname(f),{recursive:true});
|
|
92
|
-
fs.writeFileSync(f,JSON.stringify(store,null,2));
|
|
93
|
-
' "\$store" "\$key" "\$value" "\$sensitive" "\$append"
|
|
94
|
-
echo "envman: \$key set"
|
|
95
|
-
;;
|
|
96
|
-
init)
|
|
97
|
-
node -e 'const fs=require("fs");const f=process.argv[1];fs.mkdirSync(require("path").dirname(f),{recursive:true});fs.writeFileSync(f,JSON.stringify({envs:[]},null,2))' "\$store"
|
|
98
|
-
;;
|
|
99
|
-
*)
|
|
100
|
-
echo "envman: unknown command: \$cmd" >&2; return 1
|
|
101
|
-
;;
|
|
102
|
-
esac
|
|
103
|
-
}
|
|
104
|
-
export -f envman
|
|
105
|
-
`;
|
|
106
|
-
writeFileSync(tmpScriptPath, envmanShim + step.script, 'utf-8');
|
|
58
|
+
writeFileSync(tmpScriptPath, envmanShellFunction() + step.script, 'utf-8');
|
|
107
59
|
chmodSync(tmpScriptPath, 0o755);
|
|
108
60
|
args = ["-l", tmpScriptPath];
|
|
109
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/base.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EACV,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,UAAU,EACX,MAAM,wBAAwB,CAAC;AAEhC;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtF;;;;;;;OAOG;IACH,yBAAyB,CAAC,CACxB,MAAM,EAAE,GAAG,EACX,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,MAAM,EAAE,QAAQ,GACf,qBAAqB,EAAE,CAAC;IAE3B;;;;OAIG;IACH,UAAU,CAAC,IAAI,UAAU,EAAE,CAAC;IAE5B;;;OAGG;IACH,oBAAoB,CAAC,IAAI,OAAO,CAAC;CAClC;AAED;;GAEG;AACH,8BAAsB,gBAAiB,YAAW,YAAY;IAC5D;;OAEG;IACH,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAE9F;;;;;OAKG;IACH,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAUjE;;;;;OAKG;IACH,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IASjE;;;;;OAKG;IACH,SAAS,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAS/D;;;;;OAKG;IACH,SAAS,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAS/D;;;;;;OAMG;IACH,SAAS,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,GAAG,CAAC;IAOnE;;;;;;;OAOG;IACH,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG;IAS3E;;;;OAIG;IACH,SAAS,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAKzC;;;;;;OAMG;IACH,SAAS,CAAC,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM;IAgB7E;;;;;;OAMG;IACH,SAAS,CAAC,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/base.ts"],"names":[],"mappings":"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"}
|
|
@@ -133,12 +133,19 @@ set -o pipefail # Catch errors in pipes
|
|
|
133
133
|
|
|
134
134
|
# Step: ${stepName}
|
|
135
135
|
|
|
136
|
-
# Load environment variables from envman store (set by previous steps)
|
|
136
|
+
# Load environment variables from envman store (set by previous steps).
|
|
137
|
+
# Records are terminated by NUL, not by newline. An environment variable may
|
|
138
|
+
# hold a newline -- a commit message body, a PEM key, a JSON blob -- but never
|
|
139
|
+
# a NUL, so no value can be read back as a further record. The key is then
|
|
140
|
+
# checked against the shell's identifier grammar as a second, independent
|
|
141
|
+
# defence: even a malformed entry can neither be exported nor fail the step.
|
|
137
142
|
ENVSTORE_FILE="\${ENVMAN_ENVSTORE_PATH:-.ci/.envstore.json}"
|
|
138
143
|
if [ -f "$ENVSTORE_FILE" ]; then
|
|
139
|
-
while IFS='=' read -r key value; do
|
|
140
|
-
[
|
|
141
|
-
|
|
144
|
+
while IFS='=' read -r -d '' key value; do
|
|
145
|
+
if [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
|
146
|
+
export "$key=$value"
|
|
147
|
+
fi
|
|
148
|
+
done < <(node -e "const fs=require('fs');const NUL=String.fromCharCode(0);try{const s=JSON.parse(fs.readFileSync('$ENVSTORE_FILE','utf-8'));(s.envs||[]).forEach(e=>process.stdout.write(e.key+'='+e.value+NUL))}catch(err){}" 2>/dev/null || true)
|
|
142
149
|
fi
|
|
143
150
|
|
|
144
151
|
${scriptContent}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cache-pull` over the daemon: a miss and a broken daemon are different things.
|
|
3
|
+
*
|
|
4
|
+
* `{ curl | zstd | tar; } 2>/dev/null` swallowed curl's message entirely, so a
|
|
5
|
+
* 401, a 400, a 503, a 500 and a daemon that was not listening all printed the
|
|
6
|
+
* same line as an empty cache:
|
|
7
|
+
*
|
|
8
|
+
* No cache found for key: pods-0fc6c3e8f03403bc5fb869d95e505969-HEAD
|
|
9
|
+
*
|
|
10
|
+
* Same class as the push-side defect in `27e05e5`, where `curl -f` threw the
|
|
11
|
+
* response body away so `invalid_cache_key` never reached the log. A cold cache
|
|
12
|
+
* is the normal case and must stay quiet; the point is only that a broken
|
|
13
|
+
* daemon stops looking like one.
|
|
14
|
+
*
|
|
15
|
+
* These tests RUN the generated script against a **real listener with the real
|
|
16
|
+
* curl**. Stubbing curl onto PATH cannot see this: the whole question is what
|
|
17
|
+
* the real curl reports and where it writes it, so a stub that emits the status
|
|
18
|
+
* itself would pass against a step that never asked for one.
|
|
19
|
+
*
|
|
20
|
+
* `zstd` and `tar` *are* stubbed, for two different reasons. zstd is not on
|
|
21
|
+
* every host, and without it the pipeline fails before curl is reached — which
|
|
22
|
+
* would make these pass vacuously. tar is stubbed because the generated script
|
|
23
|
+
* extracts with `-C /`: nothing under test here is worth writing to the root of
|
|
24
|
+
* the machine running the suite.
|
|
25
|
+
*/
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=cache-pull-daemon.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-pull-daemon.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache-pull-daemon.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cache-pull` over the daemon: a miss and a broken daemon are different things.
|
|
3
|
+
*
|
|
4
|
+
* `{ curl | zstd | tar; } 2>/dev/null` swallowed curl's message entirely, so a
|
|
5
|
+
* 401, a 400, a 503, a 500 and a daemon that was not listening all printed the
|
|
6
|
+
* same line as an empty cache:
|
|
7
|
+
*
|
|
8
|
+
* No cache found for key: pods-0fc6c3e8f03403bc5fb869d95e505969-HEAD
|
|
9
|
+
*
|
|
10
|
+
* Same class as the push-side defect in `27e05e5`, where `curl -f` threw the
|
|
11
|
+
* response body away so `invalid_cache_key` never reached the log. A cold cache
|
|
12
|
+
* is the normal case and must stay quiet; the point is only that a broken
|
|
13
|
+
* daemon stops looking like one.
|
|
14
|
+
*
|
|
15
|
+
* These tests RUN the generated script against a **real listener with the real
|
|
16
|
+
* curl**. Stubbing curl onto PATH cannot see this: the whole question is what
|
|
17
|
+
* the real curl reports and where it writes it, so a stub that emits the status
|
|
18
|
+
* itself would pass against a step that never asked for one.
|
|
19
|
+
*
|
|
20
|
+
* `zstd` and `tar` *are* stubbed, for two different reasons. zstd is not on
|
|
21
|
+
* every host, and without it the pipeline fails before curl is reached — which
|
|
22
|
+
* would make these pass vacuously. tar is stubbed because the generated script
|
|
23
|
+
* extracts with `-C /`: nothing under test here is worth writing to the root of
|
|
24
|
+
* the machine running the suite.
|
|
25
|
+
*/
|
|
26
|
+
import { describe, test, expect } from '@jest/globals';
|
|
27
|
+
import { execFile } from 'child_process';
|
|
28
|
+
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, chmodSync } from 'fs';
|
|
29
|
+
import { createServer } from 'http';
|
|
30
|
+
import { join } from 'path';
|
|
31
|
+
import { tmpdir } from 'os';
|
|
32
|
+
import { promisify } from 'util';
|
|
33
|
+
import { CachePullStepExecutor } from './cache.js';
|
|
34
|
+
import { testConfigNoPeer } from './test-config.js';
|
|
35
|
+
/**
|
|
36
|
+
* Async on purpose. The listener below lives in this process, and
|
|
37
|
+
* `execFileSync` blocks the event loop — so it would never answer curl, every
|
|
38
|
+
* request would time out, and every case would look like a dead daemon
|
|
39
|
+
* whatever the test asked for.
|
|
40
|
+
*/
|
|
41
|
+
const run = promisify(execFile);
|
|
42
|
+
const CACHE_KEY = 'pods-abc123def456-release-1.4';
|
|
43
|
+
async function listen(handler, paths) {
|
|
44
|
+
const server = createServer((req, res) => {
|
|
45
|
+
const path = req.url ?? '/';
|
|
46
|
+
paths.push(path);
|
|
47
|
+
const { status, body } = handler(path);
|
|
48
|
+
res.writeHead(status, { 'content-type': 'application/octet-stream' });
|
|
49
|
+
res.end(body);
|
|
50
|
+
});
|
|
51
|
+
const port = await new Promise((resolve, reject) => {
|
|
52
|
+
server.once('error', reject);
|
|
53
|
+
server.listen(0, '127.0.0.1', () => {
|
|
54
|
+
const address = server.address();
|
|
55
|
+
resolve(typeof address === 'object' && address !== null ? address.port : 0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return { server, port };
|
|
59
|
+
}
|
|
60
|
+
async function runPull(options) {
|
|
61
|
+
const dir = mkdtempSync(join(tmpdir(), 'cibuild-pull-'));
|
|
62
|
+
const paths = [];
|
|
63
|
+
const { server, port } = await listen((path) => {
|
|
64
|
+
if (path.includes('/scope/')) {
|
|
65
|
+
return { status: options.scopeStatus ?? 404, body: '{"error":"no_cache_for_scope"}' };
|
|
66
|
+
}
|
|
67
|
+
const status = options.keyStatus === 'dead' ? 404 : options.keyStatus;
|
|
68
|
+
return {
|
|
69
|
+
status,
|
|
70
|
+
body: status === 200 ? 'a-tarball-shaped-payload' : '{"error":"cache_miss"}',
|
|
71
|
+
};
|
|
72
|
+
}, paths);
|
|
73
|
+
// A dead daemon is a port nothing answers on — the one the listener just
|
|
74
|
+
// released. Anything else and curl would be diagnosing the wrong failure.
|
|
75
|
+
if (options.keyStatus === 'dead') {
|
|
76
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const bin = join(dir, 'bin');
|
|
80
|
+
mkdirSync(bin, { recursive: true });
|
|
81
|
+
writeFileSync(join(bin, 'zstd'), '#!/bin/bash\ncat\n', 'utf-8');
|
|
82
|
+
chmodSync(join(bin, 'zstd'), 0o755);
|
|
83
|
+
// Consumes the stream and writes nothing: see the file header on `-C /`.
|
|
84
|
+
writeFileSync(join(bin, 'tar'), '#!/bin/bash\ncat > /dev/null\n', 'utf-8');
|
|
85
|
+
chmodSync(join(bin, 'tar'), 0o755);
|
|
86
|
+
// The preset form derives its key from a lockfile, so it needs one.
|
|
87
|
+
if (options.technology) {
|
|
88
|
+
writeFileSync(join(dir, 'package-lock.json'), '{"lockfileVersion":3}\n', 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
const { script } = await new CachePullStepExecutor().execute(options.technology
|
|
91
|
+
? { technology: options.technology }
|
|
92
|
+
: { cache_key: CACHE_KEY, cache_paths: ['Pods'] }, {}, testConfigNoPeer);
|
|
93
|
+
const scriptPath = join(dir, '__cache_pull.sh');
|
|
94
|
+
writeFileSync(scriptPath, script, 'utf-8');
|
|
95
|
+
let stdout = '';
|
|
96
|
+
try {
|
|
97
|
+
const result = await run('bash', [scriptPath], {
|
|
98
|
+
cwd: dir,
|
|
99
|
+
encoding: 'utf-8',
|
|
100
|
+
env: {
|
|
101
|
+
...process.env,
|
|
102
|
+
PATH: `${bin}:${process.env.PATH}`,
|
|
103
|
+
CIBUILD_CACHE_DAEMON: `http://127.0.0.1:${port}`,
|
|
104
|
+
CIBUILD_CACHE_TOKEN: 'tok',
|
|
105
|
+
},
|
|
106
|
+
timeout: 20000,
|
|
107
|
+
});
|
|
108
|
+
stdout = String(result.stdout) + String(result.stderr);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
const err = e;
|
|
112
|
+
stdout = String(err.stdout ?? '') + String(err.stderr ?? '');
|
|
113
|
+
}
|
|
114
|
+
return { stdout, paths };
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
if (options.keyStatus !== 'dead') {
|
|
118
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
119
|
+
}
|
|
120
|
+
rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
describe('cache-pull with an explicit key, over the daemon', () => {
|
|
124
|
+
test('an empty cache is reported, and nothing else', async () => {
|
|
125
|
+
// 404 is the daemon's answer for `cache_miss`. The normal case on any
|
|
126
|
+
// first build: it must not start printing warnings.
|
|
127
|
+
const { stdout, paths } = await runPull({ keyStatus: 404 });
|
|
128
|
+
expect(paths).toEqual([`/cache/${CACHE_KEY}.tar.zst`]);
|
|
129
|
+
expect(stdout).toContain(`No cache found for key: ${CACHE_KEY}`);
|
|
130
|
+
expect(stdout).toContain('CACHE_SOURCE=cold');
|
|
131
|
+
expect(stdout).not.toContain('Warning');
|
|
132
|
+
});
|
|
133
|
+
test('an auth failure no longer looks like an empty cache', async () => {
|
|
134
|
+
const { stdout } = await runPull({ keyStatus: 401 });
|
|
135
|
+
expect(stdout).toContain('Warning: cache-pull could not restore');
|
|
136
|
+
expect(stdout).toContain('401');
|
|
137
|
+
// Still not fatal, and still cold: a build that cannot read a cache builds
|
|
138
|
+
// anyway. Only the silence is gone.
|
|
139
|
+
expect(stdout).toContain('CACHE_SOURCE=cold');
|
|
140
|
+
});
|
|
141
|
+
test.each([
|
|
142
|
+
['a rejected key', 400],
|
|
143
|
+
['a busy cache', 503],
|
|
144
|
+
['a daemon that broke', 500],
|
|
145
|
+
])('%s says so, with the status', async (_label, status) => {
|
|
146
|
+
const { stdout } = await runPull({ keyStatus: status });
|
|
147
|
+
expect(stdout).toContain('Warning: cache-pull could not restore');
|
|
148
|
+
expect(stdout).toContain(String(status));
|
|
149
|
+
});
|
|
150
|
+
test('a daemon that is not listening says so, rather than reporting a miss', async () => {
|
|
151
|
+
const { stdout } = await runPull({ keyStatus: 'dead' });
|
|
152
|
+
expect(stdout).toContain('Warning: cache-pull could not restore');
|
|
153
|
+
// curl's own words for it. There is no HTTP status to report — which is
|
|
154
|
+
// itself the distinction from every case above.
|
|
155
|
+
expect(stdout).toMatch(/Failed to connect|Couldn't connect|Connection refused/u);
|
|
156
|
+
});
|
|
157
|
+
test('a hit is still a hit', async () => {
|
|
158
|
+
const { stdout } = await runPull({ keyStatus: 200 });
|
|
159
|
+
expect(stdout).toContain('Cache found (daemon), extracting...');
|
|
160
|
+
expect(stdout).toContain('CACHE_SOURCE=daemon');
|
|
161
|
+
expect(stdout).not.toContain('Warning');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('cache-pull from a preset, over the daemon', () => {
|
|
165
|
+
test('an empty cache is quiet through the scope fallback too', async () => {
|
|
166
|
+
const { stdout, paths } = await runPull({ keyStatus: 404, technology: 'npm' });
|
|
167
|
+
// Exact key first, then the scope's newest — the shape the fallback needs.
|
|
168
|
+
expect(paths).toHaveLength(2);
|
|
169
|
+
expect(paths[1]).toContain('/cache/scope/');
|
|
170
|
+
expect(stdout).toContain('No cache found for key: npm-');
|
|
171
|
+
expect(stdout).not.toContain('Warning');
|
|
172
|
+
});
|
|
173
|
+
test('a broken daemon is named on the preset path as well', async () => {
|
|
174
|
+
// The two forms emit the same diagnosis from one definition. They used to
|
|
175
|
+
// carry byte-identical copies of the upload block, and that is exactly how
|
|
176
|
+
// the push-side defect got in.
|
|
177
|
+
const { stdout } = await runPull({ keyStatus: 401, technology: 'npm' });
|
|
178
|
+
expect(stdout).toContain('Warning: cache-pull could not restore');
|
|
179
|
+
expect(stdout).toContain('401');
|
|
180
|
+
expect(stdout).toContain('CACHE_SOURCE=cold');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
//# sourceMappingURL=cache-pull-daemon.test.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cache-push` with an explicit `cache_key`, over the daemon.
|
|
3
|
+
*
|
|
4
|
+
* Every curated Invarn template uses the explicit-key form — `cache_key:
|
|
5
|
+
* pods-{{checksum "Podfile.lock"}}-$CIBUILD_GIT_BRANCH` and its Gradle/KMM
|
|
6
|
+
* siblings — for both halves of the pair. The pull side assigns the shell
|
|
7
|
+
* variable the daemon URL interpolates; the push side did not, and there is
|
|
8
|
+
* no `set -u`, so `$CACHE_KEY` expanded to nothing and the upload went to
|
|
9
|
+
* `<daemon>/cache/.tar.zst`. The daemon requires at least one character
|
|
10
|
+
* before `.tar.zst` and answers 400, which is exactly what the first real
|
|
11
|
+
* `invarn setup` proof logged on 2026-08-30:
|
|
12
|
+
*
|
|
13
|
+
* curl: (22) The requested URL returned error: 400
|
|
14
|
+
* Warning: failed to upload cache to the daemon
|
|
15
|
+
* [step_6] ✓ Success (109ms)
|
|
16
|
+
*
|
|
17
|
+
* Nothing was ever stored, so the pull could never hit, and every iOS build
|
|
18
|
+
* paid a cold `pod install`.
|
|
19
|
+
*
|
|
20
|
+
* These tests RUN the generated script. A containment check on the script
|
|
21
|
+
* text cannot see this defect: the text contains the literal `$CACHE_KEY`
|
|
22
|
+
* either way, which is why the existing assertion passed throughout. `curl`
|
|
23
|
+
* and `zstd` are stubbed onto PATH so the pipeline completes on any host and
|
|
24
|
+
* the URL actually requested can be read back.
|
|
25
|
+
*/
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=cache-push-daemon.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-push-daemon.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache-push-daemon.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG"}
|