Skip to content

Contributing ​

Prerequisites ​

  • Go (the version in go.mod; GOTOOLCHAIN=auto fetches 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 ​

sh
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 server

Set 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:

sh
STACKORDER_E2E_EXAMPLE_INFRA_DIR=../example-infra make test-e2e
STACKORDER_E2E_EXAMPLE_INFRA_URL=https://github.com/stackorder/example-infra.git make test-e2e

STACKORDER_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.

VariableMeaning
STACKORDER_E2E_GITHUB_TOKENToken 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_ORGThe organisation
STACKORDER_E2E_SERVER_URLThe server's public https base URL
STACKORDER_E2E_PLAN_ROLE_ARNOptional plan role, set as STACKORDER_PLAN_ROLE_ARN; with it every plan check must succeed
STACKORDER_E2E_GITHUB_API_URL, STACKORDER_E2E_GITHUB_URLOptional GitHub Enterprise Server URLs
STACKORDER_E2E_KEEP_REPOtrue 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 graph package is tested exhaustively.
  • No code comments explaining rationale; put it in the commit body.
  • CI must be green: make lint test locally 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.

sh
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/test checks pages against the code they describe, such as the module's inputs against deploy/terraform/variables.tf. Change both in the same commit.
  • Diagrams are fenced mermaid code blocks, rendered in the browser.
  • The architecture contract, this page and the changelog include ARCHITECTURE.md, CONTRIBUTING.md and CHANGELOG.md from 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.

  1. In the repository settings, under Pages, set the source to GitHub Actions.
  2. Under Secrets and variables, Actions, Variables, set DOCS_DEPLOY to true.
  3. Push to main or 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:

  1. At the DNS provider for stackorder.io, a CNAME record for docs that points to stackorder.github.io.
  2. In the repository settings, under Pages, docs.stackorder.io entered 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>/.