Wheels migration with foreign key fails on Linux. No function with name addForeignKeyOptions #2171
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Wheels Bot — Reviewer A | |
| # Two trigger paths into this workflow: | |
| # 1. pull_request — the standard path (PR opens, syncs, or marks ready). | |
| # Reviewer A submits its initial review of the diff via /review-pr. | |
| # 2. issue_comment — the convergence-loop path. When Reviewer B posts a | |
| # not-yet-aligned critique (matches `wheels-bot:review-b:` but NOT | |
| # `wheels-bot:converged-` and NOT `:terminal`), A responds via | |
| # /respond-to-critique. The response is itself a review (state=COMMENT), | |
| # which triggers Reviewer B's next round — loop continues until B | |
| # emits a converged-* marker or the round cap fires. | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, ready_for_review] | |
| branches: [develop] | |
| issue_comment: | |
| types: [created] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: wheels-bot-review-a-${{ github.event.pull_request.number || github.event.issue.number }} | |
| cancel-in-progress: false | |
| jobs: | |
| review: | |
| name: Reviewer A | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Initial review: bot PRs (even draft) OR human ready-for-review PRs. | |
| # Response: bot's own comment from Reviewer B that is NOT a converged | |
| # marker AND NOT terminal — i.e., B is signalling "more discussion | |
| # needed." | |
| # The bot-identity check on the comment path is load-bearing: prevents | |
| # humans from quoting a marker in a reply to trigger the response. | |
| if: | | |
| vars.WHEELS_BOT_ENABLED == 'true' | |
| && ( | |
| (github.event_name == 'pull_request' | |
| && github.event.pull_request.user.login != 'dependabot[bot]' | |
| && (github.event.pull_request.user.login == 'wheels-bot[bot]' | |
| || github.event.pull_request.draft == false)) | |
| || (github.event_name == 'issue_comment' | |
| && github.event.comment.user.login == 'wheels-bot[bot]' | |
| && contains(github.event.comment.body, 'wheels-bot:review-b:') | |
| && !contains(github.event.comment.body, 'wheels-bot:converged-') | |
| && !contains(github.event.comment.body, ':terminal')) | |
| ) | |
| steps: | |
| - name: Generate App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.WHEELS_BOT_APP_ID }} | |
| private-key: ${{ secrets.WHEELS_BOT_PRIVATE_KEY }} | |
| - name: Resolve PR info | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| PR_FROM_PR_EVENT: ${{ github.event.pull_request.number }} | |
| PR_FROM_COMMENT_EVENT: ${{ github.event.issue.number }} | |
| # Body of the comment that triggered response mode (Reviewer B's | |
| # critique). Passed through env — never interpolate event text | |
| # straight into the script (injection). Empty on pull_request events. | |
| COMMENT_BODY: ${{ github.event.comment.body }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then | |
| pr_num="$PR_FROM_PR_EVENT" | |
| sha="${{ github.event.pull_request.head.sha }}" | |
| mode="initial" | |
| else | |
| pr_num="$PR_FROM_COMMENT_EVENT" | |
| # Validate numeric. | |
| if ! [[ "$pr_num" =~ ^[0-9]+$ ]]; then | |
| echo "::error::Invalid PR number from issue context: $pr_num" | |
| exit 1 | |
| fi | |
| # Anchor the response to the exact commit Reviewer B critiqued — not | |
| # the PR's (possibly newer) head. The triggering comment is B's | |
| # critique, whose marker carries that SHA: | |
| # `wheels-bot:review-b:<pr>:<sha>:<round>`. Re-deriving the head via | |
| # `gh pr view` would float to the current head and diverge from B's | |
| # commit_id-anchored marker once a push lands mid-loop, leaving the | |
| # response unable to match B's round (#2848). COMMENT_BODY is read | |
| # from env, never spliced into the script as a workflow expression. | |
| sha=$(printf '%s\n' "$COMMENT_BODY" \ | |
| | grep -oiP "wheels-bot:review-b:${pr_num}:\K[0-9a-f]{7,40}" \ | |
| | head -1 || true) | |
| if [ -z "$sha" ]; then | |
| echo "::error::Could not extract the reviewed SHA from the triggering review-b comment" | |
| exit 1 | |
| fi | |
| mode="response" | |
| fi | |
| echo "pr_num=$pr_num" >> "$GITHUB_OUTPUT" | |
| echo "sha=$sha" >> "$GITHUB_OUTPUT" | |
| echo "mode=$mode" >> "$GITHUB_OUTPUT" | |
| - name: Checkout PR head | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ steps.pr.outputs.sha }} | |
| fetch-depth: 0 | |
| - name: Skip check (initial review only — response mode handles its own idempotency) | |
| if: github.event_name == 'pull_request' | |
| id: gate | |
| uses: ./.github/actions/wheels-bot-skip-check | |
| with: | |
| target-type: pr | |
| target-number: ${{ steps.pr.outputs.pr_num }} | |
| # No trailing colon: the initial review marker is | |
| # `wheels-bot:review-a:<pr>:<sha>` with no suffix. Adding a trailing | |
| # `:` made this a no-op gate (issue #2558). Response markers use the | |
| # distinct `wheels-bot:review-a-response:` prefix and won't false-match. | |
| marker-pattern: 'wheels-bot:review-a:${{ steps.pr.outputs.pr_num }}:${{ steps.pr.outputs.sha }}' | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| - name: Determine prompt | |
| id: cmd | |
| run: | | |
| # Thread the SHA resolved in "Resolve PR info" — the exact commit the | |
| # Checkout step pinned — into the prompt as a second argument. The | |
| # model emits the idempotency marker from this value instead of | |
| # re-deriving it with `gh pr view`, which races with pushes that land | |
| # mid-session and left the marker pointing at the wrong commit | |
| # (issue #2848). The Run Reviewer A step's Bash allowlist is gh + | |
| # read-only git (no echo/printenv), so the model can't read a step | |
| # env var — the SHA must travel in the prompt text, the same channel | |
| # the PR number already uses. | |
| if [ "${{ steps.pr.outputs.mode }}" = "initial" ]; then | |
| echo "cmd=/review-pr ${{ steps.pr.outputs.pr_num }} ${{ steps.pr.outputs.sha }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "cmd=/respond-to-critique ${{ steps.pr.outputs.pr_num }} ${{ steps.pr.outputs.sha }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Run Reviewer A | |
| if: github.event_name == 'issue_comment' || steps.gate.outputs.skip == 'false' | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| # Allows the App's bot identity (and github-actions[bot] for | |
| # consistency). Reviewer A runs on bot's own PRs and responds | |
| # to its own Reviewer B comments — both legitimately bot-driven. | |
| allowed_bots: 'wheels-bot[bot],github-actions[bot]' | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| github_token: ${{ steps.app-token.outputs.token }} | |
| prompt: | | |
| ${{ steps.cmd.outputs.cmd }} | |
| claude_args: | | |
| --model claude-sonnet-4-6 | |
| --max-turns 250 | |
| --allowedTools "Bash(gh:*),Bash(git log:*),Bash(git diff:*),Bash(git show:*),Bash(git grep:*),Bash(git status),Read,Grep,Glob" | |
| # Post-submission guard (issue #2558). Reviewer A is trusted to issue a | |
| # single, substantive `gh pr review` per session. When the model misbehaves | |
| # — e.g. probing the CLI with `--body "test body"` before issuing the | |
| # real review — the placeholder leaks out as a public review. This step | |
| # scans wheels-bot reviews on the current SHA after the model exits and | |
| # auto-dismisses any that look like probes (too short, or missing the | |
| # canonical `wheels-bot:review-a` marker). Runs on `always()` so it still | |
| # fires when the Claude step itself failed mid-session. | |
| - name: Validate Reviewer A output | |
| if: always() && (github.event_name == 'issue_comment' || steps.gate.outputs.skip == 'false') | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| PR_NUMBER: ${{ steps.pr.outputs.pr_num }} | |
| HEAD_SHA: ${{ steps.pr.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| # Allow-list APPROVED and CHANGES_REQUESTED only: those are the | |
| # exclusive set of states that (a) GitHub's dismiss API accepts and | |
| # (b) gate merging. Feeding the dismiss API any other state returns | |
| # HTTP 422 — e.g. "Can not dismiss a commented pull request review" | |
| # for COMMENTED, the same shape for PENDING — which would fail this | |
| # step and red-X the check. Originally observed on PR #2795 commit | |
| # 0db188a5 when a COMMENTED placeholder leaked through. | |
| reviews=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate \ | |
| | jq -c --arg sha "$HEAD_SHA" \ | |
| '[.[] | select(.user.login == "wheels-bot[bot]") | select(.commit_id == $sha) | select(.state == "APPROVED" or .state == "CHANGES_REQUESTED")]') | |
| count=$(echo "$reviews" | jq 'length') | |
| if [[ "$count" == "0" ]]; then | |
| echo "::notice::No active wheels-bot reviews on ${HEAD_SHA} to validate" | |
| exit 0 | |
| fi | |
| # Dismissal criteria: body must be (a) >= 200 chars AND (b) contain | |
| # `wheels-bot:review-a` as a substring. Both conditions are required — | |
| # a long body without the marker still gets dismissed, because a | |
| # marker-less review breaks downstream idempotency (the skip-check | |
| # action greps for the marker on subsequent runs) and is itself a | |
| # signal that the prompt was not followed. Belt-and-suspenders: the | |
| # guard treats prompt compliance as load-bearing, not advisory. | |
| dismissed=0 | |
| while IFS= read -r row; do | |
| id=$(echo "$row" | jq -r '.id') | |
| body=$(echo "$row" | jq -r '.body') | |
| body_len=${#body} | |
| if [[ "$body_len" -lt 200 ]] || ! grep -q 'wheels-bot:review-a' <<<"$body"; then | |
| echo "::warning::Dismissing bogus Reviewer A review id=${id} len=${body_len}" | |
| gh api -X PUT \ | |
| "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews/${id}/dismissals" \ | |
| -f message="Auto-dismissed by Reviewer A guard: body is shorter than 200 characters or missing the canonical \`wheels-bot:review-a\` marker. See wheels-dev/wheels#2558 for context." | |
| dismissed=$((dismissed + 1)) | |
| fi | |
| done < <(echo "$reviews" | jq -c '.[]') | |
| if [[ "$dismissed" -gt 0 ]]; then | |
| # Idempotency check: a manual workflow re-trigger combined with | |
| # new bogus reviews on the same SHA could otherwise produce | |
| # duplicate guard comments. Skip if a guard comment for this | |
| # exact PR + SHA already exists. | |
| guard_marker="wheels-bot:review-a-guard:${PR_NUMBER}:${HEAD_SHA}" | |
| existing=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \ | |
| | jq -r --arg m "$guard_marker" '[.[] | select(.body | contains($m))] | length') | |
| if [[ "$existing" == "0" ]]; then | |
| short_sha=${HEAD_SHA:0:7} | |
| gh pr comment "$PR_NUMBER" --body "## Wheels Bot — Reviewer A guard | |
| Detected and dismissed ${dismissed} bogus Reviewer A review(s) on commit \`${short_sha}\`. Cause: review body shorter than 200 characters or missing the canonical \`wheels-bot:review-a\` marker. See [wheels-dev/wheels#2558](https://github.com/wheels-dev/wheels/issues/2558) for context. | |
| <!-- ${guard_marker} -->" | |
| else | |
| echo "::notice::Guard comment already present for ${PR_NUMBER}@${HEAD_SHA}; skipping duplicate" | |
| fi | |
| fi |