Skip to content

Fix GitLab::getEvent() field/action parity with Gitea's shape#120

Merged
HarshMN2345 merged 16 commits into
mainfrom
fix/gitlab-event-parity
Jul 17, 2026
Merged

Fix GitLab::getEvent() field/action parity with Gitea's shape#120
HarshMN2345 merged 16 commits into
mainfrom
fix/gitlab-event-parity

Conversation

@HarshMN2345

Copy link
Copy Markdown
Member

Summary

GitLab's getEvent() (added in #118, feat/appwrite-provider-parity) returns push/merge_request event data in a shape that doesn't match Gitea's or GitHub's -- consumers relying on the common field contract (e.g. Appwrite's VCS integration) would silently drop every event.

Verified against real GitLab webhook payload structure:

  • Missing repositoryId entirely -- any DB lookup keyed on it finds nothing.
  • Non-standard key names: name/commitAuthor/commitMessage/commitUrl instead of repositoryName/headCommitAuthorName/headCommitMessage/headCommitUrl (matching Gitea's/GitHub's convention).
  • Merge request action passed through raw GitLab values (open/reopen/update/close) instead of the GitHub/Gitea convention (opened/reopened/synchronize/closed) consumers already branch on. Also maps GitLab's distinct merge action to closed, since a merged MR is done either way.
  • Also adds repositoryUrl, branchUrl, affectedFiles, branch create/delete detection, and cross-project (fork) detection for external MRs -- all present in Gitea's return shape but missing from GitLab's.

While integrating against a real GitLab OAuth app end-to-end (not just mocked HTTP), three further real bugs surfaced -- all fixed here too:

  • Wrong auth header for OAuth2 tokens. The adapter authenticated every request with PRIVATE-TOKEN: <token> (and ?private_token= for presigned URLs), which is only valid for GitLab Personal Access Tokens. Our integration uses OAuth2 access tokens, which GitLab requires as Authorization: Bearer <token>. Every API call was failing with 401 "Bad credentials" regardless of token freshness.
  • searchRepositories() dropped private repos in personal namespaces. The personal-namespace fallback called GET /users/{username}/projects, which GitLab only returns public projects for, even for the authenticated user's own account. Switched to GET /projects?membership=true (every project -- public and private -- the token's user is a member of) with client-side filtering by namespace.path.
  • searchRepositories() return shape didn't match GitHub/Gitea's items/total contract. It returned a bare indexed array instead of ['items' => [...], 'total' => N], breaking any consumer destructuring the common shape. total is now taken from the X-Total response header (or the filtered count, when the personal-namespace client-side filter is in effect, since the header reflects all namespaces in that case).

Test plan

  • Updated testGetEventPush/testGetEventPullRequest to assert the corrected shape
  • New testGetEventPullRequestActionMapping covers all 5 native action values
  • New testGetEventPullRequestDetectsExternal covers fork/cross-project MR detection
  • New testGetEventPushDetectsBranchCreated/testGetEventPushDetectsBranchDeleted
  • Updated testSearchRepositories/testSearchRepositoriesWithSearch to assert against the new items/total shape
  • composer format/composer check (PHPStan level 8) clean
  • Verified live end-to-end against a real GitLab OAuth app and repository (auth header + searchRepositories fixes were only caught this way -- unit tests use mocked HTTP and couldn't have caught either)

Verified against real GitLab webhook payloads: push/MR events were
missing repositoryId (breaking any DB lookup keyed on it), used
non-standard key names (name/commitAuthor/commitMessage/commitUrl
instead of repositoryName/headCommitAuthorName/headCommitMessage/
headCommitUrl), and passed merge_request action through raw
(open/reopen/update/close) instead of normalizing to the GitHub/Gitea
convention (opened/reopened/synchronize/closed) consumers already key
off of. Also adds repositoryUrl, branchUrl, affectedFiles, branch
create/delete detection, and cross-project (fork) detection for MRs,
matching Gitea's return shape field-for-field.
…l default

- Added dedicated tests for branchCreated/branchDeleted sentinel paths.
- Documented the external=false fallback as an intentional safe default.
- Reverted a pullRequestNumber strval() cast: Gitea/GitHub both return
  the raw int-or-empty-string from getEvent() too, so casting only
  GitLab would trade one inconsistency for another. Leaving all three
  adapters consistent with each other for now; a real fix needs to
  touch all three, which is a separate, broader change.
…spaces

GET /users/:id/projects only ever returns *public* projects, even for
the authenticated user's own account -- confirmed live against a real
GitLab account with private repos, which came back empty. Falls back
to GET /projects?membership=true instead (every project, public and
private, the token's user is a member of), filtered client-side to
the requested owner's namespace since that endpoint isn't scoped to
one owner. The group-namespace path is untouched, since it was
already correctly scoped by GitLab itself.

Note: the existing testSearchRepositories/testSearchRepositoriesWithSearch
tests only cover a public repo, which is exactly why this didn't get
caught -- worth adding a private-repo case as a follow-up.
…th2 tokens

Every API call used the PRIVATE-TOKEN header (and ?private_token= for
the presigned archive URL), which GitLab only honors for Personal
Access Tokens. Our entire integration is OAuth2-based (authorization
code flow), which GitLab requires via Authorization: Bearer <token>
(or ?access_token= for URL-embedded tokens) -- confirmed live: a
freshly-issued OAuth2 access token was rejected with 401 'Bad
credentials' on every single request, immediately after being minted,
which ruled out expiry/staleness and pointed at the auth mechanism
itself.
…otal contract

GitLab's version returned a bare indexed array; GitHub and Gitea both
return ['items' => ..., 'total' => ...] (confirmed by reading both).
Consumers destructure that shape directly (e.g. Appwrite's
Installations/Repositories/XList.php), so this crashed with a
TypeError the moment a real request reached it. Total count comes
from GitLab's X-Total response header for the successfully-scoped
group/personal-namespace fetch; falls back to the filtered count when
client-side namespace filtering is in effect, since X-Total in that
case reflects every namespace the token can see, not just the
requested owner.
@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates GitLab adapter behavior to match the shared provider contracts. The main changes are:

  • GitLab API requests now use bearer-style authorization.
  • Repository archive URLs now use an access-token query parameter.
  • Repository search now returns items and total.
  • GitLab push and merge request webhook payloads now use normalized field names and action values.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
src/VCS/Adapter/Git/GitLab.php Updates GitLab authentication, repository search output, archive URLs, and normalized webhook event fields.
tests/VCS/Adapter/GitLabTest.php Updates GitLab adapter tests for archive URLs, repository search shape, and normalized webhook event payloads.

Reviews (10): Last reviewed commit: "Shorten comment on searchRepositories() ..." | Re-trigger Greptile

Comment thread src/VCS/Adapter/Git/GitLab.php
Comment thread src/VCS/Adapter/Git/GitLab.php
Comment thread src/VCS/Adapter/Git/GitLab.php
testGetRepositoryPresignedUrl still asserted the old private_token
query param; the adapter now emits access_token to match OAuth2 auth.
Comment thread src/VCS/Adapter/Git/GitLab.php
Comment thread src/VCS/Adapter/Git/GitLab.php
…ship pages

Previously fetched one page of /projects?membership=true and filtered
it client-side, so a user's own repos could land on a later page than
requested and searchRepositories() would return an empty result even
though matching repos existed. Now collects all owner-matching projects
across membership pages first, then paginates the filtered set.
Comment thread src/VCS/Adapter/Git/GitLab.php Outdated
Comment thread src/VCS/Adapter/Git/GitLab.php
@HarshMN2345

Copy link
Copy Markdown
Member Author

@greptile Reverted the multi-page membership scan for now to keep this PR scoped to what's needed to get official GitLab support live — the flagged case (personal-namespace repos landing past page 1 of /projects?membership=true) only affects users in many GitLab groups. Tracking as a follow-up rather than blocking this PR on it.

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

That's a reasonable scoping call — the fix for private personal-namespace repos is a meaningful improvement over the previous approach that returned only public repos, and the pagination edge case only bites users in unusually large GitLab group memberships.

One suggestion: consider leaving a // TODO comment at the membership fallback path so the limitation is visible to anyone reading the code later, e.g.:

// TODO: This fetches only one page of membership projects before filtering to $ownerPath.
// Users with many group memberships may have personal-namespace repos on later pages.
// Follow-up: paginate exhaustively or use a namespace-scoped query.

That way the gap is self-documenting and won't need to be rediscovered from a GitHub issue. Happy to resolve this thread if you're tracking it externally.

@HarshMN2345
HarshMN2345 merged commit 15f6cd4 into main Jul 17, 2026
7 of 8 checks passed
HarshMN2345 added a commit to appwrite/appwrite that referenced this pull request Jul 17, 2026
Now that the GitLab getEvent()/auth-header/searchRepositories fixes
(utopia-php/vcs#120) are released, unpin from the temporary fork
reference and lock to the real published version.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants