Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/catalog-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Catalog API Contract Drift

on:
push:
branches: [main]
paths:
- "src/daemon/NKS.WebDevConsole.Daemon/Binaries/CatalogClient.cs"
- "scripts/check-catalog-drift.mjs"
- ".github/workflows/catalog-contract.yml"
pull_request:
paths:
- "src/daemon/NKS.WebDevConsole.Daemon/Binaries/CatalogClient.cs"
- "scripts/check-catalog-drift.mjs"
- ".github/workflows/catalog-contract.yml"
workflow_dispatch:
inputs:
catalog_version:
description: "Pinned catalog-api version (e.g. 0.2.0)"
required: false

concurrency:
group: catalog-contract-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Run drift check
env:
# Pin the catalog-api release whose openapi.json we validate
# the C# CatalogClient.cs against. Bump this whenever the
# catalog service ships a new contract version.
CATALOG_API_VERSION: ${{ inputs.catalog_version || '0.2.0' }}
run: node scripts/check-catalog-drift.mjs
139 changes: 139 additions & 0 deletions scripts/check-catalog-drift.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env node
/**
* Catalog API contract drift checker.
*
* Fetches ``openapi.json`` from a pinned ``wdc-catalog-api`` GitHub
* release (or a local file via --spec) and verifies the endpoints /
* required fields the C# ``CatalogClient.cs`` depends on are still
* present with the expected shape. Emits a non-zero exit when the
* contract drifts so CI can block a breaking merge.
*
* Usage (CI):
* CATALOG_API_VERSION=0.2.0 node scripts/check-catalog-drift.mjs
*
* Usage (local):
* node scripts/check-catalog-drift.mjs --spec /path/to/openapi.json
*/

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, "..");

// Endpoints + DTOs the C# daemon consumes. Add to this list when
// ``CatalogClient.cs`` grows a new dependency.
const REQUIRED_PATHS = [
"/healthz",
"/api/v1/catalog",
"/api/v1/catalog/{app_name}",
"/api/v1/sync/config",
"/api/v1/sync/config/{device_id}",
Comment on lines +31 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add sync-exists endpoint to drift guard

The drift check omits "/api/v1/sync/config/{device_id}/exists" from REQUIRED_PATHS, even though the daemon calls that contract in Program.cs (/api/sync/exists proxy). If the catalog API drops or renames only that endpoint, this CI check will still pass and the sync-exists flow will regress at runtime without early detection.

Useful? React with 👍 / 👎.

];

// Wire-format fields on ``CatalogDocument`` / related DTOs. Values are
// snake_case because ``CatalogClient.cs`` serializes with
// ``JsonNamingPolicy.SnakeCaseLower``.
const REQUIRED_FIELDS_BY_SCHEMA = {
TokenResponse: ["token", "email"],
ConfigSyncEntry: ["device_id", "updated_at", "payload"],
ConfigSyncUploadRequest: ["device_id", "payload"],
Comment on lines +38 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate catalog DTO schemas instead of unrelated ones

This schema map does not include the catalog document types consumed by CatalogClient (CatalogDocument, AppDoc, ReleaseDoc, DownloadDoc fields like apps, releases, downloads, url, etc.), so breaking changes to the binary catalog payload can pass this check and only fail in production when RefreshAsync deserializes. The current entries focus on auth/sync DTOs and miss the primary contract this workflow claims to protect.

Useful? React with 👍 / 👎.

};

function fail(msg) {
console.error(`[drift-check] ${msg}`);
process.exitCode = 1;
}

function ok(msg) {
console.log(`[drift-check] ${msg}`);
}

async function loadSpec(args) {
const explicit = args.indexOf("--spec");
if (explicit !== -1 && args[explicit + 1]) {
const file = args[explicit + 1];
ok(`Loading spec from ${file}`);
return JSON.parse(fs.readFileSync(file, "utf-8"));
}
const version = process.env.CATALOG_API_VERSION;
if (!version) {
fail("CATALOG_API_VERSION not set (and no --spec passed).");
fail("Either export CATALOG_API_VERSION=x.y.z or pass --spec path.");
process.exit(2);
}
const tag = version.startsWith("v") ? version : `v${version}`;
const url =
`https://github.com/nks-hub/wdc-catalog-api/releases/download/` +
`${tag}/openapi.json`;
ok(`Fetching spec: ${url}`);
const res = await fetch(url);
if (!res.ok) {
fail(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
process.exit(2);
}
return await res.json();
}

function checkPaths(spec) {
const present = spec.paths ?? {};
for (const p of REQUIRED_PATHS) {
if (!(p in present)) {
fail(`Missing required path: ${p}`);
} else {
ok(`Path present: ${p}`);
}
}
}

function checkSchemas(spec) {
const schemas = spec.components?.schemas ?? {};
for (const [name, fields] of Object.entries(REQUIRED_FIELDS_BY_SCHEMA)) {
const schema = schemas[name];
if (!schema) {
fail(`Missing schema: ${name}`);
continue;
}
const props = schema.properties ?? {};
for (const field of fields) {
if (!(field in props)) {
fail(`Schema ${name} is missing field '${field}'`);
}
}
ok(`Schema ${name} has required fields [${fields.join(", ")}]`);
}
}

function checkCSharpStillCompiles() {
// Light sanity check: the C# client file still exists and mentions
// the critical DTO class names. Full type-level check needs dotnet
// build, which the CI's downstream job already runs.
const clientPath = path.join(
REPO_ROOT,
"src/daemon/NKS.WebDevConsole.Daemon/Binaries/CatalogClient.cs",
);
if (!fs.existsSync(clientPath)) {
fail(`Missing CatalogClient.cs at ${clientPath}`);
return;
}
const source = fs.readFileSync(clientPath, "utf-8");
for (const token of ["CatalogDocument", "ReleaseDoc", "DownloadDoc"]) {
if (!source.includes(token)) {
fail(`CatalogClient.cs no longer references '${token}'`);
}
}
ok("CatalogClient.cs references CatalogDocument/ReleaseDoc/DownloadDoc");
}

const args = process.argv.slice(2);
const spec = await loadSpec(args);
checkPaths(spec);
checkSchemas(spec);
checkCSharpStillCompiles();

if (process.exitCode) {
console.error("[drift-check] FAILED — catalog-api contract has drifted.");
process.exit(1);
}
ok("All contract checks passed.");
34 changes: 34 additions & 0 deletions services/catalog-api-MOVED.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# catalog-api has moved

This service was extracted into its own public repository:

**https://github.com/nks-hub/wdc-catalog-api**

## Why

- Zero code coupling with the rest of the monorepo — only a JSON wire contract
- Separate tech stack (Python/FastAPI/Docker vs C#/.NET/Electron)
- Independent release cadence — catalog scraping iterates weekly, desktop app monthly
- Dedicated CI pipeline (ruff + mypy + pytest + pip-audit) instead of cross-stack overhead
- Cleaner contributor surface — backend contributors don't need to clone .NET + Electron

## Contract

The desktop daemon consumes the public JSON at `https://wdc.nks-hub.cz/api/v1/catalog`.
The new repo publishes `openapi.json` as a GitHub Release asset on every
tagged version — this repo's CI pins a catalog schema version via
`CATALOG_API_VERSION` and regenerates C# DTOs, failing the build if the
hand-maintained `CatalogClient.cs` drifts from the spec.

## History

The pre-split monorepo state is preserved under tag
[`catalog-api-pre-split`](https://github.com/nks-hub/webdev-console/releases/tag/catalog-api-pre-split).
The new repo was created via `git filter-repo --subdirectory-filter services/catalog-api`
so all 700+ commits retain their original authors and dates.

## Deployment

The public instance `wdc.nks-hub.cz` is deployed from
`ghcr.io/nks-hub/wdc-catalog-api:latest`. See the new repo's README for
operational runbooks.
13 changes: 0 additions & 13 deletions services/catalog-api/.dockerignore

This file was deleted.

12 changes: 0 additions & 12 deletions services/catalog-api/.gitignore

This file was deleted.

38 changes: 0 additions & 38 deletions services/catalog-api/Dockerfile

This file was deleted.

Loading
Loading