Multica Docs

GitHub integration

Connect a GitHub App once, then PRs whose branch, title, or body reference an issue identifier auto-attach to that issue — and merging the PR moves the issue to Done.

Connect a GitHub account or organization once in Settings → GitHub. After that, any pull request whose branch name, title, or body contains an issue identifier (for example MUL-123) is auto-linked to that issue, appears under Pull requests in the issue sidebar, and — when the PR is merged — moves the issue to Done.

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

What the integration does

SurfaceBehavior
Settings → GitHubWorkspace admins see the GitHub tab with a master toggle, Connect GitHub button, and feature switches (PR sidebar, Co-authored-by, auto-link). After install you bounce back to the GitHub tab.
Issue sidebar → Pull requestsEvery PR auto-linked to this issue, with title, repo, state (Open / Draft / Merged / Closed), and author. Click a row to jump to the PR on GitHub.
Webhook (background)On every pull_request event, Multica upserts the PR row, scans the PR for issue identifiers, and (re)builds the link rows. Idempotent — replaying a delivery is a no-op.
Auto-status on mergeWhen a PR transitions to merged, every linked issue not already Done or Cancelled is moved to Done. The status change is timeline-logged with source github_pr_merged.
PR card → CI & mergeabilityFor each linked PR, Multica fetches an authenticated GitHub API snapshot and mirrors its CI status and mergeability onto the card (see What the PR card shows below).

Alongside the PR's own state, the integration mirrors — for each linked PR — its CI status (all checks passed, N failed with the failing check names, or N running) and its mergeability (ready to merge, has conflicts, blocked, or behind), fetched from an authenticated GitHub API snapshot. Commits and branch refs without an open PR are still not modeled.

What the PR card shows

Each linked PR's card carries two independent elements, each built from an authenticated GitHub API snapshot:

  • CI status — one of: all checks passed (with the count, e.g. 7/7), N failed (naming the failing checks), or N running (still in progress). A PR with no checks configured yet shows neither — "no checks" is never reported as passed.
  • Mergeability — one of: ready to merge (shown only when GitHub reports the merge state is clean, i.e. no conflicts and branch protection satisfied), has conflicts, blocked, or behind.

The two never mask each other — a PR can show failed checks and a merge conflict at the same time. Merged and closed PRs show neither; the row's own state icon already says so.

The snapshot is fetched from the GitHub API and refreshed on webhook events (pull_request, check_run, check_suite, status), when you open the card, and periodically while the PR is still open and undecided. During a temporary GitHub outage the card keeps showing the last-known snapshot marked stale rather than going blank.

Multiple workspaces

One GitHub App installation can be connected in several workspaces at once — for example when different teams in the same organization run separate workspaces. When it is, every connected workspace receives each repository's pull_request events independently:

  • Each workspace mirrors the PR and auto-links it against its own issue prefix and GitHub feature switches. A PR that references MUL-1 and ENG-2 links MUL-1 in the workspace whose prefix is MUL and ENG-2 in the one whose prefix is ENG — neither sees the other's issues.
  • Disconnecting the installation from one workspace only stops that workspace from receiving events; the others keep working.

Repository scope is whatever you granted the GitHub App (all repositories, or a chosen subset). Connecting the installation in a workspace is itself the subscription — there is no separate per-repository selection inside Multica.

Upgrading a self-host deployment: event delivery is keyed on the GitHub connection, not on a workspace's code-repository list. If a workspace previously received a repo's PRs without connecting GitHub in that workspace (by listing the repo in its code-repositories entry alone), it will stop receiving events after this upgrade. To restore delivery, connect the same GitHub installation in that workspace under Settings → GitHub.

How identifiers are matched

The webhook extracts identifiers from three fields, in this order: PR head branch, PR title, PR body. 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.

You can reference multiple issues in one PR. Closes MUL-1, MUL-2 links the PR to both, and merging it advances both to Done.

The auto-merge-to-Done rule

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

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. The linked issues stay where they were — the user is the one who decides what closing-without-merge means.

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

In Settings → GitHub there is no installation list — you manage existing installations from GitHub directly:

  • 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.
  • Disconnect from inside Multica is admin-only — the Disconnect control on the GitHub tab is hidden for non-admins. It stays available even when the master GitHub switch is off, so 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 GitHub App requests read-only access to pull requests, checks, commit statuses, and metadata. Multica never pushes commits, comments, or status checks back to GitHub — the CI status and mergeability shown on the PR card are fetched read-only, never written.

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 its 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 → ChecksRead-only — powers the PR card's CI status.
Permissions → Repository → Commit statusesRead-only — the legacy status-context half of the CI rollup.
Permissions → Repository → MetadataRead-only (mandatory).
Subscribe to eventsTick Pull request, Check suite, Check run, and Status.
Where can this GitHub App be installed?Your choice. Only on this account is fine for single-org setups.

Grant read, never write, and nothing more. Do not grant Checks: Read and write — write access makes GitHub expect this App to report its own check runs, which leaves them stuck permanently pending. Read-only is correct. The feature also does not need Actions: read or Contents: read — don't add them.

Adding these permissions to an App that's already installed needs approval. When you extend an existing App with Checks, Commit statuses, Check run, and Status, GitHub holds the change as a pending permission request — it does not take effect until each installation's owner approves it (on GitHub, under that installation's settings). Until an installation approves, the PR card stays without CI or merge status on its repos.

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>

# Required for the PR card's CI status and mergeability. Without them the
# integration still mirrors PRs and auto-links / auto-closes issues, but the
# PR card shows no CI or merge status:
GITHUB_APP_ID=<numeric App ID from the App's settings page>
GITHUB_APP_PRIVATE_KEY=<full PEM block, including BEGIN/END lines>

GITHUB_APP_SLUG and GITHUB_WEBHOOK_SECRET are required for the integration to start at all. 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.

GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY are now required for the PR card's CI status and mergeability — they let Multica authenticate as the App (App JWT → installation token) and pull the GitHub API snapshot each card is built from. Generate the key under Private keys → Generate a private key on the App's settings page; paste the full PEM block (including the -----BEGIN/END RSA PRIVATE KEY----- lines) into the env var, preserving newlines. Without them the integration degrades cleanly: PRs still mirror, issues still auto-link and move to Done on merge, and the connected account name still resolves from the installation.created webhook — but the PR card shows no CI or merge status.

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.

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.

Confirm the App's events and permissions. The public App-metadata endpoint reports exactly what you granted — no auth needed:

curl -s https://api.github.com/apps/<slug>
  • The events array should include pull_request, check_suite, check_run, and status.
  • The permissions object should include checks: read and statuses: read, alongside pull_requests: read and metadata: read.

If any are missing, fix the App's Subscribe to events / Permissions — and remember that adding permissions to an already-installed App only takes effect once each installation's owner approves the pending request.

"PR card shows no CI or merge status." The card mirrors the PR fine but never shows checks or mergeability:

  • Confirm GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY are set in the running API process — they are what authenticate the snapshot fetch.
  • Confirm each installation has approved the pending permission request for Checks and Commit statuses (see the probe above).

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 review threads or reviewer list — CI status and mergeability are now surfaced on the PR card (see What the PR card shows), but review comments, individual reviewers, and their approval state are not.
  • No workspace-level config for the merge → Done rule — it's a fixed default (merged → done, unless cancelled). Workspace-customizable mappings are a future addition.
  • Multi-PR-to-one-issue is conservative on merge — if two PRs both reference MUL-123 and the first one merges, the issue is moved to Done immediately. A follow-up change to wait for all linked PRs to resolve before advancing is in progress.

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