feat: marketing pipeline (drip emails, twitter, PostHog tracking) #800
Workflow file for this run
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: Sentry Auto-Fix | |
| on: | |
| issues: | |
| types: [labeled] | |
| workflow_dispatch: | |
| inputs: | |
| issue_number: | |
| description: 'Issue number to triage' | |
| required: true | |
| pull_request_review: | |
| types: [submitted] | |
| issue_comment: | |
| types: [created] | |
| schedule: | |
| - cron: '0 */2 * * *' | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| id-token: write | |
| jobs: | |
| triage-and-fix: | |
| concurrency: | |
| group: sentry-triage-${{ github.event.issue.number || github.event.inputs.issue_number }} | |
| cancel-in-progress: false | |
| if: > | |
| ( | |
| (github.event_name == 'issues' && | |
| github.event.action == 'labeled' && | |
| contains(github.event.issue.labels.*.name, 'sentry') && | |
| !contains(github.event.issue.labels.*.name, 'needs-human')) | |
| || | |
| github.event_name == 'workflow_dispatch' | |
| ) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Get issue info | |
| id: issue_info | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| ISSUE_NUM="${{ github.event.inputs.issue_number }}" | |
| else | |
| ISSUE_NUM="${{ github.event.issue.number }}" | |
| fi | |
| echo "number=$ISSUE_NUM" >> $GITHUB_OUTPUT | |
| ISSUE_DATA=$(gh issue view "$ISSUE_NUM" --repo ${{ github.repository }} --json title,body) | |
| echo "title=$(echo "$ISSUE_DATA" | jq -r '.title')" >> $GITHUB_OUTPUT | |
| # Save body to file to avoid escaping issues | |
| echo "$ISSUE_DATA" | jq -r '.body' > /tmp/issue-body.txt | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: develop | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 18 | |
| - name: Install backend deps | |
| run: cd run && npm install | |
| - name: Install frontend deps | |
| run: yarn install | |
| - uses: superfly/flyctl-actions/setup-flyctl@master | |
| - name: Setup SSH | |
| uses: webfactory/ssh-agent@v0.9.0 | |
| with: | |
| ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| - name: Add known hosts | |
| run: | | |
| mkdir -p ~/.ssh | |
| ssh-keyscan -H 157.90.154.200 >> ~/.ssh/known_hosts | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Check for existing fix PR | |
| id: existing_pr | |
| run: | | |
| EXISTING_PR=$(gh pr list --repo ${{ github.repository }} --head "fix/sentry-${{ steps.issue_info.outputs.number }}" --state open --json number -q '.[0].number' 2>/dev/null || true) | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "PR #$EXISTING_PR already exists for this issue, skipping" | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Notify dashboard - triaging | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| curl -s -X POST "${{ secrets.APP_URL }}/webhooks/github-actions" \ | |
| -H "Authorization: Bearer ${{ secrets.ETHERNAL_WEBHOOK_SECRET }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"workflowRunId\": ${{ github.run_id }}, | |
| \"githubIssueNumber\": ${{ steps.issue_info.outputs.number }}, | |
| \"status\": \"triaging\", | |
| \"currentStep\": \"Claude is investigating the error\" | |
| }" | |
| - name: Check for regression | |
| id: regression | |
| env: | |
| SENTRY_API_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Extract Sentry issue ID from issue body | |
| SENTRY_ISSUE_ID=$(grep -oP 'sentry\.tryethernal\.com/organizations/sentry/issues/\K\d+' /tmp/issue-body.txt | head -1) | |
| CONTEXT="" | |
| if [ -n "$SENTRY_ISSUE_ID" ]; then | |
| # Check if Sentry marks this as a regression | |
| IS_REGRESSION=$(curl -s \ | |
| "https://sentry.tryethernal.com/api/0/issues/$SENTRY_ISSUE_ID/" \ | |
| -H "Authorization: Bearer $SENTRY_API_TOKEN" | jq -r '.isRegression // false') | |
| if [ "$IS_REGRESSION" = "true" ]; then | |
| # Search for previous closed issues with the same Sentry issue ID | |
| PREV_ISSUES=$(gh issue list --label sentry --state closed --limit 20 --json number,title,body \ | |
| --jq "[.[] | select(.body | contains(\"issues/$SENTRY_ISSUE_ID/\")) | {number, title}]") | |
| # Find linked PRs for those issues | |
| PREV_PRS="" | |
| for ISSUE_NUM in $(echo "$PREV_ISSUES" | jq -r '.[].number'); do | |
| PR=$(gh pr list --state merged --search "Fixes #$ISSUE_NUM" --json number,title,url \ | |
| --jq '.[0] | "PR #\(.number): \(.title) (\(.url))"' 2>/dev/null) | |
| if [ -n "$PR" ]; then | |
| PREV_PRS="$PREV_PRS\n- $PR (fixed issue #$ISSUE_NUM)" | |
| fi | |
| done | |
| if [ -n "$PREV_PRS" ]; then | |
| CONTEXT="⚠️ **REGRESSION**: This error was previously fixed and has recurred. | |
| Previous fix attempts: | |
| $(echo -e "$PREV_PRS") | |
| Review the previous PRs to understand what was tried before. The previous fix was insufficient — identify WHY it regressed and ensure your new fix addresses the root cause more thoroughly." | |
| else | |
| CONTEXT="⚠️ **REGRESSION**: Sentry reports this error was previously resolved but has recurred. Search git history for related fixes." | |
| fi | |
| fi | |
| fi | |
| # Save to file to avoid shell escaping issues | |
| echo "$CONTEXT" > /tmp/regression-context.txt | |
| if [ -n "$CONTEXT" ]; then | |
| echo "is_regression=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "is_regression=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Start conversation streamer | |
| if: steps.existing_pr.outputs.exists != 'true' | |
| continue-on-error: true | |
| run: | | |
| chmod +x .github/scripts/stream-conversation.sh | |
| .github/scripts/stream-conversation.sh & | |
| echo $! > /tmp/streamer-pid | |
| env: | |
| WEBHOOK_URL: ${{ secrets.APP_URL }}/webhooks/github-actions | |
| WEBHOOK_SECRET: ${{ secrets.ETHERNAL_WEBHOOK_SECRET }} | |
| WORKFLOW_RUN_ID: ${{ github.run_id }} | |
| GITHUB_ISSUE_NUMBER: ${{ steps.issue_info.outputs.number }} | |
| - name: Claude Code - Triage and Fix | |
| if: steps.existing_pr.outputs.exists != 'true' | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| label_trigger: sentry | |
| allowed_bots: "ethernal-sentry[bot],claude[bot]" | |
| claude_args: "--model claude-sonnet-4-20250514 --max-turns 50 --allowedTools Bash,Read,Write,Edit,Glob,Grep" | |
| show_full_output: true | |
| prompt: | | |
| You are an autonomous error-fixing agent for the Ethernal project — an EVM block explorer. | |
| A Sentry error has been auto-reported as GitHub issue #${{ steps.issue_info.outputs.number }}. | |
| ## Issue Details | |
| Title: ${{ steps.issue_info.outputs.title }} | |
| Body: | |
| $(cat /tmp/issue-body.txt) | |
| ## Regression Context | |
| $(cat /tmp/regression-context.txt) | |
| ## Your Task | |
| ### 1. Investigate | |
| - Parse the Sentry issue from the body to extract error type, stack trace, and context | |
| - Use the Sentry API to fetch full event details: | |
| ``` | |
| curl -H "Authorization: Bearer $SENTRY_API_TOKEN" \ | |
| "https://sentry.tryethernal.com/api/0/issues/{sentry_issue_id}/events/latest/" | |
| ``` | |
| The Sentry issue ID may be in the issue body URL. The org slug is "sentry". | |
| Backend project slug: "ethernal-backend", Frontend project slug: "ethernal-frontend" | |
| - Check production logs if needed: `flyctl logs -a ethernal --no-tail | tail -100` | |
| - Query the read-only database if the error involves data issues: | |
| ``` | |
| psql "$PROD_DATABASE_URL_READONLY" -c "SELECT ..." | |
| ``` | |
| NEVER write to this database. SELECT queries only. | |
| ### 2. Triage (choose ONE action) | |
| **IMPORTANT**: If the issue already has the `needs-human` label, it was flagged as a sustained infrastructure error (high volume of connection errors outside of a deploy). In this case, ALWAYS choose **AUTO-FIX** — investigate the root cause (check DB health, connection pool stats, Fly.io status) and attempt a fix. Also notify @antoinedc: | |
| ``` | |
| gh issue comment ${{ steps.issue_info.outputs.number }} --body "🚨 Sustained infra errors detected (not correlated with a deploy). Investigating and attempting a fix. @antoinedc please monitor." | |
| ``` | |
| **CLOSE** the issue if: | |
| - It's a duplicate of an existing issue | |
| - It's expected behavior (e.g., user input validation errors) | |
| - It's a third-party/network error we can't control | |
| - It's already fixed on the develop branch | |
| - It's a transient infrastructure error (database connection drops, ECONNRESET, "Connection terminated unexpectedly", SequelizeConnectionError, SequelizeConnectionRefusedError) — BUT first verify it correlates with a recent deploy/restart by checking: `flyctl releases -a ethernal --json | jq '.[0:3]'`. If the error timestamps align with a release within the last 10 minutes, it's transient → close. If there's NO recent deploy and connection errors persist, ESCALATE instead — it may be a real infra issue. Do NOT add retry logic or try-catch wrappers for these — BullMQ already handles retries at the job level. | |
| Close with: `gh issue close ${{ steps.issue_info.outputs.number }} --comment "Closing: [reason]"` | |
| **ESCALATE** if: | |
| - It involves security, data corruption, or data loss | |
| - It touches billing, auth, Stripe, or payment code | |
| - The root cause is unclear after investigation | |
| - The fix would require architectural changes | |
| Escalate with: | |
| ``` | |
| gh issue edit ${{ steps.issue_info.outputs.number }} --add-label "needs-human" | |
| gh issue comment ${{ steps.issue_info.outputs.number }} --body "Escalating: [reason]. @antoinedc please review." | |
| ``` | |
| **AUTO-FIX** if: | |
| - It's a null/undefined check, missing error handling, off-by-one error | |
| - It's a simple logic bug with clear root cause | |
| - The fix is contained (1-3 files) and low risk | |
| ### 3. Fix (only if AUTO-FIX) | |
| - Before creating a branch, verify no PR already exists: | |
| ```bash | |
| if gh pr list --repo tryethernal/ethernal --head "fix/sentry-${{ steps.issue_info.outputs.number }}" --state open --json number -q '.[0].number' 2>/dev/null | grep -q .; then | |
| echo "A fix PR already exists for this issue. Exiting." | |
| exit 0 | |
| fi | |
| ``` | |
| - Create a branch: `git checkout -b fix/sentry-${{ steps.issue_info.outputs.number }}` | |
| - Implement the minimal fix needed | |
| - Run relevant tests: | |
| - Backend: `cd run && npm test -- --testPathPattern=<relevant_test>` | |
| - Frontend: `yarn test -- <relevant_test>` | |
| - If tests fail, fix them. If you can't fix them, escalate instead. | |
| - Commit with a clear message explaining the fix | |
| - Push: `git push origin fix/sentry-${{ steps.issue_info.outputs.number }}` | |
| - Create PR: | |
| ``` | |
| gh pr create \ | |
| --title "fix: [brief description]" \ | |
| --body "## Summary | |
| Fixes #${{ steps.issue_info.outputs.number }} | |
| **Sentry Error:** [error type and message] | |
| **Root Cause:** [what caused it] | |
| **Fix:** [what was changed and why] | |
| **Regression:** [If regression, link to previous PR and explain why the previous fix was insufficient] | |
| ## Test plan | |
| - [ ] Relevant unit tests pass | |
| - [ ] Fix addresses the root cause, not just symptoms | |
| 🤖 Generated with [Claude Code](https://claude.com/claude-code)" \ | |
| --base develop | |
| ``` | |
| ### Rules | |
| - NEVER modify files in: `run/api/stripe.js`, `run/webhooks/stripe.js`, `run/lib/stripe.js`, `run/middlewares/auth.js`, `run/lib/crypto.js` | |
| - NEVER write to the production database | |
| - NEVER skip running tests | |
| - NEVER close the issue manually — GitHub auto-closes it when the PR with "Fixes #N" is merged | |
| - Keep fixes minimal — don't refactor surrounding code | |
| - If uncertain about the fix, escalate rather than guess | |
| - NEVER add retry/reconnection logic for database connection errors — BullMQ already handles retries. Adding in-job retry creates double-retry complexity. | |
| - NEVER silently catch and swallow database errors with fallback values — this masks real issues. Let errors propagate so BullMQ retries the job. | |
| - NEVER split working database queries into multiple queries to "prevent timeouts" — timeouts are solved by indexes, not query splitting. | |
| - NEVER wrap entire job functions in try-catch just to log and re-throw — this adds no value. | |
| env: | |
| SENTRY_API_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} | |
| PROD_DATABASE_URL_READONLY: ${{ secrets.PROD_DATABASE_URL_READONLY }} | |
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | |
| - name: Stop conversation streamer | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| if [ -f /tmp/streamer-pid ]; then | |
| kill $(cat /tmp/streamer-pid) 2>/dev/null || true | |
| fi | |
| - name: Extract conversation log | |
| id: conversation | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| LOG_FILE="/home/runner/work/_temp/claude-execution-output.json" | |
| if [ ! -f "$LOG_FILE" ]; then | |
| echo "[]" > /tmp/conversation-log.json | |
| exit 0 | |
| fi | |
| python3 << 'PYEOF' | |
| import json | |
| with open("/home/runner/work/_temp/claude-execution-output.json") as f: | |
| data = json.load(f) | |
| log = [] | |
| messages = data if isinstance(data, list) else [data] | |
| for msg in messages: | |
| if not isinstance(msg, dict): | |
| continue | |
| role = msg.get("role", msg.get("type", "")) | |
| if role in ("system", "preset"): | |
| continue | |
| content = msg.get("content", []) | |
| if isinstance(content, str): | |
| log.append({"role": role, "text": content}) | |
| continue | |
| if not isinstance(content, list): | |
| content = [content] if isinstance(content, dict) else [] | |
| for block in content: | |
| if not isinstance(block, dict): | |
| continue | |
| btype = block.get("type", "") | |
| if btype == "text": | |
| log.append({"role": role, "text": block.get("text", "")}) | |
| elif btype == "tool_use": | |
| inp = block.get("input", {}) | |
| log.append({ | |
| "role": role, | |
| "tool": block.get("name", ""), | |
| "input": (inp.get("command") or inp.get("description") or str(inp))[:500] | |
| }) | |
| elif btype == "tool_result": | |
| cv = block.get("content", "") | |
| if isinstance(cv, list): | |
| cv = " ".join(b.get("text", "") for b in cv if isinstance(b, dict)) | |
| log.append({"role": "tool", "output": str(cv)[:1000]}) | |
| log = log[-200:] | |
| with open("/tmp/conversation-log.json", "w") as f: | |
| json.dump(log, f) | |
| print(f"Extracted {len(log)} conversation entries") | |
| PYEOF | |
| - name: Notify dashboard - post-triage | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| # Determine status based on whether a PR branch exists | |
| if git ls-remote --heads origin "fix/sentry-${{ steps.issue_info.outputs.number }}" | grep -q .; then | |
| STATUS="fixing" | |
| STEP="Claude created a fix branch" | |
| DECISION="auto-fix" | |
| else | |
| # Check if issue was closed | |
| ISSUE_STATE=$(gh issue view ${{ steps.issue_info.outputs.number }} --repo ${{ github.repository }} --json state -q '.state') | |
| if [ "$ISSUE_STATE" = "CLOSED" ]; then | |
| STATUS="closed" | |
| STEP="Issue was closed by Claude" | |
| DECISION="close" | |
| elif gh issue view ${{ steps.issue_info.outputs.number }} --repo ${{ github.repository }} --json labels -q '.labels[].name' | grep -q "needs-human"; then | |
| STATUS="escalated" | |
| STEP="Escalated to human" | |
| DECISION="escalate" | |
| else | |
| STATUS="failed" | |
| STEP="Triage completed without action" | |
| DECISION="unknown" | |
| fi | |
| fi | |
| # Build payload with conversation log | |
| CONV_LOG=$(cat /tmp/conversation-log.json 2>/dev/null || echo "[]") | |
| jq -n \ | |
| --argjson runId "${{ github.run_id }}" \ | |
| --argjson issueNum "${{ steps.issue_info.outputs.number }}" \ | |
| --arg status "$STATUS" \ | |
| --arg step "$STEP" \ | |
| --arg decision "$DECISION" \ | |
| --argjson convLog "$CONV_LOG" \ | |
| '{ | |
| workflowRunId: $runId, | |
| githubIssueNumber: $issueNum, | |
| status: $status, | |
| currentStep: $step, | |
| triageDecision: $decision, | |
| conversationLog: $convLog | |
| }' | curl -s -X POST "${{ secrets.APP_URL }}/webhooks/github-actions" \ | |
| -H "Authorization: Bearer ${{ secrets.ETHERNAL_WEBHOOK_SECRET }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d @- | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Notify dashboard - PR created | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| PR_NUMBER=$(gh pr list --head "fix/sentry-${{ steps.issue_info.outputs.number }}" --json number -q '.[0].number' 2>/dev/null) | |
| if [ -n "$PR_NUMBER" ]; then | |
| curl -s -X POST "${{ secrets.APP_URL }}/webhooks/github-actions" \ | |
| -H "Authorization: Bearer ${{ secrets.ETHERNAL_WEBHOOK_SECRET }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"workflowRunId\": ${{ github.run_id }}, | |
| \"githubIssueNumber\": ${{ steps.issue_info.outputs.number }}, | |
| \"githubPrNumber\": $PR_NUMBER, | |
| \"status\": \"reviewing\", | |
| \"currentStep\": \"PR #$PR_NUMBER awaiting review\" | |
| }" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Notify dashboard - failure | |
| if: failure() | |
| continue-on-error: true | |
| run: | | |
| curl -s -X POST "${{ secrets.APP_URL }}/webhooks/github-actions" \ | |
| -H "Authorization: Bearer ${{ secrets.ETHERNAL_WEBHOOK_SECRET }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"workflowRunId\": ${{ github.run_id }}, | |
| \"githubIssueNumber\": ${{ steps.issue_info.outputs.number }}, | |
| \"status\": \"failed\", | |
| \"currentStep\": \"Workflow failed\" | |
| }" | |
| process-review: | |
| concurrency: | |
| group: sentry-review-${{ github.event.issue.number || github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| if: > | |
| ( | |
| (github.event_name == 'pull_request_review' && | |
| startsWith(github.event.pull_request.head.ref, 'fix/sentry-')) | |
| || | |
| (github.event_name == 'issue_comment' && | |
| github.event.issue.pull_request && | |
| contains(github.event.comment.body, '@claude')) | |
| || | |
| (github.event_name == 'issue_comment' && | |
| github.event.issue.pull_request && | |
| github.event.sender.login == 'greptile-apps[bot]') | |
| ) && | |
| github.event.sender.login != 'github-actions[bot]' && | |
| github.event.sender.login != 'claude[bot]' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Debounce | |
| run: sleep 30 | |
| - name: Get PR info | |
| id: pr_info | |
| run: | | |
| if [ "${{ github.event_name }}" = "issue_comment" ]; then | |
| PR_NUMBER="${{ github.event.issue.number }}" | |
| BRANCH=$(gh pr view "$PR_NUMBER" --repo ${{ github.repository }} --json headRefName -q '.headRefName') | |
| else | |
| PR_NUMBER="${{ github.event.pull_request.number }}" | |
| BRANCH="${{ github.event.pull_request.head.ref }}" | |
| fi | |
| echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT | |
| echo "branch=$BRANCH" >> $GITHUB_OUTPUT | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check if sentry fix PR | |
| id: check_sentry | |
| run: | | |
| BRANCH="${{ steps.pr_info.outputs.branch }}" | |
| if [[ "$BRANCH" == fix/sentry-* ]]; then | |
| echo "is_sentry=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "is_sentry=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Checkout PR branch | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ steps.pr_info.outputs.branch }} | |
| - uses: actions/setup-node@v4 | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| with: | |
| node-version: 18 | |
| - name: Install backend deps | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| run: cd run && npm install | |
| - name: Install frontend deps | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| run: yarn install | |
| - uses: superfly/flyctl-actions/setup-flyctl@master | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| - name: Setup SSH | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| uses: webfactory/ssh-agent@v0.9.0 | |
| with: | |
| ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| - name: Add known hosts | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| run: | | |
| mkdir -p ~/.ssh | |
| ssh-keyscan -H 157.90.154.200 >> ~/.ssh/known_hosts | |
| - name: Configure git | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Start conversation streamer | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| continue-on-error: true | |
| run: | | |
| chmod +x .github/scripts/stream-conversation.sh | |
| .github/scripts/stream-conversation.sh & | |
| echo $! > /tmp/streamer-pid | |
| env: | |
| WEBHOOK_URL: ${{ secrets.APP_URL }}/webhooks/github-actions | |
| WEBHOOK_SECRET: ${{ secrets.ETHERNAL_WEBHOOK_SECRET }} | |
| WORKFLOW_RUN_ID: ${{ github.run_id }} | |
| GITHUB_ISSUE_NUMBER: ${{ steps.pr_info.outputs.pr_number }} | |
| - name: Claude Code - Process Review | |
| id: claude_review | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| allowed_bots: "ethernal-sentry[bot],greptile-apps[bot]" | |
| claude_args: "--model claude-sonnet-4-20250514 --max-turns 50 --allowedTools Bash,Read,Write,Edit,Glob,Grep" | |
| show_full_output: true | |
| prompt: | | |
| You are processing code review feedback on PR #${{ steps.pr_info.outputs.pr_number }} (branch: ${{ steps.pr_info.outputs.branch }}). | |
| ## Step 1: Fetch All Comments | |
| ```bash | |
| # Fetch formal PR reviews | |
| gh api repos/${{ github.repository }}/pulls/${{ steps.pr_info.outputs.pr_number }}/reviews | |
| # Fetch inline review comments | |
| gh api repos/${{ github.repository }}/pulls/${{ steps.pr_info.outputs.pr_number }}/comments | |
| # Fetch issue-level comments (where Greptile posts its summary) | |
| gh api repos/${{ github.repository }}/issues/${{ steps.pr_info.outputs.pr_number }}/comments | |
| ``` | |
| ## Step 2: Verify and Process Review Comments | |
| For each review comment from `greptile-apps[bot]` that flags an issue: | |
| 1. **FIRST verify** the comment is correct by reading the actual code it references. Check ALL commits on the branch, not just the diff — a later commit may have already addressed the issue. | |
| 2. If it's a **valid issue** → fix it, commit, push, reply explaining the fix, and react with 👍: | |
| ```bash | |
| # For inline review comments: | |
| gh api repos/${{ github.repository }}/pulls/comments/{comment_id}/reactions -f content='+1' | |
| # For issue-level comments: | |
| gh api repos/${{ github.repository }}/issues/comments/{comment_id}/reactions -f content='+1' | |
| ``` | |
| 3. If it's **incorrect** → reply on the PR explaining why it's wrong, and react with 👎: | |
| ```bash | |
| # For inline review comments: | |
| gh api repos/${{ github.repository }}/pulls/comments/{comment_id}/reactions -f content='-1' | |
| # For issue-level comments: | |
| gh api repos/${{ github.repository }}/issues/comments/{comment_id}/reactions -f content='-1' | |
| ``` | |
| 4. If it was a **valid issue but already fixed** by a later commit on this branch → reply noting it was valid but already addressed, and react with 👍: | |
| ```bash | |
| # For inline review comments: | |
| gh api repos/${{ github.repository }}/pulls/comments/{comment_id}/reactions -f content='+1' | |
| # For issue-level comments: | |
| gh api repos/${{ github.repository }}/issues/comments/{comment_id}/reactions -f content='+1' | |
| ``` | |
| 5. If it's unclear → ask for clarification in a reply (no reaction yet) | |
| **IMPORTANT:** Do NOT react to comments before verifying them. Verify first, then react with the appropriate emoji based on your assessment. | |
| If the review summary is clean (no issues flagged), react with 👍 on the summary. | |
| Take comments seriously but verify they're correct before acting. | |
| NEVER remove working code to satisfy a review bot. | |
| NEVER modify: `run/api/stripe.js`, `run/webhooks/stripe.js`, `run/lib/stripe.js`, `run/middlewares/auth.js`, `run/lib/crypto.js` | |
| ## Step 3: Signal Merge Readiness | |
| After processing ALL comments, determine if the PR is ready to merge: | |
| - **READY**: All review issues have been addressed (fixed or correctly dismissed) | |
| - **NOT READY**: There are unresolved issues you couldn't fix, or you asked for clarification | |
| You MUST run this command at the very end: | |
| ```bash | |
| # If ALL issues are resolved: | |
| echo "READY_TO_MERGE" > /tmp/merge-status | |
| # If there are unresolved issues: | |
| echo "NOT_READY" > /tmp/merge-status | |
| ``` | |
| Then write a short summary of what you did. If not ready, explain what remains. | |
| env: | |
| SENTRY_API_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} | |
| PROD_DATABASE_URL_READONLY: ${{ secrets.PROD_DATABASE_URL_READONLY }} | |
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | |
| - name: Stop conversation streamer | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| if [ -f /tmp/streamer-pid ]; then | |
| kill $(cat /tmp/streamer-pid) 2>/dev/null || true | |
| fi | |
| - name: Check merge readiness | |
| id: merge_check | |
| if: steps.check_sentry.outputs.is_sentry == 'true' | |
| run: | | |
| if [ -f /tmp/merge-status ] && grep -q "READY_TO_MERGE" /tmp/merge-status; then | |
| echo "ready=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "ready=false" >> $GITHUB_OUTPUT | |
| echo "PR is not ready to merge. Skipping merge and deploy." | |
| fi | |
| - name: Check review confidence | |
| id: confidence_check | |
| if: steps.check_sentry.outputs.is_sentry == 'true' && steps.merge_check.outputs.ready == 'true' | |
| run: | | |
| # Fetch latest Greptile review comment | |
| GREPTILE_COMMENT=$(gh api repos/${{ github.repository }}/issues/${{ steps.pr_info.outputs.pr_number }}/comments \ | |
| --jq '[.[] | select(.user.login == "greptile-apps[bot]")] | last | .body // ""') | |
| # Greptile formats as "<h3>Confidence Score: X/5</h3>" — anchor to that | |
| SCORE=$(echo "$GREPTILE_COMMENT" | grep -oP '(?i)confidence\s+score[:\s]*\K\d(?=/5)' | head -1) | |
| # Fallback: last X/5 occurrence (summary score appears after per-file scores) | |
| if [ -z "$SCORE" ]; then | |
| SCORE=$(echo "$GREPTILE_COMMENT" | grep -oP '\d(?=/5)' | tail -1) | |
| fi | |
| if [ -z "$SCORE" ]; then | |
| echo "No confidence score found, proceeding" | |
| echo "sufficient=true" >> $GITHUB_OUTPUT | |
| elif [ "$SCORE" -lt 3 ]; then | |
| echo "Low confidence: $SCORE/5. Flagging for human review." | |
| gh pr edit ${{ steps.pr_info.outputs.pr_number }} --add-label "needs-human" --repo ${{ github.repository }} | |
| gh pr comment ${{ steps.pr_info.outputs.pr_number }} --body "Review confidence $SCORE/5 (threshold: 3/5). Flagging for human review." --repo ${{ github.repository }} | |
| echo "sufficient=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "Confidence: $SCORE/5 (sufficient)" | |
| echo "sufficient=true" >> $GITHUB_OUTPUT | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Wait for CI checks | |
| id: ci_check | |
| if: steps.check_sentry.outputs.is_sentry == 'true' && steps.merge_check.outputs.ready == 'true' && steps.confidence_check.outputs.sufficient == 'true' | |
| run: | | |
| echo "Waiting for CI checks to complete on PR #${{ steps.pr_info.outputs.pr_number }}..." | |
| for i in $(seq 1 60); do | |
| CHECKS_JSON=$(gh pr checks ${{ steps.pr_info.outputs.pr_number }} --repo ${{ github.repository }} --json name,state --jq '[.[] | select(.name != "process-review" and .name != "triage-and-fix" and .name != "Greptile Review")]') | |
| STATUS=$(echo "$CHECKS_JSON" | jq -r 'if length == 0 then "pass" elif [.[] | select(.state != "SUCCESS" and .state != "SKIPPED")] | length == 0 then "pass" elif [.[] | select(.state == "FAILURE")] | length > 0 then "fail" else "pending" end') | |
| echo "Check $i/60: $STATUS" | |
| if [ "$STATUS" = "pass" ]; then | |
| echo "passed=true" >> $GITHUB_OUTPUT | |
| exit 0 | |
| elif [ "$STATUS" = "fail" ]; then | |
| echo "CI checks failed. Skipping merge." | |
| echo "passed=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| sleep 30 | |
| done | |
| echo "CI checks timed out after 30 minutes." | |
| echo "passed=false" >> $GITHUB_OUTPUT | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT }} | |
| - name: Merge PR | |
| id: merge_pr | |
| if: steps.check_sentry.outputs.is_sentry == 'true' && steps.merge_check.outputs.ready == 'true' && steps.confidence_check.outputs.sufficient == 'true' && steps.ci_check.outputs.passed == 'true' | |
| run: | | |
| gh pr merge ${{ steps.pr_info.outputs.pr_number }} \ | |
| --squash --admin --delete-branch \ | |
| --repo ${{ github.repository }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT }} | |
| - name: Check hotfix label | |
| id: hotfix_check | |
| if: steps.merge_pr.conclusion == 'success' | |
| run: | | |
| ISSUE_NUMBER=$(gh pr view ${{ steps.pr_info.outputs.pr_number }} --repo ${{ github.repository }} --json body -q '.body' | grep -oP 'Fixes #\K\d+' | head -1) | |
| IS_HOTFIX=0 | |
| if [ -n "$ISSUE_NUMBER" ]; then | |
| IS_HOTFIX=$(gh issue view "$ISSUE_NUMBER" --repo ${{ github.repository }} --json labels -q '.labels[].name' | grep -c '^hotfix$' || true) | |
| fi | |
| echo "is_hotfix=${IS_HOTFIX}" >> "$GITHUB_OUTPUT" | |
| echo "issue_number=${ISSUE_NUMBER:-0}" >> "$GITHUB_OUTPUT" | |
| echo "Hotfix: ${IS_HOTFIX}, Issue: ${ISSUE_NUMBER:-none}" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # --- Non-hotfix path: notify merged status and stop --- | |
| - name: Notify dashboard - merged (batch deploy) | |
| if: > | |
| steps.merge_pr.conclusion == 'success' && | |
| steps.hotfix_check.outputs.is_hotfix == '0' | |
| continue-on-error: true | |
| run: | | |
| curl -s -X POST "${{ secrets.APP_URL }}/webhooks/github-actions" \ | |
| -H "Authorization: Bearer ${{ secrets.ETHERNAL_WEBHOOK_SECRET }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"githubIssueNumber\": ${{ steps.hotfix_check.outputs.issue_number }}, | |
| \"githubPrNumber\": ${{ steps.pr_info.outputs.pr_number }}, | |
| \"status\": \"merged\", | |
| \"currentStep\": \"PR merged, awaiting batch deploy\" | |
| }" | |
| # --- Hotfix path: deploy inline (existing logic) --- | |
| - name: Notify dashboard - merging (hotfix) | |
| if: > | |
| steps.merge_pr.conclusion == 'success' && | |
| steps.hotfix_check.outputs.is_hotfix == '1' | |
| continue-on-error: true | |
| run: | | |
| curl -s -X POST "${{ secrets.APP_URL }}/webhooks/github-actions" \ | |
| -H "Authorization: Bearer ${{ secrets.ETHERNAL_WEBHOOK_SECRET }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"githubIssueNumber\": ${{ steps.hotfix_check.outputs.issue_number }}, | |
| \"githubPrNumber\": ${{ steps.pr_info.outputs.pr_number }}, | |
| \"status\": \"merging\", | |
| \"currentStep\": \"PR merged, deploying (hotfix)\" | |
| }" | |
| - name: Deploy (hotfix) | |
| id: deploy_hotfix | |
| if: > | |
| steps.merge_pr.conclusion == 'success' && | |
| steps.hotfix_check.outputs.is_hotfix == '1' | |
| env: | |
| GH_PAT: ${{ secrets.GH_PAT }} | |
| run: | | |
| git remote set-url origin "https://x-access-token:${GH_PAT}@github.com/${{ github.repository }}.git" | |
| git checkout develop | |
| git pull origin develop | |
| LAST_TAG=$(git describe --tags --abbrev=0) | |
| echo "Last tag: $LAST_TAG" | |
| echo "last_tag=$LAST_TAG" >> "$GITHUB_OUTPUT" | |
| # Get commits since last tag, filter out release machinery | |
| COMMITS=$(git log --oneline "$LAST_TAG..HEAD" | grep -viE '^[a-f0-9]+ (update changelog|[0-9]+\.[0-9]+\.[0-9]+|Merge (branch|develop|master))' || true) | |
| echo "Commits since $LAST_TAG:" | |
| echo "$COMMITS" | |
| if [ -z "$COMMITS" ]; then | |
| echo "No meaningful commits to deploy" | |
| exit 0 | |
| fi | |
| # Generate changelog entry | |
| VERSION_DATE=$(date +%Y-%m-%d) | |
| CURRENT_VERSION=$(node -p "require('./package.json').version") | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" | |
| NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" | |
| CHANGELOG_ENTRY="## [$NEW_VERSION] - $VERSION_DATE | |
| ### Fixed | |
| $(echo "$COMMITS" | sed 's/^[a-f0-9]* /- /')" | |
| # Insert after header (line 5) | |
| head -5 CHANGELOG.md > CHANGELOG.tmp | |
| echo "" >> CHANGELOG.tmp | |
| echo "$CHANGELOG_ENTRY" >> CHANGELOG.tmp | |
| echo "" >> CHANGELOG.tmp | |
| tail -n +6 CHANGELOG.md >> CHANGELOG.tmp | |
| mv CHANGELOG.tmp CHANGELOG.md | |
| git add CHANGELOG.md | |
| git commit -m "update changelog" | |
| npm version patch --message '%s' | |
| git push origin develop | |
| git push origin "v$NEW_VERSION" | |
| # Sync master | |
| git checkout master | |
| git pull origin master | |
| git merge develop --no-ff -m "Merge develop into master for v$NEW_VERSION" | |
| git push origin master | |
| git checkout develop | |
| echo "Deployed v$NEW_VERSION" | |
| - name: Notify dashboard - deploying (hotfix) | |
| if: > | |
| steps.deploy_hotfix.conclusion == 'success' && | |
| steps.hotfix_check.outputs.is_hotfix == '1' | |
| continue-on-error: true | |
| run: | | |
| curl -s -X POST "${{ secrets.APP_URL }}/webhooks/github-actions" \ | |
| -H "Authorization: Bearer ${{ secrets.ETHERNAL_WEBHOOK_SECRET }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"githubIssueNumber\": ${{ steps.hotfix_check.outputs.issue_number }}, | |
| \"githubPrNumber\": ${{ steps.pr_info.outputs.pr_number }}, | |
| \"status\": \"deploying\", | |
| \"currentStep\": \"Deploying to production (hotfix)\" | |
| }" | |
| - name: Resolve Sentry issue (hotfix) | |
| if: > | |
| steps.deploy_hotfix.conclusion == 'success' && | |
| steps.hotfix_check.outputs.is_hotfix == '1' | |
| env: | |
| SENTRY_API_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| ISSUE_NUMBER=${{ steps.hotfix_check.outputs.issue_number }} | |
| if [ "$ISSUE_NUMBER" = "0" ] || [ -z "$ISSUE_NUMBER" ]; then | |
| echo "No linked issue found, skipping Sentry resolution" | |
| exit 0 | |
| fi | |
| # Extract Sentry issue ID from the GitHub issue body | |
| SENTRY_ISSUE_ID=$(gh issue view "$ISSUE_NUMBER" --repo ${{ github.repository }} --json body -q '.body' | grep -oP 'sentry\.tryethernal\.com/organizations/sentry/issues/\K\d+' | head -1) | |
| if [ -z "$SENTRY_ISSUE_ID" ]; then | |
| echo "No Sentry issue ID found in issue #$ISSUE_NUMBER body, skipping" | |
| exit 0 | |
| fi | |
| echo "Resolving Sentry issue $SENTRY_ISSUE_ID (from GitHub issue #$ISSUE_NUMBER)" | |
| curl -s -X PUT \ | |
| "https://sentry.tryethernal.com/api/0/issues/$SENTRY_ISSUE_ID/" \ | |
| -H "Authorization: Bearer $SENTRY_API_TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"status": "resolved"}' | jq '{status, statusDetails}' | |
| - name: Notify dashboard - completed (hotfix) | |
| if: > | |
| steps.deploy_hotfix.conclusion == 'success' && | |
| steps.hotfix_check.outputs.is_hotfix == '1' | |
| continue-on-error: true | |
| run: | | |
| curl -s -X POST "${{ secrets.APP_URL }}/webhooks/github-actions" \ | |
| -H "Authorization: Bearer ${{ secrets.ETHERNAL_WEBHOOK_SECRET }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"githubIssueNumber\": ${{ steps.hotfix_check.outputs.issue_number }}, | |
| \"githubPrNumber\": ${{ steps.pr_info.outputs.pr_number }}, | |
| \"status\": \"completed\", | |
| \"currentStep\": \"Fix deployed and Sentry issue resolved (hotfix)\", | |
| \"completedAt\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" | |
| }" | |
| - name: Resolve swept-in non-hotfix Sentry issues | |
| if: > | |
| steps.deploy_hotfix.conclusion == 'success' && | |
| steps.hotfix_check.outputs.is_hotfix == '1' | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SENTRY_API_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} | |
| run: | | |
| # Use pre-deploy tag captured by the deploy step | |
| LAST_TAG=${{ steps.deploy_hotfix.outputs.last_tag }} | |
| HOTFIX_PR=${{ steps.pr_info.outputs.pr_number }} | |
| PR_NUMBERS=$(git log --oneline "$LAST_TAG..HEAD" | grep -viE '^[a-f0-9]+ (update changelog|[0-9]+\.[0-9]+\.[0-9]+|Merge (branch|develop|master))' | grep -oP '#\K\d+' | grep -v "^${HOTFIX_PR}$" || true) | |
| if [ -z "$PR_NUMBERS" ]; then | |
| echo "No other PRs swept into this hotfix release" | |
| exit 0 | |
| fi | |
| echo "Found swept-in PRs: $PR_NUMBERS" | |
| for PR_NUM in $PR_NUMBERS; do | |
| echo "--- Processing swept-in PR #$PR_NUM ---" | |
| ISSUE_NUMBER=$(gh pr view "$PR_NUM" --repo ${{ github.repository }} --json body -q '.body' 2>/dev/null | grep -oP 'Fixes #\K\d+' | head -1 || true) | |
| if [ -z "$ISSUE_NUMBER" ]; then | |
| echo "No linked issue in PR #$PR_NUM, skipping" | |
| continue | |
| fi | |
| IS_SENTRY=$(gh issue view "$ISSUE_NUMBER" --repo ${{ github.repository }} --json labels -q '.labels[].name' 2>/dev/null | grep -c '^sentry$' || true) | |
| if [ "$IS_SENTRY" = "0" ]; then | |
| echo "Issue #$ISSUE_NUMBER is not a sentry issue, skipping" | |
| continue | |
| fi | |
| SENTRY_ISSUE_ID=$(gh issue view "$ISSUE_NUMBER" --repo ${{ github.repository }} --json body -q '.body' | grep -oP 'sentry\.tryethernal\.com/organizations/sentry/issues/\K\d+' | head -1 || true) | |
| if [ -z "$SENTRY_ISSUE_ID" ]; then | |
| echo "No Sentry issue ID in issue #$ISSUE_NUMBER, skipping" | |
| continue | |
| fi | |
| echo "Resolving swept-in Sentry issue $SENTRY_ISSUE_ID (issue #$ISSUE_NUMBER, PR #$PR_NUM)" | |
| curl -s -X PUT \ | |
| "https://sentry.tryethernal.com/api/0/issues/$SENTRY_ISSUE_ID/" \ | |
| -H "Authorization: Bearer $SENTRY_API_TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"status": "resolved"}' | jq '{status, statusDetails}' | |
| curl -s -X POST "${{ secrets.APP_URL }}/webhooks/github-actions" \ | |
| -H "Authorization: Bearer ${{ secrets.ETHERNAL_WEBHOOK_SECRET }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"githubIssueNumber\": ${ISSUE_NUMBER}, | |
| \"githubPrNumber\": ${PR_NUM}, | |
| \"status\": \"completed\", | |
| \"currentStep\": \"Fix deployed via hotfix release\", | |
| \"completedAt\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" | |
| }" | |
| done | |
| recover-stuck: | |
| if: github.event_name == 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Find and merge stuck sentry PRs | |
| run: | | |
| # Find open sentry fix PRs not updated in the last hour | |
| STUCK_PRS=$(gh pr list --repo ${{ github.repository }} \ | |
| --search "head:fix/sentry- is:open" \ | |
| --json number,updatedAt \ | |
| --jq '[.[] | select((.updatedAt | fromdateiso8601) < (now - 3600))] | .[].number') | |
| if [ -z "$STUCK_PRS" ]; then | |
| echo "No stuck PRs found" | |
| exit 0 | |
| fi | |
| for PR_NUM in $STUCK_PRS; do | |
| echo "--- Checking PR #$PR_NUM ---" | |
| # Check mergeable status | |
| MERGEABLE=$(gh pr view "$PR_NUM" --repo ${{ github.repository }} --json mergeable -q '.mergeable') | |
| if [ "$MERGEABLE" != "MERGEABLE" ]; then | |
| echo "PR #$PR_NUM is $MERGEABLE, skipping" | |
| continue | |
| fi | |
| # Skip PRs flagged for human review | |
| HAS_NEEDS_HUMAN=$(gh pr view "$PR_NUM" --repo ${{ github.repository }} --json labels \ | |
| -q '[.labels[].name] | any(. == "needs-human")') | |
| if [ "$HAS_NEEDS_HUMAN" = "true" ]; then | |
| echo "PR #$PR_NUM has needs-human label, skipping" | |
| continue | |
| fi | |
| # Check CI status (exclude workflow-specific checks) | |
| FAILING=$(gh pr checks "$PR_NUM" --repo ${{ github.repository }} --json name,state \ | |
| --jq '[.[] | select(.name != "process-review" and .name != "triage-and-fix" and .name != "recover-stuck" and .name != "Greptile Review") | select(.state != "SUCCESS" and .state != "SKIPPED")] | length') | |
| if [ "$FAILING" != "0" ]; then | |
| echo "PR #$PR_NUM has failing CI, skipping" | |
| continue | |
| fi | |
| echo "Merging stuck PR #$PR_NUM" | |
| gh pr merge "$PR_NUM" --squash --admin --delete-branch --repo ${{ github.repository }} || echo "Failed to merge #$PR_NUM" | |
| done | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT }} |