laneyard 0.4.1 → 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 +312 -282
- package/dist/src/cli/adopt.js +221 -0
- package/dist/src/cli/home.js +46 -0
- package/dist/src/cli/remove.js +195 -0
- package/dist/src/cli/reset.js +117 -0
- package/dist/src/cli/setup.js +123 -11
- package/dist/src/cli/uninstall.js +2 -24
- package/dist/src/config/accounts.js +103 -39
- package/dist/src/config/load.js +3 -27
- package/dist/src/config/resolve.js +36 -0
- package/dist/src/config/schema.js +25 -9
- package/dist/src/config/store.js +27 -3
- package/dist/src/data/remove-project.js +70 -0
- package/dist/src/db/runs.js +11 -0
- package/dist/src/fastfile/adoption.js +142 -0
- package/dist/src/fastfile/splice.js +38 -0
- package/dist/src/logs/store.js +9 -1
- package/dist/src/main.js +24 -16
- package/dist/src/secrets/vault.js +6 -5
- package/dist/src/server/app.js +17 -12
- package/dist/src/server/permissions.js +50 -0
- package/dist/src/server/routes/account.js +56 -1
- package/dist/src/server/routes/fastfile.js +7 -0
- package/dist/src/server/routes/projects.js +95 -86
- package/dist/src/server/routes/users.js +47 -4
- package/dist/src/sidecar/bridge.js +5 -4
- package/dist/src/sidecar/fastlane-dir.js +8 -8
- package/dist/src/sidecar/prism-ruby.js +42 -0
- package/dist/src/sidecar/scan.js +44 -0
- package/dist/web/assets/{Editor-9nLYwtg7.js → Editor-CMa4-4N3.js} +1 -1
- package/dist/web/assets/index-B8lAQPEM.js +62 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/ruby/scan.rb +176 -0
- package/dist/web/assets/index-pzPa9EZr.js +0 -62
package/README.md
CHANGED
|
@@ -2,33 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
**A self-hosted web UI for fastlane.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
10
|
-
they are.
|
|
9
|
+
It drives fastlane, it doesn't replace it. Your lanes stay where they are.
|
|
11
10
|
|
|
12
11
|

|
|
13
12
|
|
|
14
13
|
## Why
|
|
15
14
|
|
|
16
|
-
**You keep the keys.**
|
|
17
|
-
|
|
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
|
|
21
|
-
|
|
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,
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
73
|
-
|
|
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
|

|
|
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
|
|
@@ -103,7 +160,7 @@ server:
|
|
|
103
160
|
bind: 0.0.0.0
|
|
104
161
|
users: # written by `laneyard setup`, see Accounts
|
|
105
162
|
- { name: martin, role: admin, password_hash: "scrypt$…" }
|
|
106
|
-
- { name: lea, role: builder, password_hash: "scrypt$…" }
|
|
163
|
+
- { name: lea, role: builder, password_hash: "scrypt$…", projects: [cartes-ios] }
|
|
107
164
|
max_concurrent_runs: 1 # only 1 is accepted, see below
|
|
108
165
|
retention: { runs: 50, artifact_days: 30 }
|
|
109
166
|
|
|
@@ -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`
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
126
|
-
|
|
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,13 +191,29 @@ easy to add and impossible to remove.
|
|
|
135
191
|
| remove a project | ✓ | |
|
|
136
192
|
| manage accounts | ✓ | |
|
|
137
193
|
|
|
138
|
-
A builder is
|
|
139
|
-
|
|
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.
|
|
196
|
+
|
|
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.
|
|
140
200
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
201
|
+
#### Which projects a builder reaches
|
|
202
|
+
|
|
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.
|
|
207
|
+
|
|
208
|
+
The reach is a `projects` list on the account in `config.yml`, with three states:
|
|
209
|
+
|
|
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.
|
|
213
|
+
- **a list of slugs** — exactly those projects.
|
|
214
|
+
|
|
215
|
+
Removing a project strips its slug from every account, so a grant never points at a project that is
|
|
216
|
+
gone, and a re-created slug does not inherit an old grant.
|
|
145
217
|
|
|
146
218
|
Add and remove accounts from the accounts screen, or from the command line:
|
|
147
219
|
|
|
@@ -149,28 +221,24 @@ Add and remove accounts from the accounts screen, or from the command line:
|
|
|
149
221
|
echo "$PASSWORD" | laneyard user add lea --role builder
|
|
150
222
|
```
|
|
151
223
|
|
|
152
|
-
The password is read from standard input, never
|
|
153
|
-
|
|
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.
|
|
154
226
|
|
|
155
|
-
Two things are refused, in the API and
|
|
156
|
-
|
|
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.
|
|
157
229
|
|
|
158
|
-
Anyone changes their own password from **your account
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
that account has, and leaves the page you did it on signed in. That is how the random password
|
|
163
|
-
`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.
|
|
164
234
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
168
238
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
all. Do not write both forms: a file holding a `password_hash` *and* a `users` list is refused at
|
|
173
|
-
load, because there is no obvious winner.
|
|
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.
|
|
174
242
|
|
|
175
243
|
### `laneyard.yml` — in your repository, and committed
|
|
176
244
|
|
|
@@ -192,75 +260,44 @@ platforms: [ios] # or `[android]`, or both
|
|
|
192
260
|
asked for an App Store Connect key. Left out, Laneyard looks at the repository — an Xcode project
|
|
193
261
|
means iOS, a Gradle build means Android — and reports what it found rather than assuming.
|
|
194
262
|
|
|
195
|
-
It looks **beside the Fastfile**, not at the repository root, because that is where an app keeps
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
found, and a project configured with `ios/fastlane` reports iOS alone rather than being shown the
|
|
199
|
-
Android section on the strength of a sibling folder its lanes never touch. When that guess is
|
|
200
|
-
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.
|
|
201
266
|
|
|
202
267
|
Field by field, the repository file wins over the server block, which wins over the defaults. Any
|
|
203
|
-
field
|
|
204
|
-
|
|
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.
|
|
205
276
|
|
|
206
277
|
Both files are watched: edit them by hand and Laneyard picks the change up. An invalid file is
|
|
207
278
|
reported and the last valid configuration stays live — a typo never takes the server down.
|
|
208
279
|
|
|
209
280
|
### Secrets
|
|
210
281
|
|
|
211
|
-
The variables your lanes read
|
|
212
|
-
|
|
213
|
-
vault
|
|
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).
|
|
214
285
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
The value is never an argument: a command line ends up in `~/.zsh_history` and in the output of
|
|
221
|
-
`ps`. Typing the command alone leaves you at a blank line — type or paste the value, then
|
|
222
|
-
`Ctrl-D`.
|
|
223
|
-
|
|
224
|
-
**Reading one back.** The vault is write-only for anything you called a secret: the server never
|
|
225
|
-
sends a masked value back, so the interface has nothing to uncover and no browser ever holds one.
|
|
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.
|
|
226
289
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
change which it is without touching the value — otherwise revealing something would mean first
|
|
232
|
-
retyping the value you were trying to read.
|
|
233
|
-
|
|
234
|
-
**Bring the ones you already have.** A project that builds today has its variables in
|
|
235
|
-
`fastlane/.env` — gitignored, on one laptop, and therefore absent from the clone a build runs
|
|
236
|
-
from. From that working copy:
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
laneyard secret import --project cartes-ios # shows what it would store
|
|
240
|
-
laneyard secret import --project cartes-ios --yes # stores it
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
It runs from the CLI because that is where the `.env` is; the server only ever sees a clone. A
|
|
244
|
-
variable naming a service account JSON has the **file's contents** stored, under `SUPPLY_JSON_KEY_DATA`
|
|
245
|
-
— a name supply reads on its own — because a path does not travel to another machine. A variable
|
|
246
|
-
naming a `.p8` is reported and left alone: no action in fastlane reads a `.p8` out of an environment
|
|
247
|
-
variable, and that credential belongs in a signing block, described below. Everything stored is
|
|
248
|
-
masked, and nothing is printed but names.
|
|
249
|
-
|
|
250
|
-
Nothing in your lanes has to change afterwards. `key_filepath:` and `json_key:` keep working as
|
|
251
|
-
written — a signing block puts a real file back on disk for the length of a run.
|
|
252
|
-
|
|
253
|
-
A secret becomes an environment variable for every run of the project it belongs to. Without
|
|
254
|
-
`--project` it applies to every project; a project secret of the same name wins over a global
|
|
255
|
-
one. Secrets are kept out of the logs unless you pass `--no-mask`, and a masked value must be at
|
|
256
|
-
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).
|
|
257
294
|
|
|
258
295
|
### Signing credentials
|
|
259
296
|
|
|
260
|
-
A signing credential is not a string
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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.
|
|
264
301
|
|
|
265
302
|
| block | the file | the fields beside it |
|
|
266
303
|
| ------------------------------ | ---------------------- | ------------------------------------------- |
|
|
@@ -268,69 +305,55 @@ stored without its alias is not a partial success; it is a build that fails in a
|
|
|
268
305
|
| *android upload keystore* | `.jks` or `.keystore` | key alias, store password, key password |
|
|
269
306
|
| *play store service account* | JSON | — |
|
|
270
307
|
|
|
271
|
-
They live in the **signing** part of a project's Secrets tab
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
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.
|
|
275
311
|
|
|
276
312
|
A variable a block has made redundant is said to be so, beside the row and in one sentence:
|
|
277
313
|
`SUPPLY_JSON_KEY_DATA` once a Play block applies — it still works, but the same credential is then
|
|
278
314
|
stored twice — and `APP_STORE_CONNECT_API_KEY_P8`, which no action in fastlane has ever read.
|
|
279
315
|
Neither is removed for you: the row is yours, and the button to drop it is on the same line.
|
|
280
316
|
|
|
281
|
-
**A block becomes real files, for the length of a run.** Gradle's `storeFile`
|
|
282
|
-
`app_store_connect_api_key`
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
`
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
length of the run, out of the keystore block. Two things about that file cannot be read out of a
|
|
308
|
-
build script and are asked for on the block instead: where it goes, when the script names it in a
|
|
309
|
-
way that leaves the directory unresolved, and what the keys inside it are called, which start from
|
|
310
|
-
the Flutter documentation's four. Asking at configuration time is allowed; requiring a change to
|
|
311
|
-
your repository is not.
|
|
312
|
-
|
|
313
|
-
**Only the projects that sign need any of this.** fastlane is not only for shipping to stores —
|
|
314
|
-
lanes take screenshots, run tests, sync certificates — so the three blocks are an offer rather than
|
|
315
|
-
a gate. A project that wants an artifact out of a Gradle build needs the keystore and nothing else,
|
|
316
|
-
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.
|
|
317
343
|
|
|
318
344
|
A block stored on a project belongs to that project. One stored globally applies to every project,
|
|
319
345
|
and a project's own block wins over it. Both are admin-only, like the rest of the vault.
|
|
320
346
|
|
|
321
347
|
### Readiness
|
|
322
348
|
|
|
323
|
-
Every project has a Readiness tab: what stands between it and a build that runs while nobody
|
|
324
|
-
|
|
325
|
-
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.
|
|
326
351
|
|
|
327
|
-
**What a tick means.** The checks read your Fastfile, following a lane into the methods
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
*could not tell* rather than ticking. A green tick here means "looked, and it is fine" — never
|
|
333
|
-
"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".
|
|
334
357
|
|
|
335
358
|
Always:
|
|
336
359
|
|
|
@@ -341,17 +364,14 @@ Always:
|
|
|
341
364
|
- **no lane calls an action known to stop and ask** — `prompt`, `sigh`, `cert`, a writable
|
|
342
365
|
`match`, an upload waiting for its summary to be confirmed;
|
|
343
366
|
- **the variables the lanes read** are in the vault. Every `ENV.fetch("…")` a lane reaches is
|
|
344
|
-
collected and looked up
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
and `required_secrets` in `laneyard.yml` covers whatever it does not. A variable found only in
|
|
353
|
-
the server's own environment is reported rather than ticked over: it works, but it works because
|
|
354
|
-
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.
|
|
355
375
|
|
|
356
376
|
A name a signing block exports counts as being in the vault, since that is where the block is:
|
|
357
377
|
a lane reading `SUPPLY_JSON_KEY` with a Play block stored is not asked for it again, on this
|
|
@@ -379,16 +399,14 @@ On Android:
|
|
|
379
399
|
passphrase, and one that is neither in the call nor in the vault makes gradle stop and ask. A
|
|
380
400
|
keystore block settles this before the lanes are read at all — the file and both passphrases
|
|
381
401
|
reach the run together, so nothing can stop and ask, whichever lane runs;
|
|
382
|
-
- **the release is signed with the release key.** The one check
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
it, marked `# written by laneyard, do not commit`, and is removed when fastlane stops. A file of
|
|
391
|
-
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;
|
|
392
410
|
- **the Play Store service account** is there when a lane calls `upload_to_play_store`. The vault
|
|
393
411
|
first — a block, or a `SUPPLY_JSON_KEY` variable stored before blocks existed — then the
|
|
394
412
|
`json_key` argument in the call, then the **Appfile**: `json_key_file` and `json_key_data`, which
|
|
@@ -398,75 +416,97 @@ On Android:
|
|
|
398
416
|
Like the iOS ones, the Android checks read **literal arguments only**. `gradle(storePassword:
|
|
399
417
|
ENV["PW"])` is reported as undetermined, never guessed at: a checklist that guesses gets believed.
|
|
400
418
|
|
|
401
|
-
They run when you open the tab or press refresh, never on their own
|
|
402
|
-
bundler. The
|
|
403
|
-
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.
|
|
404
421
|
|
|
405
|
-
Nothing here blocks anything. A red check is never
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
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.
|
|
409
425
|
|
|
410
426
|
**What it cannot see.** The checklist reads *literal* arguments only. `match(readonly: true)` is
|
|
411
|
-
green
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
script rather than the Fastfile, since that is where it lives — and read as text, because running
|
|
416
|
-
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.
|
|
417
431
|
|
|
418
432
|
### The Fastfile
|
|
419
433
|
|
|
420
|
-
Every project has a Fastfile tab. **It is a text editor** — your file, in a box, with Ruby
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
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.
|
|
425
438
|
|
|
426
|
-
**Every write is verified.** Saving sends the file to the server, which writes it byte-for-byte
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
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.
|
|
430
442
|
A broken Fastfile never reaches a workspace a run might build from.
|
|
431
443
|
|
|
432
|
-
Saving is explicit
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
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.
|
|
436
447
|
|
|
437
|
-
Below the editor is what git makes of the workspace: the diff, a message field, `commit` and
|
|
438
|
-
|
|
439
|
-
|
|
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.
|
|
440
451
|
|
|
441
452
|
### Removing a project
|
|
442
453
|
|
|
443
|
-
Every project has a Settings tab, and the one thing on it is removal. It
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
What it
|
|
449
|
-
|
|
450
|
-
- **
|
|
451
|
-
|
|
452
|
-
- **
|
|
453
|
-
|
|
454
|
-
- **the
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
454
|
+
Every project has a Settings tab, and the one thing on it is removal. It removes everything
|
|
455
|
+
Laneyard holds for the project, in one confirmed act. It is confirmed by typing the project's
|
|
456
|
+
name: it is the one destructive action in Laneyard, and a dialogue you can click through is not a
|
|
457
|
+
confirmation.
|
|
458
|
+
|
|
459
|
+
What it removes:
|
|
460
|
+
|
|
461
|
+
- **its block in `config.yml`** — taken out through the YAML document, so your comments and your
|
|
462
|
+
key order survive;
|
|
463
|
+
- **its run history and its logs.** Every build the project ran, its rows and its logs, deleted.
|
|
464
|
+
This is the one thing here nothing can rebuild, and the reason removal is behind a typed name;
|
|
465
|
+
- **the clone and the artifacts on disk**, deleted;
|
|
466
|
+
- **its secrets and its signing blocks in the vault** — Laneyard's own encrypted copies, forgotten.
|
|
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.
|
|
469
|
+
|
|
470
|
+
What it does *not* touch:
|
|
471
|
+
|
|
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.
|
|
461
477
|
|
|
462
478
|
Removal is refused while a run of that project is in flight — that run is using the workspace. A
|
|
463
|
-
run
|
|
479
|
+
queued run will not start: it ends as failed, saying its project is gone.
|
|
480
|
+
|
|
481
|
+
The same thing from the command line, run from the app's directory — the one holding its
|
|
482
|
+
`laneyard.yml`:
|
|
464
483
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
484
|
+
```bash
|
|
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
|
|
488
|
+
```
|
|
489
|
+
|
|
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.
|
|
494
|
+
|
|
495
|
+
### Resetting
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
laneyard reset --dry-run # show what would go, and stop
|
|
499
|
+
laneyard reset # wipe it, after a typed confirmation
|
|
500
|
+
```
|
|
501
|
+
|
|
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.
|
|
506
|
+
|
|
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`.
|
|
470
510
|
|
|
471
511
|
### Uninstalling
|
|
472
512
|
|
|
@@ -477,25 +517,18 @@ npm uninstall -g laneyard # remove the package itself
|
|
|
477
517
|
```
|
|
478
518
|
|
|
479
519
|
`laneyard uninstall` removes the data folder: `config.yml`, the vault key, the database, the
|
|
480
|
-
workspaces, the artifacts and the logs. It reads the whole inventory from disk first —
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
destroys credentials, and `$LANEYARD_HOME` is exactly the case where a reflex is wrong. Anything
|
|
493
|
-
in the folder that Laneyard did not put there is named, left alone, and the folder is kept for it.
|
|
494
|
-
|
|
495
|
-
It does not remove the npm package — a command cannot sensibly delete the binary it is running
|
|
496
|
-
from — and it prints the command that does. There is no npm lifecycle hook doing any of this on
|
|
497
|
-
`npm uninstall`, on purpose: a package manager must not delete someone's signing keys on its own,
|
|
498
|
-
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.
|
|
499
532
|
|
|
500
533
|
## Security
|
|
501
534
|
|
|
@@ -511,22 +544,20 @@ Read this before putting Laneyard on a network.
|
|
|
511
544
|
- **A role is enforced by the server, not by the interface.** One table names the routes that
|
|
512
545
|
require an admin, and one hook is the only thing that reads it — there is no permission check
|
|
513
546
|
hidden inside a handler. What a builder is not shown is also what a builder is refused.
|
|
514
|
-
- **Secrets are encrypted at rest.** Values are stored with AES-256-GCM under a key
|
|
515
|
-
`~/.laneyard/key` — outside the database, mode `600`, and Laneyard refuses to start if anyone
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
- **A signing block is on disk only while a run needs it.**
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
- **Do not put secrets in `config.yml`.** It is a plain file with ordinary permissions. Use
|
|
529
|
-
`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.
|
|
530
561
|
|
|
531
562
|
What this does *not* cover, stated plainly:
|
|
532
563
|
|
|
@@ -541,11 +572,10 @@ What this does *not* cover, stated plainly:
|
|
|
541
572
|
- **Anything fastlane prints that is not a stored secret is stored in the clear**, under
|
|
542
573
|
`~/.laneyard/logs/`.
|
|
543
574
|
- **`key.properties` is written into the workspace, and it holds passwords.** It is the one
|
|
544
|
-
credential Laneyard puts in the clone rather than
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
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.
|
|
549
579
|
|
|
550
580
|
## Status
|
|
551
581
|
|
|
@@ -560,16 +590,16 @@ What this does *not* cover, stated plainly:
|
|
|
560
590
|
- `✓` edit the Fastfile in the browser, verified on every save
|
|
561
591
|
- `✓` signing credentials stored whole — the file and the fields beside it — written to disk for
|
|
562
592
|
the length of a run and exported under the names your project already reads
|
|
563
|
-
- `✓` remove a project from the interface,
|
|
593
|
+
- `✓` remove a project from the interface — everything Laneyard holds for it, behind a typed name
|
|
564
594
|
- `✓` `laneyard uninstall`: the whole inventory first, then a typed confirmation, then the folder
|
|
565
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
|
|
566
598
|
- `○` git-triggered and scheduled builds
|
|
567
599
|
|
|
568
|
-
Two things worth knowing
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
run survives a restart of the server — it is still queued when it comes back up, and starts on
|
|
572
|
-
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.
|
|
573
603
|
|
|
574
604
|
## Changelog
|
|
575
605
|
|