Skip to content

gh issue artifact stack 7/10: Add create - #14570

Open
BagToad wants to merge 3 commits into
bagtoad/artifact-downloadfrom
bagtoad/artifact-create
Open

BagToad wants to merge 3 commits into
bagtoad/artifact-downloadfrom
bagtoad/artifact-create

Conversation

@BagToad

@BagToad BagToad commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Part of the pull request stack tracked in #14563.

This adds gh issue artifact create from #14525.

Description

Issue artifacts are Markdown documents and links attached to an issue. The previous pull requests added the gh issue artifact command group, list, view, delete and download. This one adds create, with the aliases add and new. It creates one artifact for each file or URL, in argument order:

$ gh issue artifact create 142 signin-plan.md 'docs/notes.md#Barista notes' https://github.com/monalisa/monas-cafe/wiki/OAuth-runbook
✓ Created artifact 6 (signin-plan.md) on monalisa/monas-cafe#142
✓ Created artifact 7 (Barista notes) on monalisa/monas-cafe#142
✓ Created artifact 8 (github.com) on monalisa/monas-cafe#142
  • A text file becomes a generic document named after the file. Text after # names it instead. --type plan saves documents as plans.
  • A URL becomes a link named after its host. A .url Internet Shortcut file, the kind download writes, becomes a link to its URL.
  • --body and --body-file create one artifact, and --name names it. Standard input is only read with --body-file -.
  • Artifact names can't contain some characters, such as parentheses. gh drops them from names it takes from file names, so plan (1).md is named plan 1.md. Names you type are sent as typed, and the server's error is shown if it refuses one.
  • --attach uploads images and videos and points each document's references at them, as it does for gh issue create. With several documents, each file is uploaded once, and every document that references it gets its URL. An attachment no document references has no one body to be appended to, so it's an error before anything uploads.
  • Piped, each artifact gets one line with the same five columns as delete and download. The source column is the file path, the URL, or - for standard input.

Every file is read and checked before any request. A binary or empty file, or a shortcut without an http(s) URL, fails the whole command, and nothing is created. After that, each artifact succeeds or fails on its own line, and gh exits 1 if any failed.

The API client gains Create. The shared package gains what edit will also need: reading new content with the binary and empty checks, reading .url files, and a result line for an artifact saved despite a failed upload.

How did you test this change?

I recorded the built gh creating artifacts on one issue in bagtoad/issue-artifacts-demo, each case checked with view or list: a file, #<name>, a cleaned-up name, several files, standard input, --body, a plan, a URL, a .url file, --attach replacing a reference and appending an image, two files sharing one upload, an attachment no file references, and piped output. It leaves out the other errors, failed uploads and saves, references resolved from other directories, symlinks, pull requests and GitHub Enterprise Server. Separately, I ran gh issue artifact create 28 signin-plan.md in the demo repository as an account that can read it but not write to it: it printed X Failed to create artifact from signin-plan.md: Not Found, exited 1, and the issue's artifacts didn't change.

recording.mp4

Open chapters and agent notes

Key points

  • #<name> is parsed like --attach alt text. An argument that exists as a path is used whole, so barista-notes#2.md works. Otherwise the longest existing path before a # wins. internal/attachments exports its parser as SplitFileArg, unchanged, so # means the same in both.

  • Names gh takes from file names fit the server's rule. nameFromFile keeps letters, numbers, accent marks, spaces, ., - and _, then trims the ends to a letter or number. A file name with none of them fails before any request and asks for #<name> or --name, or only --name for --body-file, which takes its path as typed. A URL is named after its host without the port, since names can't hold : or /. A host that still breaks the rule, such as ::1, gets the server's error on its line.

  • An upload can't be undone, so each artifact follows the --attach rule on its own. attach uploads every file once through UploadAndAttachDocuments, from earlier in this stack, and then decides for each document:

    for j, r := range results {
    	a := &artifacts[indexes[j]]
    	// Successful uploads are linked, and failed references are left alone
    	a.body = r.Markdown
    	a.attachErr = r.Err
    	// Only a document none of whose files uploaded isn't saved
    	a.skip = r.Err != nil && len(r.Uploaded) == 0
    }

    A document that is saved with only some of its files keeps its created status, and the upload error is its reason. So a script can tell the artifact exists and doesn't create it twice. In a terminal, PrintPartialFailure prints one line on stderr, and gh exits 1:

    ! Created artifact 6 (signin-plan.md) on monalisa/monas-cafe#142, but could not upload ./latte-art.png: rate limited; wait and try again
    
  • Upload errors keep their whole text on a result line. failureReason shortens an API error to the server's message, because the line already names the artifact. An upload error wraps an API error, but its own text names the file and explains the status, so now only an error that is itself an API error is shortened:

    // Before - also matched an upload error, which wraps one
    errors.AsType[api.HTTPError](err)
    // Now - only an error from an artifact request
    err.(api.HTTPError)

    delete and download get their errors straight from the client, so their lines don't change.

  • --attach still refuses a pull request after one lookup. The client's new UploadTarget makes the same lookup as IsPullRequest, and also asks for the repository ID and permission that an upload needs. Without --attach, create checks the issue like the other commands.

  • --attach records the same telemetry event as the other --attach commands. gh issue now passes its telemetry recorder to the artifact commands.

  • A positional - is refused. Standard input is only read with --body-file -, and - is how a result line names it. The error is "cannot create an artifact from -; use --body-file - to read standard input", shaped like --attach's "cannot attach standard input; --attach needs a file path". ./- still reads a file named -.

Notes for reviewers

Start with the create run function, then readSource, which works out each artifact's type, name and content, and attach after it. TestCreateRun runs each row in a temporary directory, with the client mock for artifact requests and HTTP stubs for uploads. It has rows for failed, partial and never-tried uploads, which the recording can't show.

The saved-despite-a-failed-upload line above isn't in #14525, and neither are these messages. Each follows the closest existing message:

  • --name cannot be blank and --body cannot be blank, like --output cannot be blank.
  • failed to create artifact from plan.md: the name after `#` cannot be blank.
  • failed to create artifact from ---: the file name has no letters or numbers; name it with `#<name>` or `--name` , which ends name it with `--name` for --body-file.
  • --attach can't be used with link artifacts, when every argument is a link.
  • failed to create artifact from standard input: input is empty, and ... binary input not supported.
  • broken.url: no URL in the [InternetShortcut] section, and broken.url: the shortcut doesn't link to an http(s) URL.
  • failed to create artifact from https://ラテ.example/menu: links must be http(s) URLs with a host, in printable ASCII with no spaces.
  • X Failed to create artifact (Root cause): Not Found for --body, and X Failed to create artifact from standard input: Not Found.
  • no file references latte-art.png or menu-photo.png; reference them in a file, or attach them when creating a single artifact, for more than one file.

Related issues:

Authorship and follow-up

Who wrote this:

  • A human wrote it.
  • An agent wrote it under close human direction.
  • An agent wrote it independently, and no human has guided the implementation beyond the initial prompt.

Who answers review comments:

  • @BagToad will read and reply directly. Name the account.
  • An agent will draft replies and @BagToad will read them before they are posted.
  • Nobody has explicitly committed to replying.

Create posts an artifact's type, name and body to an issue and returns
the new artifact.

UploadTarget is the issue lookup for commands that take --attach. Like
IsPullRequest it makes one lookup, which also returns the repository ID
and the viewer's permission that an upload needs.
gh issue artifact create names an artifact with <file>#<name>, parsed
exactly like --attach's alt text: a path that exists wins, then the
longest existing path before a #. One exported parser keeps # meaning
the same in both.
create makes one artifact for each file or URL argument, in argument
order. Text files become documents named after the file, or after
#<name>; URLs and .url Internet Shortcut files become links, and a URL
is named after its host. --body and --body-file create one artifact,
with --name, and --body-file - is the only way standard input is read.
--type plan saves documents as plans.

Every file is read and checked before any request, so a binary or empty
file, or a shortcut without an http(s) URL, fails the whole command.
Names gh takes from file names drop the characters artifact names can't
contain.

--attach uploads each file once for every document that references it,
through UploadAndAttachDocuments. A document is saved when any of its
files uploaded, since uploads can't be undone, and its line gives the
upload error as the reason. One whose files all failed isn't saved.

The shared package gains what edit will need too: reading new content
with the binary and empty checks, reading .url files, the lookup that
also serves --attach, and a result line for an artifact saved despite
a failure.
@BagToad
BagToad requested a review from a team as a code owner October 1, 2026 16:59
@BagToad
BagToad requested review from babakks and removed request for a team October 1, 2026 16:59
@BagToad
BagToad added this pull request to stack #14573 October 1, 2026 16:59

This branch has not been deployed

No deployments
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.

1 participant