Skip to content

feat: Implement azdo pipelines variable-group variable update command #126

Description

@tmeckel

This issue tracks the implementation of the azdo pipelines variable-group variable update command.

Command Description

Update an existing variable within a variable group. The Azure CLI supports renaming, value changes, and secret flag updates while preventing duplicate keys (source). azdo should mirror this capability with strong validation and consistent resolver helpers.

Important REST constraint:

  • Azure DevOps REST never returns secret variable values. Secret values are write-only: the command may send them to the API, but must not expect to read them back. Human-readable output must redact secret values (e.g., ***), and JSON output must never contain a secret value.

azdo Command Signature

azdo pipelines variable-group variable update [ORGANIZATION/]PROJECT/VARIABLE_GROUP_ID_OR_NAME --name VARIABLE_NAME [flags]

Flags:

  • --name string (required): Variable key to update (matched case-insensitively).
  • --new-name string: Rename the variable (case-insensitive uniqueness enforced).
  • --value string: Replace the stored value. If the variable is secret, this value is write-only and must not be echoed.
  • --secret bool: Toggle secret flag (VariableValue.IsSecret). Tri-state: only apply when explicitly set (use Flags().Changed("secret")).
  • --read-only bool: Toggle read-only flag (VariableValue.IsReadOnly). Tri-state: only apply when explicitly set (use Flags().Changed("read-only")).
  • --prompt-value: For setting/updating a secret value without --value, prompt securely (error if prompting is not possible).
  • --clear-value: Clear the stored value for a non-secret variable only; requires confirmation via --yes.
  • --yes: Skip confirmation prompts for destructive operations (--clear-value).
  • --from-json string: Apply updates from JSON (see “--from-json semantics”).
  • JSON export flags (--json, --jq, --template) registered via util.AddJSONFlags.

At least one of --new-name, --value, --secret, --read-only, --prompt-value, --clear-value, or --from-json must be supplied.

--from-json semantics (deterministic)

  • --from-json accepts:
    • a file path
    • - to read from stdin
    • if the path does not exist, treat the argument as inline JSON
  • The JSON payload must be a single object with these keys (all optional; absent = “no change”):
    • newName (string)
    • value (string)
    • secret (bool)
    • readOnly (bool)
    • clearValue (bool)
  • Mutual exclusivity:
    • If --from-json is set, reject any combination with: --new-name, --value, --secret, --read-only, --prompt-value, --clear-value.
  • Validation rules:
    • Reject payloads that also try to specify name (the variable key is always provided by --name).
    • Reject clearValue=true together with value (ambiguous).
    • If secret=true is being set and neither value is present in JSON nor --prompt-value is available (not allowed with --from-json), return a validation error (write-only secret value required).

Behavior

  • Parse [ORGANIZATION/]PROJECT/VARIABLE_GROUP_ID_OR_NAME using util.ParseProjectTargetWithDefaultOrganization; wrap parse errors with util.FlagErrorWrap.
  • Resolve the variable group ID-or-name using the shared helper shared.ResolveVariableGroup (do not re-implement ID/name resolution).
  • Fetch the variable group and locate the existing variable key case-insensitively; error if missing.
  • If --new-name (or JSON newName) is set:
    • Ensure no other variable exists with that name (case-insensitive) before applying the rename.
    • Rename by removing the old key from the variables map and inserting the updated value under the new key.
  • Apply only explicitly specified changes:
    • Bool flags are tri-state (only apply if Flags().Changed(...) or JSON key present).
    • For --prompt-value: prompt securely and use the entered value (intended for secret rotation); never log or echo it.
  • --clear-value:
    • Only valid for non-secret variables.
    • Prompt unless --yes is set. Prompt text: Clear value of variable 'VARIABLE_NAME' in group 'VARIABLE_GROUP'?. Return util.ErrCancel on decline.
  • Azure Key Vault variable groups:
    • If the variable group type indicates Azure Key Vault, reject attempts that modify variable values (e.g., --value, --prompt-value, --clear-value) and return a clear error message.
  • Persist changes via TaskAgent.UpdateVariableGroup.
  • Output (single-object command):
    • Default output is a Go text template summarizing the updated variable (name, group, secret/read-only flags). Never print a secret value; print *** for secret value fields.
    • Reuse the same masking/redaction behavior already implemented by internal/cmd/pipelines/variablegroup/variable/list/list.go so outputs are consistent across variable-group variable commands.
  • JSON output:
    • Emit the updated SDK variable group model returned by UpdateVariableGroup.
    • util.AddJSONFlags must list fields that match the SDK JSON tags (field-level contract). Suggested list:
      • id, name, type, description, variables, variableGroupProjectReferences, providerData, createdBy, createdOn, modifiedBy, modifiedOn
    • Secret values must be absent/redacted; do not add any code path that tries to “restore” a secret value from the server response.

Implementation Notes (filled checklist)

  • Implement command: internal/cmd/pipelines/variablegroup/variable/update/update.go (type opts struct, NewCmd(ctx), run(ctx, opts)).
  • Wire command: internal/cmd/pipelines/variablegroup/variable/variable.go must AddCommand(update.NewCmd(ctx)) and be reachable from azdo pipelines variable-group variable.
  • Parse scope: util.ParseProjectTargetWithDefaultOrganization(ctx, targetArg); wrap parse errors with util.FlagErrorWrap.
  • Clients:
    • Task Agent: ctx.ClientFactory().TaskAgent(ctx.Context(), scope.Organization).
    • Prompter only if --prompt-value is supported/used.
  • Progress: ios.StartProgressIndicator(); defer ios.StopProgressIndicator() and stop before printing.
  • Update algorithm:
    • Find existing key case-insensitively.
    • If --from-json is set: load JSON (file, -, or inline), validate keys and apply changes; reject mixing with other flags.
    • Apply only explicitly-set bool flags (Flags().Changed(...)) and explicitly-present JSON keys.
    • Handle rename with collision checks.
    • Handle secret write-only semantics; never print or rely on server returning secret values.
    • For --clear-value: confirm unless --yes; return util.ErrCancel on decline.
    • Call TaskAgent.UpdateVariableGroup.
  • Output:
    • Template: create internal/cmd/pipelines/variablegroup/variable/update/update.tpl and render it (single-object output).
    • JSON: emit the updated variable group SDK model; ensure util.AddJSONFlags list matches the SDK JSON tags used in output.
  • Tests:
    • Add unit tests at internal/cmd/pipelines/variablegroup/variable/update/update_test.go.
    • Hermetic mocks: Task Agent client + prompter (only when needed).
    • Table-driven cases: non-secret value update, secret rotation via prompt, rename collision error, missing change flags validation error, read-only toggle tri-state, clear-value confirm/--yes behavior, --from-json mutual exclusivity + validation, secret value redaction in output.

Command Wiring

  • Implement internal/cmd/pipelines/variablegroup/variable/update/update.go exposing NewCmd(ctx util.CmdContext) *cobra.Command.
  • Register it from internal/cmd/pipelines/variablegroup/variable/variable.go so azdo pipelines variable-group variable update is part of the CLI hierarchy.

SDK / Client Requirements

  • Requires the Task Agent client (ClientFactory().TaskAgent(...)). If the client is missing, follow "Handling Missing Azure DevOps SDK Clients" in AGENTS.md.

References

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions