Skip to content
Draft
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
119 changes: 119 additions & 0 deletions .github/workflows/watch-mb-cloudevent.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Watch M&B cloudevent fields

# Detects drift between the Metering & Billing plugin's emitted CloudEvent fields
# (declared as LuaLS ---@class/---@field annotations in kong-ee cloudevent.lua) and
# the docs. On change, opens a PR bumping the committed catalog snapshot and pings
# the docs maintainers to review the event-dimensions reference.
#
# DRAFT — depends on the upstream kong-ee annotations PR landing first.

on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
# Optional: kong-ee could fire this on a change to cloudevent.lua for near-instant alerts.
repository_dispatch:
types: [MB_CLOUDEVENT_UPDATED]

permissions:
contents: read

env:
# Pin a real lua-language-server release; bump deliberately.
LUALS_VERSION: "3.13.6"
KONG_EE_REF: "master"
PLUGIN_DIR: "kong/plugins/metering-and-billing"

jobs:
watch-mb-cloudevent:
name: Watch M&B cloudevent fields
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit

- name: Create GitHub App Token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
id: app-token
with:
app-id: ${{ vars.GH_APP_KONG_DOCS_ID }}
private-key: ${{ secrets.GH_APP_KONG_DOCS_SECRET }}
owner: Kong

- name: Checkout developer.konghq.com
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
token: ${{ steps.app-token.outputs.token }}

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: tools/mb-event-dimensions/package-lock.json

- name: Install mb-event-dimensions dependencies
run: |
cd tools/mb-event-dimensions
npm ci

- name: Checkout kong-ee repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: 'Kong/kong-ee'
token: ${{ steps.app-token.outputs.token }}
path: 'kong-ee'
ref: ${{ env.KONG_EE_REF }}

- name: Install lua-language-server
run: |
curl -fsSL "https://github.com/LuaLS/lua-language-server/releases/download/${LUALS_VERSION}/lua-language-server-${LUALS_VERSION}-linux-x64.tar.gz" \
| sudo tar -xz -C /usr/local/bin --wildcards 'bin/lua-language-server' --strip-components=1
lua-language-server --version

- name: Export cloudevent annotations to doc.json
run: |
mkdir -p luals-ws luals-out
cp "kong-ee/${PLUGIN_DIR}/cloudevent.lua" luals-ws/cloudevent.lua
printf '%s\n' '{ "runtime.version": "LuaJIT", "diagnostics.globals": ["kong","ngx"] }' > luals-ws/.luarc.json
lua-language-server --doc luals-ws --doc_out_path luals-out

- name: Check for drift (vs snapshot and rendered data file)
id: drift
run: |
cd tools/mb-event-dimensions
if node check-drift.js ../../luals-out/doc.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
# regenerate the committed snapshot so the PR diff shows what moved
node extract-catalog.js ../../luals-out/doc.json --out event-fields.snapshot.json
fi

- name: Create pull request
if: steps.drift.outputs.changed == 'true'
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
with:
token: ${{ steps.app-token.outputs.token }}
branch: watch-mb-cloudevent
title: "Metering & Billing emitted event fields changed"
commit-message: "chore: sync M&B cloudevent field snapshot"
labels: skip-changelog,review:general
add-paths: tools/mb-event-dimensions/event-fields.snapshot.json
body: |
The Metering & Billing plugin's emitted CloudEvent fields changed upstream
(kong-ee `cloudevent.lua`). The committed field snapshot has been updated to match.

**Action:** review `app/_kong_plugins/metering-and-billing/index.md`
(Captured event dimensions) and `app/_data/plugins/metering-and-billing.yaml`,
and update them for the added/removed fields shown in the CI log.

- name: Notify Slack
if: ${{ steps.drift.outputs.changed == 'true' && !cancelled() }}
uses: ./.github/reusable-steps/notify-slack
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
status: ${{ job.status }}
87 changes: 87 additions & 0 deletions tools/mb-event-dimensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# mb-event-dimensions

Extracts the Metering & Billing plugin's emitted CloudEvent field catalog from the
LuaLS annotations in kong-ee `cloudevent.lua`, and detects drift between those fields
and the docs (the `Captured event dimensions` reference on the plugin page).

The fields are declared as `---@class` / `---@field` annotations in
`kong/plugins/metering-and-billing/cloudevent.lua`; `lua-language-server --doc` exports
them to a `doc.json`, which these scripts consume.

## How it works

Requires `Kong/kong-ee` available locally and `lua-language-server` on `PATH`. From the
root of your clone of the dev site repo:

```bash
cd tools/mb-event-dimensions
npm ci
```

Export the annotations to `doc.json` (point `<kong-ee>` at your kong-ee clone):

```bash
mkdir -p /tmp/mb-ws /tmp/mb-out
cp <kong-ee>/kong/plugins/metering-and-billing/cloudevent.lua /tmp/mb-ws/
echo '{ "runtime.version": "LuaJIT", "diagnostics.globals": ["kong","ngx"] }' > /tmp/mb-ws/.luarc.json
lua-language-server --doc /tmp/mb-ws --doc_out_path /tmp/mb-out
```

## How to run it

Extract the catalog (`{ "kong.api_request": [...], "kong.llm_request": [...] }`):

```bash
node extract-catalog.js /tmp/mb-out/doc.json # prints JSON
node extract-catalog.js /tmp/mb-out/doc.json --out event-fields.snapshot.json
```

Check for drift against the committed snapshot and the rendered data file
(`app/_data/plugins/metering-and-billing.yaml`):

```bash
node check-drift.js /tmp/mb-out/doc.json
```

Exit `0` when everything agrees; exit `1` with a printed diff (added/removed fields) when
the plugin's emitted fields no longer match the snapshot or the docs.

`event-fields.snapshot.json` is the committed baseline. The scheduled workflow
`.github/workflows/watch-mb-cloudevent.yml` runs this check daily; on drift it bumps the
snapshot, opens a PR, and pings the docs maintainers to update the reference.

## Example drift output

New field added (`cache_region`):

```
DRIFT vs snapshot [kong.api_request]:
+ cache_region: string? (new in cloudevent.lua)
DOC MISMATCH [kong.api_request] (data file vs cloudevent.lua):
emitted but undocumented: cache_region

2 problem(s) found — the docs reference needs updating.
```

Field removed (`upstream_status`):

```
DRIFT vs snapshot [kong.api_request]:
- upstream_status: integer? (gone from cloudevent.lua)
DOC MISMATCH [kong.api_request] (data file vs cloudevent.lua):
data-file only (not emitted): upstream_status

2 problem(s) found — the docs reference needs updating.
```

Field type changed (`service_port` `integer?` → `string?`):

```
DRIFT vs snapshot [kong.api_request]:
~ service_port: integer? → string? (type changed)

1 problem(s) found — the docs reference needs updating.
```

Add/remove trip both checks (the field-name set changes); a pure type change trips only the
snapshot check (the data-file cross-check is name-only). All exit `1`.
79 changes: 79 additions & 0 deletions tools/mb-event-dimensions/check-drift.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Detect drift in the Metering & Billing emitted-event fields.
//
// node check-drift.js <doc.json>
//
// Compares the field catalog freshly extracted from the kong-ee LuaLS export
// against two things:
// 1. the committed baseline snapshot (event-fields.snapshot.json) — the
// "known good" the docs were last reconciled with; drift here is what the
// scheduled workflow turns into an alert PR.
// 2. the rendered reference data file (app/_data/plugins/metering-and-billing.yaml)
// — so the published tables can't silently disagree with the plugin.
//
// Exits non-zero (with a printed diff) on any drift; exit 0 when everything agrees.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import yaml from 'js-yaml';
import { extractCatalog } from './extract-catalog.js';

const HERE = path.dirname(fileURLToPath(import.meta.url));
const SNAPSHOT = path.join(HERE, 'event-fields.snapshot.json');
const DATA_FILE = path.resolve(HERE, '../../app/_data/plugins/metering-and-billing.yaml');

const nameSet = (fields) => new Set(fields.map((f) => f.name));
const typeMap = (fields) => new Map(fields.map((f) => [f.name, f.type]));
const diff = (a, b) => ({ added: [...b].filter((x) => !a.has(x)), removed: [...a].filter((x) => !b.has(x)) });

const docPath = process.argv[2];

if (!docPath) {
console.error('usage: node check-drift.js <doc.json>');
process.exit(2);
}

const current = extractCatalog(docPath);
let problems = 0;

// 1) drift vs committed snapshot (added / removed / type-changed, keyed by field name)
const snapshot = JSON.parse(fs.readFileSync(SNAPSHOT, 'utf8'));

for (const event of Object.keys(current)) {
const cur = typeMap(current[event]);
const snap = typeMap(snapshot[event] || []);
const added = [...cur.keys()].filter((n) => !snap.has(n));
const removed = [...snap.keys()].filter((n) => !cur.has(n));
const changed = [...cur.keys()].filter((n) => snap.has(n) && snap.get(n) !== cur.get(n));

if (added.length || removed.length || changed.length) {
problems++;
console.error(`DRIFT vs snapshot [${event}]:`);
added.forEach((n) => console.error(` + ${n}: ${cur.get(n)} (new in cloudevent.lua)`));
removed.forEach((n) => console.error(` - ${n}: ${snap.get(n)} (gone from cloudevent.lua)`));
changed.forEach((n) => console.error(` ~ ${n}: ${snap.get(n)} → ${cur.get(n)} (type changed)`));
}
}

// 2) rendered data file must match the code catalog (field names)
const data = yaml.load(fs.readFileSync(DATA_FILE, 'utf8'));
const portal = (data.portal_fields || []).map((f) => f.field);

for (const ev of data.events || []) {
const shipped = new Set([...(ev.fields || []).map((f) => f.field), ...portal]);
const code = nameSet(current[ev.type] || []);
const { added, removed } = diff(code, shipped);

if (added.length || removed.length) {
problems++;
console.error(`DOC MISMATCH [${ev.type}] (data file vs cloudevent.lua):`);
added.forEach((x) => console.error(` data-file only (not emitted): ${x}`));
removed.forEach((x) => console.error(` emitted but undocumented: ${x}`));
}
}

if (problems) {
console.error(`\n${problems} problem(s) found — the docs reference needs updating.`);
process.exit(1);
}

console.log('OK: extracted catalog matches the snapshot and the rendered data file.');
Loading
Loading