Skip to content

Commit da5e151

Browse files
authored
Merge pull request #52 from nks-hub/integrate-catalog-api-hardening-clean
ci: add catalog api contract drift check
2 parents e4de493 + 414a701 commit da5e151

42 files changed

Lines changed: 214 additions & 4594 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Catalog API Contract Drift
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "src/daemon/NKS.WebDevConsole.Daemon/Binaries/CatalogClient.cs"
8+
- "scripts/check-catalog-drift.mjs"
9+
- ".github/workflows/catalog-contract.yml"
10+
pull_request:
11+
paths:
12+
- "src/daemon/NKS.WebDevConsole.Daemon/Binaries/CatalogClient.cs"
13+
- "scripts/check-catalog-drift.mjs"
14+
- ".github/workflows/catalog-contract.yml"
15+
workflow_dispatch:
16+
inputs:
17+
catalog_version:
18+
description: "Pinned catalog-api version (e.g. 0.2.0)"
19+
required: false
20+
21+
concurrency:
22+
group: catalog-contract-${{ github.ref }}
23+
cancel-in-progress: true
24+
25+
jobs:
26+
check:
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- uses: actions/setup-node@v4
32+
with:
33+
node-version: "20"
34+
35+
- name: Run drift check
36+
env:
37+
# Pin the catalog-api release whose openapi.json we validate
38+
# the C# CatalogClient.cs against. Bump this whenever the
39+
# catalog service ships a new contract version.
40+
CATALOG_API_VERSION: ${{ inputs.catalog_version || '0.2.0' }}
41+
run: node scripts/check-catalog-drift.mjs

scripts/check-catalog-drift.mjs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Catalog API contract drift checker.
4+
*
5+
* Fetches ``openapi.json`` from a pinned ``wdc-catalog-api`` GitHub
6+
* release (or a local file via --spec) and verifies the endpoints /
7+
* required fields the C# ``CatalogClient.cs`` depends on are still
8+
* present with the expected shape. Emits a non-zero exit when the
9+
* contract drifts so CI can block a breaking merge.
10+
*
11+
* Usage (CI):
12+
* CATALOG_API_VERSION=0.2.0 node scripts/check-catalog-drift.mjs
13+
*
14+
* Usage (local):
15+
* node scripts/check-catalog-drift.mjs --spec /path/to/openapi.json
16+
*/
17+
18+
import fs from "node:fs";
19+
import path from "node:path";
20+
import { fileURLToPath } from "node:url";
21+
22+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
23+
const REPO_ROOT = path.resolve(__dirname, "..");
24+
25+
// Endpoints + DTOs the C# daemon consumes. Add to this list when
26+
// ``CatalogClient.cs`` grows a new dependency.
27+
const REQUIRED_PATHS = [
28+
"/healthz",
29+
"/api/v1/catalog",
30+
"/api/v1/catalog/{app_name}",
31+
"/api/v1/sync/config",
32+
"/api/v1/sync/config/{device_id}",
33+
];
34+
35+
// Wire-format fields on ``CatalogDocument`` / related DTOs. Values are
36+
// snake_case because ``CatalogClient.cs`` serializes with
37+
// ``JsonNamingPolicy.SnakeCaseLower``.
38+
const REQUIRED_FIELDS_BY_SCHEMA = {
39+
TokenResponse: ["token", "email"],
40+
ConfigSyncEntry: ["device_id", "updated_at", "payload"],
41+
ConfigSyncUploadRequest: ["device_id", "payload"],
42+
};
43+
44+
function fail(msg) {
45+
console.error(`[drift-check] ${msg}`);
46+
process.exitCode = 1;
47+
}
48+
49+
function ok(msg) {
50+
console.log(`[drift-check] ${msg}`);
51+
}
52+
53+
async function loadSpec(args) {
54+
const explicit = args.indexOf("--spec");
55+
if (explicit !== -1 && args[explicit + 1]) {
56+
const file = args[explicit + 1];
57+
ok(`Loading spec from ${file}`);
58+
return JSON.parse(fs.readFileSync(file, "utf-8"));
59+
}
60+
const version = process.env.CATALOG_API_VERSION;
61+
if (!version) {
62+
fail("CATALOG_API_VERSION not set (and no --spec passed).");
63+
fail("Either export CATALOG_API_VERSION=x.y.z or pass --spec path.");
64+
process.exit(2);
65+
}
66+
const tag = version.startsWith("v") ? version : `v${version}`;
67+
const url =
68+
`https://github.com/nks-hub/wdc-catalog-api/releases/download/` +
69+
`${tag}/openapi.json`;
70+
ok(`Fetching spec: ${url}`);
71+
const res = await fetch(url);
72+
if (!res.ok) {
73+
fail(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
74+
process.exit(2);
75+
}
76+
return await res.json();
77+
}
78+
79+
function checkPaths(spec) {
80+
const present = spec.paths ?? {};
81+
for (const p of REQUIRED_PATHS) {
82+
if (!(p in present)) {
83+
fail(`Missing required path: ${p}`);
84+
} else {
85+
ok(`Path present: ${p}`);
86+
}
87+
}
88+
}
89+
90+
function checkSchemas(spec) {
91+
const schemas = spec.components?.schemas ?? {};
92+
for (const [name, fields] of Object.entries(REQUIRED_FIELDS_BY_SCHEMA)) {
93+
const schema = schemas[name];
94+
if (!schema) {
95+
fail(`Missing schema: ${name}`);
96+
continue;
97+
}
98+
const props = schema.properties ?? {};
99+
for (const field of fields) {
100+
if (!(field in props)) {
101+
fail(`Schema ${name} is missing field '${field}'`);
102+
}
103+
}
104+
ok(`Schema ${name} has required fields [${fields.join(", ")}]`);
105+
}
106+
}
107+
108+
function checkCSharpStillCompiles() {
109+
// Light sanity check: the C# client file still exists and mentions
110+
// the critical DTO class names. Full type-level check needs dotnet
111+
// build, which the CI's downstream job already runs.
112+
const clientPath = path.join(
113+
REPO_ROOT,
114+
"src/daemon/NKS.WebDevConsole.Daemon/Binaries/CatalogClient.cs",
115+
);
116+
if (!fs.existsSync(clientPath)) {
117+
fail(`Missing CatalogClient.cs at ${clientPath}`);
118+
return;
119+
}
120+
const source = fs.readFileSync(clientPath, "utf-8");
121+
for (const token of ["CatalogDocument", "ReleaseDoc", "DownloadDoc"]) {
122+
if (!source.includes(token)) {
123+
fail(`CatalogClient.cs no longer references '${token}'`);
124+
}
125+
}
126+
ok("CatalogClient.cs references CatalogDocument/ReleaseDoc/DownloadDoc");
127+
}
128+
129+
const args = process.argv.slice(2);
130+
const spec = await loadSpec(args);
131+
checkPaths(spec);
132+
checkSchemas(spec);
133+
checkCSharpStillCompiles();
134+
135+
if (process.exitCode) {
136+
console.error("[drift-check] FAILED — catalog-api contract has drifted.");
137+
process.exit(1);
138+
}
139+
ok("All contract checks passed.");

services/catalog-api-MOVED.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# catalog-api has moved
2+
3+
This service was extracted into its own public repository:
4+
5+
**https://github.com/nks-hub/wdc-catalog-api**
6+
7+
## Why
8+
9+
- Zero code coupling with the rest of the monorepo — only a JSON wire contract
10+
- Separate tech stack (Python/FastAPI/Docker vs C#/.NET/Electron)
11+
- Independent release cadence — catalog scraping iterates weekly, desktop app monthly
12+
- Dedicated CI pipeline (ruff + mypy + pytest + pip-audit) instead of cross-stack overhead
13+
- Cleaner contributor surface — backend contributors don't need to clone .NET + Electron
14+
15+
## Contract
16+
17+
The desktop daemon consumes the public JSON at `https://wdc.nks-hub.cz/api/v1/catalog`.
18+
The new repo publishes `openapi.json` as a GitHub Release asset on every
19+
tagged version — this repo's CI pins a catalog schema version via
20+
`CATALOG_API_VERSION` and regenerates C# DTOs, failing the build if the
21+
hand-maintained `CatalogClient.cs` drifts from the spec.
22+
23+
## History
24+
25+
The pre-split monorepo state is preserved under tag
26+
[`catalog-api-pre-split`](https://github.com/nks-hub/webdev-console/releases/tag/catalog-api-pre-split).
27+
The new repo was created via `git filter-repo --subdirectory-filter services/catalog-api`
28+
so all 700+ commits retain their original authors and dates.
29+
30+
## Deployment
31+
32+
The public instance `wdc.nks-hub.cz` is deployed from
33+
`ghcr.io/nks-hub/wdc-catalog-api:latest`. See the new repo's README for
34+
operational runbooks.

services/catalog-api/.dockerignore

Lines changed: 0 additions & 13 deletions
This file was deleted.

services/catalog-api/.gitignore

Lines changed: 0 additions & 12 deletions
This file was deleted.

services/catalog-api/Dockerfile

Lines changed: 0 additions & 38 deletions
This file was deleted.

0 commit comments

Comments
 (0)