@invarn/cibuild 2.7.3 → 2.7.5
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/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts +38 -0
- package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts.map +1 -0
- package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.js +219 -0
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts +41 -0
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts.map +1 -0
- package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.js +291 -0
- package/dist/src/commands/build.d.ts +144 -0
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +317 -22
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts +26 -0
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts.map +1 -0
- package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.js +196 -0
- package/dist/src/commands/file-secret-collector.d.ts +13 -0
- package/dist/src/commands/file-secret-collector.d.ts.map +1 -1
- package/dist/src/commands/file-secret-collector.js +3 -2
- package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts +30 -0
- package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts.map +1 -0
- package/dist/src/commands/local-properties-reaches-every-workflow.test.js +146 -0
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts +28 -0
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts.map +1 -0
- package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.js +223 -0
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts +37 -0
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts.map +1 -0
- package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.js +255 -0
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts +34 -0
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts.map +1 -0
- package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.js +218 -0
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts +27 -0
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts.map +1 -0
- package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.js +126 -0
- package/dist/src/yaml/converter.d.ts +48 -0
- package/dist/src/yaml/converter.d.ts.map +1 -1
- package/dist/src/yaml/converter.js +117 -0
- package/dist/src/yaml/env-resolver.d.ts +52 -0
- package/dist/src/yaml/env-resolver.d.ts.map +1 -1
- package/dist/src/yaml/env-resolver.js +99 -1
- package/dist/src/yaml/secrets-manager.d.ts +13 -3
- package/dist/src/yaml/secrets-manager.d.ts.map +1 -1
- package/dist/src/yaml/secrets-manager.js +25 -14
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts +17 -0
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.js +132 -0
- package/dist/src/yaml/steps/index.d.ts.map +1 -1
- package/dist/src/yaml/steps/index.js +9 -0
- package/dist/src/yaml/steps/ios-deps.d.ts +35 -0
- package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -1
- package/dist/src/yaml/steps/ios-deps.js +18 -3
- package/dist/src/yaml/steps/xcode-derived-data.test.d.ts +30 -0
- package/dist/src/yaml/steps/xcode-derived-data.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-derived-data.test.js +103 -0
- package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
- package/dist/src/yaml/steps/xcode.js +73 -20
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env-resolver.d.ts","sourceRoot":"","sources":["../../../src/yaml/env-resolver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAc,QAAQ,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"env-resolver.d.ts","sourceRoot":"","sources":["../../../src/yaml/env-resolver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAc,QAAQ,EAAE,MAAM,YAAY,CAAC;AAkEnF,qBAAa,+BAAgC,SAAQ,KAAK;aAEtC,YAAY,EAAE,MAAM;aACpB,QAAQ,CAAC,EAAE,MAAM;aACjB,IAAI,CAAC,EAAE,MAAM;gBAFb,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,YAAA,EACjB,IAAI,CAAC,EAAE,MAAM,YAAA;CAUhC;AAED;;;;;;;GAOG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;aAE/B,QAAQ,EAAE,MAAM;aAChB,QAAQ,CAAC,EAAE,MAAM;gBADjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,YAAA;CAKpC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE;CAgChF;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,cAAc,CAAiB;IACvC;;;OAGG;IACH,OAAO,CAAC,YAAY,CAA0B;IAC9C,oFAAoF;IACpF,OAAO,CAAC,YAAY,CAAS;IAC7B,oFAAoF;IACpF,OAAO,CAAC,iBAAiB,CAAW;gBAGlC,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,QAAQ,EAClB,KAAK,CAAC,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM;IAoCvB;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKrC;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B;;;;OAIG;IACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMtC;;;OAGG;IACH,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAQhC;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM;IAqCnD;;;;;OAKG;IACH,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,GAAG;IAoBnD;;;OAGG;IACH,sBAAsB,IAAI,IAAI;IAQ9B;;;;;;OAMG;IACH,OAAO,CAAC,UAAU;IAalB;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IA0BvB;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IA4FtB;;;OAGG;IACH;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,UAAU;IAiBlB,OAAO,CAAC,YAAY;IAsBpB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,OAAO,CAAC,4BAA4B;IAUpC;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAuB9B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;CAS5B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,QAAQ,EAClB,KAAK,CAAC,EAAE,MAAM,GACb,WAAW,CAOb"}
|
|
@@ -8,6 +8,7 @@ import * as fs from 'fs';
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import { execSync } from 'child_process';
|
|
10
10
|
import { SecretsManager } from './secrets-manager.js';
|
|
11
|
+
import { decodeSecretName } from '../commands/secrets-upload.js';
|
|
11
12
|
import { shellAssignedNamesIn } from './shell-assignments.js';
|
|
12
13
|
/**
|
|
13
14
|
* Hints for common variables that require user configuration
|
|
@@ -95,6 +96,58 @@ export class ChecksumFileMissingError extends Error {
|
|
|
95
96
|
this.name = 'ChecksumFileMissingError';
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* A credential slot scoped to a workflow the pipeline does not have.
|
|
101
|
+
*
|
|
102
|
+
* Thrown at generation time, before any step converts, because every way of
|
|
103
|
+
* getting the name wrong is otherwise silent. A slot naming a workflow nobody
|
|
104
|
+
* runs declares the credential for nobody: `file@1.0.0` finds no key in its
|
|
105
|
+
* map, calls the credential optional and skips it with a warning, and
|
|
106
|
+
* `ci secrets upload` cheerfully pushes a value under the same wrong name.
|
|
107
|
+
* Two green halves and a build that dies later inside the project's own build
|
|
108
|
+
* tool.
|
|
109
|
+
*
|
|
110
|
+
* The underscore case gets its own sentence because the workflow is right
|
|
111
|
+
* there in the pipeline. `encodeSecretName` upper-cases the workflow and turns
|
|
112
|
+
* dashes into underscores, so decoding turns every underscore back into a
|
|
113
|
+
* dash: a workflow genuinely called `my_wf` can only ever be addressed as
|
|
114
|
+
* `my-wf`, and "no such workflow" about a workflow that plainly exists reads
|
|
115
|
+
* as a defect in us rather than a name that cannot survive the round trip.
|
|
116
|
+
*/
|
|
117
|
+
export class UnknownWorkflowSlotError extends Error {
|
|
118
|
+
slot;
|
|
119
|
+
varName;
|
|
120
|
+
workflow;
|
|
121
|
+
constructor(slot, varName, workflow, declared) {
|
|
122
|
+
// A workflow the pipeline has that differs only in its underscores is the
|
|
123
|
+
// reason the round trip failed, and naming it is the whole fix.
|
|
124
|
+
const collapsed = declared.find((name) => name.replace(/_/g, '-') === workflow);
|
|
125
|
+
const because = collapsed
|
|
126
|
+
? `This pipeline has a workflow called "${collapsed}". The name a slot ` +
|
|
127
|
+
`carries cannot hold an underscore: it is upper-cased and its dashes ` +
|
|
128
|
+
`become underscores, so decoding turns every underscore back into a ` +
|
|
129
|
+
`dash and "${collapsed}" can only be addressed as "${workflow}".\n` +
|
|
130
|
+
`\n` +
|
|
131
|
+
`Rename the workflow to "${workflow}", or give ${varName} one value ` +
|
|
132
|
+
`for every workflow by declaring it as "${varName}".`
|
|
133
|
+
: `This pipeline declares: ${declared.join(', ')}.\n` +
|
|
134
|
+
`\n` +
|
|
135
|
+
`Fix the workflow name in the slot, or give ${varName} one value for ` +
|
|
136
|
+
`every workflow by declaring it as "${varName}".`;
|
|
137
|
+
super(`${slot} is declared for a workflow this pipeline does not have.\n` +
|
|
138
|
+
`\n` +
|
|
139
|
+
`The slot asks for ${varName}, for a workflow called "${workflow}". ` +
|
|
140
|
+
`Nothing runs under that name, so nothing would ever read the value: ` +
|
|
141
|
+
`the step that writes ${varName} would find no value and skip, and the ` +
|
|
142
|
+
`build would fail later for a reason that names your project.\n` +
|
|
143
|
+
`\n` +
|
|
144
|
+
`${because}`);
|
|
145
|
+
this.name = 'UnknownWorkflowSlotError';
|
|
146
|
+
this.slot = slot;
|
|
147
|
+
this.varName = varName;
|
|
148
|
+
this.workflow = workflow;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
98
151
|
/**
|
|
99
152
|
* EnvResolver handles environment variable merging and interpolation
|
|
100
153
|
*/
|
|
@@ -107,9 +160,15 @@ export class EnvResolver {
|
|
|
107
160
|
* not it gave one a value. Read by `importDeclaredFromProcessEnv`.
|
|
108
161
|
*/
|
|
109
162
|
declaredVars = new Set();
|
|
163
|
+
/** The workflow being resolved. Decides which scoped slots apply — `slotKeyFor`. */
|
|
164
|
+
workflowName;
|
|
165
|
+
/** Every workflow this pipeline declares. A scoped slot has to name one of them. */
|
|
166
|
+
declaredWorkflows;
|
|
110
167
|
constructor(pipeline, workflow, workflowName, platform, stack, yamlFilePath) {
|
|
111
168
|
this.envVars = new Map();
|
|
112
169
|
this.secretsManager = new SecretsManager();
|
|
170
|
+
this.workflowName = workflowName;
|
|
171
|
+
this.declaredWorkflows = Object.keys(pipeline.workflows ?? {});
|
|
113
172
|
// Step 1: Add built-in environment variables (auto-detected defaults)
|
|
114
173
|
this.addBuiltInVars(workflowName, platform, stack, yamlFilePath);
|
|
115
174
|
// Step 2: Load secrets — overrides auto-detected built-ins with user-stored values
|
|
@@ -378,9 +437,48 @@ export class EnvResolver {
|
|
|
378
437
|
* Merges environment variables from YAML array format
|
|
379
438
|
* @param envArray Array of environment variable objects
|
|
380
439
|
*/
|
|
440
|
+
/**
|
|
441
|
+
* The variable a declared `envs` key stands for, or null when the
|
|
442
|
+
* declaration belongs to a different workflow.
|
|
443
|
+
*
|
|
444
|
+
* A credential two workflows cannot share is declared once per workflow,
|
|
445
|
+
* under the same name a workflow-scoped secret carries — the pipeline says
|
|
446
|
+
* `CIBUILD_SW__RELEASE__LOCAL_PROPERTIES: ""` where it used to say
|
|
447
|
+
* `LOCAL_PROPERTIES: ""`. The build still reads the credential under its
|
|
448
|
+
* plain name, so the declaration has to mean the plain name: it is what puts
|
|
449
|
+
* an unsupplied key in the map for `file@1.0.0` to refuse over, and what
|
|
450
|
+
* lets `importDeclaredFromProcessEnv` accept a value delivered flat.
|
|
451
|
+
*
|
|
452
|
+
* A slot naming another workflow is dropped rather than declared. Declaring
|
|
453
|
+
* it would make this workflow demand a value only that one was ever going to
|
|
454
|
+
* be given. The decoding lives here, beside the decoding of the secret
|
|
455
|
+
* itself, so the steps keep reading their plain `var_name` and never learn
|
|
456
|
+
* the convention exists.
|
|
457
|
+
*
|
|
458
|
+
* A slot naming a workflow the pipeline does not have is refused rather than
|
|
459
|
+
* dropped — see `UnknownWorkflowSlotError`. Dropping it silently is the same
|
|
460
|
+
* outcome as never declaring the credential, which is the green skip issue 03
|
|
461
|
+
* exists to end.
|
|
462
|
+
*/
|
|
463
|
+
slotKeyFor(key) {
|
|
464
|
+
const decoded = decodeSecretName(key);
|
|
465
|
+
if (!decoded)
|
|
466
|
+
return key;
|
|
467
|
+
if (decoded.workflow) {
|
|
468
|
+
if (!this.declaredWorkflows.includes(decoded.workflow)) {
|
|
469
|
+
throw new UnknownWorkflowSlotError(key, decoded.varName, decoded.workflow, this.declaredWorkflows);
|
|
470
|
+
}
|
|
471
|
+
if (decoded.workflow !== this.workflowName)
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
return decoded.varName;
|
|
475
|
+
}
|
|
381
476
|
mergeEnvVars(envArray) {
|
|
382
477
|
for (const envObj of envArray) {
|
|
383
|
-
for (const [
|
|
478
|
+
for (const [declaredKey, value] of Object.entries(envObj)) {
|
|
479
|
+
const key = this.slotKeyFor(declaredKey);
|
|
480
|
+
if (key === null)
|
|
481
|
+
continue;
|
|
384
482
|
// Naming the key is the declaration, whatever value follows it
|
|
385
483
|
this.declaredVars.add(key);
|
|
386
484
|
// First resolve any secret references
|
|
@@ -43,17 +43,27 @@ export declare class SecretsManager {
|
|
|
43
43
|
* Get a secret value by variable name.
|
|
44
44
|
* Workflow-specific value takes precedence over global.
|
|
45
45
|
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present.
|
|
46
|
+
*
|
|
47
|
+
* The stored data is consulted before that fallback, and not only when a
|
|
48
|
+
* file was loaded. `storeSecret` writes the file but cannot make it one this
|
|
49
|
+
* instance *loaded*, so a value collected by `ci init` on a repository that
|
|
50
|
+
* had no store — which is the only kind of run that collects anything — read
|
|
51
|
+
* back as absent from the instance that had just stored it. `ci init`'s own
|
|
52
|
+
* `getSecret('KEYSTORE_PROPERTIES', wf)`, which is how a workflow's keystore
|
|
53
|
+
* target is taken from `storeFile=`, returned undefined on every fresh
|
|
54
|
+
* repository because of it.
|
|
46
55
|
*/
|
|
47
56
|
getSecret(name: string, workflow?: string): string | undefined;
|
|
48
57
|
/**
|
|
49
58
|
* Get all secrets merged: global + workflow-specific (workflow overrides global).
|
|
50
|
-
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present
|
|
59
|
+
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present —
|
|
60
|
+
* for the keys the store does not hold. See `getSecret`.
|
|
51
61
|
*/
|
|
52
62
|
getAll(workflow?: string): Record<string, string>;
|
|
53
63
|
/**
|
|
54
64
|
* Returns the variable name if a secret exists for it, undefined otherwise.
|
|
55
|
-
* Checks workflow scope first, then global
|
|
56
|
-
*
|
|
65
|
+
* Checks workflow scope first, then global, then the CIBUILD_-prefixed env
|
|
66
|
+
* vars when no secrets file is present. See `getSecret`.
|
|
57
67
|
*/
|
|
58
68
|
getSecretIdByName(name: string, workflow?: string): string | undefined;
|
|
59
69
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets-manager.d.ts","sourceRoot":"","sources":["../../../src/yaml/secrets-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,UAAU,CAAkB;gBAExB,eAAe,CAAC,EAAE,MAAM;IAMpC,OAAO,CAAC,WAAW;IAqCnB;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAkB/B,OAAO,CAAC,WAAW;IAUnB;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM;IAanE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAc;IAEhD;;;OAGG;IACH,OAAO,CAAC,aAAa;IAUrB
|
|
1
|
+
{"version":3,"file":"secrets-manager.d.ts","sourceRoot":"","sources":["../../../src/yaml/secrets-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,UAAU,CAAkB;gBAExB,eAAe,CAAC,EAAE,MAAM;IAMpC,OAAO,CAAC,WAAW;IAqCnB;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAkB/B,OAAO,CAAC,WAAW;IAUnB;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM;IAanE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAc;IAEhD;;;OAGG;IACH,OAAO,CAAC,aAAa;IAUrB;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAa9D;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IASjD;;;;OAIG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAatE;;OAEG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI7C;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO;IAInD;;OAEG;IACH,iBAAiB,IAAI,MAAM,EAAE;IAQ7B;;OAEG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO;IAetD,kBAAkB,IAAI,MAAM;CAG7B"}
|
|
@@ -129,25 +129,36 @@ export class SecretsManager {
|
|
|
129
129
|
* Get a secret value by variable name.
|
|
130
130
|
* Workflow-specific value takes precedence over global.
|
|
131
131
|
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present.
|
|
132
|
+
*
|
|
133
|
+
* The stored data is consulted before that fallback, and not only when a
|
|
134
|
+
* file was loaded. `storeSecret` writes the file but cannot make it one this
|
|
135
|
+
* instance *loaded*, so a value collected by `ci init` on a repository that
|
|
136
|
+
* had no store — which is the only kind of run that collects anything — read
|
|
137
|
+
* back as absent from the instance that had just stored it. `ci init`'s own
|
|
138
|
+
* `getSecret('KEYSTORE_PROPERTIES', wf)`, which is how a workflow's keystore
|
|
139
|
+
* target is taken from `storeFile=`, returned undefined on every fresh
|
|
140
|
+
* repository because of it.
|
|
132
141
|
*/
|
|
133
142
|
getSecret(name, workflow) {
|
|
134
|
-
if (!this.fileLoaded) {
|
|
135
|
-
return process.env[SecretsManager.ENV_PREFIX + name];
|
|
136
|
-
}
|
|
137
143
|
if (workflow && this.data.workflows[workflow]?.[name] !== undefined) {
|
|
138
144
|
return this.data.workflows[workflow][name];
|
|
139
145
|
}
|
|
140
|
-
|
|
146
|
+
if (this.data.global[name] !== undefined) {
|
|
147
|
+
return this.data.global[name];
|
|
148
|
+
}
|
|
149
|
+
if (!this.fileLoaded) {
|
|
150
|
+
return process.env[SecretsManager.ENV_PREFIX + name];
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
141
153
|
}
|
|
142
154
|
/**
|
|
143
155
|
* Get all secrets merged: global + workflow-specific (workflow overrides global).
|
|
144
|
-
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present
|
|
156
|
+
* Falls back to CIBUILD_-prefixed env vars when no secrets file is present —
|
|
157
|
+
* for the keys the store does not hold. See `getSecret`.
|
|
145
158
|
*/
|
|
146
159
|
getAll(workflow) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
const merged = { ...this.data.global };
|
|
160
|
+
const merged = this.fileLoaded ? {} : this.getEnvSecrets();
|
|
161
|
+
Object.assign(merged, this.data.global);
|
|
151
162
|
if (workflow && this.data.workflows[workflow]) {
|
|
152
163
|
Object.assign(merged, this.data.workflows[workflow]);
|
|
153
164
|
}
|
|
@@ -155,19 +166,19 @@ export class SecretsManager {
|
|
|
155
166
|
}
|
|
156
167
|
/**
|
|
157
168
|
* Returns the variable name if a secret exists for it, undefined otherwise.
|
|
158
|
-
* Checks workflow scope first, then global
|
|
159
|
-
*
|
|
169
|
+
* Checks workflow scope first, then global, then the CIBUILD_-prefixed env
|
|
170
|
+
* vars when no secrets file is present. See `getSecret`.
|
|
160
171
|
*/
|
|
161
172
|
getSecretIdByName(name, workflow) {
|
|
162
|
-
if (!this.fileLoaded) {
|
|
163
|
-
return process.env[SecretsManager.ENV_PREFIX + name] !== undefined ? name : undefined;
|
|
164
|
-
}
|
|
165
173
|
if (workflow && this.data.workflows[workflow]?.[name] !== undefined) {
|
|
166
174
|
return name;
|
|
167
175
|
}
|
|
168
176
|
if (this.data.global[name] !== undefined) {
|
|
169
177
|
return name;
|
|
170
178
|
}
|
|
179
|
+
if (!this.fileLoaded) {
|
|
180
|
+
return process.env[SecretsManager.ENV_PREFIX + name] !== undefined ? name : undefined;
|
|
181
|
+
}
|
|
171
182
|
return undefined;
|
|
172
183
|
}
|
|
173
184
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cocoapods-install` can be asked to skip a project that has no Podfile.
|
|
3
|
+
*
|
|
4
|
+
* The default stays a hard failure, because a pipeline that names this step is
|
|
5
|
+
* asserting the project uses CocoaPods and a missing Podfile is a real error.
|
|
6
|
+
*
|
|
7
|
+
* The iOS template needs the other behaviour: it serves every iOS project and a
|
|
8
|
+
* large share of them resolve dependencies through Swift Package Manager, where
|
|
9
|
+
* "Podfile not found" would be a CocoaPods error in a project that does not use
|
|
10
|
+
* CocoaPods. Until this input existed the template could not use this step at
|
|
11
|
+
* all, so it ran `pod install` from an inline `script@1.0.0` — which put it out
|
|
12
|
+
* of reach of the refused-fetch retry this step carries. That is the gap this
|
|
13
|
+
* closes, and it is why the release order matters: this must ship before the
|
|
14
|
+
* template is pointed at the step, or every SPM-only iOS repo breaks.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=cocoapods-skip-if-absent.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cocoapods-skip-if-absent.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cocoapods-skip-if-absent.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cocoapods-install` can be asked to skip a project that has no Podfile.
|
|
3
|
+
*
|
|
4
|
+
* The default stays a hard failure, because a pipeline that names this step is
|
|
5
|
+
* asserting the project uses CocoaPods and a missing Podfile is a real error.
|
|
6
|
+
*
|
|
7
|
+
* The iOS template needs the other behaviour: it serves every iOS project and a
|
|
8
|
+
* large share of them resolve dependencies through Swift Package Manager, where
|
|
9
|
+
* "Podfile not found" would be a CocoaPods error in a project that does not use
|
|
10
|
+
* CocoaPods. Until this input existed the template could not use this step at
|
|
11
|
+
* all, so it ran `pod install` from an inline `script@1.0.0` — which put it out
|
|
12
|
+
* of reach of the refused-fetch retry this step carries. That is the gap this
|
|
13
|
+
* closes, and it is why the release order matters: this must ship before the
|
|
14
|
+
* template is pointed at the step, or every SPM-only iOS repo breaks.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, test, expect } from '@jest/globals';
|
|
17
|
+
import { CocoapodsInstallStepExecutor } from './ios-deps.js';
|
|
18
|
+
import { testConfig } from './test-config.js';
|
|
19
|
+
const build = async (inputs = {}) => {
|
|
20
|
+
const executor = new CocoapodsInstallStepExecutor();
|
|
21
|
+
const result = await executor.execute(inputs, {}, testConfig);
|
|
22
|
+
return result.script;
|
|
23
|
+
};
|
|
24
|
+
/** The body of the `if [ ! -f "Podfile" ]` branch, which is what changes. */
|
|
25
|
+
const absentBranch = (script) => {
|
|
26
|
+
const lines = script.split('\n');
|
|
27
|
+
const start = lines.findIndex((l) => l.includes('if [ ! -f "Podfile" ]'));
|
|
28
|
+
expect(start).toBeGreaterThanOrEqual(0);
|
|
29
|
+
const end = lines.findIndex((l, i) => i > start && l.trim() === 'fi');
|
|
30
|
+
return lines.slice(start, end + 1).join('\n');
|
|
31
|
+
};
|
|
32
|
+
describe('by default a missing Podfile is still an error', () => {
|
|
33
|
+
test('exits non-zero', async () => {
|
|
34
|
+
expect(absentBranch(await build())).toContain('exit 1');
|
|
35
|
+
});
|
|
36
|
+
test('says what was not found', async () => {
|
|
37
|
+
expect(absentBranch(await build())).toContain('Podfile not found');
|
|
38
|
+
});
|
|
39
|
+
test('an explicit false is the same as omitting it', async () => {
|
|
40
|
+
expect(absentBranch(await build({ skip_if_absent: false }))).toContain('exit 1');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('skip_if_absent turns it into a clean no-op', () => {
|
|
44
|
+
test('exits zero instead of one', async () => {
|
|
45
|
+
const branch = absentBranch(await build({ skip_if_absent: true }));
|
|
46
|
+
expect(branch).toContain('exit 0');
|
|
47
|
+
expect(branch).not.toContain('exit 1');
|
|
48
|
+
});
|
|
49
|
+
test('says it skipped rather than that something failed', async () => {
|
|
50
|
+
const branch = absentBranch(await build({ skip_if_absent: true }));
|
|
51
|
+
expect(branch).toContain('skipping CocoaPods install');
|
|
52
|
+
expect(branch).not.toContain('Error');
|
|
53
|
+
});
|
|
54
|
+
test('still runs pod install when a Podfile IS present', async () => {
|
|
55
|
+
// The skip is a branch, not a mode — the rest of the step is untouched.
|
|
56
|
+
const script = await build({ skip_if_absent: true });
|
|
57
|
+
expect(script).toContain('POD_INVOCATION');
|
|
58
|
+
expect(script).toContain('$POD_INVOCATION $POD_CMD');
|
|
59
|
+
});
|
|
60
|
+
test('still wraps the pod fetch in the refused-fetch retry', async () => {
|
|
61
|
+
// This is the entire point of letting the template use the step.
|
|
62
|
+
const script = await build({ skip_if_absent: true });
|
|
63
|
+
expect(script).toContain('cibuild_fetch_was_refused');
|
|
64
|
+
expect(script).toContain('CIBUILD_FETCH_MAX_ATTEMPTS');
|
|
65
|
+
});
|
|
66
|
+
test('still chooses between bundle exec and bare pod', async () => {
|
|
67
|
+
const script = await build({ skip_if_absent: true });
|
|
68
|
+
expect(script).toContain('USE_BUNDLE_EXEC');
|
|
69
|
+
expect(script).toContain('bundle exec pod');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* The inline script the template used did two things, not one: it skipped a
|
|
74
|
+
* project with no Podfile, AND it looked for that Podfile beside the Xcode
|
|
75
|
+
* project rather than at the repository root. Both have to exist as inputs or
|
|
76
|
+
* the template cannot be switched — about a quarter of iOS repositories keep
|
|
77
|
+
* the app one directory down, and template inputs interpolate variables, not
|
|
78
|
+
* `$(dirname …)`.
|
|
79
|
+
*/
|
|
80
|
+
describe('project_path finds the Podfile beside the project', () => {
|
|
81
|
+
test('changes into the project directory, not the repo root', async () => {
|
|
82
|
+
const script = await build({ project_path: 'ios/MyApp.xcworkspace' });
|
|
83
|
+
expect(script).toContain(`WORK_DIR="$(dirname 'ios/MyApp.xcworkspace')"`);
|
|
84
|
+
expect(script).toContain('cd "$WORK_DIR"');
|
|
85
|
+
});
|
|
86
|
+
test('does not fall back to CIBUILD_SOURCE_DIR when it is given', async () => {
|
|
87
|
+
const script = await build({ project_path: 'ios/MyApp.xcworkspace' });
|
|
88
|
+
expect(script).not.toContain('cd "$CIBUILD_SOURCE_DIR"');
|
|
89
|
+
});
|
|
90
|
+
test('still falls back to CIBUILD_SOURCE_DIR when nothing is given', async () => {
|
|
91
|
+
expect(await build()).toContain('cd "$CIBUILD_SOURCE_DIR"');
|
|
92
|
+
});
|
|
93
|
+
test('an explicit source_root_path still wins over project_path', async () => {
|
|
94
|
+
const script = await build({
|
|
95
|
+
project_path: 'ios/MyApp.xcworkspace',
|
|
96
|
+
source_root_path: 'somewhere/else',
|
|
97
|
+
});
|
|
98
|
+
expect(script).toContain(`cd 'somewhere/else'`);
|
|
99
|
+
expect(script).not.toContain('dirname');
|
|
100
|
+
});
|
|
101
|
+
test('composes with skip_if_absent, which is how the template uses it', async () => {
|
|
102
|
+
const script = await build({ project_path: 'ios/MyApp.xcworkspace', skip_if_absent: true });
|
|
103
|
+
expect(script).toContain(`WORK_DIR="$(dirname 'ios/MyApp.xcworkspace')"`);
|
|
104
|
+
expect(absentBranch(script)).toContain('exit 0');
|
|
105
|
+
expect(script).toContain('cibuild_fetch_was_refused');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* The exact input combination the iOS template sends. Pinned here because the
|
|
110
|
+
* template lives in another repository and deploys on a different cadence, so
|
|
111
|
+
* nothing else would notice this step drifting away from what it sends.
|
|
112
|
+
*/
|
|
113
|
+
describe('the iOS template combination', () => {
|
|
114
|
+
const templateInputs = {
|
|
115
|
+
project_path: 'MyApp.xcworkspace',
|
|
116
|
+
skip_if_absent: true,
|
|
117
|
+
command: 'install --repo-update',
|
|
118
|
+
};
|
|
119
|
+
test('produces pod install --repo-update, flags intact', async () => {
|
|
120
|
+
const script = await build(templateInputs);
|
|
121
|
+
expect(script).toContain('POD_CMD="install --repo-update"');
|
|
122
|
+
});
|
|
123
|
+
test('runs that under the refused-fetch retry, exactly once', async () => {
|
|
124
|
+
const script = await build(templateInputs);
|
|
125
|
+
expect(script).toContain('$POD_INVOCATION $POD_CMD');
|
|
126
|
+
expect(script.split('CIBUILD_FETCH_MAX_ATTEMPTS=').length - 1).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
test('skips instead of failing when the repo has no Podfile', async () => {
|
|
129
|
+
expect(absentBranch(await build(templateInputs))).toContain('exit 0');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=cocoapods-skip-if-absent.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4CH;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4CH;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAq8C7C"}
|
|
@@ -451,11 +451,20 @@ export function initializeStepRegistry() {
|
|
|
451
451
|
description: 'Explicit path to Podfile',
|
|
452
452
|
required: false,
|
|
453
453
|
},
|
|
454
|
+
project_path: {
|
|
455
|
+
description: 'A .xcodeproj/.xcworkspace whose directory holds the Podfile. For repositories that keep the app one directory down from the root.',
|
|
456
|
+
required: false,
|
|
457
|
+
},
|
|
454
458
|
verbose: {
|
|
455
459
|
description: 'Enable verbose CocoaPods logging',
|
|
456
460
|
required: false,
|
|
457
461
|
default: false,
|
|
458
462
|
},
|
|
463
|
+
skip_if_absent: {
|
|
464
|
+
description: 'Succeed and do nothing when there is no Podfile, instead of failing. For pipelines that serve every iOS project, where plenty use Swift Package Manager and have no Podfile.',
|
|
465
|
+
required: false,
|
|
466
|
+
default: false,
|
|
467
|
+
},
|
|
459
468
|
},
|
|
460
469
|
});
|
|
461
470
|
registerStep('carthage', new CarthageStepExecutor(), {
|
|
@@ -11,8 +11,43 @@ export interface CocoapodsInstallInputs {
|
|
|
11
11
|
source_root_path?: string;
|
|
12
12
|
/** Explicit path to Podfile (overrides source_root_path) */
|
|
13
13
|
podfile_path?: string;
|
|
14
|
+
/**
|
|
15
|
+
* A .xcodeproj/.xcworkspace whose DIRECTORY holds the Podfile.
|
|
16
|
+
*
|
|
17
|
+
* About a quarter of iOS repositories keep the app one directory down from
|
|
18
|
+
* the repository root, and the Podfile is down there with it — so "run pod
|
|
19
|
+
* install at the root" is wrong for them. A caller that already knows where
|
|
20
|
+
* the project is should not have to compute its directory separately, and a
|
|
21
|
+
* pipeline template cannot: template inputs interpolate variables, not
|
|
22
|
+
* `$(dirname …)`, so `source_root_path` alone could not express it and the
|
|
23
|
+
* iOS template had to run `pod install` from an inline script to get at
|
|
24
|
+
* `dirname`. That inline script is what put it out of reach of the
|
|
25
|
+
* refused-fetch retry.
|
|
26
|
+
*
|
|
27
|
+
* Only the directory is used; the file itself need not exist.
|
|
28
|
+
*
|
|
29
|
+
* Precedence, most specific first: podfile_path, source_root_path,
|
|
30
|
+
* project_path, then $CIBUILD_SOURCE_DIR.
|
|
31
|
+
*/
|
|
32
|
+
project_path?: string;
|
|
14
33
|
/** Enable verbose CocoaPods logging */
|
|
15
34
|
verbose?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Succeed and do nothing when there is no Podfile, instead of failing.
|
|
37
|
+
*
|
|
38
|
+
* Defaults to false, which keeps the contract every existing caller was
|
|
39
|
+
* written against: a pipeline that names this step is asserting the project
|
|
40
|
+
* uses CocoaPods, and a missing Podfile is a real error worth stopping for.
|
|
41
|
+
*
|
|
42
|
+
* The iOS template is the caller that needs the other behaviour. It serves
|
|
43
|
+
* every iOS project, and a large share of them resolve dependencies through
|
|
44
|
+
* Swift Package Manager and have no Podfile at all — for those, "Podfile not
|
|
45
|
+
* found" is a CocoaPods error in a project that does not use CocoaPods.
|
|
46
|
+
* Until this existed the template could not use this step, and so ran
|
|
47
|
+
* `pod install` from an inline script instead, out of reach of the
|
|
48
|
+
* refused-fetch retry that this step carries.
|
|
49
|
+
*/
|
|
50
|
+
skip_if_absent?: boolean;
|
|
16
51
|
}
|
|
17
52
|
/**
|
|
18
53
|
* Runs `pod install` or `pod update`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios-deps.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ios-deps.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAc,MAAM,wBAAwB,CAAC;AAMhF,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ios-deps.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ios-deps.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAc,MAAM,wBAAwB,CAAC;AAMhF,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,gBAAgB;IAChE,yBAAyB,CACvB,OAAO,EAAE,sBAAsB,EAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CA+FpB;AAMD,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,6BAA6B;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,gBAAgB;IACxD,yBAAyB,CACvB,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAyDpB"}
|
|
@@ -19,7 +19,9 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
|
|
|
19
19
|
const command = this.getInput(inputs, 'command', 'install');
|
|
20
20
|
const sourceRootPath = this.getInput(inputs, 'source_root_path', '');
|
|
21
21
|
const podfilePath = this.getInput(inputs, 'podfile_path', '');
|
|
22
|
+
const projectPath = this.getInput(inputs, 'project_path', '');
|
|
22
23
|
const verbose = this.getInput(inputs, 'verbose', false);
|
|
24
|
+
const skipIfAbsent = this.getInput(inputs, 'skip_if_absent', false);
|
|
23
25
|
const commands = [];
|
|
24
26
|
commands.push('# cocoapods-install');
|
|
25
27
|
commands.push('echo "🔧 Running CocoaPods..."');
|
|
@@ -33,6 +35,11 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
|
|
|
33
35
|
else if (sourceRootPath) {
|
|
34
36
|
commands.push(`cd '${this.escapeBash(sourceRootPath)}'`);
|
|
35
37
|
}
|
|
38
|
+
else if (projectPath) {
|
|
39
|
+
commands.push('# Podfile sits beside the Xcode project, not at the repo root');
|
|
40
|
+
commands.push(`WORK_DIR="$(dirname '${this.escapeBash(projectPath)}')"`);
|
|
41
|
+
commands.push('cd "$WORK_DIR"');
|
|
42
|
+
}
|
|
36
43
|
else {
|
|
37
44
|
commands.push('# Use CIBUILD_SOURCE_DIR if set, otherwise current directory');
|
|
38
45
|
commands.push('if [ -n "$CIBUILD_SOURCE_DIR" ]; then');
|
|
@@ -40,10 +47,18 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
|
|
|
40
47
|
commands.push('fi');
|
|
41
48
|
}
|
|
42
49
|
commands.push('');
|
|
43
|
-
// Check for Podfile
|
|
50
|
+
// Check for Podfile. Absent is either a hard error or a clean no-op,
|
|
51
|
+
// depending on whether the caller is asserting "this project uses
|
|
52
|
+
// CocoaPods" or asking "use CocoaPods if this project does".
|
|
44
53
|
commands.push('if [ ! -f "Podfile" ]; then');
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
if (skipIfAbsent) {
|
|
55
|
+
commands.push(' echo "No Podfile in $(pwd) — skipping CocoaPods install."');
|
|
56
|
+
commands.push(' exit 0');
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
commands.push(' echo "❌ Error: Podfile not found in $(pwd)"');
|
|
60
|
+
commands.push(' exit 1');
|
|
61
|
+
}
|
|
47
62
|
commands.push('fi');
|
|
48
63
|
commands.push('');
|
|
49
64
|
// Determine whether to use bundle exec
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Products go under a derived data path, never into one flat directory.
|
|
3
|
+
*
|
|
4
|
+
* `CONFIGURATION_BUILD_DIR` overrides the per-configuration/per-platform
|
|
5
|
+
* product separation xcodebuild provides under
|
|
6
|
+
* `DerivedData/Build/Products/<config>-<platform>/`, flattening every target in
|
|
7
|
+
* the graph into a single directory. Two build tasks for the same target then
|
|
8
|
+
* declare the same absolute output path and xcodebuild refuses the entire build
|
|
9
|
+
* with "Multiple commands produce". The notes it prints name the SAME target
|
|
10
|
+
* twice, which is the tell: one target built twice into one directory, not two
|
|
11
|
+
* packages colliding on a name.
|
|
12
|
+
*
|
|
13
|
+
* Measured 2026-09-09 on the iOS corpus rows, all on the template's must-pass
|
|
14
|
+
* "Build App" step: 16 collisions on jellyfin/Swiftfin, 266 on
|
|
15
|
+
* Automattic/pocket-casts-ios, 425 on home-assistant/iOS.
|
|
16
|
+
*
|
|
17
|
+
* A fourth row failed differently for the same reason. CocoaPods derives
|
|
18
|
+
* `PODS_CONFIGURATION_BUILD_DIR` from `BUILD_DIR`, which `CONFIGURATION_BUILD_DIR`
|
|
19
|
+
* does NOT override — so pod products land in the flat directory while every
|
|
20
|
+
* generated copy-resources script and linker search path still points into
|
|
21
|
+
* DerivedData. shenruisi/Stay reported "Resource SDWebImage.bundle not found.
|
|
22
|
+
* Run pod install" for a bundle that had been built two directories away.
|
|
23
|
+
*
|
|
24
|
+
* Proved by control on a runner guest, same repo and image and flags with only
|
|
25
|
+
* this setting differing: `CONFIGURATION_BUILD_DIR` reproduced the collision and
|
|
26
|
+
* exit 65 with no .app, `-derivedDataPath` reached BUILD SUCCEEDED in 2m06s and
|
|
27
|
+
* left both `Debug` and `Debug-iphonesimulator` under Products.
|
|
28
|
+
*/
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=xcode-derived-data.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xcode-derived-data.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode-derived-data.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG"}
|