Skip to content
GantryCD

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 on raw runs
  • 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:

  1. inherit only PATH and HOME
  2. stack environment variables
  3. system-enforced keys (TF_IN_AUTOMATION, TF_INPUT)
  4. cloud provider credentials and GIT_CONFIG_GLOBAL last, 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:

  1. Reserve (POST .../local-deployments/uploads, gated by (stack, local_deploy), service accounts denied): the backend allocates the object key work/orgs/{org}/stacks/{stack}/local-sources/{upload_id}/source.tar.gz in the artifacts bucket, mints STS credentials scoped to s3:PutObject on exactly that key (valid for LOCAL_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.
  2. 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.
  3. 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 respect LOCAL_DEPLOY_MAX_SOURCE_BYTES, default 512 MiB), then creates the plan_unlocked / origin-local deployment and consumes the reservation in the same write tx (consumed_at IS NULL guard) — 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.

References