diff --git a/.github/workflows/agents-63-issue-intake.yml b/.github/workflows/agents-63-issue-intake.yml index eb12a6482..b29afe865 100644 --- a/.github/workflows/agents-63-issue-intake.yml +++ b/.github/workflows/agents-63-issue-intake.yml @@ -257,6 +257,38 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + + - name: Install load balancer dependencies + run: | + set -euo pipefail + npm install --no-save --no-package-lock @octokit/rest @octokit/auth-app + + - name: Export load balancer tokens + uses: ./.github/actions/export-load-balancer-tokens + with: + github_token: ${{ github.token }} + service_bot_pat: ${{ secrets.SERVICE_BOT_PAT }} + actions_bot_pat: ${{ secrets.ACTIONS_BOT_PAT }} + codespaces_workflows: ${{ secrets.CODESPACES_WORKFLOWS }} + owner_pr_pat: ${{ secrets.OWNER_PR_PAT }} + agents_automation_pat: ${{ secrets.AGENTS_AUTOMATION_PAT }} + workflows_app_id: ${{ secrets.WORKFLOWS_APP_ID }} + workflows_app_private_key: ${{ secrets.WORKFLOWS_APP_PRIVATE_KEY }} + keepalive_app_id: ${{ secrets.KEEPALIVE_APP_ID }} + keepalive_app_private_key: ${{ secrets.KEEPALIVE_APP_PRIVATE_KEY }} + gh_app_id: ${{ secrets.GH_APP_ID }} + gh_app_private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} + app_1_id: ${{ secrets.APP_1_ID }} + app_1_private_key: ${{ secrets.APP_1_PRIVATE_KEY }} + app_2_id: ${{ secrets.APP_2_ID }} + app_2_private_key: ${{ secrets.APP_2_PRIVATE_KEY }} + token_rotation_json: ${{ secrets.TOKEN_ROTATION_JSON }} + token_rotation_env_keys: ${{ vars.TOKEN_ROTATION_ENV_KEYS }} + - name: Export load balancer tokens uses: ./.github/actions/export-load-balancer-tokens with: @@ -1329,22 +1361,18 @@ jobs: echo "=== Formatting issue #${issue_num} ===" # Get issue body - if ! gh api "repos/${{ github.repository }}/issues/${issue_num}" > "/tmp/issue_${issue_num}.json"; then + if ! ISSUE_NUMBER="$issue_num" node -e "const fs=require('fs'); const { Octokit }=require('@octokit/rest'); const { createTokenAwareRetry }=require('./.github/scripts/github-api-with-retry.js'); const core={info:()=>{}, warning:console.warn, debug:()=>{}}; const github=new Octokit({auth:process.env.GITHUB_TOKEN}); (async()=>{ const { withRetry }=await createTokenAwareRetry({ github, core, env: process.env, task: 'issue-intake-format', capabilities: ['issues:read'] }); const parts=process.env.GITHUB_REPOSITORY.split('/'); const owner=parts[0]; const repo=parts[1]; const issueNumber=Number(process.env.ISSUE_NUMBER); const result=await withRetry((client)=>client.rest.issues.get({ owner, repo, issue_number: issueNumber })); const data=result.data; fs.writeFileSync('/tmp/issue_' + issueNumber + '.json', JSON.stringify(data, null, 2)); fs.writeFileSync('/tmp/issue_body_' + issueNumber + '.md', data.body || ''); })().catch((error)=>{ console.error(error); process.exit(1); });" + then echo " ❌ Failed to fetch issue #${issue_num}" failed=$((failed + 1)) continue fi - body=$(jq -r '.body // ""' "/tmp/issue_${issue_num}.json") - - if [ -z "$body" ] || [ "$body" = "null" ]; then + if [ ! -s "/tmp/issue_body_${issue_num}.md" ]; then echo " ⚠️ Issue #${issue_num} has no body, skipping" continue fi - # Save body to temp file - echo "$body" > "/tmp/issue_body_${issue_num}.md" - # Format with LangChain echo " 📝 Calling LangChain formatter..." if ! python scripts/langchain/issue_formatter.py \ @@ -1357,8 +1385,8 @@ jobs: # Update issue echo " 💾 Updating issue body..." - if ! gh api --method PATCH "repos/${{ github.repository }}/issues/${issue_num}" \ - -F body="@/tmp/formatted_${issue_num}.md" > /dev/null; then + if ! ISSUE_NUMBER="$issue_num" node -e "const fs=require('fs'); const { Octokit }=require('@octokit/rest'); const { createTokenAwareRetry }=require('./.github/scripts/github-api-with-retry.js'); const core={info:()=>{}, warning:console.warn, debug:()=>{}}; const github=new Octokit({auth:process.env.GITHUB_TOKEN}); (async()=>{ const { withRetry }=await createTokenAwareRetry({ github, core, env: process.env, task: 'issue-intake-format', capabilities: ['issues:write'] }); const parts=process.env.GITHUB_REPOSITORY.split('/'); const owner=parts[0]; const repo=parts[1]; const issueNumber=Number(process.env.ISSUE_NUMBER); const body=fs.readFileSync('/tmp/formatted_' + issueNumber + '.md', 'utf8'); await withRetry((client)=>client.rest.issues.update({ owner, repo, issue_number: issueNumber, body })); })().catch((error)=>{ console.error(error); process.exit(1); });" + then echo " ❌ Failed to update issue #${issue_num}" failed=$((failed + 1)) continue @@ -1366,7 +1394,8 @@ jobs: # Add formatted label echo " 🏷️ Adding 'agents:formatted' label..." - if ! gh issue edit "${issue_num}" --add-label "agents:formatted" --repo "${{ github.repository }}"; then + if ! ISSUE_NUMBER="$issue_num" node -e "const { Octokit }=require('@octokit/rest'); const { createTokenAwareRetry }=require('./.github/scripts/github-api-with-retry.js'); const core={info:()=>{}, warning:console.warn, debug:()=>{}}; const github=new Octokit({auth:process.env.GITHUB_TOKEN}); (async()=>{ const { withRetry }=await createTokenAwareRetry({ github, core, env: process.env, task: 'issue-intake-format', capabilities: ['issues:write'] }); const parts=process.env.GITHUB_REPOSITORY.split('/'); const owner=parts[0]; const repo=parts[1]; const issueNumber=Number(process.env.ISSUE_NUMBER); await withRetry((client)=>client.rest.issues.addLabels({ owner, repo, issue_number: issueNumber, labels: ['agents:formatted'] })); })().catch((error)=>{ console.error(error); process.exit(1); });" + then echo " ⚠️ Failed to add label (non-fatal)" fi diff --git a/.github/workflows/agents-auto-pilot.yml b/.github/workflows/agents-auto-pilot.yml index 4339eaa01..93363b97e 100644 --- a/.github/workflows/agents-auto-pilot.yml +++ b/.github/workflows/agents-auto-pilot.yml @@ -492,8 +492,37 @@ jobs: # Has PR - check if it's complete and ready to merge # Get PR state to see if keepalive has finished - if ! PR_STATE=$(gh api "/repos/${{ github.repository }}/issues/$LINKED_PR/comments" \ - --jq '.[] | select(.body | contains("keepalive-state:")) | .body' 2>&1 | tail -1); then + if ! PR_STATE=$(LINKED_PR="$LINKED_PR" node - <<'NODE' + (async () => { + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { paginateWithRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:read'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.LINKED_PR); + const comments = await paginateWithRetry( + github.rest.issues.listComments, + { owner, repo, issue_number: issueNumber, per_page: 100 } + ); + const last = [...comments].reverse().find((comment) => + (comment.body || '').includes('keepalive-state:') + ); + if (last?.body) { + process.stdout.write(last.body); + } + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE + ); then echo "⚠️ Failed to fetch PR state, defaulting to monitor-pr" echo "next_step=monitor-pr" >> "$GITHUB_OUTPUT" exit 0 @@ -537,14 +566,69 @@ jobs: run: | # Post progress comment - gh issue comment "${ISSUE_NUMBER}" --body \ - "🤖 **Auto-pilot step $((STEP_COUNT + 1))**: Starting issue formatting... - - Running formatter inline (not via label trigger)." + cat > /tmp/auto_pilot_format_start.md <<'EOF' + 🤖 **Auto-pilot step $((STEP_COUNT + 1))**: Starting issue formatting... + + Running formatter inline (not via label trigger). + EOF + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const body = fs.readFileSync('/tmp/auto_pilot_format_start.md', 'utf8'); + await withRetry((client) => client.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body, + })); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE # Get issue body - gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" > /tmp/issue.json - jq -r '.body' /tmp/issue.json > /tmp/issue_body.md + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:read'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const { data } = await withRetry((client) => client.rest.issues.get({ + owner, + repo, + issue_number: issueNumber, + })); + fs.writeFileSync('/tmp/issue.json', JSON.stringify(data, null, 2)); + fs.writeFileSync('/tmp/issue_body.md', data.body || ''); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE # Format the issue echo "Formatting issue into AGENT_ISSUE_TEMPLATE structure..." @@ -568,11 +652,66 @@ jobs: " || exit 1 # Update issue body - gh issue edit "${ISSUE_NUMBER}" --body-file /tmp/formatted_body.md + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const body = fs.readFileSync('/tmp/formatted_body.md', 'utf8'); + await withRetry((client) => client.rest.issues.update({ + owner, + repo, + issue_number: issueNumber, + body, + })); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE # Add marker labels (but don't trigger workflow) - gh issue edit "${ISSUE_NUMBER}" --add-label "agents:formatted" || true - gh issue edit "${ISSUE_NUMBER}" --add-label "agents:apply-suggestions" || true + node - <<'NODE' + (async () => { + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + try { + await withRetry((client) => client.rest.issues.addLabels({ + owner, + repo, + issue_number: issueNumber, + labels: ['agents:formatted', 'agents:apply-suggestions'], + })); + } catch (error) { + console.warn(`Label update failed: ${error.message}`); + } + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE echo "✅ Formatting complete - continuing to next step" @@ -625,14 +764,69 @@ jobs: run: | # Post progress comment - gh issue comment "${ISSUE_NUMBER}" --body \ - "🤖 **Auto-pilot step $((STEP_COUNT + 1))**: Analyzing issue for improvements... - - Running optimizer inline (not via label trigger)." + cat > /tmp/auto_pilot_optimize_start.md <<'EOF' + 🤖 **Auto-pilot step $((STEP_COUNT + 1))**: Analyzing issue for improvements... + + Running optimizer inline (not via label trigger). + EOF + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const body = fs.readFileSync('/tmp/auto_pilot_optimize_start.md', 'utf8'); + await withRetry((client) => client.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body, + })); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE # Get issue body - gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" > /tmp/issue.json - jq -r '.body' /tmp/issue.json > /tmp/issue_body.md + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:read'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const { data } = await withRetry((client) => client.rest.issues.get({ + owner, + repo, + issue_number: issueNumber, + })); + fs.writeFileSync('/tmp/issue.json', JSON.stringify(data, null, 2)); + fs.writeFileSync('/tmp/issue_body.md', data.body || ''); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE # Run optimizer analysis echo "Running optimization analysis..." @@ -669,11 +863,66 @@ jobs: } # Post suggestions comment - gh issue comment "${ISSUE_NUMBER}" --body-file /tmp/comment.md + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const body = fs.readFileSync('/tmp/comment.md', 'utf8'); + await withRetry((client) => client.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body, + })); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE # Add marker labels (but don't trigger workflow) - gh issue edit "${ISSUE_NUMBER}" --add-label "agents:formatted" || true - gh issue edit "${ISSUE_NUMBER}" --add-label "agents:apply-suggestions" || true + node - <<'NODE' + (async () => { + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + try { + await withRetry((client) => client.rest.issues.addLabels({ + owner, + repo, + issue_number: issueNumber, + labels: ['agents:formatted', 'agents:apply-suggestions'], + })); + } catch (error) { + console.warn(`Label update failed: ${error.message}`); + } + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE echo "✅ Optimization complete - continuing to next step" @@ -796,16 +1045,74 @@ jobs: run: | # Post progress comment - gh issue comment "${ISSUE_NUMBER}" --body \ - "🤖 **Auto-pilot step $((STEP_COUNT + 1))**: Applying optimization suggestions... - - Running apply inline (not via label trigger)." + cat > /tmp/auto_pilot_apply_start.md <<'EOF' + 🤖 **Auto-pilot step $((STEP_COUNT + 1))**: Applying optimization suggestions... + + Running apply inline (not via label trigger). + EOF + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const body = fs.readFileSync('/tmp/auto_pilot_apply_start.md', 'utf8'); + await withRetry((client) => client.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body, + })); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE # Get issue body and comments - gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" > /tmp/issue.json - jq -r '.body' /tmp/issue.json > /tmp/issue_body.md - gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments" \ - --paginate > /tmp/comments.json + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry, paginateWithRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:read'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const { data } = await withRetry((client) => client.rest.issues.get({ + owner, + repo, + issue_number: issueNumber, + })); + fs.writeFileSync('/tmp/issue.json', JSON.stringify(data, null, 2)); + fs.writeFileSync('/tmp/issue_body.md', data.body || ''); + const comments = await paginateWithRetry( + github.rest.issues.listComments, + { owner, repo, issue_number: issueNumber, per_page: 100 } + ); + fs.writeFileSync('/tmp/comments.json', JSON.stringify(comments, null, 2)); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE # Extract and apply suggestions python -c " @@ -844,10 +1151,66 @@ jobs: " || exit 1 # Update issue body - gh issue edit "${ISSUE_NUMBER}" --body-file /tmp/updated_body.md + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const body = fs.readFileSync('/tmp/updated_body.md', 'utf8'); + await withRetry((client) => client.rest.issues.update({ + owner, + repo, + issue_number: issueNumber, + body, + })); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE # Add marker label (but don't trigger workflow) - gh issue edit "${ISSUE_NUMBER}" --add-label "agents:formatted" || true + node - <<'NODE' + (async () => { + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'auto-pilot', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + try { + await withRetry((client) => client.rest.issues.addLabels({ + owner, + repo, + issue_number: issueNumber, + labels: ['agents:formatted'], + })); + } catch (error) { + console.warn(`Label update failed: ${error.message}`); + } + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE echo "✅ Apply complete - continuing to next step" diff --git a/.github/workflows/agents-issue-optimizer.yml b/.github/workflows/agents-issue-optimizer.yml index d7aa306f1..84fe9c426 100644 --- a/.github/workflows/agents-issue-optimizer.yml +++ b/.github/workflows/agents-issue-optimizer.yml @@ -70,6 +70,41 @@ jobs: if: steps.check.outputs.should_run == 'true' uses: actions/checkout@v6 + - name: Set up Node.js + if: steps.check.outputs.should_run == 'true' + uses: actions/setup-node@v6 + with: + node-version: 20 + + - name: Install load balancer dependencies + if: steps.check.outputs.should_run == 'true' + run: | + set -euo pipefail + npm install --no-save --no-package-lock @octokit/rest @octokit/auth-app + + - name: Export load balancer tokens + if: steps.check.outputs.should_run == 'true' + uses: ./.github/actions/export-load-balancer-tokens + with: + github_token: ${{ github.token }} + service_bot_pat: ${{ secrets.SERVICE_BOT_PAT }} + actions_bot_pat: ${{ secrets.ACTIONS_BOT_PAT }} + codespaces_workflows: ${{ secrets.CODESPACES_WORKFLOWS }} + owner_pr_pat: ${{ secrets.OWNER_PR_PAT }} + agents_automation_pat: ${{ secrets.AGENTS_AUTOMATION_PAT }} + workflows_app_id: ${{ secrets.WORKFLOWS_APP_ID }} + workflows_app_private_key: ${{ secrets.WORKFLOWS_APP_PRIVATE_KEY }} + keepalive_app_id: ${{ secrets.KEEPALIVE_APP_ID }} + keepalive_app_private_key: ${{ secrets.KEEPALIVE_APP_PRIVATE_KEY }} + gh_app_id: ${{ secrets.GH_APP_ID }} + gh_app_private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} + app_1_id: ${{ secrets.APP_1_ID }} + app_1_private_key: ${{ secrets.APP_1_PRIVATE_KEY }} + app_2_id: ${{ secrets.APP_2_ID }} + app_2_private_key: ${{ secrets.APP_2_PRIVATE_KEY }} + token_rotation_json: ${{ secrets.TOKEN_ROTATION_JSON }} + token_rotation_env_keys: ${{ vars.TOKEN_ROTATION_ENV_KEYS }} + - name: Set up Python if: steps.check.outputs.should_run == 'true' uses: actions/setup-python@v6 @@ -91,9 +126,35 @@ jobs: GH_TOKEN: ${{ github.token }} ISSUE_NUMBER: ${{ steps.check.outputs.issue_number }} run: | - gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" > /tmp/issue.json - jq -r '.body' /tmp/issue.json > /tmp/issue_body.md - echo "Issue body saved to /tmp/issue_body.md" + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'issue-optimizer', + capabilities: ['issues:read'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const { data } = await withRetry((client) => client.rest.issues.get({ + owner, + repo, + issue_number: issueNumber, + })); + fs.writeFileSync('/tmp/issue.json', JSON.stringify(data, null, 2)); + fs.writeFileSync('/tmp/issue_body.md', data.body || ''); + console.log('Issue body saved to /tmp/issue_body.md'); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE - name: Phase 1 - Analyze Issue if: steps.check.outputs.should_run == 'true' && steps.check.outputs.phase == 'analyze' @@ -138,7 +199,34 @@ jobs: } # Post comment - gh issue comment "${ISSUE_NUMBER}" --body-file /tmp/comment.md + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'issue-optimizer', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const body = fs.readFileSync('/tmp/comment.md', 'utf8'); + await withRetry((client) => client.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body, + })); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE echo "Analysis complete. Review suggestions and add 'agents:apply-suggestions' label to apply." @@ -152,8 +240,37 @@ jobs: PYTHONPATH: ${{ github.workspace }} run: | echo "Checking for potential duplicate issues (advisory)" - gh api "repos/${{ github.repository }}/issues?state=open&per_page=100" --paginate > /tmp/open_issues.json - gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments" --paginate > /tmp/dedup_comments.json + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { paginateWithRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'issue-optimizer', + capabilities: ['issues:read'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const openIssues = await paginateWithRetry( + github.rest.issues.listForRepo, + { owner, repo, state: 'open', per_page: 100 } + ); + const comments = await paginateWithRetry( + github.rest.issues.listComments, + { owner, repo, issue_number: issueNumber, per_page: 100 } + ); + fs.writeFileSync('/tmp/open_issues.json', JSON.stringify(openIssues, null, 2)); + fs.writeFileSync('/tmp/dedup_comments.json', JSON.stringify(comments, null, 2)); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE python - <<'PY' || true import json @@ -192,7 +309,38 @@ jobs: PY if [[ -f /tmp/dedup_comment.md ]]; then - gh issue comment "${ISSUE_NUMBER}" --body-file /tmp/dedup_comment.md || true + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'issue-optimizer', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const body = fs.readFileSync('/tmp/dedup_comment.md', 'utf8'); + try { + await withRetry((client) => client.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body, + })); + } catch (error) { + console.warn(`Dedup comment failed: ${error.message}`); + } + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE else echo "No likely duplicates detected." fi @@ -208,7 +356,32 @@ jobs: echo "Extracting suggestions from comments on issue #${ISSUE_NUMBER}" # Get all comments and find the one with suggestions JSON - gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments" --paginate > /tmp/comments.json + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { paginateWithRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'issue-optimizer', + capabilities: ['issues:read'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const comments = await paginateWithRetry( + github.rest.issues.listComments, + { owner, repo, issue_number: issueNumber, per_page: 100 } + ); + fs.writeFileSync('/tmp/comments.json', JSON.stringify(comments, null, 2)); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE # Extract suggestions JSON from comment python -c " @@ -247,7 +420,34 @@ jobs: " || exit 1 # Update issue body - gh issue edit "${ISSUE_NUMBER}" --body-file /tmp/updated_body.md + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'issue-optimizer', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const body = fs.readFileSync('/tmp/updated_body.md', 'utf8'); + await withRetry((client) => client.rest.issues.update({ + owner, + repo, + issue_number: issueNumber, + body, + })); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE echo "Issue body updated with applied suggestions" @@ -284,7 +484,34 @@ jobs: " || exit 1 # Update issue body with formatted version - gh issue edit "${ISSUE_NUMBER}" --body-file /tmp/formatted_body.md + node - <<'NODE' + (async () => { + const fs = require('fs'); + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'issue-optimizer', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const body = fs.readFileSync('/tmp/formatted_body.md', 'utf8'); + await withRetry((client) => client.rest.issues.update({ + owner, + repo, + issue_number: issueNumber, + body, + })); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE echo "Issue body updated with formatted structure" @@ -297,13 +524,95 @@ jobs: run: | if [[ "$PHASE" == "apply" ]]; then # Remove both optimization labels and add formatted label - gh issue edit "${ISSUE_NUMBER}" --remove-label "agents:optimize" || true - gh issue edit "${ISSUE_NUMBER}" --remove-label "agents:apply-suggestions" - gh issue edit "${ISSUE_NUMBER}" --add-label "agents:formatted" + node - <<'NODE' + (async () => { + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'issue-optimizer', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + const labelsToRemove = ['agents:optimize', 'agents:apply-suggestions']; + for (const label of labelsToRemove) { + try { + await withRetry((client) => client.rest.issues.removeLabel({ + owner, + repo, + issue_number: issueNumber, + name: label, + })); + } catch (error) { + if (!String(error.message || '').includes('Label does not exist')) { + console.warn(`Failed to remove label ${label}: ${error.message}`); + } + } + } + try { + await withRetry((client) => client.rest.issues.addLabels({ + owner, + repo, + issue_number: issueNumber, + labels: ['agents:formatted'], + })); + } catch (error) { + console.warn(`Failed to add formatted label: ${error.message}`); + } + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE echo "Labels updated: removed optimize/apply-suggestions, added formatted" elif [[ "$PHASE" == "format" ]]; then # Remove format trigger label and add formatted result label - gh issue edit "${ISSUE_NUMBER}" --remove-label "agents:format" - gh issue edit "${ISSUE_NUMBER}" --add-label "agents:formatted" + node - <<'NODE' + (async () => { + const { Octokit } = require('@octokit/rest'); + const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); + const core = { info: () => {}, warning: console.warn, debug: () => {} }; + const github = new Octokit({ auth: process.env.GITHUB_TOKEN }); + const { withRetry } = await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'issue-optimizer', + capabilities: ['issues:write'], + }); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const issueNumber = Number(process.env.ISSUE_NUMBER); + try { + await withRetry((client) => client.rest.issues.removeLabel({ + owner, + repo, + issue_number: issueNumber, + name: 'agents:format', + })); + } catch (error) { + if (!String(error.message || '').includes('Label does not exist')) { + console.warn(`Failed to remove format label: ${error.message}`); + } + } + try { + await withRetry((client) => client.rest.issues.addLabels({ + owner, + repo, + issue_number: issueNumber, + labels: ['agents:formatted'], + })); + } catch (error) { + console.warn(`Failed to add formatted label: ${error.message}`); + } + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE echo "Labels updated: removed format, added formatted" fi diff --git a/scripts/check_issue_consistency.py b/scripts/check_issue_consistency.py index 6080257e0..70a3539af 100755 --- a/scripts/check_issue_consistency.py +++ b/scripts/check_issue_consistency.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import json import os import re import subprocess @@ -68,9 +69,30 @@ def extract_head_ref_issue_numbers(head_ref: str) -> set[int]: return extract_issue_numbers(head_ref or "", include_hash=False) -def is_autofix_context(pr_title: str, head_ref: str) -> bool: +def _has_autofix_label(event_path: str | None) -> bool: + if not event_path: + return False + try: + with open(event_path, encoding="utf-8") as handle: + payload = json.load(handle) + except (OSError, json.JSONDecodeError): + return False + pull_request = payload.get("pull_request") or {} + labels = pull_request.get("labels") or [] + for label in labels: + name = label.get("name", "") if isinstance(label, dict) else str(label) + if "autofix" in name.lower(): + return True + return False + + +def is_autofix_context(pr_title: str, head_ref: str, event_path: str | None = None) -> bool: combined = f"{pr_title or ''}\n{head_ref or ''}".lower() - return "autofix" in combined or (head_ref or "").lower().startswith("autofix/") + if "autofix" in combined or (head_ref or "").lower().startswith("autofix/"): + return True + if event_path is None: + event_path = os.environ.get("GITHUB_EVENT_PATH") + return _has_autofix_label(event_path) def _run_git(args: list[str]) -> str: diff --git a/tests/scripts/test_issue_consistency_check.py b/tests/scripts/test_issue_consistency_check.py index 06ecb595c..d84da283f 100644 --- a/tests/scripts/test_issue_consistency_check.py +++ b/tests/scripts/test_issue_consistency_check.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from pathlib import Path from scripts import check_issue_consistency @@ -33,3 +34,16 @@ def test_extract_head_ref_issue_numbers_from_branch() -> None: head_ref = "codex/issue-144-keepalive" numbers = check_issue_consistency.extract_head_ref_issue_numbers(head_ref) assert numbers == {144} + + +def test_is_autofix_context_reads_event_labels(tmp_path: Path, monkeypatch) -> None: + payload = { + "pull_request": { + "labels": [{"name": "autofix"}], + } + } + event_path = tmp_path / "event.json" + event_path.write_text(json.dumps(payload), encoding="utf-8") + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_path)) + + assert check_issue_consistency.is_autofix_context("", "") is True