@invarn/cibuild 2.5.8 → 2.5.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":"base.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/base.ts"],"names":[],"mappings":"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"}
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
- [ -n "$key" ] && export "$key=$value"
141
- done < <(node -e "const fs=require('fs');try{const s=JSON.parse(fs.readFileSync('$ENVSTORE_FILE','utf-8'));(s.envs||[]).forEach(e=>console.log(e.key+'='+e.value))}catch(err){}" 2>/dev/null || true)
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-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"}
@@ -0,0 +1,132 @@
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
+ import { describe, test, expect } from '@jest/globals';
27
+ import { execFileSync } from 'child_process';
28
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, chmodSync } from 'fs';
29
+ import { join } from 'path';
30
+ import { tmpdir } from 'os';
31
+ import { CachePushStepExecutor } from './cache.js';
32
+ import { testConfigNoPeer } from './test-config.js';
33
+ const CACHE_KEY = 'pods-abc123def456-main';
34
+ /**
35
+ * Generates the explicit-key push step, runs it, and reports what `curl` was
36
+ * actually asked to do.
37
+ *
38
+ * @param curlExit exit status the stub returns, and `curlBody` is what it
39
+ * prints — so a rejection can be exercised without a daemon.
40
+ */
41
+ async function runPush({ curlExit = 0, curlBody = '' } = {}) {
42
+ const dir = mkdtempSync(join(tmpdir(), 'cibuild-push-'));
43
+ try {
44
+ // Something real to cache, so the step does not take its empty branch.
45
+ const payload = join(dir, 'Pods');
46
+ mkdirSync(payload, { recursive: true });
47
+ writeFileSync(join(payload, 'thing.txt'), 'contents\n', 'utf-8');
48
+ const bin = join(dir, 'bin');
49
+ mkdirSync(bin, { recursive: true });
50
+ const urlLog = join(dir, 'curl-urls.txt');
51
+ // Records the URL, drains stdin so the upstream pipe does not break.
52
+ writeFileSync(join(bin, 'curl'), [
53
+ '#!/bin/bash',
54
+ 'for arg in "$@"; do',
55
+ ' case "$arg" in',
56
+ ` http*) printf '%s\\n' "$arg" >> "${urlLog}" ;;`,
57
+ ' esac',
58
+ 'done',
59
+ 'cat > /dev/null',
60
+ curlBody ? `printf '%s' ${JSON.stringify(curlBody)}` : ':',
61
+ `exit ${curlExit}`,
62
+ ].join('\n'), 'utf-8');
63
+ chmodSync(join(bin, 'curl'), 0o755);
64
+ // zstd is not on every host, and without it the pipeline fails before
65
+ // curl is ever reached — which would make these tests pass vacuously.
66
+ writeFileSync(join(bin, 'zstd'), '#!/bin/bash\ncat\n', 'utf-8');
67
+ chmodSync(join(bin, 'zstd'), 0o755);
68
+ const { script } = await new CachePushStepExecutor().execute({ cache_key: CACHE_KEY, cache_paths: ['Pods'] }, {}, testConfigNoPeer);
69
+ const scriptPath = join(dir, '__cache_push.sh');
70
+ writeFileSync(scriptPath, script, 'utf-8');
71
+ let stdout = '';
72
+ try {
73
+ stdout = execFileSync('bash', [scriptPath], {
74
+ cwd: dir,
75
+ encoding: 'utf-8',
76
+ stdio: ['ignore', 'pipe', 'pipe'],
77
+ env: {
78
+ ...process.env,
79
+ PATH: `${bin}:${process.env.PATH}`,
80
+ CIBUILD_CACHE_DAEMON: 'http://daemon.invalid',
81
+ CIBUILD_CACHE_TOKEN: 'tok',
82
+ },
83
+ timeout: 20000,
84
+ });
85
+ }
86
+ catch (e) {
87
+ const err = e;
88
+ stdout = String(err.stdout ?? '') + String(err.stderr ?? '');
89
+ }
90
+ const urls = existsSync(urlLog)
91
+ ? readFileSync(urlLog, 'utf-8').split('\n').filter(Boolean)
92
+ : [];
93
+ return { stdout, urls };
94
+ }
95
+ finally {
96
+ rmSync(dir, { recursive: true, force: true });
97
+ }
98
+ }
99
+ describe('cache-push with an explicit key, over the daemon', () => {
100
+ test('uploads under the key it was given', async () => {
101
+ const { urls } = await runPush();
102
+ expect(urls).toHaveLength(1);
103
+ expect(urls[0]).toBe(`http://daemon.invalid/cache/${CACHE_KEY}.tar.zst`);
104
+ });
105
+ test('never uploads to an empty key', async () => {
106
+ // The defect, named. `<daemon>/cache/.tar.zst` is what an unassigned
107
+ // `$CACHE_KEY` produces, and what the daemon answers 400 to.
108
+ const { urls } = await runPush();
109
+ for (const url of urls) {
110
+ expect(url).not.toMatch(/\/cache\/\.tar\.zst$/);
111
+ }
112
+ });
113
+ test('the step survives a rejection rather than failing the build', async () => {
114
+ // Deliberate: a build that produced good output should not be failed by a
115
+ // cache upload. Whether the STEP should still report success having
116
+ // stored nothing is a separate, open question.
117
+ const { stdout } = await runPush({ curlExit: 22 });
118
+ expect(stdout).toContain('Warning: failed to upload cache to the daemon');
119
+ });
120
+ test('a rejection says why, not just that it happened', async () => {
121
+ // `curl -f` throws the response body away, so the daemon's own reason —
122
+ // `invalid_cache_key`, `cache_busy`, an auth failure — never reached the
123
+ // log. All the reader got was an exit code, which is how a one-line bug
124
+ // stayed open behind a warning that named nothing.
125
+ const { stdout } = await runPush({
126
+ curlExit: 22,
127
+ curlBody: '{"error":"invalid_cache_key"}',
128
+ });
129
+ expect(stdout).toContain('invalid_cache_key');
130
+ });
131
+ });
132
+ //# 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;AA6HF;;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;YA0HzF,iBAAiB;CA0MhC;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;YAsLzF,iBAAiB;CA0JhC"}
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;AAgLF;;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;YA0HzF,iBAAiB;CA0MhC;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;YAkLzF,iBAAiB;CAgJhC"}
@@ -94,6 +94,53 @@ function resolvePresetChain(technology) {
94
94
  function daemonCurl(args) {
95
95
  return `curl -fsS \${CIBUILD_CACHE_TOKEN:+-H "Authorization: Bearer $CIBUILD_CACHE_TOKEN"} ${args}`;
96
96
  }
97
+ /**
98
+ * The same request, keeping the response body when the daemon refuses.
99
+ *
100
+ * `-f` throws the body away, so a rejection reached the log as an exit code
101
+ * and nothing else. The daemon distinguishes `invalid_cache_key` (400), a
102
+ * retryable `cache_busy` (503) and an auth failure (401); all three arrived
103
+ * as the same `curl: (22)` line under the same warning, which is how a
104
+ * one-line bug stayed open behind it. Push only — on the pull side the body
105
+ * would be piped into `zstd`.
106
+ *
107
+ * `--fail-with-body` is curl 7.76+ (2021); every runner image is well past
108
+ * it. An older curl rejects the option and the step still degrades to
109
+ * today's behaviour, with the reason in the message.
110
+ */
111
+ function daemonCurlKeepingErrorBody(args) {
112
+ return `curl --fail-with-body -sS \${CIBUILD_CACHE_TOKEN:+-H "Authorization: Bearer $CIBUILD_CACHE_TOKEN"} ${args}`;
113
+ }
114
+ /**
115
+ * The daemon half of `cache-push`, shared by both step forms.
116
+ *
117
+ * Shared because it did not used to be. The two forms carried byte-identical
118
+ * copies of this block, and the explicit-key one reached it without ever
119
+ * assigning the `CACHE_KEY` it interpolates — the preset form assigned it,
120
+ * the other only assigned `CACHE_FILE`. There is no `set -u`, so the PUT went
121
+ * to `<daemon>/cache/.tar.zst`, the daemon answered 400, and the step
122
+ * reported success. Every curated Invarn template uses the explicit-key form,
123
+ * so nothing was ever stored by any of them. One definition cannot diverge
124
+ * from itself.
125
+ */
126
+ function emitDaemonPushCommands() {
127
+ return [
128
+ ` if __ci_push_err=$(tar -cf - "\${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 | ${daemonCurlKeepingErrorBody('-T - "$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst"')} 2>&1); then`,
129
+ ' echo "Cache created successfully"',
130
+ ' else',
131
+ // Never fatal: a build that produced good output should not be failed by
132
+ // a cache upload, and the next build simply misses. Saying *why* costs
133
+ // nothing and is the difference between a warning and a diagnosis.
134
+ " __ci_push_msg=$(printf '%s' \"$__ci_push_err\" | tr '\\n' ' ')",
135
+ ' __ci_push_msg=${__ci_push_msg:0:300}',
136
+ ' if [ -n "$__ci_push_msg" ]; then',
137
+ ' echo "Warning: failed to upload cache to the daemon — $__ci_push_msg"',
138
+ ' else',
139
+ ' echo "Warning: failed to upload cache to the daemon"',
140
+ ' fi',
141
+ ' fi',
142
+ ];
143
+ }
97
144
  /**
98
145
  * Restores a cache through the daemon rather than a shared directory.
99
146
  *
@@ -491,7 +538,11 @@ export class CachePushStepExecutor extends BaseStepExecutor {
491
538
  // Generate cache file name based on cache key
492
539
  commands.push('');
493
540
  commands.push('# Generate cache file path');
494
- commands.push(`CACHE_FILE="$CACHE_DIR/${this.escapeBash(cacheKey)}.tar.zst"`);
541
+ // CACHE_KEY first, CACHE_FILE derived from it — exactly as the pull side
542
+ // does. This step used to set only CACHE_FILE while the daemon URL below
543
+ // interpolated `$CACHE_KEY`; see emitDaemonPushCommands.
544
+ commands.push(`CACHE_KEY="${this.escapeBash(cacheKey)}"`);
545
+ commands.push('CACHE_FILE="$CACHE_DIR/$CACHE_KEY.tar.zst"');
495
546
  if (isDebugMode) {
496
547
  commands.push('echo "Debug mode enabled"');
497
548
  commands.push('echo "Cache file: $CACHE_FILE"');
@@ -561,11 +612,7 @@ export class CachePushStepExecutor extends BaseStepExecutor {
561
612
  }
562
613
  // Transport split — see the preset path.
563
614
  commands.push(' if [ -n "$CIBUILD_CACHE_DAEMON" ]; then');
564
- commands.push(` if tar -cf - "\${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 | ${daemonCurl('-T - "$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst"')} > /dev/null; then`);
565
- commands.push(' echo "Cache created successfully"');
566
- commands.push(' else');
567
- commands.push(' echo "Warning: failed to upload cache to the daemon"');
568
- commands.push(' fi');
615
+ commands.push(...emitDaemonPushCommands());
569
616
  commands.push(' else');
570
617
  // Atomic write into a per-process-unique temp (CBR-12). The "runs serialized
571
618
  // per runner" assumption behind a fixed "$CACHE_FILE.tmp" is false with two VM
@@ -692,13 +739,7 @@ export class CachePushStepExecutor extends BaseStepExecutor {
692
739
  // so there is no temp file to collide or orphan, and the daemon does the
693
740
  // atomic temp+rename on the host side where the cache actually lives.
694
741
  commands.push(' if [ -n "$CIBUILD_CACHE_DAEMON" ]; then');
695
- commands.push(` if tar -cf - "\${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 | ${daemonCurl('-T - "$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst"')} > /dev/null; then`);
696
- commands.push(' echo "Cache created successfully"');
697
- commands.push(' else');
698
- // Never fatal: a build that produced good output should not be failed by a
699
- // cache upload, and the next build simply misses.
700
- commands.push(' echo "Warning: failed to upload cache to the daemon"');
701
- commands.push(' fi');
742
+ commands.push(...emitDaemonPushCommands());
702
743
  commands.push(' else');
703
744
  // Per-process-unique temp (CBR-12). The old fixed "$CACHE_FILE.tmp" assumed
704
745
  // a single serialized writer — false with two VM slots, and it collides with
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The envstore loader carried by every generated step.
3
+ *
4
+ * These tests drive the *reader* — the preamble `createBashScript` prepends —
5
+ * directly, with hostile envstore content written by hand. Going through a
6
+ * producer such as git-clone would only prove the one value that producer
7
+ * happens to write; the defect lives in the reader, so the reader is what gets
8
+ * the hostile input.
9
+ *
10
+ * Each case actually runs bash. Assertions are made on files the script body
11
+ * writes with `printf '%s'`, not on stdout, so a value's own newlines and
12
+ * quoting cannot be confused with the test's framing.
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=envstore-loader.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envstore-loader.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/envstore-loader.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The envstore loader carried by every generated step.
3
+ *
4
+ * These tests drive the *reader* — the preamble `createBashScript` prepends —
5
+ * directly, with hostile envstore content written by hand. Going through a
6
+ * producer such as git-clone would only prove the one value that producer
7
+ * happens to write; the defect lives in the reader, so the reader is what gets
8
+ * the hostile input.
9
+ *
10
+ * Each case actually runs bash. Assertions are made on files the script body
11
+ * writes with `printf '%s'`, not on stdout, so a value's own newlines and
12
+ * quoting cannot be confused with the test's framing.
13
+ */
14
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
15
+ import { spawnSync } from 'child_process';
16
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'fs';
17
+ import { join } from 'path';
18
+ import { tmpdir } from 'os';
19
+ import { BaseStepExecutor } from './base.js';
20
+ /** Exposes the protected preamble builder so the reader can be tested on its own. */
21
+ class PreambleProbe extends BaseStepExecutor {
22
+ async execute(_inputs, _env, _config) {
23
+ throw new Error('not used');
24
+ }
25
+ build(scriptContent) {
26
+ return this.createBashScript(scriptContent, 'envstore-loader-probe');
27
+ }
28
+ }
29
+ describe('envstore loader', () => {
30
+ let workDir;
31
+ beforeEach(() => {
32
+ workDir = mkdtempSync(join(tmpdir(), 'envstore-loader-'));
33
+ });
34
+ afterEach(() => {
35
+ rmSync(workDir, { recursive: true, force: true });
36
+ });
37
+ /**
38
+ * Writes an envstore, runs the generated preamble over it, and captures the
39
+ * named variables as the step body sees them.
40
+ */
41
+ function run(envs, capture, extraBody = '') {
42
+ const storePath = join(workDir, 'envstore.json');
43
+ const outDir = join(workDir, 'captured');
44
+ mkdirSync(outDir, { recursive: true });
45
+ writeFileSync(storePath, JSON.stringify({ envs: envs.map(e => ({ ...e, sensitive: false })) }), 'utf-8');
46
+ const body = [
47
+ ...capture.map(name => `printf '%s' "\${${name}-}" > "$CAPTURE_DIR/${name}"`),
48
+ extraBody,
49
+ ].join('\n');
50
+ const scriptPath = join(workDir, 'step.sh');
51
+ writeFileSync(scriptPath, new PreambleProbe().build(body), 'utf-8');
52
+ const proc = spawnSync('bash', [scriptPath], {
53
+ cwd: workDir,
54
+ encoding: 'utf-8',
55
+ env: {
56
+ ...process.env,
57
+ ENVMAN_ENVSTORE_PATH: storePath,
58
+ CAPTURE_DIR: outDir,
59
+ },
60
+ });
61
+ return {
62
+ status: proc.status,
63
+ stderr: proc.stderr ?? '',
64
+ captured(name) {
65
+ const file = join(outDir, name);
66
+ return existsSync(file) ? readFileSync(file, 'utf-8') : undefined;
67
+ },
68
+ };
69
+ }
70
+ test('a value containing newlines round-trips byte for byte', () => {
71
+ const body = 'first line\nsecond line\nthird line';
72
+ const result = run([{ key: 'GIT_CLONE_COMMIT_MESSAGE_BODY', value: body }], ['GIT_CLONE_COMMIT_MESSAGE_BODY']);
73
+ expect(result.status).toBe(0);
74
+ expect(result.captured('GIT_CLONE_COMMIT_MESSAGE_BODY')).toBe(body);
75
+ });
76
+ test('security: a value whose lines look like assignments sets nothing', () => {
77
+ const hostile = 'looks harmless\nPATH=/tmp/attacker-controlled\nNODE_OPTIONS=--require=/tmp/evil.js';
78
+ const result = run([{ key: 'GIT_CLONE_COMMIT_MESSAGE_BODY', value: hostile }], ['GIT_CLONE_COMMIT_MESSAGE_BODY', 'PATH', 'NODE_OPTIONS']);
79
+ expect(result.status).toBe(0);
80
+ // The value arrives intact...
81
+ expect(result.captured('GIT_CLONE_COMMIT_MESSAGE_BODY')).toBe(hostile);
82
+ // ...and none of its lines became a variable. Both are compared against
83
+ // what the parent environment held, which is what "unchanged" means here:
84
+ // the test runner itself sets NODE_OPTIONS.
85
+ expect(result.captured('PATH')).toBe(process.env.PATH ?? '');
86
+ expect(result.captured('NODE_OPTIONS')).toBe(process.env.NODE_OPTIONS ?? '');
87
+ expect(result.captured('NODE_OPTIONS')).not.toContain('/tmp/evil.js');
88
+ });
89
+ test('awkward bytes in a value all survive unchanged', () => {
90
+ const cases = [
91
+ { key: 'HAS_EQUALS', value: 'a=b=c' },
92
+ { key: 'HAS_QUOTES', value: `single ' double " both '"` },
93
+ { key: 'HAS_BACKTICKS', value: 'a `whoami` b' },
94
+ { key: 'HAS_SUBSHELL', value: 'a $(whoami) b ${HOME} c' },
95
+ { key: 'HAS_TRAILING_NEWLINE', value: 'ends with a newline\n' },
96
+ { key: 'HAS_LONE_CR', value: 'before\rafter' },
97
+ { key: 'HAS_BACKSLASHES', value: 'C:\\path\\to\\thing and \\n literal' },
98
+ { key: 'HAS_LEADING_SPACE', value: ' padded ' },
99
+ ];
100
+ const result = run(cases, cases.map(c => c.key));
101
+ expect(result.status).toBe(0);
102
+ for (const { key, value } of cases) {
103
+ expect({ key, value: result.captured(key) }).toEqual({ key, value });
104
+ }
105
+ });
106
+ test('a non-identifier key is skipped without failing the step under set -e', () => {
107
+ const result = run([
108
+ { key: 'not a valid identifier', value: 'x' },
109
+ { key: '2_LEADING_DIGIT', value: 'x' },
110
+ { key: '', value: 'x' },
111
+ { key: 'GOOD_KEY', value: 'kept' },
112
+ ], ['GOOD_KEY'], 'echo reached-the-end');
113
+ expect(result.status).toBe(0);
114
+ expect(result.captured('GOOD_KEY')).toBe('kept');
115
+ expect(result.stderr).not.toContain('not a valid identifier');
116
+ });
117
+ test('regression: a multi-line commit message does not end the step', () => {
118
+ // The shape that killed the first real proof on 2026-08-30: git-clone
119
+ // publishes the commit body, and every later step died loading it.
120
+ const result = run([
121
+ { key: 'GIT_CLONE_COMMIT_MESSAGE_SUBJECT', value: 'Add Invarn pipeline' },
122
+ {
123
+ key: 'GIT_CLONE_COMMIT_MESSAGE_BODY',
124
+ value: 'cibuild sets CIBUILD_GIT_BRANCH, so pre-execution validation asked for a\nvalue nothing provides.\n\nSecond paragraph.',
125
+ },
126
+ ], ['GIT_CLONE_COMMIT_MESSAGE_SUBJECT'], 'echo step-completed');
127
+ expect(result.status).toBe(0);
128
+ expect(result.captured('GIT_CLONE_COMMIT_MESSAGE_SUBJECT')).toBe('Add Invarn pipeline');
129
+ expect(result.stderr).not.toContain('not a valid identifier');
130
+ });
131
+ test('an absent envstore leaves the step to run normally', () => {
132
+ const scriptPath = join(workDir, 'no-store.sh');
133
+ writeFileSync(scriptPath, new PreambleProbe().build('echo ran'), 'utf-8');
134
+ const proc = spawnSync('bash', [scriptPath], {
135
+ cwd: workDir,
136
+ encoding: 'utf-8',
137
+ env: { ...process.env, ENVMAN_ENVSTORE_PATH: join(workDir, 'missing.json') },
138
+ });
139
+ expect(proc.status).toBe(0);
140
+ expect(proc.stdout).toContain('ran');
141
+ });
142
+ });
143
+ //# sourceMappingURL=envstore-loader.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.5.8",
3
+ "version": "2.5.9",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",