Skip to content

feat(api-reference): auto-generated API changelog pages for v1 and v2 - #2796

Open
gaprl wants to merge 12 commits into
mainfrom
api-changelog-from-openapi
Open

feat(api-reference): auto-generated API changelog pages for v1 and v2#2796
gaprl wants to merge 12 commits into
mainfrom
api-changelog-from-openapi

Conversation

@gaprl

@gaprl gaprl commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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/Changelog and /api-reference/v2/Changelog
  • One entry per day the API changed, newest first, backfilled to May 2026
  • Changes grouped into Breaking / Potentially breaking / Other, each linking to its endpoint's reference page
  • Subscribable by RSS, with instructions on the page

I've modified the feed such that updates to oasdiff or to the changelog generation script won't trigger RSS feed updates per se.

Screenshot 2026-09-03 at 11 31 06 AM

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:

filters/allOf[#/components/schemas/protos.projects.v1.RepoFilters]/latestScanStatus

we render the field a customer actually sends:

filters.latestScanStatus

Worth a look

  • oasdiff is new in CI, pinned to v1.28.0 with a checksum. It runs locally against our own public specs; nothing leaves the runner.
  • v1's July 14 entry is large. That's the day docs switched to auto-generated specs, so the spec emitter changed, not the API. Easy to trim if you'd rather.

Test plan

  • 132 unit tests pass; mintlify validate passes; every emitted endpoint link verified 200 against a local mintlify dev
  • After merge: confirm the feed emits one item per day. mintlify dev serves no RSS, so this can't be checked locally.

Part of APPEX-969; implements the generator scoped in APPEX-960.

@mintlify

mintlify Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
semgrep-docs 🟢 Ready View Preview Sep 10, 2026, 9:34 PM

@gaprl

gaprl commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

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).

@gaprl gaprl closed this Aug 18, 2026
@gaprl gaprl reopened this Sep 2, 2026
gaprl and others added 3 commits September 2, 2026 15:36
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>
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>
@aaronmichaelacosta

Copy link
Copy Markdown
Contributor

Reopening this, as we got feedback from stakeholders that the changelog + an RSS feed would be important for customers to stay up-to-date.

@aaronmichaelacosta
aaronmichaelacosta marked this pull request as ready for review September 3, 2026 17:47
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>

aaronmichaelacosta commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

@gaprl gaprl left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

@gaprl

gaprl commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

I'd check the live RSS feed in the mintlify preview to verify if it's rendering all the changelog entries correctly.

@aaronmichaelacosta aaronmichaelacosta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving on behalf of @gaprl (since he cannot approve a PR he opened, and I took it over).

@abhijna abhijna left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the preview and it looks good. Ty!

aaronmichaelacosta and others added 5 commits September 8, 2026 10:25
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 `&#123;`
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants