@opsydyn/elysia-spectral 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/opsydyn/elysia-spectral/compare/v0.3.0...v0.4.0) (2026-04-15)
4
+
5
+
6
+ ### Features
7
+
8
+ * add source metadata to LintRunResult ([bdd5a81](https://github.com/opsydyn/elysia-spectral/commit/bdd5a817c00b5fdccf4e1d27c8dbf4c6d4c36d2c))
9
+
3
10
  ## [0.3.0](https://github.com/opsydyn/elysia-spectral/compare/v0.2.5...v0.3.0) (2026-04-14)
4
11
 
5
12
 
package/README.md CHANGED
@@ -426,29 +426,101 @@ spectralPlugin({
426
426
 
427
427
  Use `'warn'` for local development and `'error'` when artifact generation is required.
428
428
 
429
- ### Run the runtime in CI or tests
429
+ ### Run the runtime in CI
430
+
431
+ Use `createOpenApiLintRuntime` to run a standalone lint check in CI without starting an HTTP server. Import your Elysia app instance directly — the runtime uses `app.handle()` in-process to retrieve the generated OpenAPI document. No port binding required.
432
+
433
+ Create a script at `scripts/lint-openapi.ts`:
430
434
 
431
435
  ```ts
432
- import { Elysia } from 'elysia'
433
- import { openapi } from '@elysiajs/openapi'
436
+ import { app } from '../src/app'
434
437
  import { createOpenApiLintRuntime } from '@opsydyn/elysia-spectral'
435
438
 
436
- const app = new Elysia().use(openapi())
437
-
438
439
  const runtime = createOpenApiLintRuntime({
439
- ruleset: './spectral.yaml',
440
+ preset: 'strict',
440
441
  failOn: 'error',
441
442
  output: {
442
443
  console: true,
443
- jsonReportPath: './artifacts/openapi-lint.json',
444
- specSnapshotPath: true,
445
- artifactWriteFailures: 'error'
446
- }
444
+ sarifReportPath: './reports/openapi.sarif',
445
+ junitReportPath: './reports/openapi.junit.xml',
446
+ specSnapshotPath: './reports/openapi-snapshot.json',
447
+ artifactWriteFailures: 'error',
448
+ },
447
449
  })
448
450
 
449
- await runtime.run(app)
451
+ try {
452
+ await runtime.run(app)
453
+ process.exit(0)
454
+ } catch {
455
+ process.exit(1)
456
+ }
457
+ ```
458
+
459
+ Run it as a CI step:
460
+
461
+ ```yaml
462
+ - name: Lint OpenAPI spec
463
+ run: bun scripts/lint-openapi.ts
464
+ ```
465
+
466
+ ### Integrate SARIF with GitHub code scanning
467
+
468
+ SARIF output maps lint findings to code locations and surfaces them as GitHub code scanning alerts on pull requests.
469
+
470
+ ```yaml
471
+ - name: Lint OpenAPI spec
472
+ run: bun scripts/lint-openapi.ts
473
+
474
+ - name: Upload SARIF to GitHub code scanning
475
+ uses: github/codeql-action/upload-sarif@v3
476
+ with:
477
+ sarif_file: reports/openapi.sarif
478
+ if: always()
450
479
  ```
451
480
 
481
+ `if: always()` ensures the SARIF upload runs even when the lint step fails, so findings are visible on failing PRs.
482
+
483
+ ### Report lint findings as JUnit test results
484
+
485
+ JUnit output lets CI systems that consume test XML (Buildkite, CircleCI, GitLab, GitHub via third-party actions) show lint findings alongside test results.
486
+
487
+ ```yaml
488
+ - name: Lint OpenAPI spec
489
+ run: bun scripts/lint-openapi.ts
490
+
491
+ - name: Publish lint results
492
+ uses: dorny/test-reporter@v1
493
+ with:
494
+ name: OpenAPI Lint
495
+ path: reports/openapi.junit.xml
496
+ reporter: java-junit
497
+ if: always()
498
+ ```
499
+
500
+ ### Track OpenAPI snapshot drift
501
+
502
+ Commit the generated OpenAPI snapshot and use `git diff --exit-code` to detect when the API surface changes unexpectedly in a PR.
503
+
504
+ 1. Generate and commit the initial snapshot:
505
+
506
+ ```bash
507
+ bun scripts/lint-openapi.ts
508
+ git add reports/openapi-snapshot.json
509
+ git commit -m "chore: add openapi snapshot"
510
+ ```
511
+
512
+ 1. In CI, regenerate the snapshot and check for drift:
513
+
514
+ ```yaml
515
+ - name: Lint OpenAPI spec
516
+ run: bun scripts/lint-openapi.ts
517
+
518
+ - name: Check for snapshot drift
519
+ run: git diff --exit-code reports/openapi-snapshot.json
520
+ ```
521
+
522
+ If the snapshot has changed, the CI step fails and the diff is visible in the logs. Deliberate API changes are acknowledged by updating the committed snapshot — accidental ones are caught before they ship.
523
+
452
524
  ### Work on this repository locally
453
525
 
454
526
  From the monorepo root:
@@ -606,6 +678,7 @@ Example successful response:
606
678
  "result": {
607
679
  "ok": true,
608
680
  "generatedAt": "2026-04-06T12:00:00.000Z",
681
+ "source": "startup",
609
682
  "summary": {
610
683
  "error": 0,
611
684
  "warn": 0,
@@ -1,2 +1,2 @@
1
- import { _ as defaultRulesetResolvers, a as OpenApiLintArtifactWriteError, b as lintOpenApi, c as resolveStartupMode, d as LoadedRuleset, f as ResolvedRulesetCandidate, g as RulesetResolverInput, h as RulesetResolverContext, i as shouldFail, l as normalizeFindings, m as RulesetResolver, n as enforceThreshold, o as createOpenApiLintRuntime, p as RulesetLoadError, r as exceedsThreshold, s as isEnabled, t as OpenApiLintThresholdError, u as LoadResolvedRulesetOptions, v as loadResolvedRuleset, y as loadRuleset } from "../index-YZElrxnl.mjs";
1
+ import { _ as defaultRulesetResolvers, a as OpenApiLintArtifactWriteError, b as lintOpenApi, c as resolveStartupMode, d as LoadedRuleset, f as ResolvedRulesetCandidate, g as RulesetResolverInput, h as RulesetResolverContext, i as shouldFail, l as normalizeFindings, m as RulesetResolver, n as enforceThreshold, o as createOpenApiLintRuntime, p as RulesetLoadError, r as exceedsThreshold, s as isEnabled, t as OpenApiLintThresholdError, u as LoadResolvedRulesetOptions, v as loadResolvedRuleset, y as loadRuleset } from "../index-CMyl_MsI.mjs";
2
2
  export { LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintThresholdError, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, resolveStartupMode, shouldFail };
@@ -1,2 +1,2 @@
1
- import { a as OpenApiLintThresholdError, c as shouldFail, g as loadRuleset, h as loadResolvedRuleset, i as resolveStartupMode, m as defaultRulesetResolvers, n as createOpenApiLintRuntime, o as enforceThreshold, p as RulesetLoadError, r as isEnabled, s as exceedsThreshold, t as OpenApiLintArtifactWriteError, v as lintOpenApi, y as normalizeFindings } from "../core-DBjV7-E8.mjs";
1
+ import { a as OpenApiLintThresholdError, c as shouldFail, g as loadRuleset, h as loadResolvedRuleset, i as resolveStartupMode, m as defaultRulesetResolvers, n as createOpenApiLintRuntime, o as enforceThreshold, p as RulesetLoadError, r as isEnabled, s as exceedsThreshold, t as OpenApiLintArtifactWriteError, v as lintOpenApi, y as normalizeFindings } from "../core-D_ro1XEW.mjs";
2
2
  export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, resolveStartupMode, shouldFail };
@@ -53,6 +53,7 @@ const normalizeFindings = (diagnostics, spec) => {
53
53
  return {
54
54
  ok: summary.error === 0,
55
55
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
56
+ source: "manual",
56
57
  summary,
57
58
  findings
58
59
  };
@@ -1155,7 +1156,7 @@ const createOpenApiLintRuntime = (options = {}) => {
1155
1156
  lastSuccess: null,
1156
1157
  lastFailure: null,
1157
1158
  running: false,
1158
- async run(app) {
1159
+ async run(app, source = "manual") {
1159
1160
  if (inFlight) return await inFlight;
1160
1161
  inFlight = (async () => {
1161
1162
  const startedAt = /* @__PURE__ */ new Date();
@@ -1174,6 +1175,7 @@ const createOpenApiLintRuntime = (options = {}) => {
1174
1175
  } else if (options.preset && !loadedRuleset.source?.path) reporter.ruleset(`OpenAPI lint using "${options.preset}" preset.`);
1175
1176
  else if (loadedRuleset.source?.path) reporter.ruleset(`OpenAPI lint loaded ruleset ${loadedRuleset.source.path}.`);
1176
1177
  const result = await lintOpenApi(spec, loadedRuleset.ruleset);
1178
+ result.source = source;
1177
1179
  await writeOutputSinks(result, spec, options, artifactWriteFailureMode);
1178
1180
  runtime.latest = result;
1179
1181
  reporter.complete("OpenAPI lint completed.");
@@ -7,6 +7,7 @@ type SeverityThreshold = 'error' | 'warn' | 'info' | 'hint' | 'never';
7
7
  type LintSeverity = 'error' | 'warn' | 'info' | 'hint';
8
8
  type StartupLintMode = 'enforce' | 'report' | 'off';
9
9
  type OpenApiLintRuntimeStatus = 'idle' | 'running' | 'passed' | 'failed';
10
+ type LintRunSource = 'startup' | 'healthcheck' | 'manual';
10
11
  type ArtifactWriteFailureMode = 'warn' | 'error';
11
12
  type SpectralLogger = {
12
13
  info: (message: string) => void;
@@ -81,6 +82,7 @@ type LintFinding = {
81
82
  type LintRunResult = {
82
83
  ok: boolean;
83
84
  generatedAt: string;
85
+ source: LintRunSource;
84
86
  summary: {
85
87
  error: number;
86
88
  warn: number;
@@ -108,7 +110,7 @@ type OpenApiLintRuntime = {
108
110
  lastSuccess: LintRunResult | null;
109
111
  lastFailure: OpenApiLintRuntimeFailure | null;
110
112
  running: boolean;
111
- run: (app: AnyElysia) => Promise<LintRunResult>;
113
+ run: (app: AnyElysia, source?: LintRunSource) => Promise<LintRunResult>;
112
114
  };
113
115
  //#endregion
114
116
  //#region src/core/lint-openapi.d.ts
@@ -172,4 +174,4 @@ declare const exceedsThreshold: (severity: LintSeverity, threshold: SeverityThre
172
174
  declare const shouldFail: (result: LintRunResult, threshold: SeverityThreshold) => boolean;
173
175
  declare const enforceThreshold: (result: LintRunResult, threshold: SeverityThreshold) => void;
174
176
  //#endregion
175
- export { OpenApiLintSinkContext as A, LintRunResult as C, OpenApiLintRuntimeFailure as D, OpenApiLintRuntime as E, SpectralPluginOptions as F, StartupLintMode as I, SeverityThreshold as M, SpecProvider as N, OpenApiLintRuntimeStatus as O, SpectralLogger as P, LintFinding as S, OpenApiLintArtifacts as T, defaultRulesetResolvers as _, OpenApiLintArtifactWriteError as a, lintOpenApi as b, resolveStartupMode as c, LoadedRuleset as d, ResolvedRulesetCandidate as f, RulesetResolverInput as g, RulesetResolverContext as h, shouldFail as i, PresetName as j, OpenApiLintSink as k, normalizeFindings as l, RulesetResolver as m, enforceThreshold as n, createOpenApiLintRuntime as o, RulesetLoadError as p, exceedsThreshold as r, isEnabled as s, OpenApiLintThresholdError as t, LoadResolvedRulesetOptions as u, loadResolvedRuleset as v, LintSeverity as w, ArtifactWriteFailureMode as x, loadRuleset as y };
177
+ export { OpenApiLintSink as A, LintRunResult as C, OpenApiLintRuntime as D, OpenApiLintArtifacts as E, SpectralLogger as F, SpectralPluginOptions as I, StartupLintMode as L, PresetName as M, SeverityThreshold as N, OpenApiLintRuntimeFailure as O, SpecProvider as P, LintFinding as S, LintSeverity as T, defaultRulesetResolvers as _, OpenApiLintArtifactWriteError as a, lintOpenApi as b, resolveStartupMode as c, LoadedRuleset as d, ResolvedRulesetCandidate as f, RulesetResolverInput as g, RulesetResolverContext as h, shouldFail as i, OpenApiLintSinkContext as j, OpenApiLintRuntimeStatus as k, normalizeFindings as l, RulesetResolver as m, enforceThreshold as n, createOpenApiLintRuntime as o, RulesetLoadError as p, exceedsThreshold as r, isEnabled as s, OpenApiLintThresholdError as t, LoadResolvedRulesetOptions as u, loadResolvedRuleset as v, LintRunSource as w, ArtifactWriteFailureMode as x, loadRuleset as y };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as OpenApiLintSinkContext, C as LintRunResult, D as OpenApiLintRuntimeFailure, E as OpenApiLintRuntime, F as SpectralPluginOptions, I as StartupLintMode, M as SeverityThreshold, N as SpecProvider, O as OpenApiLintRuntimeStatus, P as SpectralLogger, S as LintFinding, T as OpenApiLintArtifacts, _ as defaultRulesetResolvers, a as OpenApiLintArtifactWriteError, b as lintOpenApi, c as resolveStartupMode, d as LoadedRuleset, f as ResolvedRulesetCandidate, g as RulesetResolverInput, h as RulesetResolverContext, i as shouldFail, j as PresetName, k as OpenApiLintSink, l as normalizeFindings, m as RulesetResolver, n as enforceThreshold, o as createOpenApiLintRuntime, p as RulesetLoadError, r as exceedsThreshold, s as isEnabled, t as OpenApiLintThresholdError, u as LoadResolvedRulesetOptions, v as loadResolvedRuleset, w as LintSeverity, x as ArtifactWriteFailureMode, y as loadRuleset } from "./index-YZElrxnl.mjs";
1
+ import { A as OpenApiLintSink, C as LintRunResult, D as OpenApiLintRuntime, E as OpenApiLintArtifacts, F as SpectralLogger, I as SpectralPluginOptions, L as StartupLintMode, M as PresetName, N as SeverityThreshold, O as OpenApiLintRuntimeFailure, P as SpecProvider, S as LintFinding, T as LintSeverity, _ as defaultRulesetResolvers, a as OpenApiLintArtifactWriteError, b as lintOpenApi, c as resolveStartupMode, d as LoadedRuleset, f as ResolvedRulesetCandidate, g as RulesetResolverInput, h as RulesetResolverContext, i as shouldFail, j as OpenApiLintSinkContext, k as OpenApiLintRuntimeStatus, l as normalizeFindings, m as RulesetResolver, n as enforceThreshold, o as createOpenApiLintRuntime, p as RulesetLoadError, r as exceedsThreshold, s as isEnabled, t as OpenApiLintThresholdError, u as LoadResolvedRulesetOptions, v as loadResolvedRuleset, w as LintRunSource, x as ArtifactWriteFailureMode, y as loadRuleset } from "./index-CMyl_MsI.mjs";
2
2
  import { RulesetDefinition } from "@stoplight/spectral-core";
3
3
  import { Elysia } from "elysia";
4
4
 
@@ -72,4 +72,4 @@ declare const strict: RulesetDefinition;
72
72
  //#region src/presets/index.d.ts
73
73
  declare const presets: Record<PresetName, RulesetDefinition>;
74
74
  //#endregion
75
- export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintSeverity, LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, PresetName, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, SeverityThreshold, SpecProvider, SpectralLogger, SpectralPluginOptions, StartupLintMode, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, presets, recommended, resolveStartupMode, server, shouldFail, spectralPlugin, strict };
75
+ export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintRunSource, LintSeverity, LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, PresetName, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, SeverityThreshold, SpecProvider, SpectralLogger, SpectralPluginOptions, StartupLintMode, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, presets, recommended, resolveStartupMode, server, shouldFail, spectralPlugin, strict };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as recommended, a as OpenApiLintThresholdError, c as shouldFail, d as server, f as resolveReporter, g as loadRuleset, h as loadResolvedRuleset, i as resolveStartupMode, l as presets, m as defaultRulesetResolvers, n as createOpenApiLintRuntime, o as enforceThreshold, p as RulesetLoadError, r as isEnabled, s as exceedsThreshold, t as OpenApiLintArtifactWriteError, u as strict, v as lintOpenApi, y as normalizeFindings } from "./core-DBjV7-E8.mjs";
1
+ import { _ as recommended, a as OpenApiLintThresholdError, c as shouldFail, d as server, f as resolveReporter, g as loadRuleset, h as loadResolvedRuleset, i as resolveStartupMode, l as presets, m as defaultRulesetResolvers, n as createOpenApiLintRuntime, o as enforceThreshold, p as RulesetLoadError, r as isEnabled, s as exceedsThreshold, t as OpenApiLintArtifactWriteError, u as strict, v as lintOpenApi, y as normalizeFindings } from "./core-D_ro1XEW.mjs";
2
2
  import { Elysia } from "elysia";
3
3
  //#region src/plugin.ts
4
4
  const spectralPlugin = (options = {}) => {
@@ -11,7 +11,7 @@ const spectralPlugin = (options = {}) => {
11
11
  const startupMode = resolveStartupMode(options);
12
12
  if (startupMode === "off") return;
13
13
  try {
14
- await runtime.run(app);
14
+ await runtime.run(app, "startup");
15
15
  } catch (error) {
16
16
  if (startupMode === "report" && error instanceof OpenApiLintThresholdError) {
17
17
  reporter.report(`OpenAPI lint exceeded the "${options.failOn ?? "error"}" threshold, but startup is continuing because startup.mode is "report".`);
@@ -37,7 +37,7 @@ const spectralPlugin = (options = {}) => {
37
37
  }
38
38
  try {
39
39
  const usedCache = !fresh && (runtime.latest !== null || runtime.running);
40
- const result = usedCache ? runtime.latest ?? await runtime.run(currentApp) : await runtime.run(currentApp);
40
+ const result = usedCache ? runtime.latest ?? await runtime.run(currentApp, "healthcheck") : await runtime.run(currentApp, "healthcheck");
41
41
  if (result === null) {
42
42
  set.status = 500;
43
43
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsydyn/elysia-spectral",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Thin Elysia plugin that lints generated OpenAPI documents with Spectral.",
5
5
  "packageManager": "bun@1.2.9",
6
6
  "publishConfig": {