mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
235 lines
9.1 KiB
YAML
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
|