From 830e73e0dcd7880e0f03974ee61df72a0e5b46ed Mon Sep 17 00:00:00 2001 From: Frank Chen Date: Fri, 31 Jul 2026 11:47:22 -0700 Subject: [PATCH] ci: reuse shared notification and AI review workflows --- .github/prompts/ai-pr-review-schema.json | 15 - .github/workflows/ai-pr-review.yml | 432 +---------------------- .github/workflows/notify-issues.yml | 23 -- .github/workflows/notify-pr.yml | 24 -- .github/workflows/notify-release.yml | 29 -- .github/workflows/notify.yml | 19 + .github/workflows/pypi-publish.yml | 9 - scripts/post_ai_review_summary.sh | 263 -------------- scripts/prepare_ai_review_context.sh | 62 ---- scripts/prepare_ai_review_user.sh | 57 --- scripts/run_claude_isolated.sh | 28 -- 11 files changed, 24 insertions(+), 937 deletions(-) delete mode 100644 .github/prompts/ai-pr-review-schema.json delete mode 100644 .github/workflows/notify-issues.yml delete mode 100644 .github/workflows/notify-pr.yml delete mode 100644 .github/workflows/notify-release.yml create mode 100644 .github/workflows/notify.yml delete mode 100644 scripts/post_ai_review_summary.sh delete mode 100644 scripts/prepare_ai_review_context.sh delete mode 100644 scripts/prepare_ai_review_user.sh delete mode 100755 scripts/run_claude_isolated.sh diff --git a/.github/prompts/ai-pr-review-schema.json b/.github/prompts/ai-pr-review-schema.json deleted file mode 100644 index 191fab53..00000000 --- a/.github/prompts/ai-pr-review-schema.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "object", - "properties": { - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 4000, - "description": "Concise Markdown summary of the PR review and any residual test risk" - } - }, - "required": [ - "summary" - ], - "additionalProperties": false -} diff --git a/.github/workflows/ai-pr-review.yml b/.github/workflows/ai-pr-review.yml index b5e95fe8..fccea6e6 100644 --- a/.github/workflows/ai-pr-review.yml +++ b/.github/workflows/ai-pr-review.yml @@ -6,435 +6,13 @@ on: permissions: {} -concurrency: - group: ai-pr-review-${{ github.event.pull_request.number }} - cancel-in-progress: true - jobs: - approve_review: - name: Approve AI PR review - if: >- - github.event.pull_request.user.login == 'dependabot[bot]' || - github.event.pull_request.draft || - github.event.pull_request.head.repo.full_name != github.repository - runs-on: ubuntu-latest - environment: ai-pr-review - permissions: {} - steps: - - name: Record approval - run: echo "Approved review of ${{ github.event.pull_request.head.sha }}" - - claude-review: - name: Claude review - needs: approve_review - if: >- - always() && - ( - ( - github.event.pull_request.user.login != 'dependabot[bot]' && - !github.event.pull_request.draft && - github.event.pull_request.head.repo.full_name == github.repository - ) || - needs.approve_review.result == 'success' - ) - runs-on: ubuntu-latest - environment: ai-pr-review-runtime - timeout-minutes: 45 + ai-pr-review: permissions: contents: read id-token: write pull-requests: write - - steps: - # pull_request_target exposes repository secrets, so only check out the - # exact trusted base revision. The PR's code is never checked out. - - name: Check out base branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 1 - persist-credentials: false - ref: ${{ github.event.pull_request.base.sha }} - - - name: Prepare pull request review context - env: - EXPECTED_BASE_SHA: ${{ github.event.pull_request.base.sha }} - EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: bash scripts/prepare_ai_review_context.sh - - - name: Prepare shared review inputs - id: review-inputs - run: | - inline_marker="" - retry_inline_marker="" - { - echo 'prompt<> "$GITHUB_OUTPUT" - - - name: Prepare unprivileged Claude user - run: | - bash scripts/prepare_ai_review_user.sh claude-review - - # The Claude CLI runs through sudo. Preserve only its Bedrock - # credentials and the action's subprocess-isolation controls. - sudo sh -c \ - 'printf "%s\n" \ - "Defaults:runner env_keep += \"AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_REGION AWS_DEFAULT_REGION AWS_BEARER_TOKEN_BEDROCK\"" \ - "Defaults:runner env_keep += \"ANTHROPIC_BEDROCK_BASE_URL CLAUDE_CODE_USE_BEDROCK CLAUDE_CODE_ENTRYPOINT CLAUDE_CODE_ACTION CLAUDE_CODE_ATTRIBUTION_HEADER\"" \ - "Defaults:runner env_keep += \"CLAUDE_CODE_SUBPROCESS_ENV_SCRUB CLAUDE_CODE_SCRIPT_CAPS DETAILED_PERMISSION_MESSAGES MCP_TIMEOUT MCP_TOOL_TIMEOUT MAX_MCP_OUTPUT_TOKENS\"" \ - > /etc/sudoers.d/claude-review-env' - sudo chmod 440 /etc/sudoers.d/claude-review-env - sudo visudo -cf /etc/sudoers.d/claude-review-env - - - name: Configure AWS credentials for Claude - uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2 - with: - role-to-assume: ${{ secrets.BEDROCK_ROLE_ARN }} - role-session-name: claude-pr-review-${{ github.run_id }} - aws-region: us-east-1 - inline-session-policy: | - { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "bedrock:InvokeModel", - "bedrock:InvokeModelWithResponseStream" - ], - "Resource": "*" - }] - } - mask-aws-account-id: true - output-env-credentials: true - - - name: Review pull request with Claude on Amazon Bedrock - id: review - continue-on-error: true - uses: anthropics/claude-code-action@fa7e2f0a29a126f0b81cdcf360561b36e44cf608 # v1.0.180 - env: - AWS_REGION: us-east-1 - CLAUDE_CODE_ATTRIBUTION_HEADER: "0" - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - use_bedrock: "true" - path_to_claude_code_executable: ${{ github.workspace }}/scripts/run_claude_isolated.sh - # This also keeps the token out of the git remote configured by the action. - allowed_non_write_users: ${{ github.actor }} - classify_inline_comments: "false" - prompt: ${{ steps.review-inputs.outputs.prompt }} - claude_args: | - --model us.anthropic.claude-opus-4-8 - --max-turns 20 - --allowedTools "Read,Grep,Glob,mcp__github_inline_comment__create_inline_comment" - --disallowedTools "Bash,Write,Edit,NotebookEdit,WebFetch,WebSearch" - --json-schema '${{ steps.review-inputs.outputs.schema }}' - - - name: Retry pull request review with Claude on Amazon Bedrock - id: review-retry - if: steps.review.outcome == 'failure' - uses: anthropics/claude-code-action@fa7e2f0a29a126f0b81cdcf360561b36e44cf608 # v1.0.180 - env: - AWS_REGION: us-east-1 - CLAUDE_CODE_ATTRIBUTION_HEADER: "0" - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - use_bedrock: "true" - path_to_claude_code_executable: ${{ github.workspace }}/scripts/run_claude_isolated.sh - # This also keeps the token out of the git remote configured by the action. - allowed_non_write_users: ${{ github.actor }} - classify_inline_comments: "false" - prompt: ${{ steps.review-inputs.outputs.retry_prompt }} - claude_args: | - --model us.anthropic.claude-opus-4-8 - --max-turns 20 - --allowedTools "Read,Grep,Glob,mcp__github_inline_comment__create_inline_comment" - --disallowedTools "Bash,Write,Edit,NotebookEdit,WebFetch,WebSearch" - --json-schema '${{ steps.review-inputs.outputs.schema }}' - - - name: Post review summary - env: - EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - CURRENT_INLINE_COMMENT_MARKER: >- - ${{ - steps.review.outcome == 'success' && - steps.review-inputs.outputs.inline_marker || - steps.review-inputs.outputs.retry_inline_marker - }} - REVIEW_OUTPUT: >- - ${{ - steps.review.outcome == 'success' && - steps.review.outputs.structured_output || - steps.review-retry.outputs.structured_output - }} - run: | - set -euo pipefail - - summary="$( - jq -ser ' - select(length == 1) | - .[0] | - select(type == "object" and keys == ["summary"]) | - .summary | - select( - type == "string" and - length <= 4000 and - test("[^[:space:]]") - ) - ' <<< "$REVIEW_OUTPUT" - )" - summary="$( - sed -E \ - -e '1{/^## (Claude|Codex) AI review$/d;}' \ - -e '/[^[:space:]]/,$!d' \ - <<< "$summary" - )" - if [[ -z "${summary//[[:space:]]/}" ]]; then - echo "::error::Claude returned an empty review body." - exit 1 - fi - - summary_file="${RUNNER_TEMP}/claude-review-summary.md" - printf '%s' "$summary" > "$summary_file" - bash scripts/post_ai_review_summary.sh \ - claude \ - "$EXPECTED_HEAD_SHA" \ - "$summary_file" - - codex-review: - name: Codex review - needs: approve_review - if: >- - always() && - ( - ( - github.event.pull_request.user.login != 'dependabot[bot]' && - !github.event.pull_request.draft && - github.event.pull_request.head.repo.full_name == github.repository - ) || - needs.approve_review.result == 'success' - ) - runs-on: ubuntu-latest - environment: ai-pr-review-runtime - timeout-minutes: 45 - permissions: - contents: read - id-token: write - pull-requests: write - - steps: - # pull_request_target exposes repository secrets, so only check out the - # exact trusted base revision. The PR's code is never checked out. - - name: Check out base branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 1 - persist-credentials: false - ref: ${{ github.event.pull_request.base.sha }} - - - name: Prepare pull request review context - env: - EXPECTED_BASE_SHA: ${{ github.event.pull_request.base.sha }} - EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: bash scripts/prepare_ai_review_context.sh - - - name: Prepare unprivileged Codex user - run: | - bash scripts/prepare_ai_review_user.sh codex-review - - # Codex runs through sudo, so preserve its AWS SDK credential chain. - sudo sh -c \ - 'printf "%s\n" \ - "Defaults:runner env_keep += \"AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_REGION AWS_DEFAULT_REGION\"" \ - > /etc/sudoers.d/codex-review-env' - sudo chmod 440 /etc/sudoers.d/codex-review-env - sudo visudo -cf /etc/sudoers.d/codex-review-env - - current_userns="$( - sysctl -n kernel.unprivileged_userns_clone 2>/dev/null || true - )" - if [[ -n "$current_userns" && "$current_userns" != "1" ]]; then - sudo sysctl -w kernel.unprivileged_userns_clone=1 - fi - - current_apparmor="$( - sysctl -n kernel.apparmor_restrict_unprivileged_userns \ - 2>/dev/null || true - )" - if [[ -n "$current_apparmor" && "$current_apparmor" != "0" ]]; then - sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - fi - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "24" - - - name: Install Codex CLI - run: npm install -g "@openai/codex@0.145.0" - - - name: Configure AWS credentials for Codex - id: aws-credentials - uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2 - with: - role-to-assume: ${{ secrets.BEDROCK_ROLE_ARN }} - role-session-name: codex-pr-review-${{ github.run_id }} - aws-region: us-east-1 - inline-session-policy: | - { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "bedrock-mantle:CreateInference", - "bedrock-mantle:GetProject", - "bedrock-mantle:ListProjects", - "bedrock-mantle:ListTagsForResources" - ], - "Resource": "*" - }] - } - mask-aws-account-id: true - output-credentials: true - output-env-credentials: false - - - name: Review pull request with Codex on Amazon Bedrock - env: - AWS_ACCESS_KEY_ID: ${{ steps.aws-credentials.outputs.aws-access-key-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.aws-credentials.outputs.aws-secret-access-key }} - AWS_SESSION_TOKEN: ${{ steps.aws-credentials.outputs.aws-session-token }} - AWS_REGION: us-east-1 - AWS_DEFAULT_REGION: us-east-1 - run: | - set -euo pipefail - - codex_bin="$(command -v codex)" - output_dir="$( - sudo -u codex-review mktemp -d -t codex-review.XXXXXX - )" - output_file="${output_dir}/review.json" - output_schema="${GITHUB_WORKSPACE}/.github/prompts/ai-pr-review-schema.json" - trap 'sudo rm -rf "$output_dir"' EXIT - - sudo -u codex-review -- sh -c ' - : "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID was not preserved by sudo}" - : "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY was not preserved by sudo}" - : "${AWS_SESSION_TOKEN:?AWS_SESSION_TOKEN was not preserved by sudo}" - : "${AWS_REGION:?AWS_REGION was not preserved by sudo}" - ' - - review_ready=false - for attempt in 1 2; do - sudo -u codex-review -- rm -f "$output_file" - - # Keep enough of the job budget for a complete second attempt. - if sudo -u codex-review -- timeout \ - --signal=TERM \ - --kill-after=30s \ - 20m \ - env \ - CODEX_HOME=/home/codex-review/.codex \ - "$codex_bin" exec \ - --skip-git-repo-check \ - --cd "$GITHUB_WORKSPACE" \ - --output-last-message "$output_file" \ - --output-schema "$output_schema" \ - --color never \ - --model openai.gpt-5.6-sol \ - --config 'model_reasoning_effort="xhigh"' \ - --config 'model_provider="amazon-bedrock"' \ - --config 'default_permissions=":read-only"' \ - --ephemeral < .github/prompts/ai-pr-review.md - then - if sudo -u codex-review -- jq -se ' - length == 1 and - ( - .[0] | - type == "object" and - keys == ["summary"] and - ( - .summary | - type == "string" and - length <= 4000 and - test("[^[:space:]]") - ) - ) - ' "$output_file" > /dev/null - then - review_ready=true - break - fi - - echo "::warning::Codex attempt ${attempt} returned invalid review output." - else - echo "::warning::Codex attempt ${attempt} failed." - fi - done - - if [[ "$review_ready" != "true" ]]; then - echo "::error::Codex did not return a valid review after two attempts." - exit 1 - fi - - sudo install \ - -m 600 \ - -o runner \ - -g runner \ - "$output_file" \ - "${RUNNER_TEMP}/codex-review.json" - - - name: Post Codex review - env: - CODEX_REVIEW_FILE: ${{ runner.temp }}/codex-review.json - EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - set -euo pipefail - - CODEX_REVIEW="$( - jq -ser ' - select(length == 1) | - .[0] | - select(type == "object" and keys == ["summary"]) | - .summary | - select( - type == "string" and - length <= 4000 and - test("[^[:space:]]") - ) - ' "$CODEX_REVIEW_FILE" - )" - CODEX_REVIEW="$( - sed -E \ - -e '1{/^## (Claude|Codex) AI review$/d;}' \ - -e '/[^[:space:]]/,$!d' \ - <<< "$CODEX_REVIEW" - )" - if [[ -z "${CODEX_REVIEW//[[:space:]]/}" ]]; then - echo "::error::Codex returned an empty review." - exit 1 - fi - - summary_file="${RUNNER_TEMP}/codex-review-summary.md" - printf '%s' "$CODEX_REVIEW" > "$summary_file" - bash scripts/post_ai_review_summary.sh \ - codex \ - "$EXPECTED_HEAD_SHA" \ - "$summary_file" + uses: aws/aws-durable-execution-ci/.github/workflows/ai-pr-review.yml@38ae538ba1950755b0fe77bc1ca24b1b4fbbb104 + with: + prompt-path: .github/prompts/ai-pr-review.md + secrets: inherit diff --git a/.github/workflows/notify-issues.yml b/.github/workflows/notify-issues.yml deleted file mode 100644 index 8e8c9f40..00000000 --- a/.github/workflows/notify-issues.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Notify Slack - Issues - -on: - issues: - types: [opened, reopened] - -permissions: {} - -jobs: - notify: - runs-on: ubuntu-latest - steps: - - name: Send issue notification to Slack - uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL_ISSUE }} - webhook-type: incoming-webhook - payload: | - { - "action": "${{ github.event.action }}", - "issue_url": "${{ github.event.issue.html_url }}", - "package_name": "${{ github.repository }}" - } diff --git a/.github/workflows/notify-pr.yml b/.github/workflows/notify-pr.yml deleted file mode 100644 index 31fcfadf..00000000 --- a/.github/workflows/notify-pr.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Notify Slack - Pull Requests - -on: - pull_request_target: - types: [opened, reopened, ready_for_review] - -permissions: {} - -jobs: - notify: - runs-on: ubuntu-latest - steps: - - name: Send pull request notification to Slack - if: github.event.pull_request.draft == false - uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL_PR }} - webhook-type: incoming-webhook - payload: | - { - "action": "${{ github.event.action }}", - "pr_url": "${{ github.event.pull_request.html_url }}", - "package_name": "${{ github.repository }}" - } diff --git a/.github/workflows/notify-release.yml b/.github/workflows/notify-release.yml deleted file mode 100644 index bbee7fd0..00000000 --- a/.github/workflows/notify-release.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Notify Slack - Release - -on: - workflow_call: - inputs: - tag_name: - required: true - type: string - release_url: - required: true - type: string - -permissions: {} - -jobs: - notify: - runs-on: ubuntu-latest - steps: - - name: Send release notification to Slack - uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL_RELEASE }} - webhook-type: incoming-webhook - payload: | - { - "tag_name": "${{ inputs.tag_name }}", - "release_url": "${{ inputs.release_url }}", - "package_name": "${{ github.repository }}" - } diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml new file mode 100644 index 00000000..1f268739 --- /dev/null +++ b/.github/workflows/notify.yml @@ -0,0 +1,19 @@ +name: Notify Slack + +on: + pull_request_target: + types: [opened, reopened, ready_for_review] + issues: + types: [opened, reopened] + release: + types: [published] + +permissions: {} + +jobs: + notify: + uses: aws/aws-durable-execution-ci/.github/workflows/notify.yml@38ae538ba1950755b0fe77bc1ca24b1b4fbbb104 + secrets: + SLACK_WEBHOOK_URL_PR: ${{ secrets.SLACK_WEBHOOK_URL_PR }} + SLACK_WEBHOOK_URL_ISSUE: ${{ secrets.SLACK_WEBHOOK_URL_ISSUE }} + SLACK_WEBHOOK_URL_RELEASE: ${{ secrets.SLACK_WEBHOOK_URL_RELEASE }} diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 54fe1949..dd9a2f40 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -82,12 +82,3 @@ jobs: uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 with: packages-dir: dist/ - - notify-release: - if: always() && contains(needs.pypi-publish.result, 'success') - needs: [pypi-publish] - uses: ./.github/workflows/notify-release.yml - with: - tag_name: ${{ github.event.release.tag_name }} - release_url: ${{ github.event.release.html_url }} - secrets: inherit diff --git a/scripts/post_ai_review_summary.sh b/scripts/post_ai_review_summary.sh deleted file mode 100644 index 8857e918..00000000 --- a/scripts/post_ai_review_summary.sh +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if [[ "$#" -ne 3 ]]; then - echo "usage: $0 " >&2 - exit 2 -fi - -reviewer="$1" -expected_head_sha="$2" -summary_file="$3" - -case "$reviewer" in - claude) - marker="" - title="Claude AI review" - ;; - codex) - marker="" - title="Codex AI review" - ;; - *) - echo "unsupported AI reviewer: $reviewer" >&2 - exit 2 - ;; -esac - -: "${GH_TOKEN:?GH_TOKEN must be set}" -: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" -: "${GITHUB_RUN_ID:?GITHUB_RUN_ID must be set}" -: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL must be set}" -: "${PR_NUMBER:?PR_NUMBER must be set}" - -current_inline_comment_marker="${CURRENT_INLINE_COMMENT_MARKER:-}" -inline_comment_marker_pattern="^$" -if [[ - -n "$current_inline_comment_marker" && - ! "$current_inline_comment_marker" =~ $inline_comment_marker_pattern -]]; then - echo "invalid current inline comment marker: $current_inline_comment_marker" >&2 - exit 2 -fi - -if [[ ! -r "$summary_file" ]]; then - echo "AI review summary is not readable: $summary_file" >&2 - exit 2 -fi - -summary="$(cat "$summary_file")" -if [[ -z "${summary//[[:space:]]/}" ]]; then - echo "::error::$title returned an empty review body." - exit 1 -fi - -current_head_sha="$( - gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq .head.sha -)" -if [[ "$current_head_sha" != "$expected_head_sha" ]]; then - echo "::error::The PR changed while it was being reviewed." - exit 1 -fi - -run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" -# shellcheck disable=SC2016 # Markdown backticks are intentionally literal. -printf -v body '%s\n## %s\n\n%s\n\nReviewed commit `%s`. [Workflow run](%s)' \ - "$marker" "$title" "$summary" "$expected_head_sha" "$run_url" - -new_comment_id="$( - gh api \ - --method POST \ - "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ - --raw-field body="$body" \ - --jq .node_id -)" -if [[ -z "$new_comment_id" ]]; then - echo "::error::GitHub did not return the new AI review comment ID." - exit 1 -fi - -owner="${GITHUB_REPOSITORY%%/*}" -repository="${GITHUB_REPOSITORY#*/}" -comments_file="$(mktemp "${RUNNER_TEMP:-/tmp}/ai-review-comments.XXXXXX")" -inline_comments_file="" -cleanup_temp_files() { - rm -f "$comments_file" - if [[ -n "$inline_comments_file" ]]; then - rm -f "$inline_comments_file" - fi -} -trap cleanup_temp_files EXIT - -minimize_comment() { - local comment_id="$1" - - # shellcheck disable=SC2016 # GraphQL variables are intentionally literal. - gh api graphql \ - -F id="$comment_id" \ - -f query=' - mutation($id: ID!) { - minimizeComment( - input: { - subjectId: $id, - classifier: OUTDATED - } - ) { - minimizedComment { - isMinimized - } - } - } - ' > /dev/null -} - -# shellcheck disable=SC2016 # GraphQL variables are intentionally literal. -if ! gh api graphql \ - --paginate \ - -F owner="$owner" \ - -F repository="$repository" \ - -F number="$PR_NUMBER" \ - -f query=' - query( - $owner: String!, - $repository: String!, - $number: Int!, - $endCursor: String - ) { - repository(owner: $owner, name: $repository) { - pullRequest(number: $number) { - comments(first: 100, after: $endCursor) { - nodes { - id - body - isMinimized - author { - login - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - } - } - ' > "$comments_file"; then - echo "::warning::Failed to list previous $title comments for cleanup." -else - previous_comment_count=0 - while IFS= read -r comment_id; do - [[ -n "$comment_id" ]] || continue - - if minimize_comment "$comment_id"; then - previous_comment_count=$((previous_comment_count + 1)) - else - echo "::warning::Failed to minimize previous $title comment ($comment_id)." - fi - done < <( - jq -rs \ - --arg current_id "$new_comment_id" \ - --arg marker "$marker" \ - --arg legacy_header "## $title" \ - ' - .[] - | .data.repository.pullRequest.comments.nodes[] - | select(.id != $current_id) - | select(.isMinimized == false) - | select(.author.login == "github-actions") - | (.body | split("\n")[0]) as $first_line - | select( - $first_line == $marker - or $first_line == $legacy_header - ) - | .id - ' \ - "$comments_file" - ) - - echo "Minimized $previous_comment_count previous $title comment(s)." -fi - -if [[ -z "$current_inline_comment_marker" ]]; then - exit 0 -fi - -inline_comments_file="$( - mktemp "${RUNNER_TEMP:-/tmp}/ai-review-inline-comments.XXXXXX" -)" - -# shellcheck disable=SC2016 # GraphQL variables are intentionally literal. -if ! gh api graphql \ - --paginate \ - -F owner="$owner" \ - -F repository="$repository" \ - -F number="$PR_NUMBER" \ - -f query=' - query( - $owner: String!, - $repository: String!, - $number: Int!, - $endCursor: String - ) { - repository(owner: $owner, name: $repository) { - pullRequest(number: $number) { - reviewThreads(first: 100, after: $endCursor) { - nodes { - comments(first: 1) { - nodes { - id - body - isMinimized - replyTo { - id - } - author { - login - } - } - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - } - } - ' > "$inline_comments_file"; then - echo "::warning::Failed to list previous $title inline comments for cleanup." - exit 0 -fi - -previous_inline_comment_count=0 -while IFS= read -r comment_id; do - [[ -n "$comment_id" ]] || continue - - if minimize_comment "$comment_id"; then - previous_inline_comment_count=$((previous_inline_comment_count + 1)) - else - echo "::warning::Failed to minimize previous $title inline comment ($comment_id)." - fi -done < <( - jq -rs \ - --arg current_marker "$current_inline_comment_marker" \ - --arg marker_pattern "$inline_comment_marker_pattern" \ - ' - .[] - | .data.repository.pullRequest.reviewThreads.nodes[] - | .comments.nodes[] - | select(.replyTo == null) - | select(.isMinimized == false) - | select(.author.login == "github-actions") - | (.body | split("\n")[0]) as $first_line - | select($first_line != $current_marker) - | select($first_line | test($marker_pattern)) - | .id - ' \ - "$inline_comments_file" -) - -echo "Minimized $previous_inline_comment_count previous $title inline comment(s)." diff --git a/scripts/prepare_ai_review_context.sh b/scripts/prepare_ai_review_context.sh deleted file mode 100644 index 6f82e998..00000000 --- a/scripts/prepare_ai_review_context.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -context_dir="${GITHUB_WORKSPACE}/.ai-review-context" -if [[ -e "$context_dir" ]]; then - echo "::error::The trusted base contains the reserved review context path." - exit 1 -fi -mkdir "$context_dir" - -verify_current_head() { - local current_head_sha - current_head_sha="$( - gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq .head.sha - )" - if [[ "$current_head_sha" != "$EXPECTED_HEAD_SHA" ]]; then - echo "::error::The PR changed; review its latest workflow run instead." - exit 1 - fi -} - -verify_current_head - -gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ - > "$context_dir/pr.raw.json" -jq \ - --arg base_sha "$EXPECTED_BASE_SHA" \ - --arg head_sha "$EXPECTED_HEAD_SHA" \ - '{ - number, - title, - body, - html_url, - draft, - author_association, - additions, - deletions, - changed_files, - user: .user.login, - base: {ref: .base.ref, sha: $base_sha}, - head: {ref: .head.ref, sha: $head_sha} - }' \ - "$context_dir/pr.raw.json" > "$context_dir/pr.json" - -gh api \ - -H "Accept: application/vnd.github.v3.diff" \ - "repos/${GITHUB_REPOSITORY}/compare/${EXPECTED_BASE_SHA}...${EXPECTED_HEAD_SHA}" \ - > "$context_dir/pr.diff" - -expected_file_count="$(jq -er '.changed_files' "$context_dir/pr.json")" -diff_file_count="$( - awk '/^diff --git / { count++ } END { print count + 0 }' \ - "$context_dir/pr.diff" -)" -if [[ "$diff_file_count" != "$expected_file_count" ]]; then - echo "::error::GitHub returned an incomplete PR diff." - exit 1 -fi - -verify_current_head -rm "$context_dir/pr.raw.json" diff --git a/scripts/prepare_ai_review_user.sh b/scripts/prepare_ai_review_user.sh deleted file mode 100644 index d5fbafdb..00000000 --- a/scripts/prepare_ai_review_user.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if [[ "$#" -ne 1 ]]; then - echo "usage: $0 " >&2 - exit 2 -fi - -review_user="$1" -home_dir="/home/${review_user}" - -if [[ -z "${GITHUB_WORKSPACE:-}" || ! -d "$GITHUB_WORKSPACE" ]]; then - echo "GITHUB_WORKSPACE must name an existing directory" >&2 - exit 2 -fi - -case "$review_user" in - claude-review) - private_dirs=("${home_dir}/.claude" "${home_dir}/tmp") - ;; - codex-review) - private_dirs=("${home_dir}/.codex") - ;; - *) - echo "unsupported AI review user: $review_user" >&2 - exit 2 - ;; -esac - -sudo adduser \ - --system \ - --home "$home_dir" \ - --shell /bin/bash \ - --group "$review_user" - -sudo install \ - -d \ - -m 700 \ - -o "$review_user" \ - -g "$review_user" \ - "${private_dirs[@]}" - -# Standard hosted runners make /home/runner traversable. Keep this exact ACL -# fallback for images that use a private runner home instead. -if ! sudo -u "$review_user" test -x /home/runner; then - sudo setfacl -m "u:${review_user}:--x" /home/runner -fi - -sudo chown -R "runner:${review_user}" "$GITHUB_WORKSPACE" -sudo chmod -R g-w,o-rwx "$GITHUB_WORKSPACE" -sudo chmod -R g+rX "$GITHUB_WORKSPACE" - -if ! sudo -u "$review_user" test -r "$GITHUB_WORKSPACE/README.md"; then - echo "$review_user cannot read the trusted workspace" >&2 - exit 1 -fi diff --git a/scripts/run_claude_isolated.sh b/scripts/run_claude_isolated.sh deleted file mode 100755 index 004e1c94..00000000 --- a/scripts/run_claude_isolated.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -claude_bin="${GITHUB_ACTION_PATH}/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64/claude" -bun_dir="${GITHUB_ACTION_PATH}/bin" - -if [[ ! -x "$claude_bin" ]]; then - echo "::error::The pinned Claude action did not install its bundled Linux CLI." - exit 1 -fi -if [[ ! -x "${bun_dir}/bun" ]]; then - echo "::error::The pinned Claude action did not expose its Bun executable." - exit 1 -fi -if ! sudo -H -u claude-review -- test -x "$claude_bin"; then - echo "::error::claude-review cannot execute the pinned Claude CLI." - exit 1 -fi -if ! sudo -H -u claude-review -- test -x "${bun_dir}/bun"; then - echo "::error::claude-review cannot execute the pinned Bun runtime." - exit 1 -fi - -exec sudo -H -u claude-review -- env \ - PATH="${bun_dir}:/usr/local/bin:/usr/bin:/bin" \ - TMPDIR=/home/claude-review/tmp \ - "$claude_bin" "$@"