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
58 changes: 29 additions & 29 deletions .terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<p align="center">
<a href="https://discord.com/invite/5UKsqAkPWG" rel="nofollow"><img src="https://img.shields.io/discord/1088753599951151154?label=Discord&logo=discord&logoColor=white" alt="Discord Server"></a>
</p>

<p align="center">
<a href="https://www.youtube.com/watch?v=cr4Q0oLaANk">🎥 Watch a demo</a> | <a href="https://docs.overmind.tech">📖 Docs</a> | <a href="https://app.overmind.tech/api/auth/signup">🚀 Sign up</a> | <a href="https://www.linkedin.com/company/overmindtech/">🙌 Follow us</a>
</p>
Expand Down
9 changes: 9 additions & 0 deletions cmd/changes_submit_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ func SubmitPlan(cmd *cobra.Command, args []string) error {
}
}

labels, err := parseLabelsArgument()
if err != nil {
return loggedError{
err: err,
fields: lf,
message: "Failed to parse labels",
}
}
properties := &sdp.ChangeProperties{
Title: title,
Description: viper.GetString("description"),
Expand All @@ -167,6 +175,7 @@ func SubmitPlan(cmd *cobra.Command, args []string) error {
CodeChanges: codeChangesOutput,
Repo: repoUrl,
EnrichedTags: enrichedTags,
Labels: labels,
}

if changeUUID == uuid.Nil {
Expand Down
41 changes: 41 additions & 0 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"strconv"
"strings"

"github.com/overmindtech/cli/sdp-go"
Expand Down Expand Up @@ -30,6 +31,7 @@ func addChangeCreationFlags(cmd *cobra.Command) {
cmd.PersistentFlags().String("terraform-plan-output", "", "Filename of cached terraform plan output for this change.")
cmd.PersistentFlags().String("code-changes-diff", "", "Filename of the code diff of this change.")
cmd.PersistentFlags().StringSlice("tags", []string{}, "Tags to apply to this change, these should be specified in key=value format. Multiple tags can be specified by repeating the flag or using a comma separated list.")
cmd.PersistentFlags().StringSlice("labels", []string{}, "Labels to apply to this change, these should be specified in name=color format where color is a hex code (e.g., FF0000 or #FF0000). Multiple labels can be specified by repeating the flag or using a comma separated list.")
}

func parseTagsArgument() (*sdp.EnrichedTags, error) {
Expand Down Expand Up @@ -58,6 +60,45 @@ func parseTagsArgument() (*sdp.EnrichedTags, error) {
return enrichedTags, nil
}

func parseLabelsArgument() ([]*sdp.Label, error) {
labels := make([]*sdp.Label, 0)
for _, label := range viper.GetStringSlice("labels") {
parts := strings.SplitN(label, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid label format: %s (expected name=color)", label)
}
if parts[0] == "" {
return nil, fmt.Errorf("invalid label format: %s (label name cannot be empty)", label)
}

// Normalise colour: strip leading # if present, validate, then add # back
colour := strings.TrimPrefix(parts[1], "#")
if colour == "" {
return nil, fmt.Errorf("invalid colour format: %s (colour cannot be empty)", parts[1])
}

// Validate it's exactly 6 hex digits
if len(colour) != 6 {
return nil, fmt.Errorf("invalid colour format: %s (must be 6 hex digits, got %d)", parts[1], len(colour))
}

// Validate all characters are valid hex digits
if _, err := strconv.ParseUint(colour, 16, 64); err != nil {
return nil, fmt.Errorf("invalid colour format: %s (must be valid hex digits)", parts[1])
}

// Normalise to canonical form: always #rrggbb
normalisedColour := "#" + strings.ToUpper(colour)

labels = append(labels, &sdp.Label{
Name: parts[0],
Colour: normalisedColour,
Type: sdp.LabelType_LABEL_TYPE_USER,
})
}
return labels, nil
}

// Adds common flags to API commands e.g. timeout
func addAPIFlags(cmd *cobra.Command) {
cmd.PersistentFlags().String("timeout", "10m", "How long to wait for responses")
Expand Down
182 changes: 182 additions & 0 deletions cmd/flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package cmd

import (
"strings"
"testing"

"github.com/overmindtech/cli/sdp-go"
"github.com/spf13/viper"
)

func TestParseLabelsArgument(t *testing.T) {
tests := []struct {
name string
labels []string
want []*sdp.Label
errorContains string
}{
{
name: "empty labels",
labels: []string{},
want: []*sdp.Label{},
},
{
name: "single label with hash",
labels: []string{"label1=#FF0000"},
want: []*sdp.Label{
{
Name: "label1",
Colour: "#FF0000",
Type: sdp.LabelType_LABEL_TYPE_USER,
},
},
},
{
name: "single label without hash",
labels: []string{"label1=ff0000"},
want: []*sdp.Label{
{
Name: "label1",
Colour: "#FF0000",
Type: sdp.LabelType_LABEL_TYPE_USER,
},
},
},
{
name: "single label with lowercase hex",
labels: []string{"label1=abc123"},
want: []*sdp.Label{
{
Name: "label1",
Colour: "#ABC123",
Type: sdp.LabelType_LABEL_TYPE_USER,
},
},
},
{
name: "multiple labels with hash",
labels: []string{"label1=#FF0000", "label2=#00FF00", "label3=#0000FF"},
want: []*sdp.Label{
{
Name: "label1",
Colour: "#FF0000",
Type: sdp.LabelType_LABEL_TYPE_USER,
},
{
Name: "label2",
Colour: "#00FF00",
Type: sdp.LabelType_LABEL_TYPE_USER,
},
{
Name: "label3",
Colour: "#0000FF",
Type: sdp.LabelType_LABEL_TYPE_USER,
},
},
},
{
name: "multiple labels mixed hash and no hash",
labels: []string{"label1=#FF0000", "label2=00FF00", "label3=#0000FF"},
want: []*sdp.Label{
{
Name: "label1",
Colour: "#FF0000",
Type: sdp.LabelType_LABEL_TYPE_USER,
},
{
Name: "label2",
Colour: "#00FF00",
Type: sdp.LabelType_LABEL_TYPE_USER,
},
{
Name: "label3",
Colour: "#0000FF",
Type: sdp.LabelType_LABEL_TYPE_USER,
},
},
},
{
name: "missing equals sign",
labels: []string{"label1FF0000"},
errorContains: "invalid label format",
},
{
name: "empty label name",
labels: []string{"=#FF0000"},
errorContains: "label name cannot be empty",
},
{
name: "empty colour",
labels: []string{"label1="},
errorContains: "colour cannot be empty",
},
{
name: "colour too short",
labels: []string{"label1=#FF00"},
errorContains: "must be 6 hex digits",
},
{
name: "colour too long",
labels: []string{"label1=#FF00000"},
errorContains: "must be 6 hex digits",
},
{
name: "invalid hex characters",
labels: []string{"label1=#GGGGGG"},
errorContains: "must be valid hex digits",
},
{
name: "colour without hash too short",
labels: []string{"label1=FF00"},
errorContains: "must be 6 hex digits",
},
{
name: "colour without hash invalid characters",
labels: []string{"label1=ZZZZZZ"},
errorContains: "must be valid hex digits",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set up viper with test labels
viper.Reset()
viper.Set("labels", tt.labels)

got, err := parseLabelsArgument()

if tt.errorContains != "" {
if err == nil {
t.Errorf("parseLabelsArgument() expected error containing %q, got nil", tt.errorContains)
return
}
if !strings.Contains(err.Error(), tt.errorContains) {
t.Errorf("parseLabelsArgument() error = %v, want error containing %q", err, tt.errorContains)
}
return
}

if err != nil {
t.Errorf("parseLabelsArgument() unexpected error: %v", err)
return
}

if len(got) != len(tt.want) {
t.Errorf("parseLabelsArgument() returned %d labels, want %d", len(got), len(tt.want))
return
}

for i, wantLabel := range tt.want {
if got[i].GetName() != wantLabel.GetName() {
t.Errorf("parseLabelsArgument() label[%d].Name = %q, want %q", i, got[i].GetName(), wantLabel.GetName())
}
if got[i].GetColour() != wantLabel.GetColour() {
t.Errorf("parseLabelsArgument() label[%d].Colour = %q, want %q", i, got[i].GetColour(), wantLabel.GetColour())
}
if got[i].GetType() != wantLabel.GetType() {
t.Errorf("parseLabelsArgument() label[%d].Type = %v, want %v", i, got[i].GetType(), wantLabel.GetType())
}
}
})
}
}
7 changes: 7 additions & 0 deletions cmd/terraform_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI
return nil
}

labels, err := parseLabelsArgument()
if err != nil {
uploadChangesSpinner.Fail(fmt.Sprintf("Uploading planned changes: failed to parse labels: %v", err))
return nil
}

properties := &sdp.ChangeProperties{
Title: title,
Description: viper.GetString("description"),
Expand All @@ -253,6 +259,7 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI
CodeChanges: codeChangesOutput,
Repo: repoUrl,
EnrichedTags: enrichedTags,
Labels: labels,
}

if changeUuid == uuid.Nil {
Expand Down
11 changes: 8 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,15 @@ require (
sigs.k8s.io/yaml v1.6.0 // indirect
)

require github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0
require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
)

require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
)
Loading