Changelog
All notable changes to this repository are documented here. The format follows Keep a Changelog, and releases follow Semantic Versioning. The CLI, the server image and the Terraform module are released together from one vX.Y.Z tag.
0.1.0 - 2026-09-30
The first release. It works with stackorder/actions v1.0.0 or later.
Added
Wire types and configuration
api/v1wire types shared by the CLI, the server, the UI and tooling, with JSON and YAML tags: stacks, edges, graphs, runs, run stacks, checks, matrix entries (with the checkoutsha,plan_run_idandartifact), plan summaries, audit entries and thev1.Errorcodes. Stacks, affected stacks, run stacks, stack details and matrix entries carry theirinstance, and a stack itswatch_paths.- Loading, defaulting and validation of
stackorder.yamland per-stack.stackorder.yaml, with unknown keys as errors and per-stack merging. - Stack instances. A key
path:instancenames one deployment of a directory, with its own state object, var files, environment variables, GitHub environment and apply role, and every instance is a stack of its own in the graph, affected sets, waves, locks, runs, checks, plan artifacts, drift and the UI. stacks.instances.from_var_filesinstackorder.yaml, a glob relative to each stack directory: every matching file declares an instance named by its base name up to the first., and is that instance's var file after the root and stackvar_files. A derived name that is not a valid instance name is an error; a matched file an explicitinstanceslist leaves out is a warning.instancesin.stackorder.yaml, a list of names or a map from name to overrides ofenvironment,workspace,backend_config,var_files,env,plan_output,apply.allowed_teams,depends_onandignore_inferred.backend_configat the root, per stack and per instance:-backend-configvalues forinit, asname=valueor a file relative to the repository root. The scanner overlays them on thebackend "s3"block, so inferredreads_stateedges and the shared-state warning work for partial backends and per instance.var_filesat the root, per stack and per instance:-var-filevalues forplan, the re-plan insideapplyanddrift, relative to the stack directory. A missing file is a scan warning and a plan error.envat the root, per stack and per instance: environment variables for the tool and the hooks, as a string or with aplan,applyanddriftvalue,driftfalling back toplan. Reserved names are refused, and values of secret-looking names are redacted.stacks.excludeinstackorder.yaml: directory globs that are never stacks, beatingstacks.discoverandstacks.include.environmentskeysprefix,prefix:instanceand:instance, the most specific key winning, with template values.- Go templates in
environmentsvalues,environment,workspace,backend_config,var_files,envvalues,depends_onandignore_inferred, with.Path,.Name,.Instanceand.Key, the functionstrimPrefix,trimSuffix,base,dir,replace,lowerandupper, the builtinsand,or,not,eq,ne,lt,le,gtandge, andifandwith. Other actions and builtins are refused, and output over 4096 bytes is an error, so a repository's configuration cannot stall the server. - The environment of a stack is the first of the instance's
environment, the stack's and theenvironmentsmatch that renders non-empty, then the instance name, thendefault. - Validation of the instance keys:
environmentsprefixes may not contain:, an empty prefix needs an instance part, and keys whose prefixes normalise to the same path with the same instance part are refused; a null item ininstancesor a nullenvvalue is refused; reservedenvnames and the instance namedefaultare refused in any letter case; twofrom_var_filesmatches that derive the same instance name are an error. depends_onentries with an instance suffix; a bare path to a directory with instances resolves to the instance of the same name.
CLI (stackorder)
- Commands
resolve,plan,apply,drift,check,graph,affected,unlockandversion, with exit codes 0, 1, 2 (changes or drift) and 3 (refused by the server or failed closed), GitHub Actions outputs and Markdown step summaries.--stacktakes a stack key,pathorpath:instance; a bare path on a directory with instances is an error that lists them. - Scanning of a checkout into the dependency graph: stack discovery,
s3backends, module sources normalised into graph identities, inferredreads_stateedges fromterraform_remote_state,depends_on(includingowner/repo//keyacross repositories),ignore_inferred, and changed paths from git with merge-base semantics. - Resolution of the affected set with reasons, propagation through modules, dependents and remote state, longest-path waves, cycle reporting, validation and Graphviz DOT output.
- Watch paths: the backend configuration files and var files outside a stack directory affect the stack with the reason
watch_path, and the tree hash covers*.tfvars,*.tfvars.jsonand*.tfbackendfiles. resolvefalls back to a local decision and marks itunconfirmedwhen the server cannot be reached, posting a neutral check withGITHUB_TOKEN.- Terraform and OpenTofu wrapper: tool detection,
initwith the stack'sbackend_configandSTACKORDER_BACKEND_CONFIG(and-reconfigurewhen a stack hasbackend_config, so instances of one directory can share a checkout), workspaces,plan,show,applyand plan JSON summaries. - Redaction of secrets from everything sent to the server, the step summary and fallback checks,
::add-mask::registration, and truncation of plan text. - Repository hooks
.stackorder/hooks/{pre-plan,post-plan,pre-apply,post-apply}.sh, run by the CLI in CI and locally withSTACKORDER_STACK,STACKORDER_RUN_ID,STACKORDER_PLAN_JSONandSTACKORDER_PLAN_FILE. The tool and the hooks also getSTACKORDER_STACK_PATHandSTACKORDER_INSTANCE. applyconfirms the run with the server, checks the run's SHA against the dispatchshaand the checkout'sHEAD, re-plans when the plan artifact is missing orapply.from_planis false, and applies only when the new plan's resource addresses match the recorded plan.apply --localfor manual applies under server locks with an API key;plananddriftstay local outside Actions unless a server, a run id and an API key are all set.- Server API client with retries, error mapping to sentinel errors and a fresh runner OIDC token for every call.
Server (stackorder-server)
- One
http.Servercomposing the webhook receiver, the queue workers, the scheduler, the JSON API and the embedded UI, configured from the environment, with thehealthcheckandversionsubcommands and graceful shutdown. - Setup mode when no GitHub App credentials are set:
/setupcreates the App from a manifest and prints its credentials once;STACKORDER_ALLOW_RESETUPallows creating another App later. - Run state machine for plan, apply and drift runs, superseding on a new pull request head, and roll-ups of per-stack status (
blocked,noop,unconfirmed,unknown,skipped). - Apply gate with five layers (requester and
allowed_teams, pull request state and approvals withfour_eyesandrequire_codeowner_review, fresh plans, named policy checks, locks), all failures reported together; policy read from the default branch. before_mergeapplies fromstackorder applycomments andon_mergeapplies on merge; comment commandsplan,applyandunlock, accepted from users with push permission and rate limited per pull request.- Stack locks taken all or nothing before wave 0 and released on merge or run completion, with daily reminders for closed pull requests that still hold locks.
- Dispatch of
stackorder-run.ymlper wave and GitHub environment, chunked byapply.max_parallel, binding of workflow runs to dispatches by run name and job names, and a reconciliation every minute that resends unsent dispatches, marks vanished jobsunknownand recovers apply runs left without a dispatch. - Cross-repository dependents: a merge that changes an upstream stack starts plan runs in the downstream repositories.
- Scheduled drift checks from
drift.schedule, one GitHub issue per drifted stack, closed when a later check finds no drift. - Pull requests are resolved under the
stackorder.yamlthey carry and under the default branch's, with the affected sets united. - Deployment protection rule answers for dispatched runs, and a neutral check for fork pull requests.
- Start-up and daily sync of installations and repositories from the App API, forgetting what GitHub no longer lists.
- Optional S3 artifact bucket for full plan text (
STACKORDER_ARTIFACT_BUCKET), served atGET /v1/runs/{id}/stacks/{key}/plan. - Retention pruning of plan text, events and drift history.
GitHub integration and security
- Hand-written GitHub App client: App JWT, cached installation tokens, retrying transport with rate limit handling and pagination, check runs, the sticky pull request comment, reviews, team membership, contents, workflow dispatch, runs, jobs and deployments, and OAuth.
- CODEOWNERS parsing with GitHub's matching semantics.
- Webhook HMAC verification, delivery deduplication and persistence.
- Verification of GitHub Actions OIDC tokens against the cached GitHub JWKS,
jtireplay protection, optionalSTACKORDER_REQUIRED_WORKFLOW_REFpinning, and binding of plan tokens to the pull request head and of dispatch tokens to their dispatch, attempt and environment. - Authentication of runners by OIDC token, automation by
sk_API keys and people by GitHub sign-in sessions, with same-origin checks on state-changing requests and per-organisation visibility. - Per-instance AWS roles and session names in
stackorder/actionsv1.0.0:aws-role-arn-mapkeyspath:instanceand:instance, matched before path prefixes, and theaws-role-session-nameinput on both reusable workflows, a name or a name per mode.
API, metrics and tracing
- Runner endpoints for runs, graph uploads, stack results, check verdicts and unlocks.
- Human and automation endpoints for the overview, repositories, graphs (by commit SHA, SHA prefix or
default), runs, stacks, modules and their consumers, unlocks, re-runs and the audit log, paginated with cursors. /healthz,/readyzand/metrics, the latter optionally behindSTACKORDER_METRICS_TOKEN.- Prometheus metrics with the
stackorder_prefix for runs, stacks, dispatches, drift, locks, commands, webhooks, the queue, GitHub requests and rate limit, HTTP requests and build info. - OpenTelemetry tracing over OTLP HTTP when
OTEL_EXPORTER_OTLP_ENDPOINTis set.
Storage
- Postgres schema through embedded golang-migrate migrations, run at start-up under an advisory lock.
- Queue of events and jobs claimed with
SKIP LOCKED, retries with backoff and dead letters; scheduler leader election by advisory lock. - Stable stack and module identities across graphs, module families and versions, and the default-branch graph of each repository.
Web UI
- Preact app embedded in the server with SPA fallback: overview, repositories, the dependency graph (dagre layout drawn as SVG), stack, run and module pages, GitHub sign-in, unlock and re-run actions, and the full plan text from the artifact bucket.
Terraform module (deploy/terraform)
- ECS Fargate service behind an Application Load Balancer with TLS, RDS PostgreSQL or Aurora Serverless v2, Secrets Manager secrets for the App credentials, and an optional artifact bucket.
- Container health check through the
healthchecksubcommand, a generated bearer token for/metrics, opt-in major database upgrades, optional load balancer access logs and WAF web ACL, and a 60 s drain before ECS stops a task. - Examples
complete,existing-vpcandself-hosted(Stackorder deploying itself), withterraform testsuites.
Packaging and release
- GoReleaser configuration for
stackorder_X.Y.Z_<os>_<arch>archives for linux, darwin and windows on amd64 and arm64, andstackorder_X.Y.Z_checksums.txt. - Multi-stage Dockerfile (Node for the UI, Go,
gcr.io/distroless/static:nonroot) with aHEALTHCHECK, published asghcr.io/stackorder/stackorder:X.Y.Z,:X.Yand:latestfor linux/amd64 and linux/arm64. - Makefile targets for building, the three test levels, linting, the UI, the docs site, a local development stack and the image; docker compose for Postgres and LocalStack.
Tests and CI
- Unit tests for every package, with
go test ./...needing no Docker, network or Postgres. - Integration tests (
integrationtag) running the server on Postgres, an in-memory fake GitHub API, a fake OIDC issuer and the CLI on a faketerraformthrough whole plan, apply, gate, drift, fork, manual apply, installation and cross-repo flows. - End-to-end tests (
e2etag) running real Terraform and OpenTofu against LocalStack S3 andstackorder/example-infra, and an optional live variant against a real GitHub organisation. - CI workflows for lint, unit, integration and build, the UI, the Terraform module, the docs site, nightly end-to-end runs and releases.
Documentation
- VitePress documentation site: guide (including getting started and a local demo), configuration (including Stack instances, with a worked example of one OIDC bootstrap role and a provider role per account, and a migration table from Terrateam), reference, operations, the design document with implementation notes, and the architecture contract.
ARCHITECTURE.mdandCONTRIBUTING.md.
Design deviations
Recorded in ARCHITECTURE.md and in the implementation notes of the design page on the documentation site.
- The CLI runs the repository hooks itself, in CI and locally, instead of the reusable workflow, and also sets
STACKORDER_PLAN_FILE;STACKORDER_PLAN_JSONandSTACKORDER_PLAN_FILEare file paths. stackorder-run.ymltakes a fifth input,sha, the commit every job checks out;modeisplan,applyordrift, neverresolve;stacksis required; the wrapper setsrun-name: stackorder ${{ inputs.mode }} ${{ inputs.run_id }} wave ${{ inputs.wave }}so the server can bind the workflow run to its dispatch.- Both reusable workflows require
server-url, and the calling jobs must carry the permissions block, since a called workflow cannot raise permissions. Theplan.ymlresolve job also needspull-requests: read. - Server-dispatched
plananddriftjobs run under the environmentdefaultand assume the plan role (aws-plan-role-arn, a newrun.ymlinput); onlyapplydispatches carry the stack's environment. Everyrun.ymljob runs underenvironment: ${{ matrix.environment }}in the concurrency groupstackorder-stack-<key>. A stack with no environment mapping runs underdefault, never an empty name. - The OIDC
shaclaim is never compared with the run's SHA. Plan runs are bound byevent_name, therefs/pull/<n>/mergeref and the pull request head read from GitHub; dispatched runs byworkflow_dispatch, the default branch ref, the dispatch'srun_idand the environment. apply.max_parallelcaps the number of stacks in one dispatch rather than concurrent jobs; the caller'smax-parallelinput limits jobs.pull_request_targetplans are not supported; fork pull requests get a neutral check from the server.- The CLI redacts with its own rules instead of a list of the runner's secret patterns.
- The implementation has more packages than the design names (
config,report,command,tf,client,principal,metrics,artifactsand others), more run and stack statuses (superseded,unconfirmed,unknown,skipped), themanualtrigger, and more API endpoints. - The schema stores a stack's backend as one
backendjsonb column and edges byfrom_keyandto_key, keeps a ref-less family row per module, and adds tables for graph membership, checks, dispatches, OIDCjtis and the audit log. - Named checks accept
warn, which passes; a stack'sapply.allowed_teamsreplaces the root list rather than narrowing it.