Run Context And Artifacts
pkg/contracts.RunContext is the backend-to-runner execution contract returned by POST /api/v1/runs/{run_id}/accept.
Core Fields
- run identity:
RunID,StackID,RunMode,CommitSHA - stack execution config:
RepositoryURL,WorkingDirectory,OpenTofuVersion,ManagedBackend,EnvironmentVariables,Command - optional
Hooks— per-phase bash scripts the runner runs around init/plan/apply, already merged and ordered by the backend (see Lifecycle Hooks); absent onrawruns - execution support config:
TerraformBackend,LogsStorage,SCMCredentials - optional
RuntimeCredentials— slice; one entry per cloud provider the run needs at runtime - optional
WorkArtifacts— present when the run reads or writes a plan file - optional
LocalSource+LocalSourceSHA256— present on local-deployment runs; the runner downloads the uploaded source tarball instead of cloning (see Local source tarballs)
Run Modes
RunMode is plan, apply, destroy, refresh, or raw. A narrowed replan is not a mode of its own: it is a plan run carrying Targets/Excludes/Replaces.
RunCommand.Mode is what the runner actually executes: plan, apply, destroy, refresh, or raw. Built-in modes derive a fixed OpenTofu invocation; raw requires RawCommand and is run via bash -xc.
Environment Construction
The ephemeral runner builds a narrow environment, in this order so later layers win:
- inherit only
PATHandHOME - stack environment variables
- system-enforced keys (
TF_IN_AUTOMATION,TF_INPUT) - cloud provider credentials and
GIT_CONFIG_GLOBALlast, so stack env vars cannot override them
GIT_CONFIG_GLOBAL points at the run’s own git config file, built from SCMCredentials.GitConfig — it is what lets tofu init fetch a private module, and it is pinned on every run so the runner host’s ~/.gitconfig never contributes. See runner_runtime_credentials.md.
Logs
Logs are chunked locally and uploaded to the prefix carried in LogsStorage. Each chunk is up to 256 KiB uncompressed (contracts.LogChunkUncompressedSize) and is gzip-compressed when uploaded to storage, so the stored object size is not knowable up front; the backend decompresses transparently on the read path and clients receive plain bytes. User-facing log APIs read individual chunks plus a per-run manifest containing chunk count and any startup failure message. Exact key layout is owned by the storage credential service; runners and clients use the prefix and manifest key returned in the contract.
Each chunk object is written to S3 exactly once: the uploader PUTs a chunk when it seals (the writer has rotated past it) and the end-of-run final flush seals the last chunk. While the run is active the manifest counts only sealed chunks; the final flush converges it to the full count, so the terminal S3 state is always complete and self-describing. The active (still growing) chunk never hits S3 — its bytes reach viewers through the live push channel (POST /api/v1/runs/{run_id}/logs/live, see runner_polling.md): the backend keeps the latest snapshot per run in Redis and serves active-run metadata and active-chunk reads from it. Without Redis this degrades cleanly — live tail waits for chunks to seal, and everything is in S3 at run end. If a runner dies mid-chunk, CleanupInactiveRunners flushes the last pushed snapshot to S3 as the run’s final chunk.
Plan Artifacts
WorkArtifacts (currently S3 only) carries the plan-file location and credentials. plan / destroy / refresh upload the plan; apply downloads it. The S3 key is fully specified in WorkArtifacts.S3.Key — the runner does not construct it.
Local Source Tarballs
Local deployments (gantrycli local-plan) replace the git clone with an uploaded tarball. The flow is two-phase, bound by a single-use token:
- Reserve (
POST .../local-deployments/uploads, gated by(stack, local_deploy), service accounts denied): the backend allocates the object keywork/orgs/{org}/stacks/{stack}/local-sources/{upload_id}/source.tar.gzin the artifacts bucket, mints STS credentials scoped tos3:PutObjecton exactly that key (valid forLOCAL_DEPLOY_UPLOAD_TTL, default 15m), and persists a reservation row (local_deployment_uploads) holding the hash of a single-use token. The raw token is returned once. - The CLI packs
git ls-files --cached --others --exclude-standard(gitignore-respecting, uncommitted changes included) into a tar.gz, hashes it, and uploads it with those credentials. - Trigger (
POST .../local-deployments): the backend verifies the token hash and its binding (same user, org, stack, and stack incarnation — any mismatch is an opaque 404), HeadObjects the tarball (must exist and respectLOCAL_DEPLOY_MAX_SOURCE_BYTES, default 512 MiB), then creates theplan_unlocked/ origin-localdeployment and consumes the reservation in the same write tx (consumed_at IS NULLguard) — the token is single-use even under concurrent triggers.
At accept, the run’s RunContext.LocalSource carries the same provider-discriminated shape as WorkArtifacts with s3:GetObject scoped to the tarball key; SCMCredentials.Type is "none" (no SCM token is minted for unreviewed code). The runner downloads the tarball, verifies its SHA-256 against LocalSourceSHA256 (captured at trigger time — this covers an object swapped while the upload credential was still live), and extracts it with a hardened extractor (no absolute/.. paths, no hardlinks/devices, symlink targets contained, decompression-bomb byte/entry caps, O_EXCL creates, no setuid/setgid bits). Like SCM credentials, LocalSource is startup-only and excluded from EarliestCredentialExpiry.
Cleanup: the local-upload-cleanup job (15m cadence) deletes the tarball + reservation row for expired-unconsumed reservations and for consumed ones whose deployment is terminal. The stack-delete work/ prefix sweep is the backstop.