laneyard 0.5.0 → 0.6.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 CHANGED
@@ -2,33 +2,30 @@
2
2
 
3
3
  **A self-hosted web UI for fastlane.**
4
4
 
5
- Laneyard is a browser interface for the fastlane lanes you already have, running on hardware you
6
- own. Pick a lane, click run, watch the output stream, download the artifact. Nobody else holds
7
- your signing keys, and nobody meters your minutes.
5
+ A browser interface for the fastlane lanes you already have, on hardware you own. Pick a lane, run
6
+ it, watch the output stream, download the artifact. Nobody else holds your signing keys, nobody
7
+ meters your minutes.
8
8
 
9
- It replaces neither fastlane nor your Fastfile it drives them. Your lanes stay exactly where
10
- they are.
9
+ It drives fastlane, it doesn't replace it. Your lanes stay where they are.
11
10
 
12
11
  ![A build running in Laneyard: step timeline on the left, live output on the right, artifacts below](https://raw.githubusercontent.com/martinfrouin/laneyard/main/docs/screenshots/run.png)
13
12
 
14
13
  ## Why
15
14
 
16
- **You keep the keys.** Signing certificates, keystores, provisioning profiles, store
17
- credentials — everything needed to publish under your name — stays on your machine. A hosted CI
18
- is a third party holding all of it.
15
+ **You keep the keys.** Certificates, keystores, profiles, store credentials — everything needed to
16
+ publish under your name — stays on your machine. Hosted CI is a third party holding all of it.
19
17
 
20
- **Minutes stop existing.** Hardware you already own costs less than a year of most CI plans,
21
- and never bills by the second. Long builds stop being a budget decision.
18
+ **Minutes stop existing.** Hardware you own costs less than a year of most CI plans and never bills
19
+ by the second. Long builds stop being a budget decision.
22
20
 
23
- **It is just fastlane.** No new DSL, no YAML dialect, no vendor config to port. Laneyard reads
24
- the Fastfile you already have and asks *your* fastlane what it can do plugins included. It
25
- hard-codes no knowledge of fastlane at all, so upgrading fastlane or adding a plugin needs no
26
- change here.
21
+ **It is just fastlane.** No new DSL, no YAML dialect, nothing to port. Laneyard reads your Fastfile
22
+ and asks *your* fastlane what it can do, plugins included — so upgrading fastlane needs no change
23
+ here.
27
24
 
28
- The adaptation goes one way. A repository that builds today keeps building unedited: signing
29
- credentials reach your lanes under the variable names your Fastfile already reads, and where
30
- Laneyard needs to know something no file can tell it, it asks on a form rather than asking you to
31
- change the file.
25
+ The adaptation goes one way. Credentials reach your lanes under the variable names your Fastfile
26
+ already reads; where a file can't tell Laneyard something, it asks on a form instead of asking you to
27
+ change the file. Every edit it offers is optional decline them all and the repository builds
28
+ exactly as before.
32
29
 
33
30
  ## Where it fits
34
31
 
@@ -41,9 +38,9 @@ change the file.
41
38
  | build queue across a team | yes, serial | yes | yes |
42
39
  | runs on pull requests | planned | yes | yes |
43
40
 
44
- Laneyard's queue is serial: runs are drained one at a time across every project, in the order
45
- they were asked for. If your team needs parallel builds across a fleet today, use something else.
46
- Laneyard is for one machine you control.
41
+ Laneyard's queue is serial: runs drain one at a time across every project, in order. If your team
42
+ needs parallel builds across a fleet today, use something else — Laneyard is for one machine you
43
+ control.
47
44
 
48
45
  ## Requirements
49
46
 
@@ -67,11 +64,10 @@ laneyard setup # answer a few questions about this project
67
64
  laneyard # start the server
68
65
  ```
69
66
 
70
- `laneyard setup` inspects what is there — the `fastlane` directory even when nested in a monorepo, a
67
+ `laneyard setup` inspects what is there — the `fastlane` directory (even nested in a monorepo), a
71
68
  `Gemfile`, an Xcode project or a Gradle build — and writes the matching block into
72
- `~/.laneyard/config.yml`. On a machine that has no account yet, it also creates the first admin:
73
- it asks what to call it and prints its generated password once. Write it down; it is not shown
74
- again, and nothing stores it.
69
+ `~/.laneyard/config.yml`. On a machine with no account yet, it also creates the first admin: it asks
70
+ the name and prints a generated password once. Write it down it is not shown again.
75
71
 
76
72
  <details>
77
73
  <summary>Running from source instead</summary>
@@ -90,6 +86,67 @@ asked your project's own fastlane for them.
90
86
 
91
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)
92
88
 
89
+ ### What setup does about a credential your Fastfile names outright
90
+
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
+
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 vault is written before the Fastfile: if lifting fails, nothing has been patched to read a
124
+ missing variable. The patch is spliced by byte offset — everything outside the replaced range is
125
+ byte-identical — and the file is re-parsed before the command returns; if it no longer parses, the
126
+ previous content is restored.
127
+
128
+ ```diff
129
+ - upload_to_play_store(json_key: "./play-service-account.json", track: "beta")
130
+ + upload_to_play_store(json_key: ENV.fetch("SUPPLY_JSON_KEY"), track: "beta")
131
+ ```
132
+
133
+ `ENV.fetch` rather than `ENV[]`: a missing variable fails at the top of the lane and names itself,
134
+ instead of reaching an action as `nil`.
135
+
136
+ **Setup does not commit or push.** It prints the `git diff` command and stops — the working copy is
137
+ yours. So **a patched Fastfile changes nothing until you push it**: Laneyard builds from the remote,
138
+ and until the commit is there, every run still reads the old path. It also does not take the
139
+ credential out of your history; where `git ls-files` finds the file, it says so — rotating the key is
140
+ the fix.
141
+
142
+ Two things it leaves alone: a value written as a heredoc (its reported location is the marker, not
143
+ the text, so patching would corrupt the file), and the Android keystore (configured in Gradle, not
144
+ the Fastfile — handled under Signing credentials below).
145
+
146
+ Declining writes nothing, anywhere. And where nothing can run Prism — a Mac whose only Ruby is the
147
+ system 2.6 — setup prints `Fastfile not analysed …` and finishes as always. The scan is a service,
148
+ never a gate.
149
+
93
150
  ## Configuration
94
151
 
95
152
  All configuration lives in files. The database holds execution state only, so backing up
@@ -115,15 +172,14 @@ projects:
115
172
  git_auth: { kind: ssh_key, ref: ~/.ssh/id_ed25519 }
116
173
  ```
117
174
 
118
- `max_concurrent_runs` accepts `1` and nothing else. Runs are drained from a single queue, one at
119
- a time across every project — parallel runs would need a working directory per run, which does
120
- not exist yet. A larger number is refused when the file loads rather than silently ignored, so a
121
- server is never configured for builds that never happen.
175
+ `max_concurrent_runs` accepts `1` only. Runs drain from a single queue, one at a time across every
176
+ project — parallel runs would need a working directory per run, which does not exist yet. A larger
177
+ number is refused at load, so a server is never configured for builds that never happen.
122
178
 
123
179
  ### Accounts
124
180
 
125
- Everyone who signs in has a name, a password and one of two roles. Two, because a third role is
126
- easy to add and impossible to remove.
181
+ Everyone who signs in has a name, a password and one of two roles — two, because a third is easy to
182
+ add and impossible to remove.
127
183
 
128
184
  | | **admin** | **builder** |
129
185
  |---|---|---|
@@ -135,33 +191,29 @@ easy to add and impossible to remove.
135
191
  | remove a project | ✓ | |
136
192
  | manage accounts | ✓ | |
137
193
 
138
- A builder is what you give someone who ships without being trusted with the signing chain: they
139
- can press the button and watch what happens, and they never see a credential.
194
+ A builder is who you give someone who ships without being trusted with the signing chain: they press
195
+ the button and watch, and never see a credential.
140
196
 
141
- The interface shows a builder only what a builder can use — the secrets, fastfile and settings
142
- tabs are not drawn, and neither is the accounts screen. That is courtesy, not security: the
143
- server refuses those routes on its own, whatever the browser was shown, and the test suite
144
- proves it for every verb and every spelling of the address.
197
+ The interface shows a builder only what a builder can use — no secrets, fastfile, settings or
198
+ accounts tabs. That is courtesy, not security: the server refuses those routes on its own, whatever
199
+ the browser was shown, and the test suite proves it for every verb and every spelling of the address.
145
200
 
146
201
  #### Which projects a builder reaches
147
202
 
148
- An admin reaches every project managing the server is the whole role. A builder reaches only the
149
- projects it is granted, one at a time, from the accounts screen: each builder carries a checklist
150
- of the projects, and ticking one grants it. A project a builder cannot reach is **invisible**, not
151
- shown-and-locked absent from its project list and its navigation, and a 404 by URL, answered with
152
- the very body a project that does not exist gives, so the two cannot be told apart. This is enforced
153
- by the server, in one place, not hidden in the browser.
203
+ An admin reaches every project. A builder reaches only the projects it is granted, from the accounts
204
+ screen. A project a builder cannot reach is **invisible**, not shown-and-locked absent from its
205
+ lists and a 404 by URL, answered with the body a nonexistent project gives, so the two cannot be told
206
+ apart. Enforced by the server, in one place, not the browser.
154
207
 
155
- The reach is a `projects` list on the account in `config.yml`, and its three states are deliberate:
208
+ The reach is a `projects` list on the account in `config.yml`, with three states:
156
209
 
157
- - **absent** — every project. A config written before this feature has no such field on anyone, so
158
- nobody loses access on an upgrade.
159
- - **`[]`** — no project. This is what a newly created account is written with, so a new builder
160
- starts seeing nothing until granted.
210
+ - **absent** — every project. A config written before this feature grants everyone, so nobody loses
211
+ access on an upgrade.
212
+ - **`[]`** — no project. What a new account starts with, so a new builder sees nothing until granted.
161
213
  - **a list of slugs** — exactly those projects.
162
214
 
163
215
  Removing a project strips its slug from every account, so a grant never points at a project that is
164
- gone, and a project re-created later under the same slug does not silently inherit an old grant.
216
+ gone, and a re-created slug does not inherit an old grant.
165
217
 
166
218
  Add and remove accounts from the accounts screen, or from the command line:
167
219
 
@@ -169,28 +221,24 @@ Add and remove accounts from the accounts screen, or from the command line:
169
221
  echo "$PASSWORD" | laneyard user add lea --role builder
170
222
  ```
171
223
 
172
- The password is read from standard input, never taken as an argument — an argument lands in your
173
- shell history. Without `--role`, the account is a builder.
224
+ The password is read from standard input, never an argument — an argument lands in your shell
225
+ history. Without `--role`, the account is a builder.
174
226
 
175
- Two things are refused, in the API and on the command line alike: removing the last admin, and
176
- demoting the last admin. A server nobody can administer cannot be repaired from the interface.
227
+ Two things are refused, in the API and CLI alike: removing or demoting the last admin. A server
228
+ nobody can administer cannot be repaired from the interface.
177
229
 
178
- Anyone changes their own password and their own name from **your account**, which is said in
179
- those words in the header and again on your own row of the accounts screen a builder included,
180
- since that page is about one person rather than about the server's list of people. Either one asks
181
- for the current password even though you are already signed in: a session
182
- is a cookie in a browser that may have been left open on a desk. Doing it ends every other session
183
- that account has, and leaves the page you did it on signed in. That is how the random password
184
- `laneyard setup` printed once stops being a string on a sticky note.
230
+ Anyone changes their own password and name from **your account** a builder included. Either asks
231
+ for the current password even though you are signed in: a session is a cookie in a browser that may
232
+ have been left open on a desk. Doing it ends every other session that account has. That is how the
233
+ password `laneyard setup` printed once stops being a string on a sticky note.
185
234
 
186
- Changing your name edits your entry in `config.yml` in place, keeping your role and your project
187
- access exactly as they were, and refuses a name another account already has. It is your login name
188
- that changes: the next time you sign in, it is the new one you type. This is self-service and needs
189
- no admin — the identifier is yours to change, whatever your role.
235
+ Changing your name edits your `config.yml` entry in place, keeping your role and access, and refuses
236
+ a name another account already has. The next time you sign in, you type the new one — self-service,
237
+ whatever your role.
190
238
 
191
- Removing an account ends its sessions immediately — "remove the account" and "revoke access" are
192
- the same act. So does editing `config.yml` by hand: every request looks the account up again, so
193
- a demotion takes effect at once rather than at the next restart.
239
+ Removing an account ends its sessions immediately — "remove the account" and "revoke access" are the
240
+ same act. So does editing `config.yml` by hand: every request looks the account up again, so a change
241
+ takes effect at once rather than at the next restart.
194
242
 
195
243
  ### `laneyard.yml` — in your repository, and committed
196
244
 
@@ -212,85 +260,44 @@ platforms: [ios] # or `[android]`, or both
212
260
  asked for an App Store Connect key. Left out, Laneyard looks at the repository — an Xcode project
213
261
  means iOS, a Gradle build means Android — and reports what it found rather than assuming.
214
262
 
215
- It looks **beside the Fastfile**, not at the repository root, because that is where an app keeps
216
- its platform folders: `ios/` and `fastlane/` are siblings, and both move together when the app is
217
- one directory of a monorepo. So `app/fastlane/Fastfile` alongside `app/ios/Runner.xcodeproj` is
218
- found, and a project configured with `ios/fastlane` reports iOS alone rather than being shown the
219
- Android section on the strength of a sibling folder its lanes never touch. When that guess is
220
- wrong, `platforms` is read first and settles it.
263
+ It looks **beside the Fastfile**, not at the repository root, because that is where an app keeps its
264
+ platform folders: `ios/` and `fastlane/` are siblings and move together when the app is one directory
265
+ of a monorepo. When that guess is wrong, `platforms` settles it.
221
266
 
222
267
  Field by field, the repository file wins over the server block, which wins over the defaults. Any
223
- field of `laneyard.yml` may also be written in the server block, so a repository you would rather
224
- not touch can be configured entirely from `config.yml`.
225
-
226
- **A monorepo of several apps carries one `laneyard.yml` per app.** The file may live in the app's
227
- own directory beside its fastlane folder rather than at the repository root, so two apps on one
228
- git remote each describe their own build without a single root file having to speak for both.
229
- `laneyard setup` writes it there for you. Inside an app-level file **paths are relative to that
230
- file's own directory**, not to the repository: `artifact_globs: ["**/*.aab"]` and a plain
231
- `fastlane_dir: fastlane` (usually left out entirely), so an app moved or duplicated keeps its file
232
- unchanged. Laneyard finds the app from the project's `fastlane_dir` in `config.yml` and reads the
233
- file from there; a `laneyard.yml` at the repository root still works and keeps its paths
234
- repo-root-relative, exactly as before.
268
+ field may also go in the server block, so a repository you would rather not touch can be configured
269
+ entirely from `config.yml`.
270
+
271
+ **A monorepo carries one `laneyard.yml` per app**, in the app's own directory beside its fastlane
272
+ folder, so two apps on one remote each describe their own build. Inside an app-level file **paths are
273
+ relative to that file's directory** `artifact_globs: ["**/*.aab"]`, a plain `fastlane_dir:
274
+ fastlane` usually left out so an app moved or duplicated keeps its file unchanged. A `laneyard.yml`
275
+ at the repository root still works, with repo-root-relative paths.
235
276
 
236
277
  Both files are watched: edit them by hand and Laneyard picks the change up. An invalid file is
237
278
  reported and the last valid configuration stays live — a typo never takes the server down.
238
279
 
239
280
  ### Secrets
240
281
 
241
- The variables your lanes read do not live in a file. They go into an encrypted vault, from the
242
- Secrets tab of a project or from the command line — the files a project signs with go in the same
243
- vault, as blocks, and have a section of their own below:
244
-
245
- ```bash
246
- laneyard secret set MATCH_PASSWORD --project cartes-ios # reads the value from standard input
247
- echo "$GITHUB_TOKEN" | laneyard secret set GITHUB_TOKEN # global, and out of your shell history
248
- ```
249
-
250
- The value is never an argument: a command line ends up in `~/.zsh_history` and in the output of
251
- `ps`. Typing the command alone leaves you at a blank line — type or paste the value, then
252
- `Ctrl-D`.
253
-
254
- **Reading one back.** The vault is write-only for anything you called a secret: the server never
255
- sends a masked value back, so the interface has nothing to uncover and no browser ever holds one.
282
+ The variables your lanes read go into an encrypted vault, from a project's Secrets tab — global
283
+ secrets apply everywhere, a project's own win over them. The files it signs with go in the same
284
+ vault as blocks (below).
256
285
 
257
- Not everything stored here is a secret, though `APP_VERSION`, `SENTRY_ORG`, an issuer id are
258
- identifiers, and being unable to check what an import stored makes the import something you take on
259
- faith. So the line is the one you drew yourself: a value kept out of the logs is never returned; a
260
- value you stored without that is shown on request, one named key at a time. `mask` and `unmask`
261
- change which it is without touching the value — otherwise revealing something would mean first
262
- retyping the value you were trying to read.
286
+ A secret is write-only: the server never sends a masked value back, so no browser ever holds one.
287
+ What you did not mark secret `APP_VERSION`, an issuer id is shown on request, one key at a time;
288
+ masking and unmasking switch which it is without retyping the value.
263
289
 
264
- **Bring the ones you already have.** A project that builds today has its variables in
265
- `fastlane/.env` gitignored, on one laptop, and therefore absent from the clone a build runs
266
- from. From that working copy:
267
-
268
- ```bash
269
- laneyard secret import --project cartes-ios # shows what it would store
270
- laneyard secret import --project cartes-ios --yes # stores it
271
- ```
272
-
273
- It runs from the CLI because that is where the `.env` is; the server only ever sees a clone. A
274
- variable naming a service account JSON has the **file's contents** stored, under `SUPPLY_JSON_KEY_DATA`
275
- — a name supply reads on its own — because a path does not travel to another machine. A variable
276
- naming a `.p8` is reported and left alone: no action in fastlane reads a `.p8` out of an environment
277
- variable, and that credential belongs in a signing block, described below. Everything stored is
278
- masked, and nothing is printed but names.
279
-
280
- Nothing in your lanes has to change afterwards. `key_filepath:` and `json_key:` keep working as
281
- written — a signing block puts a real file back on disk for the length of a run.
282
-
283
- A secret becomes an environment variable for every run of the project it belongs to. Without
284
- `--project` it applies to every project; a project secret of the same name wins over a global
285
- one. Secrets are kept out of the logs unless you pass `--no-mask`, and a masked value must be at
286
- least four characters long — see below.
290
+ Nothing in your lanes has to change afterwards: `key_filepath:` and `json_key:` keep working a
291
+ signing block puts a real file back on disk for the length of a run. A secret becomes an environment
292
+ variable for every run of its project, kept out of the logs; a masked value must be at least four
293
+ characters (see below).
287
294
 
288
295
  ### Signing credentials
289
296
 
290
- A signing credential is not a string. An Android keystore is bytes that Gradle reads through a path,
291
- and a `.p8` is useless without the key id and the issuer id that go with itso these are stored as
292
- blocks: one file, plus the handful of fields that make it usable, taken whole or refused. A keystore
293
- stored without its alias is not a partial success; it is a build that fails in a month.
297
+ A signing credential is not a string: an Android keystore is bytes Gradle reads through a path, and a
298
+ `.p8` is useless without its key id and issuer id. So these are stored as blocks one file plus the
299
+ fields that make it usable, taken whole or refused. A keystore stored without its alias is not a
300
+ partial success; it is a build that fails in a month.
294
301
 
295
302
  | block | the file | the fields beside it |
296
303
  | ------------------------------ | ---------------------- | ------------------------------------------- |
@@ -298,69 +305,55 @@ stored without its alias is not a partial success; it is a build that fails in a
298
305
  | *android upload keystore* | `.jks` or `.keystore` | key alias, store password, key password |
299
306
  | *play store service account* | JSON | — |
300
307
 
301
- They live in the **signing** part of a project's Secrets tab, beside the variables and the secrets.
302
- The file is encrypted at rest like everything else in the vault and never comes back out to a
303
- browser: a stored block shows its file name and nothing more, so replacing one means giving it again
304
- in full.
308
+ They live in the **signing** part of a project's Secrets tab. The file is encrypted at rest and never
309
+ comes back out to a browser: a stored block shows its file name and nothing more, so replacing one
310
+ means giving it again in full.
305
311
 
306
312
  A variable a block has made redundant is said to be so, beside the row and in one sentence:
307
313
  `SUPPLY_JSON_KEY_DATA` once a Play block applies — it still works, but the same credential is then
308
314
  stored twice — and `APP_STORE_CONNECT_API_KEY_P8`, which no action in fastlane has ever read.
309
315
  Neither is removed for you: the row is yours, and the button to drop it is on the same line.
310
316
 
311
- **A block becomes real files, for the length of a run.** Gradle's `storeFile` is a path, and
312
- `app_store_connect_api_key` wants a path, so a credential that exists only as ciphertext in a
313
- database cannot be used by anything. Each block that applies is written into
314
- `~/.laneyard/runs/<run id>/secrets/`, mode `600` in a `700` directory, and that directory is removed
315
- when the run ends whether it passed, failed, was cancelled or timed out. Every applicable block is
316
- written, whether or not the lane looks like it needs one: a Fastfile can reach anything through `sh`
317
- or a plugin, and a guess of "not needed" that is wrong is a debug-signed artifact rather than a
318
- missing variable.
319
-
320
- **It reaches your lanes under the names your project already reads.** Each block's form arrives
321
- pre-filled with the names fastlane itself declares `APP_STORE_CONNECT_API_KEY_KEY_FILEPATH`,
322
- `APP_STORE_CONNECT_API_KEY_KEY_ID`, `APP_STORE_CONNECT_API_KEY_ISSUER_ID` for the key, and
323
- `SUPPLY_JSON_KEY` for the service account and every one of them is editable. A Fastfile written
324
- around `ENV.fetch("ASC_KEY_FILEPATH")` is not a Fastfile doing it wrong: you say so on that form,
325
- rather than being asked to rename anything in the repository. The name stored with the block is the
326
- only name exported, and no default is emitted alongside it as a courtesy — that courtesy is what
327
- would make a typo in the configured name look like it had worked.
328
-
329
- Nothing in fastlane reads a keystore by convention, so the keystore's names are Laneyard's own —
330
- `ANDROID_KEYSTORE_PATH`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`
331
- and the same rule applies to them.
332
-
333
- **The keystore block can also supply `key.properties`.** The Flutter documentation's own build
334
- script signs with the release config when that file exists and with the debug config when it does
335
- not, and gitignores it — so on a build server it is always absent. Rather than telling you to
336
- rewrite your build script, Laneyard writes the file the script is already looking for, for the
337
- length of the run, out of the keystore block. Two things about that file cannot be read out of a
338
- build script and are asked for on the block instead: where it goes, when the script names it in a
339
- way that leaves the directory unresolved, and what the keys inside it are called, which start from
340
- the Flutter documentation's four. Asking at configuration time is allowed; requiring a change to
341
- your repository is not.
342
-
343
- **Only the projects that sign need any of this.** fastlane is not only for shipping to stores —
344
- lanes take screenshots, run tests, sync certificates — so the three blocks are an offer rather than
345
- a gate. A project that wants an artifact out of a Gradle build needs the keystore and nothing else,
346
- and three untouched circles are not three things it is failing.
317
+ **A block becomes real files, for the length of a run.** Gradle's `storeFile` and
318
+ `app_store_connect_api_key` both want a path, so a credential that exists only as ciphertext cannot
319
+ be used. Each block that applies is written into `~/.laneyard/runs/<run id>/secrets/`, mode `600` in
320
+ a `700` directory, removed when the run ends — passed, failed, cancelled or timed out. Every
321
+ applicable block is written whether or not the lane looks like it needs one: a Fastfile can reach
322
+ anything through `sh` or a plugin, and a wrong guess of "not needed" is a debug-signed artifact, not
323
+ a missing variable.
324
+
325
+ **It reaches your lanes under the names your project already reads.** Each form is pre-filled with
326
+ the names fastlane declares `APP_STORE_CONNECT_API_KEY_KEY_FILEPATH`/`_KEY_ID`/`_ISSUER_ID`,
327
+ `SUPPLY_JSON_KEY` and every one is editable. A Fastfile written around
328
+ `ENV.fetch("ASC_KEY_FILEPATH")` is not doing it wrong: you say so on the form instead of renaming
329
+ anything. The name stored is the only one exported no default is emitted alongside, which would
330
+ make a typo look like it worked. The keystore's names are Laneyard's own (`ANDROID_KEYSTORE_PATH`,
331
+ `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`), same rule.
332
+
333
+ **The keystore block can also supply `key.properties`.** Flutter's own build script signs with the
334
+ release config when that file exists and the debug config when it does not, and gitignores it — so on
335
+ a build server it is always absent. Rather than telling you to rewrite the script, Laneyard writes
336
+ the file it looks for, for the length of the run, out of the keystore block. Two things it cannot
337
+ read from the script are asked for on the block: where the file goes, and what the keys inside are
338
+ called (starting from Flutter's four).
339
+
340
+ **Only the projects that sign need any of this.** fastlane also takes screenshots, runs tests, syncs
341
+ certificates — so the three blocks are an offer, not a gate. A project that just wants an artifact
342
+ out of Gradle needs the keystore and nothing else.
347
343
 
348
344
  A block stored on a project belongs to that project. One stored globally applies to every project,
349
345
  and a project's own block wins over it. Both are admin-only, like the rest of the vault.
350
346
 
351
347
  ### Readiness
352
348
 
353
- Every project has a Readiness tab: what stands between it and a build that runs while nobody
354
- watches. Only the checks that apply to the project are shown — an Android project is never asked
355
- for an App Store Connect key, because one irrelevant warning teaches you to ignore the screen.
349
+ Every project has a Readiness tab: what stands between it and a build that runs while nobody watches.
350
+ Only the checks that apply are shown — an Android project is never asked for an App Store Connect key.
356
351
 
357
- **What a tick means.** The checks read your Fastfile, following a lane into the methods that
358
- Fastfile defines factoring your lanes into `def deploy_ios` is good practice, not something that
359
- should make Laneyard blind. Two things stay out of reach and always will: `import`/`import_from_git`
360
- brings in lanes written elsewhere, and `fastlane/actions/` holds actions whose names mean nothing to
361
- a reader that has only seen the Fastfile. Where either applies, a check that found nothing says
362
- *could not tell* rather than ticking. A green tick here means "looked, and it is fine" — never
363
- "looked, and saw nothing".
352
+ **What a tick means.** The checks read your Fastfile, following a lane into the methods it defines.
353
+ Two things stay out of reach: `import`/`import_from_git` pulls in lanes from elsewhere, and
354
+ `fastlane/actions/` holds actions whose names mean nothing to a reader that has only seen the
355
+ Fastfile. Where either applies, a check that found nothing says *could not tell* rather than ticking.
356
+ A green tick means "looked, and it is fine" never "looked, and saw nothing".
364
357
 
365
358
  Always:
366
359
 
@@ -371,17 +364,14 @@ Always:
371
364
  - **no lane calls an action known to stop and ask** — `prompt`, `sigh`, `cert`, a writable
372
365
  `match`, an upload waiting for its summary to be confirmed;
373
366
  - **the variables the lanes read** are in the vault. Every `ENV.fetch("…")` a lane reaches is
374
- collected and looked up. This is the check for the commonest way a project that works on your
375
- laptop fails on a build server: the variables live in `fastlane/.env`, that file is gitignored,
376
- and it never reaches the clone a build runs from — so the run stops at the first one with
377
- nothing on screen to say why.
378
-
379
- Two things a Fastfile cannot tell you, and two places to say them. A variable read by a tool the
380
- lane shells out to `sentry-cli` and its `SENTRY_AUTH_TOKEN` is named nowhere in the lanes. A
381
- committed `fastlane/.env.example` is read for exactly this, since that is what the file is for,
382
- and `required_secrets` in `laneyard.yml` covers whatever it does not. A variable found only in
383
- the server's own environment is reported rather than ticked over: it works, but it works because
384
- of how this server was started.
367
+ collected and looked up the commonest way a project that works on your laptop fails on a server:
368
+ the variables live in a gitignored `fastlane/.env` that never reaches the clone.
369
+
370
+ Two things a Fastfile cannot tell you. A variable read by a tool the lane shells out to —
371
+ `sentry-cli` and `SENTRY_AUTH_TOKEN` — is named nowhere in the lanes; a committed
372
+ `fastlane/.env.example` is read for exactly this, and `required_secrets` in `laneyard.yml` covers
373
+ the rest. A variable found only in the server's own environment is reported, not ticked: it works,
374
+ but because of how the server was started.
385
375
 
386
376
  A name a signing block exports counts as being in the vault, since that is where the block is:
387
377
  a lane reading `SUPPLY_JSON_KEY` with a Play block stored is not asked for it again, on this
@@ -409,16 +399,14 @@ On Android:
409
399
  passphrase, and one that is neither in the call nor in the vault makes gradle stop and ask. A
410
400
  keystore block settles this before the lanes are read at all — the file and both passphrases
411
401
  reach the run together, so nothing can stop and ask, whichever lane runs;
412
- - **the release is signed with the release key.** The one check here whose failure is silent:
413
- the Flutter documentation's own snippet signs with the release config when `key.properties`
414
- exists and with the *debug* config when it does not and gitignores `key.properties`, so it is
415
- absent from every clone. The build then succeeds, produces an artifact signed with the debug key,
416
- and the rejection arrives from the store minutes later saying nothing about signing. This reads
417
- the Gradle file as text and says so before the build, not after and then, for a project whose
418
- keystore is stored here, writes the `key.properties` that build is already asking for, for the
419
- length of the run. Your build script is not asked to change: the file arrives where it looks for
420
- it, marked `# written by laneyard, do not commit`, and is removed when fastlane stops. A file of
421
- your own without that marker is never written over and never deleted;
402
+ - **the release is signed with the release key.** The one check whose failure is silent: Flutter's
403
+ own snippet signs with the release config when `key.properties` exists and the *debug* config when
404
+ it does not, and gitignores that file so it is absent from every clone. The build then succeeds
405
+ with a debug-signed artifact, and the store rejects it minutes later saying nothing about signing.
406
+ This reads the Gradle file as text and says so before the build; and for a project whose keystore
407
+ is stored here, writes the `key.properties` the build asks for, for the length of the run marked
408
+ `# written by laneyard, do not commit`, removed when fastlane stops. A file of your own without
409
+ that marker is never touched;
422
410
  - **the Play Store service account** is there when a lane calls `upload_to_play_store`. The vault
423
411
  first — a block, or a `SUPPLY_JSON_KEY` variable stored before blocks existed — then the
424
412
  `json_key` argument in the call, then the **Appfile**: `json_key_file` and `json_key_data`, which
@@ -428,45 +416,38 @@ On Android:
428
416
  Like the iOS ones, the Android checks read **literal arguments only**. `gradle(storePassword:
429
417
  ENV["PW"])` is reported as undetermined, never guessed at: a checklist that guesses gets believed.
430
418
 
431
- They run when you open the tab or press refresh, never on their own: they shell out to git and to
432
- bundler. The time of the last run is on screen, because a stale green tick is worse than a red
433
- cross.
419
+ They run when you open the tab or press refresh, never on their own they shell out to git and
420
+ bundler. The last run's time is on screen, because a stale green tick is worse than a red cross.
434
421
 
435
- Nothing here blocks anything. A red check is never the reason a run cannot be started, and
436
- Laneyard never edits a Fastfile to make its own checklist go green — each line explains, you
437
- decide. Where the fix genuinely is one action, the line links to the Secrets tab instead of
438
- growing a second copy of its form.
422
+ Nothing here blocks anything. A red check is never why a run cannot start, and Laneyard never edits a
423
+ Fastfile to make its own checklist green — each line explains, you decide. Where the fix is one
424
+ action, the line links to the Secrets tab.
439
425
 
440
426
  **What it cannot see.** The checklist reads *literal* arguments only. `match(readonly: true)` is
441
- green and `match(readonly: false)` is a warning, but `match(readonly: ENV["RO"])` has no value
442
- until the lane runs, so it is reported as undetermined `○`, with the reason rather than
443
- guessed either way. The same applies to anything a lane computes: a checklist that guesses gets
444
- believed, and then it is worse than no checklist. Android signing is read out of the Gradle build
445
- script rather than the Fastfile, since that is where it lives — and read as text, because running
446
- someone's build script to ask it a question is not something a checklist may do.
427
+ green, `match(readonly: false)` a warning, but `match(readonly: ENV["RO"])` has no value until the
428
+ lane runs reported as undetermined (`○`, with the reason) rather than guessed. A checklist that
429
+ guesses gets believed, and then it is worse than none. Android signing is read from the Gradle
430
+ script, as text running someone's build script to ask it a question is not something it may do.
447
431
 
448
432
  ### The Fastfile
449
433
 
450
- Every project has a Fastfile tab. **It is a text editor** — your file, in a box, with Ruby
451
- syntax highlighting and nothing between you and it. The structured view described in the design
452
- document, where lanes and actions are things you arrange rather than type, is still to come. This
453
- is the honest first half of it, and it is useful on its own: fixing a lane at 2am should not
454
- require an SSH session.
434
+ Every project has a Fastfile tab. **It is a text editor** — your file, in a box, with Ruby syntax
435
+ highlighting and nothing between you and it. The structured view, where lanes and actions are things
436
+ you arrange rather than type, is still to come; this first half is useful on its own: fixing a lane
437
+ at 2am should not require an SSH session.
455
438
 
456
- **Every write is verified.** Saving sends the file to the server, which writes it byte-for-byte
457
- no reformatting, no trailing-newline fixing, no reordering then asks fastlane to parse it and
458
- list its lanes. If that fails, the previous content is put back on disk before the request
459
- answers, and the reason fastlane gave appears above the editor with your work still in the box.
439
+ **Every write is verified.** Saving sends the file to the server, which writes it byte-for-byte then
440
+ asks fastlane to parse it and list its lanes. If that fails, the previous content is restored before
441
+ the request answers, and fastlane's reason appears above the editor with your work still in the box.
460
442
  A broken Fastfile never reaches a workspace a run might build from.
461
443
 
462
- Saving is explicit, never automatic: verification is a Ruby subprocess, not a regular expression,
463
- and an editor that ran it on every keystroke would be both slow and dangerous. `⌘S` is another
464
- way to ask, not an autosave. Laneyard also refuses to write at all while a run of that project is
465
- in flight — that run is reading the very file the write would replace.
444
+ Saving is explicit: verification is a Ruby subprocess, not a regex, and running it on every keystroke
445
+ would be slow and dangerous. `⌘S` is another way to ask, not an autosave. Laneyard also refuses to
446
+ write while a run of that project is in flight that run is reading the very file the write replaces.
466
447
 
467
- Below the editor is what git makes of the workspace: the diff, a message field, `commit` and
468
- `push`. A commit stages exactly the files that changed and never `git add -A` — a build leaves
469
- artifacts and reports scattered around, and none of them belong in your history.
448
+ Below the editor is what git makes of the workspace: the diff, a message field, `commit` and `push`.
449
+ A commit stages exactly the files that changed, never `git add -A` — a build scatters artifacts and
450
+ reports around, and none belong in your history.
470
451
 
471
452
  ### Removing a project
472
453
 
@@ -483,32 +464,33 @@ What it removes:
483
464
  This is the one thing here nothing can rebuild, and the reason removal is behind a typed name;
484
465
  - **the clone and the artifacts on disk**, deleted;
485
466
  - **its secrets and its signing blocks in the vault** — Laneyard's own encrypted copies, forgotten.
486
- The screen counts them before you confirm and again once it is done, because they are the one
487
- thing on this list you cannot go and look at: no route ever sends a credential back.
467
+ The screen counts them before and after, because they are the one thing here you cannot go and look
468
+ at: no route ever sends a credential back.
488
469
 
489
- What it does *not* touch, said as plainly:
470
+ What it does *not* touch:
490
471
 
491
- - **the git remote.** The repository is on your host and your disk. Laneyard neither reads nor
492
- writes it;
493
- - **the credential originals.** The `.p8` and the keystore you uploaded are wherever you keep them
494
- a password manager, a safe. Laneyard removes only its own encrypted copy; you would upload them
495
- again from there;
496
- - **global secrets and global signing blocks.** They are read by every project on the machine, not
497
- this one's to take, and they are left alone. The result names how many it left.
472
+ - **the git remote.** The repository is on your host and disk; Laneyard neither reads nor writes it;
473
+ - **the credential originals.** The `.p8` and keystore you uploaded are wherever you keep them;
474
+ Laneyard removes only its own encrypted copy;
475
+ - **global secrets and global signing blocks.** Read by every project, not this one's to take. The
476
+ result names how many it left.
498
477
 
499
478
  Removal is refused while a run of that project is in flight — that run is using the workspace. A
500
- run still waiting in the queue will not start: it ends as failed, saying its project is gone.
479
+ queued run will not start: it ends as failed, saying its project is gone.
501
480
 
502
- The same thing from the command line:
481
+ The same thing from the command line, run from the app's directory — the one holding its
482
+ `laneyard.yml`:
503
483
 
504
484
  ```bash
505
- laneyard remove cartes-ios --dry-run # show what would go, and stop
506
- laneyard remove cartes-ios # remove it, after a typed confirmation
485
+ cd apps/cartes-ios
486
+ laneyard remove --dry-run # show what would go, and stop
487
+ laneyard remove # remove it, after a typed confirmation
507
488
  ```
508
489
 
509
- It removes exactly what the Settings tab does, leaves exactly what it leaves, and is confirmed the
510
- same way by typing the project's slug back, not `y`. `--dry-run` prints the inventory and stops.
511
- It is refused for an unknown slug, and while a run of the project is in flight.
490
+ No slug to give: it reads one from the `laneyard.yml` there, refusing if the file is missing or has
491
+ no slug (run `laneyard setup` again). It deletes that file too, and says to commit the deletion.
492
+ Otherwise it matches the Settings tab: confirmed by typing the slug back, `--dry-run` stops at the
493
+ inventory, refused during a run.
512
494
 
513
495
  ### Resetting
514
496
 
@@ -517,17 +499,14 @@ laneyard reset --dry-run # show what would go, and stop
517
499
  laneyard reset # wipe it, after a typed confirmation
518
500
  ```
519
501
 
520
- `laneyard reset` wipes the data and keeps you able to use Laneyard: every project, the database,
521
- the workspaces, the artifacts and the logs go; your accounts and the vault key stay. It is a data
522
- reset that does not lock you out you sign in with the same names afterwards — and it keeps the
523
- key, so any older `laneyard.db` backup stays readable rather than becoming ciphertext nobody can
524
- open. The database comes back empty from the schema on the next start, which also clears the
525
- sessions, so everyone signs in again.
502
+ `laneyard reset` wipes the data and keeps you able to use Laneyard: every project, the database, the
503
+ workspaces, the artifacts and the logs go; your accounts and the vault key stay. You sign in with the
504
+ same names afterwards, and keeping the key means an older `laneyard.db` backup stays readable. The
505
+ database comes back empty on the next start, which also clears sessions, so everyone signs in again.
526
506
 
527
- It keeps the `server:` block of `config.yml` (accounts, port, bind, retention) and
528
- `~/.laneyard/key`. It never touches the git remotes or the credential originals those were never
529
- Laneyard's. It reads the inventory first and, like `uninstall`, is confirmed by typing the
530
- folder's path, not `y`.
507
+ It keeps the `server:` block of `config.yml` (accounts, port, bind, retention) and `~/.laneyard/key`,
508
+ and never touches the git remotes or credential originals. It reads the inventory first and, like
509
+ `uninstall`, is confirmed by typing the folder's path, not `y`.
531
510
 
532
511
  ### Uninstalling
533
512
 
@@ -538,25 +517,18 @@ npm uninstall -g laneyard # remove the package itself
538
517
  ```
539
518
 
540
519
  `laneyard uninstall` removes the data folder: `config.yml`, the vault key, the database, the
541
- workspaces, the artifacts and the logs. It reads the whole inventory from disk first — the
542
- projects, the number of secrets and signing blocks, the real sizes and paths — and prints it
543
- before asking anything.
544
-
545
- The vault key is the one loss that cannot be undone. Every secret and every signing block is
546
- encrypted under `~/.laneyard/key`; once it is gone the database is ciphertext nobody can read, and
547
- restoring a backup of `laneyard.db` alone brings nothing back. The originals are yours and are
548
- untouched — the `.p8` in your downloads, the keystore in your safe — so what you are agreeing to
549
- is uploading them again. Global secrets and global signing blocks are shared by every project and
550
- go too; the inventory says so on its own line.
551
-
552
- It is confirmed by typing the folder's path, not `y`: this is the one command in Laneyard that
553
- destroys credentials, and `$LANEYARD_HOME` is exactly the case where a reflex is wrong. Anything
554
- in the folder that Laneyard did not put there is named, left alone, and the folder is kept for it.
555
-
556
- It does not remove the npm package — a command cannot sensibly delete the binary it is running
557
- from — and it prints the command that does. There is no npm lifecycle hook doing any of this on
558
- `npm uninstall`, on purpose: a package manager must not delete someone's signing keys on its own,
559
- and a lifecycle script cannot ask.
520
+ workspaces, the artifacts and the logs. It reads the whole inventory from disk first — projects,
521
+ secret and block counts, sizes and paths — and prints it before asking.
522
+
523
+ The vault key is the one loss that cannot be undone: every secret and block is encrypted under
524
+ `~/.laneyard/key`, and once it is gone the database is ciphertext nobody can read. The originals are
525
+ yours and untouched the `.p8` in your downloads, the keystore in your safe so what you agree to
526
+ is uploading them again. Global secrets and blocks go too; the inventory says so.
527
+
528
+ Confirmed by typing the folder's path, not `y`: this is the one command that destroys credentials.
529
+ Anything in the folder Laneyard did not put there is named, left alone, and the folder kept for it.
530
+ It does not remove the npm package — a command cannot delete the binary it runs from — and prints the
531
+ command that does, on purpose: a package manager must not delete someone's signing keys on its own.
560
532
 
561
533
  ## Security
562
534
 
@@ -572,22 +544,20 @@ Read this before putting Laneyard on a network.
572
544
  - **A role is enforced by the server, not by the interface.** One table names the routes that
573
545
  require an admin, and one hook is the only thing that reads it — there is no permission check
574
546
  hidden inside a handler. What a builder is not shown is also what a builder is refused.
575
- - **Secrets are encrypted at rest.** Values are stored with AES-256-GCM under a key kept in
576
- `~/.laneyard/key` — outside the database, mode `600`, and Laneyard refuses to start if anyone
577
- else can read it. Someone who walks off with `laneyard.db` gets ciphertext. Nothing else in the
578
- process holds plaintext: the store, the API and the interface deal in names only, and no route
579
- ever sends a value back — which is why the Secrets tab has no reveal button.
580
- - **A signing block is on disk only while a run needs it.** A keystore has no string form, so the
581
- file is written into `~/.laneyard/runs/<run id>/secrets/`, mode `600` inside a `700` directory,
582
- and that directory goes when the run ends. The block's secret fields — the two keystore
583
- passphrases are removed from a run's output the same way a masked secret is, because gradle is
584
- perfectly willing to echo one back on failure.
585
- - **Masked values are removed from output before it is written, not when it is displayed.** The
586
- substitution happens once, at the point where a run's output fans out, so the log file on disk,
587
- the live stream to your browser and the stored error summary all contain `••••••` and never the
588
- value. It survives being split across two chunks of terminal output.
589
- - **Do not put secrets in `config.yml`.** It is a plain file with ordinary permissions. Use
590
- `laneyard secret set` or the Secrets tab.
547
+ - **Secrets are encrypted at rest.** Values are stored with AES-256-GCM under a key in
548
+ `~/.laneyard/key` — outside the database, mode `600`, and Laneyard refuses to start if anyone else
549
+ can read it. Someone who walks off with `laneyard.db` gets ciphertext. Nothing else holds
550
+ plaintext: the store, API and interface deal in names only, and no route sends a value back — which
551
+ is why the Secrets tab has no reveal button.
552
+ - **A signing block is on disk only while a run needs it.** The file is written into
553
+ `~/.laneyard/runs/<run id>/secrets/`, mode `600` in a `700` directory, and that directory goes when
554
+ the run ends. The block's secret fields — the keystore passphrases — are stripped from output like
555
+ any masked secret, because gradle will echo one back on failure.
556
+ - **Masked values are removed from output before it is written, not when displayed.** The
557
+ substitution happens once, where a run's output fans out, so the log file, the live stream and the
558
+ stored error summary all hold `••••••`. It survives being split across two chunks of output.
559
+ - **Do not put secrets in `config.yml`.** It is a plain file with ordinary permissions. Use the
560
+ Secrets tab, which puts them in the encrypted vault instead.
591
561
 
592
562
  What this does *not* cover, stated plainly:
593
563
 
@@ -602,11 +572,10 @@ What this does *not* cover, stated plainly:
602
572
  - **Anything fastlane prints that is not a stored secret is stored in the clear**, under
603
573
  `~/.laneyard/logs/`.
604
574
  - **`key.properties` is written into the workspace, and it holds passwords.** It is the one
605
- credential Laneyard puts in the clone rather than in the run's own directory, because Gradle
606
- resolves that path relative to the build. It is mode `600`, carries a marker line as its first
607
- line, is removed when the run ends, and is swept for again at the start of the next run in case a
608
- server was killed mid-build. A file of yours without that marker is never written over and never
609
- removed.
575
+ credential Laneyard puts in the clone rather than the run's own directory, because Gradle resolves
576
+ that path relative to the build. Mode `600`, a marker as its first line, removed when the run ends
577
+ and swept for at the start of the next in case a server was killed mid-build. A file of yours
578
+ without that marker is never touched.
610
579
 
611
580
  ## Status
612
581
 
@@ -624,13 +593,13 @@ What this does *not* cover, stated plainly:
624
593
  - `✓` remove a project from the interface — everything Laneyard holds for it, behind a typed name
625
594
  - `✓` `laneyard uninstall`: the whole inventory first, then a typed confirmation, then the folder
626
595
  - `✓` named accounts, with a builder role that never sees a credential
596
+ - `✓` setup names the credentials a Fastfile hardcodes, and offers — refusably — to lift them into
597
+ the vault and patch the file
627
598
  - `○` git-triggered and scheduled builds
628
599
 
629
- Two things worth knowing today: listing lanes does not fetch the repository, so a lane you have
630
- just pushed appears after the next run; and runs execute one at a time across all projects, so a
631
- build triggered while another is going waits its turn rather than starting alongside it. A queued
632
- run survives a restart of the server — it is still queued when it comes back up, and starts on
633
- its own.
600
+ Two things worth knowing: listing lanes does not fetch the repository, so a lane you just pushed
601
+ appears after the next run; and runs execute one at a time across all projects. A queued run survives
602
+ a server restart and starts on its own.
634
603
 
635
604
  ## Changelog
636
605