Contributing
Prerequisites
- Go (the version in
go.mod;GOTOOLCHAIN=autofetches it) - Node 24 with npm, for the UI and the docs site
- Docker, for Postgres and LocalStack in integration and end-to-end tests
- Terraform or OpenTofu on
PATH, for end-to-end tests and the deployment module
Everyday commands
make build # bin/stackorder and bin/stackorder-server
make test # unit tests, no Docker needed
make test-integration # server + Postgres + fake GitHub, needs Docker
make test-e2e # real terraform + LocalStack + example-infra, needs Docker
make lint # gofmt, go vet, golangci-lint
make ui # build the UI into internal/ui/dist
make docs # check and build the docs site
make dev # Postgres + LocalStack via docker compose, then the serverSet TEST_DATABASE_URL to reuse an existing Postgres instead of starting a container.
test/integration/testdata/example-infra is a copy of the example monorepo without its workflow files; the integration suite always uses it and the e2e suite falls back to it when no checkout is available. Refresh it with make sync-example (EXAMPLE_INFRA=<path> points at another checkout) whenever the example changes.
End-to-end tests
make test-e2e runs test/e2e with the e2e build tag: the real terraform and tofu binaries against a LocalStack S3 bucket, the example monorepo, the server in-process on its own Postgres database and the stackorder CLI as a separate process for every job. GitHub is played by the in-memory fake of internal/testutil/ghfake and the Actions token service by internal/testutil/oidcfake, because a real organisation only talks to a server it can reach from the internet. The suite runs once with Terraform and once with OpenTofu, and skips a tool whose binary is not on PATH; each takes one to two minutes. Each run is one story: bootstrap the stacks under stacks/ with apply --local, plan a modules/vpc change, apply it wave by wave from a stackorder apply comment, refuse an apply behind another pull request's locks and release them with stackorder unlock, re-plan for an expired plan artifact and refuse one that no longer matches, open and close a drift issue, apply a stack in a non-default workspace, plan, apply and drift-check the infra/ stack instances, each with its own var file, per-mode env, GitHub environment and state object, plan them one after another in a single checkout, affect them all through their shared backend config file, and fail a run whose apply job ends without reporting.
It needs Docker for localstack/localstack:4.0 and Postgres, git 2.38 or later, and DNS that resolves s3.localhost.localstack.cloud to the loopback address (public DNS does). It looks for an example-infra checkout next to this repository; point it elsewhere with one of:
STACKORDER_E2E_EXAMPLE_INFRA_DIR=../example-infra make test-e2e
STACKORDER_E2E_EXAMPLE_INFRA_URL=https://github.com/stackorder/example-infra.git make test-e2eSTACKORDER_E2E_LOCALSTACK_URL=http://localhost:4566 reuses the LocalStack of docker compose up localstack instead of starting a container, and STACKORDER_TERRAFORM_BIN or STACKORDER_TOFU_BIN pick other binaries. .github/workflows/e2e.yml runs the suite on every push to main and nightly.
Against a real GitHub organisation
Teams with a Stackorder server GitHub can reach, and its App installed on all repositories of an organisation, can run TestLiveGitHub instead. It creates a throwaway private repository from the example, sets the STACKORDER_SERVER_URL repository variable, pushes main and a branch that changes modules/vpc, opens a pull request, waits for the server's stackorder/resolve and stackorder/plan check runs and the plan checks of both VPC stacks, and deletes the repository afterwards. It installs nothing and needs neither Docker nor Terraform locally.
| Variable | Meaning |
|---|---|
STACKORDER_E2E_GITHUB_TOKEN | Token that may create and delete repositories in the organisation, push workflow files (the workflow scope, or Workflows: write) and set their Actions variables |
STACKORDER_E2E_ORG | The organisation |
STACKORDER_E2E_SERVER_URL | The server's public https base URL |
STACKORDER_E2E_PLAN_ROLE_ARN | Optional plan role, set as STACKORDER_PLAN_ROLE_ARN; with it every plan check must succeed |
STACKORDER_E2E_GITHUB_API_URL, STACKORDER_E2E_GITHUB_URL | Optional GitHub Enterprise Server URLs |
STACKORDER_E2E_KEEP_REPO | true keeps the repository for inspection |
The first three are required together. When they are set the fake story is skipped; without them TestLiveGitHub is skipped. In CI the live job of e2e.yml runs when the repository variable STACKORDER_E2E_ORG is set, reading the token from the secret STACKORDER_E2E_GITHUB_TOKEN.
Layout and contracts
Read ARCHITECTURE.md before adding a package. It fixes package boundaries, library choices, identifiers, endpoints and status values. Behavioural questions are answered by the design document linked from the README.
Commits and pull requests
- Conventional Commits with a scope:
feat(graph): assign waves by longest path. - One logical change per commit; keep refactors separate from behaviour changes.
- Every behaviour change comes with tests at the lowest level that can observe it. The
graphpackage is tested exhaustively. - No code comments explaining rationale; put it in the commit body.
- CI must be green:
make lint testlocally reproduces it.
Documentation site
make docs runs the tests in docs/test and builds the VitePress site, and CI runs both on every pull request. .github/workflows/docs.yml publishes the site to GitHub Pages at https://docs.stackorder.io on every push to main that touches it, gated on the repository variable DOCS_DEPLOY being true. See Contributing to the docs for the Pages and DNS setup a fork needs.
Releasing
Tags vX.Y.Z on this repository release the CLI binaries through GoReleaser and push the server image to ghcr.io/stackorder/stackorder. The stackorder/actions repository moves its v1 tag independently.
Working on this site
The documentation lives in docs/ and is built with VitePress. The Markdown pages are the source; docs/.vitepress/config.ts holds the navigation and sidebars.
cd docs
npm ci
npm run docs:dev # local server with hot reload
npm run docs:build # static build into docs/.vitepress/dist
npm test # check pages against the code they describe
npm run docs:preview # serve the built site- The build fails on dead internal links. Link to pages by path, such as
/reference/cli#exit-codes. docs/testchecks pages against the code they describe, such as the module's inputs againstdeploy/terraform/variables.tf. Change both in the same commit.- Diagrams are fenced
mermaidcode blocks, rendered in the browser. - The architecture contract, this page and the changelog include
ARCHITECTURE.md,CONTRIBUTING.mdandCHANGELOG.mdfrom the repository root. Edit those files, not the pages that include them. - Document what the code and the architecture contract define. Never document a flag, variable, endpoint or configuration key that does not exist.
- Commits to the site use the scope
site:docs(site): ….
Publishing
.github/workflows/docs.yml builds the site and deploys it to GitHub Pages on every push to main that touches docs/, ARCHITECTURE.md, CONTRIBUTING.md, CHANGELOG.md or the workflow itself, and on manual dispatch. Its jobs run only when the repository variable DOCS_DEPLOY is true; otherwise the workflow skips them.
- In the repository settings, under Pages, set the source to GitHub Actions.
- Under Secrets and variables, Actions, Variables, set
DOCS_DEPLOYtotrue. - Push to
mainor run the docs workflow by hand.
The site is served at docs.stackorder.io.
Custom domain
docs/public/CNAME contains docs.stackorder.io. When the file exists, the workflow builds the site with the base path /. The domain also needs:
- At the DNS provider for
stackorder.io, aCNAMErecord fordocsthat points tostackorder.github.io. - In the repository settings, under Pages,
docs.stackorder.ioentered as the custom domain, the DNS check passed, and Enforce HTTPS turned on.
With a workflow-based deployment, GitHub takes the custom domain from the Pages settings, not from the file, so step 2 is required. The CNAME file is what switches the base path.
A fork that publishes its own copy changes the domain in docs/public/CNAME and in the site constant in docs/.vitepress/config.ts, which sets the canonical links, the sitemap and the link preview URLs. Without a CNAME file the site is served from https://<owner>.github.io/<repo>/, and the workflow builds it with the base path /<repo>/.