Slice 2 of #154: secrets: in the workflow, delivery at lease time, and the missing-name warning.
What it adds
- The field.
secrets:on a job, one name or a list, names only. Upper-case letters, digits and_; duplicates and the runner’s own variables are refused at push. No cap on how many a workflow declares. - Pinning. The names are pinned on the run row beside
envandartifacts, so editing the workflow afterwards cannot change what a queued run reads. - Delivery. At lease time the server resolves the names and sends
{name, version, value}on the channel that already carries the job. The queue never holds a value, and the store records the read against the account holding the lease. - The runner. Writes
export NAME='value'lines, POSIX-quoted, tosecrets.envin the run directory it already mounts at/run/volefor an event payload, and prefixes each step with. /run/vole/secrets.env. Values never appear in an argv:--envwould expose them to every process on the host, and--env-filecannot carry a line break, which is what a key has. - Absent is not a failure. The variable is left unset, not empty, and the log
gets one line:
vole: DEPLOY_TOKEN is not set for this repository, so this job runs without it.A required secret is${DEPLOY_TOKEN:?…}in the step, so the message is the author’s. - The record. The attempt names
{name, version}for each value the job ran with.checkRecordrefuses a name the job did not declare, so a runner cannot claim a value it was not given. - Local runs.
vole workflow runwrites the same file, reading the values with the developer’s own credential from the instance the checkout’s remote names, and leaves unset what it cannot read, with a note.
Decisions worth reviewing
- The name rule moved to
ci.forge/secretnow usesci.ValidateSecretNameandci.MaxSecretBytes, so there is one rule instead of two that can drift: the store refuses a name no workflow could declare, and a workflow refuses a name no process environment could carry. Values over 128 KiB are refused when stored, with that reason. - The secrets file is sourced, not passed. Both obvious alternatives fail for a
real key:
--env-fileis oneNAME=valueper line, so a PEM would be truncated silently, and--envputs the value in the runtime’s argument list. - A missing value is a log line, not a failed run. Optional secrets work, and a deploy whose token is absent fails in the deploy script with the message that script chose, rather than with a queue error nobody can act on.
- The test runtime learned about mounts. The integration tests’ fake docker
records
--volumepairs soexeccan resolve/run/vole(and/workspace) to the host directory. Before this, a step could not read the event payload either; the new tests are what needed it.
Not in this change
Slice 3: masking values in the log, and the remaining copy sweep. The reference page
therefore says plainly that a step which prints a secret writes it to the log; that
sentence is what slice 3 replaces. The two trust statements are already in
docs/runner.md and docs/accounts-and-access.md, because they are true the moment
delivery exists.
Checks
go vet ./...,GOFLAGS=-tags=sqlite_fts5 go test ./...,vole workflow check,python3 scripts/check-links.py- New tests: the field’s parsing and refusals; the runner’s re-validation of a
leased value; shell round-trip of a value with quotes,
$, backticks and a line break; the step’s source line; and two end-to-end runs — one that reads a value and one whose secret is absent, including the record and the log lines.