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:
- A
<server>block already has this alias: only<username>/<password>are rewritten, so<configuration>HTTP headers,<filePermissions>, and comments survive a routine token refresh. - A
</servers>closing tag on its own line: insert a block before it. - A self-closing
<servers/>: expand it into a real section. - 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. - 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
--durationbut ignored it, with a zero default so the generated docs would not advertise a value nothing read. It now defaults toartifactregistry.DefaultDurationand is range-checked throughartifactregistry.ValidateDurationon the--mavenpath.--dockerstill warns that the flag is ignored, and is still not range-checked, since nothing on that path sends the value. --dockerand--mavenare now mutually exclusive with one required, replacing step 4's singleMarkFlagRequired("docker"); the "no tool selected" guard against an explicit--flag=falsegeneralizes to both.
--maven reads GITLAB_TOKEN, --docker does not
This is the one asymmetry in the command, so options now carries two client builders:
apiClientisf.ApiClient, used by--maven. This command exchanges the token and writes it intosettings.xmlitself, so nothing re-resolves the credential afterwards and every credential glab accepts works.glab artifact-registry get-tokenis wired the same way.credHelperApiClientisapi.NewClientFromConfig(..., api.WithoutTokenFromEnvironment()), used by the--dockerverification exchange. That exchange is a rehearsal for what the Docker credential helper does atdocker pulltime, and the helper resolves its identity from the configuration file only. Verifying with aGITLAB_TOKENthe 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>matchedmavenServerBlockRebut notmavenServerCloseRe, 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.xmlshipped with apache-maven is almost entirely commented-out examples. The first</servers>could be inside a comment, and the<id>written there madefindMavenServerBlockclaim 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-onlysettings.xmlis 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.