diff --git a/.env b/.env new file mode 100644 index 0000000..9794490 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +GOLANGCI_LINT_VERSION=v1.61.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 01c884a..f60a18e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,6 +20,9 @@ jobs: - name: Mark source directory as safe. run: git config --global --add safe.directory $GITHUB_WORKSPACE + - name: Load .env file to env vars + run: cat .env >> $GITHUB_ENV + - name: Set up Go uses: actions/setup-go@v5 with: { go-version-file: go.mod } @@ -34,3 +37,4 @@ jobs: uses: golangci/golangci-lint-action@v6 with: args: --verbose + version: ${{ env.GOLANGCI_LINT_VERSION }} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..36892a1 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +include .env + +lint: + docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:$(GOLANGCI_LINT_VERSION)-alpine golangci-lint run diff --git a/client/structs/search.go b/client/structs/search.go index ab209fc..a2645cc 100644 --- a/client/structs/search.go +++ b/client/structs/search.go @@ -23,6 +23,7 @@ type QueryOrder struct { type QueryPredicate struct { Field graphql.String `json:"field"` Constraint QueryFieldConstraint `json:"constraint"` + Exclude graphql.Boolean `json:"exclude"` } // QueryFieldConstraint is a constraint used diff --git a/internal/cmd/module/search_version.go b/internal/cmd/module/search_version.go index 26761e2..b11c0fb 100644 --- a/internal/cmd/module/search_version.go +++ b/internal/cmd/module/search_version.go @@ -2,14 +2,22 @@ package module import ( "fmt" + "slices" "github.com/pkg/errors" "github.com/shurcooL/graphql" + "github.com/spacelift-io/spacectl/client/structs" "github.com/spacelift-io/spacectl/internal/cmd" "github.com/spacelift-io/spacectl/internal/cmd/authenticated" "github.com/urfave/cli/v2" ) +const ( + maxSearchModuleVersionsPageSize = 50 + moduleVersionsTableLimit = 20 + moduleVersionsJSONLimit = 0 // no limit +) + func listVersions() cli.ActionFunc { return func(cliCtx *cli.Context) error { outputFormat, err := cmd.GetOutputFormat(cliCtx) @@ -19,70 +27,137 @@ func listVersions() cli.ActionFunc { switch outputFormat { case cmd.OutputFormatTable: - return listVersionsTable(cliCtx) + versions, err := getModuleVersions(cliCtx, moduleVersionsTableLimit) + if err != nil { + return err + } + + return formatModuleVersionsTable(versions) case cmd.OutputFormatJSON: - return listVersionsJSON(cliCtx) + versions, err := getModuleVersions(cliCtx, moduleVersionsJSONLimit) + if err != nil { + return err + } + + return formatModuleVersionsJSON(versions) } return fmt.Errorf("unknown output format: %v", outputFormat) } } -func listVersionsJSON(cliCtx *cli.Context) error { - var query struct { - Module struct { - Verions []version `graphql:"versions(includeFailed: $includeFailed)"` - } `graphql:"module(id: $id)"` +func getModuleVersions(cliCtx *cli.Context, limit int) ([]version, error) { + if limit < 0 { + return nil, errors.New("limit must be greater or equal to 0") } - if err := authenticated.Client.Query(cliCtx.Context, &query, map[string]interface{}{ - "id": cliCtx.String(flagModuleID.Name), - "includeFailed": graphql.Boolean(false), - }); err != nil { - return errors.Wrap(err, "failed to query list of modules") + var cursor string + var versions []version + + fetchAll := limit == 0 + + var pageSize int + + for { + if fetchAll { + pageSize = maxSearchModuleVersionsPageSize + } else { + pageSize = slices.Min([]int{maxSearchModuleVersionsPageSize, limit - len(versions)}) + } + + result, err := getSearchModuleVersions(cliCtx, cursor, pageSize) + if err != nil { + return nil, err + } + + for _, edge := range result.Edges { + versions = append(versions, edge.Node) + } + + if result.PageInfo.HasNextPage && (fetchAll || limit > len(versions)) { + cursor = result.PageInfo.EndCursor + } else { + break + } } - return cmd.OutputJSON(query.Module.Verions) + + return versions, nil } -func listVersionsTable(cliCtx *cli.Context) error { +func getSearchModuleVersions(cliCtx *cli.Context, cursor string, limit int) (searchModuleVersions, error) { + if limit <= 0 || limit > maxSearchModuleVersionsPageSize { + return searchModuleVersions{}, errors.New("limit must be between 1 and 50") + } + var query struct { Module struct { - Verions []version `graphql:"versions(includeFailed: $includeFailed)"` + SearchModuleVersions searchModuleVersions `graphql:"searchModuleVersions(input: $input)"` } `graphql:"module(id: $id)"` } + var after *graphql.String + if cursor != "" { + after = graphql.NewString(graphql.String(cursor)) + } + if err := authenticated.Client.Query(cliCtx.Context, &query, map[string]interface{}{ - "id": cliCtx.String(flagModuleID.Name), - "includeFailed": graphql.Boolean(false), + "id": cliCtx.String(flagModuleID.Name), + "input": structs.SearchInput{ + First: graphql.NewInt(graphql.Int(int32(limit))), //nolint: gosec + After: after, + OrderBy: &structs.QueryOrder{ + Field: "createdAt", + Direction: "DESC", + }, + Predicates: &[]structs.QueryPredicate{ + { + Field: "state", + Exclude: true, + Constraint: structs.QueryFieldConstraint{ + EnumEquals: &[]graphql.String{ + "FAILED", + }, + }, + }, + }, + }, }); err != nil { - return errors.Wrap(err, "failed to query list of modules") + return searchModuleVersions{}, errors.Wrap(err, "failed to query list of modules") } + return query.Module.SearchModuleVersions, nil +} + +func formatModuleVersionsJSON(versions []version) error { + return cmd.OutputJSON(versions) +} + +func formatModuleVersionsTable(versions []version) error { columns := []string{"ID", "Author", "Message", "Number", "State", "Tests", "Timestamp"} tableData := [][]string{columns} - if len(query.Module.Verions) > 20 { - query.Module.Verions = query.Module.Verions[:20] + for _, v := range versions { + tableData = append(tableData, []string{ + v.ID, + v.Commit.AuthorName, + v.Commit.Message, + v.Number, + v.State, + fmt.Sprintf("%d", v.VersionCount), + fmt.Sprintf("%d", v.Commit.Timestamp), + }) } - // We print the versions in reverse order - // so the latest version is at the bottom, much easier to read in terminal. - for i := len(query.Module.Verions) - 1; i >= 0; i-- { - module := query.Module.Verions[i] - row := []string{ - module.ID, - module.Commit.AuthorName, - module.Commit.Message, - module.Number, - module.State, - fmt.Sprintf("%d", module.VersionCount), - fmt.Sprintf("%d", module.Commit.Timestamp), - } + return cmd.OutputTable(tableData, true) +} - tableData = append(tableData, row) - } +type searchModuleVersions struct { + PageInfo structs.PageInfo `graphql:"pageInfo"` + Edges []searchModuleVersionEdge `graphql:"edges"` +} - return cmd.OutputTable(tableData, true) +type searchModuleVersionEdge struct { + Node version `graphql:"node"` } type version struct { diff --git a/internal/cmd/profile/login_command.go b/internal/cmd/profile/login_command.go index 12c9b26..445f63e 100644 --- a/internal/cmd/profile/login_command.go +++ b/internal/cmd/profile/login_command.go @@ -100,7 +100,7 @@ func getCredentialsType(ctx *cli.Context) (session.CredentialsType, error) { return 0, err } - return session.CredentialsType(result + 1), nil + return session.CredentialsType(result + 1), nil //nolint: gosec } func readEndpoint(ctx *cli.Context, reader *bufio.Reader) (string, error) {