Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/agents-pr-health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ jobs:
health:
uses: stranske/Workflows/.github/workflows/reusable-agents-pr-health.yml@main
with:
dry_run: ${{ inputs.dry_run || false }}
max_prs: ${{ inputs.max_prs || 10 }}
cron_interval_hours: ${{ vars.PR_HEALTH_INTERVAL_HOURS || 1 }}
is_scheduled_trigger: ${{ github.event_name == 'schedule' }}
dry_run: ${{ inputs.dry_run && 'true' || 'false' }}
max_prs: ${{ inputs.max_prs || '10' }}
cron_interval_hours: ${{ vars.PR_HEALTH_INTERVAL_HOURS || '1' }}
Comment on lines +58 to +59
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max_prs is declared as a number input, but the expression ${{ inputs.max_prs || '10' }} returns a number when provided and a string when defaulting. If the called reusable workflow expects a consistent type (common when passing inputs across workflows), cast inputs.max_prs to string as well (or keep the value numeric by using a numeric default).

Copilot uses AI. Check for mistakes.
is_scheduled_trigger: ${{ github.event_name == 'schedule' && 'true' || 'false' }}
secrets: inherit
238 changes: 238 additions & 0 deletions .github/workflows/maint-76-claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
name: Claude Code Review (Opt-in)

'on':
workflow_dispatch:
inputs:
pr_number:
description: 'Pull request number to review'
required: true
type: string
pull_request:
types: [labeled, synchronize, reopened, ready_for_review]

permissions:
contents: read

jobs:
resolve-target:
name: Resolve review target
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
should_run: ${{ steps.resolve.outputs.should_run }}
pr_number: ${{ steps.resolve.outputs.pr_number }}
reason: ${{ steps.resolve.outputs.reason }}
steps:
- id: resolve
uses: actions/github-script@v8
with:
script: |
const eventName = context.eventName;
let shouldRun = "false";
let prNumber = "";
let reason = "not_opted_in";

if (eventName === "workflow_dispatch") {
const raw = `${core.getInput("pr_number") || ""}`.trim();
if (!/^\d+$/.test(raw)) {
reason = "invalid_pr_number";
} else {
const pull_number = Number(raw);
const { owner, repo } = context.repo;
try {
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number,
});
prNumber = String(pull_number);
if (pr.state !== "open") {
reason = "pr_not_open";
} else if (pr.draft) {
reason = "pr_is_draft";
} else {
shouldRun = "true";
reason = "manual_dispatch";
}
} catch (error) {
reason = "pr_not_found";
core.warning(`Unable to load PR #${pull_number}: ${error.message}`);
}
}
} else if (eventName === "pull_request") {
const pr = context.payload.pull_request;
prNumber = String(pr.number);
const labels = (pr.labels || []).map((label) => label.name);
if (labels.includes("claude-review")) {
shouldRun = "true";
reason = "label_opt_in";
Comment on lines +67 to +70
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve-target skips draft PRs for manual dispatch, but the pull_request path doesn’t check pr.draft. As written, adding the claude-review label to a draft PR will run the review (and spend tokens) on labeled/synchronize/reopened events; if drafts should be excluded, add the same pr.draft guard in the PR-event branch.

Suggested change
const labels = (pr.labels || []).map((label) => label.name);
if (labels.includes("claude-review")) {
shouldRun = "true";
reason = "label_opt_in";
if (pr.draft) {
reason = "pr_is_draft";
} else {
const labels = (pr.labels || []).map((label) => label.name);
if (labels.includes("claude-review")) {
shouldRun = "true";
reason = "label_opt_in";
}

Copilot uses AI. Check for mistakes.
}
} else {
reason = "unsupported_event";
}

core.setOutput("should_run", shouldRun);
core.setOutput("pr_number", prNumber);
core.setOutput("reason", reason);

detect-secret:
name: Detect Claude token
needs: resolve-target
if: ${{ needs.resolve-target.outputs.should_run == 'true' }}
runs-on: ubuntu-latest
outputs:
has_token: ${{ steps.check.outputs.has_token }}
token_reason: ${{ steps.check.outputs.token_reason }}
workflow_unchanged: ${{ steps.workflow-integrity.outputs.workflow_unchanged }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0

- id: workflow-integrity
name: Check workflow matches base
run: |
set -euo pipefail

base_ref="${{ github.base_ref }}"
if [ -z "$base_ref" ]; then
echo "workflow_unchanged=true" >>"$GITHUB_OUTPUT"
exit 0
Comment on lines +102 to +103
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the workflow-integrity guard, github.base_ref is empty for workflow_dispatch, so the step unconditionally sets workflow_unchanged=true and allows the secrets-gated jobs to run even when this workflow file was modified on the dispatched ref. This defeats the “workflow must match default branch to safely access secrets” intent for manual dispatch; consider comparing the file against the repository default branch for workflow_dispatch too (or failing closed unless github.ref_name is the default branch).

Suggested change
echo "workflow_unchanged=true" >>"$GITHUB_OUTPUT"
exit 0
echo "github.base_ref is empty; resolving repository default branch from origin/HEAD"
# Ensure we have the remote HEAD reference so we can determine the default branch.
git fetch --no-tags origin '+HEAD:refs/remotes/origin/HEAD'
# Resolve default branch name (strip the leading "origin/").
default_remote_head="$(git symbolic-ref --short refs/remotes/origin/HEAD || true)"
default_branch="${default_remote_head#origin/}"
if [ -z "$default_branch" ]; then
echo "Could not determine default branch; failing closed."
echo "workflow_unchanged=false" >>"$GITHUB_OUTPUT"
exit 0
fi
base_ref="$default_branch"
echo "Using default branch '${base_ref}' for workflow integrity check."

Copilot uses AI. Check for mistakes.
fi

git fetch --no-tags origin \
"refs/heads/${base_ref}:refs/remotes/origin/${base_ref}"

set +e
diff_output="$(git diff --name-only "origin/${base_ref}"...HEAD -- \
.github/workflows/maint-76-claude-code-review.yml)"
diff_ec=$?
set -e

if [ "$diff_ec" -gt 1 ]; then
echo "workflow_unchanged=false" >>"$GITHUB_OUTPUT"
exit 0
fi

if [ -n "$diff_output" ]; then
echo "workflow_unchanged=false" >>"$GITHUB_OUTPUT"
else
echo "workflow_unchanged=true" >>"$GITHUB_OUTPUT"
fi

- id: check
env:
CLAUDE_CODE_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
run: |
if [ -n "${CLAUDE_CODE_TOKEN}" ]; then
{
echo "has_token=true"
echo "token_reason=configured"
} >>"$GITHUB_OUTPUT"
else
{
echo "has_token=false"
echo "token_reason=missing"
} >>"$GITHUB_OUTPUT"
fi

claude-review:
name: Run Claude Code Review
needs: [resolve-target, detect-secret]
if: >-
${{
needs.resolve-target.outputs.should_run == 'true' &&
needs.detect-secret.outputs.has_token == 'true' &&
needs.detect-secret.outputs.workflow_unchanged == 'true'
}}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1

- name: Run Claude Code Review
id: claude
continue-on-error: true
uses: anthropics/claude-code-action@220272d38887a1caed373da96a9ffdb0919c26cc
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
allowed_bots: '*'
claude_args: |
--max-turns 8
plugin_marketplaces: https://github.com/anthropics/claude-code.git
plugins: code-review@claude-code-plugins
prompt: >-
/code-review:code-review
${{ github.repository }}/pull/${{ needs.resolve-target.outputs.pr_number }}

- name: Record review failure (non-blocking)
if: steps.claude.outcome == 'failure'
run: |
cat <<'EOF' >>"$GITHUB_STEP_SUMMARY"
### Claude Code Review failed (non-blocking)
The Claude review action failed to run for this PR.
Fix the token/config if automated reviews are required.
EOF

skipped-not-opted-in:
name: Skip review (not opted in)
needs: resolve-target
if: ${{ needs.resolve-target.outputs.should_run != 'true' }}
runs-on: ubuntu-latest
steps:
- name: Explain opt-in behavior
run: |
cat <<'EOF' >>"$GITHUB_STEP_SUMMARY"
### Claude Code Review skipped
This workflow is opt-in.

To run automatically for a PR, add the `claude-review` label.
To run on demand, use workflow dispatch and provide `pr_number`.
EOF

skipped-missing-secret:
name: Skip review (secret missing)
needs: [resolve-target, detect-secret]
if: >-
${{
needs.resolve-target.outputs.should_run == 'true' &&
needs.detect-secret.outputs.token_reason == 'missing'
}}
runs-on: ubuntu-latest
steps:
- name: Explain missing secret
run: |
cat <<'EOF' >>"$GITHUB_STEP_SUMMARY"
### Claude Code Review skipped
The `CLAUDE_CODE_OAUTH_TOKEN` secret is not configured.
Add it to enable automated reviews.
EOF

skipped-workflow-modified:
name: Skip review (workflow modified)
needs: [resolve-target, detect-secret]
if: >-
${{
needs.resolve-target.outputs.should_run == 'true' &&
needs.detect-secret.outputs.workflow_unchanged != 'true'
}}
runs-on: ubuntu-latest
steps:
- name: Explain workflow integrity guard
run: |
cat <<'EOF' >>"$GITHUB_STEP_SUMMARY"
### Claude Code Review skipped
This workflow was modified in the PR.
The Claude review action requires the workflow to match the default
branch to safely access secrets.
EOF
Loading