Merge pull request #11600 from cli/kw/997-gh-agent-task-responds-and-exits-if-user-not-authenticated-with-oauth
Introduce `gh agent-task`
This commit is contained in:
commit
09be17e18f
3 changed files with 213 additions and 0 deletions
62
pkg/cmd/agent-task/agent_task.go
Normal file
62
pkg/cmd/agent-task/agent_task.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/go-gh/v2/pkg/auth"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewCmdAgentTask creates the base `agent-task` command.
|
||||
func NewCmdAgentTask(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "agent-task",
|
||||
Aliases: []string{"agent-tasks", "agent", "agents"},
|
||||
Short: "Manage agent tasks (preview)",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return requireOAuthToken(f)
|
||||
},
|
||||
// This is required to run this root command. We want to
|
||||
// run it to test PersistentPreRunE behavior.
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// requireOAuthToken ensures an OAuth (device flow) token is present and valid.
|
||||
// agent-task subcommands inherit this check via PersistentPreRunE.
|
||||
func requireOAuthToken(f *cmdutil.Factory) error {
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authCfg := cfg.Authentication()
|
||||
host, _ := authCfg.DefaultHost()
|
||||
if host == "" {
|
||||
return errors.New("no default host configured; run 'gh auth login'")
|
||||
}
|
||||
|
||||
if auth.IsEnterprise(host) {
|
||||
return errors.New("agent tasks are not supported on this host")
|
||||
}
|
||||
|
||||
token, source := authCfg.ActiveToken(host)
|
||||
|
||||
// Tokens from sources "oauth_token" and "keyring" are likely
|
||||
// minted through our device flow.
|
||||
tokenSourceIsDeviceFlow := source == "oauth_token" || source == "keyring"
|
||||
// Tokens with "gho_" prefix are OAuth tokens.
|
||||
tokenIsOAuth := strings.HasPrefix(token, "gho_")
|
||||
|
||||
// Reject if the token is not from a device flow source or is not an OAuth token
|
||||
if !tokenSourceIsDeviceFlow || !tokenIsOAuth {
|
||||
return fmt.Errorf("this command requires an OAuth token. Re-authenticate with: gh auth login")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
149
pkg/cmd/agent-task/agent_task_test.go
Normal file
149
pkg/cmd/agent-task/agent_task_test.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
ghmock "github.com/cli/cli/v2/internal/gh/mock"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// setupMockOAuthConfig configures a blank config with a default host and optional token behavior.
|
||||
func setupMockOAuthConfig(t *testing.T, tokenSource string) gh.Config {
|
||||
t.Helper()
|
||||
c := config.NewBlankConfig()
|
||||
switch tokenSource {
|
||||
case "oauth_token":
|
||||
// valid OAuth device flow token stored in config
|
||||
c.Set("github.com", "oauth_token", "gho_OAUTH123")
|
||||
case "keyring":
|
||||
// valid OAuth device flow token stored in keyring
|
||||
c.Set("github.com", "oauth_token", "gho_OAUTH123")
|
||||
case "GH_TOKEN":
|
||||
// classic style token stored in config (will fail prefix check)
|
||||
c.Set("github.com", "oauth_token", "ghp_CLASSIC123")
|
||||
case "GH_ENTERPRISE_TOKEN":
|
||||
// enterprise style token stored in config (will fail prefix check)
|
||||
c.Set("something.ghes.com", "oauth_token", "ghe_ENTERPRISE123")
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func TestNewCmdAgentTask(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tokenSource string
|
||||
customConfig func() (gh.Config, error)
|
||||
wantErr bool
|
||||
wantErrContains string
|
||||
wantStdout string
|
||||
}{
|
||||
{
|
||||
name: "oauth token is accepted",
|
||||
tokenSource: "oauth_token",
|
||||
wantErr: false,
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "keyring oauth token is accepted",
|
||||
tokenSource: "keyring",
|
||||
wantErr: false,
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "env var token is rejected",
|
||||
tokenSource: "GH_TOKEN",
|
||||
wantErr: true,
|
||||
wantErrContains: "requires an OAuth token",
|
||||
},
|
||||
{
|
||||
name: "enterprise token alone is ignored and rejected",
|
||||
tokenSource: "GH_ENTERPRISE_TOKEN",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "github.com oauth is accepted and enterprise token ignored",
|
||||
customConfig: func() (gh.Config, error) {
|
||||
c := config.NewBlankConfig()
|
||||
c.Set("something.ghes.com", "oauth_token", "ghe_ENTERPRISE123")
|
||||
c.Set("github.com", "oauth_token", "gho_OAUTH123")
|
||||
return c, nil
|
||||
},
|
||||
wantErr: false,
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "enterprise host is rejected",
|
||||
customConfig: func() (gh.Config, error) {
|
||||
return &ghmock.ConfigMock{
|
||||
AuthenticationFunc: func() gh.AuthConfig {
|
||||
c := &config.AuthConfig{}
|
||||
c.SetDefaultHost("something.ghes.com", "GH_HOST")
|
||||
return c
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrContains: "not supported on this host",
|
||||
},
|
||||
{
|
||||
name: "empty host is rejected",
|
||||
customConfig: func() (gh.Config, error) {
|
||||
return &ghmock.ConfigMock{
|
||||
AuthenticationFunc: func() gh.AuthConfig {
|
||||
c := &config.AuthConfig{}
|
||||
c.SetDefaultHost("", "GH_HOST")
|
||||
return c
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrContains: "no default host configured",
|
||||
},
|
||||
{
|
||||
name: "no auth is rejected",
|
||||
tokenSource: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := &cmdutil.Factory{}
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
f.IOStreams = ios
|
||||
if tt.customConfig != nil {
|
||||
f.Config = tt.customConfig
|
||||
} else {
|
||||
f.Config = func() (gh.Config, error) { return setupMockOAuthConfig(t, tt.tokenSource), nil }
|
||||
}
|
||||
|
||||
cmd := NewCmdAgentTask(f)
|
||||
err := cmd.Execute()
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
if tt.wantErrContains != "" {
|
||||
require.Contains(t, err.Error(), tt.wantErrContains)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantStdout, stdout.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasAreSet(t *testing.T) {
|
||||
f := &cmdutil.Factory{}
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f.IOStreams = ios
|
||||
f.Config = func() (gh.Config, error) { return setupMockOAuthConfig(t, "oauth_token"), nil }
|
||||
|
||||
cmd := NewCmdAgentTask(f)
|
||||
|
||||
require.ElementsMatch(t, []string{"agent-tasks", "agent", "agents"}, cmd.Aliases)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/MakeNowJust/heredoc"
|
||||
accessibilityCmd "github.com/cli/cli/v2/pkg/cmd/accessibility"
|
||||
actionsCmd "github.com/cli/cli/v2/pkg/cmd/actions"
|
||||
agentTaskCmd "github.com/cli/cli/v2/pkg/cmd/agent-task"
|
||||
aliasCmd "github.com/cli/cli/v2/pkg/cmd/alias"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
|
||||
apiCmd "github.com/cli/cli/v2/pkg/cmd/api"
|
||||
|
|
@ -126,6 +127,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
|
|||
cmd.AddCommand(versionCmd.NewCmdVersion(f, version, buildDate))
|
||||
cmd.AddCommand(accessibilityCmd.NewCmdAccessibility(f))
|
||||
cmd.AddCommand(actionsCmd.NewCmdActions(f))
|
||||
cmd.AddCommand(agentTaskCmd.NewCmdAgentTask(f))
|
||||
cmd.AddCommand(aliasCmd.NewCmdAlias(f))
|
||||
cmd.AddCommand(authCmd.NewCmdAuth(f))
|
||||
cmd.AddCommand(attestationCmd.NewCmdAttestation(f))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue