autotel-eventcatalog 1.0.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 +196 -0
- package/CONTRIBUTING.md +212 -0
- package/README.md +307 -0
- package/action.yml +155 -0
- package/dist/cli.cjs +1071 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1065 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +794 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +267 -0
- package/dist/index.d.ts +267 -0
- package/dist/index.js +764 -0
- package/dist/index.js.map +1 -0
- package/docs/CONTRACT.md +280 -0
- package/docs/EXTENDING.md +248 -0
- package/docs/TROUBLESHOOTING.md +220 -0
- package/docs/UPGRADING.md +202 -0
- package/package.json +78 -0
- package/schemas/README.md +44 -0
- package/schemas/drift-report-v0.1.0.json +107 -0
- package/schemas/drift-report-v0.2.0.json +137 -0
- package/schemas/drift-summary-v0.1.0.json +74 -0
- package/schemas/drift-summary-v0.2.0.json +74 -0
- package/schemas/stamp-summary-v0.1.0.json +54 -0
- package/src/__fixtures__/drift-report-all.golden.json +33 -0
- package/src/__fixtures__/drift-summary-clean.golden.json +17 -0
- package/src/__fixtures__/drift-summary-drifty.golden.json +17 -0
- package/src/__fixtures__/stamp-summary-noop.golden.json +10 -0
- package/src/catalog.test.ts +63 -0
- package/src/catalog.ts +169 -0
- package/src/cli.e2e.test.ts +310 -0
- package/src/cli.ts +402 -0
- package/src/contract.test.ts +395 -0
- package/src/diff-vs-base.test.ts +145 -0
- package/src/diff-vs-base.ts +242 -0
- package/src/diff.test.ts +384 -0
- package/src/diff.ts +296 -0
- package/src/index.ts +73 -0
- package/src/policy.test.ts +75 -0
- package/src/policy.ts +41 -0
- package/src/renderers/index.ts +35 -0
- package/src/renderers/json.ts +33 -0
- package/src/renderers/markdown.ts +223 -0
- package/src/renderers/renderers.test.ts +79 -0
- package/src/renderers/terminal.ts +30 -0
- package/src/renderers/types.ts +26 -0
- package/src/report.test.ts +205 -0
- package/src/report.ts +27 -0
- package/src/snapshot.ts +25 -0
- package/src/stamp.test.ts +283 -0
- package/src/stamp.ts +232 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Extending autotel-eventcatalog
|
|
2
|
+
|
|
3
|
+
The package is designed to be extended in **one** direction: new
|
|
4
|
+
renderers. Everything else (the diff engine, the policy layer, the
|
|
5
|
+
stamper) is deliberately stable. See [CONTRIBUTING.md](../CONTRIBUTING.md#3-no-domain-specific-extensions-to-the-core)
|
|
6
|
+
for why.
|
|
7
|
+
|
|
8
|
+
## Writing a custom renderer
|
|
9
|
+
|
|
10
|
+
A renderer is a small adapter that turns a drift result into output
|
|
11
|
+
text. The built-ins are `markdown`, `terminal`, and `json`. To add a new
|
|
12
|
+
one (e.g. SARIF, Slack-flavoured markdown, GitHub Check Runs API JSON,
|
|
13
|
+
your in-house dashboard payload), implement the `Renderer` interface and
|
|
14
|
+
register it.
|
|
15
|
+
|
|
16
|
+
### Step 1: implement the interface
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// src/renderers/sarif.ts
|
|
20
|
+
import type { DriftReport } from '../diff';
|
|
21
|
+
import type { DriftDelta } from '../diff-vs-base';
|
|
22
|
+
import type { Renderer } from './types';
|
|
23
|
+
|
|
24
|
+
function renderReport(report: DriftReport): string {
|
|
25
|
+
// SARIF (https://sarifweb.azurewebsites.net) wants a fixed envelope
|
|
26
|
+
// with `runs[].results[]`. Each drift finding becomes one result.
|
|
27
|
+
const results = [
|
|
28
|
+
...report.events.observedButUndocumented.map((name) => ({
|
|
29
|
+
ruleId: 'autotel/event-undocumented',
|
|
30
|
+
level: 'warning',
|
|
31
|
+
message: { text: `Event \`${name}\` is emitted but not documented.` },
|
|
32
|
+
locations: [
|
|
33
|
+
{
|
|
34
|
+
physicalLocation: {
|
|
35
|
+
artifactLocation: { uri: `events/${name}` },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
})),
|
|
40
|
+
// ... documented-but-unseen, field drift, services, channels
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
return JSON.stringify(
|
|
44
|
+
{
|
|
45
|
+
version: '2.1.0',
|
|
46
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
47
|
+
runs: [
|
|
48
|
+
{
|
|
49
|
+
tool: {
|
|
50
|
+
driver: {
|
|
51
|
+
name: 'autotel-eventcatalog',
|
|
52
|
+
informationUri: 'https://github.com/jagreehal/autotel',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
results,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
null,
|
|
60
|
+
2,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderDelta(delta: DriftDelta): string {
|
|
65
|
+
// For PR-mode runs, emit results only for `delta.introduced`.
|
|
66
|
+
// ... similar shape ...
|
|
67
|
+
return JSON.stringify({
|
|
68
|
+
/* ... */
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const sarifRenderer: Renderer = {
|
|
73
|
+
name: 'sarif',
|
|
74
|
+
description:
|
|
75
|
+
'Static Analysis Results Interchange Format (GitHub Code Scanning).',
|
|
76
|
+
renderReport,
|
|
77
|
+
renderDelta,
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Step 2: register it
|
|
82
|
+
|
|
83
|
+
Add the renderer to the registry in `src/renderers/index.ts`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { sarifRenderer } from './sarif';
|
|
87
|
+
|
|
88
|
+
export const RENDERERS: readonly Renderer[] = [
|
|
89
|
+
markdownRenderer,
|
|
90
|
+
terminalRenderer,
|
|
91
|
+
jsonRenderer,
|
|
92
|
+
sarifRenderer, // ← new
|
|
93
|
+
];
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
That's it. The CLI's `--format sarif` will automatically work, the help
|
|
97
|
+
text will mention it, and the validator that catches bad `--format`
|
|
98
|
+
values will accept it.
|
|
99
|
+
|
|
100
|
+
### Step 3: write the tests
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// src/renderers/sarif.test.ts
|
|
104
|
+
import { describe, it, expect } from 'vitest';
|
|
105
|
+
import { sarifRenderer } from './sarif';
|
|
106
|
+
import type { DriftReport } from '../diff';
|
|
107
|
+
|
|
108
|
+
const driftyReport: DriftReport = {
|
|
109
|
+
snapshotGeneratedAt: '2026-05-22T00:00:00.000Z',
|
|
110
|
+
snapshotService: 'fixture',
|
|
111
|
+
events: {
|
|
112
|
+
observedButUndocumented: ['order.cancelled'],
|
|
113
|
+
documentedButUnseen: [],
|
|
114
|
+
fieldDrift: [],
|
|
115
|
+
},
|
|
116
|
+
services: { observedButUndocumented: [] },
|
|
117
|
+
channels: { observedButUndocumented: [] },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
describe('sarifRenderer', () => {
|
|
121
|
+
it('emits a valid SARIF v2.1.0 envelope', () => {
|
|
122
|
+
const out = JSON.parse(sarifRenderer.renderReport(driftyReport));
|
|
123
|
+
expect(out.version).toBe('2.1.0');
|
|
124
|
+
expect(out.runs[0].tool.driver.name).toBe('autotel-eventcatalog');
|
|
125
|
+
expect(out.runs[0].results).toHaveLength(1);
|
|
126
|
+
expect(out.runs[0].results[0].ruleId).toBe('autotel/event-undocumented');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Step 4 (optional): document it
|
|
132
|
+
|
|
133
|
+
If the renderer is going to be a first-class citizen, add a row to the
|
|
134
|
+
"Renderers" section in the README and bump the changeset (`pnpm
|
|
135
|
+
changeset`).
|
|
136
|
+
|
|
137
|
+
## What a good renderer looks like
|
|
138
|
+
|
|
139
|
+
- **Pure function from input to string.** No I/O, no globals, no side
|
|
140
|
+
effects. The CLI is responsible for writing output; the renderer is
|
|
141
|
+
responsible for shaping it.
|
|
142
|
+
- **Handles both `renderReport` and `renderDelta`.** Even if you don't
|
|
143
|
+
care about the delta mode, return _something_. Clean delta output is
|
|
144
|
+
often "no new findings" plus the resolved section.
|
|
145
|
+
- **Self-contained.** A renderer should not import from `cli.ts`, from
|
|
146
|
+
`policy.ts`, or from another renderer's internals. The core types
|
|
147
|
+
(`DriftReport`, `DriftDelta`) are the only contract.
|
|
148
|
+
- **Deterministic.** Same input, same output. No timestamps from
|
|
149
|
+
`Date.now()`, no random IDs. (The snapshot already carries
|
|
150
|
+
`snapshotGeneratedAt` if you need a timestamp.)
|
|
151
|
+
- **Compact.** The Markdown renderer is ~120 lines. If yours is
|
|
152
|
+
significantly longer, you're probably doing logic that belongs in
|
|
153
|
+
the core. Push back to the renderer interface.
|
|
154
|
+
|
|
155
|
+
## When NOT to write a renderer
|
|
156
|
+
|
|
157
|
+
- **"I want to send drift to my dashboard."** Don't write a renderer for
|
|
158
|
+
that. Your dashboard should poll the snapshot/drift endpoints (or
|
|
159
|
+
parse the JSON envelope on its own). Renderers are for tools that
|
|
160
|
+
consume the _output_, not the _event stream_.
|
|
161
|
+
- **"I want to gate CI differently."** That's a policy concern, not a
|
|
162
|
+
rendering concern. See `policy.ts` and `evaluatePolicy`.
|
|
163
|
+
- **"I want field-level severity (P0/P1/P2)."** Severity classification
|
|
164
|
+
is a policy decision that the renderer applies. A SARIF renderer can
|
|
165
|
+
map every drift category to a SARIF level; that mapping lives in the
|
|
166
|
+
renderer, not in the core types.
|
|
167
|
+
|
|
168
|
+
## A larger example: Slack Block Kit
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// src/renderers/slack.ts
|
|
172
|
+
import type { DriftReport } from '../diff';
|
|
173
|
+
import type { DriftDelta } from '../diff-vs-base';
|
|
174
|
+
import { countDriftReport } from '../diff';
|
|
175
|
+
import type { Renderer } from './types';
|
|
176
|
+
|
|
177
|
+
export const slackRenderer: Renderer = {
|
|
178
|
+
name: 'slack',
|
|
179
|
+
description: 'Slack Block Kit JSON. Post directly to a webhook.',
|
|
180
|
+
renderReport(report) {
|
|
181
|
+
const counts = countDriftReport(report);
|
|
182
|
+
return JSON.stringify({
|
|
183
|
+
blocks: [
|
|
184
|
+
{
|
|
185
|
+
type: 'header',
|
|
186
|
+
text: { type: 'plain_text', text: `${counts.total} drift findings` },
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
type: 'section',
|
|
190
|
+
text: {
|
|
191
|
+
type: 'mrkdwn',
|
|
192
|
+
text:
|
|
193
|
+
report.events.observedButUndocumented.length > 0
|
|
194
|
+
? `*Undocumented:* ${report.events.observedButUndocumented.map((n) => `\`${n}\``).join(', ')}`
|
|
195
|
+
: '_No new events to document._',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
renderDelta(delta) {
|
|
202
|
+
/* ... */
|
|
203
|
+
return JSON.stringify({ blocks: [] });
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Register, test, ship. Now `--format slack` produces JSON you can `curl`
|
|
209
|
+
straight to a Slack webhook URL.
|
|
210
|
+
|
|
211
|
+
## Library-mode renderers (out-of-tree)
|
|
212
|
+
|
|
213
|
+
If you don't want to upstream your renderer, the library API lets you
|
|
214
|
+
plug one in at runtime in your own code:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import {
|
|
218
|
+
diffCatalogAgainstSnapshot,
|
|
219
|
+
readCatalogState,
|
|
220
|
+
loadSnapshot,
|
|
221
|
+
} from 'autotel-eventcatalog';
|
|
222
|
+
import { myRenderer } from './my-renderer';
|
|
223
|
+
|
|
224
|
+
const snapshot = await loadSnapshot('./snapshot.json');
|
|
225
|
+
const catalog = await readCatalogState('./catalog');
|
|
226
|
+
const report = diffCatalogAgainstSnapshot(snapshot, catalog);
|
|
227
|
+
|
|
228
|
+
console.log(myRenderer.renderReport(report));
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
You don't have to modify the package to use your own renderer. Upstreaming
|
|
232
|
+
is for when the renderer has general value to other users.
|
|
233
|
+
|
|
234
|
+
## What's NOT extendable (and why)
|
|
235
|
+
|
|
236
|
+
| Surface | Extension allowed? | Why |
|
|
237
|
+
| ------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
238
|
+
| Renderers | Yes (registry pattern) | Output targets vary; core data does not |
|
|
239
|
+
| New CLI commands | No (without a user) | See [CONTRIBUTING.md invariant #1](../CONTRIBUTING.md#1-no-new-top-level-commands-without-a-user) |
|
|
240
|
+
| New diff categories | No (without ecosystem buy-in) | Each category cascades through diff/delta/counts/renderers/schemas |
|
|
241
|
+
| Custom policies | Yes, but file an issue first | `evaluatePolicy` is small; an extension might earn its place but probably wants a new policy mode in core |
|
|
242
|
+
| Snapshot format | No | Owned by `autotel-subscribers`; this package only consumes |
|
|
243
|
+
| Stamp marker syntax | No | Backwards compatibility with previously-stamped catalogs |
|
|
244
|
+
|
|
245
|
+
If you find yourself wanting to extend something marked "no", the answer
|
|
246
|
+
is almost always: file an issue describing the use case, and we'll figure
|
|
247
|
+
out whether it belongs in this package, in `autotel-subscribers`, in a
|
|
248
|
+
new sister package, or in your own downstream code.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## Exit codes
|
|
4
|
+
|
|
5
|
+
The `drift` CLI exits with a documented set of codes:
|
|
6
|
+
|
|
7
|
+
| Code | Meaning | When |
|
|
8
|
+
| ----- | -------------- | ------------------------------------------------------------------ |
|
|
9
|
+
| `0` | Clean | No drift, OR drift exists but `--fail-on-drift` was not set |
|
|
10
|
+
| `1` | Drift detected | `--fail-on-drift` was set AND the policy decided this is a failure |
|
|
11
|
+
| `2` | Bad arguments | Missing required flag, unknown flag, invalid value |
|
|
12
|
+
| other | Hard error | The CLI itself crashed; surface the stderr |
|
|
13
|
+
|
|
14
|
+
The `stamp` CLI exits `0` on success, `2` on bad arguments. It does not
|
|
15
|
+
have a drift-style failure mode; it either writes or it doesn't.
|
|
16
|
+
|
|
17
|
+
**If you wrap the CLI in a script:** check for codes 0/1 as expected
|
|
18
|
+
behaviour and treat anything else as a hard error to surface. The
|
|
19
|
+
[bundled action](../action.yml) does exactly this.
|
|
20
|
+
|
|
21
|
+
## Common errors
|
|
22
|
+
|
|
23
|
+
### "Both --snapshot and --catalog are required."
|
|
24
|
+
|
|
25
|
+
You omitted one of the two required arguments. Either is fine to forget;
|
|
26
|
+
the message says which.
|
|
27
|
+
|
|
28
|
+
### "--policy new-only requires --base-snapshot."
|
|
29
|
+
|
|
30
|
+
`--policy new-only` compares your current snapshot against a baseline.
|
|
31
|
+
Without a baseline, there's no "new" to compute. Either:
|
|
32
|
+
|
|
33
|
+
- Drop `--policy new-only` (defaults to `all` without a baseline), or
|
|
34
|
+
- Add `--base-snapshot <path>` pointing to the baseline snapshot.
|
|
35
|
+
|
|
36
|
+
In the GitHub Action, the baseline is fetched automatically from the PR's
|
|
37
|
+
base branch; supply `base-ref: origin/${{ github.base_ref }}`.
|
|
38
|
+
|
|
39
|
+
### "Invalid --format value: foo. Available renderers: markdown, terminal, json."
|
|
40
|
+
|
|
41
|
+
`--format` accepts a renderer name registered in
|
|
42
|
+
`src/renderers/index.ts`. If you added a custom renderer, it needs to be
|
|
43
|
+
in `RENDERERS`. See [EXTENDING.md](EXTENDING.md).
|
|
44
|
+
|
|
45
|
+
### "autotel-eventcatalog did not produce a summary output."
|
|
46
|
+
|
|
47
|
+
The action's drift step expected `--summary-output` to write a file but
|
|
48
|
+
couldn't find it after the CLI exited. The CLI itself probably crashed
|
|
49
|
+
_before_ writing the summary. Check stderr in the job log; the error
|
|
50
|
+
is up there.
|
|
51
|
+
|
|
52
|
+
### "Not an autotel architecture snapshot (missing spec marker)"
|
|
53
|
+
|
|
54
|
+
The file you passed to `--snapshot` doesn't look like a snapshot. It
|
|
55
|
+
needs to:
|
|
56
|
+
|
|
57
|
+
- Be valid JSON
|
|
58
|
+
- Have a `spec` field starting with `autotel-architecture/`
|
|
59
|
+
|
|
60
|
+
If the file is empty or zero-bytes, the snapshot subscriber probably
|
|
61
|
+
didn't run. Verify your test suite (or whatever produces the snapshot)
|
|
62
|
+
finished and wrote to disk before the CLI ran.
|
|
63
|
+
|
|
64
|
+
### Drift CLI reports events I expect to be in the catalog
|
|
65
|
+
|
|
66
|
+
The catalog reader walks `<catalog>/**/index.mdx` looking for an `id:`
|
|
67
|
+
field in the frontmatter. If your event mdx files use a different
|
|
68
|
+
filename or don't have proper frontmatter, they're invisible.
|
|
69
|
+
|
|
70
|
+
Check:
|
|
71
|
+
|
|
72
|
+
1. The event mdx file is named `index.mdx` (not `event.mdx`, not
|
|
73
|
+
`OrderPlaced.mdx`).
|
|
74
|
+
2. The path matches `.../events/<X>/index.mdx`.
|
|
75
|
+
3. The frontmatter has an `id:` (matched case-insensitively, with dots
|
|
76
|
+
and underscores normalised).
|
|
77
|
+
|
|
78
|
+
### Drift CLI doesn't see field-path drift even though I know it exists
|
|
79
|
+
|
|
80
|
+
Field-path drift is only computed for events with a `schemaPath:` in
|
|
81
|
+
their frontmatter. If your event mdx doesn't declare a schema, the
|
|
82
|
+
field-path check is skipped (you'll only get existence checks).
|
|
83
|
+
|
|
84
|
+
To enable field-path drift:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
---
|
|
88
|
+
id: OrderPlaced
|
|
89
|
+
schemaPath: schema.json # ← relative to the event mdx
|
|
90
|
+
---
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The schema file needs to be a JSON Schema with `properties` keys and
|
|
94
|
+
(optionally) nested `items` for arrays.
|
|
95
|
+
|
|
96
|
+
### Stamp output looks weird / duplicated
|
|
97
|
+
|
|
98
|
+
Stamps are scoped to the markers:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
<!-- autotel:stamp-start -->
|
|
102
|
+
...
|
|
103
|
+
<!-- autotel:stamp-end -->
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
If you see duplicate stamp blocks, either:
|
|
107
|
+
|
|
108
|
+
- Someone manually copied the marker pair without realising they're
|
|
109
|
+
load-bearing, or
|
|
110
|
+
- A previous run was interrupted before completing.
|
|
111
|
+
|
|
112
|
+
Fix: delete the malformed marker pair manually, then re-run `stamp`. The
|
|
113
|
+
stamp command will detect the first `<!-- autotel:stamp-start -->` and
|
|
114
|
+
the first `<!-- autotel:stamp-end -->` and replace between them; manual
|
|
115
|
+
cleanup is only needed if those don't bracket the block correctly.
|
|
116
|
+
|
|
117
|
+
### `pnpm catalog:stamp` failed but my catalog wasn't updated
|
|
118
|
+
|
|
119
|
+
`stamp` is mostly read-only (it reads the snapshot, reads the catalog,
|
|
120
|
+
plans what to write, then writes). If it fails mid-flight, you may
|
|
121
|
+
have a few files updated and the rest not.
|
|
122
|
+
|
|
123
|
+
The stamp summary JSON (`--summary-output`) tells you exactly which files
|
|
124
|
+
were inserts vs. replaces vs. skipped. Compare against your git status to
|
|
125
|
+
see what landed.
|
|
126
|
+
|
|
127
|
+
### The action posted no comment on my PR
|
|
128
|
+
|
|
129
|
+
Three possibilities:
|
|
130
|
+
|
|
131
|
+
1. **The repository doesn't allow comments from Actions.** Check
|
|
132
|
+
`Settings > Actions > General > Workflow permissions`. Needs
|
|
133
|
+
"Read and write permissions" or at least the `pull-requests: write`
|
|
134
|
+
permission in the workflow file.
|
|
135
|
+
2. **The sticky-comment action failed.** With
|
|
136
|
+
`continue-on-comment-failure: true` (the default), this is a
|
|
137
|
+
warning, not a failure. Look for the warning in the job log.
|
|
138
|
+
3. **You're not running on a `pull_request` event.** The action only
|
|
139
|
+
comments on PRs. On push events, it'll still run and write the
|
|
140
|
+
summary file, but the comment step is skipped.
|
|
141
|
+
|
|
142
|
+
### "autotel-eventcatalog drift CLI failed with exit $STATUS"
|
|
143
|
+
|
|
144
|
+
Any non-0, non-1 exit code from the CLI surfaces as a hard error in the
|
|
145
|
+
action. Look for the CLI output above this line in the job log; the
|
|
146
|
+
CLI's stderr will explain.
|
|
147
|
+
|
|
148
|
+
## Debugging tips
|
|
149
|
+
|
|
150
|
+
### See exactly what the catalog reader found
|
|
151
|
+
|
|
152
|
+
Add this to your local script:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { readCatalogState } from 'autotel-eventcatalog';
|
|
156
|
+
|
|
157
|
+
const state = await readCatalogState('./catalog');
|
|
158
|
+
console.log('events:', [...state.events.keys()]);
|
|
159
|
+
console.log('services:', [...state.services.keys()]);
|
|
160
|
+
console.log('channels:', [...state.channels.keys()]);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
If your event isn't listed, the reader didn't pick it up. Check the
|
|
164
|
+
filename, the frontmatter, and (on Windows) make sure the path
|
|
165
|
+
contains valid `services/.../events/` segments.
|
|
166
|
+
|
|
167
|
+
### See what the snapshot looked like
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Pretty-print, restricted to the event you care about:
|
|
171
|
+
node -e "const s=JSON.parse(require('fs').readFileSync('snapshot.json','utf8'));console.log(JSON.stringify(s.events['order.placed'],null,2))"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Reproduce a CI failure locally
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Same flags the bundled action passes internally:
|
|
178
|
+
pnpm autotel-eventcatalog drift \
|
|
179
|
+
--snapshot ./services/test/snapshot.json \
|
|
180
|
+
--catalog ./catalog \
|
|
181
|
+
--policy all \
|
|
182
|
+
--output ./drift.md \
|
|
183
|
+
--summary-output ./drift-summary.json \
|
|
184
|
+
--fail-on-drift
|
|
185
|
+
echo "exit: $?"
|
|
186
|
+
cat drift-summary.json
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If this reproduces locally with the same exit code and the same
|
|
190
|
+
summary, you've isolated the issue from CI-specific state.
|
|
191
|
+
|
|
192
|
+
### Reset to a known clean state
|
|
193
|
+
|
|
194
|
+
If you suspect the working tree has gone weird (e.g. partial stamp
|
|
195
|
+
runs), the surest reset is:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
git restore apps/example-eventcatalog/catalog
|
|
199
|
+
git clean -fd apps/example-eventcatalog/catalog
|
|
200
|
+
pnpm services:snapshot
|
|
201
|
+
pnpm catalog:stamp
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
After that, `git status` should show the catalog unchanged. If it
|
|
205
|
+
doesn't, your snapshot has changed since the last commit; that's an
|
|
206
|
+
intended diff, not a tooling problem.
|
|
207
|
+
|
|
208
|
+
## Still stuck?
|
|
209
|
+
|
|
210
|
+
Open an issue with:
|
|
211
|
+
|
|
212
|
+
- The exact command you ran
|
|
213
|
+
- The stderr output (full)
|
|
214
|
+
- The exit code
|
|
215
|
+
- The `spec:` field from your snapshot, drift report, or stamp summary
|
|
216
|
+
- Your Node version and OS
|
|
217
|
+
|
|
218
|
+
Most issues turn out to be one of: missing frontmatter, missing
|
|
219
|
+
schemaPath, paths not normalised on Windows, or the action being asked
|
|
220
|
+
to comment without `pull-requests: write` permission.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Upgrading
|
|
2
|
+
|
|
3
|
+
The package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
4
|
+
for the **npm package version** and for the **JSON contract version**
|
|
5
|
+
(the `vX.Y.Z` segment in each schema's `$id`).
|
|
6
|
+
|
|
7
|
+
These two versions usually move together but are conceptually
|
|
8
|
+
independent. The npm version changes when _any_ user-facing thing
|
|
9
|
+
changes (CLI flags, library exports, action inputs). The contract
|
|
10
|
+
version changes only when one of the **three published JSON shapes**
|
|
11
|
+
changes.
|
|
12
|
+
|
|
13
|
+
## Quick reference
|
|
14
|
+
|
|
15
|
+
| Change | npm version | Contract version |
|
|
16
|
+
| ----------------------------------------------- | ----------- | -------------------------------- |
|
|
17
|
+
| Bug fix, internal refactor | patch | unchanged |
|
|
18
|
+
| New optional CLI flag, new library export | minor | unchanged |
|
|
19
|
+
| New optional field in a JSON output | minor | minor |
|
|
20
|
+
| Renamed / removed JSON field, changed JSON type | major | major |
|
|
21
|
+
| Removed CLI flag, removed library export | major | unchanged (if JSON shape stable) |
|
|
22
|
+
| Changed CLI exit code semantics | major | unchanged |
|
|
23
|
+
|
|
24
|
+
## Reading the version
|
|
25
|
+
|
|
26
|
+
Three places to look:
|
|
27
|
+
|
|
28
|
+
1. **`package.json`**: the npm version. `pnpm list autotel-eventcatalog`
|
|
29
|
+
from your project root.
|
|
30
|
+
2. **The `spec:` field on every JSON output**: the contract version.
|
|
31
|
+
3. **The schema URLs** in `schemas/`: the contract version, with
|
|
32
|
+
`https://autotel.dev/schemas/...` so downstream tooling can validate
|
|
33
|
+
without depending on the npm package.
|
|
34
|
+
|
|
35
|
+
## Patch upgrade (e.g. `0.1.0 → 0.1.1`)
|
|
36
|
+
|
|
37
|
+
Bug fixes, internal refactors, dependency updates.
|
|
38
|
+
|
|
39
|
+
**What you do:** `pnpm update autotel-eventcatalog`. Nothing else.
|
|
40
|
+
|
|
41
|
+
**What might happen:** Behaviour changes that you weren't relying on
|
|
42
|
+
(e.g. an error message wording, the order of items in stderr logs).
|
|
43
|
+
Real public-API behaviour is preserved.
|
|
44
|
+
|
|
45
|
+
## Minor upgrade (e.g. `0.1.0 → 0.2.0`)
|
|
46
|
+
|
|
47
|
+
New optional CLI flags, new optional fields in JSON outputs, new
|
|
48
|
+
renderers in the registry, new exported types or functions.
|
|
49
|
+
|
|
50
|
+
**What you do:** `pnpm update autotel-eventcatalog`.
|
|
51
|
+
|
|
52
|
+
**What might happen:**
|
|
53
|
+
|
|
54
|
+
- A new field shows up in the JSON output. Existing parsers that ignore
|
|
55
|
+
unknown fields keep working. Parsers that explicitly reject unknown
|
|
56
|
+
fields (some strict schema validators) need to bump the schema they
|
|
57
|
+
validate against.
|
|
58
|
+
- A new renderer becomes available via `--format`. Existing `--format
|
|
59
|
+
markdown` / `--format json` calls keep working.
|
|
60
|
+
- A new library export exists. Existing imports keep working.
|
|
61
|
+
|
|
62
|
+
**Validating after a minor JSON upgrade**
|
|
63
|
+
|
|
64
|
+
If you're using a strict validator (`additionalProperties: false` in
|
|
65
|
+
your own schema, for example), you'll want to point it at the new
|
|
66
|
+
schema file:
|
|
67
|
+
|
|
68
|
+
```diff
|
|
69
|
+
- import schema from 'autotel-eventcatalog/schemas/drift-summary-v0.1.0.json';
|
|
70
|
+
+ import schema from 'autotel-eventcatalog/schemas/drift-summary-v0.2.0.json';
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If you're using `oneOf` on the `spec:` field, add the new value:
|
|
74
|
+
|
|
75
|
+
```diff
|
|
76
|
+
if (![
|
|
77
|
+
'autotel-eventcatalog-drift-summary/v0.1.0',
|
|
78
|
+
+ 'autotel-eventcatalog-drift-summary/v0.2.0',
|
|
79
|
+
].includes(summary.spec)) {
|
|
80
|
+
throw new Error(`Unknown drift summary version: ${summary.spec}`);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Major upgrade (e.g. `0.x → 1.0.0` or `1.x → 2.0.0`)
|
|
85
|
+
|
|
86
|
+
Breaking changes: renamed JSON fields, removed CLI flags, changed
|
|
87
|
+
behaviour.
|
|
88
|
+
|
|
89
|
+
**What you do:** read the CHANGELOG, update your code or workflow, then
|
|
90
|
+
`pnpm update autotel-eventcatalog`.
|
|
91
|
+
|
|
92
|
+
Major upgrades within `0.x` (e.g. `0.1.0 → 0.2.0` if it ever ships as a
|
|
93
|
+
breaking change) are signalled by the **JSON contract spec version**,
|
|
94
|
+
not just by the npm version. The package version may bump in lockstep,
|
|
95
|
+
but the contract version is what your downstream consumers need to know
|
|
96
|
+
about.
|
|
97
|
+
|
|
98
|
+
**A worked example.** Imagine v1.0.0 renames `counts.total` to
|
|
99
|
+
`counts.findings` in the drift summary. Steps:
|
|
100
|
+
|
|
101
|
+
1. **Detect the mismatch.** Your consumer's `spec:` check catches it:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
if (summary.spec !== 'autotel-eventcatalog-drift-summary/v0.1.0') {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Drift summary spec ${summary.spec} is not v0.1.0; refusing to parse.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The throw is the signal to read the upgrade notes.
|
|
112
|
+
|
|
113
|
+
2. **Read the CHANGELOG** for the new version.
|
|
114
|
+
|
|
115
|
+
3. **Update your consumer code:**
|
|
116
|
+
|
|
117
|
+
```diff
|
|
118
|
+
- if (summary.counts.total > 0) { ... }
|
|
119
|
+
+ if (summary.counts.findings > 0) { ... }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
4. **Update the spec check** to allow the new version.
|
|
123
|
+
|
|
124
|
+
5. **Update the schema file you validate against**, if any.
|
|
125
|
+
|
|
126
|
+
6. **Bump the npm package** in your `package.json`.
|
|
127
|
+
|
|
128
|
+
## Detecting an unexpected version
|
|
129
|
+
|
|
130
|
+
The `spec:` field is your defence against silent shape changes. Every
|
|
131
|
+
JSON envelope this package produces carries it. Downstream tooling
|
|
132
|
+
should always check it before parsing.
|
|
133
|
+
|
|
134
|
+
Pattern: explicit allowlist of accepted spec values, with a clear
|
|
135
|
+
error message when something unexpected lands.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const ACCEPTED_SPECS = new Set([
|
|
139
|
+
'autotel-eventcatalog-drift-summary/v0.1.0',
|
|
140
|
+
// Add new minors here as you update; reject anything else.
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
function parseSummary(json: string): DriftSummary {
|
|
144
|
+
const parsed = JSON.parse(json);
|
|
145
|
+
if (!ACCEPTED_SPECS.has(parsed.spec)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Drift summary spec "${parsed.spec}" is not in the accepted set. ` +
|
|
148
|
+
`Read the autotel-eventcatalog CHANGELOG and update this consumer.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return parsed as DriftSummary;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This is intentionally noisy. A silent fallback to "best effort parse"
|
|
156
|
+
is exactly how downstream consumers lose track of contracts.
|
|
157
|
+
|
|
158
|
+
## Renderer compatibility
|
|
159
|
+
|
|
160
|
+
The CLI's `--format` flag accepts any name registered in the renderer
|
|
161
|
+
registry. Adding a new renderer is a **minor** bump (it doesn't break
|
|
162
|
+
existing callers). Removing a renderer would be a **major** bump
|
|
163
|
+
(callers using `--format <removed>` would fail).
|
|
164
|
+
|
|
165
|
+
The built-in renderers (`markdown`, `terminal`, `json`) are intended to
|
|
166
|
+
stay forever. If a future major version retires one of them, the
|
|
167
|
+
CHANGELOG will name the migration target.
|
|
168
|
+
|
|
169
|
+
## Action upgrade
|
|
170
|
+
|
|
171
|
+
The GitHub Action uses semver via the `package-version` input. Default
|
|
172
|
+
is `^0`, major-version-pinned to v0.
|
|
173
|
+
|
|
174
|
+
When v1.0.0 ships, your workflow keeps running on the latest v0 release
|
|
175
|
+
until you explicitly opt in:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
- uses: jagreehal/autotel-eventcatalog@v0 # always latest v0
|
|
179
|
+
- uses: jagreehal/autotel-eventcatalog@v1 # opt into v1
|
|
180
|
+
- uses: jagreehal/autotel-eventcatalog@v0.1.0 # pin exactly
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Inside the action itself, the `package-version` input controls which
|
|
184
|
+
npm release runs:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
- uses: jagreehal/autotel-eventcatalog@v0
|
|
188
|
+
with:
|
|
189
|
+
package-version: '0.1.0' # exact pin
|
|
190
|
+
# OR
|
|
191
|
+
package-version: '^0.1' # minor-range
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## When in doubt
|
|
195
|
+
|
|
196
|
+
- The CHANGELOG is the source of truth for what changed.
|
|
197
|
+
- The schemas in `schemas/` are the source of truth for the JSON shape.
|
|
198
|
+
- The `spec:` field is the source of truth for which schema a given
|
|
199
|
+
output matches.
|
|
200
|
+
|
|
201
|
+
If something looks like an undocumented breaking change, file an
|
|
202
|
+
issue; that's a bug in the release process, not in your code.
|