cli / go-gh Goto Github PK
View Code? Open in Web Editor NEWA Go module for interacting with gh and the GitHub API from the command line.
Home Page: https://pkg.go.dev/github.com/cli/go-gh/v2
License: MIT License
A Go module for interacting with gh and the GitHub API from the command line.
Home Page: https://pkg.go.dev/github.com/cli/go-gh/v2
License: MIT License
Terminal users who are not terminal savvy finding the environment variable only approach to overriding the default host logic confusing.
For multi-account users whose primary GitHub host is not github.com
, they must use GH_HOST
environment variable to target the appropriate host for core GitHub CLI commands or GitHub CLI extensions without repository context. It is also possible for core GitHub CLI commands and GitHub CLI extension authors to add support for --hostname
flag to explicitly communicate this can be set / overridden but must be added intentionally.
The problem comes in for terminal users who are not savvy to working with terminals:
This setting must be managed separately from hosts authenticated via gh auth login
if the user gh auth logout
from this host, then errors occur
Shells offer different levels of scoping environment variables
GH_HOST=... gh api ...
)export GH_HOST=...
).zshrc
, .bashrc
, ~/.config/powershell/profile.ps1
)Inconsistent experiences for users unfamiliar with login sessions versus non-interactive sessions
.zprofile
vs .zshrc
, .bashrc
vs .bash_profile
What if we could indicate which host should be the default?
$ cat ~/.config/gh/hosts.yml
github.ghe.com:
default: true
git_protocol: https
users:
andyfeller:
user: andyfeller
github.com:
git_protocol: https
users:
andyfeller:
user: andyfeller
In the example above, I want to specify that github.ghe.com
is my default host in the event that the host can't be determined by repository context.
This would involve change to the following logic for determining default host:
Lines 130 to 150 in dbd982e
I've noticed what seems like some unexpected behavior when using OAuth (start with gho_
) or App (start with ghs_
) tokens. I wrote an extension that utilizes the ClientOptions
struct and when I provide ClientOptions.AuthToken
for either OAuth or App generated tokens, I get a 401.
Upon analyzing what's taking place, it appears that the authorization header that go-gh
is using has token
instead of bearer
. This works for PATs, but not for the aforementioned token types.
To get around this I had to set ClientOptions.AuthToken
and then also override ClientOptions.Headers
providing the Authorization
:bearer <token>
key-value manually.
If I try to just add the header key-value override, the lib falls back to using built-in authentication (gh auth login
) instead of the provided token.
At least, this is the functionality I'm seeing. Help?
To make clients like gh more robust, it would be great to have HTTP retries especially on 50x errors. A library like go-retryablehttp could be used for that, but that would be clearly a breaking API change.
Do you have thoughts on that?
The standard library's httptest package makes stubbing APIs easy, if the client calls the test server's URL:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
tls := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
defer tls.Close()
fmt.Println("tls hostname - ", tls.URL)
// prints: tls hostname - https://127.0.0.1:42002
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
defer ts.Close()
fmt.Println("ts hostname - ", ts.URL)
// prints: ts hostname - http://127.0.0.1:35061
}
In api.ClientOptions, one of the configuration fields is Host
go-gh/pkg/api/client_options.go
Line 38 in 25db6b9
for certain values the argument is returned unmodified
Lines 151 to 154 in 25db6b9
I would like to propose adding another condition to the restURL
function which would return the unmodified httptest
server URL
if strings.HasPrefix(hostname, "https://127.0.0.1:") || strings.HasPrefix(hostname, "http://127.0.0.1:") {
return hostname
}
This aligns with the conditional check on the prefix for the pathOrURL
argument.
For reference, setting the ts.URL
value to Host
results in a URL similar to:
as it satisfies this condition
Line 163 in 25db6b9
I don't know if this means my proposed solution would break with Enterprises.
In writing this all out, I also realized that passing the hostname + path to client.Get
, rather than just the api path, solves this issue.
I noticed that there are no examples of Mutations in https://github.com/cli/go-gh/blob/trunk/example_gh_test.go
I think this is important for a few reasons:
Line 125 in da64a13
If you agree, I'd be happy to open a PR to update, and would like to know:
addStar
a good reference, or is there another that looks better?variables
, would it be better to use https://github.com/shurcooL/githubv4 or to use only standard types?This issue is to ensure that an appropriate CODEOWNERS
file is in place to ensure the @cli/code-reviewers are assigned as reviewers to cli/go-gh
PRs.
CODEOWNERS
file marking the team as reviewersHey - just discovered something that might be interesting.
When a consuming application calls CurrentRepository()
from inside a GH workflow I'm seeing the following error:
unable to determine current repository, none of the git remotes configured for this repository point to a known GitHub host
I think this is because ~/.config/gh/hosts.yml
does not exist on a runner because of the TOKEN based auth (vs oauth).
This would cause FilterByHosts to return an error.
I have worked around this by. exporting the GH_HOST environment variable.. however it was not immediately obvious what to do.
If this is expected behaviour we could update the README with a note on compatibility with GH Actions.
Cheers!
Can we merge cli/shurcooL-graphql#4 and update the dependency version in go-gh?
Pagination is currently not possible using RESTClient
and takes lots of manual work using GQLClient
. We should support this feature for both. Initial implementation idea is to add a pagination
options to ClientOptions
which would enable this for both clients and be a noop for the HTTPClient
.
Before the bump to v2, I could do this:
import {
...
"github.com/cli/go-gh/v2/pkg/api"
}
...
err = restClient.Get(path, &resp)
if err != nil {
if err.(api.HTTPError).StatusCode == 404 {
log.Fatal("special message")
}
log.Fatal("general message")
}
Since updating, that is no longer a valid casting. I tried doing errors.As
, but that didn't seem to work either.
Based on some other repositories I found, I tried this:
import {
...
ghApi "github.com/cli/go-gh/v2/pkg/api"
"github.com/cli/cli/v2/api"
}
...
restClient, err := ghApi.DefaultRESTClient()
if err != nil {
log.Fatal(err)
}
...
err = restClient.Get(path, &resp)
if err != nil {
var httpErr api.HTTPError
if errors.As(err, &httpErr) && httpErr.StatusCode == 404 {
log.Fatal("special message")
}
log.Fatal("general message")
}
This is always going to the general Fatal response message and not the special message. It seems that the errors.As
casting is not working here, and the error is a simple string error. I'd love to be able to parse this status code without examining the error string (which does return "HTTP 404: Not Found...").
Is there some new, undocumented method to consume these errors, or was this an accidental regression? Thanks.
https://github.com/AlecAivazis/survey has been archived and won't received updates in the future.
It is used in
go-gh/pkg/prompter/prompter.go
Line 10 in 25db6b9
It should probably be moved to another library.
Hello! 👋
I have recently created a GitHub CLI extension, gh-collab-scanner, that relies on gh.CurrentRepository
to detect the current GitHub repository.
I don't understand why it does not work on my Ubuntu laptop: unable to determine current repository, none of the git remotes configured for this repository point to a known GitHub host
error occurs, see nicokosi/gh-collab-scanner#13 (but it works on my macOS laptop).
Thanks in advance for your help.
We should add pagination examples for the REST and GQL APIs
I have a use case where I need to search for repositories containing certain query criteria (say name or language): essentially reproducing the standard repository search. Using the gh
CLI this can be achieved via:
gh api -X GET /search/repositories -f q='<name> in:name language: <lang>'
I am trying to reproduce the above with the RESTClient call, passing the query search somehow as header string; however, according to the docs this grammar is not accepted (namely there is no way to pass a query string to the RESTClient object). Practically speaking I am looking to do something along the lines of
opts := api.ClientOptions{
...
Headers: <insert grammar for query string> <---- this bit must be filled correctly
Log: os.Stdout,
}
...
...
client, err := RESTClient(&opts) <--- the query string is passed to the client
...
err = client.Get("search/repositories", &response) <--- notice the endpoint /search/repositories
What is the correct way to achieve the above?
Right now when gh auth token
command fails for some reason, TokenForHost masks that with a generic "authentication token not found" error. I propose that the error also includes concrete reasons for the failure if they were known: for example, if gh wasn't found in the PATH.
TokenForHost source
return value should be a value like GH_TOKEN or GITHUB_TOKEN when the token comes from an environment variable, or a file path (e.g. /path/to/hosts.yml
) when the value comes from a file. However, a static string oauth_token
is returned instead of a file name:
Line 64 in da64a13
Since oauth_token
source is not useful to find out where a value comes from, printing the file name would be better.
Ref. #44
Right now the query
and mutations
methods on GQLCLient
return an error type from shurcooL-graphql which does not match the concrete error type from the do
methods. This causes unnecessary complexity when trying to do type assertion on the errors returned from the GQLCLient
methods. To fix this we can wrap the errors returned from query
and mutations
methods in a GQLError
.
The mock Registry
and friends from https://github.com/cli/cli/tree/trunk/pkg/httpmock would be very useful for testing extensions.
We have encountered an error when using the GitHub cli
to fetch commits in an MDN repository. And I found this error is coursed by the sanitizer which is used by GitHub cli
.
So I created a demo to reproduce the problem:
the plain text to transform:
�, plain text
When we read the plain text, and use transform
with the the sanitizer , we would got an error:
But this should be the correct text. I found the error is returned here.
So I read the signature of utf8.DecodeRune
. It may also return utf8.RuneError
if the bytes are correctly decoded. And if there does be a decode error, it will return (RuneError, 0)
or (RuneError, 1)
.
So we can't judge whether there is a decoding error just based on the first value returned, like the text I used above, which uses this unicode character. The sanitizer mishandled it.
In go.mod i see this dependency
golang.org/x/net v0.7.0 // indirect
this version is affected by CVE-2023-3978
I believe that updating golang.org/x/text v0.7.0 should solve this
As I am utilizing this module for using browser on my cli, I tried to generate error using the cli using ssh connection, but no any error is thrown and the browser also cannot start sitting on CLI.
Actual:
Press Enter to continue..
[22660:22660:0710/170853.558426:ERROR:ozone_platform_x11.cc(239)] Missing X server or $DISPLAY
[22660:22660:0710/170853.558460:ERROR:env.cc(255)] The platform failed to initialize. Exiting.
Err: <nil>
Expected: error to be not nil in value.
If anyone wants to watch out the code, then I will dump it here.
When using a template with a hyperlink at the end, if the the line is truncated the link its closed correctly.
gh api notifications --template '{{tablerow "Reason" "When" "Repo" "Title" -}}
{{ range . -}}
{{tablerow (.reason | autocolor "cyan") (timeago .updated_at) (.repository.full_name) (hyperlink "https://github.com/notifications" (.subject.title | autocolor "yellow")) -}}
{{end -}}'
Below output is from redirecting to a file, and using sed -n l0 file
With a the available space in terminal window the link is closed correctly:
\033]8;;https://github.com/notifications\033\\build(deps): bump pre-commit helm-docs to v1.13.0\033]8;;\033\\$
When the terminal is too small, the link isn't closed
\033]8;;https://github.com/notifications\033\\build(deps):...$
Therefore turning everything after that in terminal too a link, running the following clears it printf '\e]8;;'
First, this is awesome. I started out assuming I was going to need to hack my commands into the CLI proper and then found the extension mechanism. Perfect.
I have a question about how to release the extension so others can install it. I wrote up a quick Go-based extension, put it in a gh-* repo and installed it locally (gh extension install .
). Worked like a charm. So good I want to share. I saw the template put in a release workflow so I went ahead and tagged and pushed and the workflow ran and I duly got a mess of binaries in a release. Great.
I flipped back to my client and ran gh extension install org/gh-repo
and it complains that extension is uninstallable: missing executable
. I'm not sure how this is supposed to work. The doc said a few things about having to have "the executable" in the root of the repo but for compiled code
Seems ideal here to use the Release mechanism and say that installing gets the executables from latest release by default and you can say something like install org/[email protected]
if you want a specific release.
Right now the term
package exposes stdout
through the Out
method, it would be nice if stdin
and stderr
were also exposed through some methods.
gh
supports a http_unix_socket
option that impacts how API requests get sent, we should take that configuration option into account up the API clients.
I had a quick question/feedback - I was wondering if a status code could be included along with the response from the gh.RESTClient().Get?
for example I was making a call
client, err := gh.RESTClient(options)
<snip>
err = client.Get("enterprises/avocado-corp/audit-log", &response)
Unfortunately err cannot be type-casted to api.httperror since api.httperror is not exported, so I’d have to rely on string parsing to figure out the error code here. Alternatively api.httpError could be exported to api.HTTPError , in which case the caller can then typecast the error and get all the relevant bits in the struct and do things like retry on certain statusCodes etc.
e.g
err = client.Get(blah)
if err != nil {
httpError := err.(*api.HttpError)
if httpError.statusCode == 502 {
...retry
}else{
...do something else
}
}
or the other option would be to read the statusCode, if it’s set on the response.
Does that make sense?
I started a PR to export api.HttpError here - 6596865
Thanks for reading ❤️ .
It would be awesome to provide a fake out of the box for unit tests.
This project has made my work significantly easier, thank you!
I am finding that often I am wanting to print only the GraphQL query & GraphQL variables but not the HTTP information. I know you can set os.Setenv("GH_DEBUG", "api")
to print the GraphQL & HTTP information. I am finding that when I am developing my GraphQL struct that it would be useful to reduce some of the noise and only print out the GraphQL information.
The CurrentRepository
function should take into account the GH_REPO
environment variable, and if it is specified return that repository rather than the current flow of looking at the filesystem and git remotes.
As part of this work we should make the parsing function used for parsing GH_REPO
into a repository
available for user consumption.
cc #5
I would like to call gh
interactively in order to use the already existing survey prompt functionality that comes with the standard features of gh
.
With just gh.Exec
I'm not able to access the interactive mode.
Hello! I have an admittedly niche problem - I'm working on a CLI extension against github.localhost
, which means setting GH_HOST=github.localhost
for each command. Calling isEnterprise
returns true
in that case, because it is not github.com
:
go-gh/internal/config/config.go
Lines 110 to 112 in 9dbbfe2
That results in an incorrect (and un-fixable) URL here:
go-gh/internal/api/rest_client.go
Lines 112 to 117 in 9dbbfe2
As far as I can tell, there's no way for me to say "use GH_HOST
, but also use api.<GH_HOST>
instead of <GH_HOST>/api/v3
". Is that correct, or is there a patch needed here? Happy to open a PR adding an exception for github.localhost
if that makes sense, or some more consistent way of setting the full URL?
FWIW, I did think that maybe have GH_HOST=http://api.github.localhost
would work, but that gives:
Post "https://http//api.github.localhost/api/v3/repos/:owner/:repo/<REDACTED>"
This looks like a great start and has good features for basic binary extensions, but when I opened cli/cli#4264 I was hoping to see more low-level implementations either ported or refactored out into a shared library both the CLI and binary extension developers could use. For example,
-R
command line switch, or at least the APIs to make adding one work. I do see that environment variables that gh
uses are parsed, but it would be great to provide the same command line switches (even if you don't force a dependency on Cobra Command).gh
but this seems otherwise unnecessary if more of the formatting code was expose from cli/cli/pkg
.go-gh/pkg/jsonpretty/format.go
Line 55 in 0921311
Seems if indent == ""
when passed to jsonpretty.Format
, output should not be formatted at all i.e., delimiters - []{},
- should not have a newline appended.
While working on gh-not
I started experimenting with Actors interacting with the GitHub API.
A very simple one would be to mark a notification as read
, as per https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#mark-a-thread-as-read.
Here's a simplified version of the code I wanted to use:
func main() {
client, err := api.DefaultRESTClient()
if err != nil {
panic(err)
}
url := "https://api.github.com/notifications/threads/[REDACTED]"
resp := []interface{}{}
err = client.Patch(url, nil, &resp)
if err != nil {
panic(err)
}
}
And I always get: panic: unexpected end of JSON input
The documentation specifies status codes, but no response content; expecting one is thus useless in this context.
I wonder if
Lines 103 to 111 in dbd982e
b, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
+ if len(b) > 0{
err = json.Unmarshal(b, &response)
if err != nil {
return err
}
+ }
This would prevent breaking on invalid JSON when none is expected.
POC implemented in #162
The 205
status code spec specifies:
a server MUST NOT generate content in a 205 response
So maybe a better fix would be:
if resp.StatusCode == http.StatusNoContent {
return nil
}
+ if resp.StatusCode == http.StatusResetContent {
+ return nil
+ }
Implementation PR: #163
func main() {
client, err := api.DefaultRESTClient()
if err != nil {
panic(err)
}
url := "https://api.github.com/notifications/threads/[REDACTED]"
resp := []interface{}{}
err = client.Patch(url, nil, &resp)
if err != nil && err.Error() != "unexpected end of JSON input" {
panic(err)
}
// continue as planned
}
go-gh
's REST client for Patch requests if the endpoint doesn't return a valid JSON?suggestions for the example:
gh
external call fallbackRegarding the package API:
gh
itself? As opposed to always requiring the three step process (pick remote, determine host, get+set token) to create clients?Misc:
godoc
to pick up is A+ like we talked about in sync in addition to a full exampleIt'd be great if we could add more functions to the template
package, perhaps using the "WithOptions" pattern as mentioned elsewhere. The existing templates are useful, but for extensions to add other useful helpers for their own needs, passing in a map to merge after initializing the default list (so extensions could override) could prove useful.
I would like to propose the addition of a feature that enables the integration of repository-specific configurations into the pkg/config
package, drawing inspiration from the way Git handles local .git/config
versus XDG_CONFIG_HOME
configurations. This enhancement could prove invaluable for various extensions creators who could leverage distinct settings tailored to each repository's requirements.
Consider a chat-op deployment scenario - a good gh
extension that integrates well with external platforms like Discord or Slack, having the ability to define and commit repository-specific configurations directly within the repository itself can offer significant advantages. For instance, imagine a use case where different repositories need to route communications to specific channels on these platforms. The current config
package is limited in that the configs are global (AFAIK - please let me know if this is incorrect) which leaves the current approach of providing options each time a command is invoked which can become cumbersome, especially when the values for channels differ across repositories. This is just one scenario where repo specific configs might be useful but I'm sure extension creators can think of creative ways to use this feature 😄
The proposed implementation could involve creating a dedicated configuration file within the repository, such as .ghconfig, where users can define repository-specific settings. This file would be committed alongside other source code and assets.
Introducing repository-specific configurations to the "gh" library has the potential to enhance the usability, consistency, and flexibility of the tool in scenarios where distinct settings are essential on a per-repository basis. This feature aligns with the natural progression of version control practices and would undoubtedly contribute to streamlining workflows and improving overall efficiency.
If the CLI isn't already authenticated, extensions can fail in various ways depending on what they call. For example, using gh.CurrentRepository()
- even in a repo - fails with errors.New("unable to determine current repository, none of the git remotes configured for this repository point to a known GitHub host")
. If you specify a repo explicitly, gh.GQLClient()
can fail with an internal config.NotFoundError
which, given it's in internal
, cannot be referenced so might as well be a generic error
. To the user, the error simply prints "not found" which isn't actionable. Even the more wordy error mentioned first isn't that actionable, though more useful.
Instead, what about something like gh.IsAuthenticated() bool
? It could, for example, call config.Load()
and then check the Config.AuthToken
for a known host. This would allow extensions to easily check up front if they should prompt the user to authenticate.
Alternatively, expose any auth-related errors in pkg
or something so we can do type assertions.
Being in the same package, the example test file can refer to functions such as RESTClient
and Exec
as local, as opposed to gh.RESTClient
and gh.Exec
. This presents some challenges:
I would like to suggest changing the package of the example file and updating the functions in it.
Hey - thank you for pushing out the latest release. Please could you correct the tag as it has a .
between the v and semver.
https://github.com/cli/go-gh/releases/tag/v.0.0.2
Currently, there is no option to cancel the ongoing request.
Here is a small example of pagination request where this would be helpful https://gist.github.com/mszostok/b68ff95f85d4b4ff8a27aeed56f9d3ca.
This only fetches GitHub stars, so the payload is not heavy, however if you want to fetch all issues then you have even a bigger problem.
How will it benefit CLI and its users?
It improves UX as you can cancel executed command, and it will be released immediately instead of being unresponsive for a few seconds.
It reduces the quota consumption, as you won't execute next calls if not needed and also save the bandwidth as you will inform server that the client is not waiting for the response anymore.
There are at least two options how it can be achieved:
Add new methods with the WithContext
suffix. For example:
// GQLClient is the interface that wraps methods for the different types of
// API requests that are supported by the server.
type GQLClient interface {
// Do executes a GraphQL query request.
// The response is populated into the response argument.
Do(query string, variables map[string]interface{}, response interface{}) error
// DoWithContext executes a GraphQL query request.
// The response is populated into the response argument.
DoWithContext(ctx context.Context, query string, variables map[string]interface{}, response interface{}) error
}
Similar approach as Go has for http.Request. It's not a breaking change.
Change all func signature and add the context.Context
as a first parameter. It's a breaking change in external API.
There is also an option to add context.Context
to struct but it won't work in this case and it's ugly and problematic.
If you agree with one of the provided option, I can create a dedicated PR 👍
I noticed this when running some tests with the new version.
resolveOptions
is not updating the pointer reference. So any changes made locally in the method are not reflected in the caller.
For example: gh.HTTPClient(nil)
causes a nil value for ClientOptions
to be passed to the underlying NewHTTPClient
.
Hello!
Loving the abstraction so far, great job!
Was wondering if we could add an example that reads an octet-stream
similar to this in the cli repo using net/http?
I'm trying a couple of different approaches, but I'm not sure what's the best way to define the response to facilitate reading bytes?
Let me know if I can help in any way as well 😄
Edit: I'm using the rest client for reference, and looking at
go-gh/internal/api/rest_client.go
Line 48 in c41a127
Using Get
or Do
I get:
invalid character '\x1f' looking for beginning of value
I'm using this for getting changed files between two commits, I presume this will split across pages, is there a way using the HTTPClient
to get at the Link
headers?
Been using go-gh
for a few things and liking it so far!
I have a use case where I want to run gh
as a long running command and I want to take over the writer for stdout and stderr. With the current APIs, I can't do this.
See example of how I am doing it right now by directly invoking gh: https://github.com/josebalius/gh-csfs/blob/main/internal/csfs/ssh.go#L37-L56
This solution works perfectly, but I am hardcoding gh
, so it would be nice to either expose the lookup path function or a way to inject writers.
@eXamadeus GitHub CLI currently has no mechanism for switching between multiple GitHub accounts and I don't really have a workaround to suggest for you at this moment, sorry.
The only approach I could imagine, but would not recommend to anyone, would be to authenticate with 1st account, save a copy of ~/.config/gh/hosts.yml
somewhere & delete the original file, authenticate again with the 2nd account, and now you can swap the ~/.config/gh/hosts.yml
file with the backup file when you need to switch accounts.
Since this solution involves using SSH for git protocol, make sure both configuration files include the git_protocol: ssh
line.
Originally posted by @mislav in cli/cli#326 (comment)
Hey - this is a great project and helped me get up and running quickly!
I was looking at integrating https://github.com/google/go-github in to my project. It allows you to pass a pre-configured http client when creating a new GitHubClient instance. The abstraction (RESTClient
) doesn't implement the interface so cannot be used as a parameter.
Would you consider exposing a configured raw http.Client instance or the config package to allow us to build our own?
I'd be happy to help out with the implementation if it's something of use.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.