Restore the fuller GitHub bot operating guidance so headless runs have clear workflow, continuation, and scope instructions for issue comments, PR openings, and review-comment events. Add runner-level coverage around accepted issue-comment dispatch and same-thread supersession. The job runner now accepts a narrow operations seam so tests can exercise workspace setup, worktree context, OpenCode command arguments, and abort behavior without real GitHub, git, or OpenCode services. Update the server test script to discover nested __tests__ files and unref delayed workspace cleanup timers so test and short-lived process exits are not held open by background cleanup scheduling. Validation: npm run test --workspace @neuralnomads/codenomad; npm run typecheck --workspace @neuralnomads/codenomad; npm run build --workspace @codenomad/codenomad-opencode-plugin; git diff --check.
12 KiB
CodeNomadBot GitHub Automation
CodeNomadBot is the GitHub App automation built into the CodeNomad server. It receives GitHub webhooks, validates them with the app webhook secret, policy-gates accepted events, prepares a local repository worktree, runs OpenCode headlessly, and lets the assistant report back through a scoped github tool.
Goals
- Turn GitHub issue and PR activity into headless OpenCode runs.
- Keep GitHub credentials in the CodeNomad server process, not inside OpenCode.
- Validate webhooks and scope all GitHub API calls to the installed repository.
- Require explicit allow policies so public repositories do not accidentally run automation.
- Avoid run races with one active run per issue/PR thread and latest-event-wins cancellation.
Architecture
The integration has four main components:
- GitHub App: signs webhooks and grants repository-scoped permissions.
- CodeNomad server: validates webhooks, manages policy, workspaces, worktrees, OpenCode sessions, and GitHub API calls.
- OpenCode instance: runs in
CODENOMAD_MODE=githubwith GitHub-only command prompts and a restrictedgithubtool. - Local git clone/worktrees: provide deterministic working directories for issue and PR automation.
Request Flow
- GitHub sends a webhook to
/integrations/github/webhook. - CodeNomad validates
x-hub-signature-256withintegrations.github.webhookSecret. - CodeNomad parses and normalizes the webhook into a repository/thread context.
integrations.github.policy.rulesselects the first matching rule. Missing rules means implicit deny.- Actor admission checks
allowedUsers,allowedAuthorAssociations,allowAllActors,denyBots, and PR synchronize-specific options. - If
requireMentionis true, the event text must mention the configured bot handle. - CodeNomad clones or updates the repository under
workspaceRoot. - CodeNomad creates or reuses a stable managed worktree for the issue/PR thread.
- CodeNomad starts or reuses an OpenCode workspace with
CODENOMAD_MODE=github. - CodeNomad creates or reuses a deterministic OpenCode session for the GitHub thread.
- CodeNomad calls
session.commandwith a selectedcodenomad-github-*command and sanitized webhook JSON. - The assistant uses the
githubtool to list comments, add reactions, post comments, and publish PRs. - On success, the assistant is responsible for posting the final GitHub comment.
- On non-cancelled failure, CodeNomad posts an automation failure comment.
GitHub App Setup
Create a GitHub App in the account or organization that owns the repositories you want CodeNomad to automate.
Recommended repository permissions:
- Metadata: read-only.
- Contents: read and write. Required to push bot branches.
- Issues: read and write. Required to read/post issue and PR conversation comments and reactions.
- Pull requests: read and write. Required to read review comments and create/update pull requests.
Subscribe to these webhook events:
- Issue comment.
- Issues.
- Pull request.
- Pull request review comment.
Also configure a webhook secret and generate a private key PEM. Store the private key on the machine running CodeNomad.
Webhook URL
Development with smee:
smee -u https://smee.io/<channel> -t http://127.0.0.1:<port>/integrations/github/webhook
Set the GitHub App webhook URL to the smee.io channel URL.
Production:
- Point the GitHub App webhook URL directly at your public CodeNomad server URL plus
/integrations/github/webhook. - Ensure the URL reaches the same CodeNomad server that has the GitHub App private key and config.
Configuration
Add configuration under the integrations owner in config.yaml:
integrations:
github:
enabled: true
appId: "123456"
privateKeyPath: "~/.config/codenomad/github-app.pem"
webhookSecret: "change-me"
workspaceRoot: "~/.config/codenomad/github-workspace"
mentionHandle: "codenomadbot"
botLogin: "codenomadbot[bot]"
commands:
default: "codenomad-github-default"
issue_comment.created: "codenomad-github-issue-comment"
pull_request_review_comment.created: "codenomad-github-review-comment"
issues.opened: "codenomad-github-issue-opened"
pull_request.opened: "codenomad-github-pr-opened"
webhook:
agent: "build"
model:
providerId: "anthropic"
modelId: "claude-sonnet-4-5"
variant: "default"
policy:
rules:
- name: "Allow maintainer issue comments"
match:
repo: "my-org/*"
event: "issue_comment.created"
allow:
requireMention: true
allowedAuthorAssociations: ["OWNER", "COLLABORATOR"]
Configuration notes:
enabledmust betrue; disabled integrations ignore all webhooks.appId,privateKeyPath, andwebhookSecretare required for accepted jobs.workspaceRootdefaults to~/.config/codenomad/github-workspace.mentionHandlecontrols which@handletriggers mention-gated rules.botLoginprevents the bot from responding to its own comments and expands mention matching to@botand@bot[bot].commandsmaps event keys to OpenCode command names. Missing mappings fall back to built-ins.webhook.agent,webhook.model, andwebhook.variantset defaults for OpenCode runs.- Policy rule
allow.command,allow.agent,allow.model, andallow.variantoverride the defaults for that rule.
Event keys use <x-github-event>.<action>, for example:
issue_comment.createdpull_request_review_comment.createdissues.openedpull_request.openedpull_request.synchronize
Policy
Policy is implicit deny. If no rule matches, the webhook is ignored and no comment is posted.
Rule match fields:
repo: glob againstowner/repo.repoRegex: regular expression againstowner/repo.event: glob against the event key.eventRegex: regular expression against the event key.
Glob matching treats / and . as separators. * does not cross separators; ** does.
Rule allow fields:
enabled: setfalsefor an explicit deny rule.requireMention: require a bot mention before running. Defaults to true.allowedUsers: explicit GitHub logins, case-insensitive.allowedAuthorAssociations: GitHub associations such asOWNER,COLLABORATOR, andMEMBER. Defaults toOWNERandCOLLABORATORwhen omitted.allowPrAuthor: forpull_request.synchronize, allow the PR author to trigger runs. Defaults to false.allowAllActors: bypass actor allow checks.denyBots: reject actors with typeBot.command,agent,model,variant: per-rule OpenCode overrides.
Policy Cookbook
Allow maintainer mentions on issue comments:
integrations:
github:
policy:
rules:
- name: "Maintainer issue comment mentions"
match:
repo: "my-org/*"
event: "issue_comment.created"
allow:
requireMention: true
allowedAuthorAssociations: ["OWNER", "COLLABORATOR"]
Allow only named users to trigger PR review comment responses:
integrations:
github:
policy:
rules:
- name: "Named PR review responders"
match:
repo: "my-org/*"
event: "pull_request_review_comment.created"
allow:
requireMention: true
allowedUsers: ["alice", "bob"]
Run issue triage when an issue opens without requiring a mention:
integrations:
github:
policy:
rules:
- name: "Issue triage"
match:
repo: "my-org/*"
event: "issues.opened"
allow:
requireMention: false
allowedAuthorAssociations: ["OWNER", "COLLABORATOR"]
Allow PR synchronize events from maintainers, but not the PR author by default:
integrations:
github:
policy:
rules:
- name: "Maintainer PR updates"
match:
repo: "my-org/*"
event: "pull_request.synchronize"
allow:
requireMention: false
allowPrAuthor: false
allowedAuthorAssociations: ["OWNER", "COLLABORATOR"]
Use a regex for one repository:
integrations:
github:
policy:
rules:
- name: "Repo A only"
match:
repoRegex: "^my-org/repo-a$"
event: "issue_comment.created"
allow:
requireMention: true
allowedAuthorAssociations: ["OWNER", "COLLABORATOR"]
Worktree Strategy
CodeNomad uses stable managed worktrees to avoid unbounded workspace growth.
Issue threads:
- Branch/worktree slug:
codenomad/issue-<issueNumber>. - Reset target: remote bot branch if it exists, otherwise the repository default branch.
PR threads:
- Branch/worktree slug:
codenomad/pr-<prNumber>. - Bot-owned PRs operate directly on the PR head branch.
- Reset target: remote bot branch if it exists, otherwise
pull/<prNumber>/head.
Publishing defaults:
- Bot-owned PR: publish against the PR base branch.
- Same-repository contributor PR: publish against the contributor head branch, creating a stacked PR.
- Fork PR: publish against the base branch because the app may not be able to push to the fork.
Every run hard-resets and cleans the worktree before OpenCode starts. Uncommitted local changes and local-only commits are not durable between webhook runs.
GitHub Tool Operations
The OpenCode plugin exposes the github tool only when CODENOMAD_MODE=github.
Supported operations:
list_issue_comments: read issue/PR conversation comments.post_issue_comment: post a comment to an issue or PR. CodeNomad appends the bot signature.add_reaction: add a reaction to an issue comment.list_pr_review_comments: read PR review comments.add_pr_review_comment_reaction: add a reaction to a PR review comment.publish_pr: push the current branch and create or return a pull request. Requires a clean working tree.
The assistant must use this tool for GitHub operations. The gh CLI is not authenticated for bot jobs.
Concurrency
Jobs are supervised by repository thread: <owner>/<repo>#<issueOrPrNumber>.
- Only one run is active for a thread.
- A newer accepted webhook for the same thread aborts the active OpenCode request and session.
- The newest pending webhook then runs.
- Superseded jobs do not post failure comments.
Operational Limitations
- Job state and delivery dedupe are in memory. Restarting CodeNomad drops in-flight jobs and seen-delivery history.
- Webhook delivery returns
202before automation finishes. - Failure comments are best-effort and are skipped for superseded runs.
- Fork PR behavior depends on GitHub App installation permissions.
- Local repository state under
workspaceRootis managed by CodeNomad and can be hard-reset.
Troubleshooting
No run starts:
- Check
integrations.github.enabled. - Check
webhookSecretand GitHub delivery signature validation. - Check that at least one policy rule matches the repository and event key.
- Check
requireMentionand the configuredmentionHandle/botLogin. - Check actor admission (
allowedUsers,allowedAuthorAssociations,denyBots).
Git operations fail:
- Confirm the app has Contents read/write permission.
- Confirm the app is installed on the repository.
- Confirm the private key path is readable by the CodeNomad process.
Commenting or reactions fail:
- Confirm Issues read/write permission.
- Confirm Pull requests read/write permission for PR review comments.
publish_pr fails:
- Commit all file changes before calling
github(op=publish_pr, ...). - Confirm the current branch is valid and the app can push branches.
- For fork PRs, expect publishing to target the base repository branch rather than the fork branch.