open-design/.github/workflows/agent-pr-explore-sandbox.yml
2026-05-26 07:52:42 +00:00

235 lines
9.1 KiB
YAML

name: agent-pr-explore-sandbox
# Trusted-orchestrator workflow for PR exploration. It intentionally uses
# pull_request_target so the workflow file and runner script come from the
# protected base branch. The PR head is never checked out on the host runner;
# the sandbox script fetches and executes it inside Docker.
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "apps/web/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- ".github/workflows/agent-pr-explore-sandbox.yml"
- ".github/scripts/agent-pr-explore-sandbox.sh"
workflow_dispatch:
inputs:
pr_number:
description: Pull request number to explore.
required: true
type: string
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: agent-pr-explore-sandbox-${{ github.event.pull_request.number || inputs.pr_number }}
cancel-in-progress: true
jobs:
sandbox:
name: Sandbox PR runtime
if: ${{ github.event_name == 'workflow_dispatch' || !github.event.pull_request.draft }}
runs-on: [self-hosted, agent-pr-explore]
environment: agent-pr-explore
timeout-minutes: 45
steps:
- name: Checkout trusted base scripts
uses: actions/checkout@v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha || github.sha }}
persist-credentials: false
- name: Resolve PR metadata
id: pr
shell: bash
env:
GH_TOKEN: ${{ github.token }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
run: |
set -euo pipefail
if ! [[ "$EVENT_PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number: $EVENT_PR_NUMBER"
exit 1
fi
state="$(gh pr view "$EVENT_PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state --jq '.state')"
draft="$(gh pr view "$EVENT_PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft')"
author="$(gh pr view "$EVENT_PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json author --jq '.author.login')"
head_sha="$(gh pr view "$EVENT_PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json headRefOid --jq '.headRefOid')"
head_repo="$(gh pr view "$EVENT_PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json headRepositoryOwner,headRepository --jq '.headRepositoryOwner.login + "/" + .headRepository.name')"
base_repo="$GITHUB_REPOSITORY"
base_sha="$(gh pr view "$EVENT_PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json baseRefOid --jq '.baseRefOid')"
if [ "$state" != "OPEN" ]; then
echo "::error::Refusing to explore PR $EVENT_PR_NUMBER because state is $state."
exit 1
fi
if [ "$draft" != "false" ]; then
echo "::error::Refusing to explore draft PR $EVENT_PR_NUMBER."
exit 1
fi
if [ "$base_repo" != "$GITHUB_REPOSITORY" ]; then
echo "::error::Unexpected base repo $base_repo."
exit 1
fi
if ! [[ "$head_sha" =~ ^[0-9a-f]{40}$ && "$base_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::Invalid PR SHA metadata."
exit 1
fi
{
echo "number=$EVENT_PR_NUMBER"
echo "author=$author"
echo "head_sha=$head_sha"
echo "head_repo=$head_repo"
echo "base_sha=$base_sha"
} >> "$GITHUB_OUTPUT"
- name: Run expect against Docker-isolated PR app
shell: bash
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
HEAD_REPO: ${{ steps.pr.outputs.head_repo }}
BASE_REPO: ${{ github.repository }}
BASE_SHA: ${{ steps.pr.outputs.base_sha }}
OD_SANDBOX_CPUS: "4"
OD_SANDBOX_MEMORY: "8g"
OD_SANDBOX_READY_TIMEOUT_SECONDS: "900"
OD_EXPECT_TIMEOUT_SECONDS: "1200"
OD_EXPECT_CONTEXT_MAX_BYTES: "120000"
OD_TRACE_R2_UPLOAD: "1"
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_PUBLIC_ORIGIN: ${{ vars.R2_PUBLIC_ORIGIN }}
run: .github/scripts/agent-pr-explore-sandbox.sh
- name: Upload sandbox artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: agent-pr-explore-sandbox-${{ steps.pr.outputs.number }}-${{ steps.pr.outputs.head_sha }}
path: ${{ runner.temp }}/agent-pr-explore-sandbox/artifacts/
if-no-files-found: warn
retention-days: 7
- name: Comment exploration report
if: always()
shell: bash
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
PR_AUTHOR: ${{ steps.pr.outputs.author }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
run: |
set -euo pipefail
report="$RUNNER_TEMP/agent-pr-explore-sandbox/artifacts/agent-pr-exploration-report.md"
if [ ! -s "$report" ]; then
echo "No agent exploration report was produced; skipping PR comment."
exit 0
fi
marker="<!-- agent-pr-explore-sandbox:${PR_NUMBER} -->"
body_file="$(mktemp)"
{
cat "$report"
echo
echo "$marker"
} > "$body_file"
comment_id="$(
gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --paginate \
--jq ".[] | select(.user.type == \"Bot\" and (.body | contains(\"$marker\"))) | .id" \
| tail -n 1
)"
body="$(cat "$body_file")"
if [ -n "$comment_id" ]; then
gh api "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" -X PATCH -f body="$body" --silent
else
gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" -f body="$body" --silent
fi
case "${PR_AUTHOR,,}" in
nettee|mrcfps|alchemistklk|siri-ray)
;;
*)
echo "PR author $PR_AUTHOR is not configured for inline agent reports; skipping inline review comment."
exit 0
;;
esac
files_json="$(mktemp)"
gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" > "$files_json"
inline_target="$(
FILES_JSON="$files_json" node <<'NODE'
const fs = require("node:fs");
const pages = JSON.parse(fs.readFileSync(process.env.FILES_JSON, "utf8"));
const files = pages.flat();
function firstAddedLine(file) {
if (typeof file.patch !== "string") return null;
let newLine = null;
for (const line of file.patch.split("\n")) {
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
if (hunk) {
newLine = Number(hunk[1]);
continue;
}
if (newLine == null) continue;
if (line.startsWith("+") && !line.startsWith("+++")) return newLine;
if (!line.startsWith("-")) newLine += 1;
}
return null;
}
for (const file of files) {
if (!file || file.status === "removed") continue;
const line = firstAddedLine(file);
if (line != null) {
process.stdout.write(JSON.stringify({ path: file.filename, line }));
process.exit(0);
}
}
NODE
)"
if [ -z "$inline_target" ]; then
echo "No added diff line was available for an inline agent report; skipping inline review comment."
exit 0
fi
inline_path="$(node -e 'const target = JSON.parse(process.argv[1]); process.stdout.write(target.path)' "$inline_target")"
inline_line="$(node -e 'const target = JSON.parse(process.argv[1]); process.stdout.write(String(target.line))' "$inline_target")"
inline_marker="<!-- agent-pr-explore-inline:${PR_NUMBER} -->"
inline_body_file="$(mktemp)"
{
cat "$report"
echo
echo "$inline_marker"
} > "$inline_body_file"
inline_body="$(cat "$inline_body_file")"
inline_comment_id="$(
gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/comments" --paginate \
--jq ".[] | select(.user.type == \"Bot\" and (.body | contains(\"$inline_marker\"))) | .id" \
| tail -n 1
)"
if [ -n "$inline_comment_id" ]; then
gh api "repos/$GITHUB_REPOSITORY/pulls/comments/$inline_comment_id" -X PATCH -f body="$inline_body" --silent
else
gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/comments" \
-f body="$inline_body" \
-f commit_id="$HEAD_SHA" \
-f path="$inline_path" \
-F line="$inline_line" \
-f side=RIGHT \
--silent
fi