diff --git a/internal/graphql/generated.go b/internal/graphql/generated.go index 73e37f8b..89459350 100644 --- a/internal/graphql/generated.go +++ b/internal/graphql/generated.go @@ -8,6 +8,37 @@ import ( "github.com/Khan/genqlient/graphql" ) +// GetArtifactsArtifact includes the requested fields of the GraphQL type Artifact. +// The GraphQL type's documentation follows. +// +// A file uploaded from the agent whilst running a job +type GetArtifactsArtifact struct { + // The public UUID for this artifact + Uuid string `json:"uuid"` + // The path of the uploaded artifact + Path string `json:"path"` + // The download URL for the artifact. Unless you've used your own artifact storage, the URL will be valid for only 10 minutes. + DownloadURL string `json:"downloadURL"` +} + +// GetUuid returns GetArtifactsArtifact.Uuid, and is useful for accessing the field via an interface. +func (v *GetArtifactsArtifact) GetUuid() string { return v.Uuid } + +// GetPath returns GetArtifactsArtifact.Path, and is useful for accessing the field via an interface. +func (v *GetArtifactsArtifact) GetPath() string { return v.Path } + +// GetDownloadURL returns GetArtifactsArtifact.DownloadURL, and is useful for accessing the field via an interface. +func (v *GetArtifactsArtifact) GetDownloadURL() string { return v.DownloadURL } + +// GetArtifactsResponse is returned by GetArtifacts on success. +type GetArtifactsResponse struct { + // Find an artifact by its UUID + Artifact *GetArtifactsArtifact `json:"artifact"` +} + +// GetArtifact returns GetArtifactsResponse.Artifact, and is useful for accessing the field via an interface. +func (v *GetArtifactsResponse) GetArtifact() *GetArtifactsArtifact { return v.Artifact } + // GetClusterQueueAgentOrganization includes the requested fields of the GraphQL type Organization. // The GraphQL type's documentation follows. // @@ -542,6 +573,14 @@ func (v *UnblockJobResponse) GetJobTypeBlockUnblock() *UnblockJobJobTypeBlockUnb return v.JobTypeBlockUnblock } +// __GetArtifactsInput is used internally by genqlient +type __GetArtifactsInput struct { + ArtifactId string `json:"artifactId"` +} + +// GetArtifactId returns __GetArtifactsInput.ArtifactId, and is useful for accessing the field via an interface. +func (v *__GetArtifactsInput) GetArtifactId() string { return v.ArtifactId } + // __GetClusterQueueAgentInput is used internally by genqlient type __GetClusterQueueAgentInput struct { OrgSlug string `json:"orgSlug"` @@ -614,6 +653,43 @@ func (v *__UnblockJobInput) GetId() string { return v.Id } // GetFields returns __UnblockJobInput.Fields, and is useful for accessing the field via an interface. func (v *__UnblockJobInput) GetFields() *string { return v.Fields } +// The query or mutation executed by GetArtifacts. +const GetArtifacts_Operation = ` +query GetArtifacts ($artifactId: ID!) { + artifact(uuid: $artifactId) { + uuid + path + downloadURL + } +} +` + +func GetArtifacts( + ctx_ context.Context, + client_ graphql.Client, + artifactId string, +) (*GetArtifactsResponse, error) { + req_ := &graphql.Request{ + OpName: "GetArtifacts", + Query: GetArtifacts_Operation, + Variables: &__GetArtifactsInput{ + ArtifactId: artifactId, + }, + } + var err_ error + + var data_ GetArtifactsResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + // The query or mutation executed by GetClusterQueueAgent. const GetClusterQueueAgent_Operation = ` query GetClusterQueueAgent ($orgSlug: ID!, $queueId: [ID!]) { diff --git a/pkg/cmd/artifacts/artifacts.go b/pkg/cmd/artifacts/artifacts.go index 36b5975d..792ee810 100644 --- a/pkg/cmd/artifacts/artifacts.go +++ b/pkg/cmd/artifacts/artifacts.go @@ -2,10 +2,6 @@ package artifacts import ( "github.com/MakeNowJust/heredoc" - "github.com/buildkite/cli/v3/internal/build/resolver" - buildResolver "github.com/buildkite/cli/v3/internal/build/resolver" - "github.com/buildkite/cli/v3/internal/build/resolver/options" - 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" "github.com/spf13/cobra" @@ -19,31 +15,15 @@ func NewCmdArtifacts(f *factory.Factory) *cobra.Command { Short: "Manage pipeline build artifacts", Example: heredoc.Doc(` # To view pipeline build artifacts - $ bk artifacts list -b "build number" + $ bk artifacts list [build number] [flags] + + # To download a specific artifact + $ bk artifacts download `), PersistentPreRunE: validation.CheckValidConfiguration(f.Config), } cmd.AddCommand(NewCmdArtifactsList(f)) + cmd.AddCommand(NewCmdArtifactsDownload(f)) return &cmd } - -func resolveFrom(pipeline string, f *factory.Factory, args []string) resolver.AggregateResolver { - //resolve a pipeline based on how bk build resolves the pipeline - pipelineRes := pipelineResolver.NewAggregateResolver( - pipelineResolver.ResolveFromFlag(pipeline, f.Config), - pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOne), - pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne)), - ) - - // we resolve a build an optional argument or positional argument - optionsResolver := options.AggregateResolver{ - options.ResolveBranchFromRepository(f.GitRepository), - } - - buildRes := buildResolver.NewAggregateResolver( - buildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config), - buildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...), - ) - return buildRes -} diff --git a/pkg/cmd/artifacts/artifacts.graphql b/pkg/cmd/artifacts/artifacts.graphql new file mode 100644 index 00000000..085ae0bb --- /dev/null +++ b/pkg/cmd/artifacts/artifacts.graphql @@ -0,0 +1,16 @@ +query GetArtifacts($artifactId: ID!) { + artifact(uuid: $artifactId) { + uuid + path + downloadURL + job { + uuid + pipeline { + name + } + build { + number + } + } + } +} \ No newline at end of file diff --git a/pkg/cmd/artifacts/download.go b/pkg/cmd/artifacts/download.go new file mode 100644 index 00000000..d7880582 --- /dev/null +++ b/pkg/cmd/artifacts/download.go @@ -0,0 +1,94 @@ +package artifacts + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + + "github.com/MakeNowJust/heredoc" + "github.com/buildkite/cli/v3/internal/graphql" + "github.com/buildkite/cli/v3/pkg/cmd/factory" + "github.com/charmbracelet/huh/spinner" + "github.com/spf13/cobra" +) + +func NewCmdArtifactsDownload(f *factory.Factory) *cobra.Command { + + cmd := cobra.Command{ + DisableFlagsInUseLine: true, + Use: "download ", + Short: "Download an artifact by its UUID", + Args: cobra.ExactArgs(1), + Long: heredoc.Doc(` + Use this command to download a specific artifact. + `), + Example: heredoc.Doc(` + $ bk artifacts download 0191727d-b5ce-4576-b37d-477ae0ca830c + `), + RunE: func(cmd *cobra.Command, args []string) error { + artifactId := args[0] + + var err error + var downloadDir string + spinErr := spinner.New(). + Title("Downloading artifact"). + Action(func() { + downloadDir, err = download(cmd.Context(), f, artifactId) + }). + Run() + if spinErr != nil { + return spinErr + } + if err != nil { + fmt.Println("EXITING due to ERROR HERE") + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "Downloaded artifact to: %s\n", downloadDir) + + return err + }, + } + + return &cmd +} + +func download(ctx context.Context, f *factory.Factory, artifactId string) (string, error) { + var err error + var resp *graphql.GetArtifactsResponse + + resp, err = graphql.GetArtifacts(ctx, f.GraphQLClient, artifactId) + if err != nil { + return "", err + } + + directory := fmt.Sprintf("artifact-%s", artifactId) + err = os.MkdirAll(directory, os.ModePerm) + if err != nil { + return "", err + } + + filename := filepath.Base(resp.Artifact.Path) + out, fileErr := os.Create(filepath.Join(directory, filename)) + if fileErr != nil { + return "", fileErr + } + defer out.Close() + + apiResp, apiErr := http.Get(resp.Artifact.DownloadURL) + if apiErr != nil { + return "", apiErr + } + defer apiResp.Body.Close() + + // Writer the body to file + _, err = io.Copy(out, apiResp.Body) + if err != nil { + return "", err + } + + return directory, nil +} diff --git a/pkg/cmd/artifacts/list.go b/pkg/cmd/artifacts/list.go index 8ac5e1c8..c8205fed 100644 --- a/pkg/cmd/artifacts/list.go +++ b/pkg/cmd/artifacts/list.go @@ -6,6 +6,9 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/buildkite/cli/v3/internal/artifact" + buildResolver "github.com/buildkite/cli/v3/internal/build/resolver" + "github.com/buildkite/cli/v3/internal/build/resolver/options" + pipelineResolver "github.com/buildkite/cli/v3/internal/pipeline/resolver" "github.com/buildkite/cli/v3/pkg/cmd/factory" "github.com/buildkite/go-buildkite/v4" "github.com/charmbracelet/huh/spinner" @@ -39,7 +42,23 @@ func NewCmdArtifactsList(f *factory.Factory) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { var err error - buildRes := resolveFrom(pipeline, f, args) + //resolve a pipeline based on how bk build resolves the pipeline + pipelineRes := pipelineResolver.NewAggregateResolver( + pipelineResolver.ResolveFromFlag(pipeline, f.Config), + pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOne), + pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne)), + ) + + // we resolve a build an optional argument or positional argument + optionsResolver := options.AggregateResolver{ + options.ResolveBranchFromFlag(""), + options.ResolveBranchFromRepository(f.GitRepository), + } + + buildRes := buildResolver.NewAggregateResolver( + buildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config), + buildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...), + ) bld, err := buildRes.Resolve(cmd.Context()) if err != nil {