Skip to content
Open
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
6 changes: 6 additions & 0 deletions .github/workflows/review-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ on:
required: false
type: string
default: ''
max-diff-lines:
description: 'Auto-filter cap: if the diff exceeds this many lines after removing score-0 files, lowest-risk files are progressively excluded until it fits. Set to 0 to disable. Default: 3000.'
required: false
type: number
default: 3000
trigger-run-id:
description: "Workflow run ID from pr-review-trigger.yml — used to download the event context artifact. When set, resolve-context job downloads the artifact and routes to the appropriate job."
required: false
Expand Down Expand Up @@ -339,6 +344,7 @@ jobs:
additional-prompt: ${{ inputs.additional-prompt }}
add-prompt-files: ${{ inputs.add-prompt-files }}
exclude-paths: ${{ inputs.exclude-paths }}
max-diff-lines: ${{ inputs.max-diff-lines }}
model: ${{ inputs.model }}
github-token: ${{ env.GITHUB_APP_TOKEN || github.token }}
anthropic-api-key: ${{ env.ANTHROPIC_API_KEY_FROM_SSM || secrets.ANTHROPIC_API_KEY }}
Expand Down
63 changes: 45 additions & 18 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ on:
branches: [main]
push:
branches: [main]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to run mention-reply E2E tests against'
required: true
type: string

permissions:
contents: read
Expand Down Expand Up @@ -188,13 +194,13 @@ jobs:
test-mention-reply-toplevel:
name: Mention Reply (Top-Level) E2E Test
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
if: github.event_name == 'workflow_dispatch'
permissions:
contents: read
id-token: write
issues: write
env:
TEST_PR_NUMBER: ${{ github.event.pull_request.number }}
TEST_PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
steps:
- name: Check if fork PR
id: fork-check
Expand Down Expand Up @@ -233,11 +239,26 @@ jobs:
if: steps.fork-check.outputs.is_fork != 'true'
uses: docker/cagent-action/setup-credentials@2a43a3882401f45e3114df7f6d66eca184993a90 # v1.5.2

- name: Create anchor issue comment on current PR
if: steps.fork-check.outputs.is_fork != 'true'
id: create-anchor
env:
GH_TOKEN: ${{ env.GITHUB_APP_TOKEN || github.token }}
run: |
COMMENT_ID=$(gh api repos/docker/cagent-action/issues/$TEST_PR_NUMBER/comments \
--method POST \
--raw-field body="@docker-agent this is an automated e2e test — please reply with a brief acknowledgement." \
--jq .id)
echo "Created test anchor comment ID: $COMMENT_ID"
echo "test_comment_id=$COMMENT_ID" >> $GITHUB_OUTPUT

- name: Write synthetic issue_comment event
if: steps.fork-check.outputs.is_fork != 'true'
run: |
COMMENT_ID="${{ steps.create-anchor.outputs.test_comment_id }}"
jq -n \
--arg actor "${{ github.actor }}" \
--argjson comment_id "$COMMENT_ID" \
--argjson pr_number "$TEST_PR_NUMBER" \
'{
"action": "created",
Expand All @@ -246,7 +267,7 @@ jobs:
"pull_request": { "url": ("https://api.github.com/repos/docker/cagent-action/pulls/" + ($pr_number | tostring)) }
},
"comment": {
"id": 9999999901,
"id": $comment_id,
"body": "@docker-agent this is an automated e2e test — please reply with a brief acknowledgement.",
"user": { "login": $actor, "type": "User" }
},
Expand All @@ -260,13 +281,13 @@ jobs:
- name: Run mention-reply handler
if: steps.fork-check.outputs.is_fork != 'true'
id: mention-handler
uses: ./.github/actions/mention-reply
env:
GITHUB_EVENT_PATH: /tmp/test-event-toplevel.json
GITHUB_EVENT_NAME: issue_comment
with:
github-token: ${{ env.GITHUB_APP_TOKEN || github.token }}
org-membership-token: ${{ env.ORG_MEMBERSHIP_TOKEN || github.token }}
INPUT_GITHUB-TOKEN: ${{ env.GITHUB_APP_TOKEN || github.token }}
INPUT_ORG-MEMBERSHIP-TOKEN: ${{ env.ORG_MEMBERSHIP_TOKEN || github.token }}
run: |
export GITHUB_EVENT_PATH=/tmp/test-event-toplevel.json
export GITHUB_EVENT_NAME=issue_comment
node "$GITHUB_WORKSPACE/dist/mention-reply.js"

- name: Assert should-reply output
if: steps.fork-check.outputs.is_fork != 'true'
Expand Down Expand Up @@ -312,10 +333,11 @@ jobs:
echo "✅ Reply posted successfully ($FOUND comment(s) found)"

- name: Cleanup test comments
if: always() && steps.fork-check.outputs.is_fork != 'true' && steps.mention-handler.outputs.should-reply == 'true'
if: always() && steps.fork-check.outputs.is_fork != 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ env.GITHUB_APP_TOKEN || github.token }}
ANCHOR_ID: ${{ steps.create-anchor.outputs.test_comment_id }}
run: |
# Delete any test reply comments posted in the last 5 minutes
gh api repos/docker/cagent-action/issues/$TEST_PR_NUMBER/comments \
Expand All @@ -324,17 +346,22 @@ jobs:
gh api "repos/docker/cagent-action/issues/comments/$comment_id" -X DELETE || true
echo "Deleted comment $comment_id"
done
# Delete the anchor comment itself
if [ -n "$ANCHOR_ID" ]; then
gh api "repos/docker/cagent-action/issues/comments/$ANCHOR_ID" -X DELETE || true
echo "Deleted anchor comment $ANCHOR_ID"
fi

test-mention-reply-inline:
name: Mention Reply (Inline) E2E Test
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
if: github.event_name == 'workflow_dispatch'
permissions:
contents: read
id-token: write
pull-requests: write
env:
TEST_PR_NUMBER: ${{ github.event.pull_request.number }}
TEST_PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
steps:
- name: Check if fork PR
id: fork-check
Expand Down Expand Up @@ -427,13 +454,13 @@ jobs:
- name: Run mention-reply handler
if: steps.fork-check.outputs.is_fork != 'true'
id: mention-handler
uses: ./.github/actions/mention-reply
env:
GITHUB_EVENT_PATH: /tmp/test-event-inline.json
GITHUB_EVENT_NAME: pull_request_review_comment
with:
github-token: ${{ env.GITHUB_APP_TOKEN || github.token }}
org-membership-token: ${{ env.ORG_MEMBERSHIP_TOKEN || github.token }}
INPUT_GITHUB-TOKEN: ${{ env.GITHUB_APP_TOKEN || github.token }}
INPUT_ORG-MEMBERSHIP-TOKEN: ${{ env.ORG_MEMBERSHIP_TOKEN || github.token }}
run: |
export GITHUB_EVENT_PATH=/tmp/test-event-inline.json
export GITHUB_EVENT_NAME=pull_request_review_comment
node "$GITHUB_WORKSPACE/dist/mention-reply.js"

- name: Assert should-reply output
if: steps.fork-check.outputs.is_fork != 'true'
Expand Down
21 changes: 21 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# pnpm safe defaults — registry & TLS settings
#
# In pnpm v10+, most settings live in pnpm-workspace.yaml.
# Only auth, registry, SSL, and proxy settings belong in .npmrc.
#
# Copy this file alongside pnpm-workspace.yaml to your project root.
# https://github.com/docker-security/safe-defaults

# --- TLS & Registry Security ---

# Require valid TLS certificates when connecting to registries.
# Never disable this. If you need a custom CA, use `cafile=` instead.
strict-ssl=true

# Make the default registry explicit and auditable.
registry=https://registry.npmjs.org/

# --- Optional: scoped / private registries ---

# @myorg:registry=https://npm.pkg.github.com
# //npm.pkg.github.com/:_authToken=${NPM_TOKEN}
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ The action runs untrusted input (PR titles, bodies, comments, diffs) through an

```bash
# Install (uses pnpm via Corepack, see packageManager in package.json)
pnpm install
pnpm install --frozen-lockfile

# Build TypeScript bundles → dist/
pnpm build
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"typescript": "5.9.3",
"vitest": "4.0.18"
},
"packageManager": "pnpm@10.19.0",
"packageManager": "pnpm@10.26.0",
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
Expand Down
82 changes: 82 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Not using workspaces — this file exists solely for pnpm-specific settings.
# minimumReleaseAge and minimumReleaseAgeExclude are pnpm-workspace.yaml settings
# in pnpm 10.x, not .npmrc settings.
packages: []

# pnpm safe defaults for developer laptops and projects
#
# Requires pnpm >= 10.26.0 (for blockExoticSubdeps, trustPolicy)
#
# https://github.com/docker-security/safe-defaults

# --- Supply Chain Protection ---

# Refuse to install packages published less than 3 days ago.
# Most supply chain attacks are discovered within 24-72 hours.
# Value is in minutes: 10080 = 7 days.
minimumReleaseAge: 10080

# Packages exempt from the age gate (e.g. urgent security patches).
# Supports exact names, scopes (@myorg/*), and pinned versions (pkg@1.2.3).
# minimumReleaseAgeExclude:
# - "@myorg/*"

# Block transitive dependencies from using exotic sources (git repos,
# tarball URLs). Only direct dependencies may use these.
# Default since v10.26.0.
blockExoticSubdeps: true

# Fail if a package's trust level has decreased compared to previous
# releases (e.g. lost provenance or trusted-publisher status).
trustPolicy: no-downgrade

# Packages exempt from trust-policy checks.
# trustPolicyExclude:
# - "some-package@1.2.3"

# --- Build Script Control ---

# Fail if any dependency has unreviewed build scripts.
# Default since v10.3.0. Forces explicit decisions about which
# packages may run preinstall/install/postinstall scripts.
strictDepBuilds: true

# Allowlist of packages permitted to run build scripts.
# Add packages here as needed (e.g. native addons).
allowBuilds: {}
# esbuild: true
# @swc/core: true
# sharp: true

# --- Dependency Resolution ---

# Resolve subdependencies from versions published close to the direct
# dependency's publish date. Reduces subdependency hijacking risk.
resolutionMode: time-based

# --- Integrity & Reproducibility ---

# Save exact versions (1.2.3) instead of semver ranges (^1.2.3).
# Prevents silent minor/patch drift between installs.
# Pair with Dependabot or Renovate for controlled updates.
savePrefix: ""

# Verify store integrity on every install.
# Default true — made explicit.
verifyStoreIntegrity: true

# --- Peer Dependencies ---

# Treat unmet peer dependencies as errors instead of warnings.
strictPeerDependencies: true

# Exempt first-party Docker packages from the age gate so internal releases
# aren't blocked.
minimumReleaseAgeExclude:
- '@docker/*'

# --- Optional: stricter settings (uncomment to enable) ---

# Treat engine version mismatches as errors.
# Useful when your pnpm-workspace.yaml or package.json declares engines.
# engineStrict: true
35 changes: 24 additions & 11 deletions review-pr/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ inputs:
description: 'Newline-separated list of path prefixes to strip from the diff before chunking. Files matching these prefixes are excluded from review entirely.'
required: false
default: ''
max-diff-lines:
description: "Auto-filter cap: if the diff exceeds this many lines after removing score-0 files, lowest-risk files are progressively excluded until it fits. Set to 0 to disable. Default: 3000."
required: false
default: "3000"

outputs:
exit-code:
Expand Down Expand Up @@ -264,6 +268,26 @@ runs:
run: |
node "$ACTION_PATH/../dist/filter-diff.js" pr.diff "$EXCLUDE_PATHS"

- name: Score file risk
if: hashFiles('pr.diff') != ''
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
EXCLUDE_PATHS: ${{ inputs.exclude-paths }}
run: |
set -euo pipefail
node "$ACTION_PATH/../dist/score-risk.js" pr.diff "$EXCLUDE_PATHS"
echo "✅ File risk scores: $(jq -c . /tmp/file_risk_scores.json)"

- name: Auto-filter low-risk files
if: hashFiles('pr.diff') != '' && steps.lock-check.outputs.skip != 'true'
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
MAX_DIFF_LINES: ${{ inputs.max-diff-lines }}
run: |
node "$ACTION_PATH/../dist/auto-filter-diff.js" pr.diff "$MAX_DIFF_LINES"

- name: Split diff into chunks
if: hashFiles('pr.diff') != ''
id: chunk-diff
Expand Down Expand Up @@ -335,17 +359,6 @@ runs:
done
fi

- name: Score file risk
if: hashFiles('pr.diff') != ''
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
EXCLUDE_PATHS: ${{ inputs.exclude-paths }}
run: |
set -euo pipefail
node "$ACTION_PATH/../dist/score-risk.js" pr.diff "$EXCLUDE_PATHS"
echo "✅ File risk scores: $(jq -c . /tmp/file_risk_scores.json)"

- name: Generate file history
if: hashFiles('changed_files.txt') != ''
shell: bash
Expand Down
Loading
Loading