TeamAPI latest
On this page
  1. CI integration
  2. Drift watch
  3. Weekly digest

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