Contributing to Workflow¶
This guide is for working on Workflow itself: running it from source, the code generators, database migrations, the tests and the local Kubernetes loop. To install and use Workflow, see the documentation instead.
Prerequisites¶
- Go 1.26 or later
- Node 22 or later
- Docker, for PostgreSQL (and Testcontainers, if you don't point the tests at a database)
- For the in-cluster loop: k3d, Tilt and Helm 3
Run it from source¶
make db-up # PostgreSQL 17 on :5432
make dev # the API on :8080 with development sign-in (no identity provider needed)
cd web && npm ci && npm run dev # the UI on http://localhost:5173, proxying /api to :8080
Open http://localhost:5173, sign in with any email address (the first account becomes the organisation admin) and create a project from a template.
To run the production build, with the UI embedded in the binary:
make web embed-web build
WFM_ENV=development WFM_DEV_LOGIN=true ./bin/wfm serve --migrate # http://localhost:8080
make help lists every target.
Repository layout¶
cmd/wfm/ the binary: serve, migrate, workflow apply|validate|export, mcp
api/openapi.yaml the API contract (source of truth for the server and the UI's types)
internal/app/ wiring, modes (web, hooks, worker), the background job registry
internal/httpapi/ HTTP handlers (generated strict server, SSE, webhooks)
internal/tracker/ projects, issues, boards, sprints, releases, the workflow runtime
internal/workflow/ workflow YAML, validation and the CEL evaluator (pure)
internal/search/ the query language: parser and SQL compiler (pure)
internal/identity/ OIDC sign-in, sessions, tokens, SCIM, directory sync, local accounts
internal/scm/ GitHub and GitLab providers, webhook processing, reconciliation
internal/automation/ rules, notifications, outbound webhooks, Slack and Mattermost
internal/servicedesk/ service projects, the customer portal, request types, queues and approvals
internal/sla/ SLA clocks on business-hours calendars (pure)
internal/mcpserver/ the MCP server for AI assistants (tools over the API handlers)
internal/oauth/ the OAuth 2.1 authorisation server for assistants connecting to /mcp
internal/jiraimport/ the importer
internal/db/ migrations (goose) and queries (sqlc)
internal/integration/ tests of the whole API against PostgreSQL
web/ the React UI (Vite, TanStack Router and Query, dnd-kit, React Flow, Tiptap)
deploy/ the Helm chart (deploy/helm/wfm) and the local k3d setup (deploy/k3d)
test/scale/ the 100,000-issue scale test
Architecture explains how the pieces fit together.
Generated code¶
The API is defined first: change api/openapi.yaml, then regenerate. SQL lives in
internal/db/queries/*.sql and sqlc turns it into Go in internal/db/dbq.
make tools # install the pinned generators (sqlc, oapi-codegen)
make generate # sqlc, the Go API server and the UI's TypeScript API types
Generated files are committed. CI fails if they are out of date (make generate-check).
Database migrations¶
Migrations are goose SQL files in internal/db/migrations, numbered in order
(00015_service_desk.sql). In a cluster they run as an Argo CD PreSync job before new pods start,
while the previous version is still serving, so write them expand-then-contract: add columns and
tables first, backfill, and drop what the old version used only in a later release.
Tests¶
| Command | What it runs |
|---|---|
make test-unit |
Go packages that need no database: the workflow engine and CEL, the query language, ranking, passwords and tokens, rate limiting, the outbound request guard |
make test-integration |
The whole server against PostgreSQL, one fresh database per test cloned from a migrated template. Set TEST_DATABASE_URL (for example postgres://wfm:wfm@localhost:5432/wfm?sslmode=disable after make db-up), or let it start PostgreSQL with Testcontainers |
make lint |
go vet and the UI typecheck |
cd web && npm test |
Vitest unit tests for the UI |
cd web && npm run e2e |
Playwright end-to-end tests against a running server with development sign-in: make dev and npm run dev locally (or set E2E_BASE_URL) |
The end-to-end suite includes an axe audit of every main page in light and dark mode, which fails on any serious or critical WCAG 2.1 A/AA violation. Identity providers, Git hosts and the importer's source are tested against fakes of their APIs, so no test needs network access.
Before changing search, boards, reports or metrics, run the scale test before and after: it seeds 100,000 issues and times the API and the heaviest pages.
Local cluster: k3d and Tilt¶
k3d cluster create --config deploy/k3d/cluster.yaml # k3s and a local registry, Traefik on :8081
tilt up # builds the image, deploys the chart, port-forwards
open http://localhost:8080 # or http://wfm.localhost:8081 through Traefik
tilt up -- --cnpg # CloudNativePG instead of the throwaway PostgreSQL
tilt down && k3d cluster delete wfm
Tilt deploys the same chart with deploy/k3d/values-dev.yaml (development sign-in, one replica
of each mode, NetworkPolicies on) and a development Keycloak (below). For UI work, trigger the
vite resource: Vite on :5173 proxies /api to the port-forwarded web pod.
To check the chart the way CI does:
helm lint --strict deploy/helm/wfm -f deploy/helm/wfm/values-k3s.yaml \
--set secrets.values.WFM_SECRET_KEY=dummy-secret-key-0123456789abcdef
helm template wfm deploy/helm/wfm -f deploy/helm/wfm/values-eks.yaml | kubeconform -strict -kubernetes-version 1.33.0 -ignore-missing-schemas
Development Keycloak realm¶
deploy/k3d/keycloak runs Keycloak 26.4 in development mode for the k3d cluster and the CI
end-to-end job, importing the realm wfm from realm-wfm.json at every start. The admin console
is http://keycloak.localhost:8081/admin (admin / admin). The issuer is
http://keycloak.localhost:8081/realms/wfm for browsers (through Traefik) and for pods alike: a
CoreDNS rewrite (coredns-custom.yaml) resolves keycloak.localhost to the Keycloak Service,
which listens on 8081. deploy/k3d/values-dev.yaml configures the app for it; development sign-in
stays on.
| Client | Type | Settings | Used by |
|---|---|---|---|
workflow |
confidential, standard flow only | secret workflow-dev-client-secret; PKCE S256 required; redirect URIs http://wfm.localhost:8081/auth/keycloak/callback, http://localhost:8080/auth/keycloak/callback; post-logout redirects to the same hosts; back-channel logout URL http://wfm-web.wfm.svc.cluster.local/auth/keycloak/backchannel-logout with session required; front-channel logout off; mapper groups (group membership, full path, in ID, access and userinfo tokens) |
sign-in (WFM_OIDC_KEYCLOAK_*) |
workflow-sync |
confidential, service account only | secret workflow-sync-dev-secret; service account roles realm-management view-users, query-groups |
directory sync (WFM_KEYCLOAK_*) |
workflow-api |
confidential, no flows | none (it names the audience) | WFM_OIDC_KEYCLOAK_API_AUDIENCE=workflow-api |
workflow-ci |
confidential, service account only | secret workflow-ci-dev-secret; audience mapper adding workflow-api to access tokens; tokens carry azp and client_id = workflow-ci |
API tokens as the ci-bot service account (WFM_OIDC_KEYCLOAK_API_CLIENTS=workflow-ci=ci-bot) |
workflow-cli |
public, password grant and device flow | audience mapper workflow-api, groups mapper; the password grant is for tests only |
people's own API tokens |
| User | Password | Groups | Synced? | |
|---|---|---|---|---|
alice |
alice-dev-password |
alice@example.com (verified) | /workflow-users, /wfm-admins |
yes; bootstrap admin |
bob |
bob-dev-password |
bob@example.com (verified) | /workflow-users, /wfm-engineering/wfm-platform |
yes |
carol |
carol-dev-password |
carol@example.com (verified) | /wfm-engineering |
no: not in the access group |
dave |
dave-dev-password |
dave@example.com (verified) | /workflow-users |
no: disabled |
The app syncs members of /workflow-users every 5 minutes and mirrors the wfm-* groups
(wfm-admins, wfm-engineering, wfm-engineering/wfm-platform). Realm settings: SSL required
for external requests only, no self-registration, unique emails, login with email allowed.
To run the sync against a real Keycloak outside the cluster, start one with this realm and run the opt-in integration test:
docker run -d --name kc -p 127.0.0.1:8180:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
-v $PWD/deploy/k3d/keycloak/realm-wfm.json:/opt/keycloak/data/import/realm-wfm.json:ro \
quay.io/keycloak/keycloak:26.4.7 start-dev --import-realm
WFM_TEST_KEYCLOAK_URL=http://127.0.0.1:8180 go test ./internal/integration -run TestKeycloakRealm
Continuous integration¶
.github/workflows/ci.yml runs on every push and pull request:
| Job | What runs |
|---|---|
| Go | go mod tidy and generated code are up to date, go vet, and the unit and integration tests with the race detector against a PostgreSQL 17 service |
| Web | Generated API types are up to date, the typecheck, Vitest and the production build |
| Helm | helm lint and kubeconform (CRDs validated against the datree catalog) for every values overlay |
| Image | The container image builds |
| End to end | An ephemeral k3d cluster with CloudNativePG and the Keycloak realm: the chart is installed and upgraded, the ingress split is checked through Traefik, sign-in redirects to Keycloak, a directory sync provisions the realm's access group, and the Playwright suite runs against it. On pull requests it runs when the e2e label is added |
.github/workflows/release.yml runs on a vX.Y.Z tag: a multi-arch image signed with Cosign
(keyless) with an attested SBOM, the Helm chart as a signed OCI artifact, offline bundles for
amd64 and arm64, and a GitHub release.
Documentation¶
The reference documentation lives in docs/, next to the code it describes; change it in the same
pull request as the behaviour. The documentation site
(workflowproj/workflow-docs) copies these pages
with its scripts/sync_app_docs.py, so edit them here rather than there.
Pull requests¶
- Keep each pull request to one change, with its tests and documentation.
- Run
make lint,make testandcd web && npm testbefore pushing; add thee2elabel when the change touches the UI, sign-in or the chart. - Commit generated code with the change that caused it.