When to Use
This skill uniformly covers two types of work:
- Adding a new top-level system command
- Adding one or more actions to an existing system
- Adding a subsystem layer to an existing system, and adding YAML or Go actions under the subsystem
This skill also provides a summary of shared contracts that must be checked when creating/extending a system; for detailed rules, refer to docs/design.md . First determine whether the system already exists, then decide which branch to take; do not write code first and then go back to add registration or structure.
Recommended reading order
- First read
AGENTS.mdanddocs/design.md, to confirm the repository boundaries and design baseline. - Then read the "Must-check summary for extending system" in this skill to confirm the shared rules most likely to cause pitfalls when extending a system; for detailed contracts, always go back to
docs/design.md。 - Finally, follow this skill's branching process to determine whether it is "create a new system" or "add an action to an existing system".
Step zero: first determine whether a subsystem is needed
If the user's requirements, OpenAPI list, resource tags, or business description naturally divide into multiple modules, pause implementation first and give the user two options:
- Flat action: continue using
bk-cli <system> <action>, distinguishing modules by action name, for exampleget_pipeline_build_list。 - One subsystem layer: use
bk-cli <system> <subsystem> <action>, for examplebk-cli devops pipeline get_build_list、bk-cli devops codecc get_task_detail、bk-cli devops stream trigger。
Currently only one subsystem layer is allowed; bk-cli <system> <subsystem> <sub_subsystem> <action>is not allowed. If the user needs deeper levels, stop implementation and provide a public contract change proposal suggestion.
Step one: first determine whether the system already exists
Check in the following order:
- Read
AGENTS.md、docs/design.md, and first read through the "Must-check summary for extending system" later in this skill - View
cmd/system/register.go, to confirmsystemCatalog()'s registration method - Check whether the following files already exist:
cmd/system/<system>.gocmd/system/<system>/spec.gocmd/system/<system>/actions.yamlcmd/system/<system>/<subsystem>/spec.gocmd/system/<system>/<subsystem>/actions.yaml
- Search
new<System>SystemSpec()orNewSystemSpec(), to confirm whether the system has already been integrated into the catalog
Determine the result:
- The system does not exist: first create the system, then add YAML actions, Go-implemented actions, tests, and documentation as needed
- The system already exists: only add actions, and decide whether to use YAML or Go at the action granularity; do not migrate existing YAML actions for the sake of one Go action
- The system already exists but the subsystem does not exist: follow "Branch C: Add subsystem" to create the subsystem, then decide YAML or Go at the action granularity
Must-check summary for extending system
The documentation layering is based on AGENTS.md as the standard; for detailed shared contracts, refer to docs/design.md as the standard. When generating system/action, at least first confirm:
- Context represents an independent BlueKing deployment target, not an API Gateway stage; for detailed context rules, refer to
docs/design.mdas the standard. --stage、timeout、tenant、--headerDo not rewrite the priority yourself; when these behaviors are involved, go back and checkdocs/design.md。- Even if the user uses
--headerto overrideX-Bkapi-Authorization,--dry-run/--verboseSensitive authentication content must still be displayed in a redacted form. - YAML action must explicitly declare
authConfig;resourcePermissionRequired: truemust be set at the same timeappVerifiedRequired: true。 - YAML
paramsonly supportsin: path、in: queryand for help purposesin: header; do not declarein: body。 - YAML actions uniformly receive shared input:
--stage、--body '<json>', duplicate--header 'Key:Value'; when flag name conflicts are retained, the action should be skipped and a warning given. - If the OpenAPI request body is very complex and requires the Agent or caller to construct the complete JSON themselves, do not force the body fields to be split into command flags; continue to use the shared
--body '<json>', put the request body example inexamples, and configure in the YAML actionbody_schema. If OpenAPI marks the request body as required, also configurebody_required: true. By default, help is displayed in the order ofUsage、Examples, schema viewing hints, and the complete schema is viewed viabk-cli <system> [subsystem] <action> -h --body-schema. - Path placeholder values are escaped as a single URL path segment, and you cannot rely on it to inject
/or?。 - Go-implemented action must go through
systemcmd.ResolveRuntime(deps)andsystemcmd.ExecuteRequest(...)orsyslib.ExecuteRequest(...)to use the shared execution path, and must not bypass the runtime / output / credential logic. - When a Go-implemented action supports both named flags and raw
--body, treat--bodyas an explicit override.
command group form
A command group can be one of the following forms:
- YAML-driven: all actions are defined by
actions.yamlDefinition - Go-implemented: all actions are implemented by Go code
- mixed: both YAML actions and Go-implemented actions are retained
- group-only: the parent system serves only as a grouping, with subsystems attached under it
- parent actions + subsystems: the parent system has its own actions and also has subsystems attached under it
The implementation method of an action depends only on its own complexity, not on what other actions already use.
Direct subcommand names under the same parent must be unique. A parent system's action names must not conflict with subsystem names.
action selection rules
Prefer the simplest implementation that meets the requirements.
Prefer YAML if and only if all of the following conditions are met:
- essentially just
cli args -> 一次 API 调用 - no local orchestration logic is needed
- no local-specific flags are needed, and API request parameters are directly converted into flags
- no customized return structure is needed
- Even if there is a request body, as long as the caller can directly provide the complete JSON via
--bodyandexamples/body_schemais sufficient to guide the Agent in constructing the body, also prefer to keep it YAML-driven
A Go-implemented action must be used if any one of the following conditions is met:
- local validation, branching, orchestration, or pagination aggregation is needed
- zero, one, or multiple API calls are needed
- local-specific flags are needed, for example
--bk_biz_id、--fields、--limit, encapsulated/processed/orchestrated and then used as request parameters - the body needs to be synthesized by the CLI from named flags before the request, or local-specific validation/default value handling is needed for body fields
- after the request, use
mutateor a manual envelope to adjust the return
YAML body schema / example rules
body_schema Used for scenarios where "the body is complex, but the action itself is still just one API call". A typical example is when the OpenAPI request body has a large number of nested fields, arrays, or objects, and the Agent needs to construct the complete JSON itself based on the schema.
Rules:
- Do not generate a large number of command flags for complex body fields; otherwise help will bloat, and the invocation contract will easily drift from OpenAPI.
- A YAML action only converts
path/queryparameters into flags; the body continues to be input through the shared--body '<json>'input. body_schemaPut the simplified JSON schema or field structure description; a JSON example that can be used directly as a--bodystarting point goes in the actionexamples, to avoid maintaining it redundantly with examples.- If the OpenAPI request body is required, configure
body_required: true; this way, when--bodyis missing during execution, it will fail locally and will not send an empty body upstream. - By default,
bk-cli <system> [subsystem] <action> --helpmust first displayUsageandExamples, then display the schema viewing hint;body_schemamust be viewable throughbk-cli <system> [subsystem] <action> -h --body-schema. System-specificskills/*/SKILL.mdcan only contain common examples and cannot be the sole source of body structure. --body-schemais a help modifier, not an execution parameter; when used alone without-h, it must fail fast and must not enter the authentication or request execution path.- Only when the user explicitly needs friendlier local flags, the CLI needs to synthesize a body, or local validation of complex fields is needed, should this action be made into a separate Go-implemented wrapper.
Core types
systemcmd.SystemSpec
type SystemSpec struct {
Name string
Description string
YAMLFile string
RegisterGoActions RegisterGoActionsFunc
Subsystems []SystemSpec
}
systemcmd.BuildDeps
type BuildDeps struct {
GetContext func() string
IsDryRun func() bool
IsVerbose func() bool
WarnWriter io.Writer
}
syslib.RequestSpec
A Go-implemented action uses it to describe a single request. Common fields:
GatewayNameMethodPathParamsJSONBodyJSONHeadersStageTimeoutAuthConfig
AuthConfig must be set explicitly, using &syslib.AuthConfig{...}。
Shared helper map
Runtime and request execution
| Helper | Purpose |
|---|---|
systemcmd.ResolveRuntime(deps) |
At the beginning of RunE uniformly parse context, dry-run, verbose, insecure |
systemcmd.ExecuteRequest(cmd, runtime, actionName, spec, mutate) |
Standard execution path for a single-request action |
syslib.ExecuteRequest(runtime, spec) |
Multi-request orchestration, pagination aggregation |
systemcmd.EnsureEnvelope(actionName, env) |
Defend against empty envelope |
Flags, validation, and serialization
| Helper | Purpose |
|---|---|
systemcmd.AddCommonRequestFlags(cmd, &stage, &body, &headers) |
Register --stage、--body、--header |
systemcmd.AddCommonRequestFlagsWithoutBody(cmd, &stage, &headers) |
Register --stage、--header |
systemcmd.MarshalJSON(payload) |
Uniformly serialize body |
systemcmd.ValidatePositiveIntFlag(...) |
Validate required positive integer |
systemcmd.ValidatePositiveIntFlagIfChanged(...) |
Validate optional positive integer |
systemcmd.ValidateNonNegativeIntFlag(...) |
Validate non-negative integer |
systemcmd.ValidateNonEmptyStringFlag(...) |
Validate non-empty string |
systemcmd.ParseJSONObjectFlag(flagName, raw) |
Parse JSON object type flag |
Test helper
| Helper | Purpose |
|---|---|
testutil.BuildDeps(dryRun bool) |
Construct test dependencies |
testutil.SetupTestContext(baseURL string) |
Create default context and credentials |
testutil.CaptureCommandStdout(fn) |
Capture stdout |
testutil.BuildYAMLActionCmd(...) |
Construct YAML action test command |
Do not reimplement these shared capabilities yourself.
Branch A: system does not exist, create system first
Files that must be added or updated
Always required:
| File | Purpose |
|---|---|
cmd/system/<system>.go |
Thin wrapper, calls <system>.NewSystemSpec() |
cmd/system/<system>/spec.go |
NewSystemSpec() implementation |
cmd/system/register.go |
Register in systemCatalog() register in |
Add as needed:
| File | Condition |
|---|---|
cmd/system/<system>/actions.yaml |
This system needs YAML actions |
cmd/system/<system>/<action>.go |
This system needs Go-implemented actions |
cmd/system/<system>/common.go |
Multiple Go actions share logic |
cmd/system/<system>/<system>_suite_test.go |
This system has Ginkgo tests |
skills/bk-cli-<system>/SKILL.md |
When adding a public system, this must be completed, and the content must be in Chinese |
Thin wrapper template
package system
import <system>system "github.com/TencentBlueKing/bk-cli/cmd/system/<system>"
func new<System>SystemSpec() SystemSpec {
return <system>system.NewSystemSpec()
}
spec.go template
YAML-driven
func NewSystemSpec() systemcmd.SystemSpec {
return systemcmd.SystemSpec{
Name: "<system>",
Description: "<system> system commands",
YAMLFile: "<system>/actions.yaml",
}
}
Go-implemented
func NewSystemSpec() systemcmd.SystemSpec {
return systemcmd.SystemSpec{
Name: "<system>",
Description: "<system> system commands",
RegisterGoActions: func(parent *cobra.Command, deps systemcmd.BuildDeps) error {
parent.AddCommand(newSomeActionCmd(deps))
return nil
},
}
}
mixed
func NewSystemSpec() systemcmd.SystemSpec {
return systemcmd.SystemSpec{
Name: "<system>",
Description: "<system> system commands",
YAMLFile: "<system>/actions.yaml",
RegisterGoActions: func(parent *cobra.Command, deps systemcmd.BuildDeps) error {
builders := []func(systemcmd.BuildDeps) *cobra.Command{
newActionOneCmd,
newActionTwoCmd,
}
for _, build := range builders {
parent.AddCommand(build(deps))
}
return nil
},
}
}
Registration and embed constraints
In cmd/system/register.go of systemCatalog() add a new system.
If YAML is used, the file must be placed in cmd/system/<system>/actions.yaml, because the repository relies on //go:embed */actions.yaml for automatic embedding.
common.go recommendation
When multiple Go-implemented actions share logic, put the following content into common.go:
- Gateway name constants
- Shared types
- body builder
- Shared validation functions
- factory function
Ginkgo suite
As long as this system has test files, add the suite file:
package <system>_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSuite(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "<System> Suite")
}
Branch B: system already exists, add a new action
YAML action workflow
The YAML file location is fixed as:
cmd/system/<system>/actions.yaml
If SystemSpec.YAMLFile is already configured, you only need to append the action to the actions list; no additional Go wiring is required.
YAML top-level structure
name: <system>
gateway_name: bk-<upstream>
description: "System description"
actions:
- ...
YAML action fields
| Field | Required | Description |
|---|---|---|
name |
Yes | action command name |
description |
Yes | Cobra Short |
method |
Yes | HTTP method |
path |
Yes | API path, may include {param} |
timeout |
No | Override context timeout, for example 30s |
authConfig |
Yes | Authentication configuration |
params |
No | Parameter list |
examples |
No | Command example |
body_schema |
No | Schema or field structure description for a complex request body; through -h --body-schema help the Agent construct --body |
body_required |
No | Whether a non-empty --bodyis required at execution time; should be set to true when OpenAPI requestBody.required=true |
authConfig
| Field | Required | Description |
|---|---|---|
appVerifiedRequired |
Yes | Whether application authentication is required |
userVerifiedRequired |
Yes | Whether user authentication is required |
resourcePermissionRequired |
Yes | Whether resource permission validation is required |
Constraints:
resourcePermissionRequired: trueWhenappVerifiedRequiredmust also betrue- If neither app nor user is required, the CLI will not generate
X-Bkapi-Authorization - Even if the user uses
--headerto override the authentication header, dry-run / verbose must continue to display authentication content in redacted form
params
| Field | Required | Description |
|---|---|---|
name |
Yes | Parameter name, which is also the flag name |
in |
Yes | path、query or header |
type |
Yes | string、bool、int |
description |
No | Help text |
required |
No | Whether required |
default |
No | Default value |
Rules:
pathandquerywill generate CLI flagsheaderis only used for help text and does not generate a separate flag- Do not write
in: body - action additionally shares
--stage、--body '<json>'and duplicate--header 'Key:Value'; when there isbody_schemaalso supports help modifier--body-schema; whenbody_required: trueis present,--bodyis a required execution item - param names must not conflict with reserved flags:
body、body-schema、header、stage、help、context、dry-run、format、verbose、insecure - Within the same action, path and query param names must not be duplicated
Go-implemented action workflow
Files that need to be modified
| File | Purpose |
|---|---|
cmd/system/<system>/<action>.go |
action constructor |
cmd/system/<system>/spec.go |
Register a new action |
cmd/system/<system>/<action>_test.go |
action tests |
cmd/system/<system>/common.go |
Shared logic, optional |
Constructor template
func newSomeActionCmd(deps systemcmd.BuildDeps) *cobra.Command {
var (
bizID int
stage string
body string
headers []string
)
cmd := &cobra.Command{
Use: "some_action",
Short: "Short description",
RunE: func(cmd *cobra.Command, args []string) error {
runtime, err := systemcmd.ResolveRuntime(deps)
if err != nil {
return err
}
if err := systemcmd.ValidatePositiveIntFlag("bk_biz_id", bizID); err != nil {
return err
}
bodyJSON, err := buildSomeBody(body, bizID)
if err != nil {
return err
}
return systemcmd.ExecuteRequest(cmd, runtime, "some_action", syslib.RequestSpec{
GatewayName: gatewayName,
Method: "POST",
Path: "/api/v3/some/path/",
BodyJSON: bodyJSON,
Headers: headers,
Stage: stage,
AuthConfig: &syslib.AuthConfig{
AppVerifiedRequired: true,
UserVerifiedRequired: true,
ResourcePermissionRequired: false,
},
}, nil)
},
}
cmd.Flags().IntVar(&bizID, "bk_biz_id", 0, "Business ID")
systemcmd.AddCommonRequestFlags(cmd, &stage, &body, &headers)
return cmd
}
body composition mode
Prefer using the bodyOverride guard:
func buildSomeBody(bodyOverride string, bizID int) (string, error) {
if bodyOverride != "" {
return bodyOverride, nil
}
if err := systemcmd.ValidatePositiveIntFlag("bk_biz_id", bizID); err != nil {
return "", err
}
return systemcmd.MarshalJSON(map[string]any{
"bk_biz_id": bizID,
})
}
mutate callback
When the envelope needs to be adjusted before stdout, pass in mutate:
return systemcmd.ExecuteRequest(cmd, runtime, "demo_action", spec,
func(env *output.Envelope) error {
if env.DryRun {
env.Data = map[string]any{"received": localData}
return nil
}
env.Data = map[string]any{
"received": localData,
"upstream": env.Data,
}
return nil
})
No shared --body action
If the action manages body semantics itself:
- Use
systemcmd.AddCommonRequestFlagsWithoutBody - Declare local
--bodyflag RequestSpec.BodyJSONdetermined by local logic
factory pattern
When multiple actions share the same set of flag/request structures, put the common parts into common.go, and use a spec struct + factory function to generate commands, avoiding copy-paste.
Multi-request orchestration
When pagination aggregation or multiple calls are needed:
- Use
syslib.ExecuteRequest(runtime, spec)to send each request - Use
systemcmd.EnsureEnvelope(actionName, result.Envelope)to validate results - Manually aggregate data
- Manually construct the final envelope
In spec.go mount in
builders := []func(systemcmd.BuildDeps) *cobra.Command{
newExistingActionCmd,
newSomeActionCmd,
}
for _, build := range builders {
parent.AddCommand(build(deps))
}
Branch C: system already exists, add a new subsystem layer
Files that must be added or updated
| File | Purpose |
|---|---|
cmd/system/<system>/spec.go |
In SystemSpec.Subsystems register subsystem |
cmd/system/<system>/<subsystem>/spec.go |
NewSystemSpec() implementation, describing the subsystem's own YAML/Go actions |
cmd/system/<system>/<subsystem>/actions.yaml |
Used when the subsystem needs YAML actions |
cmd/system/<system>/<subsystem>/<action>.go |
Used when the subsystem needs Go-implemented actions |
cmd/system/<system>/<subsystem>/common.go |
Multiple Go actions share logic, optional |
cmd/system/<system>/<subsystem>/<subsystem>_suite_test.go |
Must exist when the subsystem has tests |
subsystem spec.go template
package <subsystem>
import (
"github.com/spf13/cobra"
systemcmd "github.com/TencentBlueKing/bk-cli/internal/systemcmd"
)
func NewSystemSpec() systemcmd.SystemSpec {
return systemcmd.SystemSpec{
Name: "<subsystem>",
Description: "<subsystem> commands",
YAMLFile: "<system>/<subsystem>/actions.yaml",
RegisterGoActions: func(parent *cobra.Command, deps systemcmd.BuildDeps) error {
parent.AddCommand(newSomeActionCmd(deps))
return nil
},
}
}
parent system registration template
func NewSystemSpec() systemcmd.SystemSpec {
return systemcmd.SystemSpec{
Name: "<system>",
Description: "<system> commands",
YAMLFile: "<system>/actions.yaml",
RegisterGoActions: func(parent *cobra.Command, deps systemcmd.BuildDeps) error {
parent.AddCommand(newParentActionCmd(deps))
return nil
},
Subsystems: []systemcmd.SystemSpec{
<subsystem>.NewSystemSpec(),
},
}
}
The parent system's YAMLFile and RegisterGoActions are both optional. Do not create meaningless parent actions just to mount a subsystem.
subsystem YAML top-level structure
name: <subsystem>
gateway_name: <subsystem-gateway-name>
description: "<subsystem> commands"
actions:
- name: <action>
The subsystem YAML's gateway_name must be declared independently and not inherited from the parent system.
Testing and documentation requirements
At least add or update:
cmd/system/register_test.go: system registration behaviorcmd/system/<system>/<action>_test.go: Go-implemented action testscmd/system/<system>/<subsystem>/<action>_test.go: subsystem Go-implemented action testscmd/system/<system>/<system>_suite_test.go: Must exist when the system has testscmd/system/<system>/<subsystem>/<subsystem>_suite_test.go: Must exist when the subsystem has tests
If adding a public system, also update:
AGENTS.mdREADME.mdREADME_EN.mdtests/integration/AGENTS.mdskills/bk-cli-<system>/SKILL.md
If only changing existing commands or parameters, also update AGENTS.md、README.md、README_EN.md、tests/integration/AGENTS.md and related skills.
If the change affects the visible behavior of a public system, add or update:
tests/integration/cases/system/<system>/YAML integration cases undertests/integration/mock_api/app.py(only whenhttpbinis insufficient to express the behavior)
Final checklist
- First confirm whether the target system already exists, then choose the branch
cmd/system/<system>.goandcmd/system/<system>/spec.goall existSystemSpec.Name, YAML top-levelname, and registration entry are consistentcmd/system/register.goofsystemCatalog()already includes the target system- The YAML file path is
cmd/system/<system>/actions.yaml - YAML actions all have complete
authConfig - Go-implemented action uses
systemcmd.ResolveRuntime - Go-implemented action uses
systemcmd.ExecuteRequestorsyslib.ExecuteRequest - Shared flags, validation, and JSON parsing should preferably reuse
systemcmdhelper - Tests should preferably reuse
cmd/system/testutil - Public system has added
skills/bk-cli-<system>/SKILL.md - The integration test cases related to the system have been
tests/integration/cases/system/<system>/completed or confirmed that no changes are needed - If the integration test case author conventions have changed, updated
tests/integration/AGENTS.md - If adding a new subsystem, confirm that only one level of subsystem is used
- If adding a new subsystem, confirm that the subsystem action path is
cmd/system/<system>/<subsystem>/... - If adding a new subsystem, confirm that the subsystem YAML top-level
nameis<subsystem>, and independently declaresgateway_name - Confirm that the parent system's action name does not conflict with the subsystem name
- After completion, execute:
make fmt
make lint
make test
make build
make test-integration SCENARIO=<SCENARIO_ID>
Troubleshooting order
- First confirm whether the context and credentials are correct.
- Then confirm whether stage, tenant, timeout, header, and body conform to the mandatory checklist summary above; for details, refer to
docs/design.md. - Use
--dry-runto inspect the final request construction. - If problems still remain, go back to the specific system skill or
docs/design.mdto dig deeper.
Common errors
- Not first determining whether the system already exists, resulting in duplicate creation of structures
- Only wrote
actions.yaml, but did not addcmd/system/<system>.goandspec.go - Forgot to add the system to
systemCatalog() - The YAML file is not in
cmd/system/<system>/actions.yaml - YAML action is missing
authConfig - In YAML
paramswrotein: body - Go-implemented action bypasses
ResolveRuntimeorExecuteRequest - Duplicate definition of test helper in the system directory
- Added a public system but did not add
skills/bk-cli-<system>/SKILL.md - The import alias is not
syslib/systemcmd - The API list provided by the user was already divided into modules such as pipeline/codecc/stream, but the agent did not first let the user choose between flat actions and subsystems
- The subsystem YAML was written as the parent system's
name - The subsystem YAML attempts to omit
gateway_nameand inherit the parent system - Created multiple levels of subsystems
- The parent system action and the subsystem use the same command name