On this page
Deployment and automation
Dockerfile builds the whole toolchain into one image whose entrypoint is the teamapi CLI, so every subcommand is available and serve-api is just the default:
docker build -t teamapi .
docker run --rm -p 3000:3000 \
-v "$PWD/examples/acme-org:/data:ro" \
-e TEAMAPI_API_TOKEN=$(openssl rand -hex 32) \
teamapi
Org documents are mounted read-only at /data. They remain in your git repository as the source of truth and are never baked into someone else's image.
The token isn't decoration. Inside a container every useful bind is non-loopback, and serve-api refuses that without a credential — so the refusal fires on the first docker run rather than after the org chart has been on the network for a month. --allow-anonymous is still there for the case where a trusted network really is the intent.
docker compose up api runs the same image with MCP over Streamable HTTP on the same port and mounts POST /reload for a deploy hook. It uses the endpoint because inotify does not propagate across every bind-mount implementation, so a filesystem watch can silently miss a change.
Full deployment notes — health checks, one-shot commands, published images — are in docs/deployment.md.
CI integration#
Add JGalego/TeamAPI/.github/actions/validate to a workflow for validation and a diagram preview on every PR that touches your teamapi.yml files, without anyone running the CLI locally:
on:
pull_request:
paths: ["org/**/teamapi.yml"]
permissions:
pull-requests: write
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: JGalego/TeamAPI/.github/actions/validate@main
with:
patterns: org
render-scope: topology
check-gaps: true # also fail on a blocking `teamapi gaps` finding
It installs @jgalego/teamapi and runs teamapi validate, then posts a single PR comment with the result — kept up to date on later pushes, and carrying a live-rendered Mermaid preview when validation passes. The job fails when validation fails, so it can gate a required check; check-gaps: true additionally runs teamapi gaps after validation passes and fails on a blocking finding (warnings print but never fail). This repo dogfoods it against examples/acme-org; see .github/workflows/teamapi-preview.yml and the action's inputs and outputs.
Drift watch#
Pull-request checks only fire when somebody touches a teamapi.yml. Most drift is the opposite: the documents sit still while the org moves around them — someone leaves and their agents keep an ownerId nobody holds, a service starts publishing an event nothing consumes. Nothing in a PR-triggered workflow ever notices.
JGalego/TeamAPI/.github/actions/drift runs the checks on a schedule and keeps one tracking issue in sync with what they find:
on:
schedule:
- cron: "0 7 * * 1-5"
permissions:
contents: read
issues: write
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: JGalego/TeamAPI/.github/actions/drift@main
with:
patterns: org
check-policies: "true"
assignees: your-github-login
It runs validate, gaps, and (optionally) policy and shadow-ai, and every check runs even when an earlier one fails — a report describing only the first problem found would be worth less than no report.
The issue is found by a marker in its body rather than by title, so it survives being renamed and the action updates it in place instead of opening a new one every morning. When everything comes back clean it closes the issue; if none is open it does nothing at all, because opening an issue to announce that there is no problem is how a bot gets muted.
This repo ships .github/workflows/drift.yml wired to examples/acme-org. Its schedule is inert until you set the repository variable TEAMAPI_DRIFT_ENABLED to true — workflow_dispatch always works, so you can try it before committing to the cadence.
Weekly digest#
Drift watch keeps an issue in sync. teamapi digest pushes the same picture somewhere people already look:
teamapi digest ./org --state .digest-state.json # posts to $TEAMAPI_DIGEST_WEBHOOK
3 teams — 2 blocking, 8 warnings.
Since last time:
blocking gaps: 0 → 2 (+2)
mean supervision: 6 → 0 (-6)
! platform-data [dangling-owner] agent 'pipeline-reviewer' is owned by 'dana-whitfield', who is not a member of platform-data
! platform-data [orphan-subscription] 'feature-store' subscribes to 'ModelTrained', which no declared service publishes
- stream-insights [unaccountable-agent] agent 'report-writer' names no ownerId, so nobody is accountable for it
gaps, policy and topology could always answer this. Getting the answer meant remembering to run three commands — and the findings that matter most are also the least urgent-feeling, so they wait behind whatever is on fire. Indefinitely.
--state records changes between digests. People learn to scroll past "four blocking gaps," while "two more than last week" shows movement. State is a JSON file that can live in a workflow cache, an artifact, or the repository; receiving a weekly summary requires no database. The digest lists only changed numbers so that repeated, unchanged summaries do not become noise.
--format html for email, --format json for anything else, --webhook (or TEAMAPI_DIGEST_WEBHOOK, since a webhook URL is a credential and shouldn't have to appear in a command line that lands in a CI log). It always exits 0: a digest that failed the build on a warning would be switched off within a fortnight, and then nobody would get the digest either.
.github/workflows/digest.yml runs it weekly, keeping the state file in the Actions cache. Like the drift watch, it's inert until you set TEAMAPI_DIGEST_ENABLED — a workflow that started posting into somebody's Slack the moment they merged it would be a bad neighbour.