@invarn/cibuild 2.7.2 → 2.7.3

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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A credential the pipeline declares and nobody supplied fails the build,
3
+ * naming itself, instead of being skipped with a warning.
4
+ *
5
+ * `file@1.0.0` carries `is_skippable: true` in every pipeline the Android
6
+ * generator emits, so an absent value printed `⚠️ X not set — skipping <file>`
7
+ * and returned success. The build then failed several steps later inside the
8
+ * project's own build tool, with an error naming the project rather than the
9
+ * value we dropped. Both failure modes in this defect were green, which is why
10
+ * it survived.
11
+ *
12
+ * The two absences are not the same absence, and the map the step already
13
+ * receives tells them apart by presence:
14
+ *
15
+ * - a `var_name` the pipeline **declares** and leaves empty is a key of the
16
+ * map with an empty value — the value was supposed to arrive as a secret
17
+ * and did not. After issue 01 that means it is in none of the four places
18
+ * a value can come from, so the diagnosis is not a guess.
19
+ * - a `var_name` **nothing declares** is not a key of the map at all — a
20
+ * release keystore on a debug workflow. Skipping is correct there.
21
+ *
22
+ * One case sits between them, and it decides the rule (issue 03 §2.3, Alex,
23
+ * 2026-09-08): the generator declares `KEYSTORE_BASE64` globally but emits its
24
+ * `file@` step in all three workflows, so a project that ran `ci init` with a
25
+ * keystore and never uploaded one would go from three green skips to three
26
+ * hard failures — including on `pull-request`, where nothing signs anything.
27
+ * So the question the step asks is the one that actually matters: **will the
28
+ * file be there when the build reads it?** A repository that carries the file
29
+ * keeps warning and building; only an absence with nothing behind it fails.
30
+ */
31
+ export {};
32
+ //# sourceMappingURL=a-missing-credential-is-loud.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a-missing-credential-is-loud.test.d.ts","sourceRoot":"","sources":["../../../src/yaml/a-missing-credential-is-loud.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG"}
@@ -0,0 +1,301 @@
1
+ /**
2
+ * A credential the pipeline declares and nobody supplied fails the build,
3
+ * naming itself, instead of being skipped with a warning.
4
+ *
5
+ * `file@1.0.0` carries `is_skippable: true` in every pipeline the Android
6
+ * generator emits, so an absent value printed `⚠️ X not set — skipping <file>`
7
+ * and returned success. The build then failed several steps later inside the
8
+ * project's own build tool, with an error naming the project rather than the
9
+ * value we dropped. Both failure modes in this defect were green, which is why
10
+ * it survived.
11
+ *
12
+ * The two absences are not the same absence, and the map the step already
13
+ * receives tells them apart by presence:
14
+ *
15
+ * - a `var_name` the pipeline **declares** and leaves empty is a key of the
16
+ * map with an empty value — the value was supposed to arrive as a secret
17
+ * and did not. After issue 01 that means it is in none of the four places
18
+ * a value can come from, so the diagnosis is not a guess.
19
+ * - a `var_name` **nothing declares** is not a key of the map at all — a
20
+ * release keystore on a debug workflow. Skipping is correct there.
21
+ *
22
+ * One case sits between them, and it decides the rule (issue 03 §2.3, Alex,
23
+ * 2026-09-08): the generator declares `KEYSTORE_BASE64` globally but emits its
24
+ * `file@` step in all three workflows, so a project that ran `ci init` with a
25
+ * keystore and never uploaded one would go from three green skips to three
26
+ * hard failures — including on `pull-request`, where nothing signs anything.
27
+ * So the question the step asks is the one that actually matters: **will the
28
+ * file be there when the build reads it?** A repository that carries the file
29
+ * keeps warning and building; only an absence with nothing behind it fails.
30
+ */
31
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
32
+ import { spawnSync } from 'node:child_process';
33
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync, } from 'node:fs';
34
+ import { tmpdir } from 'node:os';
35
+ import { dirname, join } from 'node:path';
36
+ import { convertYAMLToPipelineDef } from './converter.js';
37
+ import { StepValidator } from './step-validator.js';
38
+ import { clearRegistry, registerStep } from './steps/registry.js';
39
+ import { FileStepExecutor } from './steps/file.js';
40
+ const mockConfig = {
41
+ artifactsDir: '/test/artifacts',
42
+ maxConcurrentJobs: 1,
43
+ paths: {
44
+ buildsDir: '/test/builds',
45
+ cacheDir: '/test/cache',
46
+ derivedDataDir: '/test/derived-data',
47
+ },
48
+ interpreters: {
49
+ bash: '/bin/bash',
50
+ python: 'python3',
51
+ ruby: 'ruby',
52
+ node: 'node',
53
+ },
54
+ };
55
+ /** The 61-byte document from the PRD's reproduction. Synthetic. */
56
+ const JSON_CREDENTIAL = '{"project_info":{"project_id":"synthetic-probe"},"client":[]}';
57
+ const TARGET = 'app/google-services.json';
58
+ let workdir;
59
+ let previousCwd;
60
+ beforeEach(() => {
61
+ clearRegistry();
62
+ registerStep('file', new FileStepExecutor());
63
+ previousCwd = process.cwd();
64
+ workdir = mkdtempSync(join(tmpdir(), 'cibuild-missing-credential-'));
65
+ process.chdir(workdir);
66
+ // `workdir` is the pre-symlink path on macOS; the step resolves paths from
67
+ // CIBUILD_SOURCE_DIR, which is cwd, so read and write files the same way.
68
+ workdir = process.cwd();
69
+ delete process.env.GOOGLE_SERVICES_JSON;
70
+ delete process.env.KEYSTORE_BASE64;
71
+ delete process.env.OTHER_SECRET;
72
+ });
73
+ afterEach(() => {
74
+ process.chdir(previousCwd);
75
+ rmSync(workdir, { recursive: true, force: true });
76
+ clearRegistry();
77
+ delete process.env.GOOGLE_SERVICES_JSON;
78
+ delete process.env.KEYSTORE_BASE64;
79
+ delete process.env.OTHER_SECRET;
80
+ });
81
+ /**
82
+ * The shape the Android generator emits: empty slots under `app.envs` and one
83
+ * skippable `file@1.0.0` step per credential, in declaration order.
84
+ */
85
+ function probePipeline(declared, fileSteps, options = {}) {
86
+ const skippable = options.skippable ?? true;
87
+ const app = declared
88
+ ? { app: { envs: Object.entries(declared).map(([k, v]) => ({ [k]: v })) } }
89
+ : {};
90
+ return {
91
+ format_version: '1',
92
+ ...app,
93
+ workflows: {
94
+ probe: {
95
+ steps: fileSteps.map((inputs) => ({
96
+ 'file@1.0.0': {
97
+ title: `Setup ${String(inputs.var_name)}`,
98
+ is_skippable: skippable,
99
+ inputs,
100
+ },
101
+ })),
102
+ },
103
+ },
104
+ };
105
+ }
106
+ function writeSecretStore(global) {
107
+ writeFileSync(join(workdir, '.cibuild-secrets.json'), JSON.stringify({ global, workflows: {} }), { mode: 0o600 });
108
+ }
109
+ /** Puts a file in the checkout, the way a repository that carries one does. */
110
+ function commitToCheckout(relPath, contents) {
111
+ const target = join(workdir, relPath);
112
+ mkdirSync(dirname(target), { recursive: true });
113
+ writeFileSync(target, contents, 'utf-8');
114
+ }
115
+ /**
116
+ * Converts the pipeline and runs the generated steps the way the runner does,
117
+ * then reads back whatever landed at `target`.
118
+ */
119
+ async function land(pipeline, target = TARGET) {
120
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'probe');
121
+ const chunks = [...result.warnings];
122
+ for (const step of result.pipeline.steps) {
123
+ const scriptPath = join(workdir, `__step_${step.id}.sh`);
124
+ writeFileSync(scriptPath, step.script, 'utf-8');
125
+ const proc = spawnSync('bash', [scriptPath], {
126
+ cwd: workdir,
127
+ encoding: 'utf-8',
128
+ env: { ...process.env },
129
+ });
130
+ expect(proc.status).toBe(0);
131
+ chunks.push(proc.stdout ?? '', proc.stderr ?? '');
132
+ }
133
+ const landedAt = join(workdir, target);
134
+ return {
135
+ output: chunks.join('\n'),
136
+ bytes: existsSync(landedAt) ? readFileSync(landedAt) : undefined,
137
+ };
138
+ }
139
+ /** The conversion's outcome, without letting a throw escape the assertion. */
140
+ async function convert(pipeline) {
141
+ try {
142
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'probe');
143
+ return { steps: result.pipeline.steps };
144
+ }
145
+ catch (error) {
146
+ return { error: error };
147
+ }
148
+ }
149
+ describe('a declared credential nothing supplied', () => {
150
+ test('fails, naming the key and the target path', async () => {
151
+ const { error, steps } = await convert(probePipeline({ GOOGLE_SERVICES_JSON: '' }, [
152
+ { target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' },
153
+ ]));
154
+ expect(error).toBeDefined();
155
+ expect(error.message).toContain('GOOGLE_SERVICES_JSON');
156
+ expect(error.message).toContain(TARGET);
157
+ // Not a stack trace and not a step that runs: conversion is what builds
158
+ // the step list, so nothing at all is handed to the runner.
159
+ expect(steps).toBeUndefined();
160
+ });
161
+ test('says both places the value can come from', async () => {
162
+ const { error } = await convert(probePipeline({ GOOGLE_SERVICES_JSON: '' }, [
163
+ { target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' },
164
+ ]));
165
+ // A user reading this should not have to know that a slot, an upload and
166
+ // a `file@` step are three separate things.
167
+ expect(error.message).toContain('ci secrets add GOOGLE_SERVICES_JSON');
168
+ expect(error.message).toMatch(/organization or pipeline secret/);
169
+ });
170
+ test('is_skippable does not turn it into a pass', async () => {
171
+ // Every `file@` step the Android generator emits carries this, which is
172
+ // the whole reason the defect was invisible.
173
+ const skippable = await convert(probePipeline({ GOOGLE_SERVICES_JSON: '' }, [{ target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' }], { skippable: true }));
174
+ const notSkippable = await convert(probePipeline({ GOOGLE_SERVICES_JSON: '' }, [{ target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' }], { skippable: false }));
175
+ expect(skippable.error).toBeDefined();
176
+ expect(notSkippable.error).toBeDefined();
177
+ });
178
+ test('fails on the first unfilled slot, not the last and not all three', async () => {
179
+ // What a real Android pipeline carries: three slots, none filled.
180
+ const { error } = await convert(probePipeline({
181
+ KEYSTORE_BASE64: '',
182
+ KEYSTORE_PROPERTIES: '',
183
+ GOOGLE_SERVICES_JSON: '',
184
+ }, [
185
+ {
186
+ target_path: 'release.keystore',
187
+ var_name: 'KEYSTORE_BASE64',
188
+ base64_encoded: true,
189
+ },
190
+ { target_path: 'keystore.properties', var_name: 'KEYSTORE_PROPERTIES' },
191
+ { target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' },
192
+ ]));
193
+ expect(error).toBeDefined();
194
+ expect(error.message).toContain('KEYSTORE_BASE64');
195
+ expect(error.message).not.toContain('KEYSTORE_PROPERTIES');
196
+ expect(error.message).not.toContain('GOOGLE_SERVICES_JSON');
197
+ });
198
+ test('no secret value appears in the message', async () => {
199
+ // The failing slot has no value by definition, so the assertion that
200
+ // means anything is about the values the build *does* hold.
201
+ process.env.OTHER_SECRET = JSON_CREDENTIAL;
202
+ const { error } = await convert(probePipeline({ OTHER_SECRET: '', GOOGLE_SERVICES_JSON: '' }, [
203
+ { target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' },
204
+ ]));
205
+ expect(error).toBeDefined();
206
+ expect(error.message).not.toContain(JSON_CREDENTIAL);
207
+ expect(error.message).not.toContain('synthetic-probe');
208
+ });
209
+ });
210
+ describe('a credential the repository already carries', () => {
211
+ test('warns and proceeds — the file will be there when the build reads it', async () => {
212
+ // The `pull-request` collision: the generator declares KEYSTORE_BASE64
213
+ // globally and emits its step on every workflow, so a project whose
214
+ // keystore is in the repository must not be refused a build over a value
215
+ // it never needed.
216
+ commitToCheckout('release.keystore', 'a keystore the repository carries');
217
+ const { output } = await land(probePipeline({ KEYSTORE_BASE64: '' }, [
218
+ {
219
+ target_path: 'release.keystore',
220
+ var_name: 'KEYSTORE_BASE64',
221
+ base64_encoded: true,
222
+ },
223
+ ]), 'release.keystore');
224
+ expect(output).toContain('KEYSTORE_BASE64');
225
+ expect(output).toContain('release.keystore');
226
+ // The escape hatch has to be visible in the log, or a build that passes
227
+ // this way is unexplainable to whoever reads it later.
228
+ expect(output).toMatch(/already in the checkout/);
229
+ });
230
+ test('leaves the file it found exactly as it was', async () => {
231
+ const carried = 'the repository\'s own google-services.json';
232
+ commitToCheckout(TARGET, carried);
233
+ const { bytes } = await land(probePipeline({ GOOGLE_SERVICES_JSON: '' }, [
234
+ { target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' },
235
+ ]));
236
+ expect(bytes?.toString('utf-8')).toBe(carried);
237
+ });
238
+ test('an uploaded value still wins over the one in the checkout', async () => {
239
+ // A repository carrying a placeholder is the ordinary case for
240
+ // google-services.json, and the point of the upload is to replace it.
241
+ commitToCheckout(TARGET, 'placeholder committed for local development');
242
+ process.env.GOOGLE_SERVICES_JSON = JSON_CREDENTIAL;
243
+ const { output, bytes } = await land(probePipeline({ GOOGLE_SERVICES_JSON: '' }, [
244
+ { target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' },
245
+ ]));
246
+ expect(output).toContain('✓ Written google-services.json');
247
+ expect(bytes?.toString('utf-8')).toBe(JSON_CREDENTIAL);
248
+ });
249
+ });
250
+ describe('what issue 03 must not change', () => {
251
+ test('an undeclared key still warns and passes, is_skippable and all', async () => {
252
+ // Issue 01's class-A case, and the one this change is most likely to
253
+ // break: HOME exists in every environment this runs in, and nothing
254
+ // declares it.
255
+ expect(process.env.HOME).toBeTruthy();
256
+ const { output, bytes } = await land(probePipeline({ GOOGLE_SERVICES_JSON: '' }, [
257
+ { target_path: 'home.txt', var_name: 'HOME' },
258
+ ]), 'home.txt');
259
+ expect(output).toContain('HOME not set — skipping home.txt');
260
+ expect(bytes).toBeUndefined();
261
+ });
262
+ test('a declared value from the local store still writes', async () => {
263
+ writeSecretStore({ GOOGLE_SERVICES_JSON: JSON_CREDENTIAL });
264
+ const { output, bytes } = await land(probePipeline({ GOOGLE_SERVICES_JSON: '' }, [
265
+ { target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' },
266
+ ]));
267
+ expect(output).toContain('✓ Written google-services.json');
268
+ expect(bytes?.toString('utf-8')).toBe(JSON_CREDENTIAL);
269
+ });
270
+ test('a declared value from the dispatched environment still writes', async () => {
271
+ process.env.GOOGLE_SERVICES_JSON = JSON_CREDENTIAL;
272
+ const { output, bytes } = await land(probePipeline({ GOOGLE_SERVICES_JSON: '' }, [
273
+ { target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' },
274
+ ]));
275
+ expect(output).toContain('✓ Written google-services.json');
276
+ expect(bytes?.toString('utf-8')).toBe(JSON_CREDENTIAL);
277
+ });
278
+ test('a value the YAML itself supplies still writes', async () => {
279
+ const { output, bytes } = await land(probePipeline({ GOOGLE_SERVICES_JSON: JSON_CREDENTIAL }, [
280
+ { target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' },
281
+ ]));
282
+ expect(output).toContain('✓ Written google-services.json');
283
+ expect(bytes?.toString('utf-8')).toBe(JSON_CREDENTIAL);
284
+ });
285
+ test('ci validate still passes on a pipeline with empty slots', async () => {
286
+ // Option 3 was rejected so validation does not start refusing builds
287
+ // that are green today. This is the line that holds it.
288
+ const pipeline = probePipeline({ KEYSTORE_BASE64: '', GOOGLE_SERVICES_JSON: '' }, [
289
+ {
290
+ target_path: 'release.keystore',
291
+ var_name: 'KEYSTORE_BASE64',
292
+ base64_encoded: true,
293
+ },
294
+ { target_path: TARGET, var_name: 'GOOGLE_SERVICES_JSON' },
295
+ ]);
296
+ const validator = new StepValidator(pipeline, 'probe', mockConfig);
297
+ const result = await validator.validateWorkflow();
298
+ expect(result.valid).toBe(true);
299
+ });
300
+ });
301
+ //# sourceMappingURL=a-missing-credential-is-loud.test.js.map
@@ -1,5 +1,20 @@
1
1
  import { BaseStepExecutor } from './base.js';
2
2
  import type { StepDef, CIConfig } from '../../types.js';
3
+ /**
4
+ * A credential the pipeline declares, the build reads, and nobody supplied.
5
+ *
6
+ * Thrown at generation time, from the step that would have written the file,
7
+ * so the build stops before its own build tool fails for a reason that names
8
+ * the project instead of the value we dropped. Generation time is also the
9
+ * only place `is_skippable` cannot swallow it: the converter sets that flag on
10
+ * a StepDef *after* `execute()` returns, so a step which merely exited
11
+ * non-zero would be reported as "failed but marked skippable — continuing".
12
+ */
13
+ export declare class MissingFileSecretError extends Error {
14
+ readonly varName: string;
15
+ readonly targetPath: string;
16
+ constructor(varName: string, targetPath: string);
17
+ }
3
18
  export interface FileInputs {
4
19
  /** Path where the file should be written, relative to the project root. */
5
20
  target_path: string;
@@ -23,5 +38,15 @@ export interface FileInputs {
23
38
  */
24
39
  export declare class FileStepExecutor extends BaseStepExecutor {
25
40
  execute(inputs: FileInputs, env: Record<string, string>, _config: CIConfig): Promise<StepDef>;
41
+ /**
42
+ * Whether the build will find the file at `target_path` without this step.
43
+ *
44
+ * Asked at generation time, which is when the step decides, and answerable
45
+ * because generation happens inside the checkout — on the host path locally
46
+ * and on the sandbox path inside the guest. The path is composed exactly the
47
+ * way the generated script composes the one it writes to, so the two cannot
48
+ * disagree about which file is in question.
49
+ */
50
+ private fileAlreadyInCheckout;
26
51
  }
27
52
  //# sourceMappingURL=file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/file.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAExD,MAAM,WAAW,UAAU;IACzB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,qBAAa,gBAAiB,SAAQ,gBAAgB;IAC9C,OAAO,CACX,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CA4BpB"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/file.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAExD;;;;;;;;;GASG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;CAyBhD;AAED,MAAM,WAAW,UAAU;IACzB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,qBAAa,gBAAiB,SAAQ,gBAAgB;IAC9C,OAAO,CACX,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;IAiDnB;;;;;;;;OAQG;IACH,OAAO,CAAC,qBAAqB;CAI9B"}
@@ -1,5 +1,43 @@
1
- import { basename } from 'node:path';
1
+ import { existsSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
2
3
  import { BaseStepExecutor } from './base.js';
4
+ /**
5
+ * A credential the pipeline declares, the build reads, and nobody supplied.
6
+ *
7
+ * Thrown at generation time, from the step that would have written the file,
8
+ * so the build stops before its own build tool fails for a reason that names
9
+ * the project instead of the value we dropped. Generation time is also the
10
+ * only place `is_skippable` cannot swallow it: the converter sets that flag on
11
+ * a StepDef *after* `execute()` returns, so a step which merely exited
12
+ * non-zero would be reported as "failed but marked skippable — continuing".
13
+ */
14
+ export class MissingFileSecretError extends Error {
15
+ varName;
16
+ targetPath;
17
+ constructor(varName, targetPath) {
18
+ super(`file@1.0.0 cannot write ${targetPath}: ${varName} is declared in this ` +
19
+ `pipeline but no value reached the build.\n` +
20
+ `\n` +
21
+ `A slot declared and left empty means the value arrives as a secret. ` +
22
+ `None did:\n` +
23
+ `not the pipeline itself, not .cibuild-secrets.json, and not this ` +
24
+ `build's environment.\n` +
25
+ `\n` +
26
+ `Supply it in one of two places:\n` +
27
+ `\n` +
28
+ ` locally ci secrets add ${varName}\n` +
29
+ ` remote build add ${varName} as an organization or pipeline ` +
30
+ `secret, then\n` +
31
+ ` run the build again\n` +
32
+ `\n` +
33
+ `If ${basename(targetPath)} belongs to the repository instead, commit ` +
34
+ `it at\n` +
35
+ `${targetPath} — a file already in the checkout is used as it is.`);
36
+ this.name = 'MissingFileSecretError';
37
+ this.varName = varName;
38
+ this.targetPath = targetPath;
39
+ }
40
+ }
3
41
  /**
4
42
  * Writes a file to a target path at runtime.
5
43
  *
@@ -16,7 +54,29 @@ export class FileStepExecutor extends BaseStepExecutor {
16
54
  const filename = basename(targetPath);
17
55
  const content = env[varName];
18
56
  if (!content) {
19
- return this.createScriptStep(`echo "⚠️ ${varName} not set — skipping ${filename}"`, 'file');
57
+ // Two absences, and they mean opposite things. A `var_name` nothing
58
+ // declares is a genuinely optional credential — a release keystore on a
59
+ // debug workflow — and skipping it is correct. A `var_name` the pipeline
60
+ // declares and leaves empty is a value that was supposed to arrive as a
61
+ // secret and did not: after issue 01 it is in none of the four places a
62
+ // value can come from, so the build is guaranteed red later, for a
63
+ // reason nothing in the log connects to this step. `mergeEnvVars` puts a
64
+ // declared key in the map even when its value is the empty placeholder,
65
+ // and nothing else puts a key there, so presence is the whole test.
66
+ //
67
+ // Except when the repository already carries the file. The Android
68
+ // generator declares KEYSTORE_BASE64 once, globally, but emits its
69
+ // `file@` step on every workflow including `pull-request`, where nothing
70
+ // signs anything — so "declared and empty" alone would refuse builds
71
+ // that are green today and rightly so. The question worth asking is the
72
+ // one the build will ask: will the file be there when it is read?
73
+ if (varName in env && !this.fileAlreadyInCheckout(targetPath, env)) {
74
+ throw new MissingFileSecretError(varName, targetPath);
75
+ }
76
+ const reason = varName in env
77
+ ? `${filename} is already in the checkout, using it`
78
+ : `skipping ${filename}`;
79
+ return this.createScriptStep(`echo "⚠️ ${varName} not set — ${reason}"`, 'file');
20
80
  }
21
81
  // Base64-encode the content so it can be embedded safely in the script
22
82
  // regardless of quotes, newlines, dollar signs, or any other special characters.
@@ -31,5 +91,18 @@ printf '%s' '${encoded}' | base64 -d > "\${CIBUILD_SOURCE_DIR:-$PWD}/${targetPat
31
91
  echo "✓ Written ${filename}"`;
32
92
  return this.createScriptStep(this.createBashScript(scriptContent, 'file'), 'file');
33
93
  }
94
+ /**
95
+ * Whether the build will find the file at `target_path` without this step.
96
+ *
97
+ * Asked at generation time, which is when the step decides, and answerable
98
+ * because generation happens inside the checkout — on the host path locally
99
+ * and on the sandbox path inside the guest. The path is composed exactly the
100
+ * way the generated script composes the one it writes to, so the two cannot
101
+ * disagree about which file is in question.
102
+ */
103
+ fileAlreadyInCheckout(targetPath, env) {
104
+ const sourceDir = env.CIBUILD_SOURCE_DIR || process.cwd();
105
+ return existsSync(join(sourceDir, targetPath));
106
+ }
34
107
  }
35
108
  //# sourceMappingURL=file.js.map
@@ -146,6 +146,12 @@ describe('the steps that embed it emit syntactically valid bash', () => {
146
146
  const r = await new XcodeBuildStepExecutor().execute({ project_path: 'MyApp.xcodeproj', scheme: 'MyApp', is_clean_build: true }, {}, testConfig);
147
147
  await parses(r.script);
148
148
  });
149
+ test('xcode-build-for-simulator', async () => {
150
+ const { XcodeBuildForSimulatorStepExecutor } = await import('./xcode.js');
151
+ const { testConfig } = await import('./test-config.js');
152
+ const r = await new XcodeBuildForSimulatorStepExecutor().execute({ project_path: "My App.xcworkspace", scheme: "My Scheme", perform_clean_action: 'yes' }, {}, testConfig);
153
+ await parses(r.script);
154
+ });
149
155
  test('cocoapods-install', async () => {
150
156
  const { CocoapodsInstallStepExecutor } = await import('./ios-deps.js');
151
157
  const { testConfig } = await import('./test-config.js');
@@ -6,7 +6,7 @@
6
6
  * expensive one. These assert the split actually lands in the emitted script.
7
7
  */
8
8
  import { describe, test, expect } from '@jest/globals';
9
- import { XcodeBuildStepExecutor, XcodeBuildForTestStepExecutor } from './xcode.js';
9
+ import { XcodeBuildStepExecutor, XcodeBuildForTestStepExecutor, XcodeBuildForSimulatorStepExecutor, } from './xcode.js';
10
10
  import { CocoapodsInstallStepExecutor } from './ios-deps.js';
11
11
  import { testConfig } from './test-config.js';
12
12
  const countOccurrences = (haystack, needle) => haystack.split(needle).length - 1;
@@ -83,6 +83,41 @@ describe('xcodebuild resolves before it builds', () => {
83
83
  expect(await build({ is_clean_build: true })).toContain('xcodebuild clean build');
84
84
  });
85
85
  });
86
+ describe('xcode-build-for-simulator resolves before it builds', () => {
87
+ // This is the step the iOS template's "Build App" is, and it is the first
88
+ // step in that pipeline that resolves a package graph — so it is the one a
89
+ // refused fetch actually lands on. It was missed on the first pass.
90
+ const build = async (inputs = {}) => {
91
+ const executor = new XcodeBuildForSimulatorStepExecutor();
92
+ const result = await executor.execute({ project_path: 'MyApp.xcodeproj', scheme: 'MyApp', ...inputs }, {}, testConfig);
93
+ return result.script;
94
+ };
95
+ test('resolves package dependencies before building', async () => {
96
+ const script = await build();
97
+ expect(script).toContain('xcodebuild -resolvePackageDependencies');
98
+ expect(script.indexOf('xcodebuild -resolvePackageDependencies')).toBeLessThan(script.indexOf('xcodebuild build '));
99
+ });
100
+ test('wraps the resolve in the refused-fetch retry', async () => {
101
+ const script = await build();
102
+ expect(script).toContain('cibuild_fetch_was_refused');
103
+ expect(script).toContain('CIBUILD_FETCH_MAX_ATTEMPTS');
104
+ });
105
+ test('leaves the build invocation itself unwrapped', async () => {
106
+ expect(countOccurrences(await build(), 'CIBUILD_FETCH_MAX_ATTEMPTS=')).toBe(1);
107
+ });
108
+ test('passes the same project type and scheme as the build', async () => {
109
+ const resolveLine = (await build())
110
+ .split('\n')
111
+ .find((l) => l.includes('-resolvePackageDependencies'));
112
+ expect(resolveLine).toContain('"$PROJECT_TYPE"');
113
+ expect(resolveLine).toContain("-scheme 'MyApp'");
114
+ });
115
+ test('still honours a clean action and still finds the .app', async () => {
116
+ const script = await build({ perform_clean_action: 'yes' });
117
+ expect(script).toContain('xcodebuild clean build');
118
+ expect(script).toContain('CIBUILD_APP_DIR_PATH');
119
+ });
120
+ });
86
121
  describe('cocoapods-install retries a refused pod source fetch', () => {
87
122
  const build = async (inputs = {}) => {
88
123
  const executor = new CocoapodsInstallStepExecutor();
@@ -1 +1 @@
1
- {"version":3,"file":"xcode.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"xcode.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode.ts"],"names":[],"mappings":"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"}
@@ -1011,6 +1011,19 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
1011
1011
  commands.push(' PROJECT_TYPE="-project"');
1012
1012
  commands.push('fi');
1013
1013
  commands.push('');
1014
+ // Resolve the package graph before building, under the refused-fetch
1015
+ // retry. This is the step an iOS app is normally built with, so it is the
1016
+ // one a refused dependency fetch lands on first. No -derivedDataPath: the
1017
+ // build below does not set one either, so both use the default location
1018
+ // and the build finds the graph this just resolved.
1019
+ commands.push('# Resolve package dependencies first — see git-fetch-retry');
1020
+ commands.push(...refusedFetchHelpers());
1021
+ commands.push(...retryOnRefusedFetch({
1022
+ label: 'Package resolution',
1023
+ command: 'xcodebuild -resolvePackageDependencies' +
1024
+ ` "$PROJECT_TYPE" '${escapedPath}'` +
1025
+ ` -scheme '${escapedScheme}'`,
1026
+ }));
1014
1027
  // Build command
1015
1028
  let cmd = 'xcodebuild';
1016
1029
  if (performClean === 'yes') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.7.2",
3
+ "version": "2.7.3",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",