glab release create and upload panic on a null entry in --assets-links

Summary

When the JSON array given to --assets-links contains null, glab release create and glab release upload crash with panic: runtime error: invalid memory address or nil pointer dereference and exit status 2. Both commands decode the flag into []*upload.ReleaseAsset (create.go#L244-249, upload.go#L138-143), so a null element becomes a nil pointer, and the commands dereference every link later on, first in aliasFilePathToDirectAssetPath (releaseutils/upload/upload.go#L270-271).

By the time glab crashes, it may already have changed the release:

  • glab release upload adds the links that come before the null one.
  • glab release create on an existing release updates the release and adds the links that come before the null one.
  • glab release create for a new release creates it and adds the links that come before the null one in v1.124.0. On main, which sends the links in the create request since !4040 (merged), it crashes before the release is created.

So a null in the array can leave the release partly updated, and what the user sees is a Go stack trace rather than an error about the flag.

Environment

  • OS: Linux x86_64 (a golang:1.27.3 container)
  • SHELL: /bin/bash
  • TERM: none (the commands ran without a terminal)
  • GLAB: v1.124.0, built from the tag, and main at a6fd50ce

Other: I did not run this against a GitLab instance. glab talked to a small local HTTP server that answers the tag and release endpoints the way GitLab does and logs every request it receives, which is where the request lists below come from.

Steps to reproduce

In a project where you can create releases and that has the tag v1.0.0 (from a clone of it, or with -R <group>/<project>):

# A release that does not exist yet
glab release create v1.0.0 --notes x --assets-links='[{"name":"a","url":"https://example.com/a"}, null]'

# A release that exists. On main the command above does not create it,
# so create it first with: glab release create v1.0.0 --notes x
glab release create v1.0.0 --notes x --assets-links='[{"name":"b","url":"https://example.com/b"}, null]'
glab release upload v1.0.0 --assets-links='[{"name":"c","url":"https://example.com/c"}, null]'

What is the current bug behavior?

Each command panics and exits with status 2. The first one with v1.124.0, with the progress symbols removed and the stack trace shortened:

Validating tag v1.0.0
Creating or updating release repo=group/proj tag=v1.0.0
Release created:	url=http://127.0.0.1:18081/group/proj/-/releases/v1.0.0
Uploading release assets repo=group/proj tag=v1.0.0
Added release asset	name=a url=https://example.com/a
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x15de74f]

goroutine 1 [running]:
gitlab.com/gitlab-org/cli/internal/commands/release/releaseutils/upload.aliasFilePathToDirectAssetPath(...)
	internal/commands/release/releaseutils/upload/upload.go:220
gitlab.com/gitlab-org/cli/internal/commands/release/releaseutils/upload.CreateLink(...)
	internal/commands/release/releaseutils/upload/upload.go:54 +0x4f
gitlab.com/gitlab-org/cli/internal/commands/release/releaseutils/upload.(*Context).CreateReleaseAssetLinks(...)
	internal/commands/release/releaseutils/upload/upload.go:196 +0x152
gitlab.com/gitlab-org/cli/internal/commands/release/releaseutils.CreateReleaseAssets(...)
	internal/commands/release/releaseutils/releaseutils.go:132 +0x450
gitlab.com/gitlab-org/cli/internal/commands/release/create.createRun(...)
	internal/commands/release/create/create.go:563 +0x1d48
[...]

The requests each command sent before the panic:

Command v1.124.0 main at a6fd50ce
release create, new release GET tag, GET release, POST release, POST link a GET tag, GET release
release create, existing release GET tag, GET release, PUT release, POST link b GET tag, GET release, PUT release, POST link b
release upload GET release, POST link c GET release, POST link c

On main the first command panics in upload.AliasFilePaths (create.go#L528) and the other two in upload.CreateLink, all at the same line of aliasFilePathToDirectAssetPath.

What is the expected correct behavior?

The command refuses the flag before it sends any request, the way it already refuses malformed JSON (failed to parse JSON string: ...), and names the element that is null.

Relevant logs and/or screenshots

The output and request lists above.

Possible fixes

Decode --assets-links in one helper that both commands call, next to releaseutils.AssetsFromArgs, and return an error there for a null element, with its index. Both commands parse the flag before their first request (complete in create, validate in upload), so the command then fails before it changes anything. I have a merge request ready with this change and a test in each command.