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
107 changes: 64 additions & 43 deletions cmd/environment_stage_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"context"
"encoding/json"
"os"
"strconv"

"github.com/pterm/pterm"
Expand All @@ -19,34 +18,45 @@ var environmentStageListCmd = &cobra.Command{
utils.Capture(cmd)

tokenType, token, err := utils.GetAccessToken()
if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}
utils.CheckError(err)

client := utils.GetQoveryClient(tokenType, token)
_, _, environmentId, err := getOrganizationProjectEnvironmentContextResourcesIds(client)

if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}
utils.CheckError(err)

stages, _, err := client.DeploymentStageMainCallsAPI.ListEnvironmentDeploymentStage(context.Background(), environmentId).Execute()

if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}
utils.CheckError(err)

if jsonFlag {
utils.Println(getEnvironmentStageJsonOutput(*client, stages.GetResults()))
return
}

// Collect all skipped services across all stages
var skippedData [][]string
for _, stage := range stages.GetResults() {
for _, service := range stage.GetServices() {
if service.GetIsSkipped() {
skippedData = append(skippedData, []string{
service.Id,
service.GetServiceType(),
utils.GetServiceNameByIdAndType(client, service.GetServiceId(), service.GetServiceType()),
stage.GetName(),
})
}
}
}

// Show skipped services section first
if len(skippedData) > 0 {
pterm.DefaultSection.WithBottomPadding(0).Println("Skipped services (excluded from environment-level deployments)")
utils.Println("")
err = utils.PrintTable([]string{"Id", "Type", "Name", "Stage"}, skippedData)
utils.Println("")
utils.CheckError(err)
}

// Show each stage with only non-skipped services
for _, stage := range stages.GetResults() {
pterm.DefaultSection.WithBottomPadding(0).Println("deployment stage " + strconv.Itoa(int(stage.GetDeploymentOrder()+1)) + ": \"" + stage.GetName() + "\"")
utils.Println("Stage id: " + stage.GetId())
Expand All @@ -58,42 +68,55 @@ var environmentStageListCmd = &cobra.Command{

var data [][]string
for _, service := range stage.GetServices() {
data = append(data, []string{
service.Id,
service.GetServiceType(),
utils.GetServiceNameByIdAndType(client, service.GetServiceId(), service.GetServiceType()),
})
if !service.GetIsSkipped() {
data = append(data, []string{
service.Id,
service.GetServiceType(),
utils.GetServiceNameByIdAndType(client, service.GetServiceId(), service.GetServiceType()),
})
}
}

if len(stage.GetServices()) == 0 {
utils.Println("<no service>")
if len(data) == 0 {
if len(stage.GetServices()) == 0 {
utils.Println("<no service>")
} else {
utils.Println("<all services skipped>")
}
} else {
err = utils.PrintTable([]string{"Id", "Type", "Name"}, data)
utils.CheckError(err)
}

utils.Println("")

if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}
}
},
}

func getEnvironmentStageJsonOutput(client qovery.APIClient, stages []qovery.DeploymentStageResponse) string {
var skippedServices []interface{}
var results []interface{}

for idx, stage := range stages {
var services []interface{}

for _, service := range stage.Services {
services = append(services, map[string]interface{}{
"id": service.ServiceId,
"type": service.ServiceType,
"name": utils.GetServiceNameByIdAndType(&client, service.GetServiceId(), service.GetServiceType()),
})
entry := map[string]interface{}{
"id": service.ServiceId,
"type": service.ServiceType,
"name": utils.GetServiceNameByIdAndType(&client, service.GetServiceId(), service.GetServiceType()),
"is_skipped": service.GetIsSkipped(),
}
services = append(services, entry)

if service.GetIsSkipped() {
skippedServices = append(skippedServices, map[string]interface{}{
"id": service.ServiceId,
"type": service.ServiceType,
"name": utils.GetServiceNameByIdAndType(&client, service.GetServiceId(), service.GetServiceType()),
"stage": stage.Name,
})
}
}

results = append(results, map[string]interface{}{
Expand All @@ -105,13 +128,11 @@ func getEnvironmentStageJsonOutput(client qovery.APIClient, stages []qovery.Depl
})
}

j, err := json.Marshal(results)

if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}
j, err := json.Marshal(map[string]interface{}{
"skipped_services": skippedServices,
"stages": results,
})
utils.CheckError(err)

return string(j)
}
Expand Down
64 changes: 64 additions & 0 deletions cmd/environment_stage_skip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cmd

import (
"context"
"errors"

"github.com/qovery/qovery-cli/utils"
"github.com/qovery/qovery-client-go"
"github.com/spf13/cobra"
)

var environmentStageSkipCmd = &cobra.Command{
Use: "skip",
Short: "Skip service from environment-level deployments",
Long: "Mark a service as skipped so it is excluded from environment-level bulk deployments while staying in its current stage. To reverse this, use 'environment stage unskip' or move the service to a different deployment stage, which automatically clears the skipped status.",
Run: func(cmd *cobra.Command, args []string) {
utils.Capture(cmd)

tokenType, token, err := utils.GetAccessToken()
utils.CheckError(err)

client := utils.GetQoveryClient(tokenType, token)
_, _, environmentId, err := getOrganizationProjectEnvironmentContextResourcesIds(client)
utils.CheckError(err)

stages, _, err := client.DeploymentStageMainCallsAPI.ListEnvironmentDeploymentStage(context.Background(), environmentId).Execute()
utils.CheckError(err)

var service *qovery.DeploymentStageServiceResponse
var currentStageId string
for _, stage := range stages.GetResults() {
service, _ = getServiceByName(client, stage.GetServices(), serviceName)
if service != nil {
currentStageId = stage.GetId()
break
}
}

if service == nil {
utils.CheckError(errors.New("service not found"))
}

req := qovery.AttachServiceToDeploymentStageRequest{}
req.SetIsSkipped(true)

_, _, err = client.DeploymentStageMainCallsAPI.
AttachServiceToDeploymentStage(context.Background(), currentStageId, service.GetServiceId()).
AttachServiceToDeploymentStageRequest(req).
Execute()
utils.CheckError(err)

utils.Println("Service \"" + serviceName + "\" is now skipped from environment-level deployments")
},
}

func init() {
environmentStageCmd.AddCommand(environmentStageSkipCmd)
environmentStageSkipCmd.Flags().StringVarP(&organizationName, "organization", "", "", "Organization Name")
environmentStageSkipCmd.Flags().StringVarP(&projectName, "project", "", "", "Project Name")
environmentStageSkipCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name")
environmentStageSkipCmd.Flags().StringVarP(&serviceName, "name", "n", "", "Service Name")
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description's test plan mentions using --stage flag, but the actual implementation uses --name flag to specify the service name. The command automatically finds which stage the service is in. Update the PR description to reflect the actual command syntax: qovery environment stage skip --name &lt;service-name&gt;.

Copilot uses AI. Check for mistakes.

_ = environmentStageSkipCmd.MarkFlagRequired("name")
}
64 changes: 64 additions & 0 deletions cmd/environment_stage_unskip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cmd

import (
"context"
"errors"

"github.com/qovery/qovery-cli/utils"
"github.com/qovery/qovery-client-go"
"github.com/spf13/cobra"
)

var environmentStageUnskipCmd = &cobra.Command{
Use: "unskip",
Short: "Unskip service from environment-level deployments",
Long: "Remove the skipped flag from a service so it is included again in environment-level bulk deployments.",
Run: func(cmd *cobra.Command, args []string) {
utils.Capture(cmd)

tokenType, token, err := utils.GetAccessToken()
utils.CheckError(err)

client := utils.GetQoveryClient(tokenType, token)
_, _, environmentId, err := getOrganizationProjectEnvironmentContextResourcesIds(client)
utils.CheckError(err)

stages, _, err := client.DeploymentStageMainCallsAPI.ListEnvironmentDeploymentStage(context.Background(), environmentId).Execute()
utils.CheckError(err)

var service *qovery.DeploymentStageServiceResponse
var currentStageId string
for _, stage := range stages.GetResults() {
service, _ = getServiceByName(client, stage.GetServices(), serviceName)
if service != nil {
currentStageId = stage.GetId()
break
}
}

if service == nil {
utils.CheckError(errors.New("service not found"))
}

req := qovery.AttachServiceToDeploymentStageRequest{}
req.SetIsSkipped(false)

_, _, err = client.DeploymentStageMainCallsAPI.
AttachServiceToDeploymentStage(context.Background(), currentStageId, service.GetServiceId()).
AttachServiceToDeploymentStageRequest(req).
Execute()
utils.CheckError(err)

utils.Println("Service \"" + serviceName + "\" is no longer skipped from environment-level deployments")
},
}

func init() {
environmentStageCmd.AddCommand(environmentStageUnskipCmd)
environmentStageUnskipCmd.Flags().StringVarP(&organizationName, "organization", "", "", "Organization Name")
environmentStageUnskipCmd.Flags().StringVarP(&projectName, "project", "", "", "Project Name")
environmentStageUnskipCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name")
environmentStageUnskipCmd.Flags().StringVarP(&serviceName, "name", "n", "", "Service Name")

_ = environmentStageUnskipCmd.MarkFlagRequired("name")
}
Loading