Skip to content

Runs

Managed OpenTofu plan, policy, approval, and apply attempts executed in isolated sandboxes.

Updated View as Markdown

A run is one attempt to evaluate an immutable configuration and input snapshot. StackShip executes managed runs with OpenTofu in isolated, StackShip-managed sandboxes; Terraform binaries and customer-operated runners are not part of the first release.

Run kinds

Kind Behavior
plan_and_apply Plan, policy, approval, then apply
plan_only Plan and policy, then succeed without apply
speculative Pull-request plan and policy; never state-writing or applyable

Every run pins its configuration digest, input-snapshot digest, exact OpenTofu version, execution region, and platform before execution.

The first release does not support destroy-mode runs. The OpenTofu compatibility workspace resource advertises allow-destroy-plan, can-queue-destroy-plan, and permissions.can-queue-destroy as false, and run creation rejects is-destroy: true without starting execution. This does not suppress destroy changes discovered by an ordinary plan: those remain part of the reviewed plan counts and may be applied through the standard plan_and_apply lifecycle. Workspace-deletion permissions are separate from destroy-run capabilities.

Sources

Runs can be created from the authenticated v1 API or from the GitHub App source pipeline. GitHub is the only VCS integration today.

A tracked-branch push may create a plan_and_apply run. A pull request targeting the tracked branch creates a speculative run. Fork pull requests receive no cloud credential. A same-repository speculative run may receive only an explicitly configured, verified speculative_plan federation profile. Sensitive workspace variables are omitted from speculative input snapshots.

StackShip does not accept auto-apply on either the native /v1 run operation or the OpenTofu /api/v2 compatibility operation. A plan_and_apply run with changes reaches awaiting_approval and requires an explicit review action bound to the exact plan, policy result, predecessor state, and dependency bundle. A policy-passing no-change plan succeeds without approval or an apply process. Run tofu apply without -auto-approve for the interactive approval flow. If -auto-approve is supplied, the compatibility API returns 422 invalid_document with an explicit instruction to rerun without that flag.

This is a durable invariant, not only an HTTP validation rule. Managed-run creation and the run repository reject an automatic-apply request before creating state, every run record is written with auto_apply=false, and PostgreSQL enforces that value with a check constraint. A stale or untyped internal caller therefore cannot bypass review.

Review surface

The run page reads top to bottom as commit identity, then decision, then evidence:

  • a header whose title is the run’s own message — server-generated for automated triggers, for example GitHub <commit-sha> for a push-triggered run — with the Git commit message rendered as a secondary line when present, alongside the run’s status, kind, linked commit SHA, source ref, working directory, commit author, creation time, live duration, copyable run ID, and workspace link;
  • an attention strip offering Approve plan or Discard run while an approval decision is outstanding, gone once the run moves past it. When the reviewed plan destroys at least one resource, Approve plan arms an inline confirmation that restates the plan counts — including the destroy count — before anything is sent; a plan with zero destroys keeps one-click approval. Discard run likewise arms an inline confirmation, and the discard-reason input appears only inside that confirmation;
  • a recovery panel when the run carries unresolved recovery evidence;
  • an ordered stage timeline — plan, policy, approval, and apply, each reporting pending, running, succeeded, failed, skipped, or waiting alongside its duration and, for the plan stage, sanitized create, change, and destroy counts. While the plan has not finished, the plan summary is a quiet “Plan not finished yet” note rather than zeroed counts or an attention badge — the attention badge is reserved for a finished plan whose change result genuinely could not be recorded. Skipped stages render dimmed and neutral, not as a warning. Each non-skipped stage is a toggle: expanding it reveals that phase’s inline console — ordered sanitized plan, policy, or apply logs, loaded on first expansion and appended live while the run is not yet terminal — plus stage-specific evidence:
    • the plan stage additionally offers a Changes tab with the per-resource redacted plan changes behind the plan counts: address, provider, action, and attribute-level before/after values, with sensitive values redacted and unknown values marked. The per-resource view omits resources whose only actions are no-op or read, and classifies create-and-delete pairs as a replace, so its totals can differ from the aggregate counts;
    • the policy stage shows sanitized policy findings and the time-bounded override control, and expands by default whenever the policy outcome is not pass;
    • the approval stage leads with the human facts — whether the plan is awaiting a decision, was approved, was discarded, or expired, with the relevant time and any approval expiry — and demotes the bound plan digest to a secondary monospace line. When the review carries recorded decision evidence, the leading fact names who decided — “Approved by”, “Discarded by”, or “Canceled by” with the decision time — falling back to the monospace actor id when no display name is recorded;
    • the apply stage shows the apply status and, when a successor state was committed, the exact published state version, serial, and publication time (the state digest is part of the API response but is surfaced on the workspace state page, not this stage); when no successor state was committed or its record can’t be resolved, it shows a not-recorded or unavailable notice instead. A terminal, applied run expands its last succeeded stage by default so a published state is visible without clicking anything;
  • a downloads section exposing the redacted plan to any principal with artifact read access, plus guarded raw-plan and emergency-state downloads for sensitive-access principals. A missing permission collapses to a single quiet note, and a viewer who could never download anything sees no downloads section at all.

A run that ended for a recorded terminal reason surfaces one human sentence for that reason in the header, with the raw reason code kept as secondary monospace detail. When the failure came from the plan or apply phase, the same panel additionally shows the final lines of that phase’s sanitized log — at most eight — as a monospace block on an ink surface, captioned with the log it came from, so the underlying error is readable without expanding a stage. The tail is fetched server-side only for failed terminal runs; when no log output is available, the panel keeps its sentence-and-code form unchanged. Two blocked terminal states additionally get a guidance strip in the approval bar’s position: a plan_credentials_required run links to the workspace’s cloud access page, and an expired run notes that a new run starts from a push to the tracked branch or from the CLI, linking to the workspace source settings.

The page surfaces the run’s available actions directly: approve and reject drive the approval bar, cancel drives the header’s action button, and a soft-mandatory policy override drives the policy stage’s override control — each gated by available_actions and the caller’s permissions. The run-review response additionally carries evidence fields the page no longer renders as a dedicated section: the Cloudflare Workflow ID, definition and build SHA, the OpenTofu version, logical runner release, standard sandbox base profile, protocol, OPA and Workflow build identities, and each StackShip attempt’s ID, phase, attempt number, sandbox ID and name, region, persisted status, and terminal classification. These remain available to any authenticated API caller with run:read.

The run-review API derives these fields from PlanetScale records. It does not query a live sandbox to reconstruct history. A field that has not been recorded is shown as unavailable or not recorded; the dashboard never invents a commit, sandbox identity, policy outcome, or published state from the top-level run status.

Phase status is a closed contract. Plans use pending, running, finished, errored, or canceled; applies use the same set plus interrupted. The client rejects any other value. The dashboard renders pending and running as informational, finished as success, canceled as attention, and errored or interrupted as danger. Each phase badge includes its Plan or Apply text label and an icon, so neither color nor a raw database value carries state alone.

Policy finding titles, messages, and resource addresses are returned as text. The review response deliberately omits arbitrary finding details, raw policy input, raw plans, state bodies, artifact object keys, sandbox labels, runner capabilities, and secret handles. An active soft-mandatory result can display a time-limited override control only to a principal with policy:override_soft. The control submits the exact persisted result digest; hard-mandatory findings cannot be overridden. The review returns at most 256 override records and carries an explicit truncation flag; the organization audit history remains the durable source for older override events.

An unavailable run request is shown as unavailable, never as an empty run history.

The organization run inventory uses opaque forward pagination. A response places the continuation token in meta.page.next_cursor; callers pass a non-null token back through the cursor query parameter. Creation time and run id form the stable descending key, so runs created at the same instant are not skipped or repeated. Bare timestamps and delimiter cursors are invalid and return 400 RUN_LIST_CURSOR_INVALID.

The dashboard’s runs page consumes that same pagination: a Load more runs control fetches the next page through the browser API path whenever a continuation token remains. The list offers a free-text filter over run id, message, and workspace name, plus status and workspace filters that narrow the already-loaded rows client-side; the status filter initializes from a ?status= query parameter, so other pages can link directly to, for example, /runs?status=awaiting_approval. Each row shows the run’s duration — final for terminal runs, still ticking otherwise — and a relative created time with the absolute UTC form as its tooltip. While any listed run is not yet terminal, the list re-reads its first page on a short interval so statuses converge without a manual reload.

While a run is not yet terminal, the dashboard holds a live WebSocket to wss://stackship.run/live/runs/{run_id}, authenticated by the dashboard session cookie and a strict Origin check; plan, policy, and apply log events append to that stage’s inline console in sequence order as they arrive, and stage or apply state changes refresh the review without a manual reload. Each console loads its log window lazily, the first time its stage is expanded. A run that is already terminal when the page loads never opens a socket. On any sequence gap or disconnect the dashboard falls back to the HTTP log route, which remains authoritative regardless of what the live channel delivered. The dashboard keeps at most the newest 512 KiB of decoded log text per console either way. While the live feed is attached, each console appends on its own and shows no manual control; once the feed is not live — the run is terminal or the socket is down — a Refresh log control re-reads the HTTP route on demand. If the socket fails repeatedly, the page reports a quiet “Reconnecting — data may be stale” note beside the status badge and falls back to polling the review endpoint on an interval until the socket recovers, without changing any log-delivery semantics.

Ordinary artifact access exposes the redacted plan. Raw plan and emergency-state controls require sensitive permission, a typed operator reason, and step-up reauthentication no older than 15 minutes. The dashboard presents guarded POST forms only to authorized operators; emergency state additionally requires state:recover, and run:recover alone does not authorize the download. Restricted download routes reject every non-empty query string. The reason is audited before any bytes are released.

StackShip now publishes a distinct, renderable plan-diff artifact — stackship.plan-diff/v1 — built by the runner during the plan phase. Sensitive values are redacted through the plan’s own sensitivity masks before the artifact ever leaves the sandbox, and values OpenTofu marked unknown are marked as such rather than guessed. The artifact is bounded to 5,000 resources and 4 MiB, and the two bounds enforce differently. A plan with more than 5,000 changed resources has the excess cut from the input list outright, before redaction ever runs. A plan that still exceeds 4 MiB after that cutoff degrades resource by resource instead — withholding the largest remaining resource’s before/after detail while keeping its address, provider, and actions, repeating until the artifact fits, and only dropping trailing resources entirely if withholding every detail still is not enough. Either path sets an explicit truncated flag. Construction also fails closed on a per-resource basis: a resource whose sensitivity masks cannot be resolved is emitted with its detail withheld rather than with unredacted values. GET /v1/runs/{runId}/plan-changes serves this artifact to a principal with run:read, and the dashboard’s inline plan-changes view and its “Download redacted plan” control both read from it now.

StackShip still advertises structured run output as disabled to OpenTofu. The CLI therefore renders the standard human-readable plan logs streamed from the run instead of requesting a redacted structured-output artifact through the TFE-compatible protocol. Publishing plan_diff closes this gap for the dashboard; StackShip will not claim TFE structured-output compatibility until the compatibility API exposes an equivalent redacted artifact to the OpenTofu CLI.

The OpenTofu Cloud compatibility contract separately exposes GET /api/v2/plans/:plan_id/json-output for authenticated automation that needs the machine-readable plan export. This endpoint is not the CLI’s redacted structured-output endpoint. It requires a cloud principal with run:read inside the exact organization, project, and workspace boundary. The Worker streams the encrypted plan_json artifact with backpressure and a 100 MB limit; it does not accumulate the plan in Worker memory. Artifact metadata and the object descriptor must agree on workspace, kind, plaintext size, and SHA-256 before the stream is released. Each encrypted frame is authenticated, the stream enforces the exact declared plaintext length without releasing a trailing byte, and the object store verifies the complete artifact digests at EOF. Missing, unauthorized, oversized, or inconsistent descriptors return the same unavailable response, while a descriptor mismatch or invalid stream is canceled. Successful JSON-plan responses use application/json with Cache-Control: private, no-store; compatibility errors remain JSON:API documents.

State and apply

Only a successful apply may commit its Cloud-published successor state version. OpenTofu streams that version through the TFE-compatible API, and the runner confirms only its exact publication metadata instead of copying state through the sandbox callback. Apply authorization binds the reviewed evidence to the current fenced workspace lock. An interrupted or uncertain apply does not guess whether the provider changed infrastructure; it ends in an explicit recovery state for operator handling.

What is not included

StackShip does not include:

  • scheduled runs or drift scans;
  • cost analysis;
  • customer-operated execution agents;
  • a StackShip-specific CLI;
  • general Slack, PagerDuty, email, or customer webhook routing.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close