Multica Docs

GitHub integration

Connect a GitHub App once, then PRs whose title, body, or branch reference an issue identifier auto-attach to that issue — and qualifying merges can move the issue to Done.

Connect a GitHub account or organization once in Settings → GitHub. After that, any pull request whose title, body, or head branch contains an issue identifier (for example MUL-123) is auto-linked to that issue, appears under Pull requests in the issue sidebar, and can move the issue to Done when merge-gate conditions are met.

There is no per-issue setup. The whole flow is identifier-driven.

What the integration does

SurfaceBehavior
Settings → GitHubAll workspace members can view a read-only installation list (account and connected-by details). Numeric GitHub installation IDs are redacted for non-managers. Connect/Disconnect controls remain owner/admin only.
Issue sidebar → Pull requestsEvery PR auto-linked to this issue, with title, repo, state (Open / Draft / Merged / Closed), author, and a checks badge from aggregated check-suite status (passed / failed / pending). Click a row to jump to the PR on GitHub.
Webhook (background)On pull_request and check_suite events, Multica upserts the PR row, aggregates check suites per PR head SHA, scans identifiers, and (re)builds link rows. Idempotent — replaying a delivery is a no-op.
Auto-status on mergeA linked issue moves to Done only when at least one merged linked PR declares a closing keyword (Closes/Fixes/Resolves MUL-X) and no linked PR is still Open or Draft. The status change is timeline-logged with source github_pr_merged.

Commits, branch refs without an open PR, and PR comments are still out of scope. PR rows + aggregated check-suite conclusions are modeled.

How identifiers are matched

The webhook extracts identifiers from three fields, in this order: PR title, PR body, PR head branch. The matcher is:

  • Case-insensitive — mul-123, MUL-123, Mul-123 all match.
  • Bounded — a \b on the left and a digit anchor on the right keep it from grabbing version numbers like v1.2-3 or email-style strings.
  • Workspace-scoped — only matches the workspace's own issue prefix. FOO-1 in a workspace whose prefix is MUL is ignored, even if the integer matches another issue.
  • Deduplicated — listing MUL-1, MUL-1 in the body links the issue once.
  • Closing-keyword aware — Closes / Fixes / Resolves MUL-X in PR title/body marks close intent; bare mentions still link, but do not count as close intent.

You can reference multiple issues in one PR. Closes MUL-1, MUL-2 links the PR to both, and each issue advances to Done only after its own linked-PR gate is satisfied.

The auto-merge-to-Done rule

When a PR's merged field flips to true, every linked issue is evaluated with three gates first:

  1. At least one merged linked PR has close intent (Closes / Fixes / Resolves MUL-X in title/body).
  2. No linked PR for that issue is still Open or Draft.
  3. The issue is not already terminal (Done / Cancelled).

If all three pass, status handling is:

Issue current statusResult
doneNo change (already terminal).
cancelledNo change — cancelled means the user explicitly abandoned the work; the integration does not override that signal.
Anything else (todo, in_progress, in_review, blocked, backlog)Moved to done.

Closing a PR without merging it only updates the PR card's state to Closed. Linked issues stay where they were unless another merged close-intent PR and the "no open/draft siblings" gate together qualify.

The action is attributed to the system actor on the timeline. Subscribers of the issue receive an inbox notification for the status change, the same way they would if a human had moved it.

What's not auto-linked

  • Identifiers in commit messages — only branch / title / body are scanned. A commit titled MUL-123: fix login does not auto-link unless the same string also appears in the PR title or body.
  • Identifiers in PR comments — only the PR's own metadata is scanned; later GitHub comments are ignored.
  • PRs in repos the App isn't installed on — without the App, Multica never receives the webhook.
  • Manually linking a PR to an issue — there is no UI for this yet. If your team's convention puts identifiers in a place Multica isn't reading, add them to the PR title or body.

Disconnecting

You can manage installations from either side:

  • From GitHub — uninstall the Multica GitHub App at https://github.com/settings/installations (personal) or https://github.com/organizations/<org>/settings/installations (org). Multica receives the installation.deleted webhook and drops the row in real time; any open Settings tab updates without a refresh.
  • From Multica (owner/admin only) — Disconnect is hidden for non-managers. It stays available even when the master GitHub switch is off, so owners/admins can still revoke a stale installation after one-click-disabling the feature.

After disconnect, mirrored PR rows stay in the database so historical issue sidebars still show what was linked, but no new webhook events from that installation will be accepted.

Permissions and visibility

  • Connect / disconnect require workspace owner or admin. Members see the card description but no Connect button.
  • The Pull requests sidebar on an issue is visible to anyone who can read the issue — same permissions as the rest of issue detail.
  • The inbound GitHub App configured in Settings requests read-only access to pull requests, checks, and metadata. That App never pushes commits, comments, or status checks back to GitHub.

The chain-execution workflow uses a separate daemon-side GitHub App for outbound agent PR, review, and merge actions. It has no webhook, is not installed through Settings → GitHub, and is configured with MULTICA_CHAIN_GH_APP_* daemon env vars.

Self-host setup

If you're running Multica on Multica Cloud, the integration is already configured — skip this section.

For self-host, you create one GitHub App, point it at your server, and set two environment variables. The whole flow is below.

1. Create a GitHub App

Go to one of:

  • Personal account → https://github.com/settings/apps/new
  • Organization → https://github.com/organizations/<org>/settings/apps/new

Fill in:

FieldValue
GitHub App nameAnything recognizable, e.g. Multica or Multica (staging).
Homepage URLYour Multica frontend, e.g. https://multica.example.com.
Callback URLLeave blank — Multica doesn't use OAuth user identity.
Setup URLhttps://<api-host>/api/github/setup. Check "Redirect on update".
Webhook → ActiveEnabled.
Webhook URLhttps://<api-host>/api/webhooks/github.
Webhook secretGenerate a long random string (e.g. openssl rand -hex 32). You'll paste the same value into Multica's env in step 2.
Permissions → Repository → Pull requestsRead-only.
Permissions → Repository → MetadataRead-only (mandatory).
Permissions → Repository → ChecksRead-only — required for the Check suites event; GitHub rejects an App that subscribes to check_suite without it.
Subscribe to eventsTick Pull request and Check suites.
Where can this GitHub App be installed?Your choice. Only on this account is fine for single-org setups.

After Create GitHub App, note two things from the App's detail page:

  • The public link at the top — its tail is the slug. https://github.com/apps/multica-acme → slug = multica-acme.
  • The webhook secret you just generated (you can't read it back from GitHub later — save it now).

Webhook secret ≠ Client secret. The App settings page has both fields stacked together. The Webhook secret is what signs pull_request payloads — that's the one Multica needs. The Client secret is for OAuth and is not used by this integration. Mixing them up produces a confusing 401 invalid signature on every webhook delivery.

2. Set environment variables

On the API server:

GITHUB_APP_SLUG=multica-acme
GITHUB_WEBHOOK_SECRET=<the webhook secret you generated>

Both variables are required. If either is missing:

  • Connect GitHub in Settings is disabled and shows a "not configured" hint.
  • The /api/webhooks/github endpoint returns 503 github webhooks not configured — Multica refuses to process events with no secret, rather than silently treating every signature as valid.

FRONTEND_ORIGIN must also be set (it already is for any production self-host); the setup callback bounces the user back to <FRONTEND_ORIGIN>/settings?tab=github after install.

Restart the API after setting the env vars.

Chain App for agent PR/review/merge

If you run the issue-chain workflow, create a second GitHub App for the daemon. This Chain App is intentionally separate from the inbound webhook App above:

SettingValue
WebhookDisabled
Repository permissionsContents: Read and write; Pull requests: Read and write; Metadata: Read-only
Install scopeOnly the repositories agents may push/review/merge

On the daemon host, set:

MULTICA_CHAIN_GH_APP_ID=<numeric app id>
MULTICA_CHAIN_GH_APP_INSTALLATION_ID=<numeric installation id>
MULTICA_CHAIN_GH_APP_REPOSITORY_IDS=<comma-separated repository ids>
MULTICA_CHAIN_GH_APP_PRIVATE_KEY_PATH=/etc/multica/chain-app.pem

The daemon never passes its own process-level GH_TOKEN / GITHUB_TOKEN through to child agents. For chain tasks, it mints repository-scoped installation tokens and injects them as GH_TOKEN / GITHUB_TOKEN only for coda, wren, felix, and dax. If token minting fails for a chain task, the task fails closed rather than using any inherited GitHub token. For non-chain agents on deployments without the Chain App, configure GitHub credentials through the agent environment endpoint.

3. Run migrations

The integration ships its tables in migration 079_github_integration. If you're upgrading an older deployment:

make migrate-up

Three tables get created: github_installation, github_pull_request, issue_pull_request. They cascade-delete with their workspace, so removing a workspace cleans them up automatically.

4. Connect from the UI

In Multica:

  1. Open Settings → GitHub as an owner or admin.
  2. Click Connect GitHub. GitHub opens in a new tab.
  3. Pick the repositories to grant access to and Install.
  4. GitHub redirects back to <api-host>/api/github/setup, which records the installation and bounces you to <FRONTEND_ORIGIN>/settings?tab=github&github_connected=1.

After that, open any PR whose branch / title / body contains an issue identifier — within a few seconds the Pull requests block appears on that issue's detail page.

5. Verify with a curl probe

If GitHub's Recent Deliveries page reports 401 invalid signature after install, the two sides have different secrets. The fastest way to find out which side is wrong is to bypass GitHub:

SECRET="<the value you put in GITHUB_WEBHOOK_SECRET>"
BODY='{"zen":"test"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl -i -X POST https://<api-host>/api/webhooks/github \
  -H "X-Hub-Signature-256: sha256=$SIG" \
  -H "X-GitHub-Event: ping" \
  -H "Content-Type: application/json" \
  -d "$BODY"
HTTP statusMeaningFix
200 {"ok":"pong"}Server's loaded secret matches your $SECRET. The mismatch is on GitHub.Edit the App → Webhook secret → paste the same valueSave changes (clicking out of the field without Save keeps the old secret). Redeliver.
401 invalid signatureServer's loaded secret is not what you think it is.Confirm the env var landed in the running process (e.g. kubectl exec → `echo -n "$GITHUB_WEBHOOK_SECRET"
503 github webhooks not configuredGITHUB_WEBHOOK_SECRET is empty in the process.Set the env var, restart the API.

Limitations

A few rough edges to be aware of today:

  • No manual link UI yet — the only way to link a PR is to have the identifier in its branch, title, or body.
  • No workspace-level config for the merge → Done rule — the close-intent + sibling-resolution gate is fixed today (merged → done, unless cancelled). Workspace-customizable mappings are a future addition.

Next

  • Issues — the issue identifiers (MUL-123) referenced from PRs
  • Workspaces — where the workspace-specific issue prefix is set
  • Environment variables — full env reference, including the GitHub variables above