Skip to content

fix: reset gateBlockCount when deliverableType changes (DLD-3465) #3

fix: reset gateBlockCount when deliverableType changes (DLD-3465)

fix: reset gateBlockCount when deliverableType changes (DLD-3465) #3

Workflow file for this run

name: AI Review
on:
pull_request:
branches: [master]
types: [opened, synchronize, reopened]
concurrency:
group: ai-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
contents: write
pull-requests: write
statuses: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
- name: Detect whether AI review is required
id: changes
shell: bash
run: |
set -euo pipefail
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}")"
printf '%s\n' "$changed"
if printf '%s\n' "$changed" | grep -Eq '(^|/)(\.github/workflows/|package\.json|pnpm-workspace\.yaml|Dockerfile|scripts/|server/|ui/|cli/|packages/|tests/|tools/|agents/|skills/|deploy/|docker/|.*\.(ts|tsx|js|jsx|mjs|cjs|json|yml|yaml|sh))$'; then
echo "run_ai_review=true" >> "$GITHUB_OUTPUT"
else
echo "run_ai_review=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Node.js
if: steps.changes.outputs.run_ai_review == 'true'
uses: actions/setup-node@v4
with:
node-version: 24
- name: Check for [ai-fix] marker
if: steps.changes.outputs.run_ai_review == 'true'
run: |
HEAD_MSG="$(git log -1 --pretty=%s)"
if echo "$HEAD_MSG" | grep -qF '[ai-fix]'; then
echo "SKIP_REMEDIATION=true" >> "$GITHUB_ENV"
else
echo "SKIP_REMEDIATION=false" >> "$GITHUB_ENV"
fi
- name: Post pending status
if: steps.changes.outputs.run_ai_review == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }} \
-X POST \
-f state=pending \
-f context="ai-review/verdict" \
-f description="AI review in progress..." \
-f target_url="${{ github.event.pull_request.html_url }}"
- name: Fetch PR metadata
if: steps.changes.outputs.run_ai_review == 'true'
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
PR_JSON="$(gh pr view ${{ github.event.pull_request.number }} --json title,body,url,changedFiles,additions,deletions)"
echo "PR_TITLE=$(echo "$PR_JSON" | jq -r '.title')" >> "$GITHUB_ENV"
echo "PR_URL=$(echo "$PR_JSON" | jq -r '.url')" >> "$GITHUB_ENV"
echo "PR_FILES_CHANGED=$(echo "$PR_JSON" | jq -r '.changedFiles')" >> "$GITHUB_ENV"
echo "PR_ADDITIONS=$(echo "$PR_JSON" | jq -r '.additions')" >> "$GITHUB_ENV"
echo "PR_DELETIONS=$(echo "$PR_JSON" | jq -r '.deletions')" >> "$GITHUB_ENV"
echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> "$GITHUB_ENV"
echo "$PR_JSON" | jq -r '.body // ""' > /tmp/pr-body.txt
gh pr diff ${{ github.event.pull_request.number }} > /tmp/pr-diff.txt || true
# Fetch inline review comments and top-level reviews for context
gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments --paginate \
| jq -s --arg head_sha "$HEAD_SHA" 'add | map(select((.commit_id // "") == $head_sha or (.original_commit_id // "") == $head_sha) | {path, line, side, body, author: .user.login})' > /tmp/pr-review-comments.json 2>/dev/null || echo '[]' > /tmp/pr-review-comments.json
gh pr view ${{ github.event.pull_request.number }} --json reviews \
| jq '[.reviews[] | {author: .author.login, state, submittedAt, body, commit: (.commit.oid // "")}]' > /tmp/pr-reviews.json 2>/dev/null || echo '[]' > /tmp/pr-reviews.json
- name: Run AI review
if: steps.changes.outputs.run_ai_review == 'true'
env:
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
PR_BODY_FILE: /tmp/pr-body.txt
PR_DIFF_FILE: /tmp/pr-diff.txt
PR_REVIEW_COMMENTS_FILE: /tmp/pr-review-comments.json
PR_REVIEWS_FILE: /tmp/pr-reviews.json
run: |
if [ -z "${MINIMAX_API_KEY:-}" ]; then
echo "HAS_MINIMAX_KEY=false" >> "$GITHUB_ENV"
else
echo "HAS_MINIMAX_KEY=true" >> "$GITHUB_ENV"
fi
if [ -z "${MINIMAX_API_KEY:-}" ]; then
echo '{"verdict":"HIGH_RISK","summary":"MINIMAX_API_KEY not configured","findings":[{"severity":"note","message":"AI review secret missing; manual review required.","file":"","line":0}]}' > /tmp/review-result.json
else
node scripts/ai-review.mjs > /tmp/review-result.json || true
fi
if [ ! -s /tmp/review-result.json ]; then
echo '{"verdict":"FAIL","summary":"Review script failed","findings":[]}' > /tmp/review-result.json
fi
echo "VERDICT=$(jq -r '.verdict // "FAIL"' /tmp/review-result.json)" >> "$GITHUB_ENV"
echo "SUMMARY=$(jq -r '.summary // "Review failed"' /tmp/review-result.json)" >> "$GITHUB_ENV"
- name: Apply AI-suggested version bump
id: version_bump
if: |
steps.changes.outputs.run_ai_review == 'true' &&
env.SKIP_REMEDIATION == 'false' &&
env.HAS_MINIMAX_KEY == 'true' &&
github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ github.token }}
BOT_PAT: ${{ secrets.BOT_PAT }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
node scripts/apply-version-bump.mjs /tmp/review-result.json > /tmp/version-bump-result.json || true
if [ -s /tmp/version-bump-result.json ]; then
cat /tmp/version-bump-result.json
STATUS="$(jq -r '.status // "error"' /tmp/version-bump-result.json)"
echo "VERSION_BUMP_STATUS=$STATUS" >> "$GITHUB_ENV"
if [ "$STATUS" = "applied" ]; then
echo "applied=true" >> "$GITHUB_OUTPUT"
# GITHUB_TOKEN-triggered events do NOT fire downstream workflows
# (GitHub anti-recursion safeguard). Use BOT_PAT if configured so
# close+reopen triggers a fresh pull_request:reopened event on
# the bumped SHA; otherwise emit a warning for manual handling.
if [ -n "${BOT_PAT:-}" ]; then
GH_TOKEN="$BOT_PAT" gh pr close "$PR_NUMBER" --repo "${{ github.repository }}" || true
GH_TOKEN="$BOT_PAT" gh pr reopen "$PR_NUMBER" --repo "${{ github.repository }}" || true
else
echo "::warning::BOT_PAT secret not configured — bumped commit was pushed but new workflow runs will not fire automatically. Push, comment, or close+reopen the PR manually to trigger checks, or add a BOT_PAT secret to automate this."
fi
else
echo "applied=false" >> "$GITHUB_OUTPUT"
fi
else
echo "VERSION_BUMP_STATUS=error" >> "$GITHUB_ENV"
echo "applied=false" >> "$GITHUB_OUTPUT"
fi
- name: Determine workflow auto-remediation scope
if: steps.changes.outputs.run_ai_review == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --name-only | jq -R -s -c 'split("\n") | map(select(length > 0))')
FINDING_FILES=$(jq -c '[.findings[]? | select(.file and (.file | length > 0)) | .file] | unique' /tmp/review-result.json)
FINDINGS_COUNT=$(jq -r '(.findings // []) | length' /tmp/review-result.json)
HAS_CRITICAL=$(jq -r 'any(.findings[]?; .severity == "critical")' /tmp/review-result.json)
echo "CHANGED_FILES=$CHANGED_FILES" >> "$GITHUB_ENV"
echo "FINDING_FILES=$FINDING_FILES" >> "$GITHUB_ENV"
echo "FINDINGS_COUNT=$FINDINGS_COUNT" >> "$GITHUB_ENV"
echo "HAS_CRITICAL=$HAS_CRITICAL" >> "$GITHUB_ENV"
SAME_REPO=false
if [ "${{ github.event.pull_request.head.repo.full_name }}" = "${{ github.repository }}" ]; then
SAME_REPO=true
fi
AUTO_REMEDIATE_SCOPE=$(
CHANGED_FILES="$CHANGED_FILES" FINDING_FILES="$FINDING_FILES" node -e 'const changed = JSON.parse(process.env.CHANGED_FILES || "[]"); const findingFiles = JSON.parse(process.env.FINDING_FILES || "[]"); const allow = (file) => typeof file === "string" && file.startsWith(".github/"); const allChangedAllowed = changed.length > 0 && changed.every(allow); const allFindingsAllowed = findingFiles.every(allow); process.stdout.write(allChangedAllowed && allFindingsAllowed ? "true" : "false");'
)
echo "SAME_REPO=$SAME_REPO" >> "$GITHUB_ENV"
echo "AUTO_REMEDIATE_SCOPE=$AUTO_REMEDIATE_SCOPE" >> "$GITHUB_ENV"
- name: Auto-remediate workflow/config findings
id: workflow_remediate
if: |
steps.changes.outputs.run_ai_review == 'true' &&
steps.version_bump.outputs.applied != 'true' &&
env.SAME_REPO == 'true' &&
env.AUTO_REMEDIATE_SCOPE == 'true' &&
env.HAS_CRITICAL != 'true' &&
env.FINDINGS_COUNT != '0' &&
env.SKIP_REMEDIATION == 'false' &&
env.HAS_MINIMAX_KEY == 'true'
env:
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
run: |
set -euo pipefail
export AI_REVIEW_FINDINGS="$(jq -c '.findings // []' /tmp/review-result.json)"
export AI_REMEDIATE_PATHS="${CHANGED_FILES}"
export PR_BODY_FILE=/tmp/pr-body.txt
node scripts/ai-review-remediate.mjs > /tmp/workflow-remediation-result.json || true
if [ ! -s /tmp/workflow-remediation-result.json ]; then
echo "REMEDIATION_STATUS=error" >> "$GITHUB_ENV"
echo "applied=false" >> "$GITHUB_OUTPUT"
exit 0
fi
EDIT_COUNT="$(jq -r '(.edits // []) | length' /tmp/workflow-remediation-result.json)"
if [ "$EDIT_COUNT" = "0" ]; then
echo "REMEDIATION_STATUS=no-patches" >> "$GITHUB_ENV"
echo "applied=false" >> "$GITHUB_OUTPUT"
exit 0
fi
for file in $(jq -r '.edits[].file' /tmp/workflow-remediation-result.json); do
case "$file" in
*.yml|*.yaml)
ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "$file"
;;
*.json)
jq empty "$file"
;;
*.sh)
bash -n "$file"
;;
*.js|*.mjs|*.cjs)
node --check "$file"
;;
esac
done
if git diff --quiet; then
echo "REMEDIATION_STATUS=no-changes" >> "$GITHUB_ENV"
echo "applied=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git config user.name "ai-review[bot]"
git config user.email "ai-review[bot]@users.noreply.github.com"
git add .github scripts/ai-review-remediate.mjs
git commit -m "[ai-fix] remediate workflow review findings"
git push origin HEAD
echo "REMEDIATION_STATUS=applied" >> "$GITHUB_ENV"
echo "applied=true" >> "$GITHUB_OUTPUT"
- name: Auto-remediate review findings
if: |
steps.changes.outputs.run_ai_review == 'true' &&
steps.version_bump.outputs.applied != 'true' &&
steps.workflow_remediate.outputs.applied != 'true' &&
env.AUTO_REMEDIATE_SCOPE != 'true' &&
(env.VERDICT == 'PASS_WITH_NOTES' || env.VERDICT == 'FAIL') &&
env.SKIP_REMEDIATION == 'false' &&
env.HAS_MINIMAX_KEY == 'true'
env:
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
AFFECTED_FILES="$(jq -r '[.findings[].file // empty] | unique | .[]' /tmp/review-result.json)"
FILE_COUNT="$(echo "$AFFECTED_FILES" | grep -c . || true)"
if [ "$FILE_COUNT" -eq 0 ] || [ "$FILE_COUNT" -gt 5 ]; then
echo "REMEDIATION_STATUS=skipped-scope" >> "$GITHUB_ENV"
exit 0
fi
FILE_CONTENTS="{}"
for f in $AFFECTED_FILES; do
if [ -f "$f" ]; then
CONTENT="$(head -c 50000 "$f")"
FILE_CONTENTS="$(echo "$FILE_CONTENTS" | jq --arg path "$f" --arg content "$CONTENT" '. + {($path): $content}')"
fi
done
printf '%s' "$FILE_CONTENTS" > /tmp/remediation-file-contents.json
jq -c '.findings' /tmp/review-result.json > /tmp/remediation-findings.json
REMEDIATION_FINDINGS_FILE=/tmp/remediation-findings.json \
REMEDIATION_FILE_CONTENTS_FILE=/tmp/remediation-file-contents.json \
REMEDIATION_DIFF_FILE=/tmp/pr-diff.txt \
node scripts/ai-remediate.mjs > /tmp/remediation-result.json || true
if [ ! -s /tmp/remediation-result.json ]; then
echo "REMEDIATION_STATUS=error" >> "$GITHUB_ENV"
exit 0
fi
PATCH_COUNT="$(jq '.patches | length' /tmp/remediation-result.json)"
if [ "$PATCH_COUNT" -eq 0 ]; then
echo "REMEDIATION_STATUS=no-patches" >> "$GITHUB_ENV"
exit 0
fi
for i in $(seq 0 $((PATCH_COUNT - 1))); do
PATCH_FILE="$(jq -r ".patches[$i].file" /tmp/remediation-result.json)"
PATCH_CONTENT="$(jq -r ".patches[$i].content" /tmp/remediation-result.json)"
if [ -f "$PATCH_FILE" ]; then
printf '%s' "$PATCH_CONTENT" > "$PATCH_FILE"
fi
done
for i in $(seq 0 $((PATCH_COUNT - 1))); do
PATCH_FILE="$(jq -r ".patches[$i].file" /tmp/remediation-result.json)"
case "$PATCH_FILE" in
*.py)
python -m py_compile "$PATCH_FILE"
;;
*.json)
jq empty "$PATCH_FILE"
;;
esac
done
if git diff --quiet; then
echo "REMEDIATION_STATUS=no-changes" >> "$GITHUB_ENV"
exit 0
fi
git config user.name "ai-review[bot]"
git config user.email "ai-review[bot]@users.noreply.github.com"
git add -A
EXPLANATION="$(jq -r '.explanation // "Automated fixes"' /tmp/remediation-result.json | head -c 100)"
git commit -m "[ai-fix] $EXPLANATION"
git push origin HEAD
echo "REMEDIATION_STATUS=applied" >> "$GITHUB_ENV"
- name: Note auto-remediation push
if: steps.workflow_remediate.outputs.applied == 'true'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let summary = 'Applied workflow/config remediation to the PR branch.';
try {
const result = JSON.parse(fs.readFileSync('/tmp/workflow-remediation-result.json', 'utf8'));
if (typeof result.summary === 'string' && result.summary.trim()) {
summary = result.summary.trim();
}
} catch {}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: [
'## AI Review Auto-Remediation',
'',
summary,
'',
'A follow-up workflow run has been triggered on the updated branch.',
].join('\n'),
});
- name: Publish review status
if: always() && steps.changes.outputs.run_ai_review == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
REMEDIATION_STATUS="${REMEDIATION_STATUS:-none}"
VERSION_BUMP_STATUS="${VERSION_BUMP_STATUS:-none}"
VERDICT="${VERDICT:-FAIL}"
SUMMARY="${SUMMARY:-Review failed}"
if [ "$VERSION_BUMP_STATUS" = "applied" ]; then
STATE="success"
SUMMARY="AI version bump applied; rerun queued"
elif [ "$REMEDIATION_STATUS" = "applied" ]; then
STATE="success"
SUMMARY="AI remediation applied; rerun queued"
else
case "$VERDICT" in
PASS|PASS_WITH_NOTES|HIGH_RISK)
STATE="success"
;;
*)
STATE="failure"
;;
esac
fi
gh api repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }} \
-X POST \
-f state="$STATE" \
-f context="ai-review/verdict" \
-f description="$SUMMARY" \
-f target_url="${{ github.event.pull_request.html_url }}"
- name: Comment on PR (if findings)
if: steps.changes.outputs.run_ai_review == 'true' && env.REMEDIATION_STATUS != 'applied' && env.VERSION_BUMP_STATUS != 'applied' && env.VERDICT != 'PASS'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const result = JSON.parse(fs.readFileSync('/tmp/review-result.json', 'utf8'));
const verdict = result.verdict || 'ERROR';
const summary = result.summary || '';
const findings = result.findings || [];
const icon = { PASS: '\u2705', PASS_WITH_NOTES: '\u2705', FAIL: '\u274C', HIGH_RISK: '\u26A0\uFE0F' }[verdict] || '\u2753';
let body = `## ${icon} AI Review: ${verdict}\n\n${summary}\n\n`;
if (findings.length > 0) {
const grouped = { critical: [], warning: [], note: [] };
for (const f of findings) {
const sev = grouped[f.severity] ? f.severity : 'note';
grouped[sev].push(f);
}
for (const [severity, items] of Object.entries(grouped)) {
if (items.length === 0) continue;
const label = { critical: 'Critical', warning: 'Warnings', note: 'Notes' }[severity];
body += `### ${label}\n\n`;
for (const f of items) {
const loc = f.file ? (f.line ? `\`${f.file}:${f.line}\`` : `\`${f.file}\``) : '';
body += `- ${loc ? `${loc} - ` : ''}${f.message}\n`;
}
body += '\n';
}
}
body += '---\n*Automated review by ViraCue AI Review*';
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const marker = '*Automated review by ViraCue AI Review*';
for (const comment of comments) {
if (comment.body && comment.body.includes(marker)) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id,
});
}
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
- name: Enable auto-merge
if: |
steps.changes.outputs.run_ai_review == 'true' &&
env.REMEDIATION_STATUS != 'applied' &&
env.VERSION_BUMP_STATUS != 'applied' &&
(
env.VERDICT == 'PASS' ||
env.VERDICT == 'PASS_WITH_NOTES' ||
(env.VERDICT == 'HIGH_RISK' && env.AUTO_REMEDIATE_SCOPE == 'true' && env.FINDINGS_COUNT == '0' && env.HAS_CRITICAL != 'true')
)
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr merge ${{ github.event.pull_request.number }} --squash --auto 2>&1 || echo "Auto-merge enable failed (may already be enabled or branch protection requires review)"
- name: Post human-approval-required (HIGH_RISK)
if: |
steps.changes.outputs.run_ai_review == 'true' &&
env.REMEDIATION_STATUS != 'applied' &&
env.VERDICT == 'HIGH_RISK' &&
env.AUTO_REMEDIATE_SCOPE != 'true'
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: [
'## Human Approval Required',
'',
'The AI Code Reviewer classified this PR as **high risk**.',
'',
`Verdict: \`${process.env.VERDICT} - ${process.env.SUMMARY}\``,
'',
'This PR will not auto-merge. A human must review and approve before merge.',
].join('\n'),
});
- name: Publish skipped status for docs-only changes
if: steps.changes.outputs.run_ai_review != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }} \
-X POST \
-f state=success \
-f context="ai-review/verdict" \
-f description="PASS: AI review skipped for docs-only changes" \
-f target_url="${{ github.event.pull_request.html_url }}"