Skip to content

feat: support project-level profile binding#1504

Closed
ennann wants to merge 1 commit into
larksuite:mainfrom
ennann:codex/project-profile-config
Closed

feat: support project-level profile binding#1504
ennann wants to merge 1 commit into
larksuite:mainfrom
ennann:codex/project-profile-config

Conversation

@ennann

@ennann ennann commented Jun 17, 2026

Copy link
Copy Markdown

Summary

Adds project-local profile binding via .lark-cli/config.json, so commands can automatically use the profile selected for the current repository without requiring --profile on every invocation. Explicit --profile still takes precedence, and the project file stores only the profile name, not credentials or login state.

Supersedes #1502 with a cleaner single-commit branch and the finalized project config path.

Changes

  • Add profile bind <profile>, profile unbind, and profile current.
  • Resolve the nearest .lark-cli/config.json during bootstrap, stopping at the Git root.
  • Fail closed with typed config errors for malformed project config files and missing project profiles.
  • Surface project-profile-not-found errors through auth/config/credential paths instead of silently falling back.
  • Keep project config minimal and future-extensible; examples cover bytedance, team-prod, and lark-boe profile names.

Test Plan

  • go test ./cmd ./cmd/profile ./cmd/config ./cmd/auth ./internal/core ./internal/cmdutil ./internal/credential
  • go vet ./...
  • gofmt -l .
  • go mod tidy && git diff --exit-code -- go.mod go.sum
  • go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev=origin/main (0 issues; existing gocritic disabled-check warning only)
  • make build
  • Manual local verification with a temporary config and Git repo: profile bind team-prod, profile current, --profile bytedance profile current, and profile unbind
  • make unit-test currently fails in existing shortcuts/minutes download tests because example.com presigned download URLs are blocked as local/internal hosts; related packages and all new project-profile tests pass.

Related Issues

  • None

中文说明

这个 PR 增加项目级 profile 绑定能力。项目可以通过 .lark-cli/config.json 记录当前仓库默认使用的 profile,之后普通命令不需要重复传 --profile;显式传入 --profile 时仍然优先使用命令行参数。

项目配置文件只保存 profile 名称,不保存 app secret、token 或登录态。示例里包含 bytedanceteam-prodlark-boe 这类租户/环境命名,用来说明 profile 名称本身可以承载团队内部环境语义。

Summary by CodeRabbit

Release Notes

New Features

  • Added profile bind to set a project-level profile, and profile unbind to remove it.
  • Added profile current to show the active profile along with its source and resolved config path.
  • Project profiles are now automatically applied when present; specifying --profile overrides project settings.

Bug Fixes

  • Improved “fails closed” behavior and error messaging when a profile is configured at the project level but cannot be found.
  • More accurate reporting of whether the active profile came from CLI, project, or global configuration.

@ennann ennann requested a review from liangshuo-1 as a code owner June 17, 2026 10:53
Copilot AI review requested due to automatic review settings June 17, 2026 10:53
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 76ee7c69-e8bc-4c86-8e3d-e972b79f4183

📥 Commits

Reviewing files that changed from the base of the PR and between 5199a3c and 02da6d9.

📒 Files selected for processing (22)
  • cmd/auth/list.go
  • cmd/auth/list_test.go
  • cmd/auth/logout.go
  • cmd/auth/logout_test.go
  • cmd/bootstrap.go
  • cmd/bootstrap_test.go
  • cmd/config/active_profile.go
  • cmd/config/default_as.go
  • cmd/config/show.go
  • cmd/config/strict_mode.go
  • cmd/profile/current.go
  • cmd/profile/profile.go
  • cmd/profile/profile_test.go
  • cmd/profile/project_bind.go
  • internal/cmdutil/active_profile.go
  • internal/cmdutil/factory.go
  • internal/cmdutil/factory_default.go
  • internal/core/config.go
  • internal/core/project_config.go
  • internal/core/project_config_test.go
  • internal/credential/default_provider.go
  • internal/credential/integration_test.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/profile/profile.go
🚧 Files skipped from review as they are similar to previous changes (13)
  • internal/core/config.go
  • cmd/config/strict_mode.go
  • cmd/auth/logout.go
  • cmd/auth/list.go
  • cmd/config/show.go
  • cmd/profile/current.go
  • internal/credential/default_provider.go
  • internal/cmdutil/factory_default.go
  • cmd/profile/project_bind.go
  • internal/credential/integration_test.go
  • cmd/bootstrap_test.go
  • internal/core/project_config_test.go
  • internal/core/project_config.go

📝 Walkthrough

Walkthrough

Introduces project-level profile binding for the Lark CLI. A .lark-cli/config.json file at the Git repository root stores a profile name that the CLI bootstrap automatically resolves at startup. Three new subcommands (profile bind, profile unbind, profile current) manage the binding. The ProfileSource provenance type propagates through InvocationContext, credential provider, and all "no active profile" error paths across config and auth commands.

Changes

Project-level profile binding

Layer / File(s) Summary
ProfileSource type and project config data model
internal/core/config.go, internal/core/project_config.go, internal/cmdutil/factory.go
ProfileSource string type and CLI/Project/Global constants added; ProjectConfig, ProjectProfileBinding data models defined; InvocationContext gains Profile, ProfileSource, ProfileConfigPath fields.
Project config file operations
internal/core/project_config.go, internal/core/project_config_test.go
Implements upward path search bounded by Git root, write-path selection, JSON load/save with field merging semantics, atomic removal, and ProjectProfileNotFoundError construction. Full test coverage for all operations including edge cases and error handling.
Bootstrap invocation context: project profile resolution
cmd/bootstrap.go, cmd/bootstrap_test.go
BootstrapInvocationContext resolves project profiles via core.ResolveProjectProfile(), with early-return for CLI-flag profile and skip logic for help/completion and profile/config subcommand patterns. Tests cover project discovery, override, malformed config, and completion-mode behavior.
Credential provider: source-aware account resolution
internal/credential/default_provider.go, internal/credential/integration_test.go, internal/cmdutil/factory_default.go
DefaultAccountProvider gains profileSource/profileConfigPath fields and NewDefaultAccountProviderWithSource; ResolveAccount early-checks for missing project profiles. credentialDeps and buildCredentialProvider wired with new fields. Integration test verifies fail-closed behavior.
profile bind, unbind, current subcommands
cmd/profile/project_bind.go, cmd/profile/current.go, cmd/profile/profile.go, cmd/profile/profile_test.go
Adds profile bind <name>, profile unbind, and profile current Cobra subcommands with full runtime logic, wired into NewCmdProfile. Tests cover project config writes, validation errors, and current JSON output including missing-profile error structure.
Unified no-active-profile errors
internal/cmdutil/active_profile.go, cmd/config/active_profile.go, cmd/config/default_as.go, cmd/config/show.go, cmd/config/strict_mode.go, cmd/auth/list.go, cmd/auth/logout.go
Introduces ProjectProfileError and ActiveProfileError helpers returning structured errors based on profile source. All "no active profile" paths in config and auth commands updated. Integration tests verify error behavior for missing project profiles.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(70, 130, 180, 0.5)
    Note over CLI,BootstrapInvocationContext: Startup
  end
  participant CLI
  participant BootstrapInvocationContext
  participant core as core.ResolveProjectProfile
  participant NewDefault as cmdutil.NewDefault
  participant DefaultAccountProvider
  participant MultiAppConfig

  CLI->>BootstrapInvocationContext: args (no --profile flag)
  BootstrapInvocationContext->>core: walk up to .git, find .lark-cli/config.json
  core-->>BootstrapInvocationContext: ProjectProfileBinding{Profile, Path}
  BootstrapInvocationContext-->>NewDefault: InvocationContext{ProfileSourceProject, ProfileConfigPath}
  NewDefault->>DefaultAccountProvider: NewDefaultAccountProviderWithSource(kc, profile, ProjectSource, configPath)

  rect rgba(200, 80, 80, 0.5)
    Note over DefaultAccountProvider,MultiAppConfig: ResolveAccount
  end
  DefaultAccountProvider->>MultiAppConfig: LoadMultiAppConfig()
  alt profile missing from config
    DefaultAccountProvider-->>CLI: ProjectProfileNotFoundError (hint: profile list)
  else profile found
    DefaultAccountProvider-->>CLI: Account credentials
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • larksuite/cli#728: Both PRs modify error handling in cmd/auth/list.go and cmd/auth/logout.go to surface missing/invalid config via structured core/cmdutil errors and hints.

Suggested labels

domain/base, feature

Suggested reviewers

  • liangshuo-1
  • evandance

Poem

🐰 Hopping through the git root tree,
I found a .lark-cli config with glee!
profile bind ties the project just right,
profile unbind lets workflows take flight.
Now every directory knows which profile to see —
A source of truth from project to CLI! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: introducing project-level profile binding support.
Description check ✅ Passed The description includes all required template sections: Summary (1-3 sentences explaining motivation and scope), Changes (bulleted list of main changes), Test Plan (with checkboxes and verification details), and Related Issues section.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added the size/L Large or sensitive change across domains or core paths label Jun 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds project-local profile binding support so the CLI can resolve an “effective profile” from a repo’s .lark-cli/config.json, expose it via new profile subcommands, and fail closed with a typed config error when a project-bound profile is missing.

Changes:

  • Introduces project config discovery/read/write helpers and related tests (internal/core/project_config.go + tests).
  • Propagates ProfileSource/ProfileConfigPath through invocation/factory/credential resolution and adds “project profile missing” fail-closed behavior.
  • Adds profile current|bind|unbind commands and extends bootstrap logic to read project bindings (with selective skipping for certain commands).

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/credential/integration_test.go Adds an integration test asserting project-bound missing profile fails closed with *core.ConfigError.
internal/credential/default_provider.go Extends default account provider to retain profile source/path and emit project-specific “not found” errors early.
internal/core/project_config_test.go Adds tests for project config discovery, read/write, validation, and removal semantics.
internal/core/project_config.go Implements .lark-cli/config.json project binding discovery/updating/removal and the ProjectProfileNotFoundError helper.
internal/core/config.go Introduces core.ProfileSource and constants (cli, project, global).
internal/cmdutil/factory_default.go Threads ProfileSource and ProfileConfigPath into credential provider construction.
internal/cmdutil/factory.go Extends InvocationContext to carry profile source and originating config path.
cmd/profile/project_bind.go Adds profile bind and profile unbind commands to manage project profile bindings.
cmd/profile/profile_test.go Adds tests for bind/unbind/current behavior and typed error expectations.
cmd/profile/profile.go Registers new `profile current
cmd/profile/current.go Adds profile current command to show effective profile (including source and config path).
cmd/config/strict_mode.go Uses a shared helper to return a project-aware “no active profile” error.
cmd/config/show.go Uses the project-aware “no active profile” error helper.
cmd/config/default_as.go Uses the project-aware “no active profile” error helper.
cmd/config/active_profile.go Adds helper to produce project-aware “no active profile” errors.
cmd/bootstrap_test.go Adds tests for project profile resolution precedence and malformed config behavior.
cmd/bootstrap.go Adds project profile resolution during bootstrap with heuristics to skip lookup for certain commands.
cmd/auth/logout.go Returns ProjectProfileNotFoundError when a project-bound active profile is missing.
cmd/auth/list.go Returns ProjectProfileNotFoundError when a project-bound active profile is missing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/bootstrap.go Outdated
Comment on lines 54 to 100
func skipProjectProfileLookup(args []string) bool {
for _, arg := range args {
if arg == "-h" || arg == "--help" {
return true
}
}
parts := firstPositionalArgs(args, 2)
if len(parts) == 0 {
return false
}
switch parts[0] {
case "completion", "__complete", "__completeNoDesc":
return true
case "profile":
return len(parts) < 2 || parts[1] != "current"
case "config":
return len(parts) >= 2 && (parts[1] == "bind" || parts[1] == "init" || parts[1] == "remove")
default:
return false
}
}

func firstPositionalArgs(args []string, limit int) []string {
var out []string
skipNext := false
for _, arg := range args {
if skipNext {
skipNext = false
continue
}
if arg == "--" {
break
}
if arg == "--profile" {
skipNext = true
continue
}
if strings.HasPrefix(arg, "--profile=") || strings.HasPrefix(arg, "-") {
continue
}
out = append(out, arg)
if len(out) >= limit {
return out
}
}
return out
}
Comment thread cmd/profile/current.go
Comment on lines +50 to +57
source := f.Invocation.ProfileSource
if source == "" {
source = core.ProfileSourceGlobal
}
configPath := core.GetConfigPath()
if source == core.ProfileSourceProject {
configPath = f.Invocation.ProfileConfigPath
}
Comment on lines 70 to +72
func NewDefaultAccountProvider(kc func() keychain.KeychainAccess, profile string) *DefaultAccountProvider {
return NewDefaultAccountProviderWithSource(kc, profile, "", "")
}
Comment thread cmd/auth/list.go Outdated
Comment on lines +72 to +75
app := multi.CurrentAppConfig(f.Invocation.Profile)
if app == nil && f.Invocation.Profile != "" && f.Invocation.ProfileSource == core.ProfileSourceProject {
return core.ProjectProfileNotFoundError(f.Invocation.Profile, f.Invocation.ProfileConfigPath, multi.ProfileNames())
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/auth/list.go`:
- Around line 73-75: The check at lines 73-75 for ProjectProfileNotFoundError is
unreachable because the function returns early at line 48 when len(multi.Apps)
== 0. Move the project profile validation logic that checks if app is nil and
f.Invocation.Profile is set with ProfileSourceProject before or into the early
return condition at line 48, so that a missing project-bound profile properly
returns ProjectProfileNotFoundError instead of exiting with success. This
ensures the fail-closed behavior matches how
internal/credential/default_provider.go handles missing profiles.

In `@cmd/auth/logout.go`:
- Around line 62-64: The project-bound missing profile validation on lines 62-64
is unreachable because the code returns early when len(multi.Apps) == 0 (at line
48), breaking the fail-closed behavior for project-sourced profiles. Move the
project profile validation check (the condition checking for app == nil with
ProfileSource == core.ProfileSourceProject and the call to
core.ProjectProfileNotFoundError) to execute before the early return on
len(multi.Apps) == 0, ensuring that project-sourced profile validation always
occurs regardless of whether apps are empty.

In `@cmd/profile/current.go`:
- Around line 42-48: The error handling after the CurrentAppConfig call needs to
distinguish between CLI and Project profile sources. When app is nil and
f.Invocation.ProfileSource is CLI (not ProfileSourceProject), this represents
invalid user input and should return
errs.NewValidationError(errs.SubtypeInvalidArgument, ...) with
.WithParam("--profile") instead of the current ConfigError. Keep the existing
ProjectProfileNotFoundError branch for the project-source case unchanged, and
only return the ConfigError when neither CLI nor project source conditions apply
(or when the profile is empty).

In `@cmd/profile/profile_test.go`:
- Around line 118-120: The test
TestProfileBindRun_InvalidProfileReturnsTypedError lacks config directory
isolation, which can cause cross-test state leakage. Before calling
cmdutil.TestFactory(t, nil), add a call to t.Setenv("LARKSUITE_CLI_CONFIG_DIR",
t.TempDir()) to set up an isolated temporary config directory for this test,
matching the pattern used in other tests in this file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86c22449-ed5d-4ecd-9d11-7d7e3fa9c7b3

📥 Commits

Reviewing files that changed from the base of the PR and between 4a4c334 and 5199a3c.

📒 Files selected for processing (19)
  • cmd/auth/list.go
  • cmd/auth/logout.go
  • cmd/bootstrap.go
  • cmd/bootstrap_test.go
  • cmd/config/active_profile.go
  • cmd/config/default_as.go
  • cmd/config/show.go
  • cmd/config/strict_mode.go
  • cmd/profile/current.go
  • cmd/profile/profile.go
  • cmd/profile/profile_test.go
  • cmd/profile/project_bind.go
  • internal/cmdutil/factory.go
  • internal/cmdutil/factory_default.go
  • internal/core/config.go
  • internal/core/project_config.go
  • internal/core/project_config_test.go
  • internal/credential/default_provider.go
  • internal/credential/integration_test.go

Comment thread cmd/auth/list.go Outdated
Comment thread cmd/auth/logout.go Outdated
Comment thread cmd/profile/current.go
Comment on lines +42 to +48
app := multi.CurrentAppConfig(f.Invocation.Profile)
if app == nil {
if f.Invocation.Profile != "" && f.Invocation.ProfileSource == core.ProfileSourceProject {
return core.ProjectProfileNotFoundError(f.Invocation.Profile, f.Invocation.ProfileConfigPath, multi.ProfileNames())
}
return errs.NewConfigError(errs.SubtypeNotConfigured, "no active profile").WithHint("run: lark-cli profile list")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return a validation error when an explicit CLI profile is missing.

If f.Invocation.ProfileSource is CLI and CurrentAppConfig(...) returns nil, this is an invalid --profile input, not a SubtypeNotConfigured state. Please classify it as errs.NewValidationError(errs.SubtypeInvalidArgument, ...) with .WithParam("--profile") (and keep the project-source branch as-is).

Suggested patch
 	app := multi.CurrentAppConfig(f.Invocation.Profile)
 	if app == nil {
-		if f.Invocation.Profile != "" && f.Invocation.ProfileSource == core.ProfileSourceProject {
-			return core.ProjectProfileNotFoundError(f.Invocation.Profile, f.Invocation.ProfileConfigPath, multi.ProfileNames())
-		}
+		if f.Invocation.Profile != "" {
+			if f.Invocation.ProfileSource == core.ProfileSourceProject {
+				return core.ProjectProfileNotFoundError(f.Invocation.Profile, f.Invocation.ProfileConfigPath, multi.ProfileNames())
+			}
+			if f.Invocation.ProfileSource == core.ProfileSourceCLI {
+				return errs.NewValidationError(errs.SubtypeInvalidArgument, "profile %q not found", f.Invocation.Profile).
+					WithParam("--profile").
+					WithHint("run: lark-cli profile list")
+			}
+		}
 		return errs.NewConfigError(errs.SubtypeNotConfigured, "no active profile").WithHint("run: lark-cli profile list")
 	}

As per coding guidelines, cmd/**/*.go user flag/argument validation failures should use errs.NewValidationError(errs.SubtypeInvalidArgument, ...).WithParam("--flag").

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/profile/current.go` around lines 42 - 48, The error handling after the
CurrentAppConfig call needs to distinguish between CLI and Project profile
sources. When app is nil and f.Invocation.ProfileSource is CLI (not
ProfileSourceProject), this represents invalid user input and should return
errs.NewValidationError(errs.SubtypeInvalidArgument, ...) with
.WithParam("--profile") instead of the current ConfigError. Keep the existing
ProjectProfileNotFoundError branch for the project-source case unchanged, and
only return the ConfigError when neither CLI nor project source conditions apply
(or when the profile is empty).

Source: Coding guidelines

Comment thread cmd/profile/profile_test.go
@ennann ennann force-pushed the codex/project-profile-config branch from 5199a3c to 02da6d9 Compare June 17, 2026 11:08
@ennann

ennann commented Jun 17, 2026

Copy link
Copy Markdown
Author

Superseded by #1506.

@ennann ennann closed this Jun 17, 2026
@ennann ennann deleted the codex/project-profile-config branch June 17, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants