feat(artifact-registry): add glab artifact-registry login --maven

Step 5 of artifact-registry#222. Step 4 merged in !3705 (merged), so this MR targets main and no longer stacks.

Adds --maven to glab artifact-registry login. It upserts a <server> block into ~/.m2/settings.xml keyed by <id>, authenticating as __token__ with the exchanged token. Editing is textual, not encoding/xml: parsing and re-marshalling would reformat the file and drop comments, and one of the tests exists specifically to prove a user's comment survives.

Handles the settings.xml shapes that occur in practice, in this order:

  1. A <server> block already has this alias: only <username>/<password> are rewritten, so <configuration> HTTP headers, <filePermissions>, and comments survive a routine token refresh.
  2. A </servers> closing tag on its own line: insert a block before it.
  3. A self-closing <servers/>: expand it into a real section.
  4. A <servers> element none of the above can place a block into (for example <servers></servers> on one line): refuse with an actionable message rather than risk a second <servers> element.
  5. No <servers> at all: add a section before </settings>.

Every error path leaves the file byte-for-byte intact, since nothing is written until the whole update is computed.

Also lands --registry-alias (defaulting to a name derived from --registry; validated when given explicitly), and gives --duration its first consumer:

  • Step 4 registered --duration but ignored it, with a zero default so the generated docs would not advertise a value nothing read. It now defaults to artifactregistry.DefaultDuration and is range-checked through artifactregistry.ValidateDuration on the --maven path. --docker still warns that the flag is ignored, and is still not range-checked, since nothing on that path sends the value.
  • --docker and --maven are now mutually exclusive with one required, replacing step 4's single MarkFlagRequired("docker"); the "no tool selected" guard against an explicit --flag=false generalizes to both.

--maven reads GITLAB_TOKEN, --docker does not

This is the one asymmetry in the command, so options now carries two client builders:

  • apiClient is f.ApiClient, used by --maven. This command exchanges the token and writes it into settings.xml itself, so nothing re-resolves the credential afterwards and every credential glab accepts works. glab artifact-registry get-token is wired the same way.
  • credHelperApiClient is api.NewClientFromConfig(..., api.WithoutTokenFromEnvironment()), used by the --docker verification exchange. That exchange is a rehearsal for what the Docker credential helper does at docker pull time, and the helper resolves its identity from the configuration file only. Verifying with a GITLAB_TOKEN the helper ignores would check an identity Docker never uses.

--maven's paragraph in Long states the difference, so it is documented rather than left for a user to discover.

Review fixes on top of the first round

Two follow-up commits, each with tests verified to fail against the pre-fix source.

7dbb3c57d closes three shapes where --maven reported success while writing nothing usable:

  • A one-line <server><id>x</id></server> matched mavenServerBlockRe but not mavenServerCloseRe, which needs </server> at the start of a line, so both credential inserts were skipped and the command printed "Configured Maven server" over an untouched file. It now refuses, in the same style as the existing <servers> refusal. The variant where only the last child shares the closing line used to write a <username> and no <password>; both are now placed or neither is.
  • Nothing was comment-aware, which matters because the settings.xml shipped with apache-maven is almost entirely commented-out examples. The first </servers> could be inside a comment, and the <id> written there made findMavenServerBlock claim the commented block on every later refresh, so the live section never received credentials. A commented-out <server> for the same alias absorbed the refresh, and a commented-out <password> ahead of the live one took the replacement, leaving Maven on the expiring token. Comment spans are now computed once per file and matches inside one are skipped.
  • Smaller: a CRLF file's <servers/> line is consumed whole rather than leaving a line holding only a carriage return, and an empty or whitespace-only settings.xml is treated like a missing one.

47af9d2be makes ExchangeToken require the JWS compact form before decoding claims. Go's base64 decoder skips \r and \n as insignificant whitespace, so a token carrying them in its signature or payload segment decodes cleanly with valid exp/iss/sub and reaches the caller intact, and every caller writes that token verbatim into a line-oriented credential file. The reachable outcome is a corrupted credential file, not a redirected registry: =, :, " and space are already rejected by the base64 decode, so only [A-Za-z0-9_-] and the line breaks survive. The guard lives in internal/api/artifactregistry rather than here because step 6's .npmrc, gradle.properties and credentials.sbt writers need the same protection.

Known limitation, pre-existing and out of scope: a <servers> section inserted into a CRLF settings.xml uses \n, so such a file ends up with mixed line endings.

🤖 Generated with Claude Code

Edited by Sylvia Shen

Merge request reports

Loading
Loading