From 5f58e60b5c995cc6e57728f506da3edb22e22bfd Mon Sep 17 00:00:00 2001 From: Christine Chen <10511452+christineschen@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:04:26 -0400 Subject: [PATCH] fix: re-enable Speakeasy auto-merge with GitHub App auth Replace GH_TOKEN PAT (blocked by enterprise 366-day policy) with short-lived GH_DOCS_SYNC app tokens, matching openrouter-web #25517. Re-enables auto-merge disabled on main (SDK-466), retries branch_name log extraction, and passes run_started_at via resolve-branch job output. --- .../workflows/auto-merge-speakeasy-pr.yaml | 387 +++++++++--------- .github/workflows/dispatch-agent-bump.yaml | 13 +- .github/workflows/sdk_generation.yaml | 38 +- .../sdk_generation_for_spec_change.yaml | 38 +- 4 files changed, 251 insertions(+), 225 deletions(-) diff --git a/.github/workflows/auto-merge-speakeasy-pr.yaml b/.github/workflows/auto-merge-speakeasy-pr.yaml index e91462fae..43e039c27 100644 --- a/.github/workflows/auto-merge-speakeasy-pr.yaml +++ b/.github/workflows/auto-merge-speakeasy-pr.yaml @@ -3,6 +3,7 @@ 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: @@ -17,9 +18,10 @@ on: type: string default: "" secrets: - gh_token: - description: PAT for merge/close (triggers downstream push workflows); falls back to GITHUB_TOKEN - required: false + GH_DOCS_SYNC_APP_ID: + required: true + GH_DOCS_SYNC_APP_PRIVATE_KEY: + required: true permissions: contents: write @@ -33,202 +35,183 @@ jobs: auto-merge: runs-on: ubuntu-latest steps: - # TODO(SDK-466): re-enable the steps below once the GH PAT issues are - # resolved. The PAT is required so the merge commit triggers the - # downstream sdk_publish.yaml push workflow; with only GITHUB_TOKEN the - # publish trigger is suppressed. - - name: Auto-merge disabled - run: echo "Speakeasy regen PR resolution + auto-merge temporarily disabled pending GH PAT fix (SDK-466)" - - # - name: Resolve Speakeasy regen PR - # id: pr - # env: - # PAT: ${{ secrets.gh_token }} - # FALLBACK_TOKEN: ${{ github.token }} - # run: | - # set -euo pipefail - # if [ -n "$PAT" ]; then - # export GH_TOKEN="$PAT" - # else - # echo "::warning::gh_token secret not set — using GITHUB_TOKEN (Publish push trigger may be suppressed)" - # export GH_TOKEN="$FALLBACK_TOKEN" - # fi - # - # 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: - # PAT: ${{ secrets.gh_token }} - # FALLBACK_TOKEN: ${{ github.token }} - # run: | - # set -euo pipefail - # if [ -n "$PAT" ]; then - # export GH_TOKEN="$PAT" - # else - # export GH_TOKEN="$FALLBACK_TOKEN" - # fi - # - # 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: - # PAT: ${{ secrets.gh_token }} - # FALLBACK_TOKEN: ${{ github.token }} - # run: | - # set -euo pipefail - # if [ -n "$PAT" ]; then - # export GH_TOKEN="$PAT" - # else - # export GH_TOKEN="$FALLBACK_TOKEN" - # fi - # - # 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 - # 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: $([ -n "$PAT" ] && echo 'PAT (gh_token)' || echo 'GITHUB_TOKEN (fallback)')" - # } >> "$GITHUB_STEP_SUMMARY" + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + 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 + 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" diff --git a/.github/workflows/dispatch-agent-bump.yaml b/.github/workflows/dispatch-agent-bump.yaml index fc2ba3696..41d88a373 100644 --- a/.github/workflows/dispatch-agent-bump.yaml +++ b/.github/workflows/dispatch-agent-bump.yaml @@ -60,12 +60,19 @@ jobs: path: .last-dispatched-sdk key: dispatched-sdk-${{ steps.ver.outputs.version }} + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.GH_DOCS_SYNC_APP_ID }} + private-key: ${{ secrets.GH_DOCS_SYNC_APP_PRIVATE_KEY }} + owner: OpenRouterTeam + repositories: typescript-agent + - name: Dispatch to typescript-agent if: steps.dedupe.outputs.cache-hit != 'true' env: - # Cross-repo PAT. Needs contents:write on - # OpenRouterTeam/typescript-agent so the repository_dispatch is accepted. - GH_TOKEN: ${{ secrets.GH_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | set -euo pipefail gh api repos/OpenRouterTeam/typescript-agent/dispatches \ diff --git a/.github/workflows/sdk_generation.yaml b/.github/workflows/sdk_generation.yaml index 29a8a530f..f68da219e 100644 --- a/.github/workflows/sdk_generation.yaml +++ b/.github/workflows/sdk_generation.yaml @@ -45,6 +45,7 @@ jobs: runs-on: ubuntu-latest outputs: branch_name: ${{ steps.branch.outputs.branch_name }} + run_started_at: ${{ steps.branch.outputs.run_started_at }} steps: - name: Extract branch from Generate logs id: branch @@ -55,6 +56,13 @@ jobs: REPO="${{ github.repository }}" RUN_ID="${{ github.run_id }}" + RUN_STARTED="${{ github.run_started_at }}" + if [ -z "$RUN_STARTED" ]; then + RUN_STARTED=$(gh run view "$RUN_ID" --repo "$REPO" --json startedAt --jq -r .startedAt) + echo "Resolved run_started_at from API: $RUN_STARTED" + fi + echo "run_started_at=$RUN_STARTED" >> "$GITHUB_OUTPUT" + # Full-run logs are unavailable while the workflow is in progress; # fetch logs from the completed generate job instead. JOB_ID=$(gh run view "$RUN_ID" --repo "$REPO" --json jobs --jq \ @@ -64,14 +72,23 @@ jobs: if [ -z "$JOB_ID" ]; then echo "::warning::Could not find generate job id — auto-merge will use run_started_at fallback" else - BRANCH=$(gh run view "$RUN_ID" --repo "$REPO" --job "$JOB_ID" --log 2>/dev/null \ - | grep -oE 'branch_name=speakeasy-sdk-regen-[0-9]+' | tail -1 | cut -d= -f2 || true) - fi - - if [ -z "$BRANCH" ]; then - echo "::warning::Could not extract branch_name from Generate logs — auto-merge will use run_started_at fallback" - else - echo "Extracted branch_name=$BRANCH" + MAX_ATTEMPTS=12 + INTERVAL=10 + for attempt in $(seq 1 "$MAX_ATTEMPTS"); do + BRANCH=$(gh run view "$RUN_ID" --repo "$REPO" --job "$JOB_ID" --log 2>/dev/null \ + | grep -oE 'branch_name=speakeasy-sdk-regen-[0-9]+' | tail -1 | cut -d= -f2 || true) + if [ -n "$BRANCH" ]; then + echo "Extracted branch_name=$BRANCH (attempt $attempt)" + break + fi + if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then + echo "Job logs not ready — waiting ${INTERVAL}s (attempt $attempt/$MAX_ATTEMPTS)..." + sleep "$INTERVAL" + fi + done + if [ -z "$BRANCH" ]; then + echo "::warning::Could not extract branch_name from Generate logs after ${MAX_ATTEMPTS} attempts — auto-merge will use run_started_at fallback" + fi fi echo "branch_name=$BRANCH" >> "$GITHUB_OUTPUT" @@ -80,7 +97,8 @@ jobs: if: ${{ !cancelled() && needs.generate.result == 'success' }} uses: ./.github/workflows/auto-merge-speakeasy-pr.yaml with: - run_started_at: ${{ github.run_started_at }} + run_started_at: ${{ needs.resolve-branch.outputs.run_started_at }} branch_name: ${{ needs.resolve-branch.outputs.branch_name }} secrets: - gh_token: ${{ secrets.GH_TOKEN }} + GH_DOCS_SYNC_APP_ID: ${{ secrets.GH_DOCS_SYNC_APP_ID }} + GH_DOCS_SYNC_APP_PRIVATE_KEY: ${{ secrets.GH_DOCS_SYNC_APP_PRIVATE_KEY }} diff --git a/.github/workflows/sdk_generation_for_spec_change.yaml b/.github/workflows/sdk_generation_for_spec_change.yaml index 1284d7337..70c3c7930 100644 --- a/.github/workflows/sdk_generation_for_spec_change.yaml +++ b/.github/workflows/sdk_generation_for_spec_change.yaml @@ -46,6 +46,7 @@ jobs: runs-on: ubuntu-latest outputs: branch_name: ${{ steps.branch.outputs.branch_name }} + run_started_at: ${{ steps.branch.outputs.run_started_at }} steps: - name: Extract branch from Generate logs id: branch @@ -56,6 +57,13 @@ jobs: REPO="${{ github.repository }}" RUN_ID="${{ github.run_id }}" + RUN_STARTED="${{ github.run_started_at }}" + if [ -z "$RUN_STARTED" ]; then + RUN_STARTED=$(gh run view "$RUN_ID" --repo "$REPO" --json startedAt --jq -r .startedAt) + echo "Resolved run_started_at from API: $RUN_STARTED" + fi + echo "run_started_at=$RUN_STARTED" >> "$GITHUB_OUTPUT" + # Full-run logs are unavailable while the workflow is in progress; # fetch logs from the completed generate job instead. JOB_ID=$(gh run view "$RUN_ID" --repo "$REPO" --json jobs --jq \ @@ -65,14 +73,23 @@ jobs: if [ -z "$JOB_ID" ]; then echo "::warning::Could not find generate job id — auto-merge will use run_started_at fallback" else - BRANCH=$(gh run view "$RUN_ID" --repo "$REPO" --job "$JOB_ID" --log 2>/dev/null \ - | grep -oE 'branch_name=speakeasy-sdk-regen-[0-9]+' | tail -1 | cut -d= -f2 || true) - fi - - if [ -z "$BRANCH" ]; then - echo "::warning::Could not extract branch_name from Generate logs — auto-merge will use run_started_at fallback" - else - echo "Extracted branch_name=$BRANCH" + MAX_ATTEMPTS=12 + INTERVAL=10 + for attempt in $(seq 1 "$MAX_ATTEMPTS"); do + BRANCH=$(gh run view "$RUN_ID" --repo "$REPO" --job "$JOB_ID" --log 2>/dev/null \ + | grep -oE 'branch_name=speakeasy-sdk-regen-[0-9]+' | tail -1 | cut -d= -f2 || true) + if [ -n "$BRANCH" ]; then + echo "Extracted branch_name=$BRANCH (attempt $attempt)" + break + fi + if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then + echo "Job logs not ready — waiting ${INTERVAL}s (attempt $attempt/$MAX_ATTEMPTS)..." + sleep "$INTERVAL" + fi + done + if [ -z "$BRANCH" ]; then + echo "::warning::Could not extract branch_name from Generate logs after ${MAX_ATTEMPTS} attempts — auto-merge will use run_started_at fallback" + fi fi echo "branch_name=$BRANCH" >> "$GITHUB_OUTPUT" @@ -81,7 +98,8 @@ jobs: if: ${{ !cancelled() && needs.generate.result == 'success' }} uses: ./.github/workflows/auto-merge-speakeasy-pr.yaml with: - run_started_at: ${{ github.run_started_at }} + run_started_at: ${{ needs.resolve-branch.outputs.run_started_at }} branch_name: ${{ needs.resolve-branch.outputs.branch_name }} secrets: - gh_token: ${{ secrets.GH_TOKEN }} + GH_DOCS_SYNC_APP_ID: ${{ secrets.GH_DOCS_SYNC_APP_ID }} + GH_DOCS_SYNC_APP_PRIVATE_KEY: ${{ secrets.GH_DOCS_SYNC_APP_PRIVATE_KEY }}