Skip to content

[CE-235] baton-github: support containerization. #120

Merged
Bencheng21 merged 13 commits intomainfrom
ben/CE235_containerize_github
Feb 5, 2026
Merged

[CE-235] baton-github: support containerization. #120
Bencheng21 merged 13 commits intomainfrom
ben/CE235_containerize_github

Conversation

@Bencheng21
Copy link
Contributor

@Bencheng21 Bencheng21 commented Feb 3, 2026

Test

App authentication.

image.

Pat token.

image

Grant

image

Revoke.

image

Summary by CodeRabbit

  • New Features

    • Added explicit configuration groups for GitHub App and Personal Access Token auth, including required org setting and secure private-key upload.
  • Refactor

    • Unified pagination/sync API across resources for consistent listing and paging behavior.
    • Organization name caching moved to session-backed store for more reliable lookups.
    • Connector initialization and wiring updated to support the new sync/auth flows.

@Bencheng21 Bencheng21 requested a review from a team February 3, 2026 22:42
@linear
Copy link

linear bot commented Feb 3, 2026

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

Walkthrough

This PR replaces per-method pagination/annotation returns with a unified SyncOpAttrs/SyncOpResults API across resource types, adds NewLambdaConnector with PAT/App flows and V2 wiring, moves org-name caching to session-backed storage, and updates configuration fields and tests to match the new APIs.

Changes

Cohort / File(s) Summary
Resource type API migration
pkg/connector/api_token.go, pkg/connector/enterprise_role.go, pkg/connector/invitation.go, pkg/connector/org.go, pkg/connector/org_role.go, pkg/connector/repository.go, pkg/connector/team.go, pkg/connector/user.go
Changed List/Entitlements/Grants signatures to accept SyncOpAttrs and return *SyncOpResults. Unified pagination to opts.PageToken/SyncOpResults.NextPageToken, removed direct pagination & annotations returns, and updated error/edge-case returns to emit empty SyncOpResults.
Connector core & initialization
pkg/connector/connector.go
Replaced New with NewLambdaConnector(ctx, ghc, cliOpts) returning ConnectorBuilderV2 and opts; added newWithGithubPAT and newWithGithubApp flows, upfront validation, V2 ResourceSyncer wiring, and removed legacy inline token logic.
Org-name caching / helpers
pkg/connector/helpers.go
Replaced in-memory RWMutex/map cache with session-backed caching. Added GetOrgNameFromRemoteServer and changed GetOrgName to accept a sessions.SessionStore, storing/fetching org names in the session.
Configuration updates
pkg/config/conf.gen.go, pkg/config/config.go
Added Org field to Github config and changed AppPrivatekeyPath handling to []byte. Introduced required/secret field metadata and field groups for GitHub App vs PAT flows (GithubAppGroup, GithubPersonalAccessTokenGroup).
Tests & test infra
pkg/connector/org_role_test.go, pkg/connector/org_test.go, pkg/connector/repository_test.go, pkg/connector/team_test.go, pkg/connector/user_test.go, pkg/connector/session_helper_test.go
Updated tests to pass SyncOpAttrs and consume SyncOpResults (NextPageToken, Annotations). Added noOpSessionStore test helper implementing sessions.SessionStore. Updated pagination/assertion logic to use unified results object.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

"I nibbled tokens, thread by thread,
Synced the hops where pages led.
Sessions keep the org-name warm,
Lambda wakes a new auth form.
A rabbit claps—deploy with zest!" 🥕🐇

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.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 PR title describes the main objective of adding containerization support, which aligns with the extensive refactoring to support Lambda/containerized deployment patterns.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ben/CE235_containerize_github

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: 1

🤖 Fix all issues with AI agents
In `@pkg/connector/connector.go`:
- Around line 256-281: The function NewLambdaConnector currently accepts an
unused cliOpts parameter; either remove cliOpts from the signature and all
callers (and update any docs/tests), or wire cliOpts into the auth flow so
Lambda honors CLI-provided options (e.g. consult cliOpts.TokenSource and
cliOpts.SelectedAuthMethod to override ghc.Token/ghc.AppId/ghc.AppPrivatekeyPath
before calling New(ctx, ghc, privateKey)). If the omission is intentional, add a
clear comment above NewLambdaConnector stating that Lambda-only connectors
intentionally ignore CLI options and why; otherwise update the code paths that
construct ghc or pass credentials so New and its callers receive the CLI-auth
values.

Comment on lines 256 to 279
func NewLambdaConnector(ctx context.Context, ghc *cfg.Github, cliOpts *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error) {
// TODO(Ben.Su) Update to use field groups rather than schema relationships.
if err := field.Validate(cfg.Config, ghc); err != nil {
return nil, nil, err
}

var (
l = ctxzap.Extract(ctx)
privateKey string
)
if ghc.AppPrivatekeyPath != "" {
keyBytes, err := os.ReadFile(ghc.AppPrivatekeyPath)
if err != nil {
l.Error("error reading app private key file", zap.Error(err), zap.String("appPrivateKeyPath", ghc.AppPrivatekeyPath))
return nil, nil, fmt.Errorf("failed to read app private key file: %w", err)
}
privateKey = string(keyBytes)
}

cb, err := New(ctx, ghc, privateKey)
if err != nil {
l.Error("error creating connector", zap.Error(err))
return nil, nil, err
}
return cb, nil, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: @jugonzalez12 added auth method to connector opts, so having the field group should simplify this logic

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor

@btipling btipling left a comment

Choose a reason for hiding this comment

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

Looks fine but I'm not sure if the org cache should be via the session store. It always fetches the org when it doesn't already exist, it persists it on the connector struct, not global memory and I think this is faster than always getting from the session store but I defer to Lauren or Matt on this.

Copy link
Contributor

@jugonzalez12 jugonzalez12 left a comment

Choose a reason for hiding this comment

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

modulo question about session store

@@ -0,0 +1,39 @@
package connector
Copy link
Contributor Author

Choose a reason for hiding this comment

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

use for testing.

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: 1

🤖 Fix all issues with AI agents
In `@pkg/connector/org_role_test.go`:
- Around line 65-72: The loop should fail fast when
bag.Unmarshal(results.NextPageToken) returns an error to avoid processing the
same invalid token repeatedly; replace the current t.Error(err) handling in the
loop that processes results.NextPageToken (around
test.AssertHasRatelimitAnnotations and bag.Unmarshal) with a failing call (e.g.,
t.Fatalf or t.Fatal with the error) so the test exits immediately on unmarshal
failure rather than continuing the loop.

Comment on lines +65 to 72
test.AssertHasRatelimitAnnotations(t, results.Annotations)
if results.NextPageToken == "" {
break
}

err = bag.Unmarshal(nextToken)
err = bag.Unmarshal(results.NextPageToken)
if err != nil {
t.Error(err)
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

🌐 Web query:

baton-sdk pagination Bag Unmarshal error handling

💡 Result:

In baton-sdk, pagination tokens are typically managed with pagination.Bag, which is a stack of PageState values that can be serialized/deserialized into the pageToken string you receive on subsequent calls. Bag exposes Push/Pop/Current, PageToken(), and helpers like Next() / NextToken() plus Marshal() / Unmarshal(). [1]

Typical pagination flow (what to do, and where Unmarshal errors come from)

  1. First page: pageToken == "" (or absent) → create a new Bag, Push() an initial PageState (resource type/id + any initial upstream token), then fetch.
  2. Next pages: pageToken != ""bag.Unmarshal(pageToken) to restore state, then call your upstream API using bag.PageToken() (the “cursor” stored in the current PageState). [1]
  3. After fetching a page:
    • If upstream returned a next cursor, advance the bag with bag.Next(nextCursor) (or NextToken(nextCursor) if you want the new marshaled token in one step). [1]

A Bag.Unmarshal error almost always means the incoming pageToken string is not a token previously produced by Bag.Marshal() / NextToken(), e.g. it’s truncated, from a different connector/version, manually edited, or otherwise malformed. PageState is the underlying structure being restored (token, type, id). [1]

Error handling guidance

  • Treat Unmarshal failures as an invalid pagination token and fail the request (don’t silently restart from page 1, or you can loop/duplicate data).
  • Return a clear error that includes context (“invalid page token”), and optionally suggest rerunning from the beginning.
  • If you’re converting from the Baton “token object” pattern, note there’s also ErrNoToken used when a pagination token object is unexpectedly nil. [1]

Minimal Go sketch

bag := &pagination.Bag{}

if pageToken != "" {
  if err := bag.Unmarshal(pageToken); err != nil {
    return nil, fmt.Errorf("invalid page token: %w", err)
  }
} else {
  bag.Push(pagination.PageState{
    ResourceTypeID: resourceTypeID,
    ResourceID:     resourceID,
    Token:          "", // initial upstream cursor if any
  })
}

// use bag.PageToken() when calling the upstream API
// ...
// if upstreamNextCursor != "":
_ = bag.Next(upstreamNextCursor) // or bag.NextToken(...)
outToken, _ := bag.Marshal()

Connector-dev docs also call out that baton-sdk includes pagination helpers for API calls, which is where this pattern fits. [2]

Sources:
[1] baton-sdk pkg/pagination package docs (Bag/GenBag APIs, ErrNoToken, PageState fields) (pkg.go.dev)
[2] ConductorOne developer docs: baton-sdk includes pagination helpers (conductorone.com)

Citations:


🏁 Script executed:

# Check the pagination loop structure and context around the error handling
cd pkg/connector && head -n 80 org_role_test.go | tail -n 40

Repository: ConductorOne/baton-github

Length of output: 966


🏁 Script executed:

# Search for other usages of Bag.Unmarshal in the codebase to see common patterns
rg "Unmarshal.*NextPageToken|bag\.Unmarshal" -A 3 -B 2

Repository: ConductorOne/baton-github

Length of output: 1166


🏁 Script executed:

# Check imports to understand what 'bag' is and where it comes from
head -n 30 pkg/connector/org_role_test.go

Repository: ConductorOne/baton-github

Length of output: 997


Fail fast on next-page token unmarshal errors to avoid test hangs.

t.Error logs the error but allows the loop to continue. If Unmarshal fails on an invalid token, the bag state remains unchanged, potentially causing the same token to be processed repeatedly in the next iteration. Per baton-sdk guidance, unmarshal failures indicate an invalid pagination token and should fail the request immediately.

💡 Suggested fix
 			err = bag.Unmarshal(results.NextPageToken)
-			if err != nil {
-				t.Error(err)
-			}
+			require.NoError(t, err)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test.AssertHasRatelimitAnnotations(t, results.Annotations)
if results.NextPageToken == "" {
break
}
err = bag.Unmarshal(nextToken)
err = bag.Unmarshal(results.NextPageToken)
if err != nil {
t.Error(err)
test.AssertHasRatelimitAnnotations(t, results.Annotations)
if results.NextPageToken == "" {
break
}
err = bag.Unmarshal(results.NextPageToken)
require.NoError(t, err)
🤖 Prompt for AI Agents
In `@pkg/connector/org_role_test.go` around lines 65 - 72, The loop should fail
fast when bag.Unmarshal(results.NextPageToken) returns an error to avoid
processing the same invalid token repeatedly; replace the current t.Error(err)
handling in the loop that processes results.NextPageToken (around
test.AssertHasRatelimitAnnotations and bag.Unmarshal) with a failing call (e.g.,
t.Fatalf or t.Fatal with the error) so the test exits immediately on unmarshal
failure rather than continuing the loop.

@kans
Copy link
Contributor

kans commented Feb 4, 2026

@btipling
The connector store uses an otter cache locally, in the connector layer so it will avoid round trips. Caching and refetching data, managed locally, is fine as long as doing so is thread safe.

}

func NewLambdaConnector(ctx context.Context, ghc *cfg.Github, cliOpts *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error) {
// TODO(Ben.Su) Update to use field groups rather than schema relationships.
Copy link
Contributor

Choose a reason for hiding this comment

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

lmk when this change is addressed

Comment on lines 256 to 279
func NewLambdaConnector(ctx context.Context, ghc *cfg.Github, cliOpts *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error) {
// TODO(Ben.Su) Update to use field groups rather than schema relationships.
if err := field.Validate(cfg.Config, ghc); err != nil {
return nil, nil, err
}

var (
l = ctxzap.Extract(ctx)
privateKey string
)
if ghc.AppPrivatekeyPath != "" {
keyBytes, err := os.ReadFile(ghc.AppPrivatekeyPath)
if err != nil {
l.Error("error reading app private key file", zap.Error(err), zap.String("appPrivateKeyPath", ghc.AppPrivatekeyPath))
return nil, nil, fmt.Errorf("failed to read app private key file: %w", err)
}
privateKey = string(keyBytes)
}

cb, err := New(ctx, ghc, privateKey)
if err != nil {
l.Error("error creating connector", zap.Error(err))
return nil, nil, err
}
return cb, nil, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: @jugonzalez12 added auth method to connector opts, so having the field group should simplify this logic

)

const (
GithubAppGroup = "github_app_group"
Copy link
Contributor

Choose a reason for hiding this comment

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

style convention is usually with - for baton
and then you can map the group names in c1

Copy link
Contributor

@laurenleach laurenleach left a comment

Choose a reason for hiding this comment

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

main comment:

var Config = field.NewConfiguration(
[]field.SchemaField{
accessTokenField,
orgsField,
Copy link
Contributor

Choose a reason for hiding this comment

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

what does baton-github-test do? I think we need to add a new field for org, since github app only takes a string org, not a list

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: 1

🤖 Fix all issues with AI agents
In `@pkg/connector/connector.go`:
- Around line 256-280: NewLambdaConnector currently dereferences
cliOpts.SelectedAuthMethod without checking cliOpts, and treats an empty
SelectedAuthMethod as a GitHub App path which can mis-route PAT-capable configs;
add a nil-check for cliOpts and treat an empty SelectedAuthMethod as a fallback:
if cliOpts is nil set group by inspecting ghc for a PAT (e.g., presence of
ghc.PersonalAccessToken or equivalent) and choose
cfg.GithubPersonalAccessTokenGroup when PAT data exists, otherwise choose the
GitHub App group; then use that group to decide between calling newWithGithubPAT
and newWithGithubApp (preserve existing error handling and keep field.Validate
usage).
🧹 Nitpick comments (1)
pkg/config/config.go (1)

94-95: Minor: Inconsistent capitalization in HelpText.

Line 95 uses "github app" while other references use "GitHub App" (e.g., line 37: "GitHub App ID", line 43: "GitHub App private key"). Consider using consistent capitalization for brand names.

-		HelpText:    "Use a github app for authentication",
+		HelpText:    "Use a GitHub App for authentication.",

Comment on lines 256 to 279
func NewLambdaConnector(ctx context.Context, ghc *cfg.Github, cliOpts *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error) {
// TODO(Ben.Su) Update to use field groups rather than schema relationships.
if err := field.Validate(cfg.Config, ghc); err != nil {
return nil, nil, err
}

var (
appClient *github.Client
ts = oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: patToken},
)
group = cliOpts.SelectedAuthMethod
cb *GitHub
err error
)
if jwttoken != "" {
if len(ghc.Orgs) != 1 {
return nil, fmt.Errorf("github-connector: only one org should be specified")
}

appClient, err = newGitHubClient(ctx,
ghc.InstanceUrl,
oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: jwttoken},
),
)
if group == cfg.GithubPersonalAccessTokenGroup {
cb, err = newWithGithubPAT(ctx, ghc)
if err != nil {
return nil, err
}
installation, err := findInstallation(ctx, appClient, ghc.Orgs[0])
if err != nil {
return nil, err
return nil, nil, err
}
return cb, nil, nil
}

token, err := getInstallationToken(ctx, appClient, installation.GetID())
if err != nil {
return nil, err
}
cb, err = newWithGithubApp(ctx, ghc)
if err != nil {
return nil, nil, err
}
return cb, nil, nil
}
Copy link

@coderabbitai coderabbitai bot Feb 4, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against nil cliOpts / empty auth selection to avoid panic or wrong auth path.

Line 263 dereferences cliOpts without a nil check, which will panic if any caller passes nil. Also, an empty SelectedAuthMethod currently defaults to GitHub App even when a PAT is present, which can route valid PAT configs into the wrong flow.

🛡️ Proposed guard + fallback selection
-	var (
-		group = cliOpts.SelectedAuthMethod
-		cb    *GitHub
-		err   error
-	)
+	var (
+		group string
+		cb    *GitHub
+		err   error
+	)
+	if cliOpts != nil {
+		group = cliOpts.SelectedAuthMethod
+	}
+	if group == "" {
+		if ghc.Token != "" {
+			group = cfg.GithubPersonalAccessTokenGroup
+		} else {
+			group = cfg.GithubAppGroup
+		}
+	}

To confirm call-site behavior, please verify whether cliOpts can be nil or SelectedAuthMethod can be empty:

#!/bin/bash
# Identify call sites and check how cliOpts is passed/initialized.
rg -n "NewLambdaConnector\\(" -C 3
rg -n "SelectedAuthMethod" -C 3
🤖 Prompt for AI Agents
In `@pkg/connector/connector.go` around lines 256 - 280, NewLambdaConnector
currently dereferences cliOpts.SelectedAuthMethod without checking cliOpts, and
treats an empty SelectedAuthMethod as a GitHub App path which can mis-route
PAT-capable configs; add a nil-check for cliOpts and treat an empty
SelectedAuthMethod as a fallback: if cliOpts is nil set group by inspecting ghc
for a PAT (e.g., presence of ghc.PersonalAccessToken or equivalent) and choose
cfg.GithubPersonalAccessTokenGroup when PAT data exists, otherwise choose the
GitHub App group; then use that group to decide between calling newWithGithubPAT
and newWithGithubApp (preserve existing error handling and keep field.Validate
usage).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's not nil.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all call sites of NewLambdaConnector to verify cliOpts is always non-nil
rg -nP '\bNewLambdaConnector\s*\(' -A 5 -B 2

# Search for SelectedAuthMethod initialization and usage patterns
rg -nP '\bSelectedAuthMethod\b' -C 3

# Check ConnectorOpts struct definition to understand the field
rg -nP 'type\s+ConnectorOpts\s+struct' -A 10

Length of output: 800


@Bencheng21, thanks for confirming that cliOpts is guaranteed non-nil by the SDK's calling convention—that addresses the panic concern.

However, there's a second issue in the original review that remains: what happens when SelectedAuthMethod is an empty string?

Looking at the current logic (lines 262-270):

group = cliOpts.SelectedAuthMethod
if group == cfg.GithubPersonalAccessTokenGroup {
    cb, err = newWithGithubPAT(ctx, ghc)
    // ...
}
// Falls through to GitHub App
cb, err = newWithGithubApp(ctx, ghc)

If SelectedAuthMethod is "", it won't match GithubPersonalAccessTokenGroup, so the code will route to the GitHub App path at line 270. But your config defines the PAT group as the default (Default: true on line 97), so an empty selection should arguably choose PAT, not App.

Can SelectedAuthMethod ever be empty in practice? If so, should the code explicitly handle that case to align with the default group configuration?


🧬 Code Graph Analysis Results
  • File: pkg/customclient/client.go

    • Lines 15-17
      Client struct {
      	*uhttp.BaseHttpClient
      }
      
    • Lines 19-23
      func New(client *github.Client) *Client {
      	return &Client{
      		BaseHttpClient: uhttp.NewBaseHttpClient(client.Client()),
      	}
      }
      
  • File: pkg/config/conf.gen.go

    • Lines 6-16
      Github struct {
      	Token string `mapstructure:"token"`
      	Orgs []string `mapstructure:"orgs"`
      	Enterprises []string `mapstructure:"enterprises"`
      	InstanceUrl string `mapstructure:"instance-url"`
      	SyncSecrets bool `mapstructure:"sync-secrets"`
      	OmitArchivedRepositories bool `mapstructure:"omit-archived-repositories"`
      	AppId string `mapstructure:"app-id"`
      	AppPrivatekeyPath string `mapstructure:"app-privatekey-path"`
      	Org string `mapstructure:"org"`
      }
      
  • File: pkg/config/config.go

    • Lines 75-107

      Config = field.NewConfiguration(
      	[]field.SchemaField{
      		accessTokenField,
      		orgsField,
      		EnterprisesField,
      		instanceUrlField,
      		syncSecrets,
      		omitArchivedRepositories,
      		appIDField,
      		appPrivateKeyPath,
      		orgField,
      	},
      	field.WithConstraints(fieldRelationships...),
      	field.WithConnectorDisplayName("GitHub v2"),
      	field.WithHelpUrl("/docs/baton/github-v2"),
      	field.WithIconUrl("/static/app-icons/github.svg"),
      	field.WithFieldGroups([]field.SchemaFieldGroup{
      		{
      			Name:        GithubPersonalAccessTokenGroup,
      			DisplayName: "Personal access token",
      			HelpText:    "Use a personal access token for authentication.",
      			Fields:      []field.SchemaField{accessTokenField, orgsField, omitArchivedRepositories},
      			Default:     true,
      		},
      		{
      			Name:        GithubAppGroup,
      			DisplayName: "GitHub app",
      			HelpText:    "Use a github app for authentication",
      			Fields:      []field.SchemaField{appIDField, appPrivateKeyPath, orgField, syncSecrets, omitArchivedRepositories},
      			Default:     false,
      		},
      	}),
      )
      
    • Lines 8-8

      GithubAppGroup                 = "github-app-group"
      

Copy link
Contributor Author

Choose a reason for hiding this comment

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

falls back to the other authentication

Copy link

Choose a reason for hiding this comment

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

@Bencheng21, thanks for the confirmation on cliOpts.

Regarding your comment "falls back to the other authentication"—can you clarify what you mean?

Looking at the code flow:

  • Line 263: if group == cfg.GithubPersonalAccessTokenGroup → uses PAT
  • Line 271: Otherwise (including when group == "") → uses GitHub App

Question: When SelectedAuthMethod is empty (e.g., user doesn't explicitly select an auth method), does the SDK/CLI layer automatically populate it with the default group value (GithubPersonalAccessTokenGroup since Default: true on line 97)?

Or are you saying that when SelectedAuthMethod is empty, falling back to the GitHub App path is the intended behavior, even though PAT is marked as the default in the field group configuration?

If the SDK ensures SelectedAuthMethod is always populated with the default, then the empty-string case won't occur in practice and the current code is fine. But if empty is possible, we should verify the fallback aligns with the field group defaults.

🐰

DisplayName: "GitHub app",
HelpText: "Use a github app for authentication",
Fields: []field.SchemaField{appIDField, appPrivateKeyPath, orgsField, syncSecrets, omitArchivedRepositories},
Default: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

we should set the default to GithubPersonalAccessTokenGroup since app was added later so there are probably existing configs with no group since there were no field groups previously

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: 2

🤖 Fix all issues with AI agents
In `@pkg/connector/connector.go`:
- Around line 340-360: The code stores a request Context in appTokenRefresher
which can become stale; instead remove the ctx field from appTokenRefresher and
ensure token refreshes use a long‑lived context (e.g. context.Background()).
Concretely: update the appTokenRefresher type to drop the ctx member, update its
Token refresh implementation to use context.Background() (or accept a context
parameter during the actual refresh call), and change the
oauth2.ReuseTokenSource construction (the appTokenRefresher instantiation) to no
longer pass ctx but rely on the background context for long‑lived token
refreshes; keep references to appTokenRefresher, oauth2.ReuseTokenSource, and
appJWTTokenRefresher consistent.
- Around line 282-305: newWithGithubPAT may receive an empty personal access
token (ghc.Token), which creates an oauth2 client with no credentials and yields
confusing downstream failures; add a defensive validation at the start of
newWithGithubPAT that returns a clear error when ghc.Token == "" (e.g.,
fmt.Errorf("github personal access token is required")) so callers fail fast
with an actionable message; keep the check before creating
oauth2.StaticTokenSource and reference the function name newWithGithubPAT and
the ghc.Token field when implementing this change.

Comment on lines +282 to +304
func newWithGithubPAT(ctx context.Context, ghc *cfg.Github) (*GitHub, error) {
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: ghc.Token},
)
ghClient, err := newGitHubClient(ctx, ghc.InstanceUrl, ts)
if err != nil {
return nil, err
}
graphqlClient, err := newGitHubGraphqlClient(ctx, ghc.InstanceUrl, ts)
if err != nil {
return nil, err
}
return &GitHub{
client: ghClient,
customClient: customclient.New(ghClient),
instanceURL: ghc.InstanceUrl,
orgs: ghc.Orgs,
enterprises: ghc.Enterprises,
graphqlClient: graphqlClient,
orgCache: newOrgNameCache(ghClient),
syncSecrets: ghc.SyncSecrets,
omitArchivedRepositories: ghc.OmitArchivedRepositories,
}, 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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the Token field definition and any validation rules
rg -n "Token" pkg/config/ -B 2 -A 2 | head -100

Repository: ConductorOne/baton-github

Length of output: 1915


🏁 Script executed:

#!/bin/bash
# Find field.Validate to understand what it validates
rg -n "field\.Validate|func.*Validate" pkg/ -B 2 -A 5 --max-count=50

Repository: ConductorOne/baton-github

Length of output: 1203


🏁 Script executed:

#!/bin/bash
# Find where newWithGithubPAT is called
rg -n "newWithGithubPAT" pkg/ -B 3 -A 3

Repository: ConductorOne/baton-github

Length of output: 789


🏁 Script executed:

#!/bin/bash
# Look at the GitHub config structure
rg -n "type Github struct" pkg/config/ -A 20

Repository: ConductorOne/baton-github

Length of output: 1332


🏁 Script executed:

#!/bin/bash
# Look at the full accessTokenField definition
rg -n "accessTokenField\s*=" pkg/config/config.go -A 10

Repository: ConductorOne/baton-github

Length of output: 480


🏁 Script executed:

#!/bin/bash
# Check what field.Validate does and if it validates required fields
rg -n "func.*Validate.*field|WithRequired" . --type go | head -30

Repository: ConductorOne/baton-github

Length of output: 1912


🏁 Script executed:

#!/bin/bash
# Check field group logic - is one of the groups required?
rg -n "WithFieldGroups|Default.*true" pkg/config/config.go -B 2 -A 2

Repository: ConductorOne/baton-github

Length of output: 489


🏁 Script executed:

#!/bin/bash
# Look for field.Validate implementation to understand what it validates
rg -n "func.*Validate.*cfg\.|func Validate" vendor/github.com/conductorone/baton-sdk/pkg/field/ -A 15 | head -60

Repository: ConductorOne/baton-github

Length of output: 5606


🏁 Script executed:

#!/bin/bash
# Check if there's any validation in NewLambdaConnector after field.Validate call
rg -n "field.Validate" pkg/connector/connector.go -A 20 | head -50

Repository: ConductorOne/baton-github

Length of output: 539


🏁 Script executed:

#!/bin/bash
# Search for any other validation of Token field in the codebase
rg -n "ghc\.Token|\.Token\s*=|\.Token\s*==|Token.*empty" pkg/ --type go

Repository: ConductorOne/baton-github

Length of output: 260


🏁 Script executed:

#!/bin/bash
# Find the Validate function that takes (config, instance) parameters
rg -n "^func Validate\(" vendor/github.com/conductorone/baton-sdk/pkg/field/ -A 30

Repository: ConductorOne/baton-github

Length of output: 3154


🏁 Script executed:

#!/bin/bash
# Look at how field groups are validated and if one must have required fields filled
rg -n "FieldsMutuallyExclusive|field.*group|ValidateRequired" vendor/github.com/conductorone/baton-sdk/pkg/field/ -A 5 -B 2 | head -80

Repository: ConductorOne/baton-github

Length of output: 4417


Add validation for non-empty personal access token.

The Token field in accessTokenField is not marked with WithRequired(true), so field.Validate will not catch an empty token. An empty token can reach this function, creating an OAuth2 client with an empty access token, leading to authentication failures with unclear error messages downstream. A defensive check here provides clearer error context immediately.

Suggested fix
 func newWithGithubPAT(ctx context.Context, ghc *cfg.Github) (*GitHub, error) {
+	if ghc.Token == "" {
+		return nil, errors.New("personal access token is required")
+	}
 	ts := oauth2.StaticTokenSource(
 		&oauth2.Token{AccessToken: ghc.Token},
 	)
🤖 Prompt for AI Agents
In `@pkg/connector/connector.go` around lines 282 - 305, newWithGithubPAT may
receive an empty personal access token (ghc.Token), which creates an oauth2
client with no credentials and yields confusing downstream failures; add a
defensive validation at the start of newWithGithubPAT that returns a clear error
when ghc.Token == "" (e.g., fmt.Errorf("github personal access token is
required")) so callers fail fast with an actionable message; keep the check
before creating oauth2.StaticTokenSource and reference the function name
newWithGithubPAT and the ghc.Token field when implementing this change.

Comment on lines 340 to 359
ts := oauth2.ReuseTokenSource(
&oauth2.Token{
AccessToken: token.GetToken(),
Expiry: token.GetExpiresAt().Time,
},
&appTokenRefresher{
ctx: ctx,
instanceURL: ghc.InstanceUrl,
installationID: installation.GetID(),
jwtTokenSource: oauth2.ReuseTokenSource(
&oauth2.Token{
AccessToken: jwttoken,
Expiry: time.Now().Add(jwtExpiryTime),
},
&appJWTTokenRefresher{
appID: ghc.AppId,
privateKey: string(keyBytes),
},
),
},
)
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

Storing context.Context in struct can cause stale context issues.

The appTokenRefresher struct stores ctx at construction time (line 346), but token refresh may occur much later when this context could be cancelled or stale. The oauth2.ReuseTokenSource may call Token() during any API request, potentially long after the original context has expired.

🛠️ Suggested approach: Pass context through or use background context for long-lived token sources

Consider either:

  1. Using context.Background() for the token refresher since it's long-lived, or
  2. Restructuring to pass context through the call chain
 	ts := oauth2.ReuseTokenSource(
 		&oauth2.Token{
 			AccessToken: token.GetToken(),
 			Expiry:      token.GetExpiresAt().Time,
 		},
 		&appTokenRefresher{
-			ctx:            ctx,
+			ctx:            context.Background(),
 			instanceURL:    ghc.InstanceUrl,
 			installationID: installation.GetID(),

Note: Using context.Background() loses cancellation propagation but avoids stale context. Evaluate the trade-off based on your token refresh requirements.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ts := oauth2.ReuseTokenSource(
&oauth2.Token{
AccessToken: token.GetToken(),
Expiry: token.GetExpiresAt().Time,
},
&appTokenRefresher{
ctx: ctx,
instanceURL: ghc.InstanceUrl,
installationID: installation.GetID(),
jwtTokenSource: oauth2.ReuseTokenSource(
&oauth2.Token{
AccessToken: jwttoken,
Expiry: time.Now().Add(jwtExpiryTime),
},
&appJWTTokenRefresher{
appID: ghc.AppId,
privateKey: string(keyBytes),
},
),
},
)
ts := oauth2.ReuseTokenSource(
&oauth2.Token{
AccessToken: token.GetToken(),
Expiry: token.GetExpiresAt().Time,
},
&appTokenRefresher{
ctx: context.Background(),
instanceURL: ghc.InstanceUrl,
installationID: installation.GetID(),
jwtTokenSource: oauth2.ReuseTokenSource(
&oauth2.Token{
AccessToken: jwttoken,
Expiry: time.Now().Add(jwtExpiryTime),
},
&appJWTTokenRefresher{
appID: ghc.AppId,
privateKey: string(keyBytes),
},
),
},
)
🤖 Prompt for AI Agents
In `@pkg/connector/connector.go` around lines 340 - 360, The code stores a request
Context in appTokenRefresher which can become stale; instead remove the ctx
field from appTokenRefresher and ensure token refreshes use a long‑lived context
(e.g. context.Background()). Concretely: update the appTokenRefresher type to
drop the ctx member, update its Token refresh implementation to use
context.Background() (or accept a context parameter during the actual refresh
call), and change the oauth2.ReuseTokenSource construction (the
appTokenRefresher instantiation) to no longer pass ctx but rely on the
background context for long‑lived token refreshes; keep references to
appTokenRefresher, oauth2.ReuseTokenSource, and appJWTTokenRefresher consistent.

@Bencheng21 Bencheng21 force-pushed the ben/CE235_containerize_github branch from 9f4c618 to e449202 Compare February 4, 2026 21:36
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/connector/repository_test.go (1)

65-69: ⚠️ Potential issue | 🟡 Minor

Fail fast on pagination token unmarshal errors to prevent test hangs.

Using t.Error logs the error but allows the loop to continue. If Unmarshal fails, the bag state remains unchanged, causing the same token to be processed repeatedly. Use require.NoError to fail immediately.

💡 Proposed fix
 			err = bag.Unmarshal(results.NextPageToken)
-			if err != nil {
-				t.Error(err)
-			}
+			require.NoError(t, err)
pkg/connector/enterprise_role.go (1)

42-52: ⚠️ Potential issue | 🟠 Major

Potential race condition in cache initialization check.

The len(o.roleUsersCache) == 0 check at line 43 is performed outside the mutex lock, which could lead to a race condition where multiple goroutines simultaneously determine the cache is empty and call fillCache concurrently.

🔒 Proposed fix to make the check thread-safe
 func (o *enterpriseRoleResourceType) getRoleUsersCache(ctx context.Context) (map[string][]string, error) {
+	o.mu.Lock()
+	isEmpty := len(o.roleUsersCache) == 0
+	o.mu.Unlock()
+
-	if len(o.roleUsersCache) == 0 {
+	if isEmpty {
 		if err := o.fillCache(ctx); err != nil {
 			return nil, fmt.Errorf("baton-github: error caching user roles: %w", err)
 		}
 	}
 
 	o.mu.Lock()
 	defer o.mu.Unlock()
 	return o.roleUsersCache, nil
 }

Alternatively, consider using sync.Once for initialization:

 type enterpriseRoleResourceType struct {
 	resourceType   *v2.ResourceType
 	client         *github.Client
 	customClient   *customclient.Client
 	enterprises    []string
 	roleUsersCache map[string][]string
 	mu             *sync.Mutex
+	cacheOnce      sync.Once
+	cacheErr       error
 }
🤖 Fix all issues with AI agents
In `@pkg/connector/api_token.go`:
- Around line 114-121: The loop over tokens calls apiTokenResource(ctx, t) and
on error currently returns a non-nil resourceSdk.SyncOpResults together with
err; change this to match the file's standard pattern by returning (nil, nil,
err) when apiTokenResource fails. Locate the for _, t := range tokens loop in
pkg/connector/api_token.go and update the error branch that references
apiTokenResource, pageToken and annotations so it returns nil, nil, err instead
of returning a partial SyncOpResults.
🧹 Nitpick comments (1)
pkg/connector/user.go (1)

103-135: Inconsistent PerPage value usage.

The ListMembersOptions at line 127-131 uses maxPageSize constant, while other resource types (e.g., api_token.go line 90, invitation.go line 68) use opts.PageToken.Size. This inconsistency could lead to different pagination behaviors across resource types.

♻️ Proposed fix for consistency
 	listOpts := github.ListMembersOptions{
 		ListOptions: github.ListOptions{
 			Page:    page,
-			PerPage: maxPageSize,
+			PerPage: opts.PageToken.Size,
 		},
 	}

Comment on lines 114 to 121
for _, t := range tokens {
resource, err := apiTokenResource(ctx, t)
if err != nil {
return nil, pageToken, annotations, err
return nil, &resourceSdk.SyncOpResults{
NextPageToken: pageToken,
Annotations: annotations,
}, err
}
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

Inconsistent error handling pattern.

When apiTokenResource fails within the loop, this code returns a non-nil SyncOpResults along with the error. This differs from all other error paths in this file and other resource types, which return (nil, nil, err). Returning partial results with an error can cause unexpected behavior in callers that may not expect both a result and an error.

🐛 Proposed fix for consistent error handling
 	for _, t := range tokens {
 		resource, err := apiTokenResource(ctx, t)
 		if err != nil {
-			return nil, &resourceSdk.SyncOpResults{
-				NextPageToken: pageToken,
-				Annotations:   annotations,
-			}, err
+			return nil, nil, err
 		}
 		rv = append(rv, resource)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, t := range tokens {
resource, err := apiTokenResource(ctx, t)
if err != nil {
return nil, pageToken, annotations, err
return nil, &resourceSdk.SyncOpResults{
NextPageToken: pageToken,
Annotations: annotations,
}, err
}
for _, t := range tokens {
resource, err := apiTokenResource(ctx, t)
if err != nil {
return nil, nil, err
}
rv = append(rv, resource)
}
🤖 Prompt for AI Agents
In `@pkg/connector/api_token.go` around lines 114 - 121, The loop over tokens
calls apiTokenResource(ctx, t) and on error currently returns a non-nil
resourceSdk.SyncOpResults together with err; change this to match the file's
standard pattern by returning (nil, nil, err) when apiTokenResource fails.
Locate the for _, t := range tokens loop in pkg/connector/api_token.go and
update the error branch that references apiTokenResource, pageToken and
annotations so it returns nil, nil, err instead of returning a partial
SyncOpResults.

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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/config/config.go (1)

14-74: ⚠️ Potential issue | 🟠 Major

Remove unconditional WithRequired(true) and use field constraints to gate required validation by auth group.

Lines 19, 40, 49, and 66 mark PAT and App authentication fields as unconditionally required. In baton-sdk, WithRequired(true) applies regardless of field group membership, so users selecting GitHub App auth will still be required to provide a personal access token (and vice versa), causing validation failures.

Replace individual WithRequired(true) with conditional constraints: use FieldsDependentOn to require fields only when their parent auth group is selected. For example: "if App group is active, then appIDField, appPrivateKeyPath, and orgField are required."

(The TODO on line 12 confirms this issue was previously flagged as uncertain.)

accessTokenField,
appPrivateKeyPath,
),
field.FieldsRequiredTogether(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I think we can just remove this FieldsRequiredTogether now that we mark them as required in the groups. These change how the UI looks now

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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/config/config.go (1)

62-98: ⚠️ Potential issue | 🟡 Minor

Tighten GitHub capitalization in user-facing copy.
Minor copy fix for consistency in UI/help text.

✍️ Suggested copy edits
-	field.WithDisplayName("Github App Organization"),
-	field.WithDescription("Organization of your github app"),
+	field.WithDisplayName("GitHub App Organization"),
+	field.WithDescription("Organization of your GitHub app"),
@@
-			HelpText:    "Use a github app for authentication",
+			HelpText:    "Use a GitHub app for authentication",

Copy link
Contributor

@laurenleach laurenleach left a comment

Choose a reason for hiding this comment

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

main comm

@Bencheng21 Bencheng21 merged commit 8fefb8f into main Feb 5, 2026
12 of 15 checks passed
@Bencheng21 Bencheng21 deleted the ben/CE235_containerize_github branch February 5, 2026 00:52
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.

5 participants