Skip to content

Reduce org ID friction: add chunk org list and auto-detect org ID in init - #462

Open
schurchleycci wants to merge 4 commits into
mainfrom
org-id-friction
Open

Reduce org ID friction: add chunk org list and auto-detect org ID in init#462
schurchleycci wants to merge 4 commits into
mainfrom
org-id-friction

Conversation

@schurchleycci

@schurchleycci schurchleycci commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Sidecar commands need a CircleCI org ID, but there was no way to discover or capture it through the CLI — users either knew it already or got blocked. This PR closes that gap.

chunk init now auto-detects the org ID after writing the project config. Single-org users get it picked silently; multi-org users see a picker. The ID is saved to .chunk/config.json so sidecar commands just work from that point on.

For cases where init isn't an option, chunk org list provides a discovery command. The orgPicker error message now points to it, and the sidecar skill uses it instead of stopping to ask the user.

Test plan

  • chunk init while authenticated to one org — org ID auto-selected and written to .chunk/config.json
  • chunk init while authenticated to multiple orgs — picker appears, selected ID written
  • chunk init while not authenticated — completes without error, no org ID written
  • chunk init in a non-interactive environment (no TTY) — completes without error or spurious warning
  • chunk init --skip-org-id — org ID detection step skipped
  • chunk org list — orgs printed in table form; --json returns JSON
  • chunk sidecar create with single-org account and no orgID in config — org auto-selected, no picker

🤖 Generated with Claude Code

schurchleycci and others added 2 commits July 29, 2026 13:44
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `chunk org list` command (unhides the org group, adds list subcommand with --json flag)
- Auto-select org in orgPicker when user belongs to exactly one org
- Improve ErrNoTTY message to suggest `chunk org list` + `chunk config set`
- Update docs: chunk init now captures org ID; manual path uses `chunk org list`
- Update chunk-sidecar skill to use `chunk org list` for org discovery instead of stopping to ask the user

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@schurchleycci schurchleycci changed the title Detect org ID during chunk init Reduce org ID friction: add chunk org list and auto-detect org ID in init Jul 29, 2026

@hanabel1 hanabel1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i think this could use some automated tests tho for (detectOrgID and orgPicker)

@schurchleycci

Copy link
Copy Markdown
Contributor Author

i think this could use some automated tests tho for (detectOrgID and orgPicker)

Good call, I'll add these before merging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread internal/cmd/init.go
// interactive with multiple orgs. Skipped gracefully on auth failure, no TTY,
// or cancellation — none of these are fatal for chunk init.
func detectOrgID(ctx context.Context, rc config.ResolvedConfig, streams iostream.Streams, cfg *config.ProjectConfig) {
client, err := authprompt.ResolveCircleCIClient(rc, nil)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ResolveCircleCIClient returns ErrNeedsAuth for missing creds, but a genuine failure out of circleci.NewClient (bad base URL, etc.) gets swallowed here too. Gate the silent return on errors.Is(err, authprompt.ErrNeedsAuth) and warn otherwise?

Comment thread internal/cmd/init.go
}
}

// Step 3: CircleCI org ID

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Step numbering collides — there's already a // Step 3: Write hook config files further down.

Comment thread internal/cmd/init.go Outdated

// Step 3: CircleCI org ID
if !skipOrgID && cfg.OrgID == "" {
rc, err := config.Resolve("", "", insecureStorage)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Step 2 does rc, _ := config.Resolve(...) and ignores the error; the identical failure here prints a warning. Resolve once above both steps and handle the error consistently?

Comment thread internal/cmd/org.go
var jsonOut bool

cmd := &cobra.Command{
Use: "list",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No coverage for org list — table output, --json, and the empty case are all untested. org_test.go only exercises create.

Comment thread internal/cmd/org.go Outdated

client, err := ensureCircleCIClient(cmd.Context(), cmd, rc, io, tui.PromptHidden)
if err != nil {
return &userError{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This throws away the userError ensureCircleCIClient already built (code auth.circleci_token_required, ExitAuthError) and relabels oauth/network failures as auth failures. Returning err unwrapped keeps the exit code — matters more now the skill branches on "error means not authenticated".

Comment thread internal/cmd/org.go Outdated
}

if jsonOut {
enc := json.NewEncoder(io.Out)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Every other JSON path goes through iostream.PrintJSON (sidecar.go:180, config.go:72) — any reason to hand-roll the encoder here? Also a nil collabs encodes as null rather than [], which is awkward for anything parsing it.

}

func TestResolveOrgID_ConfigOrgID(t *testing.T) {
t.Setenv(config.EnvCircleCIOrgID, "env-org")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Name says ConfigOrgID but this sets the env var — same path as TestResolveOrgID_FallsBackToPickOrg's sibling. The .chunk/config.json branch of config.ResolveOrgID is still uncovered.

Comment thread skills/chunk-sidecar/SKILL.md Outdated
3. If **both** are unset, run `chunk org list` to discover available orgs:
- **Single org returned** — use that ID automatically: `chunk config set orgID <id>`, then continue to Step 2.
- **Multiple orgs returned** — show the list to the user and ask which org to use **exactly once**. After they reply, run `chunk config set orgID <id>`, then continue to Step 2.
- **Error or empty list** — the user is not authenticated or has no orgs. Stop and ask them to run `chunk auth set circleci` first.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Empty list exits 0 with the warning on stderr, so the agent sees success with no rows rather than an error. Point this step at chunk org list --json so there's something deterministic to parse?

Comment thread internal/cmd/init.go
if err != nil {
return
}
orgID, err := orgPicker(ctx, client)()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why go via the collaborations picker at all here? Step 1 has already detected the VCS org/repo from the git remote, and GET /api/v3/orgs?filter[slug]=<provider>/<org> resolves a slug straight to the org UUID (public-api-service v3/api.go:118). circleci-cli does exactly this in internal/apiclient/org.go:54 (ResolveOrgID), as does circleci/mcp (client/pas/org.go GetOrgBySlug).

That would be deterministic rather than a guess: no picker, no TTY dependency, correct for multi-org users, and it picks the org that actually owns this repo instead of the first collaboration in the list. The empty-list case maps to "not on CircleCI" and can stay a soft skip.

internal/circleci/projects.go:59 GetProjectBySlug also already returns org_id and is currently unused — either route gets there without a picker.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On the plumbing cost: v3 responses are JSON:API-enveloped ({"data": [...], "page": {...}}), and chunk-cli's only v3 usage today is the sidecar surface, which isn't. circleci-cli already has the generic for it — internal/apiclient/client.go:154:

type v3List[T any] struct {
	Data []T `json:"data"`
	Page struct {
		Next *string `json:"next"`
		Prev *string `json:"prev"`
	} `json:"page"`
}

plus v3Entity[T] for single-resource responses (client.go:150), and filterParam(key, val)filter[key]=val (client.go:179). ResolveOrgID is then three lines on top of that.

Worth copying the same two generics + filterParam into internal/circleci rather than hand-unwrapping data at the call site — every future v3 endpoint needs them, and internal/httpcl already has QueryParam (request.go:66) so there's nothing else to build.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why go via the collaborations picker at all here? Step 1 has already detected the VCS org/repo from the git remote, and GET /api/v3/orgs?filter[slug]=<provider>/<org> resolves a slug straight to the org UUID (public-api-service v3/api.go:118). circleci-cli does exactly this in internal/apiclient/org.go:54 (ResolveOrgID), as does circleci/mcp (client/pas/org.go GetOrgBySlug).

That would be deterministic rather than a guess: no picker, no TTY dependency, correct for multi-org users, and it picks the org that actually owns this repo instead of the first collaboration in the list. The empty-list case maps to "not on CircleCI" and can stay a soft skip.

internal/circleci/projects.go:59 GetProjectBySlug also already returns org_id and is currently unused — either route gets there without a picker.

Does that work for standalone orgs or just classic ones?

- detectOrgID: warn on unexpected errors from ResolveCircleCIClient, not
  just ErrNeedsAuth
- init: resolve config.Resolve once above steps 2+3, handle error
  consistently; fix step numbering collision (Step 3 → Step 4 for hooks)
- org list: return err unwrapped from ensureCircleCIClient to preserve
  ExitAuthError exit code; use iostream.PrintJSON; normalize nil collabs
  to [] before JSON encoding
- auth: use providerCircleCI/providerAnthropic/providerGitHub constants in
  ValidArgs instead of repeating string literals (goconst lint fix)
- tests: add org list table, --json, empty, and auth coverage; rename
  TestResolveOrgID_ConfigOrgID → EnvVar and add ProjectConfig branch test
- SKILL.md: point org preflight at chunk org list --json for deterministic
  parsing; clarify empty-array condition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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