diff --git a/cmd/job/context_flags.go b/cmd/job/context_flags.go new file mode 100644 index 00000000..c8937b52 --- /dev/null +++ b/cmd/job/context_flags.go @@ -0,0 +1,18 @@ +package job + +import ( + "fmt" + "io" + "os" +) + +func warnIgnoredJobContextFlags(w io.Writer, pipeline, buildNumber string) { + if pipeline == "" && buildNumber == "" { + return + } + if w == nil { + w = os.Stderr + } + + fmt.Fprintln(w, "Warning: --pipeline and --build are deprecated and ignored because job UUIDs no longer require pipeline or build context") +} diff --git a/cmd/job/context_flags_test.go b/cmd/job/context_flags_test.go new file mode 100644 index 00000000..191c21e9 --- /dev/null +++ b/cmd/job/context_flags_test.go @@ -0,0 +1,59 @@ +package job + +import ( + "bytes" + "strings" + "testing" +) + +func TestWarnIgnoredJobContextFlags(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + pipeline string + buildNumber string + wantWarning bool + }{ + { + name: "no flags", + wantWarning: false, + }, + { + name: "pipeline", + pipeline: "cli", + wantWarning: true, + }, + { + name: "build", + buildNumber: "123", + wantWarning: true, + }, + { + name: "both flags", + pipeline: "cli", + buildNumber: "123", + wantWarning: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var stderr bytes.Buffer + warnIgnoredJobContextFlags(&stderr, tt.pipeline, tt.buildNumber) + + got := stderr.String() + if tt.wantWarning { + if !strings.Contains(got, "Warning: --pipeline and --build are deprecated and ignored") { + t.Fatalf("warning = %q", got) + } + return + } + if got != "" { + t.Fatalf("warning = %q, want empty", got) + } + }) + } +} diff --git a/cmd/job/log.go b/cmd/job/log.go index 8ac0e426..81b1d25d 100644 --- a/cmd/job/log.go +++ b/cmd/job/log.go @@ -6,33 +6,27 @@ import ( "regexp" "github.com/alecthomas/kong" - buildResolver "github.com/buildkite/cli/v3/internal/build/resolver" - "github.com/buildkite/cli/v3/internal/build/resolver/options" "github.com/buildkite/cli/v3/internal/cli" bkIO "github.com/buildkite/cli/v3/internal/io" - pipelineResolver "github.com/buildkite/cli/v3/internal/pipeline/resolver" "github.com/buildkite/cli/v3/pkg/cmd/factory" "github.com/buildkite/cli/v3/pkg/cmd/validation" ) type LogCmd struct { JobID string `arg:"" help:"Job UUID to get logs for"` - Pipeline string `help:"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}" short:"p"` - BuildNumber string `help:"The build number" short:"b"` + Pipeline string `help:"Deprecated; ignored because job UUIDs no longer require pipeline or build context" short:"p"` + BuildNumber string `help:"Deprecated; ignored because job UUIDs no longer require pipeline or build context" short:"b"` NoTimestamps bool `help:"Strip timestamp prefixes from log output" name:"no-timestamps"` } func (c *LogCmd) Help() string { return ` Examples: - # Get a job's logs by UUID (requires --pipeline and --build) - $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 -p my-pipeline -b 123 - - # If inside a git repository with a configured pipeline - $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 -b 123 + # Get a job's logs by UUID + $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 # Strip timestamp prefixes from output - $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 -p my-pipeline -b 123 --no-timestamps + $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 --no-timestamps ` } @@ -47,46 +41,23 @@ func (c *LogCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { f.Quiet = globals.IsQuiet() f.NoPager = f.NoPager || globals.DisablePager() + organization, err := configuredOrganization(f.Config.OrganizationSlug()) + if err != nil { + return err + } if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil { return err } + warnIgnoredJobContextFlags(kongCtx.Stderr, c.Pipeline, c.BuildNumber) ctx := context.Background() - pipelineRes := pipelineResolver.NewAggregateResolver( - pipelineResolver.ResolveFromFlag(c.Pipeline, f.Config), - pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)), - pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))), - ) - - optionsResolver := options.AggregateResolver{ - options.ResolveBranchFromRepository(f.GitRepository), - } - - args := []string{} - if c.BuildNumber != "" { - args = []string{c.BuildNumber} - } - buildRes := buildResolver.NewAggregateResolver( - buildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config), - buildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...), - ) - - bld, err := buildRes.Resolve(ctx) - if err != nil { - return err - } - if bld == nil { - return fmt.Errorf("no build found") - } - var logContent string if err = bkIO.SpinWhile(f, "Fetching job log", func() error { - jobLog, _, apiErr := f.RestAPIClient.Jobs.GetJobLog( + jobLog, apiErr := getJobLog( ctx, - bld.Organization, - bld.Pipeline, - fmt.Sprint(bld.BuildNumber), + f.RestAPIClient, + organization, c.JobID, ) if apiErr != nil { diff --git a/cmd/job/organization.go b/cmd/job/organization.go new file mode 100644 index 00000000..9a4a5e1e --- /dev/null +++ b/cmd/job/organization.go @@ -0,0 +1,11 @@ +package job + +import "fmt" + +func configuredOrganization(organization string) (string, error) { + if organization == "" { + return "", fmt.Errorf("no organization configured. Run bk auth login, or bk use, to set an organization") + } + + return organization, nil +} diff --git a/cmd/job/organization_test.go b/cmd/job/organization_test.go new file mode 100644 index 00000000..fbea90e5 --- /dev/null +++ b/cmd/job/organization_test.go @@ -0,0 +1,23 @@ +package job + +import "testing" + +func TestConfiguredOrganization(t *testing.T) { + t.Parallel() + + got, err := configuredOrganization("buildkite") + if err != nil { + t.Fatalf("configuredOrganization() error = %v", err) + } + if got != "buildkite" { + t.Fatalf("configuredOrganization() = %q", got) + } +} + +func TestConfiguredOrganizationRequiresOrganization(t *testing.T) { + t.Parallel() + + if _, err := configuredOrganization(""); err == nil { + t.Fatal("configuredOrganization() error = nil, want error") + } +} diff --git a/cmd/job/reprioritize.go b/cmd/job/reprioritize.go index 4657535a..f8eb7fc2 100644 --- a/cmd/job/reprioritize.go +++ b/cmd/job/reprioritize.go @@ -5,11 +5,8 @@ import ( "fmt" "github.com/alecthomas/kong" - buildResolver "github.com/buildkite/cli/v3/internal/build/resolver" - "github.com/buildkite/cli/v3/internal/build/resolver/options" "github.com/buildkite/cli/v3/internal/cli" bkIO "github.com/buildkite/cli/v3/internal/io" - pipelineResolver "github.com/buildkite/cli/v3/internal/pipeline/resolver" "github.com/buildkite/cli/v3/pkg/cmd/factory" "github.com/buildkite/cli/v3/pkg/cmd/validation" buildkite "github.com/buildkite/go-buildkite/v4" @@ -18,18 +15,15 @@ import ( type ReprioritizeCmd struct { JobID string `arg:"" help:"Job UUID to reprioritize"` Priority int `arg:"" help:"New priority value for the job"` - Pipeline string `help:"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}" short:"p"` - BuildNumber string `help:"The build number" short:"b"` + Pipeline string `help:"Deprecated; ignored because job UUIDs no longer require pipeline or build context" short:"p"` + BuildNumber string `help:"Deprecated; ignored because job UUIDs no longer require pipeline or build context" short:"b"` } func (c *ReprioritizeCmd) Help() string { return ` Examples: - # Reprioritize a job (requires --pipeline and --build) - $ bk job reprioritize 0190046e-e199-453b-a302-a21a4d649d31 1 -p my-pipeline -b 123 - - # If inside a git repository with a configured pipeline - $ bk job reprioritize 0190046e-e199-453b-a302-a21a4d649d31 1 -b 123 + # Reprioritize a job by UUID + $ bk job reprioritize 0190046e-e199-453b-a302-a21a4d649d31 1 ` } @@ -43,51 +37,26 @@ func (c *ReprioritizeCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) er f.NoInput = globals.DisableInput() f.Quiet = globals.IsQuiet() + organization, err := configuredOrganization(f.Config.OrganizationSlug()) + if err != nil { + return err + } if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil { return err } + warnIgnoredJobContextFlags(kongCtx.Stderr, c.Pipeline, c.BuildNumber) ctx := context.Background() - pipelineRes := pipelineResolver.NewAggregateResolver( - pipelineResolver.ResolveFromFlag(c.Pipeline, f.Config), - pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)), - pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))), - ) - - optionsResolver := options.AggregateResolver{ - options.ResolveBranchFromRepository(f.GitRepository), - } - - args := []string{} - if c.BuildNumber != "" { - args = []string{c.BuildNumber} - } - buildRes := buildResolver.NewAggregateResolver( - buildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config), - buildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...), - ) - - bld, err := buildRes.Resolve(ctx) - if err != nil { - return err - } - if bld == nil { - return fmt.Errorf("no build found") - } - var job buildkite.Job if err = bkIO.SpinWhile(f, "Reprioritizing job", func() error { var apiErr error - job, _, apiErr = f.RestAPIClient.Jobs.ReprioritizeJob( + job, apiErr = reprioritizeJob( ctx, - bld.Organization, - bld.Pipeline, - fmt.Sprint(bld.BuildNumber), + f.RestAPIClient, + organization, c.JobID, - &buildkite.JobReprioritizationOptions{ - Priority: c.Priority, - }, + c.Priority, ) return apiErr }); err != nil { diff --git a/cmd/job/rest.go b/cmd/job/rest.go new file mode 100644 index 00000000..842e928e --- /dev/null +++ b/cmd/job/rest.go @@ -0,0 +1,69 @@ +package job + +import ( + "context" + "fmt" + "net/url" + + buildkite "github.com/buildkite/go-buildkite/v4" +) + +type unblockJobOptions struct { + Fields map[string]any `json:"fields,omitempty"` +} + +func organizationJobPath(organization, jobID, action string) string { + return fmt.Sprintf( + "v2/organizations/%s/jobs/%s/%s", + url.PathEscape(organization), + url.PathEscape(jobID), + action, + ) +} + +func getJobLog(ctx context.Context, client *buildkite.Client, organization, jobID string) (buildkite.JobLog, error) { + req, err := client.NewRequest(ctx, "GET", organizationJobPath(organization, jobID, "log"), nil) + if err != nil { + return buildkite.JobLog{}, err + } + req.Header.Set("Accept", "application/json") + + var jobLog buildkite.JobLog + if _, err := client.Do(req, &jobLog); err != nil { + return buildkite.JobLog{}, err + } + + return jobLog, nil +} + +func reprioritizeJob(ctx context.Context, client *buildkite.Client, organization, jobID string, priority int) (buildkite.Job, error) { + req, err := client.NewRequest(ctx, "PUT", organizationJobPath(organization, jobID, "reprioritize"), &buildkite.JobReprioritizationOptions{ + Priority: priority, + }) + if err != nil { + return buildkite.Job{}, err + } + + var job buildkite.Job + if _, err := client.Do(req, &job); err != nil { + return buildkite.Job{}, err + } + + return job, nil +} + +func unblockJob(ctx context.Context, client *buildkite.Client, organization, jobID string, fields map[string]any) (buildkite.Job, error) { + req, err := client.NewRequest(ctx, "PUT", organizationJobPath(organization, jobID, "unblock"), &unblockJobOptions{ + Fields: fields, + }) + if err != nil { + return buildkite.Job{}, err + } + + var job buildkite.Job + if _, err := client.Do(req, &job); err != nil { + return buildkite.Job{}, err + } + + return job, nil +} diff --git a/cmd/job/unblock.go b/cmd/job/unblock.go index c00e5d5d..887a245f 100644 --- a/cmd/job/unblock.go +++ b/cmd/job/unblock.go @@ -2,6 +2,7 @@ package job import ( "context" + "encoding/json" "errors" "fmt" "io" @@ -10,16 +11,12 @@ import ( "github.com/alecthomas/kong" "github.com/buildkite/cli/v3/internal/cli" - bkGraphQL "github.com/buildkite/cli/v3/internal/graphql" bkIO "github.com/buildkite/cli/v3/internal/io" - "github.com/buildkite/cli/v3/internal/util" "github.com/buildkite/cli/v3/pkg/cmd/factory" "github.com/buildkite/cli/v3/pkg/cmd/validation" - "github.com/vektah/gqlparser/v2/gqlerror" + buildkite "github.com/buildkite/go-buildkite/v4" ) -const jobBlockPrefix = "JobTypeBlock---" - type UnblockCmd struct { JobID string `arg:"" help:"Job UUID to unblock"` Data string `help:"JSON formatted data to unblock the job"` @@ -30,7 +27,6 @@ func (c *UnblockCmd) Help() string { Unblock a job. Use this command to unblock build jobs. -Currently, this does not support submitting fields to the step. Examples: # Unblock a job by UUID @@ -54,63 +50,80 @@ func (c *UnblockCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { f.NoInput = globals.DisableInput() f.Quiet = globals.IsQuiet() + organization, err := configuredOrganization(f.Config.OrganizationSlug()) + if err != nil { + return err + } if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil { return err } - // Given a job UUID argument, we need to generate the GraphQL ID matching - graphqlID := util.GenerateGraphQLID(jobBlockPrefix, c.JobID) + ctx := context.Background() - // Get unblock step fields if available - var fields *string - if bkIO.HasDataAvailable(os.Stdin) { - stdin := new(strings.Builder) - _, err := io.Copy(stdin, os.Stdin) - if err != nil { - return err - } - input := stdin.String() - fields = &input - } else if c.Data != "" { - fields = &c.Data - } else { - // The GraphQL API errors if providing a null fields value so we need to provide an empty json object - input := "{}" - fields = &input + fields, err := c.unblockFields() + if err != nil { + return err } - ctx := context.Background() - var result *bkGraphQL.UnblockJobResponse + var job buildkite.Job err = bkIO.SpinWhile(f, "Unblocking job", func() error { - result, err = bkGraphQL.UnblockJob(ctx, f.GraphQLClient, graphqlID, fields) - return err + var apiErr error + job, apiErr = unblockJob(ctx, f.RestAPIClient, organization, c.JobID, fields) + return apiErr }) if err != nil { - // Handle a "graphql error" if the job is already unblocked - var errList gqlerror.List - if errors.As(err, &errList) { - for _, gqlErr := range errList { - if gqlErr.Message == "The job's state must be blocked" { - fmt.Println("This job is already unblocked") - return nil - } - } + if isAlreadyUnblocked(err) { + fmt.Println("This job is already unblocked") + return nil } return err } - if err := validateUnblockResponse(result); err != nil { - return err + if job.WebURL != "" { + fmt.Println("Successfully unblocked job: " + job.WebURL) + return nil } fmt.Println("Successfully unblocked job") return nil } -func validateUnblockResponse(result *bkGraphQL.UnblockJobResponse) error { - if result == nil || result.JobTypeBlockUnblock == nil { - return fmt.Errorf("failed to unblock job") +func (c *UnblockCmd) unblockFields() (map[string]any, error) { + if bkIO.HasDataAvailable(os.Stdin) { + stdin := new(strings.Builder) + if _, err := io.Copy(stdin, os.Stdin); err != nil { + return nil, err + } + return parseUnblockFields(stdin.String()) + } else if c.Data != "" { + return parseUnblockFields(c.Data) + } + + return nil, nil +} + +func parseUnblockFields(input string) (map[string]any, error) { + input = strings.TrimSpace(input) + if input == "" { + return nil, nil } - return nil + var fields map[string]any + if err := json.Unmarshal([]byte(input), &fields); err != nil { + return nil, fmt.Errorf("parsing unblock data as JSON: %w", err) + } + if fields == nil { + return nil, fmt.Errorf("unblock data must be a JSON object") + } + + return fields, nil +} + +func isAlreadyUnblocked(err error) bool { + var apiErr *buildkite.ErrorResponse + if !errors.As(err, &apiErr) { + return false + } + + return apiErr.Message == "The job's state must be blocked" || apiErr.Message == "The job's state must be blocked." } diff --git a/cmd/job/unblock_test.go b/cmd/job/unblock_test.go index 40833ae8..24cfdd88 100644 --- a/cmd/job/unblock_test.go +++ b/cmd/job/unblock_test.go @@ -1,61 +1,249 @@ package job import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" "testing" - bkGraphQL "github.com/buildkite/cli/v3/internal/graphql" + buildkite "github.com/buildkite/go-buildkite/v4" ) -func TestValidateUnblockResponse(t *testing.T) { +func TestParseUnblockFields(t *testing.T) { t.Parallel() tests := []struct { name string - input *bkGraphQL.UnblockJobResponse + input string + want map[string]any wantErr bool }{ { - name: "nil response", - input: nil, - wantErr: true, + name: "empty", + input: "", + want: nil, }, { - name: "nil payload", - input: &bkGraphQL.UnblockJobResponse{ - JobTypeBlockUnblock: nil, + name: "string field", + input: `{"release":"v1.2.3"}`, + want: map[string]any{ + "release": "v1.2.3", }, - wantErr: true, }, { - name: "successful unblock", - input: &bkGraphQL.UnblockJobResponse{ - JobTypeBlockUnblock: &bkGraphQL.UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayload{ - JobTypeBlock: bkGraphQL.UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock{ - State: bkGraphQL.JobStatesUnblocked, - }, + name: "nested field", + input: `{"payload":{"confirm":true},"targets":["staging","production"]}`, + want: map[string]any{ + "payload": map[string]any{ + "confirm": true, }, + "targets": []any{"staging", "production"}, }, - wantErr: false, }, { - name: "non-nil payload with finished state", - input: &bkGraphQL.UnblockJobResponse{ - JobTypeBlockUnblock: &bkGraphQL.UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayload{ - JobTypeBlock: bkGraphQL.UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock{ - State: bkGraphQL.JobStatesFinished, - }, - }, - }, - wantErr: false, + name: "array is invalid", + input: `["staging"]`, + wantErr: true, + }, + { + name: "null is invalid", + input: `null`, + wantErr: true, + }, + { + name: "invalid JSON", + input: `{`, + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - err := validateUnblockResponse(tt.input) + + got, err := parseUnblockFields(tt.input) if (err != nil) != tt.wantErr { - t.Errorf("validateUnblockResponse() error = %v, wantErr %v", err, tt.wantErr) + t.Fatalf("parseUnblockFields() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr { + return + } + + gotJSON, err := json.Marshal(got) + if err != nil { + t.Fatalf("marshal got: %v", err) + } + wantJSON, err := json.Marshal(tt.want) + if err != nil { + t.Fatalf("marshal want: %v", err) + } + if string(gotJSON) != string(wantJSON) { + t.Fatalf("parseUnblockFields() = %s, want %s", gotJSON, wantJSON) + } + }) + } +} + +func TestUnblockJobUsesRESTEndpoint(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Fatalf("method = %s, want PUT", r.Method) + } + if r.URL.Path != "/v2/organizations/buildkite/jobs/job-1/unblock" { + t.Fatalf("path = %s", r.URL.Path) + } + + var body struct { + Fields map[string]any `json:"fields"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode body: %v", err) + } + if body.Fields["release"] != "v1.2.3" { + t.Fatalf("fields = %#v", body.Fields) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"job-1","state":"unblocked","web_url":"https://buildkite.com/buildkite/cli/builds/42#job-1"}`)) + })) + defer server.Close() + + client, err := buildkite.NewOpts( + buildkite.WithBaseURL(server.URL), + buildkite.WithTokenAuth("test-token"), + ) + if err != nil { + t.Fatalf("new client: %v", err) + } + + job, err := unblockJob(context.Background(), client, "buildkite", "job-1", map[string]any{"release": "v1.2.3"}) + if err != nil { + t.Fatalf("unblockJob() error = %v", err) + } + if job.ID != "job-1" || job.State != "unblocked" { + t.Fatalf("job = %#v", job) + } +} + +func TestGetJobLogUsesOrganizationEndpoint(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Fatalf("method = %s, want GET", r.Method) + } + if r.URL.Path != "/v2/organizations/buildkite/jobs/job-1/log" { + t.Fatalf("path = %s", r.URL.Path) + } + if r.Header.Get("Accept") != "application/json" { + t.Fatalf("Accept = %q", r.Header.Get("Accept")) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"content":"hello log","size":9}`)) + })) + defer server.Close() + + client, err := buildkite.NewOpts( + buildkite.WithBaseURL(server.URL), + buildkite.WithTokenAuth("test-token"), + ) + if err != nil { + t.Fatalf("new client: %v", err) + } + + log, err := getJobLog(context.Background(), client, "buildkite", "job-1") + if err != nil { + t.Fatalf("getJobLog() error = %v", err) + } + if log.Content != "hello log" || log.Size != 9 { + t.Fatalf("log = %#v", log) + } +} + +func TestReprioritizeJobUsesOrganizationEndpoint(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Fatalf("method = %s, want PUT", r.Method) + } + if r.URL.Path != "/v2/organizations/buildkite/jobs/job-1/reprioritize" { + t.Fatalf("path = %s", r.URL.Path) + } + + var body struct { + Priority int `json:"priority"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode body: %v", err) + } + if body.Priority != 10 { + t.Fatalf("priority = %d", body.Priority) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"job-1","state":"scheduled","web_url":"https://buildkite.com/buildkite/cli/builds/42#job-1"}`)) + })) + defer server.Close() + + client, err := buildkite.NewOpts( + buildkite.WithBaseURL(server.URL), + buildkite.WithTokenAuth("test-token"), + ) + if err != nil { + t.Fatalf("new client: %v", err) + } + + job, err := reprioritizeJob(context.Background(), client, "buildkite", "job-1", 10) + if err != nil { + t.Fatalf("reprioritizeJob() error = %v", err) + } + if job.ID != "job-1" { + t.Fatalf("job = %#v", job) + } +} + +func TestIsAlreadyUnblocked(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + err error + want bool + }{ + { + name: "blocked state API error", + err: &buildkite.ErrorResponse{Message: "The job's state must be blocked"}, + want: true, + }, + { + name: "blocked state API error with period", + err: &buildkite.ErrorResponse{Message: "The job's state must be blocked."}, + want: true, + }, + { + name: "other API error", + err: &buildkite.ErrorResponse{Message: "This job type cannot be unblocked"}, + want: false, + }, + { + name: "other error", + err: errors.New("boom"), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + if got := isAlreadyUnblocked(tt.err); got != tt.want { + t.Fatalf("isAlreadyUnblocked() = %t, want %t", got, tt.want) } }) }