laneyard 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -491
- package/dist/src/cli/adopt.js +68 -19
- package/dist/src/cli/remove.js +50 -32
- package/dist/src/cli/setup.js +85 -40
- package/dist/src/fastfile/adoption.js +59 -16
- package/dist/src/git/workspace.js +28 -0
- package/dist/src/heuristics/readiness.js +80 -135
- package/dist/src/main.js +24 -3
- package/dist/src/runner/orchestrate.js +25 -4
- package/dist/src/secrets/vault.js +52 -15
- package/dist/src/server/routes/fastfile.js +1 -1
- package/dist/src/server/routes/projects.js +59 -0
- package/dist/src/server/routes/secrets.js +20 -17
- package/dist/src/sidecar/fastlane-dir.js +18 -8
- package/dist/src/sidecar/lanes.js +1 -1
- package/dist/src/sidecar/scan.js +1 -0
- package/dist/src/sidecar/uses.js +1 -1
- package/dist/web/assets/{Editor-CMa4-4N3.js → Editor-wZdJUk7D.js} +1 -1
- package/dist/web/assets/index-CPKV0yx1.css +1 -0
- package/dist/web/assets/index-Cq1Ze8dP.js +62 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/ruby/scan.rb +44 -13
- package/dist/web/assets/index-B8lAQPEM.js +0 -62
- package/dist/web/assets/index-DsjxZtsO.css +0 -1
|
@@ -56,14 +56,12 @@ const META = {
|
|
|
56
56
|
export async function checkRepository(probe) {
|
|
57
57
|
try {
|
|
58
58
|
await probe();
|
|
59
|
-
return { ...META.repository, ...ok("the remote answers without
|
|
59
|
+
return { ...META.repository, ...ok("the remote answers without a password.") };
|
|
60
60
|
}
|
|
61
61
|
catch (cause) {
|
|
62
62
|
return {
|
|
63
63
|
...META.repository,
|
|
64
|
-
...warn(reasonOf(cause), "
|
|
65
|
-
"`git_auth: {kind: ssh_key, ref: /path/to/key}` in config.yml — or a URL that needs no password. " +
|
|
66
|
-
"A run that meets a credentials prompt does not fail: it waits."),
|
|
64
|
+
...warn(reasonOf(cause), "Point `git_auth` at a passphrase-less SSH key in config.yml. A run that meets a prompt waits."),
|
|
67
65
|
};
|
|
68
66
|
}
|
|
69
67
|
}
|
|
@@ -75,12 +73,12 @@ export async function checkDependencies(input) {
|
|
|
75
73
|
if (workspace.value.hasGemfile) {
|
|
76
74
|
try {
|
|
77
75
|
await input.bundleCheck();
|
|
78
|
-
return { ...META.dependencies, ...ok("the
|
|
76
|
+
return { ...META.dependencies, ...ok("the bundle is installed.") };
|
|
79
77
|
}
|
|
80
78
|
catch (cause) {
|
|
81
79
|
return {
|
|
82
80
|
...META.dependencies,
|
|
83
|
-
...warn(reasonOf(cause), "Run `bundle install` in the
|
|
81
|
+
...warn(reasonOf(cause), "Run `bundle install` in the workspace."),
|
|
84
82
|
};
|
|
85
83
|
}
|
|
86
84
|
}
|
|
@@ -94,14 +92,11 @@ export async function checkDependencies(input) {
|
|
|
94
92
|
if (fastlane !== null) {
|
|
95
93
|
// Not a failure, but a fact worth stating: a system fastlane can be
|
|
96
94
|
// upgraded underneath a project that was working yesterday.
|
|
97
|
-
return {
|
|
98
|
-
...META.dependencies,
|
|
99
|
-
...ok(`no Gemfile — runs use ${fastlane}, whose version nothing pins.`),
|
|
100
|
-
};
|
|
95
|
+
return { ...META.dependencies, ...ok(`no Gemfile — runs use ${fastlane}, unpinned.`) };
|
|
101
96
|
}
|
|
102
97
|
return {
|
|
103
98
|
...META.dependencies,
|
|
104
|
-
...warn("no Gemfile
|
|
99
|
+
...warn("no Gemfile and no fastlane on the PATH: a run has nothing to execute.", 'Add a Gemfile with `gem "fastlane"`, or install fastlane system-wide.'),
|
|
105
100
|
};
|
|
106
101
|
}
|
|
107
102
|
/** Any suffix: the key is split across several variables, and their names vary by lane. */
|
|
@@ -119,10 +114,8 @@ const API_KEY = /^APP_STORE_CONNECT_API_KEY/;
|
|
|
119
114
|
* being left with a checklist that has quietly gone silent about it.
|
|
120
115
|
*/
|
|
121
116
|
const DEAD_API_KEY = /^APP_STORE_CONNECT_API_KEY_P8$/;
|
|
122
|
-
const STORE_API_KEY = "Upload the `.p8` as an App Store Connect key block
|
|
123
|
-
"issuer id.
|
|
124
|
-
"the names the block carries, so nothing in the project has to change. " +
|
|
125
|
-
"An API key does not expire on its own.";
|
|
117
|
+
const STORE_API_KEY = "Upload the `.p8` as an App Store Connect key block on the signing tab, with its key id and " +
|
|
118
|
+
"issuer id. Nothing in the project has to change.";
|
|
126
119
|
/** The action whose whole job is to load a `.p8`, under both of its names. */
|
|
127
120
|
const ASC_KEY_ACTIONS = new Set(["app_store_connect_api_key", "asc_api_key"]);
|
|
128
121
|
/**
|
|
@@ -188,23 +181,18 @@ export function checkAppStoreConnect(input) {
|
|
|
188
181
|
const { secretKeys, uses, keyFilesInRepo, appfile } = input;
|
|
189
182
|
const blocks = input.blocks ?? [];
|
|
190
183
|
if (blocks.includes("apple_asc")) {
|
|
191
|
-
return {
|
|
192
|
-
...META.appStoreConnect,
|
|
193
|
-
...ok("an App Store Connect key block is in the vault: the `.p8` is written for the length of a " +
|
|
194
|
-
"run, with its key id and issuer id exported beside it."),
|
|
195
|
-
};
|
|
184
|
+
return { ...META.appStoreConnect, ...ok("a key block is in the vault.") };
|
|
196
185
|
}
|
|
197
186
|
if (secretKeys.some((key) => API_KEY.test(key) && !DEAD_API_KEY.test(key))) {
|
|
198
|
-
return { ...META.appStoreConnect, ...ok("an
|
|
187
|
+
return { ...META.appStoreConnect, ...ok("an API key is in the vault.") };
|
|
199
188
|
}
|
|
200
189
|
// The one place on this checklist where somebody is asked to redo work — and
|
|
201
190
|
// it is work that never did anything. See `DEAD_API_KEY`.
|
|
202
191
|
if (secretKeys.some((key) => DEAD_API_KEY.test(key))) {
|
|
203
192
|
return {
|
|
204
193
|
...META.appStoreConnect,
|
|
205
|
-
...warn("`APP_STORE_CONNECT_API_KEY_P8` is
|
|
206
|
-
"
|
|
207
|
-
"interface asked for it.", STORE_API_KEY, "signing"),
|
|
194
|
+
...warn("`APP_STORE_CONNECT_API_KEY_P8` is stored, and no fastlane action reads that name: no lane " +
|
|
195
|
+
"can see it.", STORE_API_KEY, "signing"),
|
|
208
196
|
};
|
|
209
197
|
}
|
|
210
198
|
// Named in a lane: the commonest way a pre-existing project holds its key,
|
|
@@ -216,8 +204,8 @@ export function checkAppStoreConnect(input) {
|
|
|
216
204
|
if (inLanes.length > 0) {
|
|
217
205
|
return {
|
|
218
206
|
...META.appStoreConnect,
|
|
219
|
-
...undetermined(`${nameList(inLanes.map((l) => l.lane))} supplies its own
|
|
220
|
-
"
|
|
207
|
+
...undetermined(`${nameList(inLanes.map((l) => l.lane))} supplies its own key — whether the \`.p8\` it names ` +
|
|
208
|
+
"is on this machine is not something a Fastfile says.", STORE_API_KEY),
|
|
221
209
|
};
|
|
222
210
|
}
|
|
223
211
|
// Nothing in the vault, and nothing named in a lane. Before reporting an
|
|
@@ -233,27 +221,22 @@ export function checkAppStoreConnect(input) {
|
|
|
233
221
|
if (blind) {
|
|
234
222
|
return {
|
|
235
223
|
...META.appStoreConnect,
|
|
236
|
-
...undetermined(`no lane seen
|
|
224
|
+
...undetermined(`no lane seen signing in to App Store Connect — but ${blind}.`, STORE_API_KEY),
|
|
237
225
|
};
|
|
238
226
|
}
|
|
239
|
-
return {
|
|
240
|
-
...META.appStoreConnect,
|
|
241
|
-
...ok("no lane signs in to App Store Connect: nothing here needs a key."),
|
|
242
|
-
};
|
|
227
|
+
return { ...META.appStoreConnect, ...ok("no lane signs in to App Store Connect.") };
|
|
243
228
|
}
|
|
244
229
|
const p8 = keyFilesInRepo.ok ? keyFilesInRepo.value : [];
|
|
245
230
|
if (p8.length > 0) {
|
|
246
231
|
return {
|
|
247
232
|
...META.appStoreConnect,
|
|
248
|
-
...undetermined(`the repository carries ${nameList(p8)}:
|
|
249
|
-
"loads it, or that it is the one App Store Connect expects.", STORE_API_KEY),
|
|
233
|
+
...undetermined(`the repository carries ${nameList(p8)}: arranged, but nothing here says a lane loads it.`, STORE_API_KEY),
|
|
250
234
|
};
|
|
251
235
|
}
|
|
252
236
|
if (secretKeys.includes("FASTLANE_SESSION")) {
|
|
253
237
|
return {
|
|
254
238
|
...META.appStoreConnect,
|
|
255
|
-
...warn("only a FASTLANE_SESSION:
|
|
256
|
-
"stops and asks for a verification code.", STORE_API_KEY, "signing"),
|
|
239
|
+
...warn("only a FASTLANE_SESSION: it expires, and the run that finds it expired asks for a code.", STORE_API_KEY, "signing"),
|
|
257
240
|
};
|
|
258
241
|
}
|
|
259
242
|
// An Apple ID in the Appfile is not a credential — it is the thing that makes
|
|
@@ -262,8 +245,7 @@ export function checkAppStoreConnect(input) {
|
|
|
262
245
|
if (appfile.ok && appfile.value.appleId.kind !== "absent") {
|
|
263
246
|
return {
|
|
264
247
|
...META.appStoreConnect,
|
|
265
|
-
...warn("the Appfile names an Apple ID and nothing else:
|
|
266
|
-
"account, and two-factor authentication stops the run to ask for a code.", STORE_API_KEY, "signing"),
|
|
248
|
+
...warn("the Appfile names an Apple ID and nothing else: two-factor stops the run to ask for a code.", STORE_API_KEY, "signing"),
|
|
267
249
|
};
|
|
268
250
|
}
|
|
269
251
|
// This one is a warning rather than a tick, so being wrong costs less — but
|
|
@@ -272,14 +254,13 @@ export function checkAppStoreConnect(input) {
|
|
|
272
254
|
if (blind) {
|
|
273
255
|
return {
|
|
274
256
|
...META.appStoreConnect,
|
|
275
|
-
...undetermined(`no
|
|
276
|
-
`lanes — but ${blind}.`, STORE_API_KEY),
|
|
257
|
+
...undetermined(`no credential in the vault, the lanes or the repository — but ${blind}.`, STORE_API_KEY),
|
|
277
258
|
};
|
|
278
259
|
}
|
|
279
260
|
return {
|
|
280
261
|
...META.appStoreConnect,
|
|
281
|
-
...warn("no
|
|
282
|
-
"
|
|
262
|
+
...warn("no credential in the vault, the lanes or the repository: a lane that uploads will ask for " +
|
|
263
|
+
"an Apple ID.", STORE_API_KEY, "signing"),
|
|
283
264
|
};
|
|
284
265
|
}
|
|
285
266
|
/** The two names for the same action. `match` is the older one, still the common one. */
|
|
@@ -302,16 +283,16 @@ export function checkMatch(uses, secretKeys) {
|
|
|
302
283
|
if (!secretKeys.includes("MATCH_PASSWORD")) {
|
|
303
284
|
return {
|
|
304
285
|
...META.match,
|
|
305
|
-
...warn(`${nameList(calls.map((c) => c.lane))} calls match,
|
|
306
|
-
"match asks for the passphrase and waits
|
|
286
|
+
...warn(`${nameList(calls.map((c) => c.lane))} calls match, and MATCH_PASSWORD is not in the vault: ` +
|
|
287
|
+
"match asks for the passphrase and waits.", "Store `MATCH_PASSWORD` on the secrets tab.", "secrets"),
|
|
307
288
|
};
|
|
308
289
|
}
|
|
309
290
|
const writable = calls.filter((c) => c.action.args["readonly"] === false);
|
|
310
291
|
if (writable.length > 0) {
|
|
311
292
|
return {
|
|
312
293
|
...META.match,
|
|
313
|
-
...warn(`${nameList(writable.map((c) => c.lane))} calls match with \`readonly: false\`:
|
|
314
|
-
"
|
|
294
|
+
...warn(`${nameList(writable.map((c) => c.lane))} calls match with \`readonly: false\`: creating a ` +
|
|
295
|
+
"certificate needs an Apple account interactively.", "Pass `readonly: true`."),
|
|
315
296
|
};
|
|
316
297
|
}
|
|
317
298
|
// `match(readonly: ENV["RO"])` arrives with no `readonly` at all: the sidecar
|
|
@@ -322,7 +303,7 @@ export function checkMatch(uses, secretKeys) {
|
|
|
322
303
|
return {
|
|
323
304
|
...META.match,
|
|
324
305
|
...undetermined(`MATCH_PASSWORD is stored, but \`readonly\` is not a literal in ` +
|
|
325
|
-
`${nameList(undecided.map((c) => c.lane))}: Laneyard reads
|
|
306
|
+
`${nameList(undecided.map((c) => c.lane))}: Laneyard reads literals only, and will not guess.`, "Pass `readonly: true` in the call itself."),
|
|
326
307
|
};
|
|
327
308
|
}
|
|
328
309
|
return { ...META.match, ...ok("MATCH_PASSWORD is stored, and every match call is readonly.") };
|
|
@@ -337,8 +318,7 @@ export function checkBlockingActions(uses, unread = { ok: true, value: READ_EVER
|
|
|
337
318
|
if (blind) {
|
|
338
319
|
return {
|
|
339
320
|
...META.blockingActions,
|
|
340
|
-
...undetermined(`no lane seen
|
|
341
|
-
"waits could be out of sight from here."),
|
|
321
|
+
...undetermined(`no lane seen calling an action known to stop and ask — but ${blind}.`),
|
|
342
322
|
};
|
|
343
323
|
}
|
|
344
324
|
return { ...META.blockingActions, ...ok("no lane calls an action known to stop and ask.") };
|
|
@@ -370,19 +350,14 @@ const KEYSTORE_PASSWORD = /(^|_)(KEYSTORE|STORE)_PASSWORD$/;
|
|
|
370
350
|
* and so not Laneyard's to ask for. A block carries the file and the passphrases
|
|
371
351
|
* together, and a run gets both without the project knowing anything about it.
|
|
372
352
|
*/
|
|
373
|
-
const STORE_KEYSTORE = "Upload the keystore as an android keystore block
|
|
374
|
-
"
|
|
375
|
-
"block carries, so no lane has to be changed to find it.";
|
|
353
|
+
const STORE_KEYSTORE = "Upload the keystore as an android keystore block on the signing tab, with its alias and its two " +
|
|
354
|
+
"passphrases. No lane has to change.";
|
|
376
355
|
export function checkAndroidKeystore(uses, secretKeys, unread = { ok: true, value: READ_EVERYTHING }, blocks = []) {
|
|
377
356
|
// Answered before the lanes are read, and on purpose: the question is whether
|
|
378
357
|
// anything will stop and ask for a passphrase, and a block means nothing can —
|
|
379
358
|
// whichever lane, and whether or not gradle is called by name.
|
|
380
359
|
if (blocks.includes("android_keystore")) {
|
|
381
|
-
return {
|
|
382
|
-
...META.androidKeystore,
|
|
383
|
-
...ok("an android keystore block is in the vault: the keystore is written for the length of a " +
|
|
384
|
-
"run and its passphrases exported with it, so nothing stops to ask for one."),
|
|
385
|
-
};
|
|
360
|
+
return { ...META.androidKeystore, ...ok("a keystore block is in the vault.") };
|
|
386
361
|
}
|
|
387
362
|
if (!uses.ok) {
|
|
388
363
|
return { ...META.androidKeystore, ...undetermined(`could not read the lanes: ${uses.reason}`) };
|
|
@@ -402,9 +377,8 @@ export function checkAndroidKeystore(uses, secretKeys, unread = { ok: true, valu
|
|
|
402
377
|
...META.androidKeystore,
|
|
403
378
|
...undetermined("no lane seen handing gradle a keystore" +
|
|
404
379
|
(blind ? ` — and ${blind}` : "") +
|
|
405
|
-
".
|
|
406
|
-
"
|
|
407
|
-
"react-native.", STORE_KEYSTORE),
|
|
380
|
+
". Signing is configured somewhere a Fastfile does not show: `build.gradle`, " +
|
|
381
|
+
"`key.properties`, or a build driven through flutter or react-native.", STORE_KEYSTORE),
|
|
408
382
|
};
|
|
409
383
|
}
|
|
410
384
|
// `given`, not `args`, for the same reason as the two credential checks:
|
|
@@ -417,15 +391,14 @@ export function checkAndroidKeystore(uses, secretKeys, unread = { ok: true, valu
|
|
|
417
391
|
// invisible from here, and claiming it is absent would be a guess.
|
|
418
392
|
return {
|
|
419
393
|
...META.androidKeystore,
|
|
420
|
-
...ok("no lane passes `storeFile` or `storePassword` to gradle
|
|
394
|
+
...ok("no lane passes `storeFile` or `storePassword` to gradle."),
|
|
421
395
|
};
|
|
422
396
|
}
|
|
423
397
|
const withoutPassphrase = signing.filter((c) => !("storePassword" in c.action.args));
|
|
424
398
|
if (withoutPassphrase.length === 0) {
|
|
425
399
|
return {
|
|
426
400
|
...META.androidKeystore,
|
|
427
|
-
...ok(`${nameList(signing.map((c) => c.lane))} passes \`storePassword\` in the call itself
|
|
428
|
-
"gradle has what it needs."),
|
|
401
|
+
...ok(`${nameList(signing.map((c) => c.lane))} passes \`storePassword\` in the call itself.`),
|
|
429
402
|
};
|
|
430
403
|
}
|
|
431
404
|
if (secretKeys.some((key) => KEYSTORE_PASSWORD.test(key))) {
|
|
@@ -433,8 +406,8 @@ export function checkAndroidKeystore(uses, secretKeys, unread = { ok: true, valu
|
|
|
433
406
|
}
|
|
434
407
|
return {
|
|
435
408
|
...META.androidKeystore,
|
|
436
|
-
...warn(`${nameList(withoutPassphrase.map((c) => c.lane))} hands gradle a keystore,
|
|
437
|
-
"
|
|
409
|
+
...warn(`${nameList(withoutPassphrase.map((c) => c.lane))} hands gradle a keystore, and no passphrase ` +
|
|
410
|
+
"is in the vault: gradle asks for it and waits.", STORE_KEYSTORE, "signing"),
|
|
438
411
|
};
|
|
439
412
|
}
|
|
440
413
|
/** The two names for the same action. `supply` is the older one. */
|
|
@@ -444,13 +417,9 @@ const PLAY_JSON_KEY = /^SUPPLY_JSON_KEY/;
|
|
|
444
417
|
/** The same credential, named in the call instead of in the vault. */
|
|
445
418
|
const PLAY_KEY_ARGS = ["json_key", "json_key_data"];
|
|
446
419
|
/** Named once: three outcomes of the environment check point at the same thing. */
|
|
447
|
-
const DECLARE_SECRETS = "A variable
|
|
448
|
-
"
|
|
449
|
-
|
|
450
|
-
const STORE_PLAY_KEY = "Upload the service account JSON as a Play Store service account block from the signing tab. " +
|
|
451
|
-
"Laneyard writes the file for the length of a run and exports its path under the name the block " +
|
|
452
|
-
"carries — `SUPPLY_JSON_KEY`, which supply reads by itself, unless the block says otherwise. " +
|
|
453
|
-
"A service account does not expire on its own.";
|
|
420
|
+
const DECLARE_SECRETS = "A variable a shelled-out tool reads — `sentry-cli` and its `SENTRY_AUTH_TOKEN` — is not in the " +
|
|
421
|
+
"Fastfile. List those under `required_secrets` in laneyard.yml.";
|
|
422
|
+
const STORE_PLAY_KEY = "Upload the service account JSON as a Play Store block on the signing tab.";
|
|
454
423
|
export function checkPlayStore(uses, secretKeys, appfile, unread = { ok: true, value: READ_EVERYTHING }, blocks = []) {
|
|
455
424
|
if (!uses.ok) {
|
|
456
425
|
return { ...META.playStore, ...undetermined(`could not read the lanes: ${uses.reason}`) };
|
|
@@ -465,8 +434,7 @@ export function checkPlayStore(uses, secretKeys, appfile, unread = { ok: true, v
|
|
|
465
434
|
if (blind) {
|
|
466
435
|
return {
|
|
467
436
|
...META.playStore,
|
|
468
|
-
...undetermined(`no lane seen
|
|
469
|
-
"as no lane uploading.", STORE_PLAY_KEY),
|
|
437
|
+
...undetermined(`no lane seen uploading to the Play Store — but ${blind}.`, STORE_PLAY_KEY),
|
|
470
438
|
};
|
|
471
439
|
}
|
|
472
440
|
return { ...META.playStore, ...ok("no lane uploads to the Play Store.") };
|
|
@@ -474,14 +442,10 @@ export function checkPlayStore(uses, secretKeys, appfile, unread = { ok: true, v
|
|
|
474
442
|
// Either route answers: the block, or the loose variable a project stored
|
|
475
443
|
// before blocks existed and which fastlane still reads exactly as it did.
|
|
476
444
|
if (blocks.includes("play_service_account")) {
|
|
477
|
-
return {
|
|
478
|
-
...META.playStore,
|
|
479
|
-
...ok("a Play Store service account block is in the vault: the JSON is written for the length of " +
|
|
480
|
-
"a run and its path exported."),
|
|
481
|
-
};
|
|
445
|
+
return { ...META.playStore, ...ok("a service account block is in the vault.") };
|
|
482
446
|
}
|
|
483
447
|
if (secretKeys.some((key) => PLAY_JSON_KEY.test(key))) {
|
|
484
|
-
return { ...META.playStore, ...ok("a
|
|
448
|
+
return { ...META.playStore, ...ok("a service account is in the vault.") };
|
|
485
449
|
}
|
|
486
450
|
// Named in the call, whether or not the value could be read. `json_key:
|
|
487
451
|
// ENV.fetch("…")` is a credential the lane supplies itself just as much as a
|
|
@@ -493,7 +457,7 @@ export function checkPlayStore(uses, secretKeys, appfile, unread = { ok: true, v
|
|
|
493
457
|
// machine. Neither a tick nor a warning would be honest.
|
|
494
458
|
return {
|
|
495
459
|
...META.playStore,
|
|
496
|
-
...undetermined(`${nameList(named.map((c) => c.lane))} supplies its own service account
|
|
460
|
+
...undetermined(`${nameList(named.map((c) => c.lane))} supplies its own service account — whether the file ` +
|
|
497
461
|
"it names is on this machine is not something a Fastfile says.", STORE_PLAY_KEY),
|
|
498
462
|
};
|
|
499
463
|
}
|
|
@@ -506,28 +470,27 @@ export function checkPlayStore(uses, secretKeys, appfile, unread = { ok: true, v
|
|
|
506
470
|
return {
|
|
507
471
|
...META.playStore,
|
|
508
472
|
...undetermined("the Appfile sets `json_key_data`: the credential travels with the repository, and " +
|
|
509
|
-
"
|
|
473
|
+
"nothing says it is still valid.", STORE_PLAY_KEY),
|
|
510
474
|
};
|
|
511
475
|
}
|
|
512
476
|
if (jsonKeyFile.kind === "literal") {
|
|
513
477
|
return {
|
|
514
478
|
...META.playStore,
|
|
515
|
-
...undetermined(`the Appfile points \`json_key_file\` at ${jsonKeyFile.value}
|
|
516
|
-
"this machine is
|
|
479
|
+
...undetermined(`the Appfile points \`json_key_file\` at ${jsonKeyFile.value} — whether that file is on ` +
|
|
480
|
+
"this machine is another matter.", STORE_PLAY_KEY),
|
|
517
481
|
};
|
|
518
482
|
}
|
|
519
483
|
if (jsonKeyFile.kind === "computed") {
|
|
520
484
|
return {
|
|
521
485
|
...META.playStore,
|
|
522
|
-
...undetermined("the Appfile
|
|
523
|
-
"most likely — which a run only resolves once it is running.", STORE_PLAY_KEY),
|
|
486
|
+
...undetermined("the Appfile computes `json_key_file`: only a run resolves it.", STORE_PLAY_KEY),
|
|
524
487
|
};
|
|
525
488
|
}
|
|
526
489
|
}
|
|
527
490
|
return {
|
|
528
491
|
...META.playStore,
|
|
529
|
-
...warn(`${nameList(calls.map((c) => c.lane))} uploads to the Play Store,
|
|
530
|
-
"the vault: supply stops and asks which credentials to use.", STORE_PLAY_KEY, "signing"),
|
|
492
|
+
...warn(`${nameList(calls.map((c) => c.lane))} uploads to the Play Store, and no service account is ` +
|
|
493
|
+
"in the vault: supply stops and asks which credentials to use.", STORE_PLAY_KEY, "signing"),
|
|
531
494
|
};
|
|
532
495
|
}
|
|
533
496
|
/**
|
|
@@ -541,18 +504,16 @@ function platformNote(platforms) {
|
|
|
541
504
|
return {
|
|
542
505
|
...META.platforms,
|
|
543
506
|
...undetermined(platforms.ok
|
|
544
|
-
? "no
|
|
545
|
-
"
|
|
546
|
-
: `could not tell: ${platforms.reason}. Only the checks above apply.`, "Add `platforms: [ios]`, `[android]` or both to laneyard.yml
|
|
547
|
-
"and the checks for that platform appear here."),
|
|
507
|
+
? "no Xcode project and no Gradle build in the repository, and laneyard.yml names none. " +
|
|
508
|
+
"Only the checks above apply."
|
|
509
|
+
: `could not tell: ${platforms.reason}. Only the checks above apply.`, "Add `platforms: [ios]`, `[android]` or both to laneyard.yml."),
|
|
548
510
|
};
|
|
549
511
|
}
|
|
550
512
|
/**
|
|
551
513
|
* The one question the block's form can settle that nothing else can. Asked
|
|
552
514
|
* here, answered there, and no file in the repository is touched either way.
|
|
553
515
|
*/
|
|
554
|
-
const SET_PROPERTIES_PATH = "Set the properties file path on the keystore block, relative to the app
|
|
555
|
-
"the file there for the length of a run.";
|
|
516
|
+
const SET_PROPERTIES_PATH = "Set the properties file path on the keystore block, relative to the app.";
|
|
556
517
|
/**
|
|
557
518
|
* Where the build resolves the properties file, as a clause a sentence can take.
|
|
558
519
|
*
|
|
@@ -612,10 +573,7 @@ export function checkReleaseSigning(input) {
|
|
|
612
573
|
};
|
|
613
574
|
}
|
|
614
575
|
if (!gradle.value.releaseCanUseDebugKey) {
|
|
615
|
-
return {
|
|
616
|
-
...META.releaseSigning,
|
|
617
|
-
...ok("the release build type never takes the debug signing config."),
|
|
618
|
-
};
|
|
576
|
+
return { ...META.releaseSigning, ...ok("the release build never takes the debug config.") };
|
|
619
577
|
}
|
|
620
578
|
const { conditionalOn } = gradle.value;
|
|
621
579
|
if (conditionalOn) {
|
|
@@ -626,23 +584,18 @@ export function checkReleaseSigning(input) {
|
|
|
626
584
|
// convention — the Flutter documentation's — and the one thing between a
|
|
627
585
|
// signed artifact and a debug-signed one that nobody read out of the script.
|
|
628
586
|
if (writes !== null && keystore !== null) {
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
`the keys ${keys}. Those names come from the block's settings rather than something the ` +
|
|
632
|
-
"build script states — what a script reads out of that file is not written anywhere this " +
|
|
633
|
-
"can see — so the setting starts from the Flutter documentation's four, and a build that " +
|
|
634
|
-
"reads others is corrected there.";
|
|
587
|
+
const supplied = `Laneyard writes ${name}${writes} for each run, with the keys ` +
|
|
588
|
+
`${nameList(keystore.propertyNames)}.`;
|
|
635
589
|
if (!conditionalFilePresent) {
|
|
636
590
|
return {
|
|
637
591
|
...META.releaseSigning,
|
|
638
|
-
...ok(
|
|
639
|
-
`${name} is not in the clone. ${supplied}`),
|
|
592
|
+
...ok(`${name} is not in the clone, so the debug config would apply. ${supplied}`),
|
|
640
593
|
};
|
|
641
594
|
}
|
|
642
595
|
return {
|
|
643
596
|
...META.releaseSigning,
|
|
644
|
-
...ok(`${name} is present${where(conditionalOn)}, so the release key is used
|
|
645
|
-
`
|
|
597
|
+
...ok(`${name} is present${where(conditionalOn)}, so the release key is used. Were it to go ` +
|
|
598
|
+
`missing: ${supplied}`),
|
|
646
599
|
};
|
|
647
600
|
}
|
|
648
601
|
// A block, and nowhere to put the file: the script names it on a receiver
|
|
@@ -653,37 +606,33 @@ export function checkReleaseSigning(input) {
|
|
|
653
606
|
if (keystore !== null && !conditionalFilePresent) {
|
|
654
607
|
return {
|
|
655
608
|
...META.releaseSigning,
|
|
656
|
-
...undetermined(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
"not guess where to write it.", SET_PROPERTIES_PATH, "signing"),
|
|
609
|
+
...undetermined(`${name} is not in the clone, so the debug config would apply. A keystore block is in ` +
|
|
610
|
+
`the vault, but the build script names ${name} without saying which directory it ` +
|
|
611
|
+
"resolves against.", SET_PROPERTIES_PATH, "signing"),
|
|
660
612
|
};
|
|
661
613
|
}
|
|
662
614
|
if (!conditionalFilePresent) {
|
|
663
615
|
const looksIn = where(conditionalOn);
|
|
664
616
|
return {
|
|
665
617
|
...META.releaseSigning,
|
|
666
|
-
...warn(
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
".
|
|
670
|
-
|
|
671
|
-
"
|
|
672
|
-
`Laneyard then writes ${name} itself for the length of a run, and removes it ` +
|
|
673
|
-
"afterwards. Nothing in the build script changes.", "signing"),
|
|
618
|
+
...warn(`${name} is not in the clone` +
|
|
619
|
+
(looksIn === "" ? "" : `, and the build looks for it${looksIn}`) +
|
|
620
|
+
". The build will not fail: it produces an artifact signed with the debug key, and the " +
|
|
621
|
+
"store rejects it.", `${name} is gitignored, so it never reaches a clone. Upload the keystore as an android ` +
|
|
622
|
+
`keystore block and Laneyard writes ${name} for each run. Nothing in the build script ` +
|
|
623
|
+
"changes.", "signing"),
|
|
674
624
|
};
|
|
675
625
|
}
|
|
676
626
|
return {
|
|
677
627
|
...META.releaseSigning,
|
|
678
|
-
...undetermined(`${conditionalOn.name} is present${where(conditionalOn)}, so the release key is ` +
|
|
679
|
-
"
|
|
680
|
-
"and does so without failing.", keystore === null ? STORE_KEYSTORE : SET_PROPERTIES_PATH),
|
|
628
|
+
...undetermined(`${conditionalOn.name} is present${where(conditionalOn)}, so the release key is used — but ` +
|
|
629
|
+
"the build falls back to the debug config if it goes missing, without failing.", keystore === null ? STORE_KEYSTORE : SET_PROPERTIES_PATH),
|
|
681
630
|
};
|
|
682
631
|
}
|
|
683
632
|
return {
|
|
684
633
|
...META.releaseSigning,
|
|
685
|
-
...undetermined("the release build
|
|
686
|
-
"
|
|
634
|
+
...undetermined("the release build can take the debug config; whether it does is decided somewhere this " +
|
|
635
|
+
"cannot read."),
|
|
687
636
|
};
|
|
688
637
|
}
|
|
689
638
|
/**
|
|
@@ -727,7 +676,7 @@ export function checkEnvironment(input) {
|
|
|
727
676
|
if (blind) {
|
|
728
677
|
return {
|
|
729
678
|
...META.environment,
|
|
730
|
-
...undetermined(`no lane seen
|
|
679
|
+
...undetermined(`no lane seen reading an environment variable — but ${blind}.`, DECLARE_SECRETS),
|
|
731
680
|
};
|
|
732
681
|
}
|
|
733
682
|
return { ...META.environment, ...ok("no lane reads an environment variable.") };
|
|
@@ -742,24 +691,20 @@ export function checkEnvironment(input) {
|
|
|
742
691
|
return {
|
|
743
692
|
...META.environment,
|
|
744
693
|
...warn(`${nameList(missing)} ${missing.length === 1 ? "is" : "are"} needed by a lane and in ` +
|
|
745
|
-
"neither the vault nor this server's environment: a run stops at the first one,
|
|
746
|
-
"builds with it empty.", "Store them from the secrets tab. A `fastlane/.env` does not travel — it is almost " +
|
|
747
|
-
"always gitignored, so it never reaches the clone a build runs from.", "secrets"),
|
|
694
|
+
"neither the vault nor this server's environment: a run stops at the first one.", "Store them on the secrets tab. A `fastlane/.env` is gitignored, so it never reaches the clone.", "secrets"),
|
|
748
695
|
};
|
|
749
696
|
}
|
|
750
697
|
const borrowed = required.filter((name) => !inVault.has(name));
|
|
751
698
|
if (borrowed.length > 0) {
|
|
752
699
|
return {
|
|
753
700
|
...META.environment,
|
|
754
|
-
...ok(`every variable
|
|
755
|
-
`${borrowed.length === 1 ? "comes" : "come"} from this server's
|
|
756
|
-
|
|
701
|
+
...ok(`every variable is available — though ${nameList(borrowed)} ` +
|
|
702
|
+
`${borrowed.length === 1 ? "comes" : "come"} from this server's environment, not the ` +
|
|
703
|
+
"vault, so another machine would not find " +
|
|
704
|
+
`${borrowed.length === 1 ? "it" : "them"}.`),
|
|
757
705
|
};
|
|
758
706
|
}
|
|
759
|
-
return {
|
|
760
|
-
...META.environment,
|
|
761
|
-
...ok(`every variable the lanes need is in the vault: ${nameList(required)}.`),
|
|
762
|
-
};
|
|
707
|
+
return { ...META.environment, ...ok(`every variable is in the vault: ${nameList(required)}.`) };
|
|
763
708
|
}
|
|
764
709
|
/**
|
|
765
710
|
* The checklist itself: a table, so that adding a check is adding a row.
|
package/dist/src/main.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { mkdir } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
6
|
import { bold, dim, field, heading } from "./cli/style.js";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { PromptAborted } from "./cli/prompt.js";
|
|
@@ -22,7 +22,28 @@ import { Vault } from "./secrets/vault.js";
|
|
|
22
22
|
import { makeInvoke } from "./sidecar/bridge.js";
|
|
23
23
|
import { LaneReader } from "./sidecar/lanes.js";
|
|
24
24
|
import { UsesReader } from "./sidecar/uses.js";
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The version, from `package.json` rather than a constant beside it. The
|
|
27
|
+
* constant this replaced was bumped by hand and drifted: a release moved
|
|
28
|
+
* `package.json` to 0.6.0 and left `laneyard --version` still saying 0.5.0.
|
|
29
|
+
*
|
|
30
|
+
* Walked up from this module so it resolves both from `dist/src` (installed) and
|
|
31
|
+
* `src` (dev under tsx), and finds the package's own `package.json` — the nearest
|
|
32
|
+
* ancestor — never a host project's when Laneyard is a dependency.
|
|
33
|
+
*/
|
|
34
|
+
function readVersion() {
|
|
35
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
for (let up = 0; up < 6; up += 1, dir = dirname(dir)) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Not this directory — try its parent.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return "0.0.0";
|
|
45
|
+
}
|
|
46
|
+
export const version = readVersion();
|
|
26
47
|
/** Assembles the server from a data folder. */
|
|
27
48
|
export async function createServerFromConfig(root) {
|
|
28
49
|
const config = new ConfigStore(join(root, "config.yml"));
|
|
@@ -4,6 +4,7 @@ import { gitEnvFor, Workspace } from "../git/workspace.js";
|
|
|
4
4
|
import { summarizeFailure } from "../heuristics/error-summary.js";
|
|
5
5
|
import { appRootOf, searchDir } from "../heuristics/platforms.js";
|
|
6
6
|
import { Redactor, scrub } from "../logs/redact.js";
|
|
7
|
+
import { assertFastlaneDir } from "../sidecar/fastlane-dir.js";
|
|
7
8
|
import { collectArtifacts } from "./artifacts.js";
|
|
8
9
|
import { removeGradleProperties, sweepGradleProperties, writeGradleProperties, } from "./gradle-properties.js";
|
|
9
10
|
import { LiveStepTracker } from "./live-steps.js";
|
|
@@ -122,10 +123,24 @@ async function execute(opts, wrote) {
|
|
|
122
123
|
// the next run is the first moment anything can. Its failure is not reported
|
|
123
124
|
// — a leftover that resists removal is a broken disk, not a broken build, and
|
|
124
125
|
// saying so here would fail a run for a file it has not needed yet.
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
//
|
|
127
|
+
// The app's own directory, which is the parent of the configured fastlane
|
|
128
|
+
// folder: the repository root for a plain app, `app/` for one in a monorepo.
|
|
129
|
+
// It is where the properties file goes and, below, where fastlane is started.
|
|
130
|
+
const appRoot = searchDir(opts.workspacePath, appRootOf(settings.fastlane_dir));
|
|
131
|
+
// Before any of that, and before fastlane: a clone without the folder is the
|
|
132
|
+
// one failure fastlane reports worst. It asks whether to set a project up,
|
|
133
|
+
// finds nobody to answer, and prints a Ruby backtrace and a list of GitHub
|
|
134
|
+
// issues over the sentence that mattered. This says it in one line instead.
|
|
135
|
+
try {
|
|
136
|
+
await assertFastlaneDir(opts.workspacePath, settings.fastlane_dir);
|
|
137
|
+
}
|
|
138
|
+
catch (cause) {
|
|
139
|
+
return fail(cause.message);
|
|
140
|
+
}
|
|
141
|
+
await sweepGradleProperties(appRoot, opts.androidKeystore).catch(() => { });
|
|
127
142
|
try {
|
|
128
|
-
wrote.properties = await writeGradleProperties(
|
|
143
|
+
wrote.properties = await writeGradleProperties(appRoot, opts.androidKeystore);
|
|
129
144
|
}
|
|
130
145
|
catch (cause) {
|
|
131
146
|
// Failing the run rather than carrying on. Carrying on is what produces the
|
|
@@ -162,7 +177,13 @@ async function execute(opts, wrote) {
|
|
|
162
177
|
args: useBundle
|
|
163
178
|
? ["exec", "fastlane", ...laneArgs(opts)]
|
|
164
179
|
: laneArgs(opts),
|
|
165
|
-
|
|
180
|
+
// The app's directory, not the repository root. fastlane looks for
|
|
181
|
+
// `fastlane/` in the directory it was started from and nowhere else — no
|
|
182
|
+
// walking up, no looking down — so a project whose Fastfile sits in
|
|
183
|
+
// `app/fastlane` has to be started from `app`. Started from the root it
|
|
184
|
+
// finds nothing and offers to set fastlane up, which on a build server
|
|
185
|
+
// means a crash: there is nobody there to answer the question.
|
|
186
|
+
cwd: appRoot,
|
|
166
187
|
env: {
|
|
167
188
|
...opts.env,
|
|
168
189
|
...(opts.secrets ?? {}),
|