Files
openrouter-python-sdk-retry…/.github/workflows/auto-merge-speakeasy-pr.yaml
T
Christine Chen 0680f01bf7 fix: proceed on checkless regen PRs in wait_for_checks
Speakeasy regen PRs are opened with the workflow GITHUB_TOKEN, which by
design does not trigger downstream on: pull_request runs, so they carry
zero status checks. main has no required checks either, so gh pr merge
--auto no-ops and the direct-merge fallback calls wait_for_checks.

The empty-rollup arm only `continue`d, so TOTAL stayed 0 until the 600s
timeout and the function exit 1'd — failing auto-merge on every regen PR.
Add a 60s grace window for checks to register, then treat a persistently
empty rollup as "no checks to wait for" and return 0 so the direct squash
merge proceeds. Matches the fix already shipped in go-sdk #318.
2026-06-25 16:20:53 -04:00

226 lines
8.9 KiB
YAML

name: Auto-merge Speakeasy PR
# Called as a follow-up job after Speakeasy Generate (mode: pr). Merges the
# regen PR opened/updated in the same workflow run so sdk_publish.yaml can run.
# Prefer branch_name from Generate logs; fall back to run_started_at heuristics.
# Uses GH_DOCS_SYNC GitHub App token (not a PAT) so merges trigger downstream push.
on:
workflow_call:
inputs:
run_started_at:
description: ISO timestamp when the parent Generate workflow run started
required: true
type: string
branch_name:
description: Speakeasy regen branch from the parent Generate run (optional)
required: false
type: string
default: ""
secrets:
GH_DOCS_SYNC_APP_ID:
required: true
GH_DOCS_SYNC_APP_PRIVATE_KEY:
required: true
permissions:
contents: write
pull-requests: write
concurrency:
group: auto-merge-speakeasy
cancel-in-progress: false
jobs:
auto-merge:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App token
id: app-token
# actions/create-github-app-token@v3.2.0 (immutable SHA)
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
app-id: ${{ secrets.GH_DOCS_SYNC_APP_ID }}
private-key: ${{ secrets.GH_DOCS_SYNC_APP_PRIVATE_KEY }}
- name: Resolve Speakeasy regen PR
id: pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
REPO="${{ github.repository }}"
RUN_STARTED="${{ inputs.run_started_at }}"
BRANCH="${{ inputs.branch_name }}"
PR_JSON=$(gh pr list \
--repo "$REPO" \
--state open \
--search "head:speakeasy-sdk-regen-" \
--limit 500 \
--json number,updatedAt,title,labels,author,headRefName)
PR_NUM=""
if [ -n "$BRANCH" ]; then
CANDIDATE=$(echo "$PR_JSON" | jq -r --arg branch "$BRANCH" \
'[.[] | select(.headRefName == $branch)] | .[0] // empty')
if [ -n "$CANDIDATE" ] && echo "$CANDIDATE" | jq -e '
(.headRefName | startswith("speakeasy-sdk-regen-"))
and (.title | contains("🐝 Update SDK"))
and ([.labels[].name] | any(. == "patch" or . == "minor" or . == "major"))
and (.author.is_bot == true)
' > /dev/null; then
PR_NUM=$(echo "$CANDIDATE" | jq -r '.number')
echo "Resolved Speakeasy regen PR #$PR_NUM from branch $BRANCH"
else
echo "::warning::branch_name=$BRANCH did not match a valid open regen PR — falling back to run_started_at"
fi
fi
if [ -z "$PR_NUM" ]; then
PR_NUM=$(echo "$PR_JSON" | jq -r --arg started "$RUN_STARTED" '
[.[]
| select(.headRefName | startswith("speakeasy-sdk-regen-"))
| select(.title | contains("🐝 Update SDK"))
| select([.labels[].name] | any(. == "patch" or . == "minor" or . == "major"))
| select(.author.is_bot == true)
| select(.updatedAt >= $started)
] | sort_by(.updatedAt) | last | .number // empty')
fi
if [ -z "$PR_NUM" ]; then
echo "No Speakeasy regen PR for this Generate run — skipping auto-merge"
echo "::notice title=auto-merge-skipped::No qualifying Speakeasy regen PR to merge"
{
echo "## Auto-merge skipped"
echo "No qualifying \`speakeasy-sdk-regen-*\` PR was found."
echo "- branch_name input: \`${BRANCH:-<empty>}\`"
echo "- run_started_at: \`$RUN_STARTED\`"
} >> "$GITHUB_STEP_SUMMARY"
echo "pr_num=" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "pr_num=$PR_NUM" >> "$GITHUB_OUTPUT"
echo "$PR_JSON" > /tmp/speakeasy_pr_json.json
- name: Close superseded Speakeasy PRs
if: steps.pr.outputs.pr_num != ''
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
CURRENT_PR="${{ steps.pr.outputs.pr_num }}"
PR_JSON=$(cat /tmp/speakeasy_pr_json.json)
PRIOR_JSON=$(echo "$PR_JSON" | jq -r --arg current_pr "$CURRENT_PR" \
'[.[].number | select(tostring != $current_pr)] | .[]')
if [ -z "$PRIOR_JSON" ]; then
echo "No superseded Speakeasy PRs to close"
exit 0
fi
mapfile -t PRIOR <<< "$PRIOR_JSON"
echo "Closing ${#PRIOR[@]} superseded Speakeasy PR(s)"
for N in "${PRIOR[@]}"; do
gh pr close "$N" --repo "${{ github.repository }}" --delete-branch \
--comment "Superseded by newer Speakeasy SDK generation PR #${CURRENT_PR}" \
|| echo "::warning::Failed to close PR #$N (continuing)"
sleep 1
done
- name: Auto-merge Speakeasy PR
if: steps.pr.outputs.pr_num != ''
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
PR_NUM="${{ steps.pr.outputs.pr_num }}"
REPO="${{ github.repository }}"
wait_for_checks() {
local pr=$1
local timeout=600
local interval=15
local elapsed=0
echo "Waiting for PR #$pr checks (timeout ${timeout}s)..."
while [ "$elapsed" -lt "$timeout" ]; do
TOTAL=$(gh pr view "$pr" --repo "$REPO" --json statusCheckRollup --jq \
'.statusCheckRollup | length')
PENDING=$(gh pr view "$pr" --repo "$REPO" --json statusCheckRollup --jq \
'[.statusCheckRollup[]? | select(.status != "COMPLETED")] | length')
FAILED=$(gh pr view "$pr" --repo "$REPO" --json statusCheckRollup --jq \
'[.statusCheckRollup[]? | select(.status == "COMPLETED") | select(.conclusion != "SUCCESS" and .conclusion != "SKIPPED" and .conclusion != "NEUTRAL")] | length')
if [ "$TOTAL" -eq 0 ]; then
# GITHUB_TOKEN-authored regen PRs get no pull_request checks at all,
# so TOTAL stays 0 forever. Wait a short grace window in case checks
# are merely slow to register, then proceed rather than time out.
if [ "$elapsed" -ge 60 ]; then
echo "No checks registered after ${elapsed}s — proceeding (checkless PR)"
return 0
fi
echo "Checks not yet registered — waiting ${interval}s..."
sleep "$interval"
elapsed=$((elapsed + interval))
continue
fi
if [ "$PENDING" -eq 0 ]; then
if [ "$FAILED" -gt 0 ]; then
echo "::error::PR #$pr has failing checks — refusing direct merge"
gh pr checks "$pr" --repo "$REPO" || true
exit 1
fi
echo "All checks completed"
return 0
fi
echo "Checks pending ($PENDING) — waiting ${interval}s..."
sleep "$interval"
elapsed=$((elapsed + interval))
done
echo "::error::Timed out waiting for PR #$pr checks"
exit 1
}
STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq .state)
if [ "$STATE" != "OPEN" ]; then
echo "PR #$PR_NUM is $STATE — skipping merge"
exit 0
fi
MERGE_METHOD=""
# Prefer GitHub auto-merge when required checks exist; otherwise
# squash-merge directly after checks pass (sdk-release-prs.yaml pattern).
AUTO_MERGE_NOOP_PATTERN='is in clean status|protected branch rules|Branch does not have required protected branch rules'
if gh pr merge "$PR_NUM" --repo "$REPO" --squash --auto --delete-branch 2> /tmp/gh-merge.err; then
MERGE_METHOD="auto-merge queued"
echo "Auto-merge enabled for Speakeasy PR #$PR_NUM"
elif grep -qiE "$AUTO_MERGE_NOOP_PATTERN" /tmp/gh-merge.err; then
echo "Auto-merge not applicable — waiting for checks, then merging PR #$PR_NUM directly"
cat /tmp/gh-merge.err >&2
wait_for_checks "$PR_NUM"
gh pr merge "$PR_NUM" --repo "$REPO" --squash --delete-branch
MERGE_METHOD="direct squash (checks passed)"
else
echo "::error::Failed to enable auto-merge on Speakeasy PR #$PR_NUM"
cat /tmp/gh-merge.err >&2
exit 1
fi
echo "::notice title=auto-merge-pr::Merged Speakeasy PR #$PR_NUM ($MERGE_METHOD)"
{
echo "## Auto-merge complete"
echo "- PR: #$PR_NUM"
echo "- Method: $MERGE_METHOD"
echo "- Token: GH_DOCS_SYNC GitHub App"
} >> "$GITHUB_STEP_SUMMARY"