Skip to content

Commit 40f55a2

Browse files
author
vsilent
committed
naming rules
1 parent 105f6f5 commit 40f55a2

File tree

1 file changed

+41
-4
lines changed

1 file changed

+41
-4
lines changed

src/cli/stacker_client.rs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,7 +1259,12 @@ pub fn build_project_body(config: &StackerConfig) -> serde_json::Value {
12591259
///
12601260
/// Format: `{project}-{4hex}` where the hex suffix is derived from the current
12611261
/// timestamp so each deploy gets a distinct name, e.g. `website-a3f1`.
1262-
/// The name is sanitised to contain only lowercase alphanumeric chars and hyphens.
1262+
///
1263+
/// The name is sanitised to satisfy the strictest provider rules (Hetzner):
1264+
/// - only lowercase `a-z`, `0-9`, `-`
1265+
/// - must start with a letter
1266+
/// - must not end with `-`
1267+
/// - max 63 characters total
12631268
fn generate_server_name(project_name: &str) -> String {
12641269
use std::time::{SystemTime, UNIX_EPOCH};
12651270

@@ -1274,7 +1279,14 @@ fn generate_server_name(project_name: &str) -> String {
12741279
.collect::<Vec<_>>()
12751280
.join("-");
12761281

1277-
let base = if sanitised.is_empty() { "server" } else { &sanitised };
1282+
// Ensure it starts with a letter (Hetzner requirement)
1283+
let base = if sanitised.is_empty() {
1284+
"srv".to_string()
1285+
} else if !sanitised.starts_with(|c: char| c.is_ascii_lowercase()) {
1286+
format!("srv-{}", sanitised)
1287+
} else {
1288+
sanitised
1289+
};
12781290

12791291
// 4-char hex suffix from current timestamp (unique per ~65k deploys within any second)
12801292
let ts = SystemTime::now()
@@ -1283,7 +1295,15 @@ fn generate_server_name(project_name: &str) -> String {
12831295
.as_millis();
12841296
let suffix = format!("{:04x}", (ts & 0xFFFF) as u16);
12851297

1286-
format!("{}-{}", base, suffix)
1298+
// Truncate base so total stays within 63 chars: base + '-' + 4-char suffix = base ≤ 58
1299+
let max_base = 63 - 1 - suffix.len(); // 58
1300+
let truncated = if base.len() > max_base {
1301+
base[..max_base].trim_end_matches('-').to_string()
1302+
} else {
1303+
base
1304+
};
1305+
1306+
format!("{}-{}", truncated, suffix)
12871307
}
12881308

12891309
pub fn build_deploy_form(config: &StackerConfig) -> serde_json::Value {
@@ -1574,12 +1594,29 @@ mod tests {
15741594
#[test]
15751595
fn test_generate_server_name_empty() {
15761596
let name = generate_server_name("");
1577-
assert!(name.starts_with("server-"), "empty input should fallback to 'server', got: {}", name);
1597+
assert!(name.starts_with("srv-"), "empty input should fallback to 'srv', got: {}", name);
15781598
}
15791599

15801600
#[test]
15811601
fn test_generate_server_name_special_chars() {
15821602
let name = generate_server_name("app___v2..beta");
15831603
assert!(name.starts_with("app-v2-beta-"), "consecutive separators collapsed, got: {}", name);
15841604
}
1605+
1606+
#[test]
1607+
fn test_generate_server_name_numeric_start() {
1608+
// Hetzner requires name to start with a letter
1609+
let name = generate_server_name("123app");
1610+
assert!(name.starts_with("srv-123app-"), "numeric start should get 'srv-' prefix, got: {}", name);
1611+
}
1612+
1613+
#[test]
1614+
fn test_generate_server_name_max_length() {
1615+
let long = "a".repeat(100);
1616+
let name = generate_server_name(&long);
1617+
assert!(name.len() <= 63, "name must be ≤63 chars (Hetzner), got {} chars: {}", name.len(), name);
1618+
assert!(name.starts_with("aaa"), "got: {}", name);
1619+
// Must not end with hyphen
1620+
assert!(!name.ends_with('-'), "must not end with hyphen, got: {}", name);
1621+
}
15851622
}

0 commit comments

Comments
 (0)