-
Notifications
You must be signed in to change notification settings - Fork 24
Normalize project name by replacing underscore with dash #1724
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f07f28e
ae26e3b
62fc267
defb3b5
ba9b76d
ae7e827
bee70a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -155,7 +155,7 @@ func (q *Query) AddCloudBuildLogQuery(stack, project, etag string, services []st | |
| query := `resource.type="build"` | ||
|
|
||
| // FIXME: Support stack | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is necessary to support stacks? |
||
| servicesRegex := `[a-zA-Z0-9-]{1,63}` | ||
| servicesRegex := `[a-zA-Z0-9-_]{1,63}` | ||
| if len(services) > 0 { | ||
| servicesRegex = fmt.Sprintf("(%v)", strings.Join(services, "|")) // Cloud build labels allows upper case letters | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,7 +12,7 @@ logName="projects/test-project/logs/docker-logs" | |||||||||||||||
| labels."defang-stack" = "beta" | ||||||||||||||||
| ) OR ( | ||||||||||||||||
| resource.type="build" | ||||||||||||||||
| labels.build_tags =~ "beta__[a-zA-Z0-9-]{1,63}_" | ||||||||||||||||
| labels.build_tags =~ "beta__[a-zA-Z0-9-_]{1,63}_" | ||||||||||||||||
| -labels.build_step="MAIN" | ||||||||||||||||
|
Comment on lines
14
to
16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ Verification inconclusiveScript executed: # First, let's find and examine the BuildTag.Parse function mentioned in the learning
find . -type f -name "*.go" | xargs rg -l "BuildTag" | head -20Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Look at the test data file to understand its context
cat -n src/pkg/cli/client/byoc/gcp/testdata/with_cd_exec.queryRepository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check git history to see what the original regex was
cd . && git log -p --follow -- "src/pkg/cli/client/byoc/gcp/testdata/with_cd_exec.query" | head -100Repository: DefangLabs/defang Repository: DefangLabs/defang Script executed: # Find BuildTag definition and Parse method
fd -t f "*.go" | xargs rg -A 30 "func.*BuildTag.*Parse|type.*BuildTag" | head -100Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: stderr: Script executed: # Search for underscore/dash normalization
rg -i "underscore|dash|normalize" src/pkg/clouds/gcp/ -A 2 -B 2Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check where build tags are created
rg -A 5 "BuildTag{" src/pkg/clouds/gcp/cloudbuild.goRepository: DefangLabs/defang Repository: DefangLabs/defang stderr: Script executed: # Look for normalization in the codebase
rg "ReplaceAll.*_.*-|strings.Replace.*_" src/pkg/ -B 3 -A 3Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Find test cases or examples of actual build tag format
rg "beta.*__" . -r "test" --type-list | head -20Repository: DefangLabs/defang Repository: DefangLabs/defang stderr: Script executed: # Check git history differently
git log --oneline -- "src/pkg/cli/client/byoc/gcp/testdata/with_cd_exec.query" 2>/dev/null || echo "No git history"Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Search for examples of build tag matching in test files
rg "beta_" src/pkg/ --type go -B 2 -A 2Repository: DefangLabs/defang Repository: DefangLabs/defang Script executed: # Look for the pattern with double underscore
rg "beta__" . -B 3 -A 3Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check if there are examples of actual build tag values in tests
rg "stack.*project.*service.*etag|BuildTag" src/pkg/clouds/gcp/cloudbuild_test.go -A 5 -B 2Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Look at the query generation code to understand the double underscore
rg -B 10 -A 10 "build_tags.*=~" src/pkg/cli/client/byoc/gcp/ --type goRepository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check test file for query generation tests
rg "with_cd_exec|build_tags" src/pkg/cli/client/byoc/gcp/*test*.go -B 5 -A 5Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Look for where these query files are generated or used
rg "with_cd_exec\|with_logtype" src/pkg/cli/client/byoc/gcp/ --type go -B 3 -A 3Repository: DefangLabs/defang Repository: DefangLabs/defang Script executed: # Look at how the test uses these query files
cat -n src/pkg/cli/client/byoc/gcp/byoc_test.go | head -150Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check if these are golden files being compared
rg "testdata|golden|with_cd_exec" src/pkg/cli/client/byoc/gcp/byoc_test.go -B 5 -A 10 | head -80Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Look at the actual test structure
ast-grep --pattern 'func Test.*($) {
$$$
}' src/pkg/cli/client/byoc/gcp/byoc_test.goRepository: DefangLabs/defang Repository: DefangLabs/defang Script executed: # Look for where AddCloudBuildLogQuery is called with CD execution
rg "AddCloudBuildLogQuery" src/pkg/cli/client/byoc/gcp/ -B 5 -A 5 --type goRepository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check how cdExecution is used in getLogStream
rg "cdExecution|getLogStream" src/pkg/cli/client/byoc/gcp/stream.go -B 3 -A 10Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Look at stream.go to understand query construction
cat -n src/pkg/cli/client/byoc/gcp/stream.goRepository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check what parameters are passed in the test
cat -n src/pkg/cli/client/byoc/gcp/byoc.go | grep -A 30 "getLogStream"Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Look at how PulumiStack is set
rg "PulumiStack" src/pkg/cli/client/byoc/gcp/ -B 3 -A 3Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check what RE2 does with [a-zA-Z0-9-_] char classes
python3 << 'EOF'
import re
# RE2 is Go's regex engine, which uses standard char class rules
# Let's test what [a-zA-Z0-9-_] actually matches
pattern1 = r'[a-zA-Z0-9-_]'
pattern2 = r'[a-zA-Z0-9_-]'
test_strings = [
'a', 'A', '0', '9', '-', '_', '.', ':',
chr(46), # .
chr(47), # /
chr(45), # -
chr(46), # .
chr(95), # _
]
print("Pattern [a-zA-Z0-9-_]:")
for s in test_strings:
if re.match(pattern1, s):
print(f" Matches: {repr(s)} (ord={ord(s)})")
print("\nPattern [a-zA-Z0-9_-]:")
for s in test_strings:
if re.match(pattern2, s):
print(f" Matches: {repr(s)} (ord={ord(s)})")
# Check if - in the middle creates a range
print("\nChecking if '-_' creates a range:")
print(f" '-' is ord {ord('-')}, '_' is ord {ord('_')}")
print(f" Characters between - (45) and _ (95):")
print(f" {[chr(i) for i in range(45, 96)]}")
EOFRepository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Find the test case definition for with_cd_exec
rg -A 10 '"with_cd_exec"' src/pkg/cli/client/byoc/gcp/byoc_test.go | head -30Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check the getLogStream test to see what values are used
rg -B 5 "with_cd_exec.*query" src/pkg/cli/client/byoc/gcp/byoc_test.go -A 2Repository: DefangLabs/defang Repository: DefangLabs/defang Script executed: # Look more carefully at the test structure and what gets passed to AddCloudBuildLogQuery
cat -n src/pkg/cli/client/byoc/gcp/byoc_test.go | grep -A 50 "TestGetLogStream"Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Now let me check how the test is executed - what are the test parameters for b.PulumiStack and project
rg -A 100 "func TestGetLogStream" src/pkg/cli/client/byoc/gcp/byoc_test.go | grep -A 30 "for _, tt := range"Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check what NewByocProvider sets for PulumiStack in the test
rg -B 5 "NewByocProvider" src/pkg/cli/client/byoc/gcp/byoc_test.go | head -30Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Find NewByocProvider implementation and what it sets PulumiStack to
rg -A 20 "func NewByocProvider" src/pkg/cli/client/byoc/gcp/ --type go | head -60Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check if PulumiStack is set to a default value like "beta"
rg "PulumiStack.*=" src/pkg/cli/client/byoc/gcp/byoc.go | head -20Repository: DefangLabs/defang Repository: DefangLabs/defang Script executed: # Find NewByocBaseClient and see what it does with the stack parameter
rg -A 30 "func NewByocBaseClient" src/pkg/cli/client/byoc/ --type goRepository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check if there's a default stack value
rg "PulumiStack.*:=|PulumiStack.*=" src/pkg/cli/client/byoc/ --type go -B 2 -A 2Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Look at ByocBaseClient structure
rg "type ByocBaseClient struct" src/pkg/cli/client/byoc/ -A 20 --type goRepository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check what stacks.DefaultBeta is
rg "DefaultBeta" src/ --type go -B 2 -A 2Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Now let me trace through what happens when AddCloudBuildLogQuery is called with empty project
python3 << 'EOF'
# Simulate what happens in AddCloudBuildLogQuery when project is empty
stack = "beta"
project = "" # Empty when not provided in test
etag = "" # Empty when not provided in test
servicesRegex = "[a-zA-Z0-9-_]{1,63}"
# The format string "%v_%v_%v_%v" with empty strings:
query = f'labels.build_tags =~ "{stack}_{project}_{servicesRegex}_{etag}"'
print("Generated query:")
print(query)
# This explains the double underscore!
EOFRepository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Verify the test doesn't pass a project
rg -A 5 '"with_cd_exec"' src/pkg/cli/client/byoc/gcp/byoc_test.goRepository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Check if there's any documentation or tests about the RE2 char class behavior
rg "RE2|Cloud Logging.*regex" src/ --type go -B 2 -A 2 | head -40Repository: DefangLabs/defang Repository: DefangLabs/defang Script executed: # The key insight: verify the context of when this regex is used
cat -n src/pkg/cli/client/byoc/gcp/query.go | sed -n '40,60p'Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Script executed: # Verify the actual character class in the code generation
cat -n src/pkg/cli/client/byoc/gcp/query.go | sed -n '70,85p'Repository: DefangLabs/defang Repository: DefangLabs/defang stdout: Fix character class ordering to avoid unintended range matching. The hyphen in The double underscore ( Proposed fix- labels.build_tags =~ "beta__[a-zA-Z0-9-_]{1,63}_"
+ labels.build_tags =~ "beta__[a-zA-Z0-9_-]{1,63}_"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| ) OR ( | ||||||||||||||||
| resource.type="cloud_run_revision" | ||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,7 @@ logName="projects/test-project/logs/docker-logs" | |
| labels."defang-stack" = "beta" | ||
| ) OR ( | ||
| resource.type="build" | ||
| labels.build_tags =~ "beta__[a-zA-Z0-9-]{1,63}_" | ||
| labels.build_tags =~ "beta__[a-zA-Z0-9-_]{1,63}_" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Find the test file and check its content
find . -name "with_logtype_build.query" -type f | head -5Repository: DefangLabs/defang Length of output: 124 🏁 Script executed: # Search for query.go in the codebase
find . -name "query.go" -path "*/byoc/gcp/*" -type fRepository: DefangLabs/defang Length of output: 99 🏁 Script executed: # Look for the pattern in production code that generates these queries
rg -l "build_tags" --type go --type txt | head -10Repository: DefangLabs/defang Length of output: 135 🏁 Script executed: # Read the test file to see the context and line 12
cat -n ./src/pkg/cli/client/byoc/gcp/testdata/with_logtype_build.query | head -20Repository: DefangLabs/defang Length of output: 571 🏁 Script executed: # Read query.go to find the regex pattern construction
cat -n ./src/pkg/cli/client/byoc/gcp/query.go | head -150Repository: DefangLabs/defang Length of output: 4370 🏁 Script executed: # Continue reading query.go to find the AddBuildLogQuery or similar method
sed -n '150,250p' ./src/pkg/cli/client/byoc/gcp/query.go | cat -nRepository: DefangLabs/defang Length of output: 4133 🏁 Script executed: # Search for where the build_tags pattern is defined
rg -n "build_tags" ./src/pkg/cli/client/byoc/gcp/query.go -A 3 -B 3Repository: DefangLabs/defang Length of output: 427 🏁 Script executed: # Verify the regex issue by checking what characters are actually matched
# Create a test to show the unintended range
python3 << 'EOF'
import re
# The problematic pattern
pattern = r'[a-zA-Z0-9-_]'
regex = re.compile(pattern)
# Test various characters
test_chars = list(range(48, 96)) + [95] # ASCII 48-95
matched = []
for code in test_chars:
char = chr(code)
if regex.match(char):
matched.append((code, char, repr(char)))
print("Characters matched by [a-zA-Z0-9-_]:")
for code, char, repr_char in matched:
if char in '0123456789':
print(f" {code}: {repr_char} (digit - INTENDED)")
elif char in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ':
print(f" {code}: {repr_char} (letter - INTENDED)")
elif char in '-_':
print(f" {code}: {repr_char} (dash/underscore - INTENDED)")
else:
print(f" {code}: {repr_char} (UNINTENDED - in range 57-95)")
print("\n\nCharacters matched by corrected [a-zA-Z0-9_-]:")
pattern_fixed = r'[a-zA-Z0-9_-]'
regex_fixed = re.compile(pattern_fixed)
for code in test_chars:
char = chr(code)
if regex_fixed.match(char):
matched_fixed = (code, char, repr(char))
if char not in [c[1] for c in matched]:
print(f" {code}: {repr(char)} (NOW EXCLUDED - UNINTENDED)")
EOFRepository: DefangLabs/defang Length of output: 147 Critical: Fix hyphen placement in character class to prevent unintended character matches. The character class Move the hyphen to the end: This pattern appears in both the test data (line 12) and the production code ( 🤖 Prompt for AI Agents |
||
| -labels.build_step="MAIN" | ||
| ) | ||
| ) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||||||
| "regexp" | ||||||||||||||||||||||||||||||||
| "slices" | ||||||||||||||||||||||||||||||||
| "strconv" | ||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||
|
|
@@ -29,6 +30,12 @@ func FixupServices(ctx context.Context, provider client.Provider, project *compo | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| oldName := project.Name | ||||||||||||||||||||||||||||||||
| project.Name = NormalizeProjectName(project.Name) | ||||||||||||||||||||||||||||||||
| if project.Name != oldName { | ||||||||||||||||||||||||||||||||
| term.Debugf("normalized project name %q -> %q", oldName, project.Name) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Preload the current config so we can detect which environment variables should be passed as "secrets" | ||||||||||||||||||||||||||||||||
| config, err := provider.ListConfig(ctx, &defangv1.ListConfigsRequest{Project: project.Name}) | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
|
|
@@ -477,3 +484,9 @@ func IsRedisRepo(repo string) bool { | |||||||||||||||||||||||||||||||
| func IsMongoRepo(repo string) bool { | ||||||||||||||||||||||||||||||||
| return strings.HasSuffix(repo, "mongo") | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| var safeProjectNameRE = regexp.MustCompile(`[^A-Za-z0-9-]+`) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func NormalizeProjectName(name string) string { | ||||||||||||||||||||||||||||||||
| return safeProjectNameRE.ReplaceAllString(name, "-") | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+488
to
+492
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle edge cases in NormalizeProjectName. The current implementation can produce invalid project names in edge cases:
These could cause downstream failures in GCP APIs. Consider adding safeguards similar to
🔎 Proposed enhancement with edge case handling+var safeProjectNameRE = regexp.MustCompile(`[^A-Za-z0-9-]+`)
+
func NormalizeProjectName(name string) string {
- return safeProjectNameRE.ReplaceAllString(name, "-")
+ normalized := safeProjectNameRE.ReplaceAllString(name, "-")
+ normalized = strings.Trim(normalized, "-")
+ if normalized == "" {
+ return "project"
+ }
+ return normalized
}Import 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a good suggestion. Let's write some test cases for this function and cover the suggested cases. I vaguely remember that repeated hyphens may also not be permitted. Is that true?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query: 💡 Result: Short answer — two different fields:
Notes:
References:
Citations:
🏁 Script executed: Length of output: 4114 @jordanstephens, great question! Based on GCP Project ID constraints (6-30 characters, lowercase letters/numbers/hyphens only, must start with a letter, cannot end with a hyphen), repeated hyphens aren't explicitly forbidden, but they're poor practice and should be collapsed. More importantly, the current implementation has gaps:
Here's a comprehensive test file covering all edge cases: package compose
import (
"testing"
)
func TestNormalizeProjectName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple alphanumeric",
input: "myproject",
expected: "myproject",
},
{
name: "with valid hyphens",
input: "my-project-123",
expected: "my-project-123",
},
{
name: "underscore to hyphen",
input: "my_project",
expected: "my-project",
},
{
name: "multiple underscores",
input: "my__project",
expected: "my-project",
},
{
name: "leading underscore",
input: "_test",
expected: "test",
},
{
name: "trailing underscore",
input: "test_",
expected: "test",
},
{
name: "leading and trailing underscores",
input: "_test_",
expected: "test",
},
{
name: "only underscores",
input: "___",
expected: "project",
},
{
name: "empty string",
input: "",
expected: "project",
},
{
name: "special characters",
input: "my@project#test",
expected: "my-project-test",
},
{
name: "mixed special characters and underscores",
input: "my_project@123",
expected: "my-project-123",
},
{
name: "spaces",
input: "my project",
expected: "my-project",
},
{
name: "dots",
input: "my.project.test",
expected: "my-project-test",
},
{
name: "consecutive special characters",
input: "my___project!!!",
expected: "my-project",
},
{
name: "starts with number",
input: "123project",
expected: "123project",
},
{
name: "starts with hyphen after normalization",
input: "-test",
expected: "test",
},
{
name: "ends with hyphen after normalization",
input: "test-",
expected: "test",
},
{
name: "only special characters",
input: "!!!@@@",
expected: "project",
},
{
name: "unicode characters",
input: "my项目test",
expected: "my-test",
},
{
name: "real world example from issue",
input: "raphaeltm-prod1_zaoconnect_project",
expected: "raphaeltm-prod1-zaoconnect-project",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NormalizeProjectName(tt.input)
if result != tt.expected {
t.Errorf("NormalizeProjectName(%q) = %q, want %q", tt.input, result, tt.expected)
}
// Additional validation: result should not have leading/trailing hyphens
if len(result) > 0 {
if result[0] == '-' {
t.Errorf("NormalizeProjectName(%q) = %q starts with hyphen", tt.input, result)
}
if result[len(result)-1] == '-' {
t.Errorf("NormalizeProjectName(%q) = %q ends with hyphen", tt.input, result)
}
}
// Result should not be empty
if result == "" {
t.Errorf("NormalizeProjectName(%q) returned empty string", tt.input)
}
})
}
}
func TestNormalizeProjectNameRepeatedHyphens(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "double hyphen",
input: "my--project",
expected: "my-project",
},
{
name: "triple hyphen",
input: "my---project",
expected: "my-project",
},
{
name: "multiple double hyphens",
input: "my--project--test",
expected: "my-project-test",
},
{
name: "from consecutive special chars",
input: "my___project",
expected: "my-project",
},
{
name: "mixed underscores and special chars",
input: "my_@_project",
expected: "my-project",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NormalizeProjectName(tt.input)
if result != tt.expected {
t.Errorf("NormalizeProjectName(%q) = %q, want %q", tt.input, result, tt.expected)
}
// Verify no consecutive hyphens remain
if len(result) > 1 {
for i := 0; i < len(result)-1; i++ {
if result[i] == '-' && result[i+1] == '-' {
t.Errorf("NormalizeProjectName(%q) = %q contains consecutive hyphens", tt.input, result)
}
}
}
})
}
}And here's the enhanced implementation to handle all these cases: var safeProjectNameRE = regexp.MustCompile(`[^A-Za-z0-9-]+`)
var repeatedHyphensRE = regexp.MustCompile(`-+`)
func NormalizeProjectName(name string) string {
// Replace non-allowed characters with hyphens
normalized := safeProjectNameRE.ReplaceAllString(name, "-")
// Collapse consecutive hyphens into single hyphen
normalized = repeatedHyphensRE.ReplaceAllString(normalized, "-")
// Trim leading and trailing hyphens
normalized = strings.Trim(normalized, "-")
// Return default if empty
if normalized == "" {
return "project"
}
return normalized
}The key improvements:
Note: I didn't enforce "must start with letter" since that might be too restrictive for project names (vs. GCP Project IDs), but you can add that if needed. 🧬 Code Graph Analysis Results[src/pkg/cli/compose/fixup.go] — Summary of relevant code for user comment
🧠 Learnings used |
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |
| "errors" | ||
|
|
||
| "github.com/DefangLabs/defang/src/pkg/cli/client" | ||
| "github.com/DefangLabs/defang/src/pkg/cli/compose" | ||
| "github.com/DefangLabs/defang/src/pkg/term" | ||
| "github.com/DefangLabs/defang/src/pkg/types" | ||
| defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" | ||
|
|
@@ -29,7 +30,7 @@ func WaitServiceState( | |
| } | ||
|
|
||
| // Assume "services" are normalized service names | ||
| subscribeRequest := defangv1.SubscribeRequest{Project: projectName, Etag: etag, Services: services} | ||
| subscribeRequest := defangv1.SubscribeRequest{Project: compose.NormalizeProjectName(projectName), Etag: etag, Services: services} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we push this responsibility into the |
||
| serverStream, err := provider.Subscribe(ctx, &subscribeRequest) | ||
| if err != nil { | ||
| return nil, err | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ import ( | |
|
|
||
| "github.com/DefangLabs/defang/src/pkg" | ||
| "github.com/DefangLabs/defang/src/pkg/cli/client" | ||
| "github.com/DefangLabs/defang/src/pkg/cli/compose" | ||
| "github.com/DefangLabs/defang/src/pkg/dryrun" | ||
| "github.com/DefangLabs/defang/src/pkg/logs" | ||
| "github.com/DefangLabs/defang/src/pkg/spinner" | ||
|
|
@@ -207,7 +208,7 @@ func streamLogs(ctx context.Context, provider client.Provider, projectName strin | |
| Etag: options.Deployment, | ||
| LogType: uint32(options.LogType), | ||
| Pattern: options.Filter, | ||
| Project: projectName, | ||
| Project: compose.NormalizeProjectName(projectName), // Matching the FixupServices behavior | ||
|
Comment on lines
-210
to
+211
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, I think |
||
| Services: options.Services, | ||
| Since: sinceTs, // this is also used to continue from the last timestamp | ||
| Until: untilTs, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,47 @@ | ||||||||||||||
| package gcp | ||||||||||||||
|
|
||||||||||||||
| import ( | ||||||||||||||
| "testing" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| func TestBuildTagString(t *testing.T) { | ||||||||||||||
| tests := []struct { | ||||||||||||||
| name string | ||||||||||||||
| bt BuildTag | ||||||||||||||
| want string | ||||||||||||||
| }{ | ||||||||||||||
| { | ||||||||||||||
| name: "with stack", | ||||||||||||||
| bt: BuildTag{Stack: "stack1", Project: "proj", Service: "svc", Etag: "123"}, | ||||||||||||||
| want: "stack1_proj_svc_123", | ||||||||||||||
| }, | ||||||||||||||
| { | ||||||||||||||
| name: "without stack", | ||||||||||||||
| bt: BuildTag{Project: "proj", Service: "svc", Etag: "123"}, | ||||||||||||||
| want: "proj_svc_123", | ||||||||||||||
| }, | ||||||||||||||
| { | ||||||||||||||
| name: "service name with underscores", | ||||||||||||||
| bt: BuildTag{Stack: "stack1", Project: "my-proj-name", Service: "svc_name", Etag: "123"}, | ||||||||||||||
| want: "stack1_my-proj-name_svc_name_123", | ||||||||||||||
| }, | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| for _, tt := range tests { | ||||||||||||||
| t.Run(tt.name, func(t *testing.T) { | ||||||||||||||
| tagStr := tt.bt.String() | ||||||||||||||
| if tagStr != tt.want { | ||||||||||||||
| t.Errorf("String() = %q, want %q", tagStr, tt.want) | ||||||||||||||
| } | ||||||||||||||
| var parsed BuildTag | ||||||||||||||
| err := parsed.Parse([]string{tagStr}) | ||||||||||||||
| if err != nil { | ||||||||||||||
| t.Fatalf("Parse() returned error: %v", err) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if parsed != tt.bt { | ||||||||||||||
| t.Errorf("Parse() = %+v, want %+v", parsed, tt.want) | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+42
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Wrong variable in error message. The error message references 🔎 Proposed fix if parsed != tt.bt {
- t.Errorf("Parse() = %+v, want %+v", parsed, tt.want)
+ t.Errorf("Parse() = %+v, want %+v", parsed, tt.bt)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| }) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,12 @@ package gcp | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||
| "crypto/sha256" | ||||||||||||||||||||||||||
| "encoding/binary" | ||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||
| "regexp" | ||||||||||||||||||||||||||
| "strconv" | ||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| "google.golang.org/api/dns/v1" | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
@@ -87,3 +92,47 @@ func (gcp Gcp) DeleteDNSZone(ctx context.Context, name string) error { | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 1. Name must be lowercase letters, numbers, and hyphens | ||||||||||||||||||||||||||
| // 2. Name may be at most 63 characters | ||||||||||||||||||||||||||
| // 3. Name must start with a lowercase letter | ||||||||||||||||||||||||||
| // 4. Name must end with a lowercase letter or a number | ||||||||||||||||||||||||||
| var safeZoneRE = regexp.MustCompile(`[^a-z0-9-]+`) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Zone names have the same requirements as label values. | ||||||||||||||||||||||||||
| func SafeZoneName(input string) string { | ||||||||||||||||||||||||||
| input = strings.ToLower(input) // Rule 1: lowercase | ||||||||||||||||||||||||||
| safe := safeZoneRE.ReplaceAllString(input, "-") // Rule 1: only letters, numbers, and hyphen | ||||||||||||||||||||||||||
| safe = strings.Trim(safe, "-") // Rule 3, 4: trim hyphens from start and end | ||||||||||||||||||||||||||
| if len(safe) == 0 || safe[0] == '-' { | ||||||||||||||||||||||||||
| safe = "zone" + safe | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+107
to
+109
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||||||||||||||||||||||||||
| if safe[0] < 'a' || safe[0] > 'z' { // Rule 3: must start with a letter | ||||||||||||||||||||||||||
| safe = "zone-" + safe | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return hashTrim(safe, 63) // Rule 2: max length 63 | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| func hashTrim(name string, maxLength int) string { | ||||||||||||||||||||||||||
| if len(name) <= maxLength { | ||||||||||||||||||||||||||
| return name | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const hashLength = 6 | ||||||||||||||||||||||||||
| prefix := name[:maxLength-hashLength] | ||||||||||||||||||||||||||
| suffix := name[maxLength-hashLength:] | ||||||||||||||||||||||||||
| return prefix + hashn(suffix, hashLength) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| func hashn(str string, length int) string { | ||||||||||||||||||||||||||
| hash := sha256.New() | ||||||||||||||||||||||||||
| hash.Write([]byte(str)) | ||||||||||||||||||||||||||
| hashInt := binary.LittleEndian.Uint64(hash.Sum(nil)[:8]) | ||||||||||||||||||||||||||
| hashBase36 := strconv.FormatUint(hashInt, 36) // base 36 string | ||||||||||||||||||||||||||
| // truncate if the hash is too long | ||||||||||||||||||||||||||
| if len(hashBase36) > length { | ||||||||||||||||||||||||||
| return hashBase36[:length] | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| // if the hash is too short, pad with leading zeros | ||||||||||||||||||||||||||
| return fmt.Sprintf("%0*s", length, hashBase36) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+133
to
+138
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: In Go's Proposed fix func hashn(str string, length int) string {
hash := sha256.New()
hash.Write([]byte(str))
hashInt := binary.LittleEndian.Uint64(hash.Sum(nil)[:8])
hashBase36 := strconv.FormatUint(hashInt, 36) // base 36 string
// truncate if the hash is too long
if len(hashBase36) > length {
return hashBase36[:length]
}
// if the hash is too short, pad with leading zeros
- return fmt.Sprintf("%0*s", length, hashBase36)
+ return strings.Repeat("0", length-len(hashBase36)) + hashBase36
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see a test case that covers this. Let's add one
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Length of output: 2120 @jordanstephens, great catch! Here's a test case that covers the zero-padding scenario when the hash is shorter than 6 characters: {
name: "hash trimming produces valid output without spaces",
input: strings.Repeat("a", 70), // Long enough to trigger hashTrim
validate: func(t *testing.T, output string) {
if len(output) != 63 {
t.Fatalf("expected length 63, got %d", len(output))
}
// Verify no spaces in output (would indicate improper padding)
if strings.Contains(output, " ") {
t.Fatalf("output contains spaces from improper padding: %q", output)
}
// Verify only valid characters (a-z, 0-9, -)
matched, _ := regexp.MatchString(`^[a-z0-9-]+$`, output)
if !matched {
t.Fatalf("output contains invalid characters: %q", output)
}
},
},This test should be added to the
With the current buggy code, if the hash happens to be short, this test would fail due to space padding. After applying the fix suggested by CodeRabbit, the test should pass consistently. 🧬 Code Graph Analysis Results[src/pkg/clouds/gcp/project.go] (lines 98-101) [src/pkg/clouds/gcp/project.go] (lines 15-15) [src/pkg/clouds/gcp/artifactregistry.go] (lines 51-61) |
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this TODO done now?