GitHub App is the only VCS integration in the first StackShip release. A workspace selects one repository, a tracked branch, a working directory, and optional trigger paths. Every accepted run is pinned to a full commit SHA before StackShip downloads configuration.
GitHub App permissions
Install the App on only the repositories StackShip should read.
| Permission | Level | Purpose |
|---|---|---|
| Metadata | Read | Verify selected repositories and their immutable provider IDs. |
| Contents | Read | Resolve a commit and download its archive. |
| Pull requests | Read | Read the complete changed-file list before applying path filters. |
| Checks | Write | Create and update the StackShip Check Run on the commit. |
StackShip does not request repository contents write, administration, Actions, packages, members, commit-status write, or pull-request write. It does not post pull-request comments in this release.
The installation callback is single use, expires after ten minutes, and is bound to both the organization and the principal who started it. StackShip stores installation and repository identities, but never stores an installation access token. A short-lived token is minted only when a GitHub operation runs.
Configure the GitHub App Setup URL as:
https://stackship.run/api/control-plane/v1/github/installations/callbackGitHub returns to the dashboard origin so the top-level callback carries the
dashboard session. The dashboard streams that request to the native API, sends
only the rewritten API session cookie, and accepts only a redirect back
to stackship.run. Do not configure the setup callback directly on
api.stackship.run; the browser does not hold the dashboard cookie on that
host.
Select push and pull_request as the App’s manual webhook subscriptions.
GitHub automatically delivers installation and
installation_repositories lifecycle events to every GitHub App, so they are
not manual subscription choices.
Workspace source settings
Source settings are immutable versions. Updating a setting creates a new version; an ingestion and run continue to reference the exact version that triggered them.
PUT /v1/workspaces/{workspace_id}/source requires Idempotency-Key.
An exact request replay returns the same immutable source-settings version.
Changing the repository, branch, working directory, trigger paths, or
speculative profile while reusing that key returns a conflict. Two writes that
race to allocate the same version return
409 WORKSPACE_SOURCE_SETTINGS_CONFLICT; retry to build on the winning
version.
| Setting | Meaning |
|---|---|
| Repository | A repository selected in the organization’s GitHub App installation. |
| Tracked branch | A matching push creates a tracked plan-and-apply run. |
| Working directory | The repository-relative OpenTofu root. |
| Trigger paths | Up to 100 repository-relative globs used for monorepo filtering. |
| Speculative federation profile | Optional verified speculative_plan cloud federation version for same-repository pull requests. |
The dashboard exposes this page as Source in the workspace section
navigation, at /workspaces/{workspace_id}/settings/source. The active
source-settings version is shown as plain metadata near the form header,
and a success confirmation appears only after a save in the current visit.
The dashboard renders enabled source controls only after the repository,
current source, and credential-profile inventories all load successfully.
workspace:admin is required to change source settings, while vcs:install
is required to start GitHub App installation. Readers can inspect the active
boundary in a disabled form, but cannot save or install. If an active source
references a credential version that is no longer selectable, its exact
version ID remains visible as unavailable instead of being shown as an empty
selection.
An empty trigger-path list matches every change. When paths are configured,
*, **, and ? follow the repository-relative matching rules shown by the
workspace form. Unsafe absolute paths, traversal, . and .. segments,
backslashes, duplicate paths, and over-limit input fail closed at
configuration time. A previously saved pattern with a leading ./ is matched
from the repository root; any other previously saved unsafe pattern can never
match, and it never disables the workspace’s remaining patterns.
Delivery and ingestion flow
StackShip reads at most 10 MB of webhook request data, verifies
X-Hub-Signature-256 over the untouched bytes, and only then parses JSON.
X-GitHub-Delivery is the global replay identity for every signed delivery,
including source, lifecycle, unknown-repository, and provider-free payloads.
The tenant-free record stores nullable provider installation and repository
IDs, signed event and nullable action, payload digest, durable result, binding
outcome, and bounded timestamps—never the raw GitHub payload. An exact replay
returns before provider hydration and does not redispatch. Reusing an ID with a
different digest, event, action, provider identity, or delivery kind is
rejected. Pending dispatch is recovered from the durable outbox by the
orchestrator’s authenticated scheduled reconciler.
If identical concurrent deliveries observe repository binding differently, the first durable outcome wins. The losing path rereads the global identity and returns the same response shape and ingestion IDs without creating another ingestion or weakening mismatch rejection.
Lifecycle deliveries also advance an installation fence. The global identity remains authoritative after later deliveries or installation binding. The installation callback is the only path that binds a new installation to an organization, so unbound lifecycle events are durably ignored and cannot claim a tenant. During rolling deployments, database triggers mirror legacy tenant delivery inserts and result updates into the global identity. They reject identity changes and attempts to bind an existing global-only result; a restrictive foreign key prevents tenant deliveries without a global identity.
For a bound installation, the signed action triggers reconciliation but does not overwrite newer provider state:
- suspension, unsuspension, and accepted-permission actions fetch and reconcile the current GitHub installation;
- deletion monotonically records revocation and deselects every repository, without requiring a provider read; and
- repository additions and removals fetch and reconcile GitHub’s complete current selected-repository set while preserving deselected repository history.
Unknown actions are recorded as explicit ignored outcomes. The lifecycle identity, fence advance, bound mutation, tenant delivery, and bounded audit event commit in one database transaction. Tenant or provider-identity conflicts fail closed without moving a repository between organizations.
For recognized non-delete actions, StackShip reads the fence, fetches current
GitHub state, and compares the locked fence before committing. A race retries
that whole sequence up to three times and refetches on every attempt. Three
consecutive races return a retryable 409 without acknowledging stale state.
Unknown actions participate in fencing without an unnecessary provider call.
Deletion performs no provider read and advances the fence monotonically, so
an older hydrated action cannot commit after revocation. GitHub network calls
happen before database transactions.
During installation, StackShip reads the fence, fetches current GitHub state,
and locks and compares the fence before consuming callback state. It retries a
lifecycle race up to three times. If the fence keeps changing, the callback
returns a retryable 409 with its state unconsumed. An existing suspended or
revoked installation is never reactivated by the callback.
A push uses the complete commit path list when GitHub supplied it. When the pushed commit list reaches GitHub’s delivery cap, created the branch, or rewrote history, the list cannot prove the effective diff, so StackShip dispatches conservatively instead of skipping a possibly relevant run. A pull-request delivery never contains the authoritative complete list, so StackShip reads the paginated pull-request files endpoint before filtering.
Webhook acceptance uses one short database transaction to:
- record the delivery;
- create or reuse each workspace-scoped source ingestion;
- associate the delivery with those exact ingestion IDs;
- enqueue one deterministic Workflow dispatch; and
- append the audit event.
GitHub calls and R2 transfers do not occur in that transaction. The Workflow
ID is source-ingestion-{sourceIngestionId}, so a lost create response is
reconciled rather than creating a second ingestion Workflow.
Immutable configuration
The ingestion Workflow resolves the recorded full commit SHA, downloads that
exact GitHub archive, validates it, and streams it through the API Worker into
encrypted R2. Compressed configuration is limited to exactly
100_000_000 bytes. The Worker applies backpressure, never assembles a second
in-memory archive, and consumes the GitHub byte stream in bounded pieces
accepted by the encrypted object writer.
Validation rejects traversal, absolute and duplicate paths, links, unsupported
tar entries, invalid checksums, excessive expansion, excessive entries, and
over-limit paths. The committed configuration archive and
.terraform.lock.hcl are immutable artifacts. Only a ready ingestion with a
verified archive digest may create a run.
Provenance in run review
The authenticated run page makes the persisted source chain reviewable without making the source archive readable. For GitHub-backed runs it shows:
- provider and full commit SHA;
- working directory;
- source-ingestion public ID,
readystatus, and Workflow ID; - GitHub delivery ID;
- configuration-version public ID;
- normalized archive SHA-256; and
- configuration-artifact public ID.
The database read is organization scoped at every join and fails closed when the chain is incomplete or ambiguous. Uploaded and other non-source runs show no source-ingestion provenance. R2 keys, ciphertext metadata, upload capabilities, archive bytes, installation tokens, webhook secrets, and source content do not cross the run-review boundary.
Tracked and speculative runs
After configuration becomes ready, StackShip creates the managed run with IDs derived deterministically from the source-ingestion ID. A retry therefore reuses the same input snapshot, run, plan, apply record, Workflow dispatch, and Check publication.
The canonical input snapshot records:
- source-settings public ID and version;
- installation and repository provider identities;
- repository owner and name;
- full commit SHA and source ref;
- working directory and trigger paths;
- pull-request number, base ref, head ref, and fork classification when applicable; and
- the immutable speculative credential decision.
| Trigger | Run structure | Credential rule |
|---|---|---|
| Push to the tracked branch | Plan, policy, approval, and apply | Uses the workspace’s verified plan-and-apply profiles. |
| Pull request targeting the tracked branch | Plan and policy only | Forks receive none. Same-repository pull requests default to none. |
| Same-repository pull request with an explicit profile version | Plan and policy only | Uses only that verified, enabled speculative_plan federation version. A plan or plan_and_apply version is never accepted. |
A speculative run has no approval or apply stage and cannot reserve the workspace state writer. Sensitive workspace variables are omitted from the canonical input snapshot, so it contains no secret-version IDs for a pull request. Manifest publication also rejects a speculative snapshot containing a sensitive reference rather than issuing a secret handle.
The speculative_plan credential phase creates a different workload-identity
subject from plan. That separation lets the customer grant a dedicated
read-only cloud role to pull-request plans without allowing a normal planning
or plan-and-apply profile to cross the speculative trust boundary.
GitHub Checks
StackShip creates one Check Run per managed StackShip run and updates it through the run lifecycle.
| StackShip state | Check state |
|---|---|
| Queued | Queued |
| Planning, policy evaluation, or applying | In progress |
| Plan and policy complete for a plan-only or speculative run | Success |
| Credentials required | Neutral with a link to the StackShip run |
| Plan or apply failure | Failure |
| Policy denial | Failure |
| Canceled, rejected, or expired | Canceled |
| Apply complete | Success |
Check publication is reserved by organization, repository, and StackShip run
before any GitHub write, and a short publication lease prevents concurrent
requests from creating duplicate Checks. GitHub’s Check Run and Check Suite IDs
are stored only after the response is verified. If a create response is lost,
StackShip searches all bounded Check pages by the run ID in GitHub’s
external_id, records the existing provider identities, and patches it to the
newest lifecycle state instead of issuing another create. Repeating the same
payload is a no-op; a later lifecycle state patches the recorded Check.
Cancel and reject actions commit the terminal run state before synchronizing
the Check. A temporary GitHub failure therefore does not roll back the action;
an exact retry reuses the stored signal (and, for rejection, the stored
approval evidence) and retries the idempotent Check update.
The durable run-signal record tracks Workflow delivery and terminal Check
synchronization independently. The orchestrator’s scheduled reconciliation
replays only the pending side effect, so a successful action response does not
depend on GitHub or Workflow availability at that instant.
Production-dark source canary
The protected production-dark Action proves the GitHub source path without a private orchestration shortcut. Each dedicated regional branch already contains a nonce file. The Action updates that file through GitHub’s public Contents API, records the returned commit SHA, and relies on the GitHub App webhook as the only source-ingestion and run trigger.
The harness polls Check Runs for the exact new commit and uses the sole
StackShip Check’s external_id as the run ID. It then polls only that
tenant-scoped run-review source object; the organization-wide run list
remains closed during dark launch. Review must report the exact commit, a
ready ingestion, its source-ingestion Workflow ID and delivery ID, and the
source and run Workflow build SHAs self-attested by their executing Worker
before the harness submits the persisted approval binding. The exact review’s
workspace ID and execution region must match the configured regional workspace
before approval and again at terminal observation. It never calls
an internal source-ingestion or run Workflow endpoint and never downloads the
source archive.
After apply, the Action reads GitHub’s bounded Check Runs response for that
same commit. Exactly one StackShip Check must conclude success, and its
external_id must equal the cross-linked StackShip run ID. These identities
are recorded separately for every launch-enabled region in external-canary
evidence version 2. A unit test, an operator-entered Check claim, or a Check for
another commit cannot satisfy that contract.
Failure behavior
Unsafe archives, invalid signed contracts, commit movement, and identity
mismatches fail closed. Temporary GitHub, database, R2, or Workflow errors
retry from the last durable stage. A source can become ready before a
temporary run or Check publication failure; the commit step then retries the
deterministic run and publication without rewriting the configuration.
Webhook response-loss retries read the durable delivery association before
checking current repository selection or pull-request state, so they return
the original ingestion IDs even if GitHub has changed since the first request.
If a delivery does not produce a run, verify:
- the GitHub App still has the repository selected and is not suspended;
- the delivery signature and delivery ID appear in GitHub’s App delivery log;
- the push branch or pull-request base exactly matches the tracked branch;
- the complete changed-file list matches at least one trigger path; and
- the workspace has execution settings, a supported OpenTofu version, and the required managed-run bindings configured.
Related pages
- Workspaces — workspace and execution settings.
- Run lifecycle — plan, policy, approval, and apply states.
- Variables — sensitive variable behavior.
- Policy sets — policy evaluation after planning.