Forgejo Action that runs opencode on /opencode mentions in issues and PRs, backed by NOMYO AI or any OpenAI-compatible model. https://www.nomyo.ai/
Find a file
2026-05-13 17:20:11 +02:00
nyx-scan nyx-scan/action.yml aktualisiert 2026-05-13 17:20:11 +02:00
.gitignore chore: untrack node_modules 2026-05-11 18:57:07 +02:00
action.yml feat: seperate read/write token for enhanced security 2026-05-11 17:27:02 +02:00
bun.lock initial commit 2026-05-11 11:18:49 +02:00
index.ts fix: remove bun timeout 2026-05-12 09:21:21 +02:00
LICENCE doc: update 2026-05-11 17:03:20 +02:00
package.json initial commit 2026-05-11 11:18:49 +02:00
README.md doc: update 2026-05-11 17:53:50 +02:00
tsconfig.json initial commit 2026-05-11 11:18:49 +02:00

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-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 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:

  1. Installs Bun and the opencode CLI.
  2. Runs bun install 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.<host>.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):

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