--- title: GitHub-Odoo Sync url: https://blog.guigpap.com/en/workflows/github-odoo-sync/ url_md: https://blog.guigpap.com/en/workflows/github-odoo-sync.md category: automation date: '2026-01-20' maturite: production techno: - github - odoo - n8n application: - automation - business --- # GitHub-Odoo Sync > Bidirectional issue/commit sync between GitHub and Odoo via an N8N hub and 7 specialised sub-workflows ## 1. What? — Definition and context The **GitHub-Odoo Sync** workflow turns every GitHub event (issue, PR, commit) into a matching Odoo operation: project task created, label applied, milestone synced, commit historised. Since the #274 refactoring, the architecture moved from a 138-node monolithic workflow to a **13-node hub that dispatches to 7 specialised sub-workflows**. > **Note - Dev ↔ Business bridge** > > GitHub is the natural tool for developers (issues, PRs, commits). Odoo is the natural tool for business management (projects, timesheets, CRM). This workflow bridges them without forcing one side to learn the other side's tool. ### Hub-spoke architecture | Workflow | ID | Nodes | Role | |----------|------|-------|------| | **GitHub Project Sync** (parent) | `JR8ESduxKNSQ7iVs` | 13 | Trigger + mapping lookup + dispatch by event type | | **SW-1 Issue Lifecycle** | `8yU07chLmZuogA3j` | 23 | opened / closed / reopened + sub-issues parent | | **SW-2 Issue Labels** | `OMeuYvh0B9XA2S1H` | 11 | labeled / unlabeled | | **SW-3 Issue Properties** | `0BoSdwR4d8S9Fq0C` | 32 | assigned / edited / pinned / renamed / deleted | | **SW-4 Issue Milestone** | `EZiZqgZNpxEzof7F` | 13 | milestoned / demilestoned | | **SW-5 PR Handler** | `3o7IzU2brzJeedB7` | 18 | pull_request opened / merged / closed | | **SW-6 Git Events** | `O2jirkOJOpOIy5ez` | 18 | push (commits) + create (branch stage transition) | | **SW-7 Repo Entity** | `WVWY01qmw9uYzSl8` | 28 | Label + Milestone CRUD at the repo level | ### Shared dependency A utility sub-workflow `odoo-get-or-create-tag` (`j7e2EzEtIJ42T2S4`) is called by SW-1, SW-2, and SW-7 to create or fetch Odoo tags matching GitHub labels. ### Architecture diagram ```mermaid flowchart TD GH["GitHub · webhooks"] Mapping["Data Table · github_project_mapping"] subgraph Hub["GitHub Project Sync · 13 nodes"] direction TB Trigger["Github Trigger"] Lookup["Lookup Mapping"] SyncEnabled["Sync Enabled?"] Switch["Switch · Event Type"] end subgraph SubsIssues["Issues sub-workflows"] direction TB SW1["SW-1 Lifecycle · 23n"] SW2["SW-2 Labels · 11n"] SW3["SW-3 Properties · 32n"] SW4["SW-4 Milestone · 13n"] end subgraph SubsOther["Other sub-workflows"] direction TB SW5["SW-5 PR Handler · 18n"] SW6["SW-6 Git Events · 18n"] SW7["SW-7 Repo Entity · 28n"] end TagSW["odoo-get-or-create-tag"] Odoo["Odoo 18 · guig_db"] GH --> Trigger Mapping --> Lookup Trigger --> Lookup --> SyncEnabled --> Switch Switch --> SW1 Switch --> SW2 Switch --> SW3 Switch --> SW4 Switch --> SW5 Switch --> SW6 Switch --> SW7 SW1 --> TagSW SW2 --> TagSW SW7 --> TagSW SW1 --> Odoo SW2 --> Odoo SW3 --> Odoo SW4 --> Odoo SW5 --> Odoo SW6 --> Odoo SW7 --> Odoo TagSW --> Odoo ``` ### Supported events Every GitHub event useful to a project workflow is synchronised: | Category | Events | Sub-workflow | |----------|--------|--------------| | Issue lifecycle | opened, closed, reopened | SW-1 | | Sub-issues | parent_added, parent_removed | SW-1 | | Issue labels | labeled, unlabeled | SW-2 | | Issue properties | assigned, edited, pinned, renamed, deleted | SW-3 | | Issue milestone | milestoned, demilestoned | SW-4 | | Pull requests | opened, closed, merged | SW-5 | | Push & branches | push (commits), create (branch) | SW-6 | | Repo-level | label/milestone CRUD | SW-7 | --- ## 2. Why? — Stakes and motivations ### The problem without sync | Without sync | Consequence | |--------------|-------------| | **Business visibility** | Issues stay invisible from Odoo | | **Time tracking** | Impossible to tie a timesheet hour to a specific issue | | **Commit history** | Scattered across GitHub, no aggregated view per Odoo task | | **Double entry** | Manual Odoo task creation for every issue | ### Why hub-spoke rather than a single workflow? The original workflow (138 nodes) had grown along with each new event handled. Three symptoms triggered the #274 refactoring: | Symptom | Cause | Consequence | |---------|-------|-------------| | **Slow editing** | N8N UI struggling with 138 nodes | Ergonomically expensive edits | | **Tedious tests** | Touching one sub-flow → re-test the whole | Silent regressions | | **Coupling** | AI tag, Odoo task, milestone code shared | Local change broke distant branches | The hub architecture with 7 sub-workflows resolves those tensions: each SW has a narrow scope, can be edited and tested in isolation, and signs its I/O contract via Execute Workflow Trigger. ### Why GitHub → Odoo and not the other way? | Criterion | Choice | |-----------|--------| | **Source of truth for code** | GitHub (where commits, PRs, CI live) | | **Source of truth for business** | Odoo (timesheets, invoices, customer projects) | | **Trigger direction** | GitHub webhooks (real-time, sub-second) | | **Enrichment** | Odoo adds business context (project, tags, complexity, estimate) | The reverse direction (closing an issue from Odoo) is technically feasible but introduces a loop risk. For now, GitHub stays the source; Odoo is sink + dashboard. --- ## 3. How? — Technical implementation ### An event's journey **1. GitHub webhook** — The N8N trigger receives the event with its `X-Hub-Signature-256` HMAC signature (verified by N8N). **2. Mapping lookup** — The hub queries the `github_project_mapping` Data Table to find the Odoo project tied to the repo (`owner/repo` → `odoo_project_id`). **3. Switch event type** — The hub routes to the appropriate sub-workflow based on `headers.x-github-event` and `body.action`. **4. Sub-workflow execution** — Each SW receives `{body, headers, query, mapping}` in passthrough and applies its business logic. **5. Odoo XML-RPC calls** — All SWs converge on the same Odoo endpoint: `http://odoo:8069/xmlrpc/2/object`, database `guig_db`. > **Caution - DB name = guig_db** > > The Odoo database is named **`guig_db`**, not `odoo`. Every XML-RPC request uses that name — any confusion produces a silent `Database does not exist` error. ### Odoo `project_github_sync` module The custom addon `project_github_sync` (v18.0.5.6.0) adds the fields needed by the sync. **Every custom field uses the `x_` prefix** to avoid collision with Odoo natives: #### Fields on `project.task` | Family | Field | Type | Source | |--------|-------|------|--------| | GitHub | `x_github_issue_id` | Integer (indexed) | issue/PR number | | GitHub | `x_github_url` | Char(500) | GitHub URL | | GitHub | `x_github_repo` | Char (indexed) | `owner/repo` | | GitHub | `x_github_milestone_id` | Integer | milestone number | | GitHub | `x_github_parent_issue_id` | Integer | parent (sub-issues) | | GitHub | `x_github_commit_ids` | One2many | linked commits | | Effort (#188) | `x_estimated_hours` | Float(10,2) | estimate at creation | | Effort (#188) | `x_complexity` | Selection | trivial / simple / moderate / complex | | AI triage (#296) | `x_ai_project_triaged_at` | Datetime | last project analysis (TTL 7d) | | AI triage (#296) | `x_ai_personal_triaged_at` | Datetime | last personal analysis (TTL 7d) | | Telemetry | `x_claude_time_total` | Float(10,2) | active Claude Code time | | Telemetry | `x_claude_cost_total` | Float(10,4) | API cost USD | | Telemetry | `x_claude_token_total` | Integer | cumulative tokens | | Telemetry | `x_claude_sessions` | Integer | session count | | Telemetry | `x_claude_lines_added/removed` | Integer | LOC delta | | Telemetry | `x_claude_session_ids` | One2many | session history | | Telemetry | `x_claude_category` | Selection | auto category for generic tasks | #### Added models | Model | Role | |-------|------| | `project.task.github.commit` | One record per commit linked to a task (sha, message, author, date) | | `project.task.claude.session` | One record per Claude Code session (id, duration, cost, tldr, category) | ### Issue Lifecycle (SW-1) detailed The busiest sub-workflow is `SW-1 Issue Lifecycle`, which handles 5 distinct actions: ```mermaid flowchart TD Trigger["Execute Workflow Trigger · passthrough"] Switch["Switch · body.action"] subgraph Opened["opened"] direction TB Prep["Prepare Odoo Data + complexity + estimate"] MD["Convert MD to HTML"] Area["Detect Area Tag · area:xxx, feat(xxx)"] Tag["Get/Create Tag"] Create["Create Task · stage Backlog"] end subgraph Closed["closed"] direction TB PrepC["Prepare Closed Data"] Find1["Find Task by x_github_issue_id"] IsProj["Is Project Task?"] CloseProj["Close Project Task · stage 25 Done"] ClosePriv["Close Private Task · personal_stage 6 Done"] end subgraph Reopened["reopened"] direction TB PrepR["Prepare Reopened Data"] Find2["Find Task"] ReopenProj["Reopen → default_stage_id"] ReopenPriv["Reopen Private → personal_stage 2 Today"] end subgraph SubLink["parent_added · parent_removed"] direction TB PrepL["Prepare Link/Unlink"] FindBoth["Find Sub + Parent Tasks"] Link["Set parent_id + x_github_parent_issue_id"] Unlink["Clear parent_id"] end Trigger --> Switch Switch --> Prep --> MD --> Area --> Tag --> Create Switch --> PrepC --> Find1 --> IsProj IsProj --> CloseProj IsProj --> ClosePriv Switch --> PrepR --> Find2 Find2 --> ReopenProj Find2 --> ReopenPriv Switch --> PrepL --> FindBoth FindBoth --> Link FindBoth --> Unlink ``` ### Effort estimation at creation (#188) When a new issue is created, SW-1 derives two fields automatically from the title + body: | Field | Calculation | |-------|-------------| | `x_complexity` | Heuristic on body length, keywords (refactor, migration, multi-step) | | `x_estimated_hours` | Deterministic model based on the complexity cohort (see [Effort Estimator V1](/en/workflows/monitoring-digests/)) | The initial estimate is a lower bound used as a reference. The `effective_hours` field (aggregated timesheets) then measures the gap between estimate and reality, feeding a weekly coverage report. ### Daily GitHub Triage + AI double-triage In parallel with event-driven sync, a daily scheduled workflow (`61uBaaFQssxT1HJ0`, 25 nodes) performs catch-up and AI triage: | Step | Action | |------|--------| | 1. Catch-up sync | Fetches GitHub issues modified in the last 24h, updates Odoo | | 2. Stage promotion | If an issue is `in progress` on GitHub but `Backlog` on Odoo → promoted to `In Progress` | | 3. AI Issue Triage (#296) | Sub-workflow `AwqOyyAC3sUg9tSm` (12n) — codex-yolo analyses the issue and proposes project stage + personal horizon | | 4. TTL 7d cache | The `x_ai_*_triaged_at` fields prevent re-triaging an issue analysed recently | The AI triage is also exposed in interactive mode via the `triage-interactive-api` consumed by Claude Code during an open session (`/triage` command). ### Passed data format The hub passes a uniform object to every sub-workflow: ```json { "body": { /* GitHub webhook payload */ }, "headers": { "x-github-event": "issues", "x-github-delivery": "..." }, "query": {}, "mapping": { "github_repo": "owner/repo", "odoo_project_id": 5, "default_stage_id": 21, "sync_enabled": true } } ``` The input contract is strict (Execute Workflow Trigger in `passthrough` mode disabled for SW-3 to SW-7, which use typed `workflowInputs`). ### Initial Sync (#50) To onboard an existing repo with hundreds of issues, the `Initial Sync` workflow (#50) allows a one-shot import: GitHub API pagination + sequential Odoo task creation + tag setup. Use it only at setup time; afterwards the event-driven sync is sufficient. --- ## 4. What if? — Outlook and limits ### Current limits | Limit | Impact | Mitigation | |-------|--------|------------| | **Unidirectional** | Closing on Odoo does not close the GitHub issue | Reverse sync to design with anti-loop flag | | **Manual mapping** | Each repo must be added to the Data Table | Auto-discovery per organisation later | | **Initial estimate = lower bound** | Complex issues underestimated | Weekly recalibration planned via cohort matching | | **AI triage 24h max lag** | Re-triage the next day if issue ignored | Acceptable, triage is an assistant not an oracle | ### Evolution scenarios **If bidirectional sync is needed**: - `automation.action` Odoo webhook when a task moves to stage `Done` - The webhook posts a GitHub comment `Closed via Odoo task #N` - `synced_from_odoo` flag in the mapping table to block back-propagation **If many repos to manage**: - Auto-discovery of repos from a GitHub organisation - Mapping templates by name convention (`*-backend` → Backend project) - Dedicated Odoo UI to configure mappings without touching the Data Table **If AI triage becomes critical**: - Enable a high-confidence mode (auto-set the stage if `confidence > 0.9`) - Log decisions in `claude_code_active_sessions` for audit - Allow a manual override via `/triage ` --- ## Related pages ### Infrastructure - [Odoo 18 on Docker](/en/infrastructure/odoo-18-setup/) — `project_github_sync` module, x_* fields - [Why Odoo](/en/infrastructure/why-odoo/) — Choices and architecture - [N8N Queue Mode](/en/infrastructure/n8n-queue-mode/) — Backend running the sub-workflows ### Workflows - [Claude Code Telemetry](/en/workflows/claude-code-telemetry/) — Feeds the x_claude_* fields - [Monitoring Digests](/en/workflows/monitoring-digests/) — Effort Estimator V1, Daily Todo Digest, Daily GitHub Triage - [Error Handler](/en/workflows/error-handler/) — Captures the 7 sub-workflow errors ### Reference - [Glossary](/en/reference/glossary/) — XML-RPC, Webhook, HMAC, Sub-workflow ## Metadonnees agent - Cet article est issu du blog GuiGPaP Lab. - Contexte global du blog: https://blog.guigpap.com/llms.txt - Contact auteur: https://odoo.guigpap.com/mon-cv - Licence: CC-BY-SA 4.0