feat: dnsname argument type
Build & Release pipeline / build (push) Canceled after 0s
Buf CI / buf (push) Canceled after 0s
Codestyle checks / codestyle (push) Canceled after 0s
Antora docs / antora (push) Canceled after 0s
Build & Release pipeline / Sign Windows artifacts (SignPath) (push) Canceled after 0s
Antora docs / trigger-docs-publish (push) Canceled after 0s

This commit is contained in:
jamesread
2026-07-25 22:00:02 +01:00
parent ecc5c64046
commit 6eb3827ddd
4 changed files with 72 additions and 1 deletions
+1
View File
@@ -10,6 +10,7 @@ A full list of argument types are below;
| (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults.
| ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation
| ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`.
| dnsname | xref:args/input.adoc[Textbox] | A DNS hostname (RFC 1123). Short names (e.g. `webserver`) and FQDNs (e.g. `webserver.example.com`). Letters/digits/hyphens only, no underscores. Optional trailing dot allowed.
| shell_safe_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but also allows `@` and `+`. Useful for shell-safe usernames and email-style identifiers.
| ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`.
| unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc.
@@ -291,7 +291,7 @@ function getInputType (arg) {
return 'checkbox'
}
if (arg.type === 'ascii_identifier' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
if (arg.type === 'ascii_identifier' || arg.type === 'dnsname' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
return 'text'
}
+32
View File
@@ -24,6 +24,9 @@ var (
"shell_safe_identifier": `^[a-zA-Z0-9@\.\_\+\-]+$`,
"ascii_sentence": `^[a-zA-Z0-9\-\._, ]+$`,
}
dnsNameLabelPattern = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
dnsNameAllNumericPattern = regexp.MustCompile(`^[0-9]+$`)
)
// parseExecArray parses all exec arguments in the action.
@@ -223,6 +226,8 @@ func TypeSafetyCheck(name string, value string, argumentType string) error {
return typeSafetyCheckUrl(value)
case "datetime":
return typeSafetyCheckDatetime(value)
case "dnsname":
return typeSafetyCheckDnsName(value)
}
return typeSafetyCheckRegex(name, value, argumentType)
@@ -304,6 +309,33 @@ func typeSafetyCheckEmail(value string) error {
return nil
}
// typeSafetyCheckDnsName validates a DNS hostname (RFC 1123 LDH labels).
// Accepts short names (e.g. webserver) and FQDNs (e.g. webserver.example.com).
// An optional trailing dot is allowed.
func typeSafetyCheckDnsName(value string) error {
hostname := strings.TrimSuffix(value, ".")
if hostname == "" || len(hostname) > 253 {
return fmt.Errorf("invalid dnsname length")
}
return typeSafetyCheckDnsNameLabels(strings.Split(hostname, "."))
}
func typeSafetyCheckDnsNameLabels(labels []string) error {
for _, label := range labels {
if !dnsNameLabelPattern.MatchString(label) {
return fmt.Errorf("invalid dnsname label %q", label)
}
}
tld := labels[len(labels)-1]
if dnsNameAllNumericPattern.MatchString(tld) {
return fmt.Errorf("dnsname top-level label must not be all-numeric")
}
return nil
}
func typeSafetyCheckDatetime(value string) error {
_, err := time.Parse("2006-01-02T15:04:05", value)
@@ -812,6 +812,44 @@ func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
}
}
func TestTypeSafetyCheckDnsName(t *testing.T) {
tests := []struct {
name string
value string
hasError bool
}{
{"Short name", "webserver", false},
{"Localhost", "localhost", false},
{"Simple domain", "example.com", false},
{"Host with subdomain", "webserver.example.com", false},
{"Deep subdomain", "a.b.c.example.co.uk", false},
{"Label starting with digit", "1host.example.com", false},
{"Trailing dot", "example.com.", false},
{"Punycode IDN", "xn--bcher-kva.example", false},
{"Underscore", "my_host.example.com", true},
{"Space", "example .com", true},
{"Leading hyphen label", "-host.example.com", true},
{"Trailing hyphen label", "host-.example.com", true},
{"Empty label", "example..com", true},
{"IP address", "192.168.1.1", true},
{"All numeric TLD", "example.123", true},
{"All numeric short name", "12345", true},
{"Special chars", "exam!ple.com", true},
{"Unicode label", "bücher.example.com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := TypeSafetyCheck("host", tt.value, "dnsname")
if tt.hasError {
assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
} else {
assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
}
})
}
}
func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) {
tests := []struct {
name string