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:-}\`" 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"