9.6 KiB
opencode Forgejo Action
A Forgejo Action that runs opencode against a Nomyo (or any OpenAI-compatible) backend, triggered by comments on issues and merge requests.
Mention /opencode or /oc in a comment and opencode reads the thread, executes the requested task, and replies. For "fix" tasks it opens a merge request; for MR comments it commits to the same MR.
Features
Explain an issue
/opencode explain this issue
opencode reads the full thread and replies with an explanation.
Fix an issue
/opencode fix this
opencode creates a branch, implements the change, and opens a merge request.
Update an open MR
Delete the attachment from S3 when the note is removed /oc
opencode commits to the same MR branch.
Review specific code lines
Comment directly on lines in the MR's "Files" tab — opencode receives the file path, line numbers, and diff hunk as context.
/oc add error handling here
Installation
Prerequisites
- A Forgejo instance with Actions enabled.
- A runner that can run Docker containers (the example below uses
docker-amd64withnode:lts-bookworm). - A Forgejo PAT (see scopes below) and a Nomyo API key.
Forgejo PATs
The action uses two Forgejo PATs with split duties — see Security for the rationale. Generate both at Settings → Applications → Manage Access Tokens (Forgejo's write scope inherently grants read on the same resource):
Read PAT — used by the outer action for fetches and read APIs. Never exposed to the opencode subprocess.
| Scope | Used for |
|---|---|
read:repository |
Clone, fetch repo info |
read:issue |
Read issue/MR thread, comments, files, commits, reviews |
read:user |
Resolve actor identity (optional, removes log noise) |
Push PAT — used only after the agent finishes, for git push and write APIs.
| Scope | Used for |
|---|---|
write:repository |
Push commits and branches |
write:issue |
Create + update comments, open MRs |
If you only have one PAT, give it the write scopes and use it for both — backward compatible.
Secrets
In Repo Settings → Actions → Secrets add:
| Secret | Value |
|---|---|
FORGEJO_TOKEN |
Read PAT (or your single full-access PAT) |
FORGEJO_PUSH_TOKEN |
Write PAT (omit to share FORGEJO_TOKEN for writes too) |
NOMYO_API_KEY |
Your Nomyo API key |
Workflow file
Add .forgejo/workflows/opencode.yml to the repo where you want opencode to respond. This example targets a Forgejo instance hosted under a subpath (bitfreedom.net/code/); adjust URLs and runs-on to match your setup.
name: opencode
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
jobs:
opencode:
if: |
contains(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, '/opencode') ||
contains(github.event.review.body, '/oc') ||
contains(github.event.review.body, '/opencode')
runs-on: docker-amd64
container:
image: node:lts-bookworm
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
steps:
- name: Install git, curl and Docker
run: |
apt-get update -qq
apt-get install -y -qq git curl unzip docker.io
- name: Start Docker daemon
run: |
dockerd --host=unix:///var/run/docker.sock --iptables=false --dns=8.8.8.8 --dns=8.8.4.4 > /tmp/dockerd.log 2>&1 &
for i in $(seq 1 30); do
sleep 2
docker info > /dev/null 2>&1 && echo "Docker daemon ready" && exit 0
echo "Waiting for Docker daemon... ($i/30)"
done
cat /tmp/dockerd.log
exit 1
- name: Checkout repository
run: |
git clone --depth=1 --branch "${{ github.ref_name }}" \
"https://oauth2:${{ github.token }}@bitfreedom.net/code/${{ github.repository }}.git" \
.
- name: Fetch action source
run: |
git clone --depth=1 --branch v1 \
"https://oauth2:${{ github.token }}@bitfreedom.net/code/nomyo-ai/actions.git" \
./.opencode-action
- name: Run opencode
uses: ./.opencode-action
with:
nomyo_api_key: ${{ secrets.NOMYO_API_KEY }}
model: nomyo/unsloth/Qwen3.6-35B-A3B-GGUF:UD-Q4_K_M
forgejo_api_url: https://bitfreedom.net/code/
forgejo_token: ${{ secrets.FORGEJO_TOKEN }}
forgejo_push_token: ${{ secrets.FORGEJO_PUSH_TOKEN }}
Why the manual clone?
act_runner's uses: parser expects https://<host>/<owner>/<repo> — it does not handle a Forgejo instance hosted under a subpath (e.g. bitfreedom.net/code/). For subpath instances, clone the action source into ./.opencode-action and reference it as a local action (uses: ./.opencode-action). If your Forgejo lives at a domain root, you can replace those two steps with uses: <your-host>/nomyo-ai/actions@v1 directly.
Configuration
| Input | Required | Default | Description |
|---|---|---|---|
model |
Yes | — | Model in the form provider/model. The provider name becomes a custom opencode provider; the action registers it as an @ai-sdk/openai-compatible provider pointing at nomyo_api_url. |
nomyo_api_key |
Yes | — | API key for the OpenAI-compatible backend. |
nomyo_api_url |
No | https://chat.nomyo.ai/api |
Base URL of the OpenAI-compatible endpoint. The adapter calls ${baseURL}/chat/completions. |
forgejo_api_url |
No | https://bitfreedom.net/code/ |
Forgejo instance base URL. |
forgejo_token |
No | — | Forgejo PAT used by the outer action for read ops (clone, fetch, read APIs). Stripped from the opencode subprocess env. |
forgejo_push_token |
No | falls back to forgejo_token |
Optional separate write PAT used only after the agent finishes (git push, comment write, MR create). |
agent |
No | — | Primary agent name to use. |
share |
No | auto | Share the opencode session (true/false). Defaults to true for public repos. |
prompt |
No | — | Custom prompt override. |
mentions |
No | /opencode,/oc |
Comma-separated trigger phrases. |
variant |
No | — | Provider-specific reasoning effort (high, max, minimal, …). |
Supported Events
| Event | Triggered by | Notes |
|---|---|---|
issue_comment |
Comment on an issue or MR | Body must contain a mention phrase |
pull_request_review_comment |
Comment on a specific line in an MR's Files tab | Receives file path, line number, and diff hunk |
pull_request_review |
Whole-review submission (Approve / Request changes / Comment) on an MR | Mention must appear in the review body |
Architecture
This action is a composite action. On each run it:
- Installs Bun and the opencode CLI.
- Runs
bun installinside the action source. - Registers
${MODEL%/*}as a custom opencode provider viaOPENCODE_CONFIG_CONTENTand the API key viaOPENCODE_AUTH_CONTENT. - Spawns
opencode servelocally and talks to it over HTTP (/session/{id}/message). - Resolves the event, builds a prompt with thread/diff context, sends it to opencode, and posts the response as a comment (and, for "fix" intents, opens an MR).
There is no compiled dist/ artifact — index.ts is executed directly by Bun.
Security
The agent runs untrusted-ish code (model output executes shell commands, edits files, etc.). The action takes the following defensive measures:
- Two-token model.
FORGEJO_TOKENis read-only, used by the outer process for fetches and read APIs.FORGEJO_PUSH_TOKENis write-capable and is only loaded into the outer process — it is never placed into the opencode subprocess environment, and it is never written to.git/config. - Env stripping. When the action spawns
opencode serve, the child env is filtered:FORGEJO_TOKEN,FORGEJO_PUSH_TOKEN, andGITHUB_TOKENare removed. The agent'sbashtool inherits opencode's env, so these variables are unreachable from any shell the agent runs. - Per-command git auth. Credentials for
git fetch/git pushare passed viagit -c http.<host>.extraheader=...on each invocation, not persisted in.git/config. A jailbroken agent cannotgit pusheven with a valid remote. - Nomyo key exposure (unavoidable).
OPENCODE_AUTH_CONTENT(containing the Nomyo API key) must be in opencode's env for the model to work; the agent's bash tool can read it. Treat the Nomyo key as compromised from the agent's perspective and rotate accordingly.
Development
Local test loop (requires Bun and opencode on PATH):
export NOMYO_API_KEY="..."
export NOMYO_API_URL="https://chat.nomyo.ai/api"
export FORGEJO_API_URL="https://bitfreedom.net/code/"
export FORGEJO_TOKEN="..." # read-scoped PAT
export FORGEJO_PUSH_TOKEN="..." # optional write-scoped PAT; falls back to FORGEJO_TOKEN
export MODEL="nomyo/unsloth/Qwen3.6-35B-A3B-GGUF:UD-Q4_K_M"
export GITHUB_RUN_ID="test-run"
export GITHUB_RUN_NUMBER="1"
export GITHUB_SERVER_URL="https://bitfreedom.net/code"
export GITHUB_REPOSITORY="owner/repo"
export GITHUB_REF_NAME="main"
export GITHUB_ACTOR="testuser"
export GITHUB_EVENT_NAME="issue_comment"
export GITHUB_EVENT_PATH="/tmp/event.json"
cat > /tmp/event.json <<'EOF'
{
"repository": { "owner": { "login": "owner" }, "name": "repo", "full_name": "owner/repo" },
"issue": { "number": 1, "index": 1, "title": "Test", "body": "Test", "user": { "login": "testuser" } },
"comment": { "id": 1, "body": "/oc explain this", "user": { "login": "testuser" } }
}
EOF
bun install
bun run index.ts
License
MIT