# GitHub Integration & Automations

Link repositories and issues, and register operation triggers and actions


The Operation plugin's GitHub App integration (issue ↔ task sync) and its automation triggers and actions.

Backend code lives under `backend/plugins/operation_api/src/modules/githubIntegration/`, `src/modules/automations/`, and `src/utils/` ([`githubClient.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/utils/githubClient.ts), [`githubWebhookHandler.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/utils/githubWebhookHandler.ts)). The UI lives at `settings/operation/github` in `operation_ui`.

## Data models

- **`operation_github_configs`**: links one team to one repo: `{ teamId, repoName, installationId, syncMode: 'oneWay' | 'twoWay', subdomain }`. Unique indexes on `{ teamId, subdomain }` and `{ repoName, subdomain }`.
- **`operation_github_connections`**: an installed GitHub App org: `{ installationId, orgName, orgAvatarUrl, orgType, initiatedUserId, isActive, subdomain }`. Unique on `{ installationId, subdomain }`.
- **`operation_github_milestone_mappings`**: maps erxes milestones to GitHub milestones per repo: `{ subdomain, installationId, repoName, erxesMilestoneId, githubMilestoneId, githubMilestoneNumber, githubMilestoneTitle }`.

Tasks and triage items carry `githubIssueNumber`, `githubIssueUrl`, `githubRepoName` once linked. Triage enforces a unique sparse index on `{ githubRepoName, githubIssueNumber }`.

## Setup and authentication

The integration is a **GitHub App** authenticated with `GITHUB_APP_ID` + `GITHUB_PRIVATE_KEY` (installation tokens via `getInstallationOctokit`); webhooks are verified with `GITHUB_WEBHOOK_SECRET` (HMAC-SHA256 over the raw body, `x-hub-signature-256`).

- `GET /pl:operation/integrations/github/setup`: returns a small HTML page that `postMessage({ type: 'github-install-complete' })` to `window.opener` and closes the popup. Point the GitHub App's post-install redirect here.
- `POST /pl:operation/integrations/github/webhook`: GitHub webhook receiver. Returns `400` without a resolvable subdomain, `401` on missing/invalid signature, `500` when `GITHUB_WEBHOOK_SECRET` is unset, `pong` on `ping`.

Webhook events handled:

| `x-github-event` | Behavior |
| --- | --- |
| `installation` | `created`/`unsuspend` upserts an active `GithubConnection`; `deleted`/`suspend` deactivates it |
| `issues` `opened` | Creates a **triage** item on the configured team and appends `<!-- erxes-task-id: ... -->` to the issue body |
| `issues` `opened`/`reopened`/`closed` | For an already-linked task, moves it to the team's first status of type UNSTARTED / COMPLETED / CANCELLED (`closed` + `not_planned` or `duplicate` → cancelled) |
| `issues` `milestoned`/`demilestoned` | Sets/clears the task's `milestoneId` via `GithubMilestoneMapping`; only when the config's `teamId` matches the task's team |
| anything else | `ignored` |

Bot senders (`sender.type === 'Bot'`) are skipped to prevent sync loops.

## Outbound (two-way) sync

When a team's config has `syncMode: 'twoWay'`:

- `createTask` opens a GitHub issue (`POST /repos/{owner}/{repo}/issues`) with a body linking back to `https://<subdomain>.erxes.io/operation/team/<teamId>/tasks/<id>` plus the `erxes-task-id` marker, then writes the issue number/URL onto the task and pushes the milestone when `milestoneId` is set.
- `updateTask` pushes status (`updateGithubIssueState` maps COMPLETED → `closed`/`completed`, CANCELLED → `closed`/`not_planned`, anything else → `open`) and milestone changes (`updateGithubIssueMilestone`, which looks up or creates the GitHub milestone by title via `resolveGithubMilestoneNumber`).

GitHub failures are logged and swallowed; a task mutation never fails because the issue sync did. In `oneWay` mode only inbound (GitHub → erxes) events apply. The sync lives in the [task mutations](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/task/graphql/resolvers/mutations/task.ts).

## Queries and mutations

| Operation | Kind | Permission |
| --- | --- | --- |
| `getGithubConfigByTeam(teamId)` / `getAllGithubConfigs(installationId)` | query | `teamRead` |
| `upsertGithubConfig(teamId, repoName, installationId, syncMode)` | mutation | `teamUpdate` |
| `operationGithubDisconnectTeam(teamId)` | mutation | `teamUpdate` |
| `getGithubConnection` / `getGithubConnections` / `getGithubRepositories(installationId)` | query | `teamRead` |
| `disconnectGithubConnection(installationId)` | mutation | `teamUpdate` |

`upsertGithubConfig` validates the team exists, the installation has an active connection, the repo isn't already linked to another team, `repoName` is `owner/repo` form, and the repo is reachable through that installation (`GET /repos/{owner}/{repo}`). Re-linking a team to a different installation/repo deletes its milestone mappings; `operationGithubDisconnectTeam` does the same; `disconnectGithubConnection` refuses while any team still references the installation.

## Automations

`meta/automations.ts` connects `modules/automations/` (handlers in `automationHandlers.ts`, actions in `actions/`) to the platform automation service.

**Triggers** (`operationAutomationConstants.triggers`):

| Trigger | Target type | Notes |
| --- | --- | --- |
| Task | `operation:task.tasks` | segment enrollment |
| Project | `operation:project.projects` | segment enrollment |
| Project completed | `operation:project.projects` (`relationType: 'completed'`) | custom; fires when every task in the project is complete |
| Milestone reached | `operation:project.milestones` (`relationType: 'reached'`) | custom; milestone tasks hit the configured completion mode |
| Team work completed | `operation:team.teams` (`relationType: 'completed'`) | custom; team tasks hit the completion mode |

The three custom triggers support completion modes `every`/`some`/`first`/`last` (`checkCustomTrigger` in [`automationHandlers.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/automations/automationHandlers.ts) evaluates task status types under the selector).

**Actions** (`actions/`):

| Action | Module | Notes |
| --- | --- | --- |
| Create task | `task` | `createTaskAction`; target source `operation:task.tasks` |
| Create project | `project` | `createProjectAction`; target source `operation:project.projects` |

`setProperties` supports writing fields on `tasks`, `projects`, `milestones`, and `teams` via `setProperty` rules and `setPropertyTargets`. Automation task outputs include a `DOMAIN`-based link (`/operation/tasks/<id>`).

## Environment variables

| Variable | Purpose |
| --- | --- |
| `GITHUB_APP_ID` | GitHub App id for installation tokens |
| `GITHUB_PRIVATE_KEY` | App private key (PEM) |
| `GITHUB_WEBHOOK_SECRET` | Webhook signature secret; webhook 500s without it |
| `DOMAIN` | Task links embedded in GitHub issue bodies and automation outputs |
| `VERSION` | `saas` changes org-scoped behavior (cycle worker, org flags) |

> [!WARNING]
> **Sync not working?**
>
> If opened issues do not create triage items, no `GithubConfig` exists for that `installationId` + `repoName` + `subdomain`. If status changes do not reach GitHub, outbound sync only applies to `twoWay` configs where `githubRepoName` still matches; outbound errors are logged, not thrown.

