Skip to content

fix(cli)!: address CLI and config robustness gaps (breaking: JSON-mode buy/create now requires --yes)#537

Open
Phil-Browne wants to merge 17 commits into
mainfrom
esd-1649-cli-config-hygiene
Open

fix(cli)!: address CLI and config robustness gaps (breaking: JSON-mode buy/create now requires --yes)#537
Phil-Browne wants to merge 17 commits into
mainfrom
esd-1649-cli-config-hygiene

Conversation

@Phil-Browne

Copy link
Copy Markdown
Contributor

Hardens a batch of small robustness gaps in the config, login, and output plumbing found during a hygiene pass.

  • Credentials file is now written atomically (temp file + rename) and re-chmodded to 0600 on every save, so a crash mid-write can't corrupt it and an over-permissive file gets tightened back up.
  • --env login now requires both the access key and secret key to come from the same source, instead of possibly mixing one from the environment and one from the active profile.
  • locations get skips nil entries in the response instead of risking a panic.
  • Exit-code classification now keys off typed errors (a typed usage error from cobra's flag parsing, and a typed *ValidationError from ParseInt) instead of matching substrings like "invalid" or "arg(s)" against error text, so an API error that happens to contain those words won't be misclassified as a usage error.
  • Every buy/create command (port, LAG port, VXC, MVE, IX, MCR, NAT Gateway) now requires --yes to proceed when using --json/--json-file, closing a gap where JSON-mode input skipped purchase confirmation entirely.
  • The docs renderer's temp-file cleanup is now driven by an explicit isTemp flag instead of a path substring check.
  • printXML now sanitizes struct tag names against the XML Name grammar (with collision disambiguation) before using them as element names, since Go's encoding/xml doesn't validate this itself.

Test plan

  • go build ./...
  • go test ./...
  • golangci-lint run
  • GOOS=js GOARCH=wasm go build -tags js,wasm ./...

Save() now writes to a temp file in the same directory and renames it
into place, so a crash mid-write cannot corrupt the credentials file.
It also chmods the file to 0600 on every save, so a pre-existing
over-permissive file gets re-tightened rather than left as-is.
Previously --env login could take the access key from the environment
while falling back to the active profile for the secret key (or vice
versa) when only one var was set. Both halves must now come from the
same source, or login errors instead of silently mixing credentials.
locations get iterated the raw []*megaport.LocationV3 without the nil
check that filterLocations already applies, so a nil entry in the
response could panic. Skip nil entries the same way filterLocations
does.
Exit-code classification previously matched "unknown flag"/"invalid"/
"arg(s)" against error text, so an API error that happened to contain
those words could be misclassified as a usage error. Flag-parse
failures and missing-required-flag errors are now tagged as a typed
usage error at the point cobra generates them (SetFlagErrorFunc and a
proactive ValidateRequiredFlags check), and ParseInt now returns a
typed *ValidationError instead of a plain error, so classifyError can
key off error types instead of wording.

isCobraUsageError's substring match remains only as a fallback for the
"unknown command" and "arg(s)" cases that run before those hooks and
have no typed equivalent.
JSON and json-file input skipped the purchase confirmation prompt
entirely, so a JSON-mode buy without --yes went straight through with
no interactive prompt and no explicit acknowledgement. Every buy/create
command (port, LAG port, VXC, MVE, IX, MCR, NAT Gateway) now requires
--yes to proceed in JSON mode, and returns a usage error otherwise.

The check runs as early as each function's data flow allows, ahead of
login and live validation calls, so a doomed request fails fast
instead of spending a network round trip first.
FindDocFile decided whether to clean up a doc file by testing its path
for a "megaport-docs-" substring, which is fragile if a real doc file
or working directory happened to contain that string. It now returns
an explicit isTemp bool and cleanup is driven by that instead.
printXML wrote struct json tag names directly as XML element local
names. Go's encoding/xml does not validate element names at encode
time, so a tag starting with a digit, containing punctuation, or
colliding with another tag after sanitization would silently produce
malformed or ambiguous XML. Sanitize each name against the XML Name
grammar (with collision disambiguation across a struct's fields) once
per printXML call, for both the native and WASM renderers.
Copilot AI review requested due to automatic review settings July 10, 2026 20:59
@Phil-Browne Phil-Browne requested review from a team and penzeliz-megaport as code owners July 10, 2026 20:59
@Phil-Browne Phil-Browne requested a review from ianb-mp July 10, 2026 20:59
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.35593% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.90%. Comparing base (9ba6a3b) to head (2259229).
⚠️ Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
internal/commands/config/manager.go 31.57% 7 Missing and 6 partials ⚠️
internal/base/cmdbuilder/docs_render.go 33.33% 5 Missing and 1 partial ⚠️
internal/commands/ports/ports_actions.go 75.00% 1 Missing and 1 partial ⚠️
...ternal/commands/nat_gateway/nat_gateway_actions.go 75.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #537      +/-   ##
==========================================
+ Coverage   78.57%   78.90%   +0.33%     
==========================================
  Files         192      193       +1     
  Lines       18522    18646     +124     
==========================================
+ Hits        14553    14712     +159     
+ Misses       2904     2863      -41     
- Partials     1065     1071       +6     
Files with missing lines Coverage Δ
cmd/megaport/megaport.go 69.35% <100.00%> (+0.84%) ⬆️
internal/base/cmdbuilder/flagsets.go 92.15% <100.00%> (ø)
internal/base/output/common.go 79.64% <100.00%> (+2.08%) ⬆️
internal/base/output/output.go 70.25% <100.00%> (+0.18%) ⬆️
internal/commands/config/login.go 92.41% <100.00%> (+4.49%) ⬆️
internal/commands/ix/ix_actions.go 93.55% <100.00%> (ø)
internal/commands/locations/locations_actions.go 85.02% <100.00%> (+0.14%) ⬆️
internal/commands/mcr/mcr_actions.go 81.63% <100.00%> (ø)
internal/commands/mve/mve_actions.go 77.60% <100.00%> (ø)
internal/commands/nat_gateway/nat_gateway.go 100.00% <100.00%> (ø)
... and 8 more

... and 16 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI 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.

Pull request overview

This PR hardens several CLI robustness edges across config persistence, credential selection, exit-code classification, confirmation flow for JSON-driven purchases/creates, and XML output generation.

Changes:

  • Make config saves atomic (temp file + rename) and re-tighten permissions to 0600.
  • Require --yes when using --json/--json-file for buy/create flows to prevent accidental purchases/creates without confirmation.
  • Improve robustness via typed/structured errors (validation + cobra flag parsing) and sanitize XML element names derived from struct tags.

Reviewed changes

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

Show a summary per file
File Description
internal/validation/parse.go Switch ParseInt failures to typed *ValidationError.
internal/validation/parse_test.go Update ParseInt tests to assert typed validation errors and updated messaging.
internal/commands/vxc/vxc_actions.go Require --yes for JSON-mode VXC purchases; keep interactive confirmation for non---yes.
internal/commands/vxc/vxc_actions_test.go Update/add tests covering JSON + --yes and JSON without --yes usage error.
internal/commands/users/users_actions_test.go Update expected validation error casing (“Invalid …”).
internal/commands/ports/ports_prompts_test.go Update expected validation error casing (“Invalid …”).
internal/commands/ports/ports_actions.go Require --yes for JSON-mode port/LAG purchases; keep confirmation for non---yes.
internal/commands/ports/ports_actions_test.go Update/add tests for JSON + --yes and JSON without --yes usage error.
internal/commands/partners/partners_actions_test.go Update expected validation error casing (“Invalid …”).
internal/commands/nat_gateway/nat_gateway_test.go Require --yes for JSON-mode NAT Gateway create; add negative test.
internal/commands/nat_gateway/nat_gateway_additional_test.go Ensure JSON-mode NAT Gateway create tests set --yes.
internal/commands/nat_gateway/nat_gateway_actions.go Enforce --yes requirement in JSON/JSON-file mode; simplify confirmation gating.
internal/commands/mve/mve_prompts_test.go Update expected validation error casing (“Invalid …”).
internal/commands/mve/mve_actions.go Require --yes for JSON-mode MVE purchase; keep confirmation for non---yes.
internal/commands/mve/mve_actions_test.go Update/add tests for JSON + --yes and JSON without --yes usage error.
internal/commands/mcr/mcr_prompts_test.go Update expected validation error casing (“Invalid …”).
internal/commands/mcr/mcr_actions.go Require --yes for JSON-mode MCR purchases; keep confirmation for non---yes.
internal/commands/mcr/mcr_actions_test.go Update/add tests for JSON + --yes and JSON without --yes usage error; update casing.
internal/commands/mcr/mcr_actions_prefix_filter_test.go Update expected validation error casing (“Invalid …”) for stderr assertions.
internal/commands/locations/locations_actions.go Skip nil location entries to avoid panic when scanning response.
internal/commands/locations/locations_actions_test.go Add coverage ensuring nil entries don’t panic; update casing.
internal/commands/ix/ix_test.go Update expected validation error casing (“Invalid …”).
internal/commands/ix/ix_actions.go Require --yes for JSON-mode IX purchases; keep confirmation for non---yes.
internal/commands/ix/ix_actions_test.go Update/add tests for JSON + --yes and JSON without --yes usage error.
internal/commands/config/manager.go Implement atomic config save (temp + rename) and re-chmod to 0600.
internal/commands/config/manager_test.go Adjust read-only test to directory permissions; add tests for chmod retightening and temp cleanup.
internal/commands/config/login.go Prevent mixing env/profile credential halves when --env is used; error on partial env vars.
internal/commands/config/login_test.go Add tests ensuring partial env vars error and don’t mix with profile.
internal/base/output/output.go Sanitize XML element names before encoding (non-WASM).
internal/base/output/output_wasm.go Sanitize XML element names before encoding (WASM).
internal/base/output/output_test.go Add tests for illegal/colliding XML element names and sanitizer behavior.
internal/base/output/common.go Add XML name sanitization + collision disambiguation helpers.
internal/base/cmdbuilder/docs_render.go Replace temp-file cleanup substring heuristic with explicit isTemp flag.
cmd/megaport/megaport.go Tag cobra flag-parse/required-flag errors as typed usage errors (early), reducing substring-based classification.
cmd/megaport/megaport_test.go Add tests asserting unknown flags / missing required flags become typed usage errors.

Comment thread internal/commands/config/login.go
Comment thread internal/validation/parse.go
Comment thread internal/base/output/output_test.go Outdated
Comment thread internal/base/output/output_test.go Outdated
- quote the offending value in ParseInt's validation error so an
  empty or whitespace-only input isn't ambiguous
- wrap the --env partial-credential guard in a typed usage error
  instead of a plain error
- assert io.EOF specifically when checking XML output is
  well-formed, so a malformed document with an early syntax error
  can't slip past the same as a valid one
- add exit-code assertions to the partial-env-var login tests to
  cover the new typed usage error

Copilot AI 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.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 1 comment.

Comment thread internal/commands/config/manager.go Outdated
os.File.Write can return n < len(data) with a nil error; check the
byte count so a short write doesn't get silently renamed into place
as the new config file.

Copilot AI 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.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 1 comment.

Comment thread internal/commands/config/manager.go

Copilot AI 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.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 1 comment.

Comment thread internal/validation/parse.go Outdated
Pre-formatting the value with %q before handing it to
NewValidationError corrupted the typed field: any caller inspecting
ValidationError.Value got a quoted/escaped display string instead of
the actual offending input. Pass the raw value through, matching
every other validator in the package.

Copilot AI 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.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 1 comment.

Comment thread internal/commands/config/login_test.go
The "neither env var set" subtest reached the real Authorize() call
and relied on whatever network error came back, making it
environment-dependent and possibly slow or flaky under CI egress
restrictions. Point utils.BaseURL/utils.TokenURL at a local
httptest server returning 401 so it fails fast and deterministically
while still proving the code path reaches Authorize.

Copilot AI 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.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 3 comments.

Comment thread internal/validation/parse.go
Comment thread cmd/megaport/megaport.go Outdated
Comment thread internal/commands/ports/ports_actions.go Outdated
…rror

Keeps ValidationError.Value raw (the caller passes the original string
unmodified) but quotes it in Error() only when it is empty or
whitespace-only, so the message doesn't read as a bare, ambiguous dash.
The comment claimed every RunE in the codebase is wrapped by
utils.Wrap*. The cmdbuilder-injected docs subcommand and the
--generate-skeleton bypass are unwrapped exceptions; note them and why
the substring match is still safe.
BuyPort, BuyLAGPort, BuyIX, BuyMCR, BuyMVE, and BuyVXC each repeated
the same --yes check for JSON-mode purchases. Extract it into
utils.RequireYesForJSONBuy.

Copilot AI 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.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.

Copilot AI 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.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated 2 comments.

Comment thread internal/commands/config/manager_test.go
Comment thread internal/commands/config/manager_test.go
TestReadOnlyConfigFile and TestSaveRetightensOverPermissiveFile rely on
chmod semantics and permission-denied error text that don't hold on
Windows, so skip them there rather than let them fail or be
meaningless for local Windows contributors.

Copilot AI 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.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.

Copilot AI 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.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated 1 comment.

Comment thread cmd/megaport/megaport_test.go
… state

This test runs the real rootCmd through PersistentPreRunE, which reads
the config dir and mutates shared output config globals. Point it at
a temp config dir and reset output state afterward, matching the
sibling PersistentPreRunE tests in this file.

Copilot AI 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.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.

Copilot AI 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.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.

@penzeliz-megaport

Copy link
Copy Markdown
Collaborator

Code is sound; the behaviour change needs a release note. RequireYesForJSONBuy (internal/utils/utils.go:326) makes a JSON-mode buy without --yes error, where before JSON auto-confirmed. Any existing automation running <x> buy --json ... without --yes starts exiting non-zero. Intended, but breaking: document it in the changelog and bump the version accordingly.

Flag help text and generated docs for every buy/create command didn't
mention that --yes is required in JSON mode, even after the enforcement
went in. Update the shared --yes flag description and NAT Gateway's
create flag, then regenerate docs/.
@Phil-Browne Phil-Browne changed the title ESD-1649: Address CLI and config robustness gaps fix(cli)!: address CLI and config robustness gaps (breaking: JSON-mode buy/create now requires --yes) Jul 13, 2026
@Phil-Browne

Copy link
Copy Markdown
Contributor Author

Agreed this needs to be surfaced, but CHANGELOG.md's [Unreleased] section is auto-generated by the release workflow (scripts/update-changelog.sh) from squash-merge commit subjects at tag time, not hand-edited per PR, so I didn't add a bullet there directly. Instead:

  • Retitled the PR to fix(cli)!: address CLI and config robustness gaps (breaking: JSON-mode buy/create now requires --yes). Since this repo squash-merges, that title becomes the commit subject on main, so the ! and "breaking" callout will carry through into the generated release notes/changelog when the next version is tagged.
  • Added a commit documenting the --yes requirement in the flag help text for every affected buy/create command (port, LAG port, VXC, MVE, IX, MCR, NAT Gateway) and regenerated docs/, so it's visible in --help output and the generated CLI docs, not just the code.

Version bumps aren't done per-PR here, they happen via a git tag cut in a separate release-prep step, so there's nothing to bump in this branch.

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