Skip to content

ci: add opencode GitHub code review with Muse Spark model - #162

Open
vuon9 wants to merge 43 commits into
mainfrom
chore/opencode-github-review
Open

ci: add opencode GitHub code review with Muse Spark model#162
vuon9 wants to merge 43 commits into
mainfrom
chore/opencode-github-review

Conversation

@vuon9

@vuon9 vuon9 commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • Auto-review every non-draft PR using opencode-go/muse-spark-1.2-contributor (Muse Spark via OpenCode Go)
  • On-demand review by commenting /oc or /opencode on any PR/issue
  • Uses the runner GITHUB_TOKEN (use_github_token: true), so no GitHub App install is required
  • Model API key is read from the OPENCODE_API_KEY repo secret (already set)

Notes

  • Workflow file was pushed over SSH because the local gh token lacks the workflow OAuth scope
  • Takes effect once merged to main

Test Plan

  • OPENCODE_API_KEY secret set on repo (value from local opencode auth, not exposed)
  • First PR after merge triggers an automatic review comment from opencode-agent

N/A

  • Frontend changes: N/A
  • Backend code changes: N/A

vuon9 added 2 commits August 21, 2026 17:00
- Auto-review non-draft PRs using opencode-go/muse-spark-1.2-contributor
- On-demand runs via /oc or /opencode comments
- Uses the runner GITHUB_TOKEN (use_github_token) so no GitHub App install
  is required; model API key comes from the OPENCODE_API_KEY secret
The anomalyco/opencode/github action requires GITHUB_TOKEN env when
use_github_token: true, otherwise it fails with:
'GITHUB_TOKEN environment variable is not set.'

GitHub Actions does not inject GITHUB_TOKEN into env automatically;
it must be passed explicitly via secrets.GITHUB_TOKEN.
@github-actions

Copy link
Copy Markdown
Contributor

APIError: This model collects data used to improve its quality and requires explicit opt in: https://opencode.ai/workspace/wrk_01KF3S6SYD5YNMYQWYD6S6340A/go

opencode session  |  github run

muse-spark-1.2-contributor requires training-data opt-in (DataPolicyError),
conflicts with the no-training data privacy preference. deepseek-v4-flash
is available on the same provider without opt-in.
@github-actions

Copy link
Copy Markdown
Contributor

APIError: The latest version of this model is only available hosted in China and requires explicit opt in: https://opencode.ai/workspace/wrk_01KF3S6SYD5YNMYQWYD6S6340A/go

opencode session  |  github run

opencode-go/* models are China-hosted and require opt-in (RegionError).
opencode/deepseek-v4-flash resolves via the zen gateway without opt-in.
@github-actions

Copy link
Copy Markdown
Contributor

APIError: Insufficient balance. Manage your billing here: https://opencode.ai/workspace/wrk_01KF3S6SYD5YNMYQWYD6S6340A/billing

opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

APIError: Insufficient balance. Manage your billing here: https://opencode.ai/workspace/wrk_01KF3S6SYD5YNMYQWYD6S6340A/billing

opencode session  |  github run

opencode workspace has insufficient balance (CreditsError) and the
China-hosted opencode-go models need opt-in. muse-spark-1.2-contributor-free
is the free tier of the originally intended reviewer model.
@github-actions

Copy link
Copy Markdown
Contributor

Review: ci: add opencode GitHub code review with Muse Spark model — PR #162

Scope: Single added file .github/workflows/opencode-review.yml (72 lines, 5 commits). No app code. Automated review workflow.

Overall: Functional direction is correct, but this iteration regresses trigger precision vs. archived workflows, introduces cost/DoS surface, and has standard workflow hardening gaps. Should fix on-demand trigger + pin action + add guards before merge. History comments (insufficient balance / opt-in errors at 3247...) already show reliability risk of chosen model.


🔴 Bugs / Logic Errors

1. Over-broad on-demand trigger — false positives opencode-review.yml:50-51

(contains(github.event.comment.body, '/oc') ||
 contains(github.event.comment.body, '/opencode'))
  • contains('/oc') matches any substring /oc (e.g. https://example.com/foo/oc/bar, /octocat, /october). Second condition is dead code (/opencode always contains /oc).
  • Regression vs archived: .github/workflows/_comment-review.yml.archived:12-15 correctly used contains(' /oc') || startsWith('/oc') (space-prefix) to avoid this.
  • Fix:
(contains(github.event.comment.body, ' /oc') ||
 startsWith(github.event.comment.body, '/oc') ||
 contains(github.event.comment.body, ' /opencode') ||
 startsWith(github.event.comment.body, '/opencode'))

2. on-demand fires on plain Issues, not just PRs opencode-review.yml:46-51
issue_comment fires for any issue. No guard github.event.issue.pull_request → burns OPENCODE_API_KEY quota on unrelated issues/comments. Add:

if: |
  github.event.issue.pull_request &&
  (contains(...) || startsWith(...))

Same for pull_request_review_comment — fine, but issue_comment needs the guard.

3. No actor/association guard → anyone can burn quota opencode-review.yml:46-51
Any external commenter can trigger /oc → DoS/cost exhaustion. Comments on this PR already show Insufficient balance (32471456882). Add:

&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
# or: github.actor != 'github-actions[bot]' to avoid bot loops

4. DRY / prompt drift opencode-review.yml:37-44 vs 65-72
review-pr has explicit prompt: but on-demand has none (falls back to action default). Intent unclear. Reuse prompt or extract to env/reusable workflow.

🟠 Security / Hardening

5. Unpinned mutable action opencode-review.yml:30,66 uses: anomalyco/opencode/github@latest
@latest is movable tag → supply-chain risk. Pin to SHA: anomalyco/opencode/github@<sha> # vX.Y.Z (Dependabot can still update). Same for actions/checkout@v6v6.0.3 like ci.yml:22 does.

6. Over-permissive permissions opencode-review.yml:18-21,53-57

  • id-token: write unused if use_github_token: true + GITHUB_TOKEN. Either justify (OIDC) or drop.
  • Inconsistent: review-pr: issues: read vs on-demand: issues: write. Both need pull-requests: write to comment, issues: write only if commenting on issues. Align and use least privilege.

7. Redundant token wiring opencode-review.yml:32-33,68-69
use_github_token: true already injects GITHUB_TOKEN; explicitly setting env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} is redundant/confusing. Keep one. Archived workflows didn't set it.

8. Fork PRs will silently fail opencode-review.yml:4-5
pull_request workflows don't get secrets.OPENCODE_API_KEY on forks. Review will error. Document as known limitation or switch to pull_request_target with safe checkout (with persist-credentials: false you already do — good).

🟡 Quality / Maintainability

9. No concurrency / timeout-minutes
synchronize pushes spawn parallel runs. Add:

concurrency:
  group: opencode-review-${{ github.event.pull_request.number || github.event.issue.number }}
  cancel-in-progress: true
# and
timeout-minutes: 10

at job level.

10. No workflow_dispatch for manual retry ci.yml:11 has it; useful here for debugging after Insufficient balance failures.

11. Duplicate job boilerplate Two jobs share 90% steps. Consider single job with:

if: |
  (github.event_name == 'pull_request' && !github.event.pull_request.draft) ||
  ((github.event_name == 'issue_comment' || ...) && contains(...))

Or reusable workflow. Minor, but reduces drift seen in #4.

12. Model reliability opencode-review.yml:35
opencode/muse-spark-1.2-contributor-free already produced 3 distinct API errors in this PR's checks: opt-in required, China-hosted, insufficient balance. Worth pinning fallback model or documenting that OPENCODE_API_KEY must have billing enabled. The free tier is not SLA.

⚪ Tests / Verification

  • No workflow tests — expected. No change to N/A in PR body is correct.
  • Missing: actionlint / yaml lint in CI, and a dry-run check that OPENCODE_API_KEY secret exists (fail fast with helpful message vs opaque APIError).
  • Suggested: add .github/workflows/ to actionlint step or run go vet equivalent for workflows. Verify locally with actionlint opencode-review.yml.

Concrete Suggested Patch (key lines)

-  (contains(github.event.comment.body, '/oc') ||
-   contains(github.event.comment.body, '/opencode'))
+  github.event.issue.pull_request &&
+  (contains(github.event.comment.body, ' /oc') ||
+   startsWith(github.event.comment.body, '/oc') ||
+   contains(github.event.comment.body, ' /opencode') ||
+   startsWith(github.event.comment.body, '/opencode')) &&
+  contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)

- uses: anomalyco/opencode/github@latest
+ uses: anomalyco/opencode/github@<pinned-sha> # pin, e.g. v1.x.x

- uses: actions/checkout@v6
+ uses: actions/checkout@v6.0.3

And unify prompt:, add concurrency + timeout-minutes: 10, deduplicate GITHUB_TOKEN env.

Verdict: Request changes on trigger precision + fork/DoS guards + pinning before merge. Low-risk otherwise; good fetch-depth: 1 + persist-credentials: false hygiene.

New%20session%20-%202026-08-21T10%3A13%3A22.351Z
opencode session  |  github run

vuon9 added 20 commits August 21, 2026 17:18
opencode-review.yml is now a thin dispatcher that delegates to
opencode-review-reusable.yml (workflow_call), so any repo can reuse
the review logic via:
  uses: vuon9/devtoolbox/.github/workflows/opencode-review-reusable.yml@main
  secrets:
    OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}

Reusable workflow defaults to free-tier model (no billing, no opt-in).
workflow_call inputs do not support multi-line default values (startup
failure). Move the default review prompt to a job-level env var and pick
it in a prepare step instead.
GITHUB_OUTPUT rejects multi-line values written as plain echo. Use the
heredoc delimiter form (value<<PROMPT_EOF ... PROMPT_EOF) so the default
multi-line review prompt passes through correctly.
GitHub rejects if: conditions on jobs that call reusable workflows
(startup_failure even with if: true). Move the pr/comment gating into
the reusable workflow's job-level if, selected via a mode input.

Also fix multi-line prompt output: GITHUB_OUTPUT requires the heredoc
delimiter form for multi-line values.
vuon9 added 16 commits August 21, 2026 17:43
- Default model switched to opencode-go/hy3 per request (no billing)
- Pin anomalyco/opencode/github@v1.18.20: @latest is not resolvable
  inside a reusable workflow and causes startup_failure
- Remove temporary isolation test workflows
The anomalyco/opencode/github composite action cannot be resolved inside
a reusable workflow (startup_failure regardless of @latest or pinned tag).
Inline its exact steps (version lookup, cache, install, PATH, run) as
plain shell steps so the reusable workflow parses and runs.
GitHub caches workflow definitions by file path. The opencode-review.yml
path was cached with a broken definition (if-on-uses-job, composite action
inside reusable), so every edit kept failing with startup_failure. New
paths ai-code-review.yml + ai-code-review-reusable.yml parse fresh:

- default model opencode-go/hy3
- mode input gates pr vs /oc on-demand inside the reusable job
- opencode run inlined as shell steps (composite action not resolvable
  inside reusable workflows)
- multi-line prompt via GITHUB_OUTPUT heredoc form
Root cause of repeated startup_failure: a permissions block inside a
reusable workflow (workflow_call) job is rejected by GitHub Actions.
Move permissions to the caller jobs (review-pr, on-demand); the reusable
job now inherits them.

Confirmed by isolation test: identical caller+reusable pair succeeds with
permissions on caller, fails with permissions inside reusable.
@github-actions

Copy link
Copy Markdown
Contributor

I've read both workflow files. Note this is a rewrite of the earlier opencode-review.yml approach (manual CLI install + opencode github run instead of the anomalyco/opencode/github@latest action), so prior-review notes about pinning that action no longer apply. Here's my review of the current code.


Review: ci: add opencode GitHub code review — PR #162

Two new files, no app code. The reuse-via-workflow_call structure is sound, but there are real trigger bugs, cost/DoS exposure, and hardening gaps.

🔴 Bugs / Logic Errors

1. pull_request_review_comment trigger is dead for on-demand ai-code-review-reusable.yml:55

(inputs.mode == 'comment' && (contains(github.event.comment.body, '/oc') || ...))

For pull_request_review_comment events the body lives at github.event.review_comment.body, not github.event.comment.body. So a /oc written as an inline code review comment never matches (expression evaluates false), and that trigger in ai-code-review.yml:25-27 does nothing. Either add a github.event.review_comment.body branch or drop the trigger.

2. Substring /oc match → false positives + dead /opencode clause ai-code-review-reusable.yml:55
contains(body, '/oc') matches /octocat, /october, https://…/oc/…. And /opencode always contains /oc, so the second contains is dead code. Fix with word-boundary checks:

contains(body, ' /oc') || startsWith(body, '/oc') ||
contains(body, ' /opencode') || startsWith(body, '/opencode')

3. on-demand fires on plain Issues, not just PRs ai-code-review-reusable.yml:55
issue_comment fires for any issue. No guard for github.event.issue.pull_request, so the action may try to "review" an unrelated issue and waste OPENCODE_API_KEY quota. Add github.event.issue.pull_request && to the comment branch.

🟠 Security / Cost / Hardening

4. No actor/association guard → anyone can burn quota ai-code-review-reusable.yml:53-55
Any external commenter posting /oc triggers a paid model call (this PR’s run logs already show Insufficient balance). Add an author_association guard:

contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)

Also exclude the bot itself to avoid loops: github.actor != 'github-actions[bot]'.

5. Unused / over-broad permission ai-code-review.yml:32,44
id-token: write is unused (use_github_token: true + GITHUB_TOKEN). Drop it. Also on-demand requests issues: write while review-pr only needs issues: read — use least privilege; both need pull-requests: write to comment.

6. Unpinned mutable actions ai-code-review-reusable.yml:68,94
actions/checkout@v6 and actions/cache@v4 are moving tags (supply-chain risk). Pin to SHAs (e.g. mimic ci.yml which pins checkout@v6.0.3).

7. Remote script executed as root ai-code-review-reusable.yml:102

curl -fsSL https://opencode.ai/install | bash

Acceptable as official install, but it ignores the VERSION computed at :90 — so the cache key (:97) varies with releases yet install always pulls latest, making the cache effectively never reused. Either pass the version to the installer or remove the version step/cache-key dependency.

🟡 Quality / Maintainability

8. Fork PRs will silently fail ai-code-review.yml:22-23
pull_request (non-_target) doesn't expose secrets.OPENCODE_API_KEY to forks, so review errors opaquely. Document as a known limitation or switch to pull_request_target with a safe checkout (you already use persist-credentials: false, good).

9. No concurrency / timeout-minutes
synchronize pushes spawn parallel review jobs. Add at the dispatcher jobs:

concurrency:
  group: ai-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true
timeout-minutes: 15

10. Missing workflow_dispatch inputs
Dispatcher has workflow_dispatch (:21) but no inputs, so manual runs can’t set model/prompt/mode — only the reusable defaults apply. Either add inputs or remove the trigger.

11. Redundant GITHUB_TOKEN wiring ai-code-review-reusable.yml:114
USE_GITHUB_TOKEN: "true" already injects the token; setting GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} explicitly is redundant (harmless, but confusing).

12. Expression injection in bash ai-code-review-reusable.yml:77

PROMPT="${{ inputs.prompt }}"

A caller-supplied prompt containing " breaks the assignment. Pass it via an env var instead of inline interpolation.

⚪ Tests / Verification

  • No workflow tests — expected for CI. Missing: run actionlint on these files in CI; add a fast guard that fails clearly when OPENCODE_API_KEY is absent (vs. opaque APIError).
  • The chosen model (opencode-go/hy3) is more reliable than the earlier muse-spark free tier (which produced opt-in/region/balance errors), but document that billing must be enabled.

Suggested key patch

   if: |
-    (inputs.mode == 'pr' && github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
-    (inputs.mode == 'comment' && (contains(github.event.comment.body, '/oc') || contains(github.event.comment.body, '/opencode')))
+    (inputs.mode == 'pr' && github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
+    (inputs.mode == 'comment' &&
+     github.event.issue.pull_request &&
+     github.actor != 'github-actions[bot]' &&
+     contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) &&
+     (contains(github.event.comment.body, ' /oc') || startsWith(github.event.comment.body, '/oc') ||
+      contains(github.event.comment.body, ' /opencode') || startsWith(github.event.comment.body, '/opencode')))

And add a github.event.review_comment.body branch if you keep the pull_request_review_comment trigger.

Verdict: Request changes on #1#4 (trigger correctness, issue/DoS guards) before merge. Good reuse structure and persist-credentials: false hygiene otherwise.

New%20session%20-%202026-08-21T11%3A00%3A49.031Z
opencode session  |  github run

vuon9 added 2 commits August 21, 2026 18:03
From the hy3 review on PR #162:
- Fix on-demand gating: support both issue_comment and
  pull_request_review_comment event payload shapes, word-boundary /oc
  match, PR-only guard, author_association guard, exclude bot
- Least privilege: drop unused id-token, issues: write -> read
- Pin actions: checkout@v6.0.3, cache@v5.0.5 (match ci.yml)
- concurrency group per PR/event with cancel-in-progress: a new push
  cancels the previous run instead of stacking more runs/comments
- timeout-minutes: 10 on the review job so a slow/hung model run
  fails fast instead of hanging ~50 minutes
@github-actions

Copy link
Copy Markdown
Contributor

I've reviewed the two workflow files. Here is my structured review.


Code Review: ci: add opencode GitHub code review — PR #162

Two new files, no app code. The reusable/dispatcher split is a clean structure, and the author already addressed several prior concerns (action pinning, id-token removed, author_association + bot-actor guards, persist-credentials: false, concurrency, timeout-minutes). However, there is one critical trigger bug that makes the pull_request_review_comment path dead, plus a few correctness/robustness issues.

🔴 Bugs / Logic Errors

1. Inline review comments (pull_request_review_comment) never trigger — dead branch ai-code-review-reusable.yml:53-66

The if unconditionally requires contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) (line 58). For a pull_request_review_comment event, github.event.comment does not exist (only github.event.review_comment does), so .author_association is nullcontains([...], null) is false → the entire expression is false.

The body-matching clauses do include github.event.review_comment.body (lines 63-66), but they're gated behind the github.event.comment.author_association check, so they can never succeed either. Net effect: the pull_request_review_comment trigger declared in ai-code-review.yml:26-27 does nothing.

Fix — split the guards by event source:

(inputs.mode == 'comment' &&
 github.actor != 'github-actions[bot]' &&
 (github.event.pull_request != null || github.event.issue.pull_request != null) &&
 (
   (github.event.comment != null &&
    contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) &&
    (contains(github.event.comment.body, ' /oc') || startsWith(github.event.comment.body, '/oc') ||
     contains(github.event.comment.body, ' /opencode') || startsWith(github.event.comment.body, '/opencode')))
   ||
   (github.event.review_comment != null &&
    contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review_comment.author_association) &&
    (contains(github.event.review_comment.body, ' /oc') || startsWith(github.event.review_comment.body, '/oc') ||
     contains(github.event.review_comment.body, ' /opencode') || startsWith(github.event.review_comment.body, '/opencode')))
 ))

2. Prompt injection / broken assignment via inline interpolation ai-code-review-reusable.yml:89

PROMPT="${{ inputs.prompt }}"

If inputs.prompt contains a double quote or a newline, this breaks the assignment or injects shell commands. Currently the only caller passes the default (''), so risk is low, but the reusable workflow is documented for external reuse with a custom prompt. Pass it via an env var instead:

env:
  PROMPT: ${{ inputs.prompt }}
run: |
  p="${PROMPT:-$DEFAULT_PROMPT}"

(or use the environment file / >> "$GITHUB_OUTPUT" without interpolation).

🟠 Robustness / Correctness

3. Cache key never matches install — cache is dead weight ai-code-review-reusable.yml:98-114

VERSION is computed from the latest release and used in the cache key (line 109), but the install step (line 114) always runs curl -fsSL https://opencode.ai/install | bash, which ignores VERSION and installs the newest build. So the cache key changes on every release while the install always pulls latest → cache-hit is effectively never true. Either pin the install to $VERSION or drop the version from the key. As written, the actions/cache step adds no value.

4. Redundant GITHUB_TOKEN wiring ai-code-review-reusable.yml:126 + 127

USE_GITHUB_TOKEN: "true" already injects the runner token; explicitly setting GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} (line 126) is redundant. Harmless, but remove for clarity (the dispatcher/docs no longer claim this is needed).

5. Shared concurrency group can cancel a running review ai-code-review.yml:31-33

Both review-pr and on-demand share group: ai-code-review-${{ ... }}. A /oc comment (issue_comment) on a PR whose auto-review is still running will cancel the in-flight review-pr job (cancel-in-progress: true). Consider separate groups per job/mode, or scope the group to github.event_name.

🟡 Quality / Maintainability

6. Doc/code drift ai-code-review-reusable.yml:5-14 vs actual

The header comment example still shows id-token: write in the caller permissions block, but neither the dispatcher nor the reusable job requests it. Update the comment so copy-paste callers don't add an unused permission.

7. workflow_dispatch has no inputs ai-code-review.yml:21

workflow_dispatch: is declared but supplies no inputs, and the two uses: jobs can't receive dispatch inputs anyway (they're hardcoded mode: pr/comment). So manual dispatch can only ever run the default PR review. Either add inputs + a third dispatch-driven job or drop the trigger to avoid implying configurability.

8. Fork PRs fail opaquely ai-code-review.yml:22-23

pull_request (not _target) doesn't expose secrets.OPENCODE_API_KEY to fork PRs, so review-pr errors with an opaque API error. This is acceptable for a personal repo, but worth a one-line comment as a known limitation.

⚪ Tests / Verification

  • No workflow-level tests — expected. Missing: an actionlint step in CI for these files, and a fast fail when OPENCODE_API_KEY is absent (clear "secret not configured" vs. opaque APIError). Earlier run logs on this PR already showed Insufficient balance from the model provider — document that billing must be enabled on the key.

Verdict

Request changes on #1 (critical: inline review-comment trigger is dead) and #2 (prompt interpolation) before merge. #3#5 are worth fixing for correctness/cleanliness. The structure, least-privilege permissions, and action pinning are in good shape.

Suggested minimal patch for #1 (replace lines 53-66 if:), and for #2 replace line 89 with env-var passing.

New%20session%20-%202026-08-21T11%3A52%3A43.252Z
opencode session  |  github run

@github-actions

Copy link
Copy Markdown
Contributor

Code Coverage

Package Line Rate Health
devtoolbox/internal/barcode 93%
devtoolbox/internal/codeformatter 80%
devtoolbox/internal/converter 65%
devtoolbox/internal/datagenerator 81%
devtoolbox/internal/datetimeconverter 75%
devtoolbox/internal/jwt 47%
devtoolbox/internal/numberconverter 98%
devtoolbox/internal/settings 0%
devtoolbox/internal/themes 0%
Summary 70% (3726 / 5294)

Minimum allowed line rate is 60%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant