Skip to content

Commit 039437d

Browse files
committed
fix: support escaped delimiters in --package argument for Maven packages
Maven package IDs contain colons (e.g. com.yourcompany:project-name) which conflict with the delimiter used to separate package ID from version. Add backslash escape support (\: \/ \=) so users can specify Maven packages: --package "com.yourcompany\:project-name:1.0-SNAPSHOT" Also escape delimiter chars when reconstructing override strings for the server API.
1 parent f2871d1 commit 039437d

2 files changed

Lines changed: 74 additions & 10 deletions

File tree

pkg/cmd/release/create/create_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2422,6 +2422,11 @@ func TestReleaseCreate_ToPackageOverrideString(t *testing.T) {
24222422
{name: "action-ref-ver", input: &packages.PackageVersionOverride{PackageReferenceName: "pterm-on-install", ActionName: "Install", Version: "6.1.2"}, expect: "Install:pterm-on-install:6.1.2"},
24232423
{name: "star-ref-ver", input: &packages.PackageVersionOverride{PackageReferenceName: "pterm-on-install", Version: "6.1.2"}, expect: "*:pterm-on-install:6.1.2"},
24242424
{name: "pkg-action-ref-ver", input: &packages.PackageVersionOverride{PackageReferenceName: "pterm", PackageID: "pterm", ActionName: "Install", Version: "1.2.3"}, expect: "pterm:pterm:1.2.3"},
2425+
// Maven package IDs with colons get escaped (FD-135)
2426+
{name: "maven-pkg-ver", input: &packages.PackageVersionOverride{PackageID: "com.yourcompany:project-name", Version: "1.0"}, expect: `com.yourcompany\:project-name:1.0`},
2427+
{name: "maven-pkg-ref-ver", input: &packages.PackageVersionOverride{PackageID: "com.juliusbaer.fi-master:deployment", PackageReferenceName: "ref", Version: "25.2026.04.1"}, expect: `com.juliusbaer.fi-master\:deployment:ref:25.2026.04.1`},
2428+
// Step name with slash gets escaped
2429+
{name: "step-slash-ver", input: &packages.PackageVersionOverride{ActionName: "Deploy Templates/templates", Version: "1.0"}, expect: `Deploy Templates\/templates:1.0`},
24252430
}
24262431

24272432
for _, test := range tests {
@@ -2455,6 +2460,16 @@ func TestReleaseCreate_ParsePackageOverrideString(t *testing.T) {
24552460
{input: "pterm/Push Package=9.7-pre-xyz", expect: &packages.AmbiguousPackageVersionOverride{PackageReferenceName: "Push Package", ActionNameOrPackageID: "pterm", Version: "9.7-pre-xyz"}},
24562461
{input: "pterm=Push Package/9.7-pre-xyz", expect: &packages.AmbiguousPackageVersionOverride{PackageReferenceName: "Push Package", ActionNameOrPackageID: "pterm", Version: "9.7-pre-xyz"}},
24572462

2463+
// Maven packages with escaped colons (FD-135)
2464+
{input: `com.yourcompany\:project-name:1.0-SNAPSHOT`, expect: &packages.AmbiguousPackageVersionOverride{ActionNameOrPackageID: "com.yourcompany:project-name", Version: "1.0-SNAPSHOT"}},
2465+
{input: `com.juliusbaer.fi-master\:deployment:25.2026.04.1`, expect: &packages.AmbiguousPackageVersionOverride{ActionNameOrPackageID: "com.juliusbaer.fi-master:deployment", Version: "25.2026.04.1"}},
2466+
// Maven package with escaped colon and package reference name
2467+
{input: `com.yourcompany\:project-name:ref:1.0`, expect: &packages.AmbiguousPackageVersionOverride{ActionNameOrPackageID: "com.yourcompany:project-name", PackageReferenceName: "ref", Version: "1.0"}},
2468+
// Step name with escaped slash (additional packages)
2469+
{input: `Deploy Templates\/templates:1.0`, expect: &packages.AmbiguousPackageVersionOverride{ActionNameOrPackageID: "Deploy Templates/templates", Version: "1.0"}},
2470+
// Escaped backslash
2471+
{input: `foo\\bar:1.0`, expect: &packages.AmbiguousPackageVersionOverride{ActionNameOrPackageID: `foo\bar`, Version: "1.0"}},
2472+
24582473
{input: "", expectErr: errors.New("empty package version specification")},
24592474

24602475
// bare identifiers aren't valid

pkg/packages/packages.go

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"github.com/MakeNowJust/heredoc/v2"
1313
"github.com/OctopusDeploy/cli/pkg/output"
1414
"github.com/OctopusDeploy/cli/pkg/question"
15-
"github.com/OctopusDeploy/cli/pkg/util"
1615
octopusApiClient "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
1716
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds"
1817
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/releases"
@@ -193,19 +192,19 @@ type PackageVersionOverride struct {
193192
func (p *PackageVersionOverride) ToPackageOverrideString() string {
194193
components := make([]string, 0, 3)
195194

196-
// stepNameOrPackageID always comes first if we have it
195+
// stepNameOrPackageID always comes first if we have it; escape delimiter chars so the server can parse correctly
197196
if p.PackageID != "" {
198-
components = append(components, p.PackageID)
197+
components = append(components, escapePackageDelimiters(p.PackageID))
199198
} else if p.ActionName != "" { // can't have both PackageID and ActionName; PackageID wins
200-
components = append(components, p.ActionName)
199+
components = append(components, escapePackageDelimiters(p.ActionName))
201200
}
202201

203202
// followed by package reference name if we have it
204203
if p.PackageReferenceName != "" {
205204
if len(components) == 0 { // if we have an explicit packagereference but no packageId or action, we need to express it with *:ref:version
206205
components = append(components, "*")
207206
}
208-
components = append(components, p.PackageReferenceName)
207+
components = append(components, escapePackageDelimiters(p.PackageReferenceName))
209208
}
210209

211210
if len(components) == 0 { // the server can't deal with just a number by itself; if we want to override everything we must pass *:Version
@@ -216,12 +215,62 @@ func (p *PackageVersionOverride) ToPackageOverrideString() string {
216215
return strings.Join(components, ":")
217216
}
218217

219-
// splitPackageOverrideString splits the input string into components based on delimiter characters.
220-
// we want to pick up empty entries here; so "::5" and ":pterm:5" should both return THREE components, rather than one or two
221-
// and we want to allow for multiple different delimeters.
222-
// neither the builtin golang strings.Split or strings.FieldsFunc support this. Logic borrowed from strings.FieldsFunc with heavy modifications
218+
// splitPackageOverrideString splits the input string into components based on delimiter characters (:, /, =).
219+
// Supports escaping delimiters with a backslash (e.g. \: for a literal colon in Maven package IDs).
220+
// Empty entries are preserved; "::5" and ":pterm:5" both return THREE components.
223221
func splitPackageOverrideString(s string) []string {
224-
return util.SplitString(s, []int32{':', '/', '='})
222+
type span struct {
223+
start int
224+
end int
225+
}
226+
spans := make([]span, 0, 3)
227+
start := 0
228+
escaped := false
229+
230+
for idx, ch := range s {
231+
if ch == '\\' && !escaped {
232+
escaped = true
233+
continue
234+
}
235+
if (ch == ':' || ch == '/' || ch == '=') && !escaped {
236+
spans = append(spans, span{start, idx})
237+
start = idx + 1
238+
} else {
239+
escaped = false
240+
}
241+
}
242+
spans = append(spans, span{start, len(s)})
243+
244+
a := make([]string, len(spans))
245+
for i, span := range spans {
246+
a[i] = unescapePackageString(s[span.start:span.end])
247+
}
248+
return a
249+
}
250+
251+
func unescapePackageString(s string) string {
252+
result := make([]rune, 0, len(s))
253+
escaped := false
254+
for _, ch := range s {
255+
if ch == '\\' && !escaped {
256+
escaped = true
257+
continue
258+
}
259+
result = append(result, ch)
260+
escaped = false
261+
}
262+
return string(result)
263+
}
264+
265+
func escapePackageDelimiters(s string) string {
266+
var result strings.Builder
267+
for _, ch := range s {
268+
if ch == ':' || ch == '/' || ch == '=' || ch == '\\' {
269+
result.WriteRune('\\')
270+
}
271+
result.WriteRune(ch)
272+
}
273+
return result.String()
225274
}
226275

227276
// AmbiguousPackageVersionOverride tells us that we want to set the version of some package to `Version`

0 commit comments

Comments
 (0)