Fix GitLab::getEvent() field/action parity with Gitea's shape#120
Conversation
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 SummaryThis PR updates GitLab adapter behavior to match the shared provider contracts. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (10): Last reviewed commit: "Shorten comment on searchRepositories() ..." | Re-trigger Greptile |
testGetRepositoryPresignedUrl still asserted the old private_token query param; the adapter now emits access_token to match OAuth2 auth.
…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.
…r membership pages" This reverts commit b7ce717.
|
@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. |
|
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: 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. |
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.
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:
repositoryIdentirely -- any DB lookup keyed on it finds nothing.name/commitAuthor/commitMessage/commitUrlinstead ofrepositoryName/headCommitAuthorName/headCommitMessage/headCommitUrl(matching Gitea's/GitHub's convention).actionpassed 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 distinctmergeaction toclosed, since a merged MR is done either way.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:
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 asAuthorization: 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 calledGET /users/{username}/projects, which GitLab only returns public projects for, even for the authenticated user's own account. Switched toGET /projects?membership=true(every project -- public and private -- the token's user is a member of) with client-side filtering bynamespace.path.searchRepositories()return shape didn't match GitHub/Gitea'sitems/totalcontract. It returned a bare indexed array instead of['items' => [...], 'total' => N], breaking any consumer destructuring the common shape.totalis now taken from theX-Totalresponse 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
testGetEventPush/testGetEventPullRequestto assert the corrected shapetestGetEventPullRequestActionMappingcovers all 5 native action valuestestGetEventPullRequestDetectsExternalcovers fork/cross-project MR detectiontestGetEventPushDetectsBranchCreated/testGetEventPushDetectsBranchDeletedtestSearchRepositories/testSearchRepositoriesWithSearchto assert against the newitems/totalshapecomposer format/composer check(PHPStan level 8) clean