Skip to content

[17.0][ADD] project_git, project_github, project_gitlab connector - #1792

Open
FrancescoBallerini wants to merge 81 commits into
OCA:17.0from
FrancescoBallerini:17.0-add_project_git_connectors
Open

[17.0][ADD] project_git, project_github, project_gitlab connector#1792
FrancescoBallerini wants to merge 81 commits into
OCA:17.0from
FrancescoBallerini:17.0-add_project_git_connectors

Conversation

@FrancescoBallerini

@FrancescoBallerini FrancescoBallerini commented Aug 15, 2026

Copy link
Copy Markdown

Continuation of #1390

The original work by @alan196, whose commits are preserved as-is in the history, had already introduced several features

  • HTTP controller for GitHub/GitLab webhooks
  • creation of pull request records
  • tracking of the PR/MR status and CI tags
  • on a PR/MR event, a link to the matching Odoo task is posted if the task ID matches the taskid#<id> reference in the PR/MR title
  • deployment of the repository webhook from the Odoo project form
  • mapping of GitHub/GitLab users to Odoo users
  • retry of the latest odoo_sh_deploy CI job on GitLab

This PR introduces the following additional features

  • support for commit and branch records along with project task tracking. An inbound webhook handles the following:

    • registers commits, branches and pull/merge requests as Odoo records when matched to a project.task (explicit taskid#/tid# reference, or a Jira-style ABC-123 key found in the task name)

    • keeps the entities correlated with each other, so that each record can be reached from the ones it belongs to. For example, on a pull/merge request event, the request is linked to its source branch and to the commits it carries

  • the webhook_gitlab module has been revamped and split into a base platform-agnostic connector project_git along with the project_gitlab and project_github bridges. For reference, see fcee2223d, [REF] webhook_gitlab: split into project_git, project_github, project_gitlab

  • unit test suite covering the three modules

  • smaller improvements:

    • tasks related to a GitHub pull request are now correctly tagged as "PR: Merged" instead of "PR: Closed" when the PR is merged
    • tasks related to an approved GitLab merge request now keep the "Approved" tag when a later event arrives (e.g. a title edit), instead of losing it while the MR is still approved
    • tasks are no longer tagged as "CI: Pending" as soon as a pull request is created: the CI tag is now applied only when a pipeline event is actually tracked

Screenshots

Taken on a demo repository with the webhook actually deployed, so the records below were created by the inbound events themselves (not by hand)

📸 1. The Odoo project points to the repository, and deploys the webhook on it
01-annotated-odoo-project
📸 2. A pull request is opened on GitHub, and the connector posts back the link to the matching task
02-annotated-gh-pr-conversation
📸 3. The task shows its branch, commits and pull request
03-annotated-odoo-task
📸 4. The pull request record, with its source branch and its commits
04-annotated-odoo-pr-form
📸 5. The same commits and SHAs on the GitHub side
05-annotated-gh-pr-commits
📸 6. Tags follow the pull request state, with no manual step
06-annotated-odoo-kanban

Why project.git.* and not git.*

To avoid clashing with more generic implementations such as https://github.com/OCA/interface-git/tree/17.0/github_connector/. Writing a fully generic, project-agnostic connector (or extending the one in OCA/interface-git) would have meant a lot of extra work on a PR that is already large, so this is a deliberate compromise: a project-scoped connector that keeps its own namespace.

Additional Notes for developers and code reviewers

Along with the revamp and split of the modules, I've tried to establish a pattern that could support future bridges (e.g. Bitbucket) in two ways:

  • I've extended the event dispatching in the webhook controller: it now dispatches to a generic project.git.event model, whereas previously it dispatched to the pull request model. That model handles the creation of commit, PR and branch records, and the task matching. See (de76608, [REF] webhook_gitlab: refactoring of git.request into git.event)

  • I've added to project.git.event (and to the project_git codebase in general) a set of hooks resolved through getattr(), dispatching platform-specific logic by event source, so that any bridge can implement its own. See (c618bc822, [REF] webhook_gitlab: dispatch platform-specific logic by event source). On top of that, the controller itself now dispatches on event type + source, invoking _process_<event_type>_<source>: each bridge explicitly binds every event it handles with a per-source entrypoint, delegating the shared flows to the sourceless _process_*_event helpers of the base (769d464, [REF] project_git, project_github, project_gitlab: standardize event dispatch naming). project_github and project_gitlab are fully functional bridges built on this mechanism, so they also serve as reference implementations. See project_git/readme/DEVELOP.md for details

AI assistance

Following the OCA AI policy, the commits carry an Assisted-by: trailer naming the model that assisted them:

Assisted-by: Claude Fable 5
Assisted-by: Claude Opus 4.7

alan196 and others added 30 commits August 8, 2026 15:19
…instead of making validations for each method
… already linked with gitlab MR, remove Title from template
Allow to link more than one merge request to task or project.
Allow to see in Odoo status of merge request and  pipeline
- Replace the PR/branch/commit tabs on project.task with 3 smart buttons
  opening the filtered list views
- Add search views for git.branch, git.commit and git.pull.request and
  hook them to the menu actions
- Show related git entities as clickable embedded lists (readonly)
  instead of many2many tags on the git entity forms
- Rename view xml_ids and names to the standard patterns
  (view_git_branch_form / view_git_branch_search...; inherited views
  named <base view>.inherit.webhook.gitlab)

Assisted-by: Claude Fable 5
A GitLab MR opened with no commits yet (e.g. source branch identical to
the target) carries last_commit: null in the webhook payload: the event
processing crashed with a TypeError while preparing the pull request
values. Default the last commit SHA to an empty string instead.

Assisted-by: Claude Fable 5
Add 24 tests on top of the state-based suite, covering the code areas
the suite bypassed so far:

- controller authorization (test_controller.py, HttpCase with real
  requests): GitLab token and GitHub HMAC signature validation, missing
  or insecure authorization_token sysparam, queue job enqueueing,
  silent skip of events without a handler;
- MR/PR state transitions and task tags: merge, approval, WIP, close;
- GitLab pipeline: CI tag replacement on the task, non-success
  statuses, no update on head SHA mismatch;
- matching/config edge cases: invalid custom regex falling back to the
  default, broken taskid# reference outside PR titles, repository
  matching via git_dev_project_url and .git suffix variants;
- GitHub push symmetry (branch deletion, unmapped repository) and
  res.users mapping via gitlab_username/github_username.

Assisted-by: Claude Fable 5
The git.request -> git.event rename left create_project_webhook and
retry_odoo_sh_deploy_job pointing to the dropped model, so both buttons
on the project form crashed.

Besides the rename fix:
- subscribe the deployed hook to push events too (the module now
  tracks commits and branch creation/deletion from push events)
- tolerate a trailing .git in the configured project URL when deriving
  the GitLab API project path
- document the deployment button in the readme

Covered by tests/test_project.py (mocked GitLab API): hook payload,
replacement of an existing hook, deploy on both project URLs, and the
odoo_sh_deploy retry paths.

Assisted-by: Claude Fable 5
create_project_webhook now dispatches on the URL host: github.com
repositories get a web hook via the GitHub API (push and pull request
events, json payload, authorization token as HMAC secret - the same
signature the controller authorizes), any other host is treated as a
GitLab instance as before. Same replace-then-create semantics on both
platforms.

The Odoo.sh job retry stays GitLab-only and now skips github.com URLs
instead of querying them with the GitLab client.

Assisted-by: Claude Fable 5
Rename the _create_or_update_{commit,branch,pull_request} helpers to
_get_or_create_*: callers use them as getters of the tracked entity,
with the update_existing flag making the default refresh behavior
explicit.

Move out of git.event the helpers unrelated to event processing:
- git.utils (new AbstractModel): task matching configuration and
  explicit task id reference extraction;
- git.auth (new AbstractModel): authenticated GitLab/GitHub API
  clients, so that git.pull.request and project.project no longer
  reach into the event processor to get an API connection.

Assisted-by: Claude Fable 5
This refactoring paves the way for a potential split of the connector
into a platform-agnostic base module plus one bridge module per
platform: once every platform-specific behavior is reached through an
explicit dispatch seam, moving the implementations into their own
modules becomes an almost purely mechanical operation.

Platform-specific behavior is now reached through a single explicit
dispatch pattern (if hasattr: getattr, as in delivery_carrier) instead
of scattered if/else on the event source:

- git.event._dispatch_by_source routes calls to <method>_<source>
  implementations (warn and skip when missing) and serves the optional
  per-source hooks of the generic methods (commit vals, PR identifiers,
  PR opening detection).

- Platform implementations are uniformly named with a source suffix
  (_extract_branch_names_from_event_<source>, _fetch_pr_commits_<source>,
  _prepare_pull_request_vals_<source>, _post_message_<source>).

- The controller recognizes the event source from the platform-claimed
  headers (_detect_event_source) and delegates the token check to
  _verify_webhook_token_<source>; request parsing is a pure dispatch.

- Parsers normalize the event type onto the module-owned
  project_git_event_type key (replacing the GitLab-specific
  object_kind), and push events are refined into their concrete type
  (branch_creation, branch_deletion, commit_push) so that the key
  always names the git.event handler to invoke.

- project.project._get_url_platform replaces _is_github_url (github
  claims its host, any other host is treated as a GitLab instance).

- No more implicit "gitlab" defaults: the source is always explicit.

Assisted-by: Claude Fable 5
…ader

GitHub carries the event type in the X-GitHub-Event request header,
not in the payload: the previous parser guessed the type by probing
payload keys (pull_request / pusher), which misclassifies events that
embed a pull_request object without being pull request events (e.g.
pull_request_review, delivered by hooks subscribed to every event) and
crashes their processing jobs.

Request headers are now handed down to the source parsers
(_parse_git_request_data(event, headers)): the GitHub parser maps the
header - the platform's authoritative discriminator - onto
project_git_event_type, so event types without a handler (ping,
reviews, comments...) are skipped cleanly. The GitLab parser keeps
reading object_kind, its own authoritative discriminator, from the
payload and ignores the headers.

Assisted-by: Claude Fable 5
GitHub delivers tag pushes as regular push events (X-GitHub-Event:
push, ref refs/tags/...): a pushed tag whose name or commit messages
referenced a task was classified as a branch creation and tracked as a
git.branch record named "refs/tags/..." linked to the task. GitLab is
not affected: its tag pushes carry their own object_kind (tag_push),
which has no handler.

The GitHub parser now remaps push events whose ref is not a branch
onto the tag_push type, which no handler picks up - mirroring the
GitLab behavior. The test dispatch helper skips handlerless event
types the way the controller does.

Assisted-by: Claude Fable 5
When the API commit fetch failed, the GitHub fallback fabricated a
head commit dict whose message embedded the PR title ("HEAD commit
from PR: ..."): it added no matching information (the title is
already a matching source of its own) and it created git.commit
records with made-up name/description - systematically so on
deployments without a GitHub token, where every fetch fails.

The payload-based fallback is now an optional per-source hook
(mandatory=False): GitLab keeps it (the MR payload carries the real
head commit in last_commit), GitHub simply does not implement it, and
GitHub PR commit tracking relies on the API fetch alone.

Assisted-by: Claude Fable 5
Three changes to _find_matching_tasks:

- replace the pattern×project×task Python scan (which loaded every
  task of the mapped projects and ran a regex on each name) with an
  ORM search: project mapping + name ilike prefilter on the database
  side, then a Python refinement on the few candidates with the
  whole-word case-insensitive check that ilike cannot express. The
  ilike prefilter is a superset of the whole-word match, so no true
  match can be lost

- hardcode the issue key pattern (Jira-strict \b[A-Z][A-Z]+-\d+\b),
  dropping the task_name_match_regex system parameter and its
  install-time seeding: a freely configurable regex was a foot-gun
  (broken or surprising patterns might be reported as module
  malfunctions) and required defensive handling in the matching code.
  The pattern stays overridable by inheriting git.utils._get_task_name_match_regex

- match tasks whatever their state: the previous scan walked
  project.task_ids, which only carries open tasks, so keys referencing
  a closed task were silently ignored. Late commits on a task already
  marked as done are a common human pipeline slip, and still worth
  linking

Assisted-by: Claude Fable 5
Drop the readonly flags from the git.branch / git.commit /
git.pull.request form views: system administrators (who already have
full access rights on the git models) can now create entities and
adjust their task links by hand, e.g. to record a commit whose message
forgot the task reference. For regular users nothing changes: without
write access the forms stay readonly anyway.

Assisted-by: Claude Fable 5
…_gitlab

Split the connector into a platform-agnostic base module plus one bridge
module per platform (project_github, project_gitlab), each providing its
platform parser and token verification, the per-source hooks, the API
client with its python library dependency, the user mapping field and
the webhook deployment.

The models gain a project. prefix (git.event -> project.git.event,
git.branch -> project.git.branch, and so on): generic names like
git.commit would claim a global namespace they do not own and could
collide with other git integrations installed alongside (e.g. the OCA
github_connector suite), while the prefix scopes the models to the
project app this connector extends.

Every other artifact is renamed accordingly: python files and classes,
view and action xmlids, security rules, relation tables, the webhook
route (/project_git/webhook/) and the system parameters
(project_git.authorization_token, project_github.token,
project_gitlab.token.<instance root>). The base module readme documents
the contract for writing new platform bridges.

The split also hardens the platform API access. The token sysparams are
seeded by the bridges with a demo placeholder, and the banned-token
guard of the controller now covers the outbound direction too:
_get_token_param raises an explicit UserError on a missing or demo
token instead of calling the platform with bogus credentials. Task
matching runs its searches with a targeted sudo, since webhook jobs
execute as the public user, which has no task access of its own.

The es_419 translations shipped by the original module are realigned
with the new terms: each module carries the entries it owns, keeping
the existing translations where the source text survives.

Assisted-by: Claude Fable 5
GitHub reports the merge with the "merged" boolean of the payload,
never with state "merged": the state mapping entry was unreachable, so
a merged PR was tracked as closed and the "MR: Merged" tag was never
applied on GitHub tasks.

Assisted-by: Claude Fable 5
…queue_job

The module only uses with_delay(), which lives in queue_job:
queue_job_cron_jobrunner is a deployment choice (a cron-based runner
for hostings where the standard jobrunner cannot run, e.g. Odoo.sh)
and should not be forced on every installation.

project_git now depends on queue_job instead; the install notes
mention queue_job_cron_jobrunner as an option for such hostings.

Also add a roadmap fragment to project_gitlab, proposing to make the
hardcoded odoo_sh_deploy job name of the "Update Odoo.sh" button
configurable.

Assisted-by: Claude Fable 5
_search_existing_pull_request looked up pull requests by
(id_request, id_project) only: a GitLab pair (iid, project id)
can collide with a GitHub pair (number, repository id).

On collision the event of one platform would overwrite the pull
request of the other.

The lookup now also filters on the source platform of the event.

Each bridge gets a test dispatching an event over an existing pull
request with the same identifiers and no source, asserting that a
new record is created and the existing one is left untouched.

Assisted-by: Claude Fable 5
…name

The PR author matching searched res.users by github_username /
gitlab_username without limit: two users sharing the same username
would raise a singleton error and make the webhook job fail.

A Python constraint on res.users now rejects duplicate platform
usernames (checked globally, archived users included), and the event
search takes the first match as a safeguard for data predating the
constraint. No SQL constraint on purpose: res.users is an existing
model.

Each bridge gets a test asserting that a duplicate username is
rejected.

Assisted-by: Claude Fable 5
The approved flag was recomputed from the current webhook action, so
any event after the approval (e.g. a title edit) reset it to False and
removed the "Approved" tag from the linked tasks while the MR was
still approved on GitLab.

Write the flag only on the approval actions (approved/unapproved):
any other action leaves the last known approval state untouched.

Assisted-by: Claude Fable 5
The commit/branch/pull request deduplication relies on
search-then-create in the event flow: concurrent queue jobs processing
the same entity could slip through the search and create duplicates.

Add unique SQL constraints on the identifying keys: full_sha (commit),
url (branch) and (source, id_project, id_request) (pull request). On a
collision the losing job fails and simply finds the existing record
when requeued.

Assisted-by: Claude Fable 5
Move the pure model tests out of the event flow suites into per-model
test files, one per entity as for project and project.task: unique
constraints in the base (test_project_git_commit/branch/pull_request),
platform username uniqueness and PR identifier collision in the
bridges (test_res_users, test_project_git_pull_request, which on
gitlab also hosts the _post_message URL fallback test).

test_gitlab.py and test_github.py keep the git.event flow tests only.

Assisted-by: Claude Fable 5
The source branch URL was always built from project.web_url, which in
MR payloads is the target repository: correct when the MR comes from
the same repository, wrong for a MR opened from a fork (the source
branch lives in the fork). Prefer object_attributes.source.web_url,
falling back to project.web_url for push events, as the GitHub bridge
already does.

The builder is renamed _build_branch_url -> _build_source_branch_url
(base and bridges) to state the contract in the name.

Assisted-by: Claude Fable 5
ci_status defaulted to "pending", so every pull request immediately
tagged its tasks with "CI: Pending" — forever on GitHub, where no
pipeline event will ever arrive to update it.

Drop the default: the field stays empty until a real pipeline event
is tracked, and the CI tag becomes the last known pipeline state.
GitHub CI events (check_suite) are noted in the bridge roadmap.

Assisted-by: Claude Fable 5
The task tags master-data and logic lived in the base module, modeled
on what the GitLab bridge populates: any new connector would need its
own vocabulary anyway.

The whole tagging process is now per-source: each bridge ships its own
tags master-data, manages its own tag namespace and handles the fields
it actually populates (full set on GitLab, PR states on GitHub, in
platform wording).

Base module project_git only dispatches to _assign_tags_to_task_<source>
and provides the generic tag replacement helper, skipping records without
a source.

Assisted-by: Claude Fable 5
- Add a step-by-step configuration overview

- Document the queue_job jobrunner requirement: queue_job must be added
  to the server_wide_modules; on hostings where the jobrunner cannot
  run (e.g. Odoo.sh), queue_job_cron_jobrunner is advised instead

- Document the caveats of explicit id references

Assisted-by: Claude Fable 5
@FrancescoBallerini
FrancescoBallerini force-pushed the 17.0-add_project_git_connectors branch from 07207b3 to 000b515 Compare August 15, 2026 05:38
@FrancescoBallerini
FrancescoBallerini marked this pull request as ready for review August 15, 2026 05:57
In the tree view, keep the request id next to the title (author and
source branch move right before the related tasks) and make the URL
clickable (url widget), as in the branch and commit views. In the form
view, show the plain source branch name only when the PR/MR has no
tracked branch record (the record link already carries it).

Assisted-by: Claude Fable 5
…dispatch naming

Before this commit the http route `/project_git/webhook/` dispatched events by
type only, so the names of the platform-specific event handlers did not carry
the platform. That did not cause many issues directly, but it was a bit
misleading and could cause annoying namespace conflicts (for example when
implementing a _process_commit_push handler for another platform with
different logic from the existing one).

This is why, after this commit, the route dispatches on event type + source,
invoking _process_<event_type>_<source>: a more standardized API where every
bridge explicitly binds each event it handles.

For similar reasons the per-source parsers in the controller become
_parse_git_request_data_<source>, so the platform-specific hook names stay
aligned with the generic method that dispatches them.

Along with this alignment, as an additional small cleanup, every dynamically
built method name is composed with f-strings.

The development readme documents the bridge extension points with code
examples.

Assisted-by: Claude Fable 5
@FrancescoBallerini
FrancescoBallerini force-pushed the 17.0-add_project_git_connectors branch from 989265a to 769d464 Compare August 17, 2026 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod:project_git Module project_git mod:project_github Module project_github mod:project_gitlab Module project_gitlab series:17.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants