Merge branch 'print-policy-info' of github.com:malancas/cli into print-policy-info
This commit is contained in:
commit
c9be8ce7b9
7 changed files with 163 additions and 22 deletions
24
pkg/cmd/cache/delete/delete.go
vendored
24
pkg/cmd/cache/delete/delete.go
vendored
|
|
@ -35,23 +35,23 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
cmd := &cobra.Command{
|
||||
Use: "delete [<cache-id>| <cache-key> | --all]",
|
||||
Short: "Delete GitHub Actions caches",
|
||||
Long: `
|
||||
Delete GitHub Actions caches.
|
||||
Long: heredoc.Docf(`
|
||||
Delete GitHub Actions caches.
|
||||
|
||||
Deletion requires authorization with the "repo" scope.
|
||||
`,
|
||||
Deletion requires authorization with the %[1]srepo%[1]s scope.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
# Delete a cache by id
|
||||
$ gh cache delete 1234
|
||||
# Delete a cache by id
|
||||
$ gh cache delete 1234
|
||||
|
||||
# Delete a cache by key
|
||||
$ gh cache delete cache-key
|
||||
# Delete a cache by key
|
||||
$ gh cache delete cache-key
|
||||
|
||||
# Delete a cache by id in a specific repo
|
||||
$ gh cache delete 1234 --repo cli/cli
|
||||
# Delete a cache by id in a specific repo
|
||||
$ gh cache delete 1234 --repo cli/cli
|
||||
|
||||
# Delete all caches
|
||||
$ gh cache delete --all
|
||||
# Delete all caches
|
||||
$ gh cache delete --all
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/codespaces"
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
"github.com/cli/cli/v2/internal/codespaces/portforwarder"
|
||||
|
|
@ -20,9 +21,12 @@ func newRebuildCmd(app *App) *cobra.Command {
|
|||
rebuildCmd := &cobra.Command{
|
||||
Use: "rebuild",
|
||||
Short: "Rebuild a codespace",
|
||||
Long: `Rebuilding recreates your codespace. Your code and any current changes will be
|
||||
preserved. Your codespace will be rebuilt using your working directory's
|
||||
dev container. A full rebuild also removes cached Docker images.`,
|
||||
Long: heredoc.Doc(`
|
||||
Rebuilding recreates your codespace.
|
||||
|
||||
Your code and any current changes will be preserved. Your codespace will be rebuilt using
|
||||
your working directory's dev container. A full rebuild also removes cached Docker images.
|
||||
`),
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return app.Rebuild(cmd.Context(), selector, fullRebuild)
|
||||
|
|
|
|||
|
|
@ -477,6 +477,21 @@ func createRun(opts *CreateOptions) error {
|
|||
}
|
||||
|
||||
newRelease, err := createRelease(httpClient, baseRepo, params)
|
||||
|
||||
var errMissingRequiredWorkflowScope *errMissingRequiredWorkflowScope
|
||||
if errors.As(err, &errMissingRequiredWorkflowScope) {
|
||||
host := errMissingRequiredWorkflowScope.Hostname
|
||||
refreshInstructions := fmt.Sprintf("gh auth refresh -h %[1]s -s workflow", host)
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("%s Failed to create release, \"workflow\" scope may be required.\n", cs.WarningIcon()))
|
||||
sb.WriteString(fmt.Sprintf("To request it, run:\n%s\n", cs.Bold(refreshInstructions)))
|
||||
fmt.Fprint(opts.IO.ErrOut, sb.String())
|
||||
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
|
|
@ -1082,6 +1083,74 @@ func Test_createRun(t *testing.T) {
|
|||
runStubs: defaultRunStubs,
|
||||
wantErr: "cannot generate release notes from tag v1.2.3 as it does not exist locally",
|
||||
},
|
||||
{
|
||||
name: "API returns 404, OAuth token has no workflow scope",
|
||||
isTTY: false,
|
||||
opts: CreateOptions{
|
||||
TagName: "Does not matter",
|
||||
},
|
||||
runStubs: func(rs *run.CommandStubber) {
|
||||
rs.Register(contentCmd, 0, "some tag message")
|
||||
rs.Register(signatureCmd, 0, "")
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL("RepositoryFindRef"),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/releases"),
|
||||
httpmock.StatusScopesResponder(404, `repo,read:org`))
|
||||
},
|
||||
wantStderr: heredoc.Doc(`
|
||||
! Failed to create release, "workflow" scope may be required.
|
||||
To request it, run:
|
||||
gh auth refresh -h github.com -s workflow
|
||||
`),
|
||||
wantErr: cmdutil.SilentError.Error(),
|
||||
},
|
||||
{
|
||||
name: "API returns 404, OAuth token has workflow scope",
|
||||
isTTY: false,
|
||||
opts: CreateOptions{
|
||||
TagName: "Does not matter",
|
||||
},
|
||||
runStubs: func(rs *run.CommandStubber) {
|
||||
rs.Register(contentCmd, 0, "some tag message")
|
||||
rs.Register(signatureCmd, 0, "")
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL("RepositoryFindRef"),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/releases"),
|
||||
httpmock.StatusScopesResponder(404, `repo,read:org,workflow`))
|
||||
},
|
||||
wantErr: "HTTP 404 (https://api.github.com/repos/OWNER/REPO/releases)",
|
||||
},
|
||||
{
|
||||
name: "API returns 404, not an OAuth token",
|
||||
isTTY: false,
|
||||
opts: CreateOptions{
|
||||
TagName: "Does not matter",
|
||||
},
|
||||
runStubs: func(rs *run.CommandStubber) {
|
||||
rs.Register(contentCmd, 0, "some tag message")
|
||||
rs.Register(signatureCmd, 0, "")
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL("RepositoryFindRef"),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/releases"),
|
||||
httpmock.StatusStringResponse(404, `HTTP 404 (https://api.github.com/repos/OWNER/REPO/releases)`))
|
||||
},
|
||||
wantErr: "HTTP 404 (https://api.github.com/repos/OWNER/REPO/releases)",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -1115,7 +1184,6 @@ func Test_createRun(t *testing.T) {
|
|||
err := createRun(&tt.opts)
|
||||
if tt.wantErr != "" {
|
||||
require.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,16 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/release/shared"
|
||||
"github.com/shurcooL/githubv4"
|
||||
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
)
|
||||
|
||||
type tag struct {
|
||||
|
|
@ -27,6 +31,14 @@ type releaseNotes struct {
|
|||
|
||||
var notImplementedError = errors.New("not implemented")
|
||||
|
||||
type errMissingRequiredWorkflowScope struct {
|
||||
Hostname string
|
||||
}
|
||||
|
||||
func (e errMissingRequiredWorkflowScope) Error() string {
|
||||
return "workflow scope may be required"
|
||||
}
|
||||
|
||||
func remoteTagExists(httpClient *http.Client, repo ghrepo.Interface, tagName string) (bool, error) {
|
||||
gql := api.NewClientFromHTTP(httpClient)
|
||||
qualifiedTagName := fmt.Sprintf("refs/tags/%s", tagName)
|
||||
|
|
@ -174,6 +186,24 @@ func createRelease(httpClient *http.Client, repo ghrepo.Interface, params map[st
|
|||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check if we received a 404 while attempting to create a release without
|
||||
// the workflow scope, and if so, return an error message that explains a possible
|
||||
// solution to the user.
|
||||
//
|
||||
// If the same file (with both the same path and contents) exists
|
||||
// on another branch in the repo, releases with workflow file changes can be
|
||||
// created without the workflow scope. Otherwise, the workflow scope is
|
||||
// required to create the release, but the API does not indicate this criteria
|
||||
// beyond returning a 404.
|
||||
//
|
||||
// https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes
|
||||
if resp.StatusCode == http.StatusNotFound && !tokenHasWorkflowScope(resp) {
|
||||
normalizedHostname := ghauth.NormalizeHostname(resp.Request.URL.Hostname())
|
||||
return nil, &errMissingRequiredWorkflowScope{
|
||||
Hostname: normalizedHostname,
|
||||
}
|
||||
}
|
||||
|
||||
success := resp.StatusCode >= 200 && resp.StatusCode < 300
|
||||
if !success {
|
||||
return nil, api.HandleHTTPError(resp)
|
||||
|
|
@ -254,3 +284,18 @@ func deleteRelease(httpClient *http.Client, release *shared.Release) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tokenHasWorkflowScope checks if the given http.Response's token has the workflow scope.
|
||||
// Tokens that do not have OAuth scopes are assumed to have the workflow scope.
|
||||
func tokenHasWorkflowScope(resp *http.Response) bool {
|
||||
scopes := resp.Header.Get("X-Oauth-Scopes")
|
||||
|
||||
// Return true when no scopes are present - no scopes in this header
|
||||
// means that the user is probably authenticating with a token type other
|
||||
// than an OAuth token, and we don't know what this token's scopes actually are.
|
||||
if scopes == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return slices.Contains(strings.Split(scopes, ","), "workflow")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -38,12 +39,14 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
cmd := &cobra.Command{
|
||||
Use: "delete [<repository>]",
|
||||
Short: "Delete a repository",
|
||||
Long: `Delete a GitHub repository.
|
||||
Long: heredoc.Docf(`
|
||||
Delete a GitHub repository.
|
||||
|
||||
With no argument, deletes the current repository. Otherwise, deletes the specified repository.
|
||||
|
||||
With no argument, deletes the current repository. Otherwise, deletes the specified repository.
|
||||
|
||||
Deletion requires authorization with the "delete_repo" scope.
|
||||
To authorize, run "gh auth refresh -s delete_repo"`,
|
||||
Deletion requires authorization with the %[1]sdelete_repo%[1]s scope.
|
||||
To authorize, run %[1]sgh auth refresh -s delete_repo%[1]s
|
||||
`, "`"),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
|
|
|
|||
|
|
@ -225,10 +225,16 @@ func GraphQLQuery(body string, cb func(string, map[string]interface{})) Responde
|
|||
}
|
||||
}
|
||||
|
||||
// ScopesResponder returns a response with a 200 status code and the given OAuth scopes.
|
||||
func ScopesResponder(scopes string) func(*http.Request) (*http.Response, error) {
|
||||
return StatusScopesResponder(http.StatusOK, scopes)
|
||||
}
|
||||
|
||||
// StatusScopesResponder returns a response with the given status code and OAuth scopes.
|
||||
func StatusScopesResponder(status int, scopes string) func(*http.Request) (*http.Response, error) {
|
||||
return func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
StatusCode: status,
|
||||
Request: req,
|
||||
Header: map[string][]string{
|
||||
"X-Oauth-Scopes": {scopes},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue