feat(ci): add production council review workflow chain#313
feat(ci): add production council review workflow chain#313sonupreetam wants to merge 21 commits into
Conversation
| github.event_name == 'workflow_dispatch' || | ||
| github.event.workflow_run.conclusion == 'success' | ||
| # TODO: Change back to @main before merging | ||
| uses: complytime/org-infra/.github/workflows/reusable_council_review.yml@feat/council-review-production |
hbraswelrh
left a comment
There was a problem hiding this comment.
PR Review: #313 -- feat(ci): add production council review workflow chain
Well-designed two-workflow chain for AI council review. Architecture is sound: fork-safe collect phase, consumer phase, and reusable review phase. Spec, design doc, and task breakdown are thorough. Two issues need addressing before merge.
Findings Summary
| # | Severity | Category | File | Description |
|---|---|---|---|---|
| 1 | HIGH | Alignment | reusable_council_review.yml |
Comment deduplication broken -- creates new comment on every push |
| 2 | HIGH | Constitution | All 3 workflow files | Missing header comment blocks (MUST per constitution) |
| 3 | MEDIUM | Security | reusable_council_review.yml |
PR title/diff content flows unsanitized into AI prompt |
| 4 | MEDIUM | Alignment | tasks.md |
Concurrency group uses workflow_run.id but implementation uses head_sha |
| 5 | LOW | Security | reusable_council_review.yml |
Claude CLI installed unpinned from npm |
See inline comments for details.
This review was generated by /review-pr (AI-assisted).
| - name: Install Claude Code CLI | ||
| if: steps.gate.outputs.should_review == 'true' | ||
| run: npm install -g @anthropic-ai/claude-code | ||
|
|
There was a problem hiding this comment.
[LOW] Claude CLI installed without version pin
npm install -g @anthropic-ai/claude-code installs the latest version on every run. A supply-chain compromise of this package would inject malicious code into the workflow (which has id-token: write and pull-requests: write permissions).
Consider pinning to a specific version:
| run: npm install -g @anthropic-ai/[email protected] |
(Replace 1.0.0 with the current stable version.)
|
@hbraswelrh this PR won't be merged yet, check the reviewer guidance. The PR is supposed to check the AI council review bot message instead. It has knowingly these findings to suggest how the reviews would look in the future. So it wont follow the regular PR review of getting the findings. The AI review bot already suggests the above. |
84cb377 to
4804dc4
Compare
Implements the two-workflow chain for AI council review using Claude on Vertex AI via ITPC WIF authentication: - ci_council_review_collect.yml: fork-safe diff collection (pull_request) - reusable_council_review.yml: WIF auth, Claude CLI, review posting (workflow_call) - ci_council_review.yml: thin consumer wiring collect to review (workflow_run) Includes gate checks (skip drafts, dependabot), configurable model/diff limit inputs, cross-run artifact download via run-id, and SHA-pinned actions (Node 24). Adds consumer workflows to sync-config.yml. Closes complytime/nunya#384 Assisted-by: Claude (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
Adds temporary workflow_dispatch trigger to ci_council_review.yml and points reusable workflow reference to the feature branch for pre-merge validation. Both changes marked with TODO for removal before merging. Assisted-by: Claude (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
- Fix concurrency group to use head_sha (not workflow_run.id) so rapid pushes cancel in-progress reviews per commit - Separate Claude stderr from stdout to avoid posting CLI errors as review comments - Remove invalid comment-author/body-includes inputs (not in v5 of peter-evans/create-or-update-comment) - Remove unused env vars from collect diff step Assisted-by: Claude (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
- Group GITHUB_OUTPUT redirects (SC2129 shellcheck style) - Add zizmor ignore for workflow_run dangerous-triggers (by design for fork PR support via two-workflow chain pattern) - Add zizmor ignore for temporary branch ref (pre-merge testing) Assisted-by: Claude (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
Removes workflow_dispatch trigger and temporary branch ref, restoring the consumer workflow to its production state pointing to @main. Assisted-by: Claude (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
- Add h1 headings to OpenSpec markdown files (MD041) - Fix table column spacing in design.md (MD060) - Add zizmor ignore for reusable workflow @main ref (unpinned-uses is expected for same-repo reusable workflow references) Assisted-by: Claude (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
- Replace curl|bash install with npm for Claude Code CLI to avoid pipe-to-shell security pattern flagged by GHAS - Replace em dash Unicode characters with ASCII dashes in all workflow files to resolve hidden Unicode text alerts Assisted-by: Claude (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
Replace single PR comment with structured inline review using the GitHub Pull Request Review API. Claude now produces JSON with file paths, line numbers, and comments, which are posted as inline review comments on the exact lines of code. Falls back to a regular PR comment when Claude output is not valid JSON or when the review API call fails. Assisted-by: Claude (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
Delete previous bot comments (identified by <!-- council-review-bot --> marker) before posting a new review, preventing duplicate comments on rapid pushes. Inline review comments become "outdated" naturally in GitHub UI when code changes, so only issue comments need cleanup. Assisted-by: Claude (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
4804dc4 to
dc70a82
Compare
marcusburghardt
left a comment
There was a problem hiding this comment.
Nice @sonupreetam . I included some comments more about clarification. Also thinking if ci_council_review.yml and ci_council_review_collect.yml could be merged in a single file.
| TRUNCATION_NOTE="Note: The diff was truncated from ${TOTAL_LINES} to ${MAX_LINES} lines. Review only covers the included portion." | ||
| fi | ||
|
|
||
| cat > review_prompt.txt <<'PROMPT_HEADER' |
There was a problem hiding this comment.
It would be nice if we could use the uf command review-pr or have a separate markdown for it so we don't need to touch the workflow in order to update the prompt. It could also allow users to inform where to find the prompt as a input, for example.
There was a problem hiding this comment.
I think a way this could reference the review-pr command would be referencing the uf repo review-pr command by a separate actions/checkoutstep to cat unbound-force's .opencode/commands/review-pr.md and > review_prompt
There was a problem hiding this comment.
Thank you @hbraswelrh & @marcusburghardt, for your feedbacks. I have used the claude hardcoded one-shot prompt for this initial work. But you are right about adding the uf commands already. Moving this to draft until ready. The final workflow would be basically WIF auth → Vertex AI → Claude → runs uf commands (review-council, Divisor agents).
| @@ -0,0 +1,88 @@ | |||
| --- | |||
| name: Council Review - Collect | |||
There was a problem hiding this comment.
Looking to this title alone it was not clear that the goal here is to get a collect and safe a diff to be used later. I was wondering "Collect what?". : ) Maybe we can reword a bit.
There was a problem hiding this comment.
I agree that changing the word "collect" may help clarify intent. I would even say using "cache" may be an option here. If the intent is to save the diff and use it later on the PR, it would probably be fine to say something like "Council Review - Cache Diff for Later Usage" or if keeping "collect" -> the title could be "Council Review - Collect Diff for open, non-dependabot-authored PRs"
Wdyt? @sonupreetam
There was a problem hiding this comment.
@hbraswelrh & @marcusburghardt Thank you for your feedbacks. I think "Council Review - Collect PR Diff" will be less verbose. WDYT?
| path: | | ||
| pr-diff.patch | ||
| pr-meta.json | ||
| retention-days: 1 |
There was a problem hiding this comment.
Is this enough? It is only used synchronously, right? If so, we could delete right after the review. Or we could also keep it longer. Just trying to understand why 1 day. :)
There was a problem hiding this comment.
@marcusburghardt The workflow_run trigger fires immediately when the collect workflow completes, so the review workflow downloads the artifact within seconds/minutes, so no human-in-the-loop delay between upload and download. We can delete it right after or keep it longer. But I dont see re-use scenario for keeping it longer and also for the 1 day quota was "good enough" to avoid quota pressure, and right after deletion was eliminated as it avoids an extra API call and the actions: write permission that explicit deletion would require (currently we only need actions: read for cross-workflow artifact download).
Hi @sonupreetam, I am reviewing the responses from the bot. The level of detail is very nice. It's comprehensive and provides next steps that factor in considerations from the OpenSpec design files. I believe the review serves as a great starting point when a human wants to pick up the fixes using the
|
| @@ -0,0 +1,88 @@ | |||
| --- | |||
| name: Council Review - Collect | |||
There was a problem hiding this comment.
I agree that changing the word "collect" may help clarify intent. I would even say using "cache" may be an option here. If the intent is to save the diff and use it later on the PR, it would probably be fine to say something like "Council Review - Cache Diff for Later Usage" or if keeping "collect" -> the title could be "Council Review - Collect Diff for open, non-dependabot-authored PRs"
Wdyt? @sonupreetam
| TRUNCATION_NOTE="Note: The diff was truncated from ${TOTAL_LINES} to ${MAX_LINES} lines. Review only covers the included portion." | ||
| fi | ||
|
|
||
| cat > review_prompt.txt <<'PROMPT_HEADER' |
There was a problem hiding this comment.
I think a way this could reference the review-pr command would be referencing the uf repo review-pr command by a separate actions/checkoutstep to cat unbound-force's .opencode/commands/review-pr.md and > review_prompt
| artifacts that follow the org's reusable workflow pattern and handle fork PRs | ||
| via the `workflow_run` two-workflow chain. | ||
|
|
||
| Closes complytime/nunya#384 |
There was a problem hiding this comment.
@sonupreetam Maybe we remove the reference to the private repo and document in a sub-issue on the private repo issue?
| council-review: | ||
| name: AI Council Review | ||
| if: github.event.workflow_run.conclusion == 'success' | ||
| uses: complytime/org-infra/.github/workflows/reusable_council_review.yml@main # zizmor: ignore[unpinned-uses] |
There was a problem hiding this comment.
Follow-up will be to pin the reusable_council_review.yml to a sha
- Add header comment blocks to all three council review workflow files per constitution requirement (HIGH, hbraswelrh) - Add prompt injection defense preamble and truncate PR title to 200 chars in the review prompt (MEDIUM, hbraswelrh) - Pin Claude Code CLI to v2.1.168 instead of unpinned latest (LOW, hbraswelrh + GHAS) - Rename collect workflow to "Council Review - Collect PR Diff" for clarity (marcusburghardt + hbraswelrh) - Sync tasks.md concurrency group to match implementation (head_sha not workflow_run.id) (MEDIUM, hbraswelrh) Assisted-by: Claude (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
- Fetch PR body via gh pr view instead of reading non-existent field from pr-meta.json artifact (linked issues always returned empty) - Use .labels[]? to prevent jq errors on issues with no labels - Replace shell variable interpolation with --rawfile/--body-file for safe handling of arbitrary text from Claude output - Split prompt heredoc into quoted (static) and unquoted (dynamic) sections to prevent variable expansion in methodology instructions Assisted-by: Cursor (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
Temporary testing scaffolding to manually trigger the consumer workflow with a collect run ID. Points reusable workflow reference to the feature branch for pre-merge validation. TODO: Remove workflow_dispatch and revert to @main before merging. Assisted-by: Cursor (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
Empty commit to trigger collect workflow for prototype validation. Assisted-by: Cursor (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]> Co-authored-by: Cursor <[email protected]>
Assisted-by: Cursor (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
The PR diff was truncated at 1060 lines with the 1000 default. 2000 lines covers typical PRs without excessive token usage. Assisted-by: Cursor (claude-opus-4-6) Signed-off-by: sonupreetam <[email protected]>
|
@hbraswelrh & @marcusburghardt After reviewing the invocation layer, prompt injection risk and other reviewer feedback, this PR is being reworked with a different architecture. |
Scope change: action-based architecture with OpenCodeAfter reviewing the prompt injection risk and reviewer feedback, this PR is being reworked with a different architecture: What changedThe review orchestration logic (agent discovery, prompt construction, invocation, output parsing) has been moved to a composite GitHub Action in Additionally, the invocation layer switches from Claude Code CLI to OpenCode ( This addresses:
What stays in this PR
What moved out
The Branch will be force-pushed with the reworked commits. |
Rework reusable_council_review.yml to be a thin consumer that delegates review logic to unbound-force/council-review-action. Switch invocation from Claude Code CLI to OpenCode. Add workflow_dispatch to ci_council_review.yml for manual testing and point reusable reference to feature branch. Signed-off-by: sonupreetam <[email protected]>
| - name: Run council review | ||
| if: steps.gate.outputs.should_review == 'true' | ||
| id: review | ||
| uses: unbound-force/unbound-force/council-review-action@feat/council-review-action # zizmor: ignore[unpinned-uses] |
Use fromJSON() to ensure the dispatched string input is properly coerced to number for the reusable workflow. Inline the if condition to avoid block scalar parsing issues. Signed-off-by: sonupreetam <[email protected]>
The collect workflow skipped draft PRs at the gate check, but did not trigger on ready_for_review, so undrafted PRs never got a fresh collect run. Signed-off-by: sonupreetam <[email protected]>
OpenCode expects VERTEX_LOCATION, not CLOUD_ML_REGION, for Vertex AI region configuration. Signed-off-by: sonupreetam <[email protected]>
OpenCode uses google-vertex-anthropic for Claude on Vertex AI. Remove ANTHROPIC_VERTEX_PROJECT_ID (legacy Claude CLI env var); setup-gcloud already sets GOOGLE_CLOUD_PROJECT from the auth step's project_id. Signed-off-by: sonupreetam <[email protected]>
Add continue-on-error to the action step so review failures surface as warnings, not workflow failures. Gate post steps on review outcome to skip posting when the action failed. Signed-off-by: sonupreetam <[email protected]>
|
@sonupreetam we need to review and clean-up the last comment. It is huge and hard to read. |
|
@marcusburghardt This is testing with opencode CLI (output unable to parse, as it is JSONL), ignore it for now, this will be deleted with the next runs.
|
AI Council ReviewWell-architected two-workflow chain for fork-safe AI council review. The security model, permission scoping, gate checks, and concurrency controls are correctly implemented. Two findings require attention before merge: the reusable workflow references the council-review-action at an unpinned mutable branch ref (violating the org's SHA-pinning constitution requirement documented in design decision D6), and the workflow's own reusable reference in the consumer uses a feature branch tag that will break after merge; additionally there is a default value drift between the design doc (max_diff_lines: 1000) and the implementation (2000). Model: |
Summary
pull_request), reusable review (workflow_call), and consumer (workflow_run)sync-config.ymlto sync consumer workflows to downstream reposArchitecture
Inline review comments
The council review now uses a structured prompt that asks Claude to produce JSON output with:
summaryfield for the overall PR assessmentinline_commentsarray withpath,line, andbodyfor each findingThese are posted as a GitHub PR review with inline comments on the exact lines of code, giving reviewers file-level context. If Claude produces unstructured output or the Review API call fails, the workflow gracefully falls back to a regular PR comment.
Reviewer guidance
What to review
Feedback on the AI review
The council review on this PR was generated by the workflow itself during validation. Please share your thoughts on the AI review quality:
Test plan
sonupreetam)unbound-forceproject)Closes complytime/nunya#384