Why should a CI runner call match with readonly: true for an iOS build?
answer
- fetch and install, never create
- fails instead of minting a certificate
- CI consumes, developers publish
- protects limited certificate slots
- regeneration needs force, not readonly
basics
~20 sreadonly true makes sync_code_signing only fetch and install existing Apple certificates and profiles, never creating, renewing or revoking any. A CI runner then cannot mint a new certificate, exhaust the team's certificate slots, or write to the shared signing repository.
solid answer
~40 s`readonly: true` puts `sync_code_signing` in fetch-and-install mode: it reads the storage backend, decrypts, installs the certificate and profile for the requested `app_identifier` and `type`, and stops. If nothing matches, the lane **fails** rather than creating a new identity. That is what you want on CI for the falconry weight-log iOS app, because a runner allowed to create will quietly enrol a fresh distribution certificate on every clean machine, burn the team's limited slots, and push changes into the shared repository from a job nobody is watching. Creation stays a deliberate act on a developer machine running without `readonly`, which then publishes the new encrypted assets for everyone; CI only consumes them.
code
ruby · 11 linesplatform :ios do
lane :sync_signing do |options|
sync_code_signing(
type: "appstore",
app_identifier: "com.mews.falconry-weight-log",
storage_mode: "git",
git_url: "[email protected]:mews/falconry-signing.git",
readonly: options.fetch(:readonly, true)
)
end
endgo deeper
Remember the one-line rule: on CI, match runs readonly, and readonly means it installs what already exists and never creates anything new.
Explain the mechanics both ways — what a readonly run does step by step, and what a read-write run would have done instead when it finds no matching certificate or profile.
Bring the operational argument: certificate quotas, unattended writes to the team's key vault, and the discipline of fixing the cause on a developer machine rather than flipping the flag under release pressure.
Frame it as a permissions boundary across the estate — which machines may create identities, how that is enforced rather than merely configured, and what a leaked runner credential can actually do.
## What readonly actually changes `readonly` is an option on `sync_code_signing` — the canonical action behind `match` — and it draws a line through the middle of the action's behaviour. With `readonly: true` a run may **fetch, decrypt and install**; it may not **create, renew, revoke or publish**. Concretely, for the falconry weight-log iOS build, a readonly run reads the backend named by `storage_mode`, decrypts with the team passphrase, finds the material matching `app_identifier` and the requested `type`, imports the certificate into the keychain and drops the profile where Xcode will find it. If nothing matches, it fails. That last clause is the whole point. A read-write run treats *nothing matches* as an instruction to go and make one. | | `readonly: true` (CI) | read-write (a developer machine) | |---|---|---| | missing profile | the lane fails with an error | the profile is created at the portal | | missing certificate | the lane fails | a new certificate is enrolled against the team quota | | storage backend | read only | freshly encrypted assets are written back | | portal activity | none of consequence | creates, and with the right options renews or removes | | where it belongs | every CI runner | one machine, with a person watching | ## Why a writing CI runner is a slow-motion outage A CI runner is, by design, a machine with no memory. Every job starts from a clean image, which means every job starts with no certificate. Give that job permission to create one and you have built a certificate factory: - **Quota exhaustion.** Apple allows a team only a small number of distribution certificates. A pipeline that enrols one per clean runner reaches the ceiling within days, and the failure surfaces as a completely unrelated build error. - **Silent identity churn.** Each new certificate is written back into the shared repository, so the next developer to sync picks up an identity created by a machine, not a person. Nobody can say which certificate the last release was signed with. - **Unattended writes.** The runner is pushing to the vault that holds every private key the team owns, at 3 a.m., with no review. - **Lost failures.** Auto-creation hides the real problem. A missing profile usually means something upstream is wrong — a device was added, a `type` is misspelled, the wrong branch of the storage repo is checked out — and creating a replacement papers over it. ## What the failure looks like, and how you fix it When a readonly run fails, the error is a good one: match says it cannot find matching material and will not create it. The fix is a sequence, not a switch: 1. Work out *why* nothing matched — usually a new registered device invalidating an `adhoc` profile, an expired certificate, or a mismatched `app_identifier` or `type`. 2. On a machine that holds portal credentials and write access to the backend, run `sync_code_signing` **without** `readonly` for that `app_identifier` and `type`. 3. Let it write the new encrypted assets back to the storage backend. 4. Re-run the CI lane unchanged. Its readonly run now finds the material and installs it. The temptation, when a release is late, is to flip `readonly` off in the CI lane 'just this once'. That change tends to survive, and then step 1 never happens again. ## The options that deliberately do write Readonly is the default posture, not the only one. The write-side options exist for the developer machine in step 2: - `force` — regenerate the provisioning profile even though a usable one exists. - `force_for_new_devices` — regenerate when the registered device list has changed, the usual cause of a broken `adhoc` profile for beta testers of the weight-log app. - `force_for_new_certificates` — regenerate profiles when the certificate underneath them changed. - `renew_expired_certs` and `safe_remove_certs` — deliberate lifecycle operations on the certificates themselves. None of these belong in a lane a runner executes unattended. Read the pairing as a permissions model: **CI consumes, humans publish.** ## Least privilege, not zero credentials Readonly narrows what the runner can do; it does not make credentials disappear. The job still needs to reach the storage backend, still needs the decryption passphrase, and depending on how the lane is written may still pass an `api_key` or `username` for the portal calls that remain. Where those values live in the pipeline is a CI secrets question rather than a match option — but the security argument for `readonly` is exactly the least-privilege one: even if the runner's credentials leak, they buy read access to ciphertext rather than the ability to mint an identity in your team's name. There is no Android equivalent of this switch. match syncs Apple material only; an Android release job obtains its keystore by other means, configured in the Gradle build.
- Your readonly CI lane fails because the ad-hoc profile is missing a newly registered iPhone. What is the fix?Regenerate on a machine that may write: run `sync_code_signing` without `readonly` and with `force_for_new_devices`, which recreates the ad-hoc profile covering the new device and stores the updated encrypted assets. CI then picks it up on its next readonly run, and the runner is never granted permission to do this itself.
- Does readonly mode remove the runner's need for Apple credentials entirely?It removes the *write* activity — nothing is created, renewed or revoked from the runner. The job still needs to reach the storage backend, hold the decryption passphrase and, depending on the lane, an `api_key` or `username` for the portal calls that remain. Treat readonly as least privilege, not as zero credentials.
saying these in an interview costs you the question
- Thinks readonly makes match skip signing entirely
- Lets CI create certificates so builds never fail
- Believes readonly still renews an expired certificate
- Confuses readonly with shallow_clone or skip_docs
- Assumes each runner needs its own certificate