Skip to content

docs(compat): retire the 1.x condition telemetry cannot satisfy - #393

Draft
dobby-coder[bot] wants to merge 7 commits into
mainfrom
docs/382-retirement-condition-telemetry
Draft

docs(compat): retire the 1.x condition telemetry cannot satisfy#393
dobby-coder[bot] wants to merge 7 commits into
mainfrom
docs/382-retirement-condition-telemetry

Conversation

@dobby-coder

@dobby-coder dobby-coder Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Closes #382.

COMPATIBILITY.md conditioned removal on telemetry showing no traffic, in three places. For @e4a/pg-js 1.x that condition cannot be satisfied in either direction, so this replaces it with what the metric can answer. No version number, no reader row and no line inside the fenced block moves: the diff hunks are at :90, :103, :181 and :207, and the block sits at :125-130.

The three edits

  1. The @e4a/pg-js support-window bullet retires 1.x on the calendar rule already in that section, at least 12 months after its successor shipped. npm published 2.0.0 on 2026-06-02, so 1.x stays in the window until 2027-06-02 at the earliest. The add-in note, the apps/outlook-addon pointer and the postguard-outlook-addon#125 record all stay. The clause that carried them opened with "The other half of that condition", which no longer has a first half, so it now reads "which retirement also used to wait on".
  2. The support-window paragraph drops "and longer while live client-version telemetry still shows it" and its [postguard-ops#64] reference. The 12-month rule stands alone.
  3. Deprecation steps 2 and 3 are narrowed rather than dropped. Step 2 separates what postguard_clients settles, which routes are called and which identified clients call them (path is the pattern actix matched against the server's own route table), from what it cannot: which client and which version sent a request carrying no header. Step 3 keeps the telemetry gate where the thing being removed is observable, a route or a field, and drops it for a client version, where the expired window and step 1's announcement are the whole condition.

Two claims re-checked against main

The exporter landed after this ticket was filed, so step 2's "not measurable" bullet describes the code it lands on rather than the code it was written against. pg-pkg/src/middleware/metrics.rs counts every /v2 request, and a header that is absent, unreadable or not exactly four fields lands as client="unknown" (UNKNOWN at metrics.rs:18, the fields.unwrap_or_else fallback at metrics.rs:164). Header-less traffic is visible as traffic, and the bullet says so. What stays unavailable is who sent it: @e4a/pg-js 1.x shares that bucket with probes, scanners, every direct HTTP caller and E4A.PostGuard below 0.5.0, which is why no measurement can retire the line.

The issue's own measurement holds against the readers pg-compat-js installs. X-POSTGUARD-CLIENT-VERSION appears 0 times in node_modules/pg-js-1-11-0/dist/index.mjs and 3 times in the 2.4.0 one.

postguard-ops#64 to #71

Three occurrences in COMPATIBILITY.md at HEAD: the two uses at :103-104 and :183, plus the link definition at :207. Edits 2 and 3 rewrite both uses, so the definition was this file's last one, and [postguard-ops#71] takes its position. One reference survives outside the file, the comment at .github/workflows/api-diff.yml:22; it is out of this issue's scope and outside what this account can push.

I could not open either ops issue from here (this installation has no read access to privacybydesign/postguard-ops), so #71's number and title are the amendment's, not the tracker's.

Review fixes (6f30e66)

Two claims about E4A.PostGuard were wrong, both inherited from the issue's premise that nothing in the tree makes the .NET SDK send the header. That premise predates the release that does, so I checked it against the SDK rather than against this tree:

  • E4A.PostGuard-v0.5.0 (2026-06-19) shipped "send X-POSTGUARD-CLIENT-VERSION on every request" (postguard-dotnet#33).
  • src/ClientVersion.cs builds dotnet,<framework>,pg-dotnet,<version> and PostGuard.cs:49 applies it.
  • pg-pkg allowlists both halves deliberately: dotnet at metrics.rs:37, pg-dotnet at :48, with test_dotnet_sdk_host_is_not_other asserting client="pg-dotnet".

So step 2's bullet now puts only the sub-0.5.0 releases in the unknown bucket. Those are still supported, because the window counts 0.x as one line, so the bullet keeps a real .NET case rather than dropping the SDK from it. Its conclusion is untouched: probes, scanners and direct HTTP callers are what keep that bucket off zero.

Step 3 said a client version is not observable with no qualifier, which waived the telemetry condition for every client that does send the header — pg-dotnet, pg4ol, pg4tb, cli and pg-js 2.x all land with a real client_version, and E4A.PostGuard has its own support-window bullet. The discriminator is now whether the client sends the header, so the condition still binds wherever it can be met, and is waived only where it cannot.

Also dropped the four em-dashes these two hunks had added, putting the file back to the two it already had.

The third review point, .github/workflows/api-diff.yml:22 still reading postguard-ops#64, is left alone on purpose: issue #382 puts .github/workflows/ out of scope, and this app has no workflows permission to push it. It needs a separate human commit.

Review fixes (868b0d7)

Step 3 waived a qualifier the metric needs. It said a client that sends the header "is counted under its own client_version, and the condition holds for it unchanged", which reads a zero against a real version as zero traffic. client_version_label (pg-pkg/src/middleware/metrics.rs:97) does not promise that:

  • A misshapen version field returns other: empty, longer than MAX_CLIENT_VERSION_LEN = 32, or carrying a byte outside [A-Za-z0-9.-].
  • So does a well-shaped version, once that client's SEEN_CLIENT_VERSIONS bucket already holds MAX_CLIENT_VERSIONS = 64 series. The static never evicts, so a full bucket only empties on a process restart, and @e4a/pg-js has 55 published versions against that cap with an attacker-controlled field for the 9 slots left.

So step 3 now tells the reader to read that client's other next to the version's own series: zero on both is absence, a non-zero other means the version may be folded into it and the metric has not answered. The condition still binds for every client that sends the header, and is still waived only where no header arrives.

Both branches are shipped behaviour rather than a prediction about it, each with a test in pg-pkg/src/middleware/metrics.rs: test_misshapen_client_version_becomes_other and test_client_version_cap_holds.

The same commit rewraps step 2's first sentence to the file's 80 columns, which ff27ef1's rate-limiter qualifier left short. No wording changed there.

One coupling this adds, stated rather than left to be found: 64 and 55 now sit in COMPATIBILITY.md with nothing checking them against metrics.rs. Both are cited beside MAX_CLIENT_VERSIONS by name, so a grep for the constant reaches the prose, and #382 rules out touching pg-pkg/ to add the test that would close it properly.

The second point, .github/workflows/api-diff.yml:22 still reading postguard-ops#64, stands where the previous round left it, for the same two reasons: #382 puts .github/workflows/ out of scope, and this app has no workflows permission to push it.

Testing

Both acceptance checks, against a sample set sealed by HEAD:

cargo test --manifest-path pg-compat/Cargo.toml   # 18 passed
cd pg-compat-js && npm ci && npm test             # 26 passed

One correction to that second line, from running it in a clean container this round: npm test on its own fails 5 of the 26 in test/wire-compat.test.mjs, before this change as well as after it, because the pinned readers resolve the sample set from PG_COMPAT_ARTIFACTS and fall back to a path that does not exist. The invocation that passes is the one the test file documents at test/wire-compat.test.mjs:5-6:

PG_COMPAT_ARTIFACTS=$PWD/target/wire-compat/artifacts npm --prefix pg-compat-js test

What a prose edit could have disturbed is the fenced block's two machine-readers, and both parse it and compare it against their own list: the_support_window_is_the_crates_io_reader_list_in_compatibility_md in pg-compat/tests/support_window.rs, with no_two_pinned_readers_share_a_minor_line alongside it, and the support window is the npm reader list in COMPATIBILITY.md in pg-compat-js/test/manifest.test.mjs. published_readers_open_the_head_sealed_sample_set and published JS readers open the HEAD-sealed sample set then opened every case with the pinned readers.

Both suites need the sample set sealed first, or seven of those tests fail on a missing manifest.json, before this change as well as after it:

cargo run -p pg-core --features stream --example seal-samples -- target/wire-compat/artifacts

Also green, though a markdown edit cannot reach it: cargo test --workspace, 407 passed.

All three re-run on 6f30e66: pg-compat 18 passed, pg-compat-js 26 passed, workspace 407 passed. The fenced reader block is byte-identical to main's, and every version string in the file is an addition — no existing pin moved.

All three re-run on 868b0d7: pg-compat 18 passed (support_window 2 of them), pg-compat-js 26 passed (manifest.test.mjs 7 of them, including the npm reader-list assertion), workspace 407 passed. The fenced block is still byte-identical to main's, and comm -23 over the version-like tokens of both revisions is empty, so no pin was dropped by the rewrap.

Review fixes (ab5c3b6)

Three findings, all three real. I re-measured each one before editing.

2.0.x is header-less too (590eae1). The premise both earlier rounds
inherited from the issue was that the header arrived with the 2.x line. It
arrived one minor later. Packed all 55 published @e4a/pg-js tarballs and
grepped each for the header name: 0 occurrences in every version through
2.0.2, 7 from 2.1.0 on. Step 2's bullet now dates the header to 2.1.0 and
puts 1.x and 2.0.x in the client="unknown" bucket, and step 3's
parenthetical reads below 2.1.0 instead of naming a major. The bullet also
says 2.0.x is inside the window, which is the part that bites: the window is
the last two majors, so this is a retirement the metric cannot answer for a
line that is not on its way out.

The 55/9 headroom sum was wrong by roughly 5x (ab5c3b6). A version spends a
slot in SEEN_CLIENT_VERSIONS only by arriving in the header, and only 15 of
the 55 published versions send one, so real releases leave 49 of the 64 free
rather than 9. Dropped the arithmetic rather than correcting it: the
misshapen-field branch fires with no cap involved and the field is
attacker-controlled, so other is worth reading either way. That also retires
the coupling the last round's body flagged, since 64 and 55 are no longer
transcribed out of metrics.rs with nothing checking them. MAX_CLIENT_VERSIONS
stays named, so a grep for the constant still reaches the prose. The same sum in
that static's own doc comment is pre-existing (#384) and stays put, because #382
rules out touching pg-pkg/.

Step 3's reader instruction stopped one client short (ab5c3b6).
client="pg-js" at zero with its other at zero is still not absence.
Checked against postguard-js rather than reasoned about:
apps/outlook-addon/src/lib/pkg-client.ts:20 sends
Outlook,1.0,pg4ol,<addin version>, packages/pg-js/src/postguard-base.ts:29-31
says an embedding host's header wins, the add-in is on
"@e4a/pg-js": "workspace:*" which resolves to 2.4.0, and pg-pkg allowlists
pg4ol and pg4tb in KNOWN_CLIENTS (metrics.rs:46). So the 2.4.0 the
add-in ships lands under neither series a maintainer was told to read, and step
3 now names that as the second reading.

api-diff.yml:22 is handed over in a comment on this PR: the full file
first, then one gh api -X PUT with the base64 inlined, since this app cannot
push .github/workflows/. Only the ticket number is stale there. That sentence
is about retiring /v2, and a route is observable, so the gate it describes
survives step 3's narrowing.

Testing (ab5c3b6)

All three suites on the pushed head:

cargo test --manifest-path pg-compat/Cargo.toml                              # 18 passed
PG_COMPAT_ARTIFACTS=$PWD/target/wire-compat/artifacts npm --prefix pg-compat-js test   # 26 passed
cargo test --workspace                                                      # 407 passed

Both machine-readers of the fenced block ran green:
the_support_window_is_the_crates_io_reader_list_in_compatibility_md with
no_two_pinned_readers_share_a_minor_line beside it, and the support window is the npm reader list in COMPATIBILITY.md (subtest 22 of 26). The block is still
byte-identical to main's, no version-like token from main was dropped, the
file still holds main's two em-dashes and no more, and every new line is
inside the file's 80 columns.

Reviewed by dobby: the one binding rule for this repo (code-comments) over the markdown-only COMPATIBILITY.md diff — it carries no source-code comments, so the rule cannot fire and there are no rule findings — plus step 2's and step 3's new prose re-checked against pg-pkg's actual middleware layering and metric labels on main, which contradict two of its claims — built pg-pkg, pg-compat 3/3, pg-compat-js 26/26, cargo test --workspace 407/407, plus a scratch actix test reproducing the rate-limiter/counter layering, all passed — request-changes.
Attention: COMPATIBILITY.md:182-185's parenthetical, that an over-limit request "is rejected with 429 before it reaches the counter", is true only of the general /v2 limiter and false for exactly the endpoints a deprecation would read — Governor::new(&general_ratelimit) is registered after .wrap_fn(collect_metrics) on the /v2 scope so it sits outside the counter, but the stricter per-endpoint limiter is wrapped on resources inside that scope (pg-pkg/src/server.rs:295, :306, :323, :330, :346, :356) and its rejections are counted with status="429", and it is the one that fires in practice (2/s burst 10 against the general 10/s burst 50) on /request-start, /key, /key/{timestamp}, /sign/key and /api-key/validate.
Attention: COMPATIBILITY.md:205-206's "A route or a field is observable" is half wrong and contradicts step 2 four lines above it — postguard_clients carries path, host, client, client_version, status (pg-pkg/src/server.rs:29-33) and is the only metric family the PKG registers, so no label carries a /v2 request or response field, and as written step 3 puts a telemetry gate on removing a field that the telemetry cannot open.
Attention: COMPATIBILITY.md:242's postguard-ops#71 could not be verified from this container either — privacybydesign/postguard-ops 404s through the per-org token (the repo itself, not just the issue) and #64 404s the same way — so a human with access needs to confirm #71 is the scrape ticket before merge, since it is the only pointer step 2 leaves.
Attention: .github/workflows/api-diff.yml:22 still reads postguard-ops#64 while COMPATIBILITY.md:242 now reads #71, so the repo's two references to the scrape ticket disagree; leaving it here is correct (issue #382 puts .github/workflows/ out of scope and this app has no workflows permission to push it), so it needs the separate human commit already handed over in a PR comment.

`COMPATIBILITY.md` conditioned removal on telemetry showing no traffic in
three places. For `@e4a/pg-js` 1.x that condition cannot be satisfied in
either direction: 1.x never sends `X-POSTGUARD-CLIENT-VERSION` (zero
occurrences in 1.11.0's `dist/index.mjs`, three in 2.4.0's), and it requests
the same route set with the same start-request body as 2.x, so nothing
separates its traffic from any other unidentified caller.

- The `@e4a/pg-js` support-window bullet now retires `1.x` on the calendar
  rule already on the page: 12 months after `2.0.0` shipped on 2026-06-02,
  so 2027-06-02 at the earliest.
- The support-window paragraph drops the "longer while telemetry still shows
  it" clause; the 12-month rule stands alone.
- Deprecation steps 2 and 3 narrow rather than drop the telemetry condition.
  Step 2 says what `postguard_clients` can settle (routes, and identified
  clients) and what it cannot (which client and version sent a header-less
  request, which `e86ff19` now counts as `client="unknown"`). Step 3 keeps
  the telemetry gate where the thing being removed is observable, a route or
  a field, and drops it for a client version.

Step 2's scrape pointer moves from postguard-ops#64, closed 2026-08-19, to
the live postguard-ops#71. No version number, no reader row and no line
inside the fenced block changes.

Closes #382
@dobby-coder
dobby-coder Bot requested a review from rubenhensen August 31, 2026 12:17
dobby-coder Bot and others added 6 commits August 31, 2026 12:35
…trigger

The bullet gave two different retirement conditions one sentence apart. Its
own first clause is "the last two majors", so `1.x` drops when `3.0.0` ships;
the calendar rule below is "at least 12 months", a floor that never removes
anything. 2027-06-02 stays as the floor it is, so a consumer planning a
migration does not read it as the drop date.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two claims in the deprecation steps were wrong about `E4A.PostGuard`.
The issue's premise, that nothing makes the .NET SDK send
`X-POSTGUARD-CLIENT-VERSION`, predates the release that does:
`E4A.PostGuard-v0.5.0` (2026-06-19) shipped "send
X-POSTGUARD-CLIENT-VERSION on every request", `src/ClientVersion.cs`
sends `dotnet,<framework>,pg-dotnet,<version>`, and `pg-pkg` allowlists
both halves (`metrics.rs:37`, `:48`, asserted by
`test_dotnet_sdk_host_is_not_other`).

Step 2's "not measurable" bullet therefore no longer puts the whole of
`E4A.PostGuard` in the `unknown` bucket, only the releases below 0.5.0 —
which are still supported, because the window counts `0.x` as one line.
The bullet's conclusion is unchanged: probes, scanners and direct HTTP
callers keep that bucket from reaching zero.

Step 3 said a client version is not observable, full stop, which waived
the telemetry condition for every client that does send the header
(`pg-dotnet`, `pg4ol`, `pg4tb`, `cli`, `pg-js` 2.x all carry a real
`client_version`). The discriminator is now whether the client sends the
header, so the condition still binds where it can be met.

Also drops the four em-dashes these hunks had added, restoring the
file's own count of two.
Step 2 said `pg-pkg` counts every `/v2` request. The Governor is registered
after `collect_metrics` and so sits in front of it (`pg-pkg/src/server.rs`),
which is the invariant `server.rs:411` already states: `collect_metrics`
counts every `/v2` request that gets past the rate limiter. A 429 increments
nothing, not even the `unknown` bucket.

This is the one place in the file that enumerates what the metric settles, so
the qualifier belongs here; the paragraph's argument is unchanged, since
header-less traffic that is served is still visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Step 3 said a client that sends the header "is counted under its own
`client_version`, and the condition holds for it unchanged". That waived
a qualifier the metric needs: `client_version_label`
(`pg-pkg/src/middleware/metrics.rs:97`) returns `other` when the version
field is misshapen, and when that client's `SEEN_CLIENT_VERSIONS` bucket
has already created `MAX_CLIENT_VERSIONS` = 64 series, which the static
never evicts before a restart. `@e4a/pg-js` has 55 published versions
against that cap and the field is attacker-controlled, so an empty
series for one version is not on its own zero traffic.

Step 3 now says to read that client's `other` alongside the version's own
series: zero on both is absence, a non-zero `other` means the version may
be folded into it and the metric has not answered. Both branches are
shipped behaviour, covered by `test_misshapen_client_version_becomes_other`
and `test_client_version_cap_holds`.

Also rewraps step 2's first sentence to the file's 80 columns, which the
rate-limiter qualifier in ff27ef1 left short.
Step 2 named `1.x` as the concrete unidentifiable client and step 3's
parenthetical repeated it, both on the issue's premise that the header
arrived with the 2.x line. It arrived one minor later. Packing all 55
published tarballs and grepping for the header name: 0 occurrences in
every version up to and including `2.0.2`, 7 from `2.1.0` on.

So `2.0.x` shares the `client="unknown"` bucket with `1.x`, and unlike
`1.x` it is not a line on its way out: the window is the last two majors,
so a `2.0.x` retirement is one this metric cannot answer either. Both
places now say `2.1.0` rather than naming a major.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two problems with the same sentence, both found by reading it as a
maintainer about to retire a version.

It said 55 published `@e4a/pg-js` versions leave 9 slots under the
64-series cap. A version only spends a slot by arriving in the header,
and only 15 of the 55 send one, so real releases leave 49 free. The sum
argued for a `MAX_CLIENT_VERSIONS` bump nobody needs. Dropped rather than
corrected: the misshapen branch needs no cap to fire and the field is
attacker-controlled, so `other` is worth reading either way, and one less
constant transcribed out of `metrics.rs` is one less to drift.

It also stopped a reading short. `client="pg-js"` at zero with its
`other` at zero is still not absence, because the add-ins override the
header wholesale and `pg-pkg` allowlists `pg4ol` and `pg4tb` as clients
in their own right (`KNOWN_CLIENTS`). The add-in ships `@e4a/pg-js`
2.4.0 and reports `Outlook,1.0,pg4ol,<addin version>`, so that 2.4.0
touches neither series a maintainer was told to check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dobby-coder

dobby-coder Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Hand-over: .github/workflows/api-diff.yml:22 still says postguard-ops#64

COMPATIBILITY.md:238 now points at postguard-ops#71, and the comment at
api-diff.yml:22 is the repo's last reference to the old number, so the two
disagree. It cannot ride along on this PR: #382 rules out .github/workflows/,
and this app has no workflows permission, so a push touching that path is
rejected.

One line changes. The sentence is about retiring /v2, and a route is
observable, so the telemetry gate it describes still holds after this PR's
narrowing of step 3; only the ticket number is stale.

Full file as it would land:

name: API diff
#
# Breaking-change gate on both OpenAPI contracts (#249, cryptify#196/#202).
#
# Two specs, one matrix: pg-pkg/api-description.yaml (versioned /v2 routes)
# and cryptify/api-description.yaml (unversioned). The matrix is not
# cosmetic. Both pg-pkg/tests/api_gate.rs and cryptify's `mod
# api_gate_tests` read this file and require exactly one `fail-on:` and one
# `include-checks:` line, so that neither can claim a setting the committed
# job does not use. Two separate oasdiff steps would give two of each and
# fail both suites; the matrix varies only the spec paths, leaving the two
# settings written once. Keep it that way: moving `fail-on` into the matrix
# would remove the literal both tests look for.
#
# 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#71 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 (${{ matrix.spec }})
    runs-on: ubuntu-latest
    strategy:
      # One spec's verdict must not cancel the other's.
      fail-fast: false
      matrix:
        spec:
          - pg-pkg/api-description.yaml
          - cryptify/api-description.yaml
    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/${{ matrix.spec }}
          revision: ${{ matrix.spec }}
          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

Applying it needs the workflow token scope (gh auth refresh -s workflow):

gh api -X PUT repos/encryption4all/postguard/contents/.github/workflows/api-diff.yml -f branch=docs/382-retirement-condition-telemetry -f message="docs(ci): point the api-diff escape hatch at postguard-ops#71" -f sha="$(gh api "repos/encryption4all/postguard/contents/.github/workflows/api-diff.yml?ref=docs/382-retirement-condition-telemetry" --jq .sha)" -f content=bmFtZTogQVBJIGRpZmYKIwojIEJyZWFraW5nLWNoYW5nZSBnYXRlIG9uIGJvdGggT3BlbkFQSSBjb250cmFjdHMgKCMyNDksIGNyeXB0aWZ5IzE5Ni8jMjAyKS4KIwojIFR3byBzcGVjcywgb25lIG1hdHJpeDogcGctcGtnL2FwaS1kZXNjcmlwdGlvbi55YW1sICh2ZXJzaW9uZWQgL3YyIHJvdXRlcykKIyBhbmQgY3J5cHRpZnkvYXBpLWRlc2NyaXB0aW9uLnlhbWwgKHVudmVyc2lvbmVkKS4gVGhlIG1hdHJpeCBpcyBub3QKIyBjb3NtZXRpYy4gQm90aCBwZy1wa2cvdGVzdHMvYXBpX2dhdGUucnMgYW5kIGNyeXB0aWZ5J3MgYG1vZAojIGFwaV9nYXRlX3Rlc3RzYCByZWFkIHRoaXMgZmlsZSBhbmQgcmVxdWlyZSBleGFjdGx5IG9uZSBgZmFpbC1vbjpgIGFuZCBvbmUKIyBgaW5jbHVkZS1jaGVja3M6YCBsaW5lLCBzbyB0aGF0IG5laXRoZXIgY2FuIGNsYWltIGEgc2V0dGluZyB0aGUgY29tbWl0dGVkCiMgam9iIGRvZXMgbm90IHVzZS4gVHdvIHNlcGFyYXRlIG9hc2RpZmYgc3RlcHMgd291bGQgZ2l2ZSB0d28gb2YgZWFjaCBhbmQKIyBmYWlsIGJvdGggc3VpdGVzOyB0aGUgbWF0cml4IHZhcmllcyBvbmx5IHRoZSBzcGVjIHBhdGhzLCBsZWF2aW5nIHRoZSB0d28KIyBzZXR0aW5ncyB3cml0dGVuIG9uY2UuIEtlZXAgaXQgdGhhdCB3YXk6IG1vdmluZyBgZmFpbC1vbmAgaW50byB0aGUgbWF0cml4CiMgd291bGQgcmVtb3ZlIHRoZSBsaXRlcmFsIGJvdGggdGVzdHMgbG9vayBmb3IuCiMKIyBwZy1wa2cvYXBpLWRlc2NyaXB0aW9uLnlhbWwgaXMgdGhlIHBpbm5lZCB2MiBIVFRQIGNvbnRyYWN0ICgjMjQyKSwgb25lIG9mIHRoZQojIHRocmVlIHNlYW1zIENPTVBBVElCSUxJVFkubWQgZ3VhcmFudGVlcy4gVGhpcyBqb2IgZGlmZnMgdGhlIFBSJ3Mgc3BlYyBhZ2FpbnN0CiMgdGhlIGJyYW5jaCB0aGUgUFIgdGFyZ2V0cyBhbmQgZmFpbHMgb24gYW55IGNoYW5nZSBvYXNkaWZmIHJhdGVzIFdBUk4gb3IgRVJSLgojCiMgRXNjYXBlIGhhdGNoLCBub3QgYSB3YWxsOiBwZy1wa2cncyByb3V0ZXMgYXJlIHZlcnNpb25lZCAodW5saWtlIGNyeXB0aWZ5J3MsCiMgd2hvc2UgZ2F0ZSB0aGlzIG1pcnJvcnMpLCBzbyBhIGNoYW5nZSAvdjIgY2Fubm90IHRha2UgYWRkaXRpdmVseSBnb2VzIHVuZGVyIGEKIyBuZXcgcHJlZml4ICgvdjMvLi4uKSB3aXRoIC92MiBsZWZ0IHJ1bm5pbmcsIGFuZCAvdjIgaXMgcmV0aXJlZCBsYXRlciB0aHJvdWdoCiMgdGhlIGRlcHJlY2F0aW9uIHByb2Nlc3MgaW4gQ09NUEFUSUJJTElUWS5tZCwgb25jZSBwb3N0Z3VhcmQtb3BzIzcxIHRlbGVtZXRyeQojIHNob3dzIG5vYm9keSBjYWxscyBpdC4gQSAvdjMgcm91dGUgYWRkZWQgbmV4dCB0byAvdjIgcmVhZHMgYXMgYWRkaXRpdmUsIHNvCiMgdGhpcyBnYXRlIHBhc3NlcyBpdC4gUmVhY2ggZm9yIHRoYXQgYmVmb3JlIHJlYWNoaW5nIGZvciBlcnItaWdub3JlLgojCiMgV2h5IFdBUk4gYW5kIG5vdCBFUlIKIyAtLS0tLS0tLS0tLS0tLS0tLS0tLQojIGBmYWlsLW9uOiBFUlJgIHdvdWxkIGxlYXZlIGZvdXIgb2YgdGhlIGNoYW5nZXMgQ09NUEFUSUJJTElUWS5tZCBmb3JiaWRzCiMgcGFzc2luZyBzaWxlbnRseSwgYmVjYXVzZSBvYXNkaWZmIHJhdGVzIHRoZW0gV0FSTiwgYW5kIG9uIHRoaXMgc3BlYyB0aGF0IGlzCiMgbW9zdCBvZiB0aGUgIm5vIHJlbW92aW5nIGEgcm91dGUgb3IgYSBmaWVsZCIgcnVsZTogb25seSBgc3RhdHVzYCBpcyBldmVyCiMgYHJlcXVpcmVkYCwgc28gYGtleWAgKHRoZSBJQkUgdXNlciBzZWNyZXQga2V5IHRoZSBlbmRwb2ludCBleGlzdHMgdG8gcmV0dXJuKSwKIyBgcHJvb2ZTdGF0dXNgLCBgcHViU2lnbktleWAgYW5kIHRoZSByZXN0IGFyZSBvcHRpb25hbCwgYW5kIHJlbW92aW5nIG9yCiMgcmVuYW1pbmcgYW4gb3B0aW9uYWwgcmVzcG9uc2UgcHJvcGVydHkgaXMgV0FSTi4gU28gaXMgcmVtb3ZpbmcgYSByZXF1ZXN0CiMgcGFyYW1ldGVyLiBNZWFzdXJlZCBvbiB0aGlzIHNwZWMgYWdhaW5zdCBvYXNkaWZmIHYxLjI2LjE6CiMKIyAgIG11dGF0aW9uICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZhaWwtb24gRVJSICAgZmFpbC1vbiBXQVJOCiMgICBvcHRpb25hbCByZXNwb25zZSBwcm9wZXJ0eSByZW1vdmVkIChga2V5YCkgICBwYXNzZXMgICAgICAgIGZhaWxzCiMgICBvcHRpb25hbCByZXNwb25zZSBwcm9wZXJ0eSByZW5hbWVkICAgICAgICAgICBwYXNzZXMgICAgICAgIGZhaWxzCiMgICByZXF1aXJlZCBwYXRoIHBhcmFtZXRlciByZW1vdmVkICAgICAgICAgICAgICBwYXNzZXMgICAgICAgIGZhaWxzCiMgICByZXNwb25zZSBlbnVtIHZhbHVlIGFkZGVkICAgICAgICAgICAgICAgICAgICBwYXNzZXMgICAgICAgIGZhaWxzCiMKIyBUaGUgZm91cnRoIGlzIHRoZSBydWxlIENPTVBBVElCSUxJVFkubWQgZ2FpbnMgYWxvbmdzaWRlIHRoaXMgZ2F0ZTsgc2VlIGJlbG93LgojCiMgV0FSTiBhZGRzIDMxIGNoZWNrcyBvbiB0b3Agb2YgRVJSJ3MgMjEzLiBBbGwgYnV0IG9uZSBhcmUgY2hhbmdlcwojIENPTVBBVElCSUxJVFkubWQgYWxyZWFkeSBmb3JiaWRzIChyZXF1ZXN0LXBhcmFtZXRlci1yZW1vdmVkLAojIHJlcXVlc3QtcHJvcGVydHktcmVtb3ZlZCwgcmVzcG9uc2UtYm9keS1tZWRpYS10eXBlLXNjaGVtYS1yZW1vdmVkLCBhbmQgdGhlCiMgY29uc3RyYWludC1uYXJyb3dpbmcgKi1zZXQgZmFtaWx5KS4gVGhlIGV4Y2VwdGlvbiBpcwojIHJlc3BvbnNlLXByb3BlcnR5LWVudW0tdmFsdWUtYWRkZWQ6IGFkZGluZyBhIHZhbHVlIHRvIFNlc3Npb25TdGF0dXMgb3IKIyBQcm9vZlN0YXR1cyBmYWlscyB0aGlzIGdhdGUuIFRoYXQgaXMgZGVsaWJlcmF0ZSwgYW5kIENPTVBBVElCSUxJVFkubWQgbm93CiMgc2F5cyBzbyB0b28uIEEgY2xpZW50IHRoYXQgc3dpdGNoZXMgb24gdGhlIGVudW0gd2l0aG91dCBhIGRlZmF1bHQgYnJhbmNoCiMgYnJlYWtzIG9uIGEgdmFsdWUgaXQgaGFzIG5ldmVyIHNlZW4sIHNvIGEgbmV3IHN0YXR1cyBpcyBhIC92MyBjaGFuZ2UgKG9yIGFuCiMgeC1leHRlbnNpYmxlLWVudW0gb25lKSwgbm90IGFuIGFkZGl0aXZlIG9uZS4gSXQgaXMgdGhlIG9uZSBydWxlIGhlcmUgdGhhdAojIG9ubHkgV0FSTiBlbmZvcmNlcywgc28gaXQgaXMgYWxzbyB0aGUgZmlyc3QgY2FzdWFsdHkgb2YgYSByZXZlcnQgdG8gRVJSOwojIHBnLXBrZy90ZXN0cy9hcGlfZ2F0ZS5ycyBwaW5zIGl0LgojCiMgVHdvIG1vcmUgY2hlY2tzIGFyZSBvcHQtaW46IHRoZXkgcmF0ZSBFUlIgYnV0IG9ubHkgcnVuIHdoZW4gbmFtZWQsIHNvIHRoZXkKIyBuZWVkIHRoZSBpbmNsdWRlLWNoZWNrcyBpbnB1dCBiZWxvdy4gV2l0aG91dCBpdCwgY2hhbmdpbmcgYSA0MDEgdG8gYSA0MDMgYW5kCiMgZHJvcHBpbmcgYW4gZW51bSB2YWx1ZSBmcm9tIGEgcmVzcG9uc2UgYm90aCBwYXNzLiBLZWVwIGluY2x1ZGUtY2hlY2tzIGFuZAojIGZhaWwtb24gaW4gc3RlcCB3aXRoIHRoZSBsb2NhbC1yZXBybyBjb21tYW5kIGluIENMQVVERS5tZCwgb3IgYSBsb2NhbCBydW4KIyBxdWlldGx5IGRpc2FncmVlcyB3aXRoIENJLgojCiMgTm90IGNvdmVyZWQ6IHRoZSBnYXRlIGNvbXBhcmVzIGRvY3VtZW50ZWQgcGF0aHMsIGFuZCB0aGUgc3BlYyBkb2N1bWVudHMgdGhlCiMgY2Fub25pY2FsIC92Mi9yZXF1ZXN0Ly4uLiBwYXRocyBvbmx5IChzZWUgaXRzICJQYXRoIHByZWZpeCBhbGlhc2VzIiBub3RlKSwgc28KIyBkcm9wcGluZyB0aGUgL3YyL2lybWEvLi4uIGFsaWFzIGhhbmRsZXJzIGJlZm9yZSAjMjU3J3MgZGVwcmVjYXRpb24gaGFzIHJ1bgojIHBhc3NlcyB0aGlzIGdhdGUuIFRoYXQgb25lIHN0YXlzIGEgcmV2aWV3IHJ1bGUuCiMKIyBUaGVyZSBpcyBkZWxpYmVyYXRlbHkgbm8gYG9uOiBwYXRoczpgIGZpbHRlci4gQSBwYXRoLWZpbHRlcmVkIGpvYiByZXBvcnRzIG5vCiMgc3RhdHVzIG9uIHRoZSBQUnMgaXQgc2tpcHMsIHNvIGFzIGEgcmVxdWlyZWQgY2hlY2sgaXQgd291bGQgbGVhdmUgZXZlcnkgUFIKIyB0aGF0IGRvZXMgbm90IHRvdWNoIHRoZSBzcGVjIHBlbmRpbmcgZm9yZXZlci4gVGhlIGpvYiBpcyB0d28gY2hlY2tvdXRzIGFuZCBvbmUKIyBjb250YWluZXIsIHNvIGl0IGp1c3QgYWx3YXlzIHJ1bnMuCiMKCm9uOgogIHB1bGxfcmVxdWVzdDoKICAgICMgYGVkaXRlZGAgaW5jbHVkZWQgZm9yIGJhc2UgcmV0YXJnZXRzIChlLmcuIGEgc3RhY2tlZCBQUidzIGJhc2UgbWVyZ2luZyk6CiAgICAjIHRoaXMgam9iJ3MgdmVyZGljdCBkZXBlbmRzIG9uIHRoZSBiYXNlIHNoYSwgYW5kIHdpdGhvdXQgYGVkaXRlZGAgYSBzdGFsZQogICAgIyB2ZXJkaWN0IHN0YXlzIGF0dGFjaGVkIHRvIHRoZSB1bmNoYW5nZWQgaGVhZCBzaGEuCiAgICB0eXBlczogW29wZW5lZCwgc3luY2hyb25pemUsIHJlb3BlbmVkLCBlZGl0ZWRdCgpwZXJtaXNzaW9uczoKICBjb250ZW50czogcmVhZAoKam9iczoKCiAgYnJlYWtpbmctY2hhbmdlczoKICAgIG5hbWU6IEFQSSBicmVha2luZyBjaGFuZ2VzICgke3sgbWF0cml4LnNwZWMgfX0pCiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CiAgICBzdHJhdGVneToKICAgICAgIyBPbmUgc3BlYydzIHZlcmRpY3QgbXVzdCBub3QgY2FuY2VsIHRoZSBvdGhlcidzLgogICAgICBmYWlsLWZhc3Q6IGZhbHNlCiAgICAgIG1hdHJpeDoKICAgICAgICBzcGVjOgogICAgICAgICAgLSBwZy1wa2cvYXBpLWRlc2NyaXB0aW9uLnlhbWwKICAgICAgICAgIC0gY3J5cHRpZnkvYXBpLWRlc2NyaXB0aW9uLnlhbWwKICAgIHN0ZXBzOgogICAgICAtIG5hbWU6IENoZWNrIG91dCB0aGUgcHVsbCByZXF1ZXN0CiAgICAgICAgdXNlczogYWN0aW9ucy9jaGVja291dEB2NgogICAgICAgIHdpdGg6CiAgICAgICAgICAjIFRoZSBzcGVjIGlzIGFsbCB0aGlzIGpvYiByZWFkcywgYW5kIHRoZSBvYXNkaWZmIGNvbnRhaW5lciBnZXRzIHRoZQogICAgICAgICAgIyB3b3Jrc3BhY2UgbW91bnRlZDsgZG8gbm90IGxlYXZlIGEgcHVzaCB0b2tlbiBpbiAuZ2l0L2NvbmZpZyBmb3IgaXQuCiAgICAgICAgICBwZXJzaXN0LWNyZWRlbnRpYWxzOiBmYWxzZQogICAgICAtIG5hbWU6IENoZWNrIG91dCB0aGUgYmFzZSBzcGVjCiAgICAgICAgdXNlczogYWN0aW9ucy9jaGVja291dEB2NgogICAgICAgIHdpdGg6CiAgICAgICAgICByZWY6ICR7eyBnaXRodWIuZXZlbnQucHVsbF9yZXF1ZXN0LmJhc2Uuc2hhIH19CiAgICAgICAgICBwYXRoOiBiYXNlCiAgICAgICAgICBwZXJzaXN0LWNyZWRlbnRpYWxzOiBmYWxzZQogICAgICAtIG5hbWU6IERpZmYgdGhlIHNwZWMgYWdhaW5zdCB0aGUgYmFzZSBicmFuY2gKICAgICAgICAjIFBpbm5lZCBieSBzaGEgYmVjYXVzZSB0aGlzIHN0ZXAgaXMgdGhlIGdhdGUncyB2ZXJkaWN0LiBUaGUgdGFnIHBpbnMKICAgICAgICAjIHRoZSBlbmdpbmU6IHYwLjEuMTAgaXMgRlJPTSB0dWZpbi9vYXNkaWZmOnYxLjI2LjEsIHNvIGEgbG9jYWwKICAgICAgICAjIGBvYXNkaWZmIHYxLjI2LjFgIHJlcHJvZHVjZXMgd2hhdCBDSSBkZWNpZGVzIGhlcmUuCiAgICAgICAgdXNlczogb2FzZGlmZi9vYXNkaWZmLWFjdGlvbi9icmVha2luZ0AwYWI4YWQyMDRiMDBkMjVhY2M1YWU4NzEwNjI4MTQzM2UyODhkMGMxICMgdjAuMS4xMAogICAgICAgIHdpdGg6CiAgICAgICAgICBiYXNlOiBiYXNlLyR7eyBtYXRyaXguc3BlYyB9fQogICAgICAgICAgcmV2aXNpb246ICR7eyBtYXRyaXguc3BlYyB9fQogICAgICAgICAgZmFpbC1vbjogV0FSTgogICAgICAgICAgIyBCb3RoIG9mIHRoZXNlIHJhdGUgRVJSIGJ1dCBhcmUgb3B0LWluLCBzbyB0aGV5IGRvIG5vdCBydW4gdW5sZXNzCiAgICAgICAgICAjIG5hbWVkOiBhIGNoYW5nZWQgbm9uLXN1Y2Nlc3Mgc3RhdHVzICg0MDEgLT4gNDAzKSBhbmQgYW4gZW51bSB2YWx1ZQogICAgICAgICAgIyBkcm9wcGVkIGZyb20gYSByZXNwb25zZSBwcm9wZXJ0eS4KICAgICAgICAgIGluY2x1ZGUtY2hlY2tzOiByZXNwb25zZS1ub24tc3VjY2Vzcy1zdGF0dXMtcmVtb3ZlZCxyZXNwb25zZS1wcm9wZXJ0eS1lbnVtLXZhbHVlLXJlbW92ZWQKICAgICAgICAgICMgRG8gbm90IHVwbG9hZCB0aGUgdHdvIHNwZWNzIHRvIG9hc2RpZmYuY29tIGZvciBhIHNpZGUtYnktc2lkZQogICAgICAgICAgIyByZXZpZXcgcGFnZS4gVGhlIGRlZmF1bHQgaXMgYHRydWVgOyB0aGUgZGV0ZWN0aW9uIGFuZCB0aGUgaW5saW5lCiAgICAgICAgICAjIGFubm90YXRpb25zIHdvcmsgd2l0aG91dCBpdCwgc28gbm90aGluZyBsZWF2ZXMgQ0kuCiAgICAgICAgICByZXZpZXc6IGZhbHNlCg==

It targets this PR's branch so the change goes through the same review. Swap
-f branch= for one of your own if you would rather keep this PR's diff to
COMPATIBILITY.md alone.

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.

COMPATIBILITY.md: the 1.x retirement condition rests on telemetry that cannot see 1.x, in three places

0 participants