Skip to content

Auto-merge OpenAPI description update PRs - #6993

Draft
shawnHartsell wants to merge 5 commits into
mainfrom
auto-merge-openapi-updates
Draft

Auto-merge OpenAPI description update PRs#6993
shawnHartsell wants to merge 5 commits into
mainfrom
auto-merge-openapi-updates

Conversation

@shawnHartsell

Copy link
Copy Markdown
Contributor

What

Adds .github/workflows/auto-merge-openapi-updates.yml, which auto-merges the newest open github-openapi-bot "Update OpenAPI 3.x Descriptions" PRs and closes the older superseded ones.

Merging these by hand is pure toil: they are machine-generated from github/github, they are effectively never reviewed, and they stack up. There are 34 superseded PRs open right now.

How it works

Runs every 2 hours (plus workflow_dispatch, which supports a dry_run input):

  1. Selects the newest open bot PR for 3.0 and for 3.1; everything else from that author is superseded.
  2. Gates on Lint OpenAPI 3.0 releases and Lint OpenAPI 3.1 releases only.
  3. Scans api.github.com.yaml for removed path keys, enum values, and schema definitions. Any hit skips the merge and pings #api-platform.
  4. Merges 3.0 then 3.1 with plain git and pushes to main.
  5. Closes the superseded PRs with a comment.
  6. Notifies #api-platform on failure or breaking-change detection only. Success is silent.

Design notes

Why git instead of the merge API / native auto-merge. These PRs are 100K+ lines across 64+ files. PUT /pulls/{n}/merge returns 502/504, GET /pulls/{n}/files returns 422 "diff taking too long", and GET /pulls/{n} reports changed_files: 0. Native auto-merge is also disabled on this repo (allow_auto_merge: false) and would depend on that same API. Local git merge + push is the only thing that works, which matches the existing manual runbook.

Why the repo size is not a problem. Checkout uses filter: blob:none, so the ~4.6 GB of history is not downloaded; blobs are fetched lazily only for the files actually touched.

Why CodeQL is excluded from the gate. The CodeQL failures on these PRs are timed_out, not findings. Code scanning here is default setup scanning only the actions language, which is unrelated to YAML description changes, and it is not a required check on main (required_status_checks.contexts is empty), so it is not gating merges today either. Excluding it is an explicit allowlist decision rather than a bypass.

Token. Uses the existing (currently unused) OPENAPI_MERGE_TOKEN secret, falling back to github.token. Note that pushes made with GITHUB_TOKEN do not trigger downstream workflows, so OPENAPI_MERGE_TOKEN is the intended path.

Validation

Suggested rollout

Merge, then run workflow_dispatch with dry_run: true once to confirm selection in-situ before letting the schedule take over.

Note

Left as a draft for a human to review and mark ready.

@shawnHartsell

Copy link
Copy Markdown
Contributor Author

Added a hard-fail Preflight step that runs before anything else, per review feedback. Both credentials are now verified up front instead of failing opaquely mid-run.

Merge token. Probes repos/{repo} with OPENAPI_MERGE_TOKEN and asserts permissions.push == true.

  • unset → warns and falls back to GITHUB_TOKEN (unchanged behaviour)
  • set but expired/revoked/no access → fails the run with that reason
  • set but read-only → fails the run, naming the missing contents:write

This matters because OPENAPI_MERGE_TOKEN was created 2026-07-10 and is referenced by zero workflows in this repo, so its scopes and validity have never been exercised. Previously a stale token would have surfaced only as a confusing git push rejection after the merge commits were already built locally.

Chatterbox. Posts a real message to #api-platform and classifies the response: 2xx passes; missing secrets, connection failure/timeout, 401/403, or any other status fails the run. Worth noting the notify step in linter-failure-notifier.yml has been skipped on all 40 of its recent runs (lint never fails), so this curl has plausibly never actually executed. Since the notifier is the only signal that a breaking change was skipped, a silently-dead route would have made the guard worthless.

Also added a test_notify dispatch input to prove the chatterbox route end-to-end without waiting for a genuine failure; it sends the preflight post and stops before touching any PR. And the notify step is now gated on preflight having succeeded, so a known-dead route doesn't generate a second failure trying to report the first.

Two real bugs surfaced while testing the new code:

  1. gh api writes its error body to stdout, so the perms=$(... || echo 'ERROR') sentinel appended to the JSON rather than replacing it. $perms was therefore never equal to ERROR, and a bad token was misreported as "lacks push" instead of "cannot read repo". Now branches on exit status via if perms=$(...).
  2. curl writes 000 on connection failure and exits non-zero, so || echo '000' concatenated into 000000, which fell through to the generic *) branch instead of being reported as unreachable. Now uses || true.

Validation. Token probe exercised against live API across all four branches (unset / valid / bad credentials / valid-token-but-unreadable-repo), each reporting correctly. HTTP classifier verified against live 200, 401, 404, and a genuinely unreachable host. Workflow YAML parses and every run block passes bash -n.

@shawnHartsell
shawnHartsell marked this pull request as ready for review August 14, 2026 15:45
Copilot AI balanced review requested due to automatic review settings August 14, 2026 15:45

Copilot AI 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.

Pull request overview

Adds automation to merge current bot-generated OpenAPI updates and close superseded PRs.

Changes:

  • Selects and validates the latest OpenAPI 3.0/3.1 PRs.
  • Scans for potential breaking changes before merging.
  • Pushes merges, closes older PRs, and sends failure alerts.
Show a summary per file
File Description
.github/workflows/auto-merge-openapi-updates.yml Implements the scheduled auto-merge workflow.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 1/1 changed files
  • Comments generated: 7
  • Review effort level: Balanced

Comment on lines +99 to +103
code=$(curl --silent --output /dev/null --write-out '%{http_code}' \
--max-time 20 \
-u "${CHATTERBOX_TOKEN}:" \
"${CHATTERBOX_URL%/}/topics/%23api-platform" \
--data ':white_check_mark: OpenAPI auto-merge preflight: chatterbox route is alive.' \
Comment on lines +169 to +173
superseded=$(jq -r \
--argjson keep30 "${pr_30:-0}" \
--argjson keep31 "${pr_31:-0}" \
'[.[].number | select(. != $keep30 and . != $keep31)] | join(" ")' \
<<<"$open_prs")
# Removed top-level path key, e.g. ` "/repos/{owner}/{repo}":`
paths=$(count '^- "/')
# Removed enum member, e.g. ` - archived`
enums=$(count '^-[[:space:]]+- [A-Za-z0-9_.-]+$')
[ -n "$ref" ] || continue

git fetch --no-tags --filter=blob:none origin "$ref":"refs/remotes/origin/$ref"
diff=$(git diff "$base...origin/$ref" -- "$file" || true)
for pair in "$PR_30:$REF_30" "$PR_31:$REF_31"; do
pr="${pair%%:*}"; ref="${pair#*:}"
[ -n "$pr" ] && [ -n "$ref" ] || continue
git fetch --no-tags origin "$ref":"refs/remotes/origin/$ref" --filter=blob:none
Comment on lines +328 to +330
if: >-
steps.preflight.outcome == 'success' &&
(failure() || steps.breaking.outputs.status == 'breaking')
Comment on lines +25 to +27
permissions:
contents: write
pull-requests: write
@shawnHartsell

Copy link
Copy Markdown
Contributor Author

Aligned the breaking-change gate with the team's own definition and added the release-notes summary, per the first-responder runbook comparison.

1. Semantic breaking-change detection (replaces the grep heuristics)

The previous scan caught 3 of the 9 breaking changes listed in docs/creating-an-openapi-release.md. More importantly it was structurally incapable of catching some of them: it only inspected removed diff lines, and for a request body it is adding to required that breaks callers. No amount of grep tuning fixes that.

Replaced with .github/scripts/openapi-breaking-changes.py, which parses both descriptions and compares them semantically. It covers all 9 documented rules plus schema-removed and enum-value-removed:

Rule Direction
operation-removed removal
operationid-changed rename
url-parameter-renamed rename
requestbody-now-required addition
requestbody-required-added addition
response-field-removed removal
response-field-type-changed change
response-required-removed removal
schema-removed removal
enum-value-removed removal

Note the opposing directions on required: breaking to add on a request, breaking to remove on a response.

Cost is acceptable: parsing the 9.8 MB api.github.com.yaml takes ~1.8s via CSafeLoader, and a full comparison ~3.8s. PyYAML comes from the repo's existing hash-pinned requirements.txt, so no new dependency surface.

Validation. Against the real #6989 diff: 0 findings (no false positives), and an identical-file control also yields 0. Then each of the 10 rules was exercised by mutating a real description and confirming that rule - and only that rule - fires. That test caught a bug in my own test harness: the first requestbody-now-required probe targeted an operation whose requestBody was already required: true, so the mutation was a no-op. Re-run against an operation with a falsy required, it fires correctly.

2. Change summary posted before merge

creating-an-openapi-release.md generates release notes from PR descriptions and asks for a human-written summary before merge. Auto-merging untouched ":wave: humans." bodies would have quietly removed that input.

The scanner already computes exactly this, so the workflow now posts operation/schema counts, added operations, and any breaking findings as a PR comment before merging. This should make release-note prep better than the status quo, where the step is skipped in practice.

3. Alignment with the FR runbook

This automates step 6 (openapi-pr-reviewer) of the first-responder runbook. Deliberately keeping it continuous rather than mirroring the runbook's Tuesday/Thursday cadence: continuous processing means the backlog never accumulates (there are currently 34 superseded PRs open, which is the cadence's cost). The safety properties the FR provided by hand are preserved - lint green, breaking-change scan clean, change summary posted, human notified on any exception.

Companion PR github/api-platform#9851 updates workflow.md (which currently states these "need to be merged manually by a human") and rescopes FR step 6 to exception handling.

Still a gap

workflow.md notes the bot opens PRs on both this repo and github/docs-internal, and both must be merged. Only this side is automated, so docs.github.com could drift behind the OSS description. Out of scope here, but worth tracking - the automation makes the asymmetry more pronounced.

@shawnHartsell
shawnHartsell marked this pull request as draft August 14, 2026 16:40
@shawnHartsell

Copy link
Copy Markdown
Contributor Author

Converting to draft so we can work through how to detect locks for GHES releases

@shawnHartsell

Copy link
Copy Markdown
Contributor Author

Added GHES release-candidate freeze detection.

The signal

The key observation is that step 1 of the sequence is what creates the evidence here. Setting published: true in github/github is precisely what makes the new GHES version's descriptions start appearing in the bot's PRs. So "this PR introduces a ghes-<version> directory that is not yet on the default branch" is a direct consequence of the freeze trigger, not a coincidence.

That gives a detector needing no cross-repo polling, no Slack integration, and no manual state:

Version published=true merged Descriptions land here docs-internal RC merged
3.21 May 8 May 12 19:25 May 12 19:43
3.22 Aug 7 Aug 14 00:08 Aug 11 18:31

It is also self-clearing: once the version is on main, later PRs stop matching and merges resume with no all-clear action required.

Note the 3.21 row: the descriptions landed 18 minutes before the docs RC merged. That is the entire freeze window, and it is why this is evaluated on every run rather than cached.

Two signals

1. New GHES version (automatic). Compares ghes-* directories in descriptions/ and descriptions-next/ on the PR against the default branch. Costs one contents API call per ref (~0.3s), no checkout.

2. merge-freeze label (manual). An open issue with this label holds merges unconditionally. This is the escape hatch for a hold that isn't tied to a new GHES version, since step 2 of the sequence is a Slack message the workflow cannot see. I've created the label on the repo so it works immediately.

Behaviour when frozen

Merging, closing superseded PRs, and the breaking-change scan are all gated. The workflow posts a notice on the affected PRs explaining why it's holding and pings #api-platform once - deduplicated via a marker in the PR comment, since a 2-hourly job across a multi-day RC window would otherwise spam the channel. A workspace file would not have survived between runs, so the comment is the durable marker.

A freeze is reported as a distinct :snowflake: state, not a failure. It's an expected condition and shouldn't look like a broken workflow.

Validation

  • Replayed the real 3.22 event. Using the commit that introduced ghes-3.22 and its parent as simulated PR/main: the guard fires with descriptions introduces new GHES version(s): ghes-3.22. It would have correctly held that merge.
  • Self-clearing confirmed: comparing post-merge state against itself yields no findings, so merges resume automatically.
  • Current state: no freeze detected (3.22 is now on main), so this does not block today's queue.
  • Label branch: verified gh label list --search returns empty with exit 0 for a missing label (degrades safely), and exercised the full path against an existing label to confirm issue lookup works.

Companion doc update in github/api-platform#9851 covers the sequence, the detection, and the manual override.

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.

2 participants