feat(api-reference): auto-generated API changelog pages for v1 and v2 - #2796
feat(api-reference): auto-generated API changelog pages for v1 and v2#2796gaprl wants to merge 12 commits into
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
Closing after team discussion — we decided an auto-generated API changelog page isn't the right investment right now. The branch is kept for reference: the generator (scripts/generate_api_changelog.py), its test suite, and the verified Mintlify URL-slug rules are reusable if we revisit (see APPEX-960 / APPEX-969). |
Add a per-version API changelog (api-reference/v1/Changelog, api-reference/v2/Changelog) generated from the OpenAPI specs' git history with oasdiff. One <Update> block per day the spec changed, rendered as Stripe-style tables: a colored change-verb chip, the oasdiff description, and the endpoint as a method Badge linking to its generated reference page. Repetitive changes (enum values, format flips) are coalesced into single rows, and rss: true gives subscribers a feed per API version. The page fully regenerates on every run, so the backfill is retroactive (v1 to 2026-05-28, v2 to 2026-07-14) and self-correcting. Endpoint links are built only from the current spec, so removed endpoints render unlinked instead of 404ing. The update-openapi-specs cron now installs a checksum-pinned oasdiff, regenerates both pages after the mirror and sort steps, and ships them in the same signed-commit PR as the spec change.
Replace the split method-badge + underlined path link with a single Badge pill containing method and path, wrapped in a raw anchor (a markdown link would keep Mintlify's bottom-border underline). <wbr/> elements after each path segment let long paths wrap inside the pill instead of overflowing the table, without adding characters that survive copy-paste; path braces are entity-escaped for JSX. Tables gain an api-changelog-table wrapper div styled in styles.css: Mintlify puts min-width: 150px on every td, which held the Change column open well past its chip.
Rewrite oasdiff's JSON Schema property paths into the field reference a consumer actually writes, and stop filing routine enum additions under a breaking-change heading. - Drop `allOf[#/components/schemas/protos.*]` segments. These specs are protobuf-generated and wrap every described $ref in a single-element allOf (the OpenAPI 3.0 workaround for $ref siblings being ignored), so the segment is composition bookkeeping that the wire payload has no level for. Removing it is more accurate as well as shorter, and it was the only place an internal proto package name reached the page. - Fold an `items` segment into its parent as `[]` and join with `.`, so `automations/items/filters/allOf[...]/conditions/items/type` renders as `automations[].filters.conditions[].type`. - Demote `response-property-enum-value-added` to INFO. oasdiff rates it WARN on the theory that a consumer may switch exhaustively over the enum, but these enums come from protobuf, where they are open by construction; 46 routine additions were burying the actual removals. Field-name case is deliberately untouched: the specs emit both `deployment_id` and `deploymentId` depending on the service, and the changelog has to name the key the endpoint actually accepts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
48a7336 to
82fbf0c
Compare
The pages already carried `rss: true`, but Mintlify would have produced a
poor feed from them:
- It emits one entry per Markdown heading inside an `<Update>`, so our
severity sections turned 3 dated entries into 6 for v1 and 10 into 12
for v2 -- titled "Changes" (9x), "Breaking changes", "Potentially
breaking changes", undated and repeated across every day.
- Feed entries "contain pure Markdown only ... exclude components, code,
and HTML elements", and our rows are a JSX table of Badges and links, so
subscribers would have received near-empty items.
Emit an explicit `rss={{ title, description }}` per `<Update>`, which is
what Mintlify recommends for updates containing excluded content. That
pins the feed to one dated item per day with a prose summary, and
decouples it from the heading structure -- otherwise a re-level that adds
or drops a section heading counts as "modifying headings inside an
existing Update" and republishes the entry to subscribers.
Also add `--rss-url`, which renders subscribe instructions on the page.
Mintlify only emits a `<link rel="alternate">` autodiscovery tag for
`rss: true` pages and no visible affordance, so the page has to say it.
Fixes a pre-existing summary bug along the way: the noun was attached only
via the "other" bucket, so a breaking-only day read "1 breaking" instead
of "1 breaking change". The count logic is now shared between the on-page
summary and the RSS description so the two cannot drift.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The caveat duplicates what the v2 Introduction page already states, and on a page whose whole purpose is to label breaking changes individually it read as a blanket disclaimer over entries that are mostly additive. The generator keeps its --note flag; the v2 invocation just stops passing one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Reopening this, as we got feedback from stakeholders that the changelog + an RSS feed would be important for customers to stay up-to-date. |
Mintlify "publishes entries when you add new Update components and when you modify headings inside of existing Update components". Our severity sections were Markdown headings, and which of them a given day has depends on the level mapping -- so demoting a check (LEVEL_OVERRIDES) or bumping oasdiff removes or adds a heading inside months-old entries and re-notifies every subscriber about them. Today's enum demotion would have done exactly that. Render the section labels in bold instead. With no headings inside an <Update>, the only remaining publish trigger is adding a new one. The rss= prop already supplies the feed title, so nothing depended on these being headings, and their anchors were colliding anyway -- every day emitted the same #changes. Verified on the PR preview deployment beforehand: an edit outside the Update blocks left all 10 v2 items' guid and pubDate untouched, confirming both are per-block rather than per-file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This stack of pull requests is managed by Graphite. Learn more about stacking. |
gaprl
left a comment
There was a problem hiding this comment.
@aaronmichaelacosta do you think we could surface the stability badge on each changelog row? so the customer knows a "removed" was for an experimental endpoint, for example
|
I'd check the live RSS feed in the mintlify preview to verify if it's rendering all the changelog entries correctly. |
aaronmichaelacosta
left a comment
There was a problem hiding this comment.
Approving on behalf of @gaprl (since he cannot approve a PR he opened, and I took it over).
abhijna
left a comment
There was a problem hiding this comment.
I reviewed the preview and it looks good. Ty!
Regrouping the public API into its new services re-tags roughly 200 endpoints without touching a path, a request, or a response. oasdiff reports each one twice -- `api-tag-added` and `api-tag-removed` -- so the reorganisation would land as ~400 rows on a single date. Measured, not guessed: re-tagging the 6 MiscService operations produced exactly 12 changes. A day of nothing but re-tagging would therefore create a new <Update>, and a new <Update> is the one thing that still publishes to RSS subscribers. The feed's largest entry ever would be an internal filing change, and any real change shipped the same day would be buried under it. Drop both checks. A tag decides which nav group an endpoint renders under; oasdiff itself rates both INFO. Not entirely free -- some SDK generators namespace by tag, so a consumer generating a client from the published spec could see a method move class -- but it is not a change to the HTTP contract, and that caveat is recorded next to the exclusion. Real changes shipped alongside a re-tag still appear; only the tag rows are dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ilter
Selecting "Breaking" in the side panel did nothing on either changelog
page: the chip lit up and the URL gained ?tags=Breaking, but all 10 v2
(and all 3 v1) updates stayed on screen.
The cause is Mintlify's <Update> visibility predicate, which reduces to
"hide me only if I have tags and none of them is selected":
if (!filters.length) return true;
const active = filters.filter(f => f.active);
return !(active.length !== 0 && tags?.length)
|| tags.some(t => active.some(f => f.tag === t));
For an <Update> with no tags= at all, `tags` is undefined, `tags?.length`
is falsy, and the first clause short-circuits to true -- it is visible no
matter what is selected. We tagged only the days containing a level-3
change, so the other days had no tags to fail to match and nothing could
ever be hidden. Confirmed with a two-<Update> scratch page: one tagged +
one untagged filters nothing, while tagging both hides the non-match.
So give every <Update> a tag naming the day's highest severity --
Breaking, Potentially breaking, or Non-breaking. The chips are OR'd and
individually toggleable, so "show me anything that might break me" is
Breaking + Potentially breaking, and each severity is now also visible at
a glance under its date.
This rules out the structural suspects, incidentally: the scratch repro
had no table, no <div className="api-changelog-table">, no rss= prop and
no headings, and still failed -- so none of those, and not d4157be's
switch from headings to bold, had anything to do with it.
The regenerated pages change only <Update> opening lines, adding an
attribute. That is neither a new <Update> nor a heading edit inside an
existing one, so it does not trip the feed republish triggers d4157be
documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two presentation fixes for rows that span several endpoints: - The endpoint cell stacks its pills with `<br/>`, which gave them no leading and read as one solid block. Adds a little vertical rhythm, scoped to the generated table so no badge elsewhere is affected. - The "and N more" hovercard lists full endpoint paths, and a path is one unbreakable token. Mintlify caps the popup at 16rem, so the text ran ~190px past the rounded background instead of wrapping inside it. Allows a break mid-path and widens the cap so a path takes two lines, not five. Verified at 1440px and 390px: no overflow, still bounded by the viewport. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
oasdiff reports per endpoint and per property, so one change to a shared type fanned out into a row per place it surfaced. Adding a single enum value to `TriggerSource` produced six near-identical rows -- the same sentence, once for each automations endpoint, split again across the request and response copies of the property. The reader had to diff six rows by eye to work out they were one change. Two folds, both lossless: - `merge_across_endpoints` folds rows whose wording is *identical* into one carrying an `endpoints` list. Only identical wording merges, so nothing is summarised away. Endpoint-scoped checks (`endpoint-*`, `api-*`) are exempt: two endpoints added on the same day both read "endpoint added", and folding those would hide the endpoints. - `merge_enum_additions` then folds the request and response copies of one property together and lists the values once, normalising the wrapper path to its leaf. The endpoint cell shows the first three and puts the rest behind an "and N more" hovercard, so a broad change stays a readable height without making any endpoint unreachable. RSS is untouched: the feed description is built from the raw, unmerged changes, so its endpoint count stays exact and nothing is truncated there. 193 rows to 112 across the two changelogs; the Aug 20 v2 entry goes from 10 rows to 4, and `TRIGGER_SOURCE_PATCH_READY` from 6 rows to 1. Both files regenerate byte-identical from the committed specs with oasdiff 1.28.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "and N more" hovercard ran its paths together with commas. Paths are
long enough to wrap inside a 28rem popup, so the separator landed mid-line
and there was no way to see where one path ended and the next began -- five
endpoints read as one run-on string.
One bullet per line instead. The tip is now passed as a JSX expression
rather than an attribute string so the newlines survive, with `json.dumps`
doing the escaping. That also drops the entity encoding of `{` and `}`:
they open an expression as bare JSX text, but are ordinary characters
inside a string literal, and encoding them showed the reader `{`
instead of `{`. `white-space: pre-line` renders the breaks while still
collapsing spaces and wrapping long paths, which `pre` would not.
Verified in the browser: bullets on separate lines, braces intact, no
overflow. Both changelogs regenerate byte-identical from the committed
specs with oasdiff 1.28.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
d413c13 to
e15c480
Compare
Two problems, both found by running the deprecation lifecycle through the pinned oasdiff (v1.28.0) rather than reasoning about it. **A deprecation announcement was filed as "Non-breaking."** oasdiff rates `endpoint-deprecated` INFO, which put it under "Changes" with that tag. The announcement is the entire advance warning a caller gets, and "Non-breaking" hides it from the one reader who most needs it: someone filtering the changelog for what is going to break them. Re-levelled to potentially breaking -- true on the day it ships, where "Breaking" would cry wolf. This is not hypothetical; three v1 policies endpoints are already deprecated, and that day moves from "6 changes / Non-breaking" to "3 potentially breaking · 3 other changes". **A removal done right produced no entry at all.** oasdiff reports a removal only when something went wrong with it -- gone with no deprecation, or gone before its published sunset date. Retire an endpoint exactly as the policy prescribes and `oasdiff changelog` says "No changes to report", so the day the endpoint stopped answering was the one day missing from the changelog. The changelog recorded only the removals we botched. There is no oasdiff flag for that case; the check does not exist upstream. So `sunset_removals` diffs the two snapshots' operation sets directly and synthesizes the entry, staying quiet whenever oasdiff already had something to say so a removal never lands twice. Both need `x-sunset` in the published spec to work, which protobuf-tools#202 now emits -- `x-deprecation` alone is invisible to oasdiff. Not doing the third thing considered here: `--deprecation-days-stable 180` would re-check every historical deprecation against *today*, so regenerating the page would retroactively flag long-served windows as too short. The notice windows are enforced at the proto level by protobuf-tools#203, which is the right layer -- it fails the change before it ships rather than narrating it afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

What this does
Semgrep's API changes most weeks, and until now nothing told customers what changed. This adds a changelog page for each API version, generated from the OpenAPI specs' own git history — so it stays current without anyone writing it by hand.
/api-reference/v1/Changelogand/api-reference/v2/ChangelogI've modified the feed such that updates to
oasdiffor to the changelog generation script won't trigger RSS feed updates per se.How it works
A script walks each spec's git history and compares consecutive days with oasdiff, which diffs specs semantically — reordered lines and description edits produce no entry, only real API changes do. The weekday cron that already syncs the specs now regenerates these pages in the same PR.
oasdiff's raw wording isn't customer-ready, so we rewrite it. Where it emits this:
we render the field a customer actually sends:
Worth a look
Test plan
mintlify validatepasses; every emitted endpoint link verified 200 against a localmintlify devmintlify devserves no RSS, so this can't be checked locally.Part of APPEX-969; implements the generator scoped in APPEX-960.