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.
@@ -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 asking for credentials.") };
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), "Give the project a key it can use without a passphrase " +
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 Gemfile's bundle is installed.") };
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 project's workspace."),
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, and no fastlane on the PATH: a run has nothing to execute.", 'Add a Gemfile with `gem "fastlane"` and run `bundle install`, or install fastlane system-wide.'),
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 from the signing tab, with its key id and " +
123
- "issuer id. Laneyard writes the file for the length of a run and exports the three of them under " +
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 App Store Connect API key is in the vault.") };
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 in the vault, and no action in fastlane reads a variable " +
206
- "of that name: the value is stored, and no lane can see it. An earlier version of this " +
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 API key: whether the \`.p8\` it ` +
220
- "names is on this machine is not something a Fastfile says.", STORE_API_KEY),
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 to sign in to App Store Connect — but ${blind}.`, STORE_API_KEY),
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)}: a key is arranged, but nothing here says a lane ` +
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: Apple sessions expire, and the run that finds it expired " +
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: a lane that uploads signs in as that " +
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 App Store Connect credential in the vault or in the repository, and none seen in the ` +
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 App Store Connect credential in the vault, in the lanes, or in the repository: " +
282
- "a lane that uploads will ask for an Apple ID.", STORE_API_KEY, "signing"),
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, but MATCH_PASSWORD is not in the vault: ` +
306
- "match asks for the passphrase and waits for it.", "Store `MATCH_PASSWORD` from the secrets tab.", "secrets"),
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\`: it may try to ` +
314
- "create certificates, which needs an Apple account interactively.", "Pass `readonly: true` so it only fetches what already exists."),
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 literal arguments only, and will not guess.`, "Pass `readonly: true` in the call itself if that is what you mean."),
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 to call an action known to stop and ask — but ${blind}, so a lane that ` +
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 from the signing tab, with its alias and its " +
374
- "two passphrases. Laneyard writes the file for the length of a run and exports the names the " +
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
- ". This project builds for Android, so signing is configured somewhere a Fastfile does " +
406
- "not show: `build.gradle`, `key.properties`, or a build driven through flutter or " +
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: nothing here needs unlocking."),
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, but no keystore ` +
437
- "passphrase is in the vault: gradle asks for it and waits.", STORE_KEYSTORE, "signing"),
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 read by a tool the lane shells out to — `sentry-cli` and its " +
448
- "`SENTRY_AUTH_TOKEN`, say — is not something a Fastfile mentions. List those under " +
449
- "`required_secrets` in laneyard.yml and they are checked like the rest.";
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 to upload to the Play Store — but ${blind}, so this is not the same ` +
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 Play Store service account is in the vault.") };
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: whether the file ` +
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
- "whether it is still valid is not something a file says.", STORE_PLAY_KEY),
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}: whether that file is on ` +
516
- "this machine is not something an Appfile says.", STORE_PLAY_KEY),
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 sets `json_key_file` to something it computes an environment variable, " +
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, but no service account is in ` +
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 platform detected: no Xcode project and no Gradle build in the repository, " +
545
- "and laneyard.yml names none. Only the checks above apply."
546
- : `could not tell: ${platforms.reason}. Only the checks above apply.`, "Add `platforms: [ios]`, `[android]` or both to laneyard.yml in the repository, " +
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, and Laneyard writes " +
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 keys = nameList(keystore.propertyNames);
630
- const supplied = `Laneyard writes ${name}${writes} for the length of a run, from the keystore block, with ` +
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(`the release build falls back to the debug signing config when ${name} is missing, and ` +
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 and that file is ` +
645
- `the project's own, which Laneyard leaves alone. Were it ever to go missing, ${supplied}`),
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(`the release build falls back to the debug signing config when ${name} is missing, and ` +
657
- `${name} is not in the clone. A keystore block is in the vault, but the build script ` +
658
- `names ${name} without saying which directory it resolves against, so Laneyard will ` +
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(`the release build falls back to the debug signing config when ${name} is missing, and ` +
667
- `${name} is not in the clone` +
668
- (looksIn === "" ? "" : ` the build looks for it${looksIn}`) +
669
- ". The build will not fail: it will produce an artifact signed with the debug key, and " +
670
- "the store will reject it.", `${name} is gitignored by the same documentation that recommends it, so it never reaches ` +
671
- "a clone. Upload the keystore as an android keystore block from the signing tab: " +
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
- "used — but the build falls back to the debug signing config if it ever goes missing, " +
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 type can take the debug signing config; whether it does is decided by " +
686
- "something this cannot read."),
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 to read an environment variable — but ${blind}.`, DECLARE_SECRETS),
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, or " +
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 the lanes need is available — though ${nameList(borrowed)} ` +
755
- `${borrowed.length === 1 ? "comes" : "come"} from this server's own environment rather ` +
756
- `than the vault, so another machine would not find ${borrowed.length === 1 ? "it" : "them"}.`),
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
- export const version = "0.5.0";
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
- const androidRoot = searchDir(opts.workspacePath, appRootOf(settings.fastlane_dir));
126
- await sweepGradleProperties(androidRoot, opts.androidKeystore).catch(() => { });
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(androidRoot, opts.androidKeystore);
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
- cwd: opts.workspacePath,
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 ?? {}),