id: BB-SPEC-251EC85B type: spec title: Luci CI service research:
- BB-RESEARCH-0B529BC1
Luci CI service
Boundary
Luci runs jobs; repos own domain logic. Every visible repo, run, log, and artifact is public-readable. Private repos and authenticated UI are out of scope. Reports and action inbox preserve evidence and unresolved work.
Configuration
Only direct .ci/*.kdl files define jobs, in lexical file and declaration order.
Job names are unique; each job requires one nonblank image and at least one command.
Unknown or ambiguous syntax fails.
KDL declares; repo programs compute.
No programmable conditions, loops, expressions, or conditional commands.
Bounded matrices and explicit metadata interpolation remain configuration, not control flow.
Ordered run commands share one shell, child container, and workspace.
A KDL filename in a command does not implicitly launch a workflow.
If declarative limits require generation, repo programs generate KDL in their chosen language; Luci does not grow a pseudo-language.
Dynamic workflow execution is outside this contract.
Triggers and admission
Triggers are additive and opt-in; no triggers means manual-only. Push filters cover branches, tags, included paths, and excluded paths. A push requires a matching ref and at least one included, non-excluded changed path; without path filters, ref matching suffices. Manual submission selects an existing job by exactly one ref or revision, ignoring automatic filters. Ref polling establishes a silent baseline, then queues changed branches and tags.
A job permits one five-field UTC cron schedule, evaluated from the symbolic default branch at minute precision. Admission pins its commit and identifies the occurrence by repo, job, and scheduled UTC minute. No downtime backfill, duplicate minute, or clock-rollback replay. Schedules run at most once per 15 minutes; ticks coalesce while that job is pending, claimed, or active. At most 64 scheduled parents wait; excess ticks drop without backfill. Push and manual requests take priority over queued schedules.
Execution
One luci serve consumer owns execution for one data set.
Manual and forced-SSH commands submit requests or read state; they never execute containers directly.
Admission freezes revision and selected jobs.
Matrix expansion and execution order are deterministic; at most 64 children per job matrix.
Children execute sequentially; first failure fails the parent and skips remaining children.
CI failure is a normal persisted outcome.
CI_* metadata comes only from the frozen plan.
Each child gets a pinned detached shallow checkout, local Git metadata, and no remote. Containers run rootless with dropped capabilities, no-new-privileges, and timeout, memory, CPU, and process limits. Repo config cannot request privilege, capabilities, host mounts, or container sockets. Image and publish operands cannot become Podman flags.
State
The execution-queue filename stem identifies the run across pending, active, history, workspace, logs, UI, and CLI.
Child IDs are positive zero-padded ordinals; parent child ID is run.
Display names never identify storage.
History is immutable, size-safe, failure-atomic, and recoverable. Terminal parent and done marker become durable before acknowledging claimed work. Recovery never replays claimed work: record interruptions, acknowledge completed work, dead-letter unresolved processing state.
Pending comes from execution-queue files, running from active records, terminal from history. UI and CLI share one projection: latest parent failure > progress > latest parent success. Malformed individual pending/active records produce warnings; unreadable required sources fail projection.
Watch
Local and forced-SSH luci watch share behavior.
Selectors follow show: latest by default, repo, full ID, or unique supported prefix, including queued runs.
Resolve once; never follow a newer run accidentally.
Watcher lifetime is independent of run lifetime.
stateDiagram-v2
[*] --> Resolve
Resolve --> Error: Invalid or ambiguous selector
Resolve --> Follow: Pin run and emit snapshot
Follow --> Follow: State changes and optional log chunks
Follow --> Finished: Terminal run, final logs, summary
Follow --> Timeout: Explicit deadline
Follow --> Stopped: Interrupt or disconnect
Follow --> Error: Read or output failure
Finished --> [*]
Timeout --> [*]
Stopped --> [*]
Error --> [*]
Human output is append-only: proquint ID, repo/job, full revision, timestamps. NDJSON carries the same facts through snapshot, parent, child, log, and summary events, with canonical IDs and UTC timestamps. Zero-child success explicitly means zero children, not checks passed.
Optional log following preserves final tails and character boundaries without duplicate chunks, replay, or terminal controls. Reads and buffers are bounded; completed logs are not repeatedly reread. Claim gaps and between-child gaps preserve identity; terminal history outranks stale active records.
No default timeout; explicit timeout must be positive. Exit codes: 0 success, 1 unsuccessful run, 2 command/operational error, 124 timeout, 130 cancellation. Interrupt/disconnect stops only the watcher; every exit releases its resources.
Logs and workspace
Live logs become compressed completed logs. Inline output is a bounded tail; raw output validates and spools the full readable stream before success. Presentation strips terminal controls; secret masking remains best-effort, never a security boundary. Completed workspaces retain no source, outputs, or injected secrets. Persistent keyed caches are locked, non-authoritative, and separate from workspaces.
Secrets and mounts
Operator-managed secrets are scoped by repo/job; missing secrets fail before execution. File secrets use temporary read-only binds; environment secrets use temporary env files, never process arguments. Temporary secret material is removed on every outcome.
Durable mounts use single safe-segment names and full-job locks.
Operator-created auth mounts are required, private, read-write, and excluded from UI/artifact collection.
Luci-created cache mounts are read-write, disposable, and non-authoritative.
Publishing and artifacts
Publishing requires successful commands; artifact declarations may explicitly collect after failure. Registry, package, and site adapters use operator config; replacement is interruption-safe. Registry publication identifies the current job's image, never stale host state. Publish failure fails the job.
Artifact paths are workspace-relative; reject traversal, symlinks, devices, sockets, and escapes.
Collection stages privately before atomic visibility; required missing artifacts fail finalization.
Limits per set: 100 regular files, 10 MiB per file, 50 MiB total.
Artifacts are immutable, public, script-free, nosniff, with sandboxed HTML serving.