From 85f2f033072c6306bbfbfa6c05295449e0719c11 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Mon, 1 Jun 2026 21:52:49 -0700 Subject: [PATCH 1/3] Automate spec drift response: open regen PR instead of issue When upstream OpenAPI spec diverges, fetch the new spec, regenerate the client, and open a PR assigned to ionq/developer-tools rather than filing an issue for someone to do manually. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/spec-drift.yml | 63 ++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/.github/workflows/spec-drift.yml b/.github/workflows/spec-drift.yml index 30afff7..5db7861 100644 --- a/.github/workflows/spec-drift.yml +++ b/.github/workflows/spec-drift.yml @@ -1,4 +1,4 @@ -name: Spec drift check +name: Spec drift — auto regen PR on: schedule: @@ -6,19 +6,21 @@ on: workflow_dispatch: permissions: - contents: read - issues: write + contents: write + pull-requests: write jobs: - check: + regen: runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 15 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false + # persist-credentials required to push the regen branch + - uses: ./.github/actions/setup-uv + - run: uv sync --group regen - name: Fetch latest spec run: | + set -euo pipefail BASE_URL=$(jq -r '.servers[0].url' openapi.json) echo "BASE_URL=${BASE_URL}" >> "$GITHUB_ENV" curl -sf "${BASE_URL}/api-docs" -o /tmp/latest-spec.json @@ -26,27 +28,58 @@ jobs: id: drift run: | norm() { jq -S 'del(.info.description)' "$1"; } - if ! diff -u --label vendored --label upstream <(norm openapi.json) <(norm /tmp/latest-spec.json) > /tmp/spec.diff; then + if ! diff -u --label vendored --label upstream \ + <(norm openapi.json) <(norm /tmp/latest-spec.json) > /tmp/spec.diff; then echo "drifted=true" >> "$GITHUB_OUTPUT" fi - - name: Open or update issue + - name: Regenerate client + if: steps.drift.outputs.drifted == 'true' + run: | + set -euo pipefail + cp /tmp/latest-spec.json openapi.json + if [[ -f openapi-overlay.yaml ]]; then + uv run oas-patch overlay openapi.json openapi-overlay.yaml -o /tmp/patched-spec.json + else + cp openapi.json /tmp/patched-spec.json + fi + uv run openapi-python-client generate \ + --path /tmp/patched-spec.json \ + --meta none \ + --config openapi-python-client-config.yaml \ + --custom-template-path custom-templates \ + --output-path ionq_core \ + --overwrite + - name: Open or update PR if: steps.drift.outputs.drifted == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + set -euo pipefail + BRANCH="spec-drift/auto-regen" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + git add openapi.json ionq_core/ + git commit -m "Regenerate client for updated OpenAPI spec" + git push origin "$BRANCH" --force + { - echo "The spec at ${BASE_URL}/api-docs has diverged from the vendored openapi.json. Fetch the new spec and regenerate the client." - printf '\n
Diff (sorted, pretty-printed JSON)\n\n```diff\n' + echo "The spec at \`${BASE_URL}/api-docs\` has diverged from the vendored \`openapi.json\`. This PR fetches the new spec and regenerates the client." + printf '\n
Spec diff (sorted, pretty-printed JSON)\n\n```diff\n' head -c 60000 /tmp/spec.diff [[ $(wc -c < /tmp/spec.diff) -gt 60000 ]] && printf '\n... (truncated)\n' printf '```\n
\n' } > /tmp/body.md - existing=$(gh issue list --label spec-drift --state open --json number --jq '.[0].number // empty') + + existing=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty') if [[ -z "$existing" ]]; then - gh issue create \ - --title "OpenAPI spec has changed upstream" \ + gh pr create \ + --title "Regenerate client for updated OpenAPI spec" \ --body-file /tmp/body.md \ + --head "$BRANCH" \ + --base main \ + --reviewer ionq/developer-tools \ --label spec-drift else - gh issue edit "$existing" --body-file /tmp/body.md + gh pr edit "$existing" --body-file /tmp/body.md fi From c9a9df591ddac62d2bf4d025e34eee6d7135965a Mon Sep 17 00:00:00 2001 From: nate stemen Date: Mon, 24 Aug 2026 12:03:18 -0700 Subject: [PATCH 2/3] Trigger required checks on auto-regen PRs without extra credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pushes and PRs made with GITHUB_TOKEN never fire pull_request events, so the regen PRs from the spec-drift workflow would sit unmergeable: main's ruleset requires the CI, staleness, and CodeQL checks. Rather than introduce a GitHub App or PAT, dispatch the required workflows directly on the regen branch — workflow_dispatch is exempt from GitHub's event suppression for GITHUB_TOKEN. - Restore persist-credentials: false on checkout (fixes the zizmor artipacked finding failing the Audit workflows check) - Replace the hand-rolled git push + gh pr bash with peter-evans/create-pull-request, authenticated by plain GITHUB_TOKEN - Add workflow_dispatch triggers to ci.yml and generated.yml so spec-drift.yml can kick them off on the regen branch - Drop the explicit team-reviewers request (GITHUB_TOKEN cannot request team reviews); CODEOWNERS already assigns ionq/developer-tools CodeQL uses GitHub's default setup and cannot be dispatched; verify it reports on the first regen PR. Assisted-By: Claude Fable 5 --- .github/workflows/ci.yml | 3 ++ .github/workflows/generated.yml | 3 ++ .github/workflows/spec-drift.yml | 57 ++++++++++++++++++-------------- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35d2797..8033ea2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: push: branches: [main] pull_request: + # Dispatched by spec-drift.yml on auto-regen branches, whose GITHUB_TOKEN + # pushes cannot fire pull_request events. + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/generated.yml b/.github/workflows/generated.yml index 6a6c7d7..fe9c3b6 100644 --- a/.github/workflows/generated.yml +++ b/.github/workflows/generated.yml @@ -2,6 +2,9 @@ name: Generated code check on: pull_request: + # Dispatched by spec-drift.yml on auto-regen branches, whose GITHUB_TOKEN + # pushes cannot fire pull_request events. + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/spec-drift.yml b/.github/workflows/spec-drift.yml index 5db7861..52b214a 100644 --- a/.github/workflows/spec-drift.yml +++ b/.github/workflows/spec-drift.yml @@ -8,6 +8,7 @@ on: permissions: contents: write pull-requests: write + actions: write jobs: regen: @@ -15,7 +16,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - # persist-credentials required to push the regen branch + with: + persist-credentials: false - uses: ./.github/actions/setup-uv - run: uv sync --group regen - name: Fetch latest spec @@ -49,20 +51,10 @@ jobs: --custom-template-path custom-templates \ --output-path ionq_core \ --overwrite - - name: Open or update PR + - name: Compose PR body if: steps.drift.outputs.drifted == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail - BRANCH="spec-drift/auto-regen" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -B "$BRANCH" - git add openapi.json ionq_core/ - git commit -m "Regenerate client for updated OpenAPI spec" - git push origin "$BRANCH" --force - { echo "The spec at \`${BASE_URL}/api-docs\` has diverged from the vendored \`openapi.json\`. This PR fetches the new spec and regenerates the client." printf '\n
Spec diff (sorted, pretty-printed JSON)\n\n```diff\n' @@ -70,16 +62,31 @@ jobs: [[ $(wc -c < /tmp/spec.diff) -gt 60000 ]] && printf '\n... (truncated)\n' printf '```\n
\n' } > /tmp/body.md - - existing=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty') - if [[ -z "$existing" ]]; then - gh pr create \ - --title "Regenerate client for updated OpenAPI spec" \ - --body-file /tmp/body.md \ - --head "$BRANCH" \ - --base main \ - --reviewer ionq/developer-tools \ - --label spec-drift - else - gh pr edit "$existing" --body-file /tmp/body.md - fi + # Pushes and PR creation made with GITHUB_TOKEN never fire pull_request + # events, so the required checks are dispatched explicitly below — + # workflow_dispatch is exempt from that suppression. Team review comes + # from CODEOWNERS (GITHUB_TOKEN cannot request team reviewers itself). + - name: Open or update PR + if: steps.drift.outputs.drifted == 'true' + id: pr + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + branch: spec-drift/auto-regen + base: main + add-paths: | + openapi.json + ionq_core/ + commit-message: Regenerate client for updated OpenAPI spec + title: Regenerate client for updated OpenAPI spec + body-path: /tmp/body.md + labels: spec-drift + - name: Trigger required checks + if: steps.pr.outputs.pull-request-operation == 'created' || steps.pr.outputs.pull-request-operation == 'updated' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + # CodeQL runs via GitHub's default setup and has no dispatchable + # workflow file — confirm it reports on regen PRs. + gh workflow run ci.yml --ref spec-drift/auto-regen + gh workflow run generated.yml --ref spec-drift/auto-regen From dbad1e0f77325b877cb9823b7116962cb4c29082 Mon Sep 17 00:00:00 2001 From: nate stemen Date: Mon, 24 Aug 2026 14:20:53 -0700 Subject: [PATCH 3/3] Drop redundant set -euo pipefail from spec-drift steps GitHub Actions already runs unspecified run: steps with bash -e, and none of these steps contains a pipe or unset-variable hazard, so the explicit flags added nothing. Assisted-By: Claude Fable 5 --- .github/workflows/spec-drift.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/spec-drift.yml b/.github/workflows/spec-drift.yml index 43307a0..eddfeb8 100644 --- a/.github/workflows/spec-drift.yml +++ b/.github/workflows/spec-drift.yml @@ -22,7 +22,6 @@ jobs: - run: uv sync --group regen - name: Fetch latest spec run: | - set -euo pipefail BASE_URL=$(jq -r '.servers[0].url' openapi.json) echo "BASE_URL=${BASE_URL}" >> "$GITHUB_ENV" curl -sf "${BASE_URL}/api-docs" -o /tmp/latest-spec.json @@ -37,7 +36,6 @@ jobs: - name: Regenerate client if: steps.drift.outputs.drifted == 'true' run: | - set -euo pipefail cp /tmp/latest-spec.json openapi.json if [[ -f openapi-overlay.yaml ]]; then uv run oas-patch overlay openapi.json openapi-overlay.yaml -o /tmp/patched-spec.json @@ -54,7 +52,6 @@ jobs: - name: Compose PR body if: steps.drift.outputs.drifted == 'true' run: | - set -euo pipefail { echo "The spec at \`${BASE_URL}/api-docs\` has diverged from the vendored \`openapi.json\`. This PR fetches the new spec and regenerates the client." printf '\n
Spec diff (sorted, pretty-printed JSON)\n\n```diff\n' @@ -85,7 +82,6 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - set -euo pipefail # CodeQL runs via GitHub's default setup and has no dispatchable # workflow file — confirm it reports on regen PRs. gh workflow run ci.yml --ref spec-drift/auto-regen