Skip to content

Commit 50f51f3

Browse files
committed
fix: harden SDK review workflow security and robustness
- Fix script injection via review body heredoc (use env var + printenv) - Fix commit message and PR comment indentation from heredoc misuse - Add reviewer org membership check for automatic triggers - Distinguish Claude failure from no-changes with separate PR comment - Stage new files with git add -A (excluding workflow artifacts) - Log committed files for audit trail in artifacts
1 parent f9f68e2 commit 50f51f3

1 file changed

Lines changed: 68 additions & 36 deletions

File tree

.github/workflows/sdk_pr_review.yml

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
# Gates (automatic trigger — all must pass or job never starts):
1616
# 1. Review state == changes_requested, or commented with body
1717
# 2. PR author == yenkins-admin or tychtjan
18-
# 3. Same repo (no forks)
18+
# 3. Reviewer is a gooddata org member (checked at runtime)
19+
# 4. Same repo (no forks)
1920
#
2021
# Additional protections:
2122
# - Claude output is never exposed in PR comments (stays in workflow artifacts)
2223
# - Claude prompt includes strict security guardrails
24+
# - --dangerously-skip-permissions is required for headless CI; real protection
25+
# comes from ephemeral runner + limited PAT scope + prompt guardrails
2326
#
2427
# SECRETS (gooddata-python-sdk repo settings)
2528
# -------------------------------------------
@@ -69,19 +72,24 @@ jobs:
6972
pull-requests: write
7073

7174
steps:
72-
# ── Org membership check (manual trigger only) ────────────
75+
# ── Org membership check ──────────────────────────────────
7376
- name: Verify org membership
74-
if: github.event_name == 'workflow_dispatch'
7577
env:
7678
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
77-
ACTOR: ${{ github.actor }}
7879
run: |
79-
HTTP_STATUS=$(gh api "orgs/gooddata/members/${ACTOR}" --silent -i 2>/dev/null | head -1 | awk '{print $2}')
80+
# For manual trigger, check the actor; for review trigger, check the reviewer
81+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
82+
CHECK_USER="${{ github.actor }}"
83+
else
84+
CHECK_USER="${{ github.event.review.user.login }}"
85+
fi
86+
87+
HTTP_STATUS=$(gh api "orgs/gooddata/members/${CHECK_USER}" --silent -i 2>/dev/null | head -1 | awk '{print $2}')
8088
if [ "$HTTP_STATUS" != "204" ]; then
81-
echo "ERROR: ${ACTOR} is not a member of the gooddata organization"
89+
echo "ERROR: ${CHECK_USER} is not a member of the gooddata organization"
8290
exit 1
8391
fi
84-
echo "${ACTOR} is a gooddata org member — proceeding"
92+
echo "${CHECK_USER} is a gooddata org member — proceeding"
8593
8694
# ── Resolve PR metadata ────────────────────────────────────
8795
- name: Resolve PR details
@@ -131,16 +139,15 @@ jobs:
131139
REPO: ${{ github.repository }}
132140
REVIEW_ID: ${{ github.event.review.id }}
133141
EVENT_NAME: ${{ github.event_name }}
142+
REVIEW_BODY: ${{ github.event.review.body }}
134143
run: |
135144
mkdir -p review-context
136145
137146
# Review body (only available from event trigger)
138147
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
139148
echo "(manual trigger — no review body)" > review-context/review-body.txt
140149
else
141-
cat > review-context/review-body.txt << 'EOF'
142-
${{ github.event.review.body }}
143-
EOF
150+
printenv REVIEW_BODY > review-context/review-body.txt
144151
fi
145152
146153
# Inline comments for specific review (only for event trigger)
@@ -338,41 +345,56 @@ jobs:
338345
339346
# ── Apply fixes ──────────────────────────────────────────
340347
- name: Apply fixes with Claude
348+
id: claude
341349
if: steps.extract.outputs.has_comments == 'true'
342350
env:
343351
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
344352
run: |
353+
touch claude-output.txt
354+
EXIT_CODE=0
345355
claude --dangerously-skip-permissions \
346356
--model sonnet \
347357
--max-turns 30 \
348358
--output-format text \
349359
-p "$(cat review-context/prompt.md)" \
350-
> claude-output.txt 2>&1 || true
360+
> claude-output.txt 2>&1 || EXIT_CODE=$?
351361
352-
echo "=== Claude finished ==="
362+
echo "=== Claude finished (exit code: ${EXIT_CODE}) ==="
353363
echo "Lines of output: $(wc -l < claude-output.txt)"
354364
365+
if [ "$EXIT_CODE" -ne 0 ]; then
366+
echo "claude_failed=true" >> "$GITHUB_OUTPUT"
367+
echo "::warning::Claude exited with code ${EXIT_CODE}"
368+
else
369+
echo "claude_failed=false" >> "$GITHUB_OUTPUT"
370+
fi
371+
355372
# ── Commit and push ──────────────────────────────────────
356373
- name: Commit and push fixes
357374
id: push
358-
if: steps.extract.outputs.has_comments == 'true'
375+
if: steps.extract.outputs.has_comments == 'true' && steps.claude.outputs.claude_failed != 'true'
359376
env:
360377
PR_BRANCH: ${{ steps.pr.outputs.pr_branch }}
361378
run: |
362-
if git diff --quiet && git diff --cached --quiet; then
379+
if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
363380
echo "No changes made by Claude"
364381
echo "pushed=false" >> "$GITHUB_OUTPUT"
365382
exit 0
366383
fi
367384
368-
git add -u
369-
git commit -m "$(cat <<'EOF'
370-
fix: address review feedback
385+
# Stage all changes but exclude workflow artifacts
386+
git add -A
387+
git reset -- review-context/ claude-output.txt
388+
389+
# Log changed files for audit
390+
echo "=== Files being committed ==="
391+
git diff --cached --name-status
392+
git diff --cached --name-status > review-context/committed-files.txt
371393
372-
Applied fixes based on code review feedback.
373-
Automated by sdk-review-fix workflow.
374-
EOF
375-
)"
394+
git commit -m "fix: address review feedback
395+
396+
Applied fixes based on code review feedback.
397+
Automated by sdk-review-fix workflow."
376398
git push origin "$PR_BRANCH"
377399

378400
echo "pushed=true" >> "$GITHUB_OUTPUT"
@@ -392,34 +414,43 @@ jobs:
392414
RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}"
393415
394416
gh pr comment "$PR" \
395-
--body "$(cat <<EOF
396-
### Review fixes applied
417+
--body "### Review fixes applied
418+
419+
Addressed feedback from @${REVIEWER} in [\`${SHA:0:7}\`](https://github.com/${REPO}/commit/${SHA}).
420+
421+
_[Workflow run](${RUN_URL}) &#x2022; Claude output available in workflow artifacts_"
422+
423+
- name: Post Claude failure notice
424+
if: steps.claude.outputs.claude_failed == 'true'
425+
env:
426+
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
427+
run: |
428+
PR="${{ steps.pr.outputs.pr_number }}"
429+
REVIEWER="${{ steps.pr.outputs.reviewer }}"
430+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
431+
432+
gh pr comment "$PR" \
433+
--body "### Review fix failed
397434
398-
Addressed feedback from @${REVIEWER} in [\`${SHA:0:7}\`](https://github.com/${REPO}/commit/${SHA}).
435+
Claude encountered an error while processing review feedback from @${REVIEWER}. Manual intervention is needed.
399436

400-
_[Workflow run](${RUN_URL}) &#x2022; Claude output available in workflow artifacts_
401-
EOF
402-
)"
437+
_[Workflow run](${RUN_URL}) &#x2022; Check workflow logs for details_"
403438

404439
- name: Post no-changes notice
405-
if: steps.extract.outputs.has_comments == 'true' && steps.push.outputs.pushed == 'false'
440+
if: steps.extract.outputs.has_comments == 'true' && steps.push.outputs.pushed == 'false' && steps.claude.outputs.claude_failed != 'true'
406441
env:
407442
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
408443
run: |
409-
REPO="${{ github.repository }}"
410444
PR="${{ steps.pr.outputs.pr_number }}"
411445
REVIEWER="${{ steps.pr.outputs.reviewer }}"
412-
RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}"
446+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
413447
414448
gh pr comment "$PR" \
415-
--body "$(cat <<EOF
416-
### No changes applied
449+
--body "### No changes applied
417450
418-
Claude analyzed the review feedback from @${REVIEWER} but made no file changes. Manual intervention may be needed.
451+
Claude analyzed the review feedback from @${REVIEWER} but made no file changes. Manual intervention may be needed.
419452

420-
_[Workflow run](${RUN_URL}) &#x2022; Claude output available in workflow artifacts_
421-
EOF
422-
)"
453+
_[Workflow run](${RUN_URL}) &#x2022; Claude output available in workflow artifacts_"
423454

424455
# ── Artifacts (Claude output stays here, not in PR) ──────
425456
- name: Upload artifacts
@@ -430,4 +461,5 @@ jobs:
430461
path: |
431462
review-context/
432463
claude-output.txt
464+
if-no-files-found: warn
433465
retention-days: 14

0 commit comments

Comments
 (0)