Starter prompt (paste into Claude Code / your agent):
Get familiar with the repo, prepare the environment, and run the tests in
tests/pr-governance.feature.md. Present a summary as a table with links so I can verify the intermediate states.
A local, self-contained demo of API governance across the full delivery loop — documented → discovered → delivered — running entirely on your machine:
- Gitea — local Git server + pull requests.
- Gitea Actions (
act_runner) — local CI. - Spectral — policy-as-code lint of OpenAPI on every PR (design-time quality).
- Microcks — contract testing of a running implementation against its contract.
- Backstage — API catalog that discovers contracts from Gitea.
The goal is not to deploy a production service. It simulates the control points around API delivery: source control, PRs, automated quality gates, contract testing, and a discoverable catalog.
The demo is split so a product team's repo stays separate from the platform that governs it:
- Governance context —
governance/: the policy (spectral/— ruleset + custom functions + examples,api-guidelines/— the guidelines doc + its TechDocs wiring) and the catalog app (api-catalog/). Single source of truth for the rules. (docker-compose.yml,scripts/andrunner-config.yamlstay at the repo root as orchestration that wiresgovernance/andexample/together.) - Consumer repo —
example/: the product unit (OpenAPI contract +catalog-info.yaml+ backend implementation) and a thin PR workflow. It is the starting template:gitea-seedmaterializes it into a fresh Gitea repo on eachup, and PRs happen against that Gitea repo (clone it, branch, PR) — so test PRs never pollute this project.
The consumer's CI links the ruleset (clones the governance repo at run time)
rather than vendoring it. Details: docs/demo-isolation.md.
Requires Docker Desktop (Compose). From the repo root:
docker compose --profile contract --profile catalog up -dThat brings up everything and seeds it — no .env, no scripts, no manual
Gitea setup. Two one-shot seed services do the first-run work a plain up
cannot:
- gitea-seed (after Gitea is healthy): creates the admin (
demo/demo12345), thegovernance-demoorg, and two repos — the consumer repo (devops-api-governance, pushed fromexample/) and a lean governance repo (api-governance, ruleset + functions + guidelines). Also writes a runner registration token to a shared volume. - microcks-seed (after Microcks starts): imports the OpenAPI contract from the consumer repo.
Gitea secrets are auto-generated and persisted in the gitea-data volume.
gitea-runner and backstage wait for gitea-seed via compose depends_on
conditions; the runner reads its token from the shared volume.
The seed pushes the committed HEAD, so commit your work before
upfor it to appear in Gitea / the catalog.
Endpoints:
| Service | URL | Notes |
|---|---|---|
| Gitea | http://localhost:3000 | login demo / demo12345 |
| Microcks | http://localhost:8080 | mocks + contract test results |
| Backstage | http://localhost:7007 | API catalog (guest sign-in) |
| sample-backend | http://localhost:8081 | provider under test |
After the stack is up, governance runs through pull requests in the consumer
repo (example/ → Gitea governance-demo/devops-api-governance):
- Branch → edit
contracts/orders-openapi.yamland/orsample-backend/→ open a PR intomainin Gitea. - Gitea Actions runs three gates in order (
pr-governance.yml), each gated byneeds:so the next stage only runs if the previous one passed:- Spectral (
spectral-openapi-check) — clones the governance repo for the ruleset, then lints the OpenAPI files changed in the PR, fails on error-severity findings. - Backwards-compatibility (
breaking-changes-check) — installs a pinnedoasdiff(v1.19.0) and diffs every PR-modified*openapi*.{yml,yaml}against its version on the PR's base branch. Fails on any ERR-severity breaking finding (--fail-on ERR). Brand-new files (no baseline) and identical-content edits are skipped; the whole job is skipped onworkflow_dispatch(no PR base ref). - Microcks contract test (
contract-test) — imports the PR branch's contract and tests the runningsample-backendagainst it via the Microcks REST API; fails on contract drift.
- Spectral (
- On merge, Backstage's Gitea provider discovers
catalog-info.yamlfrommainand the API entity appears/updates in the catalog.
A step-by-step walkthrough (green/red for each gate + merge→catalog) is in
docs/ci-test-path.md.
Root — orchestrator + meta:
| Path | Purpose |
|---|---|
docker-compose.yml |
All services + the seed services + profiles (contract, catalog). |
scripts/ |
seed-gitea.sh, seed-microcks.sh — orchestration run by the seed services. |
runner-config.yaml |
Joins CI job containers to gitea-network. |
tests/ |
pr-governance.feature.md — BDD walkthrough of the PR loop. |
docs/ |
demo-isolation.md (two-repo model), ci-test-path.md walkthrough. |
gitea-data/, runner-data/ |
Local runtime state, git-ignored, disposable. |
Governance context (governance/) — policy + catalog app:
| Path | Purpose |
|---|---|
governance/spectral/ |
spectral-ruleset.yaml + spectral-functions/ + examples/ — rules (policy as code), pushed to the api-governance Gitea repo and linked by consumer CI. |
governance/api-guidelines/ |
docs/index.md (the API design guidelines the rules encode) + catalog-info.yaml + mkdocs.yml — published to the api-governance Gitea repo and surfaced in Backstage as TechDocs. |
governance/api-catalog/ |
The Backstage app (committed source; builds entirely in Docker). |
Consumer repo (example/) — the product unit / starting template (nested, git-ignored):
| Path | Purpose |
|---|---|
contracts/orders-openapi.yaml |
The live OpenAPI contract (imported into Microcks). |
catalog-info.yaml |
Backstage entities (API + Component + Group) discovered from Gitea. |
sample-backend/ |
Minimal provider-under-test (conformant, or drifting via DRIFT=true). |
.gitea/workflows/pr-governance.yml |
One PR gate, three stages ordered via needs:: spectral-openapi-check (linked ruleset) → breaking-changes-check (oasdiff) → contract-test (Microcks). |
The ruleset extends Spectral's built-in OpenAPI rules with organization-specific
checks based on api-guidelines.md. Examples:
- OpenAPI documents must use OpenAPI 3.x.y and should use 3.1.y.
info.titlemust be Title Case and end withAPI;info.versionsemver.- JSON properties camelCase; schema names PascalCase; array names plural.
- paths kebab-case, no trailing
/; URI template vars per RFC 6570. - HTTPS server URLs; error responses
application/problem+jsonwithtype,title,detail.
Severities: error (mandatory, fails the gate) · warn (recommended) ·
info (optional).
npx -y @stoplight/spectral-cli lint -r governance/spectral/spectral-ruleset.yaml governance/spectral/examples/openapi-valid.yaml # passes
npx -y @stoplight/spectral-cli lint -r governance/spectral/spectral-ruleset.yaml governance/spectral/examples/openapi-invalid.yaml # violationsgovernance/spectral/examples/openapi-invalid.yaml intentionally breaks rules (version, title,
HTTPS, naming, Problem Detail, nullable). The CI gate lints only PR-changed
files, so this example does not break unrelated PRs.
The breaking-changes-check gate uses oasdiff
v1.19.0 (pinned, installed in the job at run time — no host install needed) to
diff every PR-modified *openapi*.{yml,yaml} against its version on the PR's
base branch and fail on any ERR-severity breaking finding (oasdiff breaking --fail-on ERR). Typical findings it blocks:
- New required request parameter (query / header / cookie).
- Existing request parameter narrowed (pattern added,
minLengthraised, …). - Response property removed from an existing operation.
- Operation removed.
Behaviour edge cases (verified in tests/pr-governance.feature.md):
| Case | Behaviour |
|---|---|
PR adds a BRAND-NEW *openapi*.yaml file (no baseline on main) |
skipped (--diff-filter=M); BC job passes |
| PR touches an OpenAPI file but content is identical vs main | skipped via git diff --quiet; emits a ::notice:: |
PR has no *openapi*.{yml,yaml} change at all |
has_files=false; oasdiff step not executed |
Workflow triggered via workflow_dispatch |
whole BC job skipped via if: github.event_name == 'pull_request' |
The gate is strict-mode: only making the change backward-compatible
(optional parameter, default value, removal of the new constraint, …) clears
it. Bumping info.version to a new MAJOR alone does not pass the gate —
semver-aware logic is intentionally out of scope for this iteration.
# Install (Linux amd64). For other platforms, see https://github.com/oasdiff/oasdiff/releases
curl -fsSL -o /tmp/oasdiff.tgz \
https://github.com/oasdiff/oasdiff/releases/download/v1.19.0/oasdiff_1.19.0_linux_amd64.tar.gz
tar -xzf /tmp/oasdiff.tgz -C /tmp && sudo install -m 0755 /tmp/oasdiff /usr/local/bin/oasdiff
# Diff the PR head against main (annotated, fails on ERR):
oasdiff breaking --fail-on ERR \
origin/main:contracts/orders-openapi.yaml \
contracts/orders-openapi.yaml -f githubactionssample-backend implements the Sample Orders API contract. To prove the gate:
# Conformant -> contract test passes (green).
# Drift -> contract test fails (red):
SAMPLE_BACKEND_DRIFT=true docker compose --profile contract up -d sample-backend
# ...open a PR... then restore:
SAMPLE_BACKEND_DRIFT=false docker compose --profile contract up -d sample-backendMicrocks uber is all-in-one (no Keycloak, embedded in-memory Mongo), so imported contracts are ephemeral and re-imported on each run.
Committed under governance/api-catalog/ and built entirely in Docker (Node 24 in-image — no
host Node). It serves frontend + backend on port 7007 and authenticates to Gitea
with the seeded admin credentials. The Gitea provider scans the governance-demo
org for catalog-info.yaml and renders the API with its OpenAPI document and a
link to the Microcks mocks/tests.
The official
@microcks/microcks-backstage-provider(0.0.7) is not used — it depends on removed Backstage packages and crashes startup on current Backstage. Microcks is surfaced via a link on the API entity instead.
This demo originally stopped at design-time linting. All three roadmap directions are now implemented:
- ✅ Central API catalog — Backstage discovers contracts from the Gitea org.
- ✅ Provider contract testing (Microcks) — running impl tested vs contract in CI.
- ✅ Backwards-compatibility checks (oasdiff) — PR diff of each modified
*openapi*.{yml,yaml}vs the base branch fails the gate on breaking changes (new required parameters, narrowed types, removed response fields, removed operations, …). Aligns withapi-guidelines.mdsemver/extension rules.
- Docker socket:
gitea-runnerandgitea-seedmount/var/run/docker.sock(runner starts job containers; seed runsgiteaCLI). Local-demo convenience, not a hardened pattern. - Runner network:
runner-config.yamlputs CI job containers ongitea-networkso checkout reacheshttp://gitea:3000/. - Backstage config:
governance/api-catalog/app-config.yamlis mounted, so config changes need only arestart(not a rebuild). For hostyarn dev, override the compose service-name hosts withlocalhostingovernance/api-catalog/app-config.local.yaml.
docker compose --profile contract --profile catalog down # stop
docker compose --profile contract --profile catalog down -v # + drop seeded volumes
rm -rf gitea-data runner-data # + drop Gitea/runner stateLicensed under CC BY 4.0. Share and adapt with attribution — see LICENSE.