Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ local credential file. On success it returns:
{"signed_in":true,"env":"test","basecamp_account_id":"123","basecamp_person_id":"456","display_name":"Alex"}
```

The Basecamp account and person IDs are strings identifying the actual signed-in
principal (including the bot for API keys). No tokens are returned.
The Basecamp account and person IDs are strings identifying the signed-in
person. No tokens are returned.
Missing, invalid, or unverifiable authentication exits nonzero without success
JSON. Credential precedence remains `--api-key`, `FLUX_API_KEY`, then the selected
environment's saved personal credentials.
JSON. Sign in with `flux auth login`; the CLI uses the selected environment's
saved credentials.

### Issue thread JSON

Expand Down Expand Up @@ -224,5 +224,6 @@ go build -o flux .
./flux issues list --env test

# Run against a local server
FLUX_BASE_URL=http://localhost:4040 ./flux scenes list --project next --api-key <key>
FLUX_BASE_URL=http://localhost:4040 ./flux auth login
FLUX_BASE_URL=http://localhost:4040 ./flux scenes list --project next
```
3 changes: 2 additions & 1 deletion cmd/ai_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ func TestContentCommandsAIModelRequests(t *testing.T) {
} {
t.Run(model.name, func(t *testing.T) {
isolateContentCommand(t, operation.command)
if err := rootCmd.PersistentFlags().Parse([]string{"--api-key", "fixture-key", "--env", "test", "--project", "fixture-project", "--json"}); err != nil {
saveFixtureToken(t, "test")
if err := rootCmd.PersistentFlags().Parse([]string{"--env", "test", "--project", "fixture-project", "--json"}); err != nil {
t.Fatal(err)
}
want := map[string]interface{}{"project": "fixture-project"}
Expand Down
12 changes: 6 additions & 6 deletions cmd/appsignal.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var appsignalAppsCmd = &cobra.Command{
Use: "apps",
Short: "List available AppSignal applications",
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

apps, err := client.ListAppSignalApps()
if err != nil {
Expand Down Expand Up @@ -72,7 +72,7 @@ var appsignalIncidentsListCmd = &cobra.Command{
return fmt.Errorf("--app is required")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

opts := api.ListIncidentsOptions{
App: appsignalAppFlag,
Expand Down Expand Up @@ -125,7 +125,7 @@ var appsignalIncidentsGetCmd = &cobra.Command{
return fmt.Errorf("invalid incident number: %s", args[0])
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

incident, err := client.GetIncident(appsignalAppFlag, number)
if err != nil {
Expand Down Expand Up @@ -185,7 +185,7 @@ var appsignalIncidentsUpdateCmd = &cobra.Command{
return fmt.Errorf("--app is required")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

req := api.UpdateIncidentRequest{
State: appsignalStateFlag,
Expand Down Expand Up @@ -220,7 +220,7 @@ var appsignalIncidentsNoteCmd = &cobra.Command{
return fmt.Errorf("invalid incident number: %s", args[0])
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

if err := client.AddIncidentNote(appsignalAppFlag, number, appsignalContentFlag); err != nil {
return err
Expand All @@ -240,7 +240,7 @@ var appsignalResourcesCmd = &cobra.Command{
return fmt.Errorf("--app is required")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

resources, err := client.GetAppSignalResources(appsignalAppFlag, appsignalSectionsFlag)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ var authStatusCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
env := getEnv()

client := api.NewClient(baseURLForEnv(env), getAPIKey())
client := api.NewClient(baseURLForEnv(env), getAccessToken())
identity, err := client.GetIdentity()
if err != nil {
return err
Expand Down
76 changes: 64 additions & 12 deletions cmd/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,25 @@ func isolateCommand(t *testing.T) {
})
}

// saveFixtureToken signs in to env with the access token "fixture-key".
func saveFixtureToken(t *testing.T, env string) {
t.Helper()
if err := auth.Save(env, &auth.Credentials{AccessToken: "fixture-key", ExpiresAt: time.Now().Add(time.Hour)}); err != nil {
t.Fatal(err)
}
}

func TestAuthStatusVerifiedIdentity(t *testing.T) {
for _, source := range []string{"personal", "environment key", "explicit key"} {
t.Run(source, func(t *testing.T) {
for _, tc := range []struct {
name, apiKeyEnv string
}{
{name: "personal"},
{name: "FLUX_API_KEY is ignored", apiKeyEnv: "environment-key"},
} {
t.Run(tc.name, func(t *testing.T) {
isolateCommand(t)
t.Setenv("FLUX_ENV", "test")
t.Setenv("FLUX_API_KEY", tc.apiKeyEnv)
if err := rootCmd.PersistentFlags().Parse([]string{"--env", "prod", "--json"}); err != nil {
t.Fatal(err)
}
Expand All @@ -67,16 +81,6 @@ func TestAuthStatusVerifiedIdentity(t *testing.T) {
t.Fatal(err)
}
credential := "prod-personal"
if source != "personal" {
t.Setenv("FLUX_API_KEY", "environment-key")
credential = "environment-key"
}
if source == "explicit key" {
if err := rootCmd.PersistentFlags().Set("api-key", "explicit-key"); err != nil {
t.Fatal(err)
}
credential = "explicit-key"
}
requests := 0
http.DefaultTransport = roundTripFunc(func(r *http.Request) (*http.Response, error) {
requests++
Expand Down Expand Up @@ -164,3 +168,51 @@ func TestEnvironmentAndProjectSelection(t *testing.T) {
})
}
}

func TestRemovedAPIKey(t *testing.T) {
for _, tc := range []struct {
name, apiKeyEnv string
args []string
wantErr error
wantWarning bool
}{
{name: "no key", args: []string{"projects", "list"}},
{name: "flag", args: []string{"projects", "list", "--api-key", "old-key"}, wantErr: errAPIKeyRemoved},
{name: "flag before command", args: []string{"--api-key=old-key", "issues", "list"}, wantErr: errAPIKeyRemoved},
{name: "flag on auth status", args: []string{"auth", "status", "--api-key", "old-key"}, wantErr: errAPIKeyRemoved},
{name: "environment", apiKeyEnv: "old-key", args: []string{"projects", "list"}, wantWarning: true},
} {
t.Run(tc.name, func(t *testing.T) {
isolateCommand(t)
t.Setenv("FLUX_API_KEY", tc.apiKeyEnv)
command, _, err := rootCmd.Find(tc.args)
if err != nil {
t.Fatal(err)
}
if err := rootCmd.ParseFlags(tc.args); err != nil {
t.Fatal(err)
}
if err := command.ParseFlags(tc.args); err != nil {
t.Fatal(err)
}
var stderr bytes.Buffer
command.SetErr(&stderr)
t.Cleanup(func() { command.SetErr(nil) })
err = rootCmd.PersistentPreRunE(command, nil)
if !errors.Is(err, tc.wantErr) {
t.Fatalf("got error %v; want %v", err, tc.wantErr)
}
warned := strings.Contains(stderr.String(), "FLUX_API_KEY is ignored")
if warned != tc.wantWarning || (tc.wantWarning && !strings.Contains(stderr.String(), "flux auth login")) {
t.Fatalf("unexpected warning output: %q", stderr.String())
}
})
}
}

func TestAPIKeyFlagHidden(t *testing.T) {
flag := rootCmd.PersistentFlags().Lookup("api-key")
if flag == nil || !flag.Hidden {
t.Fatal("--api-key must stay registered but hidden, so old scripts get a helpful error")
}
}
4 changes: 2 additions & 2 deletions cmd/comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ var commentsUpdateCmd = &cobra.Command{
return fmt.Errorf("--content is required")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())
result, err := client.UpdateComment(args[0], api.CommentRequest{
Content: commentContent,
Project: getProject(),
Expand All @@ -49,7 +49,7 @@ var commentsDeleteCmd = &cobra.Command{
Short: "Delete (trash) a comment",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())
result, err := client.DeleteComment(args[0], getProject())
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/diagrams.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var diagramsRenderCmd = &cobra.Command{
return fmt.Errorf("--file or --mermaid is required")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())
result, err := client.RenderDiagram(mermaid, diagramCaptionFlag, getProject())
if err != nil {
return err
Expand Down
14 changes: 7 additions & 7 deletions cmd/epics.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var epicsListCmd = &cobra.Command{
Use: "list",
Short: "List epics",
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

opts := api.ListEpicsOptions{
Milestone: epicMilestoneFlag,
Expand Down Expand Up @@ -73,7 +73,7 @@ var epicsGetCmd = &cobra.Command{
Short: "Get epic details",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

epic, err := client.GetEpic(args[0], getProject())
if err != nil {
Expand Down Expand Up @@ -131,7 +131,7 @@ var epicsCreateCmd = &cobra.Command{
return fmt.Errorf("--title is required")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

req := api.CreateEpicRequest{
Title: epicTitleFlag,
Expand Down Expand Up @@ -163,7 +163,7 @@ var epicsUpdateCmd = &cobra.Command{
Short: "Update an existing epic",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

req := api.UpdateEpicRequest{
Title: epicTitleFlag,
Expand Down Expand Up @@ -195,7 +195,7 @@ var epicsIssuesCmd = &cobra.Command{
Short: "List issues linked to an epic",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

issues, err := client.ListEpicIssues(args[0], epicCompletedFlag, getProject())
if err != nil {
Expand Down Expand Up @@ -242,7 +242,7 @@ var epicsCommentCmd = &cobra.Command{
return fmt.Errorf("--content is required")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

req := api.CommentRequest{
Content: epicContentFlag,
Expand Down Expand Up @@ -270,7 +270,7 @@ var epicsResyncCmd = &cobra.Command{
Short: "Refresh linked issue titles on an epic",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())
result, err := client.ResyncEpic(args[0], getProject())
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var imagesUploadCmd = &cobra.Command{
return fmt.Errorf("--file is required")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())
result, err := client.UploadImage(imageFileFlag, imageCaptionFlag, getProject())
if err != nil {
return err
Expand Down
18 changes: 9 additions & 9 deletions cmd/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var issuesListCmd = &cobra.Command{
Use: "list",
Short: "List issues",
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

issues, err := client.ListIssues(api.ListIssuesOptions{
Stage: stageFlag,
Expand Down Expand Up @@ -94,7 +94,7 @@ var issuesGetCmd = &cobra.Command{
Short: "Get issue details",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

issue, err := client.GetIssue(args[0], getProject())
if err != nil {
Expand Down Expand Up @@ -175,7 +175,7 @@ var issuesCreateCmd = &cobra.Command{
return fmt.Errorf("--app is required (web, ios, or android)")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())
if err := checkUseCaseTarget(cmd, client); err != nil {
return fmt.Errorf("no issue was created: %w", err)
}
Expand Down Expand Up @@ -211,7 +211,7 @@ var issuesUpdateCmd = &cobra.Command{
Short: "Update an existing issue",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())
if err := checkUseCaseTarget(cmd, client); err != nil {
return fmt.Errorf("issue %s was not updated: %w", args[0], err)
}
Expand Down Expand Up @@ -246,7 +246,7 @@ var issuesDeleteCmd = &cobra.Command{
Short: "Delete an issue",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

if err := client.DeleteIssue(args[0], getProject()); err != nil {
return err
Expand All @@ -270,7 +270,7 @@ var issuesCommentCmd = &cobra.Command{
return fmt.Errorf("--content is required")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

req := api.CommentRequest{
Content: issueCommentContentFlag,
Expand Down Expand Up @@ -298,7 +298,7 @@ var issuesAdvanceCmd = &cobra.Command{
Short: "Advance an issue to the next stage",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

req := api.AdvanceIssueRequest{
TargetStage: issueTargetStageFlag,
Expand Down Expand Up @@ -344,7 +344,7 @@ var issuesLinkCmd = &cobra.Command{
return fmt.Errorf("--target-type and --target-id are required")
}

client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())

action := "link"
if issueUnlinkFlag {
Expand Down Expand Up @@ -382,7 +382,7 @@ var issuesAssignCmd = &cobra.Command{
if issueAssigneeIDsFlag == "" {
return fmt.Errorf("--assignees is required")
}
client := api.NewClient(baseURLForEnv(getEnv()), getAPIKey())
client := api.NewClient(baseURLForEnv(getEnv()), getAccessToken())
req := api.AssignIssueRequest{
AssigneeIDs: issueAssigneeIDsFlag,
Project: getProject(),
Expand Down
Loading
Loading