fix: bundle deployed bun scripts whose only pin is on a dynamic import #11691
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Pi Auto Review | |
| on: | |
| pull_request: | |
| types: [ready_for_review, opened, synchronize] | |
| workflow_call: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to review' | |
| required: true | |
| type: number | |
| extra_prompt: | |
| description: 'Additional reviewer instructions appended to the standard review prompt' | |
| required: false | |
| type: string | |
| default: '' | |
| triggered_by: | |
| description: 'GitHub username that triggered this review (for audit only)' | |
| required: false | |
| type: string | |
| default: '' | |
| secrets: | |
| DEEPSEEK_API_KEY: | |
| required: false | |
| WINDMILL_EE_PRIVATE_ACCESS: | |
| required: false | |
| concurrency: | |
| group: pi-review-${{ inputs.pr_number || github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| pi-review: | |
| runs-on: ubicloud-standard-2 | |
| timeout-minutes: 30 | |
| # A non-fork PR (head.repo.fork == false) can only be opened by someone with push | |
| # access to this repo, so fork==false already enforces write access. Do NOT re-add | |
| # an author_association gate: the pull_request webhook payload reports private org | |
| # members as CONTRIBUTOR/NONE (only public members show as MEMBER), which silently | |
| # skips auto-review for every private member. | |
| if: | | |
| github.event_name == 'workflow_call' || | |
| ( | |
| github.event.pull_request.draft == false && | |
| github.event.pull_request.head.repo.fork == false | |
| ) | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Check Pi configuration | |
| id: pi_config | |
| env: | |
| DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} | |
| run: | | |
| if [ -n "$DEEPSEEK_API_KEY" ]; then | |
| echo "enabled=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "DEEPSEEK_API_KEY is not configured; skipping Pi review." | |
| fi | |
| - name: Resolve PR metadata | |
| if: steps.pi_config.outputs.enabled == 'true' | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| INPUT_PR_NUMBER: ${{ inputs.pr_number }} | |
| EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} | |
| EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| EVENT_TITLE: ${{ github.event.pull_request.title }} | |
| EVENT_BODY: ${{ github.event.pull_request.body }} | |
| EVENT_FORK: ${{ github.event.pull_request.head.repo.fork }} | |
| EVENT_AUTHOR: ${{ github.event.pull_request.user.login }} | |
| EVENT_ACTION: ${{ github.event.action }} | |
| run: | | |
| if [ -n "$INPUT_PR_NUMBER" ]; then | |
| PR_JSON=$(gh pr view "$INPUT_PR_NUMBER" --repo "${{ github.repository }}" \ | |
| --json number,baseRefName,baseRefOid,headRefOid,title,body,isCrossRepository,author) | |
| PR_NUMBER=$(echo "$PR_JSON" | jq -r '.number') | |
| BASE_REF=$(echo "$PR_JSON" | jq -r '.baseRefName') | |
| BASE_SHA=$(echo "$PR_JSON" | jq -r '.baseRefOid') | |
| HEAD_SHA=$(echo "$PR_JSON" | jq -r '.headRefOid') | |
| PR_TITLE=$(echo "$PR_JSON" | jq -r '.title') | |
| PR_BODY=$(echo "$PR_JSON" | jq -r '.body // ""') | |
| IS_FORK=$(echo "$PR_JSON" | jq -r '.isCrossRepository') | |
| PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.author.login // ""') | |
| else | |
| PR_NUMBER="$EVENT_PR_NUMBER" | |
| BASE_REF="$EVENT_BASE_REF" | |
| BASE_SHA="$EVENT_BASE_SHA" | |
| HEAD_SHA="$EVENT_HEAD_SHA" | |
| PR_TITLE="$EVENT_TITLE" | |
| PR_BODY="$EVENT_BODY" | |
| IS_FORK="$EVENT_FORK" | |
| PR_AUTHOR="$EVENT_AUTHOR" | |
| fi | |
| # Fork PRs run untrusted code with secrets present, so the automatic | |
| # pull_request trigger never reviews them. A non-empty INPUT_PR_NUMBER | |
| # means we arrived via workflow_call (a maintainer /pi comment gated by | |
| # check-write-access), so allow forks only on that path. | |
| if [ "$IS_FORK" = "true" ] && [ -z "$INPUT_PR_NUMBER" ]; then | |
| echo "Skipping Pi review for fork PR (automatic trigger)." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # An agent-driven PR flips to ready only after a clean /review round on | |
| # a draft, marked by an author comment naming the head SHA (pr skill, | |
| # "Review rounds"). Re-reviewing that same head on ready_for_review is | |
| # redundant. The marker alone is author attestation, so also require | |
| # reviewer evidence: a Codex review (posted by github-actions[bot], not | |
| # forgeable by the author) that predates the marker and carries a | |
| # non-blocking verdict. Comment-triggered and synchronize runs never | |
| # skip. Keep the three copies of this check in sync (pr-ready-review / | |
| # codex-pr-review / pi-pr-review); a shared local action would need the | |
| # repo checked out before the check, which the fork paths here | |
| # deliberately avoid. | |
| if [ "$EVENT_ACTION" = "ready_for_review" ] && [ -z "$INPUT_PR_NUMBER" ]; then | |
| # Fetch failures fail open (no skip): an API hiccup must run the | |
| # review, never skip it or fail the job. | |
| COMMENTS=$(gh api "repos/${{ github.repository }}/issues/$PR_NUMBER/comments?per_page=100" --paginate | jq -s '[.[][]]') || COMMENTS='[]' | |
| MARKER_TIME=$(jq -r --arg author "$PR_AUTHOR" --arg marker "✅ Review round clean @ $HEAD_SHA" \ | |
| '[.[] | select(.user.login == $author) | select(.body | contains($marker)) | .created_at] | min // empty' <<<"$COMMENTS") | |
| CODEX_VERDICT='' | |
| if [ -n "$MARKER_TIME" ]; then | |
| # Only Codex evidence that predates the marker counts: the ready- | |
| # triggered Codex run itself posts after the flip and must not | |
| # vouch for a sibling reviewer's skip. | |
| CODEX_VERDICT=$(jq -r --arg mt "$MARKER_TIME" \ | |
| '[.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("## Codex Review")) | select(.created_at < $mt)] | last | .body // ""' <<<"$COMMENTS" \ | |
| | grep -m1 -oE '(Good to merge|Mergeable, but should ideally address nits|Should address issues before merging)' || true) | |
| fi | |
| if [ -n "$MARKER_TIME" ] && [ -n "$CODEX_VERDICT" ] && [ "$CODEX_VERDICT" != "Should address issues before merging" ]; then | |
| echo "Clean review round marker found for $HEAD_SHA with pre-marker non-blocking Codex verdict; skipping redundant review." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| fi | |
| # PR title/body are attacker-controlled free text. Use an unguessable | |
| # per-run delimiter so a fork can't embed a fixed heredoc terminator to | |
| # inject extra outputs — e.g. is_fork=false (last-write-wins), which | |
| # would re-enable the EE checkout and trusted-path settings for forks. | |
| RAND=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n') | |
| TITLE_EOF="TITLE_EOF_${RAND}" | |
| BODY_EOF="BODY_EOF_${RAND}" | |
| { | |
| echo "skip=false" | |
| echo "is_fork=$IS_FORK" | |
| echo "pr_number=$PR_NUMBER" | |
| echo "base_ref=$BASE_REF" | |
| echo "base_sha=$BASE_SHA" | |
| echo "head_sha=$HEAD_SHA" | |
| echo "pr_author=$PR_AUTHOR" | |
| echo "title<<$TITLE_EOF" | |
| printf '%s\n' "$PR_TITLE" | |
| echo "$TITLE_EOF" | |
| echo "body<<$BODY_EOF" | |
| printf '%s\n' "$PR_BODY" | |
| echo "$BODY_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Checkout repository | |
| if: steps.pi_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true' | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: refs/pull/${{ steps.pr.outputs.pr_number }}/merge | |
| fetch-depth: 1 | |
| # Don't persist github.token in .git/config: the review agent can read | |
| # the checkout, and on the fork path that token (issue/PR write) would | |
| # otherwise be exfiltratable. All later git ops target the public origin | |
| # and need no auth; EE checkout and gh use their own explicit tokens. | |
| persist-credentials: false | |
| # Never expose the EE private-repo token to untrusted fork code. Skipping | |
| # this step leaves steps.ee.outputs.available empty, so the EE checkout and | |
| # substitution steps below are skipped too. | |
| - name: Check EE access | |
| if: steps.pi_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true' && steps.pr.outputs.is_fork != 'true' | |
| id: ee | |
| env: | |
| EE_TOKEN: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }} | |
| run: | | |
| if [ -n "$EE_TOKEN" ]; then | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Checkout EE repository | |
| if: steps.ee.outputs.available == 'true' | |
| uses: actions/checkout@v5 | |
| with: | |
| repository: windmill-labs/windmill-ee-private | |
| path: ./windmill-ee-private | |
| ref: ${{ steps.ee.outputs.ee_repo_ref }} | |
| token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }} | |
| fetch-depth: 1 | |
| - name: Substitute EE code | |
| if: steps.ee.outputs.available == 'true' | |
| run: ./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private | |
| - name: Set up Node.js | |
| if: steps.pi_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Install Pi CLI | |
| if: steps.pi_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true' | |
| # Pinned: this job holds DEEPSEEK_API_KEY and PR write access, and an | |
| # unpinned reviewer also makes verdicts non-reproducible across runs. | |
| run: npm install --global @earendil-works/pi-coding-agent@0.84.1 | |
| - name: Pre-fetch base and head refs for the PR | |
| if: steps.pi_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true' | |
| env: | |
| PR_BASE_REF: ${{ steps.pr.outputs.base_ref }} | |
| PR_NUMBER: ${{ steps.pr.outputs.pr_number }} | |
| run: | | |
| git fetch --no-tags origin \ | |
| "$PR_BASE_REF" \ | |
| "+refs/pull/$PR_NUMBER/head" | |
| - name: Fetch prior PR discussion | |
| if: steps.pi_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| PR_NUMBER: ${{ steps.pr.outputs.pr_number }} | |
| run: | | |
| # Write outside the checkout: on the fork path the merge tree is | |
| # attacker-controlled, and a committed symlink at this path would | |
| # redirect the write. | |
| gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \ | |
| --jq '[.[] | {user: .user.login, created_at: .created_at, body: (.body | .[:4000])}] | sort_by(.created_at) | .[-20:]' \ | |
| > "$RUNNER_TEMP/prior-comments.json" || echo "[]" > "$RUNNER_TEMP/prior-comments.json" | |
| - name: Write Pi review context | |
| if: steps.pi_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true' | |
| env: | |
| PR_REPOSITORY: ${{ github.repository }} | |
| PR_NUMBER: ${{ steps.pr.outputs.pr_number }} | |
| PR_BASE_SHA: ${{ steps.pr.outputs.base_sha }} | |
| PR_HEAD_SHA: ${{ steps.pr.outputs.head_sha }} | |
| PR_TITLE: ${{ steps.pr.outputs.title }} | |
| PR_BODY: ${{ steps.pr.outputs.body }} | |
| PR_AUTHOR: ${{ steps.pr.outputs.pr_author }} | |
| EXTRA_PROMPT: ${{ inputs.extra_prompt }} | |
| run: | | |
| node <<'NODE' | |
| const fs = require('fs'); | |
| const tmp = process.env.RUNNER_TEMP; | |
| const lines = [ | |
| `Repository: ${process.env.PR_REPOSITORY}`, | |
| `PR number: ${process.env.PR_NUMBER}`, | |
| ]; | |
| if (process.env.PR_AUTHOR) { | |
| lines.push(`PR AUTHOR: ${process.env.PR_AUTHOR}`); | |
| } | |
| lines.push( | |
| `Base SHA: ${process.env.PR_BASE_SHA}`, | |
| `Head SHA: ${process.env.PR_HEAD_SHA}`, | |
| '', | |
| 'PR title:', | |
| process.env.PR_TITLE || '(empty)', | |
| '', | |
| 'PR body:', | |
| process.env.PR_BODY || '(empty)', | |
| '', | |
| 'Changed commits command:', | |
| `git log --oneline ${process.env.PR_BASE_SHA}...${process.env.PR_HEAD_SHA}`, | |
| '', | |
| 'Changed files command:', | |
| `git diff --stat ${process.env.PR_BASE_SHA}...${process.env.PR_HEAD_SHA}`, | |
| '', | |
| 'Full review diff command:', | |
| `git diff --unified=0 ${process.env.PR_BASE_SHA}...${process.env.PR_HEAD_SHA}` | |
| ); | |
| if (process.env.EXTRA_PROMPT && process.env.EXTRA_PROMPT.trim()) { | |
| lines.push('', 'Additional reviewer instructions:', process.env.EXTRA_PROMPT.trim()); | |
| } | |
| if (fs.existsSync(`${tmp}/prior-comments.json`)) { | |
| try { | |
| const comments = JSON.parse(fs.readFileSync(`${tmp}/prior-comments.json`, 'utf8')); | |
| if (Array.isArray(comments) && comments.length > 0) { | |
| lines.push( | |
| '', | |
| 'Prior PR discussion (most recent up to 20 comments):', | |
| '', | |
| 'If you have already reviewed this PR (look for your own earlier "## Pi Review (DeepSeek V4)" comment), focus on what changed since then per the diff and respect any decisions the human made in replies. Do not re-flag findings the human already pushed back on.', | |
| '' | |
| ); | |
| for (const c of comments) { | |
| lines.push(`### @${c.user} (${c.created_at})`, '', c.body, '', '---', ''); | |
| } | |
| } | |
| } catch (_) {} | |
| } | |
| fs.writeFileSync(`${tmp}/pr-review-context.md`, `${lines.join('\n')}\n`); | |
| NODE | |
| - name: Run Pi review | |
| if: steps.pi_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true' | |
| env: | |
| DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} | |
| PI_SKIP_VERSION_CHECK: '1' | |
| PR_IS_FORK: ${{ steps.pr.outputs.is_fork }} | |
| PR_BASE_REF: ${{ steps.pr.outputs.base_ref }} | |
| PR_BASE_SHA: ${{ steps.pr.outputs.base_sha }} | |
| PR_HEAD_SHA: ${{ steps.pr.outputs.head_sha }} | |
| run: | | |
| set -o pipefail | |
| PI_HARDEN_FLAGS=() | |
| # Keep generated files (final message, events, context) outside the | |
| # checkout: on the fork path a committed symlink at any of these paths | |
| # would redirect our write and could overwrite an action's code that | |
| # then runs with our credentials. RUNNER_TEMP is outside the checkout. | |
| OUT_DIR="$RUNNER_TEMP" | |
| CTX="$RUNNER_TEMP/pr-review-context.md" | |
| if [ "$PR_IS_FORK" = "true" ]; then | |
| # Fork code is untrusted. Read the review policy/prompt from the base | |
| # ref (git show) rather than the attacker-controlled merge checkout, | |
| # so a malicious fork can't rewrite the reviewer's own instructions, | |
| # and drop the bash tool so the agent has no shell to exfiltrate with. | |
| git show "origin/$PR_BASE_REF:REVIEW.md" > /tmp/pi-prompt.md | |
| git show "origin/$PR_BASE_REF:.github/pi/pr-review.prompt.md" >> /tmp/pi-prompt.md | |
| PI_TOOLS=read,grep,find,ls | |
| # The agent has no shell, so pre-compute the diff (base...head SHAs are | |
| # trusted) into the context file it reads. It may still read fork files | |
| # by absolute path for extra context — reads are safe. | |
| { | |
| echo "" | |
| echo "## Pre-computed review diff (base...head)" | |
| echo "You have no shell. The full diff is below. The repository checkout" | |
| echo "is at $GITHUB_WORKSPACE — you may read files there by absolute path." | |
| echo '```diff' | |
| git -C "$GITHUB_WORKSPACE" diff --unified=0 "$PR_BASE_SHA...$PR_HEAD_SHA" | |
| echo '```' | |
| } >> "$CTX" | |
| # Pi resolves ALL project config from <cwd>/.pi (settings/packages, | |
| # extensions, skills, themes, prompts, SYSTEM.md); inside the fork | |
| # checkout a fork could inject any to run code or rewrite our system | |
| # prompt. Discovery is cwd-based, so run from a fresh empty dir. | |
| PI_WORKDIR=$(mktemp -d) | |
| cd "$PI_WORKDIR" | |
| # Belt-and-suspenders on top of the isolated cwd: refuse discovery of | |
| # extensions/skills/templates/themes/context-files, and PI_OFFLINE=1 to | |
| # block any startup network op or package install. PI_OFFLINE gates only | |
| # startup network ops, not the provider inference call. | |
| PI_HARDEN_FLAGS=(--no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files) | |
| export PI_OFFLINE=1 | |
| else | |
| cat REVIEW.md .github/pi/pr-review.prompt.md > /tmp/pi-prompt.md | |
| PI_TOOLS=read,grep,find,ls,bash | |
| fi | |
| # The context file lives in RUNNER_TEMP (outside the checkout); tell the | |
| # agent its absolute path. | |
| printf '\nReview context file (absolute path): %s\n' "$CTX" >> /tmp/pi-prompt.md | |
| # DeepSeek's reasoning_effort accepts low/high/max and silently maps both | |
| # medium and xhigh onto high. Set the level explicitly rather than letting | |
| # pi's default clamp onto it, so a change to either the default or the | |
| # clamping is a visible diff here instead of a silent shift in review depth. | |
| pi -p \ | |
| --provider deepseek \ | |
| --model deepseek-v4-pro \ | |
| --thinking high \ | |
| --tools "$PI_TOOLS" \ | |
| "${PI_HARDEN_FLAGS[@]}" \ | |
| --mode json \ | |
| < /tmp/pi-prompt.md \ | |
| | tee "$OUT_DIR/pi-events.jsonl" \ | |
| | jq -rc --unbuffered ' | |
| if .type == "agent_start" then "🤖 pi agent started" | |
| elif .type == "turn_start" then "── turn ──" | |
| elif .type == "message_end" then | |
| "[\(.message.role)] " + ( | |
| (.message.content // []) | |
| | map( | |
| if .type == "text" then "text(\(.text | length)c)" | |
| elif .type == "tool_use" then "🔧 \(.name) \(.input | @json | .[:160])" | |
| elif .type == "tool_result" then "✅ result" | |
| else .type | |
| end | |
| ) | |
| | join(" | ") | |
| ) | |
| elif .type == "turn_end" then "── turn done (\((.toolResults // []) | length) tool result(s)) ──" | |
| elif .type == "agent_end" then "🏁 pi agent done" | |
| else empty | |
| end | |
| ' | |
| jq -r ' | |
| select(.type == "agent_end") | |
| | .messages | |
| | map(select(.role == "assistant")) | |
| | last | |
| | (.content[]? | select(.type == "text") | .text) | |
| ' "$OUT_DIR/pi-events.jsonl" > "$OUT_DIR/pi-final-message.md" | |
| # The final message often opens with chatter ("Now I have all the context | |
| # I need..."), which would land above the verdict in the posted comment. | |
| # Keep the trim conditional: without the heading there is nothing to cut | |
| # and the range expression would empty the file. | |
| if grep -q '^## Pi Review' "$OUT_DIR/pi-final-message.md"; then | |
| sed -i -n '/^## Pi Review/,$p' "$OUT_DIR/pi-final-message.md" | |
| fi | |
| - name: Post Pi review comment | |
| if: steps.pi_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true' | |
| uses: actions/github-script@v7 | |
| env: | |
| PR_NUMBER: ${{ steps.pr.outputs.pr_number }} | |
| DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} | |
| GH_JOB_TOKEN: ${{ github.token }} | |
| with: | |
| github-token: ${{ github.token }} | |
| script: | | |
| const fs = require('fs'); | |
| const path = `${process.env.RUNNER_TEMP}/pi-final-message.md`; | |
| if (!fs.existsSync(path)) { | |
| core.info('Pi did not produce a final message; skipping PR comment.'); | |
| return; | |
| } | |
| let body = fs.readFileSync(path, 'utf8').trim(); | |
| if (!body) { | |
| core.info('Pi final message was empty; skipping PR comment.'); | |
| return; | |
| } | |
| // Defense-in-depth for fork reviews: the model call needs the provider | |
| // credential in the environment (readable via /proc/self/environ), and | |
| // the posted comment is an exfiltration channel that bypasses GitHub | |
| // Actions log masking. Strip the credential if it leaked into the text. | |
| for (const s of [process.env.DEEPSEEK_API_KEY, process.env.GH_JOB_TOKEN]) { | |
| if (typeof s === 'string' && s.length >= 8) { | |
| body = body.split(s).join('[REDACTED]'); | |
| } | |
| } | |
| body = body.trim(); | |
| if (!body) { | |
| core.info('Pi final message was empty after redaction; skipping PR comment.'); | |
| return; | |
| } | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: Number(process.env.PR_NUMBER), | |
| body, | |
| }); |