Skip to content

Conversation

@russellhaering
Copy link

@russellhaering russellhaering commented Jan 9, 2026

Summary

  • Add invite-based user provisioning via organizationInviteCreate GraphQL mutation
  • Add role management (admin promotion/demotion) via userUpdate mutation
  • Add user suspension/reactivation via userUpdate mutation
  • Add comprehensive tests for all provisioning functionality

Changes

Linear Client (pkg/linear/)

  • CreateOrganizationInvite(email, role) - sends email invite to provision new users
  • UpdateUser(userID, admin, active) - updates user admin status or suspends/reactivates
  • OrganizationInvite model and role constants (ADMIN, MEMBER, GUEST)

Connector (pkg/connector/)

  • user.go: Create() method for invite-based provisioning
  • role.go: Grant() and Revoke() methods for role management

Provisioning Matrix

Operation API Mutation Notes
Create user organizationInviteCreate User receives email invite
Grant admin userUpdate(admin: true) Promotes user to admin
Revoke admin userUpdate(admin: false) Demotes to member
Suspend user userUpdate(active: false) Deprovisioning
Reactivate userUpdate(active: true) Unsuspend user

Limitations

  • Guest/Owner roles cannot be changed via API (set at invite time / Enterprise UI)
  • New users must accept email invitation to complete provisioning
  • No direct user deletion (only suspension)

Test plan

  • Unit tests for CreateOrganizationInvite (5 test cases)
  • Unit tests for UpdateUser (7 test cases)
  • Tests for user Create method with email validation
  • Tests for role Grant/Revoke with validation (10 test cases)
  • All 22 tests passing

Summary by CodeRabbit

  • New Features

    • Added user provisioning capabilities to create and invite new users to organizations.
    • Added role management functionality to grant and revoke role assignments (admin, member, guest).
  • Tests

    • Comprehensive test suite added to validate provisioning, role operations, and API integrations.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add CreateOrganizationInvite() to send email invites for new users
- Add UpdateUser() to modify admin status and suspend/unsuspend users
- Implement Create() on user resource for invite-based provisioning
- Implement Grant() and Revoke() on role resource for admin role management
- Add client tests for CreateOrganizationInvite and UpdateUser mutations
- Add connector tests for user Create method and role Grant/Revoke methods
- Test validation logic for role assignments (guest/owner cannot be changed via API)
- Test error handling for missing email and non-user principals
@russellhaering russellhaering requested a review from a team January 9, 2026 01:35
@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 2026

Walkthrough

This pull request adds provisioning capabilities to a Linear connector by introducing user creation and role management functionality. New methods enable creating Linear users via organization invites and granting/revoking roles (admin, member, guest). The implementation spans the Linear API client with two new GraphQL mutation methods, connector resource types with provisioning logic, and comprehensive test coverage across both layers.

Changes

Cohort / File(s) Summary
Linear API Client
pkg/linear/client.go, pkg/linear/models.go
Added CreateOrganizationInvite() and UpdateUser() methods to the Linear client for GraphQL mutations. Introduced OrganizationInvite type and role constants (LinearRoleAdmin, LinearRoleMember, LinearRoleGuest) to support organization invite operations.
Linear Client Tests
pkg/linear/client_test.go
New comprehensive test suite validating CreateOrganizationInvite() and UpdateUser() methods with multiple scenarios: role assignment (member/admin/guest), user state changes (admin promotion/demotion, suspension/reactivation), request body structure validation, and error handling paths.
Connector User Provisioning
pkg/connector/user.go
Added Create() method to userResourceType that provisions new Linear users by extracting email traits, validating email presence, and invoking CreateOrganizationInvite() with default MEMBER role.
Connector Role Management
pkg/connector/role.go
Added Grant() and Revoke() methods to roleResourceType for role entitlement changes. Grant validates user principals and updates admin status; Revoke demotes admins or suspends users. Both reject non-user principals and unsupported roles (guest/owner).
Connector Provisioning Tests
pkg/connector/provisioning_test.go
New comprehensive test suite covering user creation (success and missing email validation), role grant/revoke operations for all supported roles (admin, member, guest), non-user principal rejection, and role constant assertions.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant Connector as Connector<br/>(user.Create)
    participant LinearClient as Linear Client
    participant LinearAPI as Linear API

    Client->>Connector: Create User<br/>(with email)
    Connector->>Connector: Extract email trait
    Connector->>Connector: Validate email exists
    Connector->>LinearClient: CreateOrganizationInvite<br/>(email, MEMBER)
    LinearClient->>LinearAPI: GraphQL Mutation<br/>(create invite)
    LinearAPI-->>LinearClient: OrganizationInvite
    LinearClient-->>Connector: Success
    Connector-->>Client: User Resource
Loading
sequenceDiagram
    participant Client
    participant Connector as Connector<br/>(role.Grant/Revoke)
    participant LinearClient as Linear Client
    participant LinearAPI as Linear API

    Client->>Connector: Grant/Revoke Role<br/>(principal, entitlement)
    Connector->>Connector: Validate principal is User
    Connector->>Connector: Validate role supported<br/>(admin/member only)
    Connector->>LinearClient: UpdateUser<br/>(userID, admin flag)
    LinearClient->>LinearAPI: GraphQL Mutation<br/>(update user)
    LinearAPI-->>LinearClient: Success/Error
    LinearClient-->>Connector: Result
    Connector-->>Client: Annotations or Error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A whisker-twitch of joy! Fresh methods hop into place,
Users now invited with a role and grace,
Admins granted, members managed, guest roles declined,
Linear's doors swing open—provisioning, refined! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main purpose of the changeset—adding account provisioning support via user creation, role grants/revokes, and user state management in the Linear connector.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @pkg/connector/provisioning_test.go:
- Around line 12-62: TestUserCreate (and similar tests at ranges 89-196,
241-335) currently constructs a real Linear client via linear.NewClient and
makes real HTTP calls; replace that with a testable approach by either (A)
changing userBuilder/roleResourceType constructors to accept a LinearClient
interface (instead of concrete client) and update TestUserCreate to inject a
fake/mock implementation, or (B) add an option to linear.NewClient such as
WithAPIEndpoint so tests can start an httptest.Server (as in
pkg/linear/client_test.go) and point the client at that server; update tests to
create a mock/fake client or a client with WithAPIEndpoint and assert success
and error paths deterministically when calling userType.Create and
roleResourceType.Create rather than relying on real network calls.

In @pkg/connector/role.go:
- Around line 99-131: The Grant method's roleUser branch only clears admin but
never reactivates suspended users; update the roleUser case in
roleResourceType.Grant to call o.client.UpdateUser with admin set to false and
active set to true (pass a pointer to true as the second argument) so suspended
users are reactivated when granted the user role; keep error handling consistent
with the other cases and do not change other branches.

In @pkg/linear/client_test.go:
- Line 364: Update the file's top-level comment "Helper functions" to end with a
period so it reads "Helper functions."; locate the comment in
pkg/linear/client_test.go (currently "// Helper functions") and add the trailing
period to satisfy the linter.
- Around line 106-139: The test HTTP handler in the httptest.NewServer closure
(the func literal starting at "server :=
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)
{") calls w.Write(...) without checking the returned error, which trips
errcheck; change those calls to check the error and fail the test on write error
(e.g., if _, err := w.Write([]byte(tt.serverResponse)); err != nil {
t.Fatalf("write response: %v", err) }) or explicitly ignore the value with a
blank identifier if you prefer to suppress the checker, and apply the same fix
to the other occurrence mentioned (lines ~288-337).

In @pkg/linear/client.go:
- Around line 956-1048: The UpdateUser function currently allows no-op calls
when both admin and active are nil, which sends an empty UserUpdateInput and can
produce confusing server errors; add an early check in UpdateUser (before
creating the mutation/vars) to detect if admin == nil && active == nil and
return a clear error (e.g., "no update fields provided") instead of proceeding,
so the function rejects empty updates and avoids sending an empty input map to
the server.
- Around line 956-1048: The docstring for CreateOrganizationInvite is wrong:
update it to state role accepts the lowercase enums "user", "admin", "guest",
"owner", or "app". Remove the unsupported admin and active parameters from
UpdateUser and instead implement/replace with dedicated mutations: add/change
functions that call the GraphQL mutations userChangeRole (for role changes),
userSuspend, and userUnsuspend (for active/suspended state changes); each should
build the appropriate mutation payload, call c.doRequest, check the returned
success boolean (like other methods), close resp.Body, and return errors on
failure. Ensure to reference these new functions (e.g., ChangeUserRole /
SuspendUser / UnsuspendUser or similar) in place of the old UpdateUser behavior
and keep existing error handling pattern.
🧹 Nitpick comments (4)
pkg/connector/user.go (1)

139-161: Prefer selecting the primary email + validate non-empty/trimmed address before inviting.

Right now emails[0] may be non-primary, and GetAddress() could be empty/whitespace; consider selecting the primary email (or first non-empty) and strings.TrimSpace before calling the API.

Proposed tweak
-	emails := userTrait.GetEmails()
+	emails := userTrait.GetEmails()
 	if len(emails) == 0 {
 		return nil, nil, fmt.Errorf("linear-connector: user email is required for provisioning")
 	}
-	email := emails[0].GetAddress()
+	email := ""
+	for _, e := range emails {
+		addr := strings.TrimSpace(e.GetAddress())
+		if addr == "" {
+			continue
+		}
+		// Prefer primary email if present.
+		if e.GetPrimary() {
+			email = addr
+			break
+		}
+		if email == "" {
+			email = addr
+		}
+	}
+	if email == "" {
+		return nil, nil, fmt.Errorf("linear-connector: user email is required for provisioning")
+	}
pkg/linear/models.go (1)

200-211: Consider a type LinearRole string for compile-time safety.
Right now role string is easy to misuse; a dedicated type makes call sites and tests harder to get wrong.

pkg/connector/role.go (1)

99-131: Add nil-guards for principal, principal.Id, entitlement.Resource, etc. to avoid panics.
These methods assume fully-populated proto structs; a malformed request could panic on principal.Id.ResourceType or entitlement.Resource.Id.

Also applies to: 133-165

pkg/connector/provisioning_test.go (1)

383-395: Replace custom containsString with strings.Contains.
Less code, clearer intent.

Proposed fix
 import (
 	"context"
 	"testing"
+	"strings"
@@
 func containsString(s, substr string) bool {
-	return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStringHelper(s, substr))
-}
-
-func containsStringHelper(s, substr string) bool {
-	for i := 0; i <= len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return true
-		}
-	}
-	return false
+	return strings.Contains(s, substr)
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94699c0 and 78bc27a.

📒 Files selected for processing (6)
  • pkg/connector/provisioning_test.go
  • pkg/connector/role.go
  • pkg/connector/user.go
  • pkg/linear/client.go
  • pkg/linear/client_test.go
  • pkg/linear/models.go
🧰 Additional context used
🧬 Code graph analysis (2)
pkg/connector/user.go (1)
pkg/linear/models.go (1)
  • LinearRoleMember (209-209)
pkg/connector/provisioning_test.go (2)
pkg/linear/client.go (1)
  • NewClient (24-43)
pkg/linear/models.go (3)
  • LinearRoleAdmin (208-208)
  • LinearRoleMember (209-209)
  • LinearRoleGuest (210-210)
🪛 GitHub Actions: ci
pkg/linear/client_test.go

[error] 138-138: golangci-lint: Error return value of w.Write is not checked. (errcheck)

🪛 GitHub Check: go-lint
pkg/linear/client_test.go

[failure] 364-364:
Comment should end in a period (godot)


[failure] 336-336:
Error return value of w.Write is not checked (errcheck)


[failure] 138-138:
Error return value of w.Write is not checked (errcheck)

Comment on lines +12 to +62
func TestUserCreate(t *testing.T) {
// Note: These tests validate the code structure and error handling paths.
// Full integration tests would require mocking the Linear API client.
tests := []struct {
name string
email string
}{
{
name: "user creation with valid email",
email: "newuser@example.com",
},
{
name: "user creation with different email",
email: "another@example.com",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := linear.NewClient(context.Background(), "test-api-key")
if err != nil {
t.Fatalf("failed to create client: %v", err)
}

// Create user resource type
userType := userBuilder(client)

// Create a test user resource with email
userResource, err := resource.NewUserResource(
"Test User",
resourceTypeUser,
"test-user-id",
[]resource.UserTraitOption{
resource.WithEmail(tt.email, true),
},
)
if err != nil {
t.Fatalf("failed to create user resource: %v", err)
}

// Call Create - will fail at API level but validates code path
_, _, err = userType.Create(context.Background(), userResource)

// We expect an error because we're not mocking the API
// This test validates the code runs without panics and handles the resource correctly
if err == nil {
t.Log("Note: API call succeeded (unexpected in unit test)")
}
})
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Tests currently make real HTTP calls and don’t assert success-path behavior.
These “unit tests” can be flaky (network/endpoint), and wantErr: false cases never fail even if the API call errors. Recommend injecting a fake Linear client (interface) into userResourceType / roleResourceType, or adding a linear.NewClient(..., WithAPIEndpoint(...)) option so tests can use httptest.Server like pkg/linear/client_test.go.

Also applies to: 89-196, 241-335

🤖 Prompt for AI Agents
In @pkg/connector/provisioning_test.go around lines 12 - 62, TestUserCreate (and
similar tests at ranges 89-196, 241-335) currently constructs a real Linear
client via linear.NewClient and makes real HTTP calls; replace that with a
testable approach by either (A) changing userBuilder/roleResourceType
constructors to accept a LinearClient interface (instead of concrete client) and
update TestUserCreate to inject a fake/mock implementation, or (B) add an option
to linear.NewClient such as WithAPIEndpoint so tests can start an
httptest.Server (as in pkg/linear/client_test.go) and point the client at that
server; update tests to create a mock/fake client or a client with
WithAPIEndpoint and assert success and error paths deterministically when
calling userType.Create and roleResourceType.Create rather than relying on real
network calls.

Comment on lines +99 to +131
// Grant assigns a role to a user.
func (o *roleResourceType) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) (annotations.Annotations, error) {
if principal.Id.ResourceType != resourceTypeUser.Id {
return nil, fmt.Errorf("linear-connector: only users can be granted roles")
}

roleID := entitlement.Resource.Id.Resource
userID := principal.Id.Resource

switch roleID {
case roleAdmin:
// Promote to admin
admin := true
err := o.client.UpdateUser(ctx, userID, &admin, nil)
if err != nil {
return nil, fmt.Errorf("linear-connector: failed to grant admin role: %w", err)
}
case roleUser:
// Demote from admin to regular user
admin := false
err := o.client.UpdateUser(ctx, userID, &admin, nil)
if err != nil {
return nil, fmt.Errorf("linear-connector: failed to grant user role: %w", err)
}
case roleGuest, roleOwner:
// Guest is set at invite time, Owner requires enterprise UI
return nil, fmt.Errorf("linear-connector: %s role cannot be granted via API", roleID)
default:
return nil, fmt.Errorf("linear-connector: unknown role: %s", roleID)
}

return nil, nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all UpdateUser calls and active flag operations
echo "=== UpdateUser calls ==="
rg -n --type=go '\.UpdateUser\(' pkg/connector/

echo -e "\n=== Setting active via input ==="
rg -n --type=go 'input\["active"\]' pkg/connector/

echo -e "\n=== Searching for active=true or boolPtr(true) ==="
rg -n --type=go 'active\s*:=\s*true|boolPtr\(true\)' pkg/connector/

echo -e "\n=== Searching for 'reactivate' keyword ==="
rg -in 'reactivate' pkg/connector/

Repository: ConductorOne/baton-linear

Length of output: 548


🏁 Script executed:

#!/bin/bash
# Read the Revoke method to understand the complete picture
cat -n pkg/connector/role.go | sed -n '133,165p'

Repository: ConductorOne/baton-linear

Length of output: 1385


🏁 Script executed:

#!/bin/bash
# Check if there are other resource types or methods that might handle reactivation
fd -t f -e go pkg/connector/ | xargs grep -l "Grant\|Revoke" | head -5

Repository: ConductorOne/baton-linear

Length of output: 430


Add reactivation to Grant for roleUser case—currently only demotes from admin without reactivating suspended users.
The Revoke(roleUser) case suspends users via active=false, but Grant(roleUser) only sets admin=false and never sets active=true. No code path in the repository activates suspended users, breaking the suspend/reactivate lifecycle. When a suspended user is granted the user role, they remain suspended.

🤖 Prompt for AI Agents
In @pkg/connector/role.go around lines 99 - 131, The Grant method's roleUser
branch only clears admin but never reactivates suspended users; update the
roleUser case in roleResourceType.Grant to call o.client.UpdateUser with admin
set to false and active set to true (pass a pointer to true as the second
argument) so suspended users are reactivated when granted the user role; keep
error handling consistent with the other cases and do not change other branches.

Comment on lines +106 to +139
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request method
if r.Method != http.MethodPost {
t.Errorf("expected POST request, got %s", r.Method)
}

// Verify request body contains expected data
var reqBody map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
t.Errorf("failed to decode request body: %v", err)
}

variables, ok := reqBody["variables"].(map[string]interface{})
if !ok {
t.Error("request body missing variables")
}

input, ok := variables["input"].(map[string]interface{})
if !ok {
t.Error("variables missing input")
}

if input["email"] != tt.email {
t.Errorf("expected email %s, got %s", tt.email, input["email"])
}

if input["role"] != tt.role {
t.Errorf("expected role %s, got %s", tt.role, input["role"])
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(tt.statusCode)
w.Write([]byte(tt.serverResponse))
}))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix CI failure: check w.Write errors (or handle explicitly).
Pipeline is failing on errcheck for these w.Write calls.

Proposed fix
 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				defer r.Body.Close()
 				// Verify request method
 				if r.Method != http.MethodPost {
 					t.Errorf("expected POST request, got %s", r.Method)
 				}
@@
 				w.Header().Set("Content-Type", "application/json")
 				w.WriteHeader(tt.statusCode)
-				w.Write([]byte(tt.serverResponse))
+				if _, err := w.Write([]byte(tt.serverResponse)); err != nil {
+					t.Errorf("failed to write response: %v", err)
+				}
 			}))
 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				defer r.Body.Close()
 				// Verify request method
 				if r.Method != http.MethodPost {
 					t.Errorf("expected POST request, got %s", r.Method)
 				}
@@
 				w.Header().Set("Content-Type", "application/json")
 				w.WriteHeader(tt.statusCode)
-				w.Write([]byte(tt.serverResponse))
+				if _, err := w.Write([]byte(tt.serverResponse)); err != nil {
+					t.Errorf("failed to write response: %v", err)
+				}
 			}))

Also applies to: 288-337

🧰 Tools
🪛 GitHub Actions: ci

[error] 138-138: golangci-lint: Error return value of w.Write is not checked. (errcheck)

🪛 GitHub Check: go-lint

[failure] 138-138:
Error return value of w.Write is not checked (errcheck)

🤖 Prompt for AI Agents
In @pkg/linear/client_test.go around lines 106 - 139, The test HTTP handler in
the httptest.NewServer closure (the func literal starting at "server :=
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)
{") calls w.Write(...) without checking the returned error, which trips
errcheck; change those calls to check the error and fail the test on write error
(e.g., if _, err := w.Write([]byte(tt.serverResponse)); err != nil {
t.Fatalf("write response: %v", err) }) or explicitly ignore the value with a
blank identifier if you prefer to suppress the checker, and apply the same fix
to the other occurrence mentioned (lines ~288-337).

}
}

// Helper functions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Lint: comment should end with a period.
// Helper functions.

🧰 Tools
🪛 GitHub Check: go-lint

[failure] 364-364:
Comment should end in a period (godot)

🤖 Prompt for AI Agents
In @pkg/linear/client_test.go at line 364, Update the file's top-level comment
"Helper functions" to end with a period so it reads "Helper functions."; locate
the comment in pkg/linear/client_test.go (currently "// Helper functions") and
add the trailing period to satisfy the linter.

Comment on lines +956 to +1048
// CreateOrganizationInvite sends an email invite to provision a new user.
// Role should be one of: LinearRoleAdmin, LinearRoleMember, or LinearRoleGuest.
func (c *Client) CreateOrganizationInvite(ctx context.Context, email string, role string) (*OrganizationInvite, error) {
mutation := `mutation OrganizationInviteCreate($input: OrganizationInviteCreateInput!) {
organizationInviteCreate(input: $input) {
success
organizationInvite {
id
email
}
}
}`

vars := map[string]interface{}{
"input": map[string]interface{}{
"email": email,
"role": role,
},
}

b := map[string]interface{}{
"query": mutation,
"variables": vars,
}

var res struct {
Data struct {
OrganizationInviteCreate struct {
Success bool `json:"success"`
OrganizationInvite OrganizationInvite `json:"organizationInvite"`
} `json:"organizationInviteCreate"`
} `json:"data"`
}
resp, _, err := c.doRequest(ctx, b, &res)
if err != nil {
return nil, err
}

defer resp.Body.Close()

if !res.Data.OrganizationInviteCreate.Success {
return nil, fmt.Errorf("failed to create organization invite")
}

return &res.Data.OrganizationInviteCreate.OrganizationInvite, nil
}

// UpdateUser updates user properties such as admin status and active status.
func (c *Client) UpdateUser(ctx context.Context, userID string, admin *bool, active *bool) error {
mutation := `mutation UserUpdate($id: String!, $input: UserUpdateInput!) {
userUpdate(id: $id, input: $input) {
success
}
}`

input := make(map[string]interface{})
if admin != nil {
input["admin"] = *admin
}
if active != nil {
input["active"] = *active
}

vars := map[string]interface{}{
"id": userID,
"input": input,
}

b := map[string]interface{}{
"query": mutation,
"variables": vars,
}

var res struct {
Data struct {
UserUpdate struct {
Success bool `json:"success"`
} `json:"userUpdate"`
} `json:"data"`
}
resp, _, err := c.doRequest(ctx, b, &res)
if err != nil {
return err
}

defer resp.Body.Close()

if !res.Data.UserUpdate.Success {
return fmt.Errorf("failed to update user")
}

return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject no-op updates: return an error if admin == nil && active == nil.
Otherwise you send an empty UserUpdateInput, which may fail server-side in a confusing way.

Proposed fix
 func (c *Client) UpdateUser(ctx context.Context, userID string, admin *bool, active *bool) error {
+	if admin == nil && active == nil {
+		return fmt.Errorf("update user: at least one of admin or active must be set")
+	}
 	mutation := `mutation UserUpdate($id: String!, $input: UserUpdateInput!) {
 		userUpdate(id: $id, input: $input) {
 			success
 		}
 	}`
🤖 Prompt for AI Agents
In @pkg/linear/client.go around lines 956 - 1048, The UpdateUser function
currently allows no-op calls when both admin and active are nil, which sends an
empty UserUpdateInput and can produce confusing server errors; add an early
check in UpdateUser (before creating the mutation/vars) to detect if admin ==
nil && active == nil and return a clear error (e.g., "no update fields
provided") instead of proceeding, so the function rejects empty updates and
avoids sending an empty input map to the server.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Linear GraphQL API documentation userUpdate mutation id type OrganizationInviteCreateInput

💡 Result:

Short answer: Linear uses GraphQL ID scalars that are UUID strings for entity IDs. For mutations like userUpdate (or organization-invite mutations) you should pass the GraphQL ID/UUID (type ID / uuid string). See Linear’s GraphQL docs and integrations guidance which state the API is GraphQL, supports introspection, and that entity IDs are UUIDs (use nested objects with id fields). (linear.app)

If you need exact input fields/types (e.g. the exact name of the userUpdate input field or OrganizationInviteCreateInput shape), run an introspection query against https://api.linear.app/graphql (or view the GraphQL schema in Linear’s docs/SDK) — that will show the precise input object fields (organizationId / userId fields are ID!/uuid). (linear.app)

If you want, I can run an example introspection query and show the exact userUpdate and OrganizationInviteCreateInput definitions.

Citations:


🏁 Script executed:

#!/bin/bash

# Introspection query to get schema info about the mutations and input types
cat > /tmp/introspection.json <<'EOF'
{
  "query": "query IntrospectionQuery { __schema { types { name kind fields(includeDeprecated: false) { name type { name kind ofType { name kind } } } inputFields { name type { name kind ofType { name kind } } } enumValues { name } } } }"
}
EOF

# Query Linear GraphQL API for schema information
curl -s -X POST https://api.linear.app/graphql \
  -H "Content-Type: application/json" \
  -d @/tmp/introspection.json 2>/dev/null | jq '.data.__schema.types[] | select(.name == "OrganizationInviteCreateInput" or .name == "UserUpdateInput" or .name == "String" or .name == "ID")' || echo "Could not fetch schema"

Repository: ConductorOne/baton-linear

Length of output: 2450


🏁 Script executed:

#!/bin/bash

# Query for UserRoleType enum values and mutation signatures
cat > /tmp/enum_query.json <<'EOF'
{
  "query": "query { __schema { types { name enumValues { name } } directives { name locations } } } }"
}
EOF

curl -s -X POST https://api.linear.app/graphql \
  -H "Content-Type: application/json" \
  -d @/tmp/enum_query.json 2>/dev/null | jq '.data.__schema.types[] | select(.name == "UserRoleType")' 

# Query for mutation signatures
cat > /tmp/mutation_query.json <<'EOF'
{
  "query": "query { __type(name: \"Query\") { fields { name } } __type(name: \"Mutation\") { fields { name args { name type { name kind ofType { name kind } } } type { name } } } }"
}
EOF

curl -s -X POST https://api.linear.app/graphql \
  -H "Content-Type: application/json" \
  -d @/tmp/mutation_query.json 2>/dev/null | jq '.data.__type.fields[] | select(.name == "userUpdate" or .name == "organizationInviteCreate")'

Repository: ConductorOne/baton-linear

Length of output: 184


🏁 Script executed:

#!/bin/bash

# Get UserRoleType enum values
cat > /tmp/query1.json <<'EOF'
{
  "query": "{ __type(name: \"UserRoleType\") { name enumValues { name } } }"
}
EOF

echo "=== UserRoleType enum values ==="
curl -s -X POST https://api.linear.app/graphql \
  -H "Content-Type: application/json" \
  -d @/tmp/query1.json 2>/dev/null | jq '.'

# Get mutation argument types
cat > /tmp/query2.json <<'EOF'
{
  "query": "{ __type(name: \"Mutation\") { fields(includeDeprecated: false) { name args { name type { kind name ofType { kind name } } } } } }"
}
EOF

echo "=== Mutation fields ==="
curl -s -X POST https://api.linear.app/graphql \
  -H "Content-Type: application/json" \
  -d @/tmp/query2.json 2>/dev/null | jq '.data.__type.fields[] | select(.name == "userUpdate" or .name == "organizationInviteCreate")'

Repository: ConductorOne/baton-linear

Length of output: 1184


🏁 Script executed:

#!/bin/bash

# Check if there are any other mutation types or alternate ways to handle user updates
cat > /tmp/admin_query.json <<'EOF'
{
  "query": "{ __type(name: \"UserUpdateInput\") { inputFields { name type { kind name ofType { kind name } } } } }"
}
EOF

echo "=== Complete UserUpdateInput schema ==="
curl -s -X POST https://api.linear.app/graphql \
  -H "Content-Type: application/json" \
  -d @/tmp/admin_query.json 2>/dev/null | jq '.data.__type.inputFields[] | .name'

# Check for other user mutation types
cat > /tmp/mutations_query.json <<'EOF'
{
  "query": "{ __type(name: \"Mutation\") { fields { name } } }"
}
EOF

echo "=== All available mutations ==="
curl -s -X POST https://api.linear.app/graphql \
  -H "Content-Type: application/json" \
  -d @/tmp/mutations_query.json 2>/dev/null | jq '.data.__type.fields[].name' | grep -i user

Repository: ConductorOne/baton-linear

Length of output: 559


Remove unsupported admin and active parameters from UpdateUser; refactor to use userChangeRole, userSuspend, and userUnsuspend mutations.

The UpdateUser function accepts admin and active parameters, but Linear's UserUpdateInput schema does not support these fields. Use the dedicated mutations instead: userChangeRole (for admin role changes) and userSuspend/userUnsuspend (for active status). Additionally, fix the CreateOrganizationInvite documentation: the role parameter should accept "user", "admin", "guest", "owner", or "app" (lowercase enum values), not LinearRoleAdmin, LinearRoleMember, or LinearRoleGuest.

🤖 Prompt for AI Agents
In @pkg/linear/client.go around lines 956 - 1048, The docstring for
CreateOrganizationInvite is wrong: update it to state role accepts the lowercase
enums "user", "admin", "guest", "owner", or "app". Remove the unsupported admin
and active parameters from UpdateUser and instead implement/replace with
dedicated mutations: add/change functions that call the GraphQL mutations
userChangeRole (for role changes), userSuspend, and userUnsuspend (for
active/suspended state changes); each should build the appropriate mutation
payload, call c.doRequest, check the returned success boolean (like other
methods), close resp.Body, and return errors on failure. Ensure to reference
these new functions (e.g., ChangeUserRole / SuspendUser / UnsuspendUser or
similar) in place of the old UpdateUser behavior and keep existing error
handling pattern.

}

// Create provisions a new user by sending an organization invite email.
func (o *userResourceType) Create(ctx context.Context, resource *v2.Resource) (*v2.Resource, annotations.Annotations, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be func (b *userBuilder) CreateAccount(? like this PR ConductorOne/baton-datadog#19

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.

3 participants