From ef7c54750d1c4513086d06f83e0279174b069740 Mon Sep 17 00:00:00 2001 From: Stephen Lumenta Date: Wed, 10 Jun 2026 15:24:11 +0200 Subject: [PATCH 1/3] ci: add oasdiff breaking-change gate on PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generated-client test matrix only catches spec changes that fail to compile. A renamed operationId, a removed parameter, or a changed type regenerates clients and their tests together, so everything stays green while every SDK consumer breaks — and build.yml then auto-publishes the regenerated clients to seven downstream repos on merge. oasdiff compares the compiled spec semantically: doc-only edits (descriptions, examples) pass, functional surface changes fail with --fail-on ERR. Both sides of the diff are the committed doc/compiled.json, which the existing compare-output job already keeps honest, so no recompile is needed. Intentional API changes opt out via the breaking-change-approved label; the job also posts the oasdiff changelog as a sticky PR comment so reviewers see the semantic diff instead of bundle noise. Co-Authored-By: Claude Fable 5 --- .github/workflows/breaking-changes.yml | 74 ++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/breaking-changes.yml diff --git a/.github/workflows/breaking-changes.yml b/.github/workflows/breaking-changes.yml new file mode 100644 index 00000000..0724554d --- /dev/null +++ b/.github/workflows/breaking-changes.yml @@ -0,0 +1,74 @@ +name: Breaking changes + +# Gate every PR on a semantic diff of the compiled spec. The generated-client +# test matrix only catches changes that fail to compile; a renamed operationId, +# a removed parameter, or a type change regenerates clients (and their tests) +# that stay green while breaking every SDK consumer. oasdiff compares the spec +# itself, so doc-only edits (descriptions, examples) pass and functional +# surface changes fail. +# +# Both sides of the diff are the committed doc/compiled.json: the +# compare-output job in lint.yml already guarantees it matches a fresh bundle, +# so no recompile is needed here. +# +# Escape hatch for intentional API changes: add the +# `breaking-change-approved` label to the PR and the gate is skipped +# (the changelog comment still posts). + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + contents: read + pull-requests: write + +jobs: + breaking-changes: + name: Breaking changes + runs-on: ubuntu-latest + steps: + - name: Checkout PR head + uses: actions/checkout@v4 + + - name: Checkout base spec + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + path: base + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.24.4 + + - name: Install oasdiff + run: go install github.com/oasdiff/oasdiff@v1.18.6 + + - name: Post API changelog as PR comment + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + MARKER="" + CHANGELOG=$(oasdiff changelog base/doc/compiled.json doc/compiled.json || true) + BODY="$MARKER + ### API changelog (oasdiff) + + Doc-only edits (descriptions, examples) do not appear here. + + \`\`\` + $CHANGELOG + \`\`\`" + EXISTING=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ + --jq "[.[] | select(.body | startswith(\"$MARKER\"))][0].id // empty") + if [ -n "$EXISTING" ]; then + gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${EXISTING}" -f body="$BODY" > /dev/null + else + gh api --method POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" -f body="$BODY" > /dev/null + fi + + - name: Fail on breaking API changes + if: ${{ !contains(github.event.pull_request.labels.*.name, 'breaking-change-approved') }} + run: oasdiff breaking base/doc/compiled.json doc/compiled.json --fail-on ERR From 5c326dcd61630c96845aac641b12ddad54513125 Mon Sep 17 00:00:00 2001 From: Stephen Lumenta Date: Wed, 10 Jun 2026 15:31:23 +0200 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/breaking-changes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/breaking-changes.yml b/.github/workflows/breaking-changes.yml index 0724554d..5a566377 100644 --- a/.github/workflows/breaking-changes.yml +++ b/.github/workflows/breaking-changes.yml @@ -21,8 +21,8 @@ on: permissions: contents: read + issues: write pull-requests: write - jobs: breaking-changes: name: Breaking changes From 723267713963af02979e6077ff7f3cb15016fbf1 Mon Sep 17 00:00:00 2001 From: Stephen Lumenta Date: Wed, 10 Jun 2026 15:36:11 +0200 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/breaking-changes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/breaking-changes.yml b/.github/workflows/breaking-changes.yml index 5a566377..4564feaf 100644 --- a/.github/workflows/breaking-changes.yml +++ b/.github/workflows/breaking-changes.yml @@ -61,7 +61,7 @@ jobs: \`\`\` $CHANGELOG \`\`\`" - EXISTING=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ + EXISTING=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments?per_page=100" \ --jq "[.[] | select(.body | startswith(\"$MARKER\"))][0].id // empty") if [ -n "$EXISTING" ]; then gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${EXISTING}" -f body="$BODY" > /dev/null