mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 08:01:25 +00:00
322 lines
12 KiB
YAML
322 lines
12 KiB
YAML
name: Codex 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:
|
|
OPENAI_API_KEY:
|
|
required: false
|
|
CODEX_AUTH_JSON:
|
|
required: false
|
|
WINDMILL_EE_PRIVATE_ACCESS:
|
|
required: false
|
|
|
|
concurrency:
|
|
group: codex-review-${{ inputs.pr_number || github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
check-membership:
|
|
if: github.event_name == 'pull_request'
|
|
uses: ./.github/workflows/check-org-membership.yml
|
|
with:
|
|
commenter: ${{ github.event.pull_request.user.login }}
|
|
secrets:
|
|
access_token: ${{ secrets.ORG_ACCESS_TOKEN }}
|
|
|
|
codex-review:
|
|
needs: check-membership
|
|
runs-on: ubicloud-standard-2
|
|
timeout-minutes: 30
|
|
if: |
|
|
always() &&
|
|
(
|
|
needs.check-membership.result == 'skipped' ||
|
|
(needs.check-membership.result == 'success' && needs.check-membership.outputs.is_member == 'true')
|
|
) &&
|
|
(
|
|
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 Codex configuration
|
|
id: codex_config
|
|
env:
|
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
|
|
run: |
|
|
if [ -n "$OPENAI_API_KEY" ]; then
|
|
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
|
echo "auth_mode=api_key" >> "$GITHUB_OUTPUT"
|
|
elif [ -n "$CODEX_AUTH_JSON" ]; then
|
|
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
|
echo "auth_mode=oauth_json" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
|
echo "Codex auth is not configured; set OPENAI_API_KEY or CODEX_AUTH_JSON to enable Codex review."
|
|
fi
|
|
|
|
- name: Resolve PR metadata
|
|
if: steps.codex_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 }}
|
|
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
|
|
if [ "$IS_FORK" = "true" ]; then
|
|
echo "Skipping Codex review for fork PR."
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
{
|
|
echo "skip=false"
|
|
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<<PR_TITLE_EOF'
|
|
printf '%s\n' "$PR_TITLE"
|
|
echo 'PR_TITLE_EOF'
|
|
echo 'body<<PR_BODY_EOF'
|
|
printf '%s\n' "$PR_BODY"
|
|
echo 'PR_BODY_EOF'
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Checkout repository
|
|
if: steps.codex_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
|
|
|
|
- name: Check EE access
|
|
if: steps.codex_config.outputs.enabled == 'true' && steps.pr.outputs.skip != '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.codex_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true'
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
|
|
- name: Install Codex CLI
|
|
if: steps.codex_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true'
|
|
run: npm install --global @openai/codex@0.128.0
|
|
|
|
- name: Configure Codex auth
|
|
if: steps.codex_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true'
|
|
env:
|
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
|
|
run: |
|
|
CODEX_HOME="$HOME/.codex"
|
|
echo "CODEX_HOME=$CODEX_HOME" >> "$GITHUB_ENV"
|
|
mkdir -p "$CODEX_HOME"
|
|
chmod 700 "$CODEX_HOME"
|
|
cat > "$CODEX_HOME/config.toml" <<'EOF'
|
|
cli_auth_credentials_store = "file"
|
|
EOF
|
|
if [ -n "$OPENAI_API_KEY" ]; then
|
|
printf '%s' "$OPENAI_API_KEY" | codex login --with-api-key
|
|
else
|
|
printf '%s' "$CODEX_AUTH_JSON" > "$CODEX_HOME/auth.json"
|
|
chmod 600 "$CODEX_HOME/auth.json"
|
|
node -e 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))' "$CODEX_HOME/auth.json"
|
|
fi
|
|
|
|
- name: Pre-fetch base and head refs for the PR
|
|
if: steps.codex_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.codex_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: |
|
|
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:]' \
|
|
> prior-comments.json || echo "[]" > prior-comments.json
|
|
|
|
- name: Write Codex review context
|
|
if: steps.codex_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: |
|
|
mkdir -p .github/codex
|
|
node <<'NODE'
|
|
const fs = require('fs');
|
|
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('prior-comments.json')) {
|
|
try {
|
|
const comments = JSON.parse(fs.readFileSync('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 "## Codex Review" 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('.github/codex/pr-review-context.md', `${lines.join('\n')}\n`);
|
|
NODE
|
|
|
|
- name: Run Codex review
|
|
if: steps.codex_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true'
|
|
run: |
|
|
cat REVIEW.md .github/codex/pr-review.prompt.md > /tmp/codex-prompt.md
|
|
codex exec \
|
|
-C "$GITHUB_WORKSPACE" \
|
|
-m gpt-5.5 \
|
|
-c 'model_reasoning_effort="xhigh"' \
|
|
-s danger-full-access \
|
|
-o codex-final-message.md \
|
|
- < /tmp/codex-prompt.md
|
|
|
|
- name: Post Codex review comment
|
|
if: steps.codex_config.outputs.enabled == 'true' && steps.pr.outputs.skip != 'true'
|
|
uses: actions/github-script@v7
|
|
env:
|
|
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
script: |
|
|
const fs = require('fs');
|
|
const path = `${process.env.GITHUB_WORKSPACE}/codex-final-message.md`;
|
|
if (!fs.existsSync(path)) {
|
|
core.info('Codex did not produce a final message; skipping PR comment.');
|
|
return;
|
|
}
|
|
const body = fs.readFileSync(path, 'utf8').trim();
|
|
if (!body) {
|
|
core.info('Codex final message was empty; 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,
|
|
});
|