diff --git a/lib/towerops/agent/validator.ex b/lib/towerops/agent/validator.ex index 2c7fba91..395888fc 100644 --- a/lib/towerops/agent/validator.ex +++ b/lib/towerops/agent/validator.ex @@ -191,28 +191,29 @@ defmodule Towerops.Agent.Validator do {:error, {:decode_error, "Invalid protobuf message format"}} end - # Validate version format - accepts either semantic version or RFC 3339 timestamp - # Empty string allowed for backward compat + # Validate version format - accepts semver, RFC 3339 timestamp, git describe, + # git short SHA, or "dev". Empty string allowed for backward compat. defp validate_version(""), do: :ok defp validate_version(version) when is_binary(version) do cond do - # Semantic version format (backward compatibility with old agents) - # Pattern: X.Y.Z or X.Y.Z-suffix - String.match?(version, ~r/^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/) -> - if String.length(version) <= @max_short_string_length, - do: :ok, - else: {:error, {:invalid_version, "Version too long"}} + String.length(version) > @max_short_string_length -> + {:error, {:invalid_version, "Version too long"}} - # RFC 3339 timestamp format (new agents with compile timestamp) - # Pattern: YYYY-MM-DDTHH:MM:SSZ + # Semantic version with git-describe suffixes (e.g., 1.2.3, 1.2.3-rc.1, 1.2.3-5-gabcdef0) + String.match?(version, ~r/^\d+\.\d+\.\d+(-[a-zA-Z0-9.\-]+)?$/) -> + :ok + + # RFC 3339 timestamp format (e.g., 2026-02-11T17:00:00Z) String.match?(version, ~r/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/) -> - if String.length(version) <= @max_short_string_length, - do: :ok, - else: {:error, {:invalid_version, "Version too long"}} + :ok + + # Git short SHA (7-40 hex chars) or short identifier like "dev" + String.match?(version, ~r/^[a-zA-Z0-9]{1,40}$/) -> + :ok true -> - {:error, {:invalid_version, "Version must be semver format (e.g., 1.2.3) or RFC 3339 timestamp"}} + {:error, {:invalid_version, "Version must be semver, RFC 3339 timestamp, or short identifier"}} end end diff --git a/test/towerops/agent/validator_test.exs b/test/towerops/agent/validator_test.exs index e45f9a89..47069a20 100644 --- a/test/towerops/agent/validator_test.exs +++ b/test/towerops/agent/validator_test.exs @@ -535,6 +535,43 @@ defmodule Towerops.Agent.ValidatorTest do assert {:ok, _} = Validator.validate_heartbeat(binary) end + test "accepts git-describe version with commit count and SHA" do + heartbeat = %AgentHeartbeat{ + version: "0.1.0-5-gabcdef0", + hostname: "agent01", + uptime_seconds: 100, + ip_address: "" + } + + binary = AgentHeartbeat.encode(heartbeat) + assert {:ok, validated} = Validator.validate_heartbeat(binary) + assert validated.version == "0.1.0-5-gabcdef0" + end + + test "accepts bare git short SHA as version" do + heartbeat = %AgentHeartbeat{ + version: "abcdef0", + hostname: "agent01", + uptime_seconds: 100, + ip_address: "" + } + + binary = AgentHeartbeat.encode(heartbeat) + assert {:ok, _} = Validator.validate_heartbeat(binary) + end + + test "accepts 'dev' as version" do + heartbeat = %AgentHeartbeat{ + version: "dev", + hostname: "agent01", + uptime_seconds: 100, + ip_address: "" + } + + binary = AgentHeartbeat.encode(heartbeat) + assert {:ok, _} = Validator.validate_heartbeat(binary) + end + test "accepts RFC 3339 timestamp version format" do heartbeat = %AgentHeartbeat{ version: "2025-02-09T15:30:45Z",