Repository navigation
Conversation
Harden filesystem writes against symlinks
A Markdown file's relative links are written relative to the file, but a --body-file had to reference attached images relative to the working directory instead. References in a --body-file now resolve against the file's directory first and the working directory second, so references that matched before still match. --body and standard input still resolve against the working directory. The directory is a parameter of UploadAndAttach, so all six --attach commands resolve references the same way.
A command that takes several Markdown files, such as the planned `gh issue artifact create`, needs --attach to work across all of them. UploadAndAttachDocuments uploads each file once, in order, stopping at the first failure, and gives every document that references a file the same URL. Each document resolves its references against its own directory and gets its own result, so the command can decide whether to save it. With several documents, a file that no document references has no body to be appended to. It is refused before anything uploads, with an error that names it. With one document, unreferenced files are still appended, and UploadAndAttach now runs through the same code with one document.
Commands reach the issue artifacts REST API through the ArtifactClient interface, so their tests can use the generated moq mock, as gh discussion does. List asks for pages of up to 100 artifacts until one comes back short. Every request uses the same page size, so page numbers stay aligned when the limit is above 100.
gh issue artifact is a new command group, in preview, for the Markdown documents and links attached to an issue. It starts with list. gh treats generic and plan artifacts alike as documents, so --type generic and --type plan list both, merged in number order. Without a total count from the API, the header counts the artifacts shown.
Get returns one artifact with the versions the API returns alongside it, newest first, for gh issue artifact view. Only a single artifact has an edit history, since list responses don't include one, so its --json fields add versions to the artifact fields.
view shows an artifact like gh issue view: its name and issue, who created and last edited it, its body rendered as Markdown or a link's URL, and its edit history. --version shows an earlier version from that history. Piped output has key lines, then the body, and --json adds the versions field. Artifacts have no web page of their own, so the footer links to the issue and --web only opens link artifacts. The shared package gains three checks later commands need too: the artifact number argument, refusing types this version of gh doesn't know, and the strict http(s) URL check for links.
Delete removes one artifact from an issue, for gh issue artifact delete. The API can't restore a deleted artifact.
delete asks before deleting, since a deleted artifact can't be restored. It looks up every number first, so the prompt names each artifact, and without --yes a number that can't be found stops the command before anything is deleted. --yes skips the prompt and is required when gh can't prompt, like gh label delete. Commands that act on several artifacts report one line per artifact. The shared package gains those result lines, so each such command prints the same terminal lines and the same five-column piped lines.
download --version follows the same rules as view --version, so both commands use one lookup.
Download writes each artifact to <number>-<name>.md, or .url for a link, never through a symlink. The shared result lines gain a skipped line for --skip-existing.
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.
Update saves a new version of one artifact. Its PATCH request sends only the name or body it changes, since the API keeps any field a request leaves out. An artifact's type can't change.
gh issue artifact edit needs the uploader create builds: the one issue lookup that refuses a pull request and returns the repository ID and permission an upload needs, then the checks attachments.NewUploader makes. shared.NewUploader now builds it for both.
edit renames an artifact with --name, replaces its content with --body or --body-file, or both, and saves a new version. --body-file - is the only way standard input is read, and a file never renames the artifact. The type never changes, so it is enforced before anything is uploaded or saved. A link's new content must be an http(s) URL: a .url file is read as an Internet Shortcut, and anything else must be a bare URL, with its surrounding whitespace trimmed. A document never takes a .url file, and a link can't take --attach. Types gh doesn't know are refused. --attach on its own appends the files to the current body, like gh issue comment --edit-last --attach. With new content, it points the content's references at the uploads. The artifact is saved when any file uploaded, since uploads can't be undone, and the upload error is the reason on its line. edit reports its artifact on one result line in the format create, delete and download share. A failed lookup or save is a failed line, like in delete.
The gh skill had no guidance for the gh issue artifact commands, and its --attach section listed only the six earlier commands. Add an issue artifacts section with each command's usage and the rules that apply across them, and note how --attach differs for gh issue artifact create and edit.
Resolve go.mod/go.sum conflicts by keeping trunk's dependency versions and re-pinning go-gh to v2.16.2-0.20260914233729-2ce792192e43. The old pseudo-version was v2.16.1-0.*, which sorts below the v2.16.1 release. The cli/oauth pin is unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
v0.0.27 eagerly built a 2.2MB width lookup table in init(), adding ~12ms to every gh invocation. v0.0.30 builds it lazily. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Bump go-runewidth to v0.0.30 for faster startup
gh issue artifact edit --restore-version saves nothing when an artifact already matches the version. PrintUnchanged reports that: in a terminal, a warning on stderr like gh issue close on a closed issue, so stdout stays empty, and when piped, a skipped line with the reason. PrintSkip doesn't fit, since its terminal line goes to stdout.
--restore-version saves an earlier version's name and body as a new version. The version comes from the edit history that the artifact's lookup already returns, so a restore makes no extra request, and a version that isn't there fails before anything is saved. The name and body are sent as returned, so the server checks them like any other edit. Every save adds a version to the edit history, even when nothing changed. So nothing is saved when the artifact already matches the version, whether that's the current version or an older one with the same name and body. The flag takes the same values as view --version, and can't be combined with --name, --body, --body-file or --attach.
Add the flag to the gh issue artifact edit entry, and the restored status to the piped result lines.
Signed-off-by: reginaldalfret <reginaldalfret@gmail.com>
…ix-shell docs: recommend nix-shell and declarative config for Nix/NixOS
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
FuzzyIn chained truncating integer divisions, so a duration a hair under a round boundary read one unit short: a 6 month refresh token reported "in about 5 months" a second after issuance. Round each unit to the nearest whole value instead, and report a value that rounds up to the next unit's threshold in that larger unit so it never reads "in about 60 minutes" or "in about 12 months". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Both successful expiring-token text fixtures set a refresh token expiry, so the guard that omits the refresh token expiry line was only reached through the rejected-token path. Add a fixed-clock case that asserts the access token annotation is shown and the refresh token expiry line is absent, and a --json case that asserts refreshTokenExpiresAt is omitted while tokenExpiresAt is kept. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Refreshable tokens: show relative time-to-expiry in `gh auth status`
Add an internal/debug package that exposes Config, which returns a DebugConfig holding internal debugging overrides as Option values, and Enabled, which reports whether gh was built with the ghdebug build tag. In ghdebug builds Config reads GH_INTERNAL_AT_EXPIRES_IN and GH_INTERNAL_RT_EXPIRES_IN as token lifetimes in seconds and GH_INTERNAL_DEBUG_REFRESH_TOKEN as a boolean, leaves unset overrides as None, and panics on invalid values. In regular builds every override is always None, so the overrides cannot be enabled in released binaries. The tag is prefixed with gh to avoid colliding with a generic debug tag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Add "make debug", which builds bin/gh like "make" but with the ghdebug build tag so the internal debug overrides are available. It removes bin/gh first because script/build skips rebuilding when no source file has changed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Append "[debug build]" to the first line of gh version and gh --version when gh is built with the ghdebug build tag, so a debug binary is easy to tell apart from a regular one. Regular builds are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Replace the ad hoc os.Getenv testing hacks with the build-gated internal/debug configuration. The access and refresh token lifetime overrides in internal/config and the verbose refresh logging switch in the factory now come from GH_INTERNAL_AT_EXPIRES_IN, GH_INTERNAL_RT_EXPIRES_IN, and GH_INTERNAL_DEBUG_REFRESH_TOKEN, and have no effect in builds without the ghdebug tag. Update the refreshable-token e2e scripts to use the new variable names, rename their own settings to GH_INTERNAL_E2E_BIN, GH_INTERNAL_E2E_HOST, and GH_INTERNAL_E2E_ALLOW_VSCODE, and refuse to run unless gh version reports a debug build, pointing at "make debug" otherwise. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Check that gh auth token prints the expiry warning on stderr whenever it prints a stored expiring token, and that the warning is absent when no stored expiring token is printed, such as with GH_TOKEN. Also run gh auth refresh both with and without --expiring and assert that each receives an expiring refreshable token. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
script/build skipped rebuilding bin/gh whenever no source file was newer than the binary, regardless of build tags. A plain `make` straight after `make debug` therefore kept the debug binary. Compare the tags recorded in the existing binary's build info with GO_BUILDTAGS and rebuild when they differ. This also lets `make debug` drop its `rm -f bin/gh` workaround. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Move the environment parsing into an unexported configFromEnv in the untagged config.go. The ghdebug implementation of Config now just calls it, while the release implementation still returns an empty DebugConfig. This lets the regular test run cover all parsing and panic cases on every OS without the ghdebug tag. The tagged test only checks that Config reads the real environment. Release builds never reference configFromEnv, so the linker still drops the parser and the variable names from them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
The ghdebug implementation of internal/debug is not compiled by the regular test run or build, so it could silently stop compiling. Build ./cmd/gh with the tag on the Ubuntu runner to catch that. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Refreshable tokens: build-gated debug overrides for testing token expiry
cli/oauth v1.3.0 exposes the access and refresh token expiry fields as time.Duration values, so convert them when building the stored credential. Refresh now keeps the previous refresh token when the server omits a new one, which the godoc and the omitted refresh token test reflect. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Lip Gloss v2 includes borders in Width, which made the bordered left column one cell narrower and wrapped truncated rows. Widen it by one cell and restore the original test expectations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Refreshable tokens: bump cli/oauth to v1.3.0 and fix `gh status` column width
Teach agents how gh reports and refreshes expiring tokens, which hosts support them, why `gh auth token` output must not be cached, and how to handle a rejected refresh token. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
Clarify behavior of `gh auth status --json` regarding token refresh and expiration. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Document expiring tokens in the gh skill
Keep the pre-install disclaimer and present post-install review guidance without a second warning. Preserve pinned preview commands and cover local and remote output in TTY and non-TTY modes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c5ee8a8a-f0e8-432e-8ebb-62ff72712b41
Avoid repeating the skill installation safety warning
Refresh the pinned revision and module checksums. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 437a79bf-e43d-4730-9528-13e90c8bcc9a
Update github.com/digitorus/pkcs7 dependency
Copilot-Session: 9524ec94-45f1-4a7a-aff5-fd85cda6ff2b
Print a one line hint on stderr pointing users to --expiring and the changelog post (https://gh.io/gh-expiring-tokens). The hint only shows when stderr is a TTY, neither --expiring nor expiring_credentials is set, and the host is not GitHub Enterprise Server. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac178b5a-5b62-49bd-9e83-4796c7ddec46
feat(gh auth login): hint at `--expiring` when no preference is set
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Allow installation tokens to attach files
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 664e416d-0c36-420b-a93c-dbfd8ff83769
…okens Defer `--attach` credential checks to the server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )