Skip to content

Commit 4e88b7a

Browse files
committed
feat(ci): automate OpenAPI spec sync from backend, add ADR 0020
Add ADR 0020 (OpenAPI spec pipeline and version compatibility) and the receiver side of the pipeline: a `Sync OpenAPI spec from backend` workflow that, on a repository_dispatch from smartem-decisions (or manual/scheduled fallback), refreshes the committed swagger caches and rebuilds Pages by calling deploy-webui as a reusable workflow (workflow_call with a ref input, so it deploys the freshly-synced commit). Align docs and Claude Code guidance with the new flow: smartem-decisions is the canonical spec publisher; the devtools and frontend specs are downstream caches; the frontend's version check becomes semantic and observe-only.
1 parent a021a4f commit 4e88b7a

10 files changed

Lines changed: 171 additions & 18 deletions

File tree

.github/workflows/deploy-webui.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
name: Deploy WebUI to GitHub Pages
22

33
on:
4+
workflow_call:
5+
inputs:
6+
ref:
7+
description: 'Git ref to build (defaults to the triggering ref)'
8+
required: false
9+
type: string
10+
default: ''
411
workflow_dispatch:
512
push:
613
branches: [main]
@@ -23,6 +30,8 @@ jobs:
2330
runs-on: ubuntu-latest
2431
steps:
2532
- uses: actions/checkout@v6
33+
with:
34+
ref: ${{ inputs.ref }}
2635

2736
- name: Setup Node.js
2837
uses: actions/setup-node@v6
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Sync OpenAPI spec from backend
2+
3+
# Downstream cache of the canonical SmartEM OpenAPI spec (ADR 0020). smartem-decisions
4+
# fires a repository_dispatch (openapi-spec-updated) when it publishes a changed spec;
5+
# we pull it, refresh both committed swagger.json copies, and rebuild GitHub Pages.
6+
# workflow_dispatch (manual) and a daily schedule act as fallbacks if a dispatch is missed.
7+
8+
on:
9+
repository_dispatch:
10+
types: [openapi-spec-updated]
11+
workflow_dispatch:
12+
schedule:
13+
- cron: '17 4 * * *'
14+
15+
permissions:
16+
contents: write
17+
pages: write
18+
id-token: write
19+
20+
concurrency:
21+
group: sync-openapi-spec
22+
cancel-in-progress: false
23+
24+
env:
25+
CANONICAL_SPEC_URL: https://raw.githubusercontent.com/DiamondLightSource/smartem-decisions/main/docs/api/openapi.json
26+
27+
jobs:
28+
sync:
29+
runs-on: ubuntu-latest
30+
outputs:
31+
changed: ${{ steps.write.outputs.changed }}
32+
steps:
33+
- uses: actions/checkout@v6
34+
35+
- name: Download canonical spec
36+
run: |
37+
curl -fsSL "$CANONICAL_SPEC_URL" -o /tmp/openapi.json
38+
python3 -c "import json; json.load(open('/tmp/openapi.json'))" # validate JSON
39+
40+
- name: Refresh committed copies + detect change
41+
id: write
42+
run: |
43+
changed=false
44+
for dest in docs/api/smartem/swagger.json webui/public/api/smartem/swagger.json; do
45+
mkdir -p "$(dirname "$dest")"
46+
if ! cmp -s /tmp/openapi.json "$dest"; then changed=true; fi
47+
cp /tmp/openapi.json "$dest"
48+
done
49+
echo "changed=$changed" >> "$GITHUB_OUTPUT"
50+
echo "Spec changed: $changed"
51+
52+
- name: Commit refreshed copies
53+
if: steps.write.outputs.changed == 'true'
54+
run: |
55+
git config user.name "github-actions[bot]"
56+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
57+
git add docs/api/smartem/swagger.json webui/public/api/smartem/swagger.json
58+
git commit -m "chore(api): sync OpenAPI spec from smartem-decisions [skip ci]"
59+
git push origin HEAD:main
60+
61+
deploy:
62+
needs: sync
63+
if: needs.sync.outputs.changed == 'true'
64+
permissions:
65+
contents: read
66+
pages: write
67+
id-token: write
68+
uses: ./.github/workflows/deploy-webui.yml
69+
with:
70+
ref: main

claude-code/ARCHITECTURE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ smartem-backend API serves multiple consumers with different needs:
9090
| Frontend client | TBD | smartem-frontend | May need SSE for live updates |
9191
| Deposition client | No | fandanGO-cryoem-dls | REST only |
9292

93+
### OpenAPI spec flow (ADR 0020)
94+
95+
smartem-decisions is the **canonical OpenAPI spec publisher**. On push to main, when the API surface changes, it regenerates and commits the spec at `docs/api/openapi.json`, then fires a `repository_dispatch` to smartem-devtools. The receiver there refreshes its committed swagger copies under `docs/api/smartem/` and `webui/public/api/smartem/` and rebuilds GitHub Pages. smartem-frontend's `packages/api/src/openapi.json` is a downstream cache refreshed from the canonical backend spec (`npm run api:fetch`); both downstream copies are caches, never hand-maintained. The backend exposes a `/version` endpoint; the frontend runs an observe-only, semantic version check against it at boot. The agent ships from the same repo/tag as the backend, so it is version-locked by construction.
96+
9397
## Mocking Requirements for E2E Testing
9498

9599
| External Dependency | Mock Strategy | Status |
@@ -150,10 +154,10 @@ export ARIA_GQL_LOCAL=http://localhost:9002/graphql
150154

151155
| Affected | Action Required |
152156
|----------|-----------------|
153-
| smartem-frontend | Regenerate OpenAPI client |
157+
| smartem-frontend | Refresh cached spec from the canonical backend (`npm run api:update`) + regenerate client |
154158
| smartem-agent | Update api_client imports |
155159
| fandanGO-cryoem-dls | Update SmartEMAPIClient |
156-
| Docs | Regenerate OpenAPI spec |
160+
| Docs (smartem-devtools) | Auto-refreshed — backend `repository_dispatch` updates the swagger caches and redeploys Pages (no manual step; ADR 0020) |
157161
| Containers | Rebuild images |
158162

159163
### When smartem-backend MQ schema changes

claude-code/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ The primary repo of the workspace. Originally the only repo, now the hub of a mu
8282

8383
Pure SPA that talks to smartem-decisions backend API. Hosted in proximity to backend.
8484

85-
- Auto-generated API client from backend OpenAPI spec
85+
- API client auto-generated from a cached copy of the canonical backend OpenAPI spec (ADR 0020)
8686
- Models route for viewing ML prediction models
8787

8888
**Tech**: React 19, TanStack Router, Material-UI, Node.js 22+, Biome, Lefthook

claude-code/smartem-decisions/REPO-GUIDELINES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ uv run alembic revision --autogenerate -m "Description"
9292

9393
## Documentation
9494
- **Markdown**: Documentation in `docs/` synced to smartem-devtools webui as MDX
95-
- **API documentation**: Swagger/OpenAPI specs auto-generated
95+
- **API documentation**: this repo is the canonical OpenAPI spec publisher (ADR 0020). On push to main, when the API surface changes, CI regenerates and commits the spec at `docs/api/openapi.json` and fires a `repository_dispatch` to smartem-devtools, which refreshes its downstream swagger caches and redeploys Pages. The backend also exposes a `/version` endpoint consumed by the frontend's observe-only version check.
9696
- **Live development**: Run `npm run dev` in smartem-devtools/webui for hot-reload
9797

9898
## Available Skills

claude-code/smartem-frontend/REPO-GUIDELINES.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ smartem-frontend/
4040
│ │ │ ├── mutator.ts # Axios configuration
4141
│ │ │ ├── stubs.ts # Development stubs
4242
│ │ │ └── index.ts # Barrel export
43-
│ │ ├── openapi.json # OpenAPI spec (version controlled)
43+
│ │ ├── openapi.json # Cached copy of the canonical backend spec (committed; refreshed via api:fetch)
4444
│ │ └── orval.config.ts
4545
│ └── ui/ # Shared UI component library (@smartem/ui)
4646
│ └── src/
@@ -62,9 +62,11 @@ npm run dev:smartem # New app dev server
6262
npm run dev:smartem:mock # New app with mock API data
6363

6464
# API Client
65-
npm run api:update # Fetch OpenAPI spec + regenerate client
65+
npm run api:update # Refresh cached spec from the canonical backend, then regenerate client
6666
npm run api:fetch:local # Fetch from local backend (localhost:8000)
67-
npm run api:generate # Regenerate client from current spec
67+
npm run api:generate # Regenerate client from the committed spec cache
68+
# api:fetch pulls the canonical spec published by smartem-decisions (the backend),
69+
# NOT the devtools GitHub Pages copy. See ADR 0020.
6870

6971
# Code Quality
7072
npm run check # Biome lint + format (recommended)
@@ -80,7 +82,7 @@ npm run build:smartem # Build new app
8082

8183
## API Client Workflow
8284

83-
The frontend uses Orval to generate a type-safe API client from the backend OpenAPI spec. The client lives in `packages/api/` and is shared across both apps as `@smartem/api`.
85+
The frontend uses Orval to generate a type-safe API client from the backend OpenAPI spec. The client lives in `packages/api/` and is shared across both apps as `@smartem/api`. The committed `openapi.json` is a downstream cache of the canonical spec published by smartem-decisions; `npm run api:fetch` refreshes it from the backend (ADR 0020).
8486

8587
### When Backend API Changes
8688

@@ -109,12 +111,14 @@ function MyComponent() {
109111
}
110112
```
111113

112-
### Version Mismatch Warning
114+
### API Version Check (observe-only)
113115

114-
If console shows API version mismatch, regenerate the client:
115-
```bash
116-
npm run api:update
117-
```
116+
On boot the app compares the backend API version its client was built against to
117+
the live backend `/version` endpoint, using a semantic comparison (release portion
118+
only; the `dev`/`+sha` suffix is ignored). A difference is logged as an advisory and,
119+
in development, shown as a non-blocking banner — it is **observed, not enforced**, so
120+
rolling deploys where the two momentarily differ never break the app. If you want the
121+
client rebuilt against the current backend contract, run `npm run api:update`. See ADR 0020.
118122

119123
## Code Quality Tools
120124

docs/backend/api-documentation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ You can download the raw OpenAPI specifications:
8383

8484
- [Athena API Spec](../api/athena/swagger.json) - Official external specification
8585
- [Athena Source Spec](../athena-decision-service-api-spec.json) - Original specification file
86-
- [SmartEM API Spec](../api/smartem/swagger.json) - Generated from our implementation
86+
- [SmartEM API Spec](../api/smartem/swagger.json) - Published by the smartem-decisions backend and cached here automatically (ADR 0020)
8787

8888
### Using Specifications
8989

docs/decision-records/decisions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ Architectural decisions are made throughout a project's lifetime. As a way of ke
2222
- [ADR-0016: Facility Connector Fork Synchronization](/docs/explanations/decisions/0016-facility-connector-fork-sync) (Proposed)
2323
- [ADR-0017: SmartEM Frontend Monorepo Restructure](/docs/explanations/decisions/0017-smartem-frontend-monorepo-restructure)
2424
- [ADR-0019: SmartEM Frontend Release and Deployment Pipeline](/docs/explanations/decisions/0019-smartem-frontend-release-pipeline)
25+
- [ADR-0020: SmartEM OpenAPI Specification Pipeline and Version Compatibility](/docs/explanations/decisions/0020-openapi-spec-pipeline-and-version-compatibility)
2526

2627
For more information on ADRs see this [blog by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# 20. SmartEM OpenAPI Specification Pipeline and Version Compatibility
2+
3+
Date: 2026-06-01
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The SmartEM OpenAPI specification exists as **three independently committed copies** with no automation keeping them in step:
12+
13+
1. `smartem-decisions` — the FastAPI backend, which is the only true source: `app.openapi()`. It does **not** commit or publish its own spec.
14+
2. `smartem-frontend``packages/api/src/openapi.json`, the Orval input that generates the `@smartem/api` client.
15+
3. `smartem-devtools``docs/api/smartem/swagger.json` and `webui/public/api/smartem/swagger.json`, served as human-facing Swagger UI on GitHub Pages and fetched by the frontend's `npm run api:update`.
16+
17+
Because every copy is refreshed by hand, all three drift. As observed on 2026-06-01, the Pages-served copy was ~9 months stale (25 paths, `0.1.dev276…d20250818`) while the backend served 61 paths; the two devtools copies were themselves inconsistent (25 vs 58 paths). Issue #253 (`smartem-decisions`) records the same drift. A consumer running `npm run api:update` would have regenerated a badly regressed client.
18+
19+
Two further facts shape the design:
20+
21+
- **The backend version changes on every commit.** `info.version` is `setuptools_scm`-derived (e.g. `0.1.1rc48.dev3+gcd5206327`), so the *string* changes per commit even when the API surface does not. "Has the API changed?" must therefore be answered by diffing spec **content** (paths + components), not the version field.
22+
- **Compatibility checking exists but is unusable.** ADR 0019 specified a frontend `/version.json` manifest stamping the backend API version it was built against (`write-version-json.mjs`, shipped), plus a boot-time comparison against a backend `/version` endpoint, *observable not enforced*. But the backend `/version` endpoint was never built (only `/status` and `/health` exist), the boot check was never wired into the new app (issue #93), and the helper that does exist (`packages/api/src/version-check.ts`) compares with `serverVersion === API_VERSION` — an **exact full-string match**. Given commit-granular versions, that reports a mismatch on essentially every deployment, so it is wired only into the legacy app and is effectively inert.
23+
24+
The agent ships from the same repository and tag as the backend, so agent↔backend versions are locked by construction; there is no runtime check, and none is needed.
25+
26+
## Decision
27+
28+
Establish a single-source, automated pipeline with the backend as publisher, and finish ADR 0019's compatibility model.
29+
30+
### 1. The backend is the canonical publisher
31+
32+
`smartem-decisions` commits its own spec at `docs/api/openapi.json`. A CI job on push to `main` regenerates the spec from `app.openapi()` and, **only when the content has changed** (the spec compared with `info.version` and `servers` normalised out, so per-commit version churn does not trigger it), commits the refreshed file. The committed file is the canonical artefact, fetchable at a stable raw URL on `main`; it is also attached to GitHub Releases for version pinning. `smartem-frontend` and `smartem-devtools` are **downstream caches** of this artefact and are never hand-edited.
33+
34+
### 2. Backend → devtools sync is push-triggered, and rebuilds Pages
35+
36+
When the backend commits a changed spec, the same job sends a `repository_dispatch` (`event_type: openapi-spec-updated`) to `smartem-devtools`. A receiver workflow there downloads the canonical spec, writes both devtools copies, and rebuilds GitHub Pages by calling the existing deploy as a reusable job (`workflow_call`) — this side-steps the GitHub rule that a `GITHUB_TOKEN` commit does not itself trigger `on: push` workflows. The workflow also accepts `workflow_dispatch` (manual) and a low-frequency `schedule` as a fallback if a dispatch is ever missed.
37+
38+
The cross-repo dispatch requires one credential in `smartem-decisions` (a fine-grained token with `Contents: write` on `smartem-devtools`, or a GitHub App). This is the only new secret the design introduces; promptness across a repository boundary is not achievable without one, and a scheduled-only poll was rejected because it does not satisfy the requirement that Pages rebuild *when the backend publishes*.
39+
40+
### 3. The frontend refreshes from the canonical source
41+
42+
`smartem-frontend`'s `api:fetch` is repointed from the stale devtools Pages URL to the backend's canonical spec. `packages/api/src/openapi.json` remains committed — it is the hermetic build input for Orval and the source `write-version-json.mjs` stamps `backendApi` from, and it gives a reviewable contract diff in pull requests — but it is now a cache refreshed from the single source. The frontend keeps its own independent semantic version (ADR 0019); that is unaffected.
43+
44+
### 4. Compatibility is observable, semantic, and finished
45+
46+
- The backend gains a `GET /version` endpoint returning the API version (the ADR 0019 contract, finally built; `/status` is unchanged).
47+
- `version-check.ts` is rewritten to compare **semantically** — the release portion only, ignoring the `dev`/`+sha` suffix — and to read the backend version from `/version`. It is wired into `apps/smartem/src/main.tsx` to run once, non-blocking, at boot (closing #93). On divergence it logs to the console always and shows a non-blocking banner in development only; production logs. Compatibility is **observed, never enforced**, so rolling updates where the two momentarily differ do not self-inflict an outage. A pinned compatibility range remains deferred, as in ADR 0019.
48+
49+
### 5. The agent is documented as version-locked
50+
51+
No runtime check is added; the shared repository and tag guarantee a matched build. This is recorded so the absence of a check is a decision, not an oversight.
52+
53+
## Consequences
54+
55+
- Three drifting copies collapse to one source plus two derived caches; the drift class is eliminated and the published Swagger UI tracks the backend automatically.
56+
- `npm run api:update` becomes safe and canonical again.
57+
- One new secret (`DEVTOOLS_DISPATCH_TOKEN`) is required in `smartem-decisions`; the dispatch wiring is inert until it exists, and the manual `workflow_dispatch` path covers the gap.
58+
- This ADR supersedes the relevant surface of ADR 0019: the backend `/version` endpoint and the semantic, observe-only check are now specified and built here.
59+
- Closes `smartem-decisions` #253 (spec sync), `smartem-frontend` #93 (wire the boot check); partially addresses `smartem-devtools` #8 (consolidate API specs).
60+
- New releases are warranted on completion: `smartem-decisions` (new `/version` endpoint and the committed/published spec) and `smartem-frontend` (the wired compatibility check). `smartem-devtools` deploys continuously via Pages and needs no version tag for this change.
61+
- Documentation and Claude Code configuration across the three repositories that described the hand-maintained flow are updated to describe the pipeline.

docs/development/tools.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,16 +168,20 @@ uv run python tools/db_table_totals.py
168168

169169
### Generate API Docs
170170

171-
Generates API documentation from OpenAPI specs into `docs/api/`. Processes two APIs:
171+
Generates API documentation from OpenAPI specs into `docs/api/`:
172172

173-
- **Athena API**: copies the original spec from `docs/athena-decision-service-api-spec.json` and adds a local mock server entry
174-
- **SmartEM API**: imports the FastAPI app and extracts the OpenAPI schema at runtime
173+
- **Athena API**: copies the original spec from `docs/athena-decision-service-api-spec.json` and adds a local mock server entry.
175174

176175
```bash
177176
uv run python tools/generate_api_docs.py
178177
```
179178

180-
Output is written to `docs/api/athena/swagger.json` and `docs/api/smartem/swagger.json`.
179+
Output is written to `docs/api/athena/swagger.json`.
180+
181+
The **SmartEM API** spec is no longer generated here. smartem-decisions is the canonical
182+
publisher (ADR 0020): `docs/api/smartem/swagger.json` and `webui/public/api/smartem/swagger.json`
183+
are downstream caches, refreshed automatically by the `Sync OpenAPI spec from backend` workflow
184+
when smartem-decisions publishes a changed spec (which then rebuilds GitHub Pages). Do not hand-edit them.
181185

182186
## Miscellaneous Tools
183187

0 commit comments

Comments
 (0)