Skip to content

Commit 8adeaab

Browse files
waleedlatif1mzxchandragreptile-apps[bot]
authored
feat(gitlab): access, membership, and user-admin operations (#5710)
* feat(gitlab): add access, membership, and user-admin tools Adds member, invitation, access-request, SAML group link, and user administration tools to the GitLab integration. Resource-scoped ops work against projects or groups; user-admin ops require an admin token. All tools reuse the existing host/SSRF guard via getGitLabApiBase and add a shared getGitLabResourcePath helper. * feat(gitlab): wire access operations into the GitLab block Adds the new operations to the block dropdown and tools access list, with a named access-level dropdown (enum in, integer out), first-class expires_at, a /members/all default (direct-only opt-in), resource-type selector, and member_role_id passthrough. * test(gitlab): cover access operations Covers the access_level enum-to-integer coercion, the /members/all default vs direct-only, the 409-duplicate-add soft success, invitation per-email error handling, user-status-action response parsing, and getGitLabResourcePath. * docs(gitlab): document access and membership operations * Update apps/sim/blocks/blocks/gitlab.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix(gitlab): address review findings - update_user now sends admin:false so the Administrator switch can demote (an untouched switch stays undefined and leaves the flag unchanged) - expose the access-level dropdown for Update Invitation - normalize comma-separated invite emails so spaced multi-email input works * fix(gitlab): make update-invitation access level optional Update Invitation now uses a dedicated dropdown that defaults to 'Leave unchanged', so updating only the expiration no longer silently resets the invitation's access level to Developer. The level is sent only when explicitly chosen. * fix(gitlab): validation pass — SAML provider param, member/invitation query filter, moderation user guard, registry order * fix(gitlab): apply 10-agent validation findings across all 62 tools - MR draft flag now applied via Draft: title prefix (GitLab has no draft body param) - update_user only sends admin when a real boolean (untouched switch serialized null and could demote admins) - add_member 409 soft-success now verified against the conflict body - auto_merge sent alongside deprecated merge_when_pipeline_succeeds - job log capped at 200k chars, file content at 1M chars, with truncated outputs - MR diffs signal hasMore beyond 100 files - wire dropped params: update_issue milestoneId, MR milestone/squash/removeSourceBranch, pipelines ref, tree ref, branches search, commits since/until/path/author, update_file lastCommitId, jobs includeRetried, create_user forceRandomPassword - complete pipeline/job status enums, access-level enums, widen stale type unions - guards: update_invitation requires a change; create_user requires a password strategy - fix double-encoding trap in path descriptions; doc-accuracy touch-ups * fix(gitlab): review round 1 — dedicated no-default access level for update member, expiration clearing via explicit empty string * fix(gitlab): explicit Clear Expiration toggle for update member/invitation * feat(gitlab): expose full documented API surface across tools and block - membership: add-by-username, remove-member cleanup flags, list-member filters (user_ids/state/seat info), invite_source - listings: search/visibility/owned/membership, assignee/milestone filters, source/target branch filters, per-domain order-by + sort direction - CI: pipeline variables + spec:inputs, manual-job variables - repo: commit authoring (start branch, author, execute flag), release tag message + asset links, cross-fork compare + unidiff, internal notes - catalog: access-governance template + member-provisioning and access-request-audit skills - hardening from 3-agent validation: declared release params, tolerate single-object asset links, NaN guard on assignee filter, null/scalar JSON rejection * fix(gitlab): tri-state controls for update-op booleans (executable flag, MR squash/remove-source-branch) --------- Co-authored-by: Marcus Chandra <mzxchandra@gmail.com> Co-authored-by: mzxchandra <129460234+mzxchandra@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent b5196a4 commit 8adeaab

63 files changed

Lines changed: 6068 additions & 190 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/content/docs/en/integrations/gitlab.mdx

Lines changed: 996 additions & 43 deletions
Large diffs are not rendered by default.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { GitLabBlock } from './gitlab'
6+
7+
const block = GitLabBlock
8+
9+
describe('GitLabBlock access operations', () => {
10+
it('routes every access operation to its matching tool id without serialization-time coercion', () => {
11+
const accessOps = [
12+
'gitlab_list_members',
13+
'gitlab_add_member',
14+
'gitlab_update_member',
15+
'gitlab_remove_member',
16+
'gitlab_invite_member',
17+
'gitlab_approve_access_request',
18+
'gitlab_search_users',
19+
'gitlab_block_user',
20+
'gitlab_add_saml_group_link',
21+
]
22+
for (const toolId of accessOps) {
23+
expect(block.tools.access).toContain(toolId)
24+
expect(block.tools.config.tool?.({ operation: toolId })).toBe(toolId)
25+
}
26+
})
27+
28+
it('exposes the named access-level dropdown with GitLab integer ids', () => {
29+
const accessLevel = block.subBlocks.find((s) => s.id === 'accessLevel')
30+
expect(accessLevel?.type).toBe('dropdown')
31+
const options = typeof accessLevel?.options === 'function' ? undefined : accessLevel?.options
32+
expect(options?.map((o) => o.id)).toEqual(['0', '5', '10', '15', '20', '25', '30', '40', '50'])
33+
expect(accessLevel?.value?.()).toBe('30')
34+
})
35+
36+
it('coerces the selected access level from the dropdown string to an integer at execution time', () => {
37+
const addParams = block.tools.config.params?.({
38+
accessToken: 'pat',
39+
operation: 'gitlab_add_member',
40+
resourceType: 'group',
41+
resourceId: '42',
42+
userId: '7',
43+
accessLevel: '40',
44+
expiresAt: '2026-12-31',
45+
memberRoleId: '5',
46+
})
47+
expect(addParams).toMatchObject({
48+
resourceType: 'group',
49+
resourceId: '42',
50+
userId: 7,
51+
accessLevel: 40,
52+
expiresAt: '2026-12-31',
53+
memberRoleId: 5,
54+
})
55+
expect(typeof addParams?.accessLevel).toBe('number')
56+
})
57+
58+
it('defaults list members to inherited members (directOnly falsy)', () => {
59+
const listParams = block.tools.config.params?.({
60+
accessToken: 'pat',
61+
operation: 'gitlab_list_members',
62+
resourceType: 'project',
63+
resourceId: 'grp/proj',
64+
})
65+
expect(listParams).toMatchObject({ resourceType: 'project', resourceId: 'grp/proj' })
66+
expect(listParams?.directOnly).toBeUndefined()
67+
68+
const directParams = block.tools.config.params?.({
69+
accessToken: 'pat',
70+
operation: 'gitlab_list_members',
71+
resourceType: 'project',
72+
resourceId: 'grp/proj',
73+
directMembersOnly: true,
74+
})
75+
expect(directParams?.directOnly).toBe(true)
76+
})
77+
78+
it('coerces the target user id for admin user actions', () => {
79+
const blockParams = block.tools.config.params?.({
80+
accessToken: 'pat',
81+
operation: 'gitlab_block_user',
82+
userId: '99',
83+
})
84+
expect(blockParams).toMatchObject({ userId: 99 })
85+
expect(typeof blockParams?.userId).toBe('number')
86+
})
87+
88+
it('optionally coerces the granted access level for approve access request', () => {
89+
const approveParams = block.tools.config.params?.({
90+
accessToken: 'pat',
91+
operation: 'gitlab_approve_access_request',
92+
resourceType: 'group',
93+
resourceId: '42',
94+
userId: '7',
95+
accessLevel: '30',
96+
})
97+
expect(approveParams).toMatchObject({ userId: 7, accessLevel: 30 })
98+
})
99+
100+
it('sends admin:false so update user can demote an administrator', () => {
101+
const demote = block.tools.config.params?.({
102+
accessToken: 'pat',
103+
operation: 'gitlab_update_user',
104+
userId: '9',
105+
userAdminIsAdmin: false,
106+
})
107+
expect(demote?.admin).toBe(false)
108+
109+
// An untouched switch is undefined and must leave the admin flag unchanged.
110+
const untouched = block.tools.config.params?.({
111+
accessToken: 'pat',
112+
operation: 'gitlab_update_user',
113+
userId: '9',
114+
})
115+
expect(untouched?.admin).toBeUndefined()
116+
})
117+
118+
it('exposes an optional access-level dropdown for update invitation that defaults to unchanged', () => {
119+
const invAccess = block.subBlocks.find((s) => s.id === 'invitationAccessLevel')
120+
expect(invAccess?.type).toBe('dropdown')
121+
expect(invAccess?.value?.()).toBe('')
122+
const options = typeof invAccess?.options === 'function' ? undefined : invAccess?.options
123+
expect(options?.[0]).toEqual({ label: 'Leave unchanged', id: '' })
124+
125+
// Updating only the expiration must NOT send an access level (no silent reset).
126+
const expiryOnly = block.tools.config.params?.({
127+
accessToken: 'pat',
128+
operation: 'gitlab_update_invitation',
129+
resourceType: 'group',
130+
resourceId: '42',
131+
email: 'a@b.com',
132+
expiresAt: '2027-01-01',
133+
invitationAccessLevel: '',
134+
})
135+
expect(expiryOnly).toMatchObject({ email: 'a@b.com', expiresAt: '2027-01-01' })
136+
expect(expiryOnly?.accessLevel).toBeUndefined()
137+
138+
// Choosing a level sends the coerced integer.
139+
const withLevel = block.tools.config.params?.({
140+
accessToken: 'pat',
141+
operation: 'gitlab_update_invitation',
142+
resourceType: 'group',
143+
resourceId: '42',
144+
email: 'a@b.com',
145+
invitationAccessLevel: '40',
146+
})
147+
expect(withLevel).toMatchObject({ email: 'a@b.com', accessLevel: 40 })
148+
})
149+
150+
it('throws when required access fields are missing', () => {
151+
expect(() =>
152+
block.tools.config.params?.({
153+
accessToken: 'pat',
154+
operation: 'gitlab_add_member',
155+
resourceType: 'group',
156+
resourceId: '42',
157+
})
158+
).toThrow()
159+
})
160+
})

0 commit comments

Comments
 (0)