laneyard 0.6.1 → 0.8.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.
Files changed (36) hide show
  1. package/README.md +13 -497
  2. package/dist/src/cli/adopt.js +24 -8
  3. package/dist/src/cli/remove.js +52 -34
  4. package/dist/src/cli/reset.js +2 -2
  5. package/dist/src/cli/setup.js +85 -40
  6. package/dist/src/cli/uninstall.js +5 -21
  7. package/dist/src/config/env-file-setting.js +57 -0
  8. package/dist/src/config/resolve.js +8 -5
  9. package/dist/src/config/schema.js +29 -0
  10. package/dist/src/db/credentials.js +18 -54
  11. package/dist/src/db/migrate-global-scope.js +75 -0
  12. package/dist/src/db/open.js +21 -0
  13. package/dist/src/db/schema.sql +10 -5
  14. package/dist/src/db/secrets.js +44 -65
  15. package/dist/src/fastfile/adoption.js +19 -4
  16. package/dist/src/git/workspace.js +28 -0
  17. package/dist/src/heuristics/readiness.js +80 -135
  18. package/dist/src/main.js +31 -2
  19. package/dist/src/runner/env-file.js +144 -0
  20. package/dist/src/runner/orchestrate.js +39 -5
  21. package/dist/src/secrets/vault.js +75 -40
  22. package/dist/src/server/app.js +4 -0
  23. package/dist/src/server/routes/credentials.js +0 -13
  24. package/dist/src/server/routes/fastfile.js +1 -1
  25. package/dist/src/server/routes/projects.js +59 -16
  26. package/dist/src/server/routes/readiness.js +2 -3
  27. package/dist/src/server/routes/secrets.js +111 -39
  28. package/dist/src/sidecar/fastlane-dir.js +18 -8
  29. package/dist/src/sidecar/lanes.js +1 -1
  30. package/dist/src/sidecar/uses.js +1 -1
  31. package/dist/web/assets/{Editor-CMa4-4N3.js → Editor-BK-ZRKlI.js} +1 -1
  32. package/dist/web/assets/index-DnUdqjT4.js +62 -0
  33. package/dist/web/assets/{index-DsjxZtsO.css → index-r3IyRuO4.css} +1 -1
  34. package/dist/web/index.html +2 -2
  35. package/package.json +1 -1
  36. package/dist/web/assets/index-B8lAQPEM.js +0 -62
package/README.md CHANGED
@@ -86,502 +86,18 @@ asked your project's own fastlane for them.
86
86
 
87
87
  ![The project screen: lanes read from the Fastfile, with their descriptions, and the run history](https://raw.githubusercontent.com/martinfrouin/laneyard/main/docs/screenshots/project.png)
88
88
 
89
- ### What setup does about a credential your Fastfile names outright
89
+ Setup has a second act: it names any credential your Fastfile hardcodes — a path only your laptop
90
+ has, a key written inline — and offers, refusably, to lift it into the vault and patch the file.
91
+ See [Credentials](docs/credentials.md).
90
92
 
91
- `laneyard setup` has a second act, after `Project "x" is set up` prints. The order is the guarantee:
92
- everything below is offered to a project that already works, so declining it all costs nothing.
93
+ ## Documentation
93
94
 
94
- It looks for a credential named by a literal value:
95
-
96
- ```ruby
97
- upload_to_play_store(json_key: "./play-service-account.json", track: "beta")
98
- ```
99
-
100
- That line builds on the laptop it was written on and nowhere else. Laneyard builds from a clone of
101
- your remote, so the JSON is either gitignored (absent from the clone — the run fails) or committed (a
102
- service account key in your history). Neither is something a checklist can fix later.
103
-
104
- So setup reads the Fastfile with Prism and offers — one at a time, refusably — to lift each
105
- credential it recognises into the vault and replace the literal with `ENV.fetch(…)`. It reads the
106
- syntax tree, not text, because `json_key:` also appears in comments and strings: a wrong patch to a
107
- build file is the worst thing this feature could do.
108
-
109
- Three kinds, by how sure the reading is:
110
-
111
- - **a path to a credential file** — `key_filepath:` on `app_store_connect_api_key`, `json_key:` on
112
- `supply`/`upload_to_play_store`/`validate_play_store_json_key` — offered only when the path
113
- resolves to a file on disk. Defaults to yes. A `key_id:` or `issuer_id:` written beside an adopted
114
- key is carried with it, rewritten to the variable the block exports and used to pre-fill its fields;
115
- - **the credential's contents, inline** — `key_content:`, `json_key_data:`, a private key in
116
- cleartext. Defaults to yes. The patch renames the keyword too — `key_content:` becomes
117
- `key_filepath:`, `json_key_data:` becomes `json_key:` — because a stored block is exported as a
118
- *path*, not text;
119
- - **an argument that looks like a secret** — a literal ending in `token`, `password`, `secret`,
120
- `api_key` or `url`. **Defaults to no**, value masked: it is the one kind where a false positive is
121
- likely, and patching a non-secret by default is a silent regression.
122
-
123
- The credential args of `app_store_connect_api_key` and Play — `key_id`, `issuer_id`, `key_filepath`,
124
- `json_key` — are also normalised when they already read a variable: `issuer_id: ENV["ASC_ISSUER_ID"]`
125
- becomes `ENV.fetch("APP_STORE_CONNECT_API_KEY_ISSUER_ID")`, the name a signing block exports. Store
126
- the `.p8`/JSON once as a block and the old `ASC_*` secrets can go. A value already reading the right
127
- name is left alone.
128
-
129
- The vault is written before the Fastfile: if lifting fails, nothing has been patched to read a
130
- missing variable. The patch is spliced by byte offset — everything outside the replaced range is
131
- byte-identical — and the file is re-parsed before the command returns; if it no longer parses, the
132
- previous content is restored.
133
-
134
- ```diff
135
- - upload_to_play_store(json_key: "./play-service-account.json", track: "beta")
136
- + upload_to_play_store(json_key: ENV.fetch("SUPPLY_JSON_KEY"), track: "beta")
137
- ```
138
-
139
- `ENV.fetch` rather than `ENV[]`: a missing variable fails at the top of the lane and names itself,
140
- instead of reaching an action as `nil`.
141
-
142
- **Setup does not commit or push.** It prints the `git diff` command and stops — the working copy is
143
- yours. So **a patched Fastfile changes nothing until you push it**: Laneyard builds from the remote,
144
- and until the commit is there, every run still reads the old path. It also does not take the
145
- credential out of your history; where `git ls-files` finds the file, it says so — rotating the key is
146
- the fix.
147
-
148
- Two things it leaves alone: a value written as a heredoc (its reported location is the marker, not
149
- the text, so patching would corrupt the file), and the Android keystore (configured in Gradle, not
150
- the Fastfile — handled under Signing credentials below).
151
-
152
- Declining writes nothing, anywhere. And where nothing can run Prism — a Mac whose only Ruby is the
153
- system 2.6 — setup prints `Fastfile not analysed …` and finishes as always. The scan is a service,
154
- never a gate.
155
-
156
- ## Configuration
157
-
158
- All configuration lives in files. The database holds execution state only, so backing up
159
- Laneyard means copying one file, and restoring it means copying it back.
160
-
161
- ### `~/.laneyard/config.yml` — the server and its projects
162
-
163
- ```yaml
164
- server:
165
- port: 7890
166
- bind: 0.0.0.0
167
- users: # written by `laneyard setup`, see Accounts
168
- - { name: martin, role: admin, password_hash: "scrypt$…" }
169
- - { name: lea, role: builder, password_hash: "scrypt$…", projects: [cartes-ios] }
170
- max_concurrent_runs: 1 # only 1 is accepted, see below
171
- retention: { runs: 50, artifact_days: 30 }
172
-
173
- projects:
174
- - slug: cartes-ios
175
- name: Cartes iOS
176
- git_url: git@github.com:you/cartes.git
177
- default_branch: main
178
- git_auth: { kind: ssh_key, ref: ~/.ssh/id_ed25519 }
179
- ```
180
-
181
- `max_concurrent_runs` accepts `1` only. Runs drain from a single queue, one at a time across every
182
- project — parallel runs would need a working directory per run, which does not exist yet. A larger
183
- number is refused at load, so a server is never configured for builds that never happen.
184
-
185
- ### Accounts
186
-
187
- Everyone who signs in has a name, a password and one of two roles — two, because a third is easy to
188
- add and impossible to remove.
189
-
190
- | | **admin** | **builder** |
191
- |---|---|---|
192
- | start a build, watch it, cancel it | ✓ | ✓ |
193
- | download artifacts, read logs and the Fastfile | ✓ | ✓ |
194
- | see the readiness checklist | ✓ | ✓ |
195
- | read and write secrets | ✓ | |
196
- | save, commit and push the Fastfile | ✓ | |
197
- | remove a project | ✓ | |
198
- | manage accounts | ✓ | |
199
-
200
- A builder is who you give someone who ships without being trusted with the signing chain: they press
201
- the button and watch, and never see a credential.
202
-
203
- The interface shows a builder only what a builder can use — no secrets, fastfile, settings or
204
- accounts tabs. That is courtesy, not security: the server refuses those routes on its own, whatever
205
- the browser was shown, and the test suite proves it for every verb and every spelling of the address.
206
-
207
- #### Which projects a builder reaches
208
-
209
- An admin reaches every project. A builder reaches only the projects it is granted, from the accounts
210
- screen. A project a builder cannot reach is **invisible**, not shown-and-locked — absent from its
211
- lists and a 404 by URL, answered with the body a nonexistent project gives, so the two cannot be told
212
- apart. Enforced by the server, in one place, not the browser.
213
-
214
- The reach is a `projects` list on the account in `config.yml`, with three states:
215
-
216
- - **absent** — every project. A config written before this feature grants everyone, so nobody loses
217
- access on an upgrade.
218
- - **`[]`** — no project. What a new account starts with, so a new builder sees nothing until granted.
219
- - **a list of slugs** — exactly those projects.
220
-
221
- Removing a project strips its slug from every account, so a grant never points at a project that is
222
- gone, and a re-created slug does not inherit an old grant.
223
-
224
- Add and remove accounts from the accounts screen, or from the command line:
225
-
226
- ```bash
227
- echo "$PASSWORD" | laneyard user add lea --role builder
228
- ```
229
-
230
- The password is read from standard input, never an argument — an argument lands in your shell
231
- history. Without `--role`, the account is a builder.
232
-
233
- Two things are refused, in the API and CLI alike: removing or demoting the last admin. A server
234
- nobody can administer cannot be repaired from the interface.
235
-
236
- Anyone changes their own password and name from **your account** — a builder included. Either asks
237
- for the current password even though you are signed in: a session is a cookie in a browser that may
238
- have been left open on a desk. Doing it ends every other session that account has. That is how the
239
- password `laneyard setup` printed once stops being a string on a sticky note.
240
-
241
- Changing your name edits your `config.yml` entry in place, keeping your role and access, and refuses
242
- a name another account already has. The next time you sign in, you type the new one — self-service,
243
- whatever your role.
244
-
245
- Removing an account ends its sessions immediately — "remove the account" and "revoke access" are the
246
- same act. So does editing `config.yml` by hand: every request looks the account up again, so a change
247
- takes effect at once rather than at the next restart.
248
-
249
- ### `laneyard.yml` — in your repository, and committed
250
-
251
- Build behaviour belongs next to the code, so it can be versioned with it — `laneyard setup`
252
- writes this file for you, and you should commit it. A colleague who clones the repository then
253
- builds it the same way, without configuring anything.
254
-
255
- ```yaml
256
- fastlane_dir: fastlane
257
- runtime: bundle # or `system`
258
- timeout_minutes: 60
259
- artifact_globs:
260
- - "build/**/*.ipa"
261
- - "build/**/*.app.dSYM.zip"
262
- platforms: [ios] # or `[android]`, or both
263
- ```
264
-
265
- `platforms` decides which half of the readiness checklist applies: an Android project is never
266
- asked for an App Store Connect key. Left out, Laneyard looks at the repository — an Xcode project
267
- means iOS, a Gradle build means Android — and reports what it found rather than assuming.
268
-
269
- It looks **beside the Fastfile**, not at the repository root, because that is where an app keeps its
270
- platform folders: `ios/` and `fastlane/` are siblings and move together when the app is one directory
271
- of a monorepo. When that guess is wrong, `platforms` settles it.
272
-
273
- Field by field, the repository file wins over the server block, which wins over the defaults. Any
274
- field may also go in the server block, so a repository you would rather not touch can be configured
275
- entirely from `config.yml`.
276
-
277
- **A monorepo carries one `laneyard.yml` per app**, in the app's own directory beside its fastlane
278
- folder, so two apps on one remote each describe their own build. Inside an app-level file **paths are
279
- relative to that file's directory** — `artifact_globs: ["**/*.aab"]`, a plain `fastlane_dir:
280
- fastlane` usually left out — so an app moved or duplicated keeps its file unchanged. A `laneyard.yml`
281
- at the repository root still works, with repo-root-relative paths.
282
-
283
- Both files are watched: edit them by hand and Laneyard picks the change up. An invalid file is
284
- reported and the last valid configuration stays live — a typo never takes the server down.
285
-
286
- ### Secrets
287
-
288
- The variables your lanes read go into an encrypted vault, from a project's Secrets tab — global
289
- secrets apply everywhere, a project's own win over them. The files it signs with go in the same
290
- vault as blocks (below).
291
-
292
- A secret is write-only: the server never sends a masked value back, so no browser ever holds one.
293
- What you did not mark secret — `APP_VERSION`, an issuer id — is shown on request, one key at a time;
294
- masking and unmasking switch which it is without retyping the value.
295
-
296
- Nothing in your lanes has to change afterwards: `key_filepath:` and `json_key:` keep working — a
297
- signing block puts a real file back on disk for the length of a run. A secret becomes an environment
298
- variable for every run of its project, kept out of the logs; a masked value must be at least four
299
- characters (see below).
300
-
301
- ### Signing credentials
302
-
303
- A signing credential is not a string: an Android keystore is bytes Gradle reads through a path, and a
304
- `.p8` is useless without its key id and issuer id. So these are stored as blocks — one file plus the
305
- fields that make it usable, taken whole or refused. A keystore stored without its alias is not a
306
- partial success; it is a build that fails in a month.
307
-
308
- | block | the file | the fields beside it |
309
- | ------------------------------ | ---------------------- | ------------------------------------------- |
310
- | *app store connect key* | `.p8` | key id, issuer id |
311
- | *android upload keystore* | `.jks` or `.keystore` | key alias, store password, key password |
312
- | *play store service account* | JSON | — |
313
-
314
- They live in the **signing** part of a project's Secrets tab. The file is encrypted at rest and never
315
- comes back out to a browser: a stored block shows its file name and nothing more, so replacing one
316
- means giving it again in full.
317
-
318
- A variable a block has made redundant is said to be so, beside the row and in one sentence:
319
- `SUPPLY_JSON_KEY_DATA` once a Play block applies — it still works, but the same credential is then
320
- stored twice — and `APP_STORE_CONNECT_API_KEY_P8`, which no action in fastlane has ever read.
321
- Neither is removed for you: the row is yours, and the button to drop it is on the same line.
322
-
323
- **A block becomes real files, for the length of a run.** Gradle's `storeFile` and
324
- `app_store_connect_api_key` both want a path, so a credential that exists only as ciphertext cannot
325
- be used. Each block that applies is written into `~/.laneyard/runs/<run id>/secrets/`, mode `600` in
326
- a `700` directory, removed when the run ends — passed, failed, cancelled or timed out. Every
327
- applicable block is written whether or not the lane looks like it needs one: a Fastfile can reach
328
- anything through `sh` or a plugin, and a wrong guess of "not needed" is a debug-signed artifact, not
329
- a missing variable.
330
-
331
- **It reaches your lanes under the names your project already reads.** Each form is pre-filled with
332
- the names fastlane declares — `APP_STORE_CONNECT_API_KEY_KEY_FILEPATH`/`_KEY_ID`/`_ISSUER_ID`,
333
- `SUPPLY_JSON_KEY` — and every one is editable. A Fastfile written around
334
- `ENV.fetch("ASC_KEY_FILEPATH")` is not doing it wrong: you say so on the form instead of renaming
335
- anything. The name stored is the only one exported — no default is emitted alongside, which would
336
- make a typo look like it worked. The keystore's names are Laneyard's own (`ANDROID_KEYSTORE_PATH`,
337
- `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`), same rule.
338
-
339
- **The keystore block can also supply `key.properties`.** Flutter's own build script signs with the
340
- release config when that file exists and the debug config when it does not, and gitignores it — so on
341
- a build server it is always absent. Rather than telling you to rewrite the script, Laneyard writes
342
- the file it looks for, for the length of the run, out of the keystore block. Two things it cannot
343
- read from the script are asked for on the block: where the file goes, and what the keys inside are
344
- called (starting from Flutter's four).
345
-
346
- **Only the projects that sign need any of this.** fastlane also takes screenshots, runs tests, syncs
347
- certificates — so the three blocks are an offer, not a gate. A project that just wants an artifact
348
- out of Gradle needs the keystore and nothing else.
349
-
350
- A block stored on a project belongs to that project. One stored globally applies to every project,
351
- and a project's own block wins over it. Both are admin-only, like the rest of the vault.
352
-
353
- ### Readiness
354
-
355
- Every project has a Readiness tab: what stands between it and a build that runs while nobody watches.
356
- Only the checks that apply are shown — an Android project is never asked for an App Store Connect key.
357
-
358
- **What a tick means.** The checks read your Fastfile, following a lane into the methods it defines.
359
- Two things stay out of reach: `import`/`import_from_git` pulls in lanes from elsewhere, and
360
- `fastlane/actions/` holds actions whose names mean nothing to a reader that has only seen the
361
- Fastfile. Where either applies, a check that found nothing says *could not tell* rather than ticking.
362
- A green tick means "looked, and it is fine" — never "looked, and saw nothing".
363
-
364
- Always:
365
-
366
- - **the repository** answers `git ls-remote` without asking for credentials — a run that meets a
367
- password prompt does not fail, it waits;
368
- - **dependencies** are installable: `bundle check` against your Gemfile, or the `fastlane` a run
369
- would otherwise find on the PATH;
370
- - **no lane calls an action known to stop and ask** — `prompt`, `sigh`, `cert`, a writable
371
- `match`, an upload waiting for its summary to be confirmed;
372
- - **the variables the lanes read** are in the vault. Every `ENV.fetch("…")` a lane reaches is
373
- collected and looked up — the commonest way a project that works on your laptop fails on a server:
374
- the variables live in a gitignored `fastlane/.env` that never reaches the clone.
375
-
376
- Two things a Fastfile cannot tell you. A variable read by a tool the lane shells out to —
377
- `sentry-cli` and `SENTRY_AUTH_TOKEN` — is named nowhere in the lanes; a committed
378
- `fastlane/.env.example` is read for exactly this, and `required_secrets` in `laneyard.yml` covers
379
- the rest. A variable found only in the server's own environment is reported, not ticked: it works,
380
- but because of how the server was started.
381
-
382
- A name a signing block exports counts as being in the vault, since that is where the block is:
383
- a lane reading `SUPPLY_JSON_KEY` with a Play block stored is not asked for it again, on this
384
- checklist or on the Secrets tab. The two read the same answer.
385
-
386
- On iOS:
387
-
388
- - **App Store Connect** has an API key. The vault is checked first and is the only thing that
389
- earns a tick — a signing block, or the variables a project stored before blocks existed, since
390
- fastlane reads those exactly as it did. A project that configured fastlane long before it met
391
- Laneyard keeps its key elsewhere, so the lanes are read for `app_store_connect_api_key` and for a `key_filepath` or
392
- `api_key_path` argument, and the repository for a `.p8`. Any of those is reported as *could not
393
- tell*, not as a warning: a path in a Fastfile says a key was arranged, not that the file is on
394
- this machine. An Appfile holding only an `apple_id` is a warning — that is the account
395
- two-factor authentication will stop the run to ask about. One name is called out rather than
396
- accepted: a value stored under `APP_STORE_CONNECT_API_KEY_P8` used to earn a tick here, and no
397
- action in fastlane has ever read a variable of that name — the check now says so, because being
398
- told to redo the work beats a screen that has quietly gone silent about it;
399
- - **match** has its `MATCH_PASSWORD` stored and is called `readonly`, so it fetches certificates
400
- instead of trying to create them.
401
-
402
- On Android:
403
-
404
- - **the keystore** is reachable without a prompt: a lane handing `gradle` a `storeFile` needs a
405
- passphrase, and one that is neither in the call nor in the vault makes gradle stop and ask. A
406
- keystore block settles this before the lanes are read at all — the file and both passphrases
407
- reach the run together, so nothing can stop and ask, whichever lane runs;
408
- - **the release is signed with the release key.** The one check whose failure is silent: Flutter's
409
- own snippet signs with the release config when `key.properties` exists and the *debug* config when
410
- it does not, and gitignores that file — so it is absent from every clone. The build then succeeds
411
- with a debug-signed artifact, and the store rejects it minutes later saying nothing about signing.
412
- This reads the Gradle file as text and says so before the build; and for a project whose keystore
413
- is stored here, writes the `key.properties` the build asks for, for the length of the run — marked
414
- `# written by laneyard, do not commit`, removed when fastlane stops. A file of your own without
415
- that marker is never touched;
416
- - **the Play Store service account** is there when a lane calls `upload_to_play_store`. The vault
417
- first — a block, or a `SUPPLY_JSON_KEY` variable stored before blocks existed — then the
418
- `json_key` argument in the call, then the **Appfile**: `json_key_file` and `json_key_data`, which
419
- is where a long-standing project almost always keeps it. Only the vault is a tick; the other two
420
- are *could not tell*, for the same reason as above.
421
-
422
- Like the iOS ones, the Android checks read **literal arguments only**. `gradle(storePassword:
423
- ENV["PW"])` is reported as undetermined, never guessed at: a checklist that guesses gets believed.
424
-
425
- They run when you open the tab or press refresh, never on their own — they shell out to git and
426
- bundler. The last run's time is on screen, because a stale green tick is worse than a red cross.
427
-
428
- Nothing here blocks anything. A red check is never why a run cannot start, and Laneyard never edits a
429
- Fastfile to make its own checklist green — each line explains, you decide. Where the fix is one
430
- action, the line links to the Secrets tab.
431
-
432
- **What it cannot see.** The checklist reads *literal* arguments only. `match(readonly: true)` is
433
- green, `match(readonly: false)` a warning, but `match(readonly: ENV["RO"])` has no value until the
434
- lane runs — reported as undetermined (`○`, with the reason) rather than guessed. A checklist that
435
- guesses gets believed, and then it is worse than none. Android signing is read from the Gradle
436
- script, as text — running someone's build script to ask it a question is not something it may do.
437
-
438
- ### The Fastfile
439
-
440
- Every project has a Fastfile tab. **It is a text editor** — your file, in a box, with Ruby syntax
441
- highlighting and nothing between you and it. The structured view, where lanes and actions are things
442
- you arrange rather than type, is still to come; this first half is useful on its own: fixing a lane
443
- at 2am should not require an SSH session.
444
-
445
- **Every write is verified.** Saving sends the file to the server, which writes it byte-for-byte then
446
- asks fastlane to parse it and list its lanes. If that fails, the previous content is restored before
447
- the request answers, and fastlane's reason appears above the editor with your work still in the box.
448
- A broken Fastfile never reaches a workspace a run might build from.
449
-
450
- Saving is explicit: verification is a Ruby subprocess, not a regex, and running it on every keystroke
451
- would be slow and dangerous. `⌘S` is another way to ask, not an autosave. Laneyard also refuses to
452
- write while a run of that project is in flight — that run is reading the very file the write replaces.
453
-
454
- Below the editor is what git makes of the workspace: the diff, a message field, `commit` and `push`.
455
- A commit stages exactly the files that changed, never `git add -A` — a build scatters artifacts and
456
- reports around, and none belong in your history.
457
-
458
- ### Removing a project
459
-
460
- Every project has a Settings tab, and the one thing on it is removal. It removes everything
461
- Laneyard holds for the project, in one confirmed act. It is confirmed by typing the project's
462
- name: it is the one destructive action in Laneyard, and a dialogue you can click through is not a
463
- confirmation.
464
-
465
- What it removes:
466
-
467
- - **its block in `config.yml`** — taken out through the YAML document, so your comments and your
468
- key order survive;
469
- - **its run history and its logs.** Every build the project ran, its rows and its logs, deleted.
470
- This is the one thing here nothing can rebuild, and the reason removal is behind a typed name;
471
- - **the clone and the artifacts on disk**, deleted;
472
- - **its secrets and its signing blocks in the vault** — Laneyard's own encrypted copies, forgotten.
473
- The screen counts them before and after, because they are the one thing here you cannot go and look
474
- at: no route ever sends a credential back.
475
-
476
- What it does *not* touch:
477
-
478
- - **the git remote.** The repository is on your host and disk; Laneyard neither reads nor writes it;
479
- - **the credential originals.** The `.p8` and keystore you uploaded are wherever you keep them;
480
- Laneyard removes only its own encrypted copy;
481
- - **global secrets and global signing blocks.** Read by every project, not this one's to take. The
482
- result names how many it left.
483
-
484
- Removal is refused while a run of that project is in flight — that run is using the workspace. A
485
- queued run will not start: it ends as failed, saying its project is gone.
486
-
487
- The same thing from the command line, run from the app's directory — the one holding its
488
- `laneyard.yml`:
489
-
490
- ```bash
491
- cd apps/cartes-ios
492
- laneyard remove --dry-run # show what would go, and stop
493
- laneyard remove # remove it, after a typed confirmation
494
- ```
495
-
496
- No slug to give: it reads one from the `laneyard.yml` there, refusing if the file is missing or has
497
- no slug (run `laneyard setup` again). It deletes that file too, and says to commit the deletion.
498
- Otherwise it matches the Settings tab: confirmed by typing the slug back, `--dry-run` stops at the
499
- inventory, refused during a run.
500
-
501
- ### Resetting
502
-
503
- ```bash
504
- laneyard reset --dry-run # show what would go, and stop
505
- laneyard reset # wipe it, after a typed confirmation
506
- ```
507
-
508
- `laneyard reset` wipes the data and keeps you able to use Laneyard: every project, the database, the
509
- workspaces, the artifacts and the logs go; your accounts and the vault key stay. You sign in with the
510
- same names afterwards, and keeping the key means an older `laneyard.db` backup stays readable. The
511
- database comes back empty on the next start, which also clears sessions, so everyone signs in again.
512
-
513
- It keeps the `server:` block of `config.yml` (accounts, port, bind, retention) and `~/.laneyard/key`,
514
- and never touches the git remotes or credential originals. It reads the inventory first and, like
515
- `uninstall`, is confirmed by typing the folder's path, not `y`.
516
-
517
- ### Uninstalling
518
-
519
- ```bash
520
- laneyard uninstall --dry-run # list what is there, and stop
521
- laneyard uninstall # remove it, after a typed confirmation
522
- npm uninstall -g laneyard # remove the package itself
523
- ```
524
-
525
- `laneyard uninstall` removes the data folder: `config.yml`, the vault key, the database, the
526
- workspaces, the artifacts and the logs. It reads the whole inventory from disk first — projects,
527
- secret and block counts, sizes and paths — and prints it before asking.
528
-
529
- The vault key is the one loss that cannot be undone: every secret and block is encrypted under
530
- `~/.laneyard/key`, and once it is gone the database is ciphertext nobody can read. The originals are
531
- yours and untouched — the `.p8` in your downloads, the keystore in your safe — so what you agree to
532
- is uploading them again. Global secrets and blocks go too; the inventory says so.
533
-
534
- Confirmed by typing the folder's path, not `y`: this is the one command that destroys credentials.
535
- Anything in the folder Laneyard did not put there is named, left alone, and the folder kept for it.
536
- It does not remove the npm package — a command cannot delete the binary it runs from — and prints the
537
- command that does, on purpose: a package manager must not delete someone's signing keys on its own.
538
-
539
- ## Security
540
-
541
- Read this before putting Laneyard on a network.
542
-
543
- - **It is built for a local network, not the internet.** It listens on `0.0.0.0` so you can reach
544
- it from your laptop, behind a password. Do not expose it publicly. If you need remote access,
545
- put it behind a VPN or an SSH tunnel.
546
- - **Passwords** are stored as scrypt hashes and repeated failures are throttled, per account, so
547
- hammering one name cannot lock out the others. Sessions survive a restart, and what is stored is
548
- a SHA-256 of the token rather than the token: a stolen `laneyard.db` is a list of digests, not a
549
- ring of working keys.
550
- - **A role is enforced by the server, not by the interface.** One table names the routes that
551
- require an admin, and one hook is the only thing that reads it — there is no permission check
552
- hidden inside a handler. What a builder is not shown is also what a builder is refused.
553
- - **Secrets are encrypted at rest.** Values are stored with AES-256-GCM under a key in
554
- `~/.laneyard/key` — outside the database, mode `600`, and Laneyard refuses to start if anyone else
555
- can read it. Someone who walks off with `laneyard.db` gets ciphertext. Nothing else holds
556
- plaintext: the store, API and interface deal in names only, and no route sends a value back — which
557
- is why the Secrets tab has no reveal button.
558
- - **A signing block is on disk only while a run needs it.** The file is written into
559
- `~/.laneyard/runs/<run id>/secrets/`, mode `600` in a `700` directory, and that directory goes when
560
- the run ends. The block's secret fields — the keystore passphrases — are stripped from output like
561
- any masked secret, because gradle will echo one back on failure.
562
- - **Masked values are removed from output before it is written, not when displayed.** The
563
- substitution happens once, where a run's output fans out, so the log file, the live stream and the
564
- stored error summary all hold `••••••`. It survives being split across two chunks of output.
565
- - **Do not put secrets in `config.yml`.** It is a plain file with ordinary permissions. Use the
566
- Secrets tab, which puts them in the encrypted vault instead.
567
-
568
- What this does *not* cover, stated plainly:
569
-
570
- - **Git credentials are not in the vault.** `git_auth` points at an SSH key on disk by path;
571
- token authentication is refused at load time rather than silently ignored, so a project cannot
572
- be configured for something that never happens. Laneyard removes the configured repository URL
573
- from its own git error messages — so a token embedded in an HTTPS URL does not leak that way —
574
- but that is one string, not a vault.
575
- - **A value shorter than four characters is refused, not protected.** Removing a two-character
576
- string from a log would shred the output while hiding nothing, so Laneyard says no rather than
577
- pretending. Store it unmasked if it genuinely does not matter.
578
- - **Anything fastlane prints that is not a stored secret is stored in the clear**, under
579
- `~/.laneyard/logs/`.
580
- - **`key.properties` is written into the workspace, and it holds passwords.** It is the one
581
- credential Laneyard puts in the clone rather than the run's own directory, because Gradle resolves
582
- that path relative to the build. Mode `600`, a marker as its first line, removed when the run ends
583
- and swept for at the start of the next in case a server was killed mid-build. A file of yours
584
- without that marker is never touched.
95
+ - **[Configuration](docs/configuration.md)** `config.yml` and `laneyard.yml`.
96
+ - **[Accounts](docs/accounts.md)** — the two roles, and which projects a builder reaches.
97
+ - **[Credentials](docs/credentials.md)** — hardcoded credentials at setup, secrets, the environment file, signing blocks.
98
+ - **[Readiness](docs/readiness.md)** — the per-project checklist, and what a tick means.
99
+ - **[Managing a project](docs/managing.md)** — the Fastfile editor, removing, resetting, uninstalling.
100
+ - **[Security](docs/security.md)** — what is encrypted, what is not, and what this does not cover.
585
101
 
586
102
  ## Status
587
103
 
@@ -603,9 +119,9 @@ What this does *not* cover, stated plainly:
603
119
  the vault and patch the file
604
120
  - `○` git-triggered and scheduled builds
605
121
 
606
- Two things worth knowing: listing lanes does not fetch the repository, so a lane you just pushed
607
- appears after the next run; and runs execute one at a time across all projects. A queued run survives
608
- a server restart and starts on its own.
122
+ Two things worth knowing: the screens read the clone rather than the remote, so a lane you just
123
+ pushed appears after the next run — or after pressing refresh; and runs execute one at a time across
124
+ all projects. A queued run survives a server restart and starts on its own.
609
125
 
610
126
  ## Changelog
611
127
 
@@ -55,9 +55,16 @@ export async function runAdoption(options) {
55
55
  // A rewrite-only proposal stores nothing — it renames a variable your lane
56
56
  // already reads — so the question is not "store it here".
57
57
  const rewriteOnly = proposal.tier === "file" && !found.has(proposal);
58
+ // Plural when more than one argument changes — the mapping is on screen
59
+ // above, so the question counts them rather than repeating three long names.
60
+ const all = proposal.rewrites.length;
58
61
  const question = rewriteOnly
59
- ? ` Rewrite it to ${bold(proposal.varName)}?`
60
- : ` Store it here and use ${bold(proposal.varName)}?`;
62
+ ? all > 1
63
+ ? ` Rewrite all ${all} to the names the block exports?`
64
+ : ` Rewrite it to ${bold(proposal.varName)}?`
65
+ : all > 1
66
+ ? ` Store it here and rewrite all ${all}?`
67
+ : ` Store it here and use ${bold(proposal.varName)}?`;
61
68
  if (!(await asker.confirm(question, proposal.checked))) {
62
69
  continue;
63
70
  }
@@ -124,23 +131,31 @@ function describe(fastlaneDir, proposal) {
124
131
  // or a password, and setup's output is pasted into bug reports and kept in CI
125
132
  // transcripts — printing it there would be this feature leaking the very
126
133
  // thing it exists to put away. The file and line above say where to look.
127
- const shown = literal.kind === "env"
128
- ? `ENV["${literal.value}"]`
134
+ const shown = (r) => r.literal.kind === "env"
135
+ ? `ENV["${r.literal.value}"]`
129
136
  : proposal.tier === "file"
130
- ? `"${literal.value}"`
137
+ ? `"${r.literal.value}"`
131
138
  : proposal.tier === "inline"
132
139
  ? dim("(a key, inline in the file)")
133
140
  : dim("(a literal value, masked)");
141
+ const many = proposal.rewrites.length > 1;
134
142
  const why = literal.kind === "env"
135
- ? "Renamed to the variable Laneyard's signing block exports."
143
+ ? `Renamed to the variable${many ? "s" : ""} Laneyard's signing block exports.`
136
144
  : proposal.tier === "inline"
137
145
  ? "This key is in your repository in cleartext."
138
146
  : proposal.tier === "file"
139
147
  ? "That path does not survive the clone: Laneyard builds from your remote."
140
148
  : "A literal secret in a build file is a secret in your history.";
149
+ // Every argument that changes, not just the one the proposal is anchored on:
150
+ // an `apple_asc` block rewrites its key file *and* both identifiers, and
151
+ // naming one of the three made the other two look like they were not touched.
152
+ const width = Math.max(...proposal.rewrites.map((r) => r.arg.length));
153
+ const lines = proposal.rewrites
154
+ .map((r) => ` ${`${r.arg}:`.padEnd(width + 2)} ${shown(r)} → ${bold(r.varName)}\n`)
155
+ .join("");
141
156
  return ("\n" +
142
- ` ${bold(`${fastlaneDir}/Fastfile:${literal.line}`)} ${literal.action}(${literal.arg}:)\n` +
143
- ` → ${shown}\n` +
157
+ ` ${bold(`${fastlaneDir}/Fastfile:${literal.line}`)} ${literal.action}\n` +
158
+ lines +
144
159
  dim(` ${why}\n`));
145
160
  }
146
161
  /**
@@ -163,6 +178,7 @@ async function named(asker, proposal) {
163
178
  ...proposal,
164
179
  varName,
165
180
  edits: [{ ...proposal.edits[0], replacement: `ENV.fetch("${varName}")` }],
181
+ rewrites: [{ ...proposal.rewrites[0], varName }],
166
182
  };
167
183
  }
168
184
  /** Lifts one accepted proposal into the vault. */