-
Notifications
You must be signed in to change notification settings - Fork 3
ci: breaking-change gate on the pg-pkg OpenAPI contract (oasdiff) #269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1593102
56ce480
ffc5bd4
f0edf44
8f26cf7
ce0fc59
44d6379
94e6ed5
e86c71b
6aedefd
4334f9a
3ff6d8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # pg-pkg/tests/api_gate.rs locates blocks in this spec with multi-line raw-string | ||
| # anchors. rustc normalises CRLF to LF inside raw strings, but the file read off | ||
| # disk keeps whatever git wrote, so on a Windows checkout (core.autocrlf=true is | ||
| # the Git for Windows default) every anchor misses and the suite goes red on a | ||
| # clean tree. Keeping the stored bytes is also what makes the oasdiff gate's | ||
| # verdict reproducible off a local checkout. | ||
| pg-pkg/api-description.yaml -text |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| name: API diff | ||
| # | ||
| # Breaking-change gate on the pg-pkg OpenAPI contract (#249). | ||
| # | ||
| # pg-pkg/api-description.yaml is the pinned v2 HTTP contract (#242), one of the | ||
| # three seams COMPATIBILITY.md guarantees. This job diffs the PR's spec against | ||
| # the branch the PR targets and fails on any change oasdiff rates WARN or ERR. | ||
| # | ||
| # Escape hatch, not a wall: pg-pkg's routes are versioned (unlike cryptify's, | ||
| # whose gate this mirrors), so a change /v2 cannot take additively goes under a | ||
| # new prefix (/v3/...) with /v2 left running, and /v2 is retired later through | ||
| # the deprecation process in COMPATIBILITY.md, once postguard-ops#64 telemetry | ||
| # shows nobody calls it. A /v3 route added next to /v2 reads as additive, so | ||
| # this gate passes it. Reach for that before reaching for err-ignore. | ||
| # | ||
| # Why WARN and not ERR | ||
| # -------------------- | ||
| # `fail-on: ERR` would leave four of the changes COMPATIBILITY.md forbids | ||
| # passing silently, because oasdiff rates them WARN, and on this spec that is | ||
| # most of the "no removing a route or a field" rule: only `status` is ever | ||
| # `required`, so `key` (the IBE user secret key the endpoint exists to return), | ||
| # `proofStatus`, `pubSignKey` and the rest are optional, and removing or | ||
| # renaming an optional response property is WARN. So is removing a request | ||
| # parameter. Measured on this spec against oasdiff v1.26.1: | ||
| # | ||
| # mutation fail-on ERR fail-on WARN | ||
| # optional response property removed (`key`) passes fails | ||
| # optional response property renamed passes fails | ||
| # required path parameter removed passes fails | ||
| # response enum value added passes fails | ||
| # | ||
| # The fourth is the rule COMPATIBILITY.md gains alongside this gate; see below. | ||
| # | ||
| # WARN adds 31 checks on top of ERR's 213. All but one are changes | ||
| # COMPATIBILITY.md already forbids (request-parameter-removed, | ||
| # request-property-removed, response-body-media-type-schema-removed, and the | ||
| # constraint-narrowing *-set family). The exception is | ||
| # response-property-enum-value-added: adding a value to SessionStatus or | ||
| # ProofStatus fails this gate. That is deliberate, and COMPATIBILITY.md now | ||
| # says so too. A client that switches on the enum without a default branch | ||
| # breaks on a value it has never seen, so a new status is a /v3 change (or an | ||
| # x-extensible-enum one), not an additive one. It is the one rule here that | ||
| # only WARN enforces, so it is also the first casualty of a revert to ERR; | ||
| # pg-pkg/tests/api_gate.rs pins it. | ||
| # | ||
| # Two more checks are opt-in: they rate ERR but only run when named, so they | ||
| # need the include-checks input below. Without it, changing a 401 to a 403 and | ||
| # dropping an enum value from a response both pass. Keep include-checks and | ||
| # fail-on in step with the local-repro command in CLAUDE.md, or a local run | ||
| # quietly disagrees with CI. | ||
| # | ||
| # Not covered: the gate compares documented paths, and the spec documents the | ||
| # canonical /v2/request/... paths only (see its "Path prefix aliases" note), so | ||
| # dropping the /v2/irma/... alias handlers before #257's deprecation has run | ||
| # passes this gate. That one stays a review rule. | ||
| # | ||
| # There is deliberately no `on: paths:` filter. A path-filtered job reports no | ||
| # status on the PRs it skips, so as a required check it would leave every PR | ||
| # that does not touch the spec pending forever. The job is two checkouts and one | ||
| # container, so it just always runs. | ||
| # | ||
|
|
||
| on: | ||
| pull_request: | ||
| # `edited` included for base retargets (e.g. a stacked PR's base merging): | ||
| # this job's verdict depends on the base sha, and without `edited` a stale | ||
| # verdict stays attached to the unchanged head sha. | ||
| types: [opened, synchronize, reopened, edited] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
|
|
||
| breaking-changes: | ||
| name: API breaking changes (oasdiff) | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Check out the pull request | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| # The spec is all this job reads, and the oasdiff container gets the | ||
| # workspace mounted; do not leave a push token in .git/config for it. | ||
| persist-credentials: false | ||
| - name: Check out the base spec | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| ref: ${{ github.event.pull_request.base.sha }} | ||
| path: base | ||
| persist-credentials: false | ||
| - name: Diff the spec against the base branch | ||
| # Pinned by sha because this step is the gate's verdict. The tag pins | ||
| # the engine: v0.1.10 is FROM tufin/oasdiff:v1.26.1, so a local | ||
| # `oasdiff v1.26.1` reproduces what CI decides here. | ||
| uses: oasdiff/oasdiff-action/breaking@0ab8ad204b00d25acc5ae87106281433e288d0c1 # v0.1.10 | ||
| with: | ||
| base: base/pg-pkg/api-description.yaml | ||
| revision: pg-pkg/api-description.yaml | ||
| fail-on: WARN | ||
| # Both of these rate ERR but are opt-in, so they do not run unless | ||
| # named: a changed non-success status (401 -> 403) and an enum value | ||
| # dropped from a response property. | ||
| include-checks: response-non-success-status-removed,response-property-enum-value-removed | ||
| # Do not upload the two specs to oasdiff.com for a side-by-side | ||
| # review page. The default is `true`; the detection and the inline | ||
| # annotations work without it, so nothing leaves CI. | ||
| review: false | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit, needs your hands (the App has no The input is left at its action default, which is That makes it a free removal, and it is the same threat model this step already reasons about two lines up, where |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, and a question rather than a defect.
This line presents
/v3as the escape hatch while pointing aterr-ignoreas the thing not to reach for.err-ignoreis a file of regexes; what #249 actually offered as the alternative to a versioned route was "an explicit override label", and this PR does not implement one.Dropping the label is defensible: on a required check it makes the gate advisory for anyone who can apply a label, and because pg-pkg's routes are versioned,
/v3is a real alternative rather than a formality. But it is a second deviation from #249, and the PR body claimed only one until this review. I have added it to the body; if you agree with the choice, a clause saying so belongs here too, since this comment is where the next person hitting a red gate will look.If you would rather have the label, it is
if: "!contains(github.event.pull_request.labels.*.name, 'api-break-approved')"on the job, and it needs a maintainer commit for the same permissions reason.