GitHub Integration & Automations
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, 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 thatpostMessage({ type: 'github-install-complete' })towindow.openerand closes the popup. Point the GitHub App's post-install redirect here.POST /pl:operation/integrations/github/webhook: GitHub webhook receiver. Returns400without a resolvable subdomain,401on missing/invalid signature,500whenGITHUB_WEBHOOK_SECRETis unset,pongonping.
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':
createTaskopens a GitHub issue (POST /repos/{owner}/{repo}/issues) with a body linking back tohttps://<subdomain>.erxes.io/operation/team/<teamId>/tasks/<id>plus theerxes-task-idmarker, then writes the issue number/URL onto the task and pushes the milestone whenmilestoneIdis set.updateTaskpushes status (updateGithubIssueStatemaps COMPLETED →closed/completed, CANCELLED →closed/not_planned, anything else →open) and milestone changes (updateGithubIssueMilestone, which looks up or creates the GitHub milestone by title viaresolveGithubMilestoneNumber).
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.
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 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) |
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.