Skip to content

Commit 1a567c9

Browse files
committed
ci(openapi): validate contract in CI
1 parent 9c6a9dd commit 1a567c9

8 files changed

Lines changed: 74 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ jobs:
3434
- name: Lint
3535
run: npm run lint
3636

37+
- name: OpenAPI contract
38+
run: npm run contract
39+
3740
- name: Test
3841
run: npm test
3942

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Deterministic `npm run benchmark:local` benchmark for cold upstream-backed searches and warm cache hits.
1717
- Cache resilience for article searches: cache read/write failures are logged and metriced without failing requests.
1818
- In-flight coalescing for identical cache misses so concurrent same-key searches share one upstream request per process.
19+
- Redocly-powered `npm run contract` check for the OpenAPI document, wired into CI.
1920

2021
### Security
2122

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ Errors: `{ "error": "message" }`. Rate limit: `429` with standard rate-limit hea
122122
| `npm run test:watch` | Vitest watch. |
123123
| `npm run test:coverage` | Tests + coverage. |
124124
| `npm run lint` | ESLint. |
125+
| `npm run contract` | Validate `docs/openapi.yaml` with Redocly CLI. |
125126
| `npm run smoke` | Curl-based smoke test against a running instance (`BASE_URL`, `QUERY`, `COUNT`, optional `CLIENT_API_KEY`). |
126127
| `npm run benchmark:local` | Builds the app, starts a fake GNews provider, and measures cold searches vs warm cache hits. See [docs/BENCHMARKS.md](docs/BENCHMARKS.md). |
127128

docs/CI.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,27 @@ The [workflow](../.github/workflows/ci.yml) runs on `ubuntu-latest` with **Node.
99
1. **`npm ci`** — reproducible install from `package-lock.json`.
1010
2. **`npm audit --audit-level=high`** — fails the job if high or critical advisories remain.
1111
3. **`npm run lint`**[ESLint](https://eslint.org/) on `src/`, `test/`, and `vitest.config.ts`.
12-
4. **`npm test`**[Vitest](https://vitest.dev/). GNews is **not** called: tests mock `axios`; no API key in GitHub Actions.
13-
5. **`npm run test:coverage`****Node 22 only**; uploads the `coverage/` directory (including `lcov.info`) as a workflow artifact named `coverage-lcov`.
14-
6. **[Codecov](https://codecov.io)****Node 22 only**; uploads `coverage/lcov.info`. For private repos set repository secret `CODECOV_TOKEN`. `fail_ci_if_error` is off so missing token does not break the build.
15-
7. **`npm run build`** — TypeScript compile to `dist/`.
12+
4. **`npm run contract`**[Redocly CLI](https://redocly.com/docs/cli) validates `docs/openapi.yaml` so the published API contract stays parseable and policy-compliant.
13+
5. **`npm test`**[Vitest](https://vitest.dev/). GNews is **not** called: tests mock `axios`; no API key in GitHub Actions.
14+
6. **`npm run test:coverage`****Node 22 only**; uploads the `coverage/` directory (including `lcov.info`) as a workflow artifact named `coverage-lcov`.
15+
7. **[Codecov](https://codecov.io)****Node 22 only**; uploads `coverage/lcov.info`. For private repos set repository secret `CODECOV_TOKEN`. `fail_ci_if_error` is off so missing token does not break the build.
16+
8. **`npm run build`** — TypeScript compile to `dist/`.
1617

1718
### Container (`docker` job)
1819

19-
8. **Buildx build**[Dockerfile](../Dockerfile) with **`provenance: mode=max`** and **SBOM** (no registry push). Validates supply-chain metadata generation in CI.
20+
9. **Buildx build**[Dockerfile](../Dockerfile) with **`provenance: mode=max`** and **SBOM** (no registry push). Validates supply-chain metadata generation in CI.
2021

2122
### Pull requests only
2223

23-
9. **[Dependency review](../.github/workflows/dependency-review.yml)** — flags vulnerable or blocked dependencies introduced by the PR.
24+
10. **[Dependency review](../.github/workflows/dependency-review.yml)** — flags vulnerable or blocked dependencies introduced by the PR.
2425

2526
### Every push / PR (supply chain)
2627

27-
10. **[SBOM](../.github/workflows/supply-chain.yml)**[Anchore SBOM Action](https://github.com/anchore/sbom-action) produces SPDX JSON and uploads it as a workflow artifact.
28+
11. **[SBOM](../.github/workflows/supply-chain.yml)**[Anchore SBOM Action](https://github.com/anchore/sbom-action) produces SPDX JSON and uploads it as a workflow artifact.
2829

2930
### `main` branch pushes only
3031

31-
11. **[Provenance](../.github/workflows/provenance.yml)**[build provenance attestation](https://github.com/actions/attest-build-provenance) for `package-lock.json` (best-effort; `continue-on-error` if attestations are unavailable on the plan).
32+
12. **[Provenance](../.github/workflows/provenance.yml)**[build provenance attestation](https://github.com/actions/attest-build-provenance) for `package-lock.json` (best-effort; `continue-on-error` if attestations are unavailable on the plan).
3233

3334
### Code scanning (`CodeQL` workflow)
3435

@@ -48,6 +49,7 @@ When you push an annotated tag matching `v*.*.*`, [release.yml](../.github/workf
4849
npm ci
4950
npm audit --audit-level=high
5051
npm run lint
52+
npm run contract
5153
npm test
5254
npm run build
5355
docker build .

docs/openapi.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ info:
55
REST API backed by the GNews search endpoint.
66
When `CLIENT_API_KEYS` is set on the server, send a matching `X-API-Key` header on `/api/*` requests.
77
version: 1.2.0
8+
license:
9+
name: MIT
10+
url: https://opensource.org/license/mit
811
servers:
912
- url: http://localhost:3000
1013
description: Local development
@@ -14,6 +17,7 @@ paths:
1417
get:
1518
summary: Liveness probe
1619
operationId: getHealth
20+
security: []
1721
responses:
1822
"200":
1923
description: Process is running
@@ -27,6 +31,7 @@ paths:
2731
summary: Readiness probe
2832
description: Returns 503 when `GNEWS_API_KEY` is not configured (non-test environments).
2933
operationId: getReady
34+
security: []
3035
responses:
3136
"200":
3237
description: Ready to serve traffic
@@ -45,6 +50,7 @@ paths:
4550
get:
4651
summary: Prometheus metrics
4752
operationId: getMetrics
53+
security: []
4854
responses:
4955
"200":
5056
description: Prometheus text / OpenMetrics exposition format
@@ -53,10 +59,26 @@ paths:
5359
schema:
5460
type: string
5561

62+
/openapi.yaml:
63+
get:
64+
summary: OpenAPI contract
65+
operationId: getOpenApiYaml
66+
security: []
67+
responses:
68+
"200":
69+
description: OpenAPI 3 document
70+
content:
71+
application/yaml:
72+
schema:
73+
type: string
74+
5675
/api/articles:
5776
get:
5877
summary: Search articles
5978
operationId: searchArticles
79+
security:
80+
- ApiKeyAuth: []
81+
- {}
6082
parameters:
6183
- name: X-API-Key
6284
in: header
@@ -136,6 +158,9 @@ paths:
136158
get:
137159
summary: Find article by exact title
138160
operationId: getArticleByTitle
161+
security:
162+
- ApiKeyAuth: []
163+
- {}
139164
parameters:
140165
- name: X-API-Key
141166
in: header
@@ -174,6 +199,9 @@ paths:
174199
get:
175200
summary: Filter articles by news source name
176201
operationId: listArticlesBySource
202+
security:
203+
- ApiKeyAuth: []
204+
- {}
177205
parameters:
178206
- name: X-API-Key
179207
in: header
@@ -250,6 +278,13 @@ paths:
250278
$ref: "#/components/responses/InternalError"
251279

252280
components:
281+
securitySchemes:
282+
ApiKeyAuth:
283+
type: apiKey
284+
in: header
285+
name: X-API-Key
286+
description: Required for `/api/*` requests only when the server sets `CLIENT_API_KEYS`.
287+
253288
responses:
254289
Unauthorized:
255290
description: Missing or invalid X-API-Key when CLIENT_API_KEYS is configured

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"test:watch": "vitest",
1515
"test:coverage": "vitest run --coverage",
1616
"lint": "eslint src test vitest.config.ts",
17+
"contract": "redocly lint docs/openapi.yaml",
1718
"smoke": "sh scripts/smoke-test.sh",
1819
"benchmark:local": "npm run build && node scripts/local-benchmark.mjs"
1920
},
@@ -44,6 +45,7 @@
4445
},
4546
"devDependencies": {
4647
"@eslint/js": "^9.39.4",
48+
"@redocly/cli": "^2.34.0",
4749
"@types/express": "^4.17.21",
4850
"@types/express-rate-limit": "^5.1.3",
4951
"@types/node": "^20.19.43",

redocly.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extends:
2+
- recommended
3+
4+
rules:
5+
no-server-example.com: off
6+
operation-4xx-response: off

0 commit comments

Comments
 (0)