From cb8a10f41d17503d2e8e9487a0ac851e551f2e2a Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 12 Mar 2026 12:56:05 -0500 Subject: [PATCH] fix: use OIDC via claude-code-action, remove ANTHROPIC_API_KEY, harden security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch from curl+ANTHROPIC_API_KEY to claude-code-action with OIDC token exchange — no long-lived secrets on the runner - Parse classifier output from the bot comment instead of step output - Delete the raw JSON bot comment after parsing - Pin actions/checkout and actions/github-script to commit SHAs - Add id-token: write permission for OIDC Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/pr-classify.yml | 137 +++++++++++++----------------- 1 file changed, 57 insertions(+), 80 deletions(-) diff --git a/.github/workflows/pr-classify.yml b/.github/workflows/pr-classify.yml index 49b4bc5..6b152b3 100644 --- a/.github/workflows/pr-classify.yml +++ b/.github/workflows/pr-classify.yml @@ -7,12 +7,13 @@ on: permissions: contents: read pull-requests: write + id-token: write jobs: classify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 @@ -21,70 +22,51 @@ jobs: env: BASE_REF: ${{ github.base_ref }} run: | - # Get the list of changed files and diff stat DIFF_STAT=$(git diff "origin/$BASE_REF"...HEAD --stat 2>/dev/null || echo "Unable to compute diff stat") CHANGED_FILES=$(git diff "origin/$BASE_REF"...HEAD --name-only 2>/dev/null || echo "") - - # Write to files to avoid shell escaping issues when passing to the action echo "$DIFF_STAT" > /tmp/diff_stat.txt echo "$CHANGED_FILES" > /tmp/changed_files.txt - name: Classify PR id: classifier continue-on-error: true - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - PR_TITLE: ${{ github.event.pull_request.title }} - PR_BODY: ${{ github.event.pull_request.body }} - run: | - CHANGED_FILES=$(cat /tmp/changed_files.txt) - DIFF_STAT=$(cat /tmp/diff_stat.txt) - - PROMPT="You are classifying a pull request for jitsi-meet, an open-source WebRTC - video conferencing platform built with React/Redux for web and React Native for mobile. - - PR Title: $PR_TITLE - PR Body: $PR_BODY - - Changed files: - $CHANGED_FILES - - Diff stat: - $DIFF_STAT - - Platform hints for changed files: - - Files in android/, *.android.ts → platform: android - - Files in ios/, *.ios.ts → platform: ios - - Files matching *.native.ts or in react-native-sdk/ → platform: native - - Files matching *.web.ts, *.web.tsx, css/, webpack.config.js, modules/ → platform: web - - Files matching *.any.ts or in react/features/ with no platform suffix → could be multiple platforms - - Based ONLY on the PR title, body, and changed files listed above, respond with ONLY - valid JSON (no explanation, no markdown fences): - { - \"type\": \"\", - \"platforms\": [\"\"], - \"summary\": \"<1-2 sentence plain English summary of what this PR does>\" - }" - - RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \ - -H "x-api-key: $ANTHROPIC_API_KEY" \ - -H "anthropic-version: 2023-06-01" \ - -H "content-type: application/json" \ - -d "$(jq -n --arg prompt "$PROMPT" '{ - model: "claude-haiku-4-5-20251001", - max_tokens: 256, - messages: [{role: "user", content: $prompt}] - }')") - - OUTPUT=$(echo "$RESPONSE" | jq -r '.content[0].text' | tr -d '\n') - echo "output<> $GITHUB_OUTPUT - echo "$OUTPUT" >> $GITHUB_OUTPUT - echo "EOF_DELIMITER" >> $GITHUB_OUTPUT - - - name: Apply labels and post comment + uses: anthropics/claude-code-action@beta + with: + model: claude-haiku-4-5-20251001 + github_token: ${{ secrets.GITHUB_TOKEN }} + direct_prompt: | + You are classifying a pull request for jitsi-meet, an open-source WebRTC + video conferencing platform built with React/Redux for web and React Native for mobile. + + PR Title: ${{ github.event.pull_request.title }} + PR Body: ${{ github.event.pull_request.body }} + + Changed files: + $(cat /tmp/changed_files.txt) + + Diff stat: + $(cat /tmp/diff_stat.txt) + + Platform hints for changed files: + - Files in android/, *.android.ts → platform: android + - Files in ios/, *.ios.ts → platform: ios + - Files matching *.native.ts or in react-native-sdk/ → platform: native + - Files matching *.web.ts, *.web.tsx, css/, webpack.config.js, modules/ → platform: web + - Files matching *.any.ts or in react/features/ with no platform suffix → could be multiple platforms + + Based ONLY on the PR title, body, and changed files listed above, respond with ONLY + valid JSON (no explanation, no markdown fences): + { + "type": "", + "platforms": [""], + "summary": "<1-2 sentence plain English summary of what this PR does>" + } + + - name: Apply labels if: always() - uses: actions/github-script@v7 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const pr = context.payload.pull_request; @@ -92,54 +74,49 @@ jobs: const repo = context.repo.repo; const issue_number = pr.number; - // --- Parse classifier output --- - const classifierOutput = '${{ steps.classifier.outputs.output }}'; + const validTypes = ['feature', 'fix', 'chore', 'refactor', 'language', 'documentation', 'test', 'security']; + const validPlatforms = ['web', 'android', 'ios', 'native']; + + // Read the JSON posted as a comment by claude-code-action + const comments = await github.rest.issues.listComments({ owner, repo, issue_number }); + const botComment = comments.data + .reverse() + .find(c => c.user.login === 'github-actions[bot]' && c.body.includes('"type"')); + let typeLabel = null; let platformLabels = []; - let parseError = false; - if (classifierOutput) { + if (botComment) { try { - // Extract JSON even if there's surrounding text - const match = classifierOutput.match(/\{[\s\S]*\}/); + const match = botComment.body.match(/\{[\s\S]*\}/); if (match) { const parsed = JSON.parse(match[0]); - const validTypes = ['feature', 'fix', 'chore', 'refactor', 'language', 'documentation', 'test', 'security']; - const validPlatforms = ['web', 'android', 'ios', 'native']; - - if (validTypes.includes(parsed.type)) { - typeLabel = parsed.type; - } + if (validTypes.includes(parsed.type)) typeLabel = parsed.type; if (Array.isArray(parsed.platforms)) { - platformLabels = parsed.platforms - .filter(p => validPlatforms.includes(p)); + platformLabels = parsed.platforms.filter(p => validPlatforms.includes(p)); } } } catch (e) { - parseError = true; core.warning(`Failed to parse classifier output: ${e.message}`); } + + // Delete the raw JSON comment + await github.rest.issues.deleteComment({ owner, repo, comment_id: botComment.id }); } - // --- Remove old type/platform labels --- - const validTypes = ['feature', 'fix', 'chore', 'refactor', 'language', 'documentation', 'test', 'security']; - const validPlatforms = ['web', 'android', 'ios', 'native']; + // Remove old type/platform labels const existingLabels = await github.rest.issues.listLabelsOnIssue({ owner, repo, issue_number }); const toRemove = existingLabels.data .filter(l => validTypes.includes(l.name) || validPlatforms.includes(l.name)) .map(l => l.name); - for (const label of toRemove) { try { await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label }); - } catch (e) { - // ignore if label was already removed - } + } catch (e) {} } - // --- Apply new labels --- + // Apply new labels const labelsToAdd = [typeLabel, ...platformLabels].filter(Boolean); if (labelsToAdd.length > 0) { await github.rest.issues.addLabels({ owner, repo, issue_number, labels: labelsToAdd }); } -