11name : Auto-merge Speakeasy PR
22
3- # Speakeasy mode:pr opens PRs (speakeasy-sdk-regen-*) and labels them
4- # patch/minor/major. Merge automatically so sdk_publish.yaml can run.
3+ # Called as a follow-up job after Speakeasy Generate (mode: pr). Merges the
4+ # regen PR opened/updated in the same workflow run so sdk_publish.yaml can run.
5+ # Prefer branch_name from Generate logs; fall back to run_started_at heuristics.
6+ # Uses GH_DOCS_SYNC GitHub App token (not a PAT) so merges trigger downstream push.
57
68on :
7- pull_request :
8- types : [labeled, opened]
9+ workflow_call :
10+ inputs :
11+ run_started_at :
12+ description : ISO timestamp when the parent Generate workflow run started
13+ required : true
14+ type : string
15+ branch_name :
16+ description : Speakeasy regen branch from the parent Generate run (optional)
17+ required : false
18+ type : string
19+ default : " "
20+ secrets :
21+ GH_DOCS_SYNC_APP_ID :
22+ required : true
23+ GH_DOCS_SYNC_APP_PRIVATE_KEY :
24+ required : true
925
1026permissions :
1127 contents : write
@@ -17,18 +33,6 @@ concurrency:
1733
1834jobs :
1935 auto-merge :
20- if : |
21- github.event.sender.login == 'github-actions[bot]' &&
22- github.event.pull_request.user.login == 'github-actions[bot]' &&
23- startsWith(github.event.pull_request.head.ref, 'speakeasy-sdk-regen-') &&
24- contains(github.event.pull_request.title, '🐝 Update SDK') &&
25- (
26- (github.event.action == 'labeled' && contains(fromJSON('["patch", "minor", "major"]'), github.event.label.name)) ||
27- (github.event.action == 'opened' &&
28- (contains(github.event.pull_request.labels.*.name, 'patch') ||
29- contains(github.event.pull_request.labels.*.name, 'minor') ||
30- contains(github.event.pull_request.labels.*.name, 'major')))
31- )
3236 runs-on : ubuntu-latest
3337 steps :
3438 - name : Generate GitHub App token
@@ -39,21 +43,80 @@ jobs:
3943 app-id : ${{ secrets.GH_DOCS_SYNC_APP_ID }}
4044 private-key : ${{ secrets.GH_DOCS_SYNC_APP_PRIVATE_KEY }}
4145
42- - name : Close superseded Speakeasy PRs
46+ - name : Resolve Speakeasy regen PR
47+ id : pr
4348 env :
4449 GH_TOKEN : ${{ steps.app-token.outputs.token }}
4550 run : |
4651 set -euo pipefail
47- CURRENT_PR="${{ github.event.pull_request.number }}"
4852
49- PRIOR_JSON=$(gh pr list \
50- --repo "${{ github.repository }}" \
53+ REPO="${{ github.repository }}"
54+ RUN_STARTED="${{ inputs.run_started_at }}"
55+ BRANCH="${{ inputs.branch_name }}"
56+
57+ PR_JSON=$(gh pr list \
58+ --repo "$REPO" \
5159 --state open \
5260 --search "head:speakeasy-sdk-regen-" \
5361 --limit 500 \
54- --json number \
55- | jq -r --arg cur "$CURRENT_PR" \
56- '[.[].number | select(tostring != $cur)] | .[]')
62+ --json number,updatedAt,title,labels,author,headRefName)
63+
64+ PR_NUM=""
65+ if [ -n "$BRANCH" ]; then
66+ CANDIDATE=$(echo "$PR_JSON" | jq -r --arg branch "$BRANCH" \
67+ '[.[] | select(.headRefName == $branch)] | .[0] // empty')
68+ if [ -n "$CANDIDATE" ] && echo "$CANDIDATE" | jq -e '
69+ (.headRefName | startswith("speakeasy-sdk-regen-"))
70+ and (.title | contains("🐝 Update SDK"))
71+ and ([.labels[].name] | any(. == "patch" or . == "minor" or . == "major"))
72+ and (.author.is_bot == true)
73+ ' > /dev/null; then
74+ PR_NUM=$(echo "$CANDIDATE" | jq -r '.number')
75+ echo "Resolved Speakeasy regen PR #$PR_NUM from branch $BRANCH"
76+ else
77+ echo "::warning::branch_name=$BRANCH did not match a valid open regen PR — falling back to run_started_at"
78+ fi
79+ fi
80+
81+ if [ -z "$PR_NUM" ]; then
82+ PR_NUM=$(echo "$PR_JSON" | jq -r --arg started "$RUN_STARTED" '
83+ [.[]
84+ | select(.headRefName | startswith("speakeasy-sdk-regen-"))
85+ | select(.title | contains("🐝 Update SDK"))
86+ | select([.labels[].name] | any(. == "patch" or . == "minor" or . == "major"))
87+ | select(.author.is_bot == true)
88+ | select(.updatedAt >= $started)
89+ ] | sort_by(.updatedAt) | last | .number // empty')
90+ fi
91+
92+ if [ -z "$PR_NUM" ]; then
93+ echo "No Speakeasy regen PR for this Generate run — skipping auto-merge"
94+ echo "::notice title=auto-merge-skipped::No qualifying Speakeasy regen PR to merge"
95+ {
96+ echo "## Auto-merge skipped"
97+ echo "No qualifying \`speakeasy-sdk-regen-*\` PR was found."
98+ echo "- branch_name input: \`${BRANCH:-<empty>}\`"
99+ echo "- run_started_at: \`$RUN_STARTED\`"
100+ } >> "$GITHUB_STEP_SUMMARY"
101+ echo "pr_num=" >> "$GITHUB_OUTPUT"
102+ exit 0
103+ fi
104+
105+ echo "pr_num=$PR_NUM" >> "$GITHUB_OUTPUT"
106+ echo "$PR_JSON" > /tmp/speakeasy_pr_json.json
107+
108+ - name : Close superseded Speakeasy PRs
109+ if : steps.pr.outputs.pr_num != ''
110+ env :
111+ GH_TOKEN : ${{ steps.app-token.outputs.token }}
112+ run : |
113+ set -euo pipefail
114+
115+ CURRENT_PR="${{ steps.pr.outputs.pr_num }}"
116+ PR_JSON=$(cat /tmp/speakeasy_pr_json.json)
117+
118+ PRIOR_JSON=$(echo "$PR_JSON" | jq -r --arg current_pr "$CURRENT_PR" \
119+ '[.[].number | select(tostring != $current_pr)] | .[]')
57120
58121 if [ -z "$PRIOR_JSON" ]; then
59122 echo "No superseded Speakeasy PRs to close"
@@ -71,30 +134,92 @@ jobs:
71134 done
72135
73136 - name : Auto-merge Speakeasy PR
137+ if : steps.pr.outputs.pr_num != ''
74138 env :
75139 GH_TOKEN : ${{ steps.app-token.outputs.token }}
76140 run : |
77141 set -euo pipefail
78- PR_NUM="${{ github.event.pull_request.number }}"
142+
143+ PR_NUM="${{ steps.pr.outputs.pr_num }}"
79144 REPO="${{ github.repository }}"
80145
146+ wait_for_checks() {
147+ local pr=$1
148+ local timeout=600
149+ local interval=15
150+ local elapsed=0
151+
152+ echo "Waiting for PR #$pr checks (timeout ${timeout}s)..."
153+ while [ "$elapsed" -lt "$timeout" ]; do
154+ TOTAL=$(gh pr view "$pr" --repo "$REPO" --json statusCheckRollup --jq \
155+ '.statusCheckRollup | length')
156+ PENDING=$(gh pr view "$pr" --repo "$REPO" --json statusCheckRollup --jq \
157+ '[.statusCheckRollup[]? | select(.status != "COMPLETED")] | length')
158+ FAILED=$(gh pr view "$pr" --repo "$REPO" --json statusCheckRollup --jq \
159+ '[.statusCheckRollup[]? | select(.status == "COMPLETED") | select(.conclusion != "SUCCESS" and .conclusion != "SKIPPED" and .conclusion != "NEUTRAL")] | length')
160+
161+ if [ "$TOTAL" -eq 0 ]; then
162+ # GITHUB_TOKEN-authored regen PRs get no pull_request checks at all,
163+ # so TOTAL stays 0 forever. Wait a short grace window in case checks
164+ # are merely slow to register, then proceed rather than time out.
165+ if [ "$elapsed" -ge 60 ]; then
166+ echo "No checks registered after ${elapsed}s — proceeding (checkless PR)"
167+ return 0
168+ fi
169+ echo "Checks not yet registered — waiting ${interval}s..."
170+ sleep "$interval"
171+ elapsed=$((elapsed + interval))
172+ continue
173+ fi
174+
175+ if [ "$PENDING" -eq 0 ]; then
176+ if [ "$FAILED" -gt 0 ]; then
177+ echo "::error::PR #$pr has failing checks — refusing direct merge"
178+ gh pr checks "$pr" --repo "$REPO" || true
179+ exit 1
180+ fi
181+ echo "All checks completed"
182+ return 0
183+ fi
184+
185+ echo "Checks pending ($PENDING) — waiting ${interval}s..."
186+ sleep "$interval"
187+ elapsed=$((elapsed + interval))
188+ done
189+
190+ echo "::error::Timed out waiting for PR #$pr checks"
191+ exit 1
192+ }
193+
81194 STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq .state)
82195 if [ "$STATE" != "OPEN" ]; then
83196 echo "PR #$PR_NUM is $STATE — skipping merge"
84197 exit 0
85198 fi
86199
200+ MERGE_METHOD=""
87201 # Prefer GitHub auto-merge when required checks exist; otherwise
88- # squash-merge directly (same pattern as sdk-release-prs.yaml).
202+ # squash-merge directly after checks pass ( sdk-release-prs.yaml pattern ).
89203 AUTO_MERGE_NOOP_PATTERN='is in clean status|protected branch rules|Branch does not have required protected branch rules'
90204 if gh pr merge "$PR_NUM" --repo "$REPO" --squash --auto --delete-branch 2> /tmp/gh-merge.err; then
205+ MERGE_METHOD="auto-merge queued"
91206 echo "Auto-merge enabled for Speakeasy PR #$PR_NUM"
92207 elif grep -qiE "$AUTO_MERGE_NOOP_PATTERN" /tmp/gh-merge.err; then
93- echo "Auto-merge not applicable — merging PR #$PR_NUM directly"
208+ echo "Auto-merge not applicable — waiting for checks, then merging PR #$PR_NUM directly"
94209 cat /tmp/gh-merge.err >&2
210+ wait_for_checks "$PR_NUM"
95211 gh pr merge "$PR_NUM" --repo "$REPO" --squash --delete-branch
212+ MERGE_METHOD="direct squash (checks passed)"
96213 else
97214 echo "::error::Failed to enable auto-merge on Speakeasy PR #$PR_NUM"
98215 cat /tmp/gh-merge.err >&2
99216 exit 1
100217 fi
218+
219+ echo "::notice title=auto-merge-pr::Merged Speakeasy PR #$PR_NUM ($MERGE_METHOD)"
220+ {
221+ echo "## Auto-merge complete"
222+ echo "- PR: #$PR_NUM"
223+ echo "- Method: $MERGE_METHOD"
224+ echo "- Token: GH_DOCS_SYNC GitHub App"
225+ } >> "$GITHUB_STEP_SUMMARY"
0 commit comments