# opencode Forgejo Action A Forgejo Action that runs [opencode](https://opencode.ai) 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-amd64` with `node: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](#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. ```yaml 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:////` — 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: /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: 1. Installs Bun and the opencode CLI. 2. Runs [`bun install`](package.json) inside the action source. 3. Registers `${MODEL%/*}` as a custom opencode provider via `OPENCODE_CONFIG_CONTENT` and the API key via `OPENCODE_AUTH_CONTENT`. 4. Spawns `opencode serve` locally and talks to it over HTTP (`/session/{id}/message`). 5. 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_TOKEN` is read-only, used by the outer process for fetches and read APIs. `FORGEJO_PUSH_TOKEN` is 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`, and `GITHUB_TOKEN` are removed. The agent's `bash` tool inherits opencode's env, so these variables are unreachable from any shell the agent runs. - **Per-command git auth.** Credentials for `git fetch` / `git push` are passed via `git -c http..extraheader=...` on each invocation, not persisted in `.git/config`. A jailbroken agent cannot `git push` even 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): ```bash 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