diff --git a/client/enums/run_review_decision.go b/client/enums/run_review_decision.go new file mode 100644 index 0000000..8d8a35c --- /dev/null +++ b/client/enums/run_review_decision.go @@ -0,0 +1,12 @@ +package enums + +// RunReviewDecision represents the API RunReviewDecision enum. +type RunReviewDecision string + +const ( + // RunReviewDecisionApprove represents an approval decision. + RunReviewDecisionApprove = "APPROVE" + + // RunReviewDecisionReject represents a rejection decision. + RunReviewDecisionReject = "REJECT" +) diff --git a/internal/cmd/stack/flags.go b/internal/cmd/stack/flags.go index 6e73521..8c0765a 100644 --- a/internal/cmd/stack/flags.go +++ b/internal/cmd/stack/flags.go @@ -36,12 +36,17 @@ var flagRequiredCommitSHA = &cli.StringFlag{ Required: true, } -var flagRun = &cli.StringFlag{ +var flagRequiredRun = &cli.StringFlag{ Name: "run", Usage: "[Required] `ID` of the run", Required: true, } +var flagRun = &cli.StringFlag{ + Name: "run", + Usage: "`ID` of the run", +} + var flagNoInit = &cli.BoolFlag{ Name: "noinit", Usage: "Indicate whether to skip initialization for a task", diff --git a/internal/cmd/stack/run_confirm.go b/internal/cmd/stack/run_confirm.go index 6778474..18f91f9 100644 --- a/internal/cmd/stack/run_confirm.go +++ b/internal/cmd/stack/run_confirm.go @@ -22,7 +22,7 @@ func runConfirm() cli.ActionFunc { variables := map[string]interface{}{ "stack": graphql.ID(stackID), - "run": graphql.ID(cliCtx.String(flagRun.Name)), + "run": graphql.ID(cliCtx.String(flagRequiredRun.Name)), } ctx := context.Background() diff --git a/internal/cmd/stack/run_discard.go b/internal/cmd/stack/run_discard.go index 808210a..afb555a 100644 --- a/internal/cmd/stack/run_discard.go +++ b/internal/cmd/stack/run_discard.go @@ -22,7 +22,7 @@ func runDiscard() cli.ActionFunc { variables := map[string]interface{}{ "stack": graphql.ID(stackID), - "run": graphql.ID(cliCtx.String(flagRun.Name)), + "run": graphql.ID(cliCtx.String(flagRequiredRun.Name)), } ctx := context.Background() diff --git a/internal/cmd/stack/run_retry.go b/internal/cmd/stack/run_retry.go index aa28e7b..ac9621d 100644 --- a/internal/cmd/stack/run_retry.go +++ b/internal/cmd/stack/run_retry.go @@ -11,7 +11,7 @@ import ( func runRetry(cliCtx *cli.Context) error { stackID := cliCtx.String(flagStackID.Name) - runID := cliCtx.String(flagRun.Name) + runID := cliCtx.String(flagRequiredRun.Name) var mutation struct { RunRetry struct { diff --git a/internal/cmd/stack/run_review.go b/internal/cmd/stack/run_review.go new file mode 100644 index 0000000..7085aed --- /dev/null +++ b/internal/cmd/stack/run_review.go @@ -0,0 +1,65 @@ +package stack + +import ( + "context" + "fmt" + + "github.com/shurcooL/graphql" + "github.com/urfave/cli/v2" + + "github.com/spacelift-io/spacectl/client/enums" + "github.com/spacelift-io/spacectl/internal/cmd/authenticated" +) + +type runReviewMutation struct { + Review struct { + ID string `graphql:"id"` + } `graphql:"runReview(stack: $stack, run: $run, decision: $decision, note: $note)"` +} + +var flagRunReviewNote = &cli.StringFlag{ + Name: "note", + Usage: "Description of why the review decision was made.", + Required: false, +} + +func runApprove(cliCtx *cli.Context) error { + stackID := cliCtx.String(flagStackID.Name) + runID := cliCtx.String(flagRequiredRun.Name) + note := cliCtx.String(flagRunReviewNote.Name) + + if nArgs := cliCtx.NArg(); nArgs != 0 { + return fmt.Errorf("expected zero arguments but got %d", nArgs) + } + + return addRunReview(cliCtx.Context, stackID, runID, note, enums.RunReviewDecisionApprove) +} + +func runReject(cliCtx *cli.Context) error { + stackID := cliCtx.String(flagStackID.Name) + runID := cliCtx.String(flagRequiredRun.Name) + note := cliCtx.String(flagRunReviewNote.Name) + + if nArgs := cliCtx.NArg(); nArgs != 0 { + return fmt.Errorf("expected zero arguments but got %d", nArgs) + } + + return addRunReview(cliCtx.Context, stackID, runID, note, enums.RunReviewDecisionReject) +} + +func addRunReview(ctx context.Context, stackID, runID, note string, decision enums.RunReviewDecision) error { + var runIDGQL *graphql.ID + if runID != "" { + runIDGQL = graphql.NewID(runID) + } + + var mutation runReviewMutation + variables := map[string]interface{}{ + "stack": graphql.ID(stackID), + "run": runIDGQL, + "decision": decision, + "note": graphql.String(note), + } + + return authenticated.Client.Mutate(ctx, &mutation, variables) +} diff --git a/internal/cmd/stack/stack.go b/internal/cmd/stack/stack.go index df5614a..ce12128 100644 --- a/internal/cmd/stack/stack.go +++ b/internal/cmd/stack/stack.go @@ -21,7 +21,7 @@ func Command() *cli.Command { Usage: "Confirm an unconfirmed tracked run", Flags: []cli.Flag{ flagStackID, - flagRun, + flagRequiredRun, flagRunMetadata, flagTail, }, @@ -35,13 +35,39 @@ func Command() *cli.Command { Usage: "Discard an unconfirmed tracked run", Flags: []cli.Flag{ flagStackID, - flagRun, + flagRequiredRun, flagTail, }, Action: runDiscard(), Before: authenticated.Ensure, ArgsUsage: cmd.EmptyArgsUsage, }, + { + Category: "Run management", + Name: "approve", + Usage: "Approves a run or task. If no run is specified, the approval will be added to the current stack blocker.", + Flags: []cli.Flag{ + flagStackID, + flagRun, + flagRunReviewNote, + }, + Action: runApprove, + Before: authenticated.Ensure, + ArgsUsage: cmd.EmptyArgsUsage, + }, + { + Category: "Run management", + Name: "reject", + Usage: "Rejects a run or task. If no run is specified, the rejection will be added to the current stack blocker.", + Flags: []cli.Flag{ + flagStackID, + flagRun, + flagRunReviewNote, + }, + Action: runReject, + Before: authenticated.Ensure, + ArgsUsage: cmd.EmptyArgsUsage, + }, { Category: "Run management", Name: "deploy", @@ -62,7 +88,7 @@ func Command() *cli.Command { Usage: "Retry a failed run", Flags: []cli.Flag{ flagStackID, - flagRun, + flagRequiredRun, flagTail, }, Action: runRetry, @@ -100,11 +126,11 @@ func Command() *cli.Command { Usage: "Show logs for a particular run", Flags: []cli.Flag{ flagStackID, - flagRun, + flagRequiredRun, }, Action: func(cliCtx *cli.Context) error { stackID := cliCtx.String(flagStackID.Name) - _, err := runLogs(context.Background(), stackID, cliCtx.String(flagRun.Name)) + _, err := runLogs(context.Background(), stackID, cliCtx.String(flagRequiredRun.Name)) return err }, Before: authenticated.Ensure, diff --git a/specs/spacectl-to-v1.md b/specs/spacectl-to-v1.md index 4caea33..e242c2d 100644 --- a/specs/spacectl-to-v1.md +++ b/specs/spacectl-to-v1.md @@ -169,8 +169,11 @@ We should support the following commands for working with Stacks: - [ ] `delete` - deletes a stack - [x] `show` - outputs information about a specified Stack - [ ] `edit` - edits the name, labels and description for the stack - - [ ] `set-current-commit` - sets the current commit for the stack + - [x] `set-current-commit` - sets the current commit for the stack - [x] `confirm` - confirms a run awaiting approval + - [x] `discard` - discards a run awaiting approval + - [x] `approve` - approves a run or task + - [x] `reject` - rejects a run or task - [x] `deploy` - triggers a tracked (i.e. deployment) run - [x] `preview` - triggers a preview run for a specific commit - [x] `local-preview` - triggers a local-preview run using the current directory as the workspace