fix: parse day and fractional-second components in FromTimeSpan (changes parsed values) - #435
Conversation
FromTimeSpan read the time span fields at fixed offsets and took the day component from timeSpan[0:0], which is always the empty string. Every value carrying a day component therefore parsed to zero, including "1.00:00:00" — the interval on the default machine policy — and any fractional seconds were dropped. An empty string panicked on a slice bound. Parse the components by separator instead. The day and fractional-second parts are both optional, and the server does not pad the day component to a fixed width, so offsets cannot be assumed. Malformed input now yields a zero duration rather than a panic. The existing tests only logged their results and asserted nothing, which is why this went unnoticed; they now assert, and every case they already covered was returning zero. Closes OctopusDeploy#434 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
eda5cf7 to
5d7ef5e
Compare
NickJosevski
left a comment
There was a problem hiding this comment.
Thanks for this, and for splitting it out of #436 so it can land on its own.
I checked the behaviour against main at 1b925bc rather than reading the diff, by running the current parser and yours over the same inputs:
| input | correct | main today |
this PR |
|---|---|---|---|
00:05:00 |
5m0s | 5m0s | 5m0s |
01:30:00 |
1h30m0s | 1h30m0s | 1h30m0s |
1.00:00:00 |
24h0m0s | 0s | 24h0m0s |
7.12:30:00 |
180h30m0s | 12h30m0s | 180h30m0s |
07.12:30:00 |
180h30m0s | 0s | 180h30m0s |
10.00:00:00 |
240h0m0s | 0s | 240h0m0s |
00:00:00.5000000 |
500ms | 0s | 500ms |
1.02:03:04.5000000 |
26h3m4.5s | 2h3m4s | 26h3m4.5s |
"" |
0s | panic | 0s |
Every day-bearing value was silently wrong rather than failing loudly, which is the worst shape for a bug like this — a 10-day health check interval reads back as 0s, and 7.12:30:00 is out by a factor of 14. The plain hh:mm:ss cases are identical before and after, so nothing that works today changes.
Worth noting for anyone reviewing the blast radius: ToTimeSpan is untouched, and all 14 in-repo callers of FromTimeSpan are inside UnmarshalJSON on machine policy, cleanup policy, and health check policy. So this only affects what the SDK reads back, never what it sends. It also closes a quiet data-loss path — today you can read a policy, write it back unchanged, and send a day-scale interval out as zero.
Two notes, neither blocking:
1. Negative time spans are still wrong, in both directions. Not a regression and I don't think it belongs in this PR, but recording it so it isn't rediscovered later:
FromTimeSpan("-1.02:03:04") = -21h56m56s // .NET means -26h3m4s; the sign only reaches the days field
FromTimeSpan("-00:05:00") = 5m0s // sign dropped entirely
ToTimeSpan(-26h3m4s) = "-1.-2:-3:-4"
That last line is existing code this PR doesn't touch, which is a decent argument that negative spans were never supported on either side. Happy for it to stay out of scope — a follow-up issue seems right if we care.
2. This changes a value consumers already read. Anyone who has been getting 0s for these fields starts getting the true value, so a Terraform plan may show a one-off diff on machine policy timeouts after upgrading. That diff is the correction, but it'll generate questions if it ships unannounced, so I'd like the release to call it out.
One process thing before merge: this is a fork PR, so the Go test workflow has never actually run — the only green checks are CLA and GitGuardian. I'll approve the run so we have CI on the record. Locally it builds clean, vets clean, and the formatter tests pass in both pkg/machines and pkg/machinepolicies.
|
On the CI run you approved: the Integration Tests job failed, but no Go tests executed. It died in the "Initialize containers" step, before the test job started. The Octopus Server container could not log into SQL Server ( On your two notes:
|
|
Two follow-ups: one for @Scott-Emberson, one for whoever merges and tags this. The red
|
|
Understood on the fork CI limitation, and agreed there is nothing more I can do about it from this side. If a maintainer wants a real integration run before merge, pushing the branch in-repo works for me (maintainerCanModify is true). To make the release handling harder to miss at merge and tag time:
release-please adoption sounds right for exactly this reason; happy to review that PR when it exists. |
Fixes #434.
FromTimeSpanread the time span fields at fixed offsets and took the day component fromtimeSpan[0:0], which is always the empty string. Every value carrying a day component parsed to zero, fractional seconds were dropped, and an empty string panicked on a slice bound. Only the plainhh:mm:ssform worked.The fixed offsets also assumed a single-digit day, and the server does not pad the day component, so
"7.12:30:00"and"07.12:30:00"could not both be read correctly even with the days segment fixed.What it returned before
"1.00:00:00"is the health check interval on the default machine policy, so this sits on a common path.The change
Split on the separators rather than slicing at fixed offsets. Both uses of
.are ambiguous (d.hh:mm:ssagainsthh:mm:ss.fffffff), so a leading segment is only treated as the day component when what follows still holds a completehh:mm:ss. The fractional part is read as a decimal fraction of a second, which handles both the five-digit form this package writes and the seven-digit form .NET produces. Malformed input returns a zero duration instead of panicking.Applied to
pkg/machinepolicies/andpkg/machines/, which each carry their own copy of the function. Patching only one would leave consumers of the two packages parsing the same payload differently.Tests
pkg/machines/duration_formatter_test.gocalledFromTimeSpanseven times and logged each result without asserting anything. All seven returned0sand the test passed, which is why this went unnoticed. It now asserts, along with a round trip check overToTimeSpan, and the same file is added topkg/machinepolicies/.Verified against a 2026.x server: a policy with a seven day interval now reads back as
168h0m0sinstead of0s.ToTimeSpanis unchanged. The server accepts its zero-padded day output and normalises it on read.Impact
Any
time.Durationread back throughFromTimeSpanwas affected. InMachinePolicythat coversConnectionConnectTimeout,ConnectionRetrySleepInterval,ConnectionRetryTimeLimit,PollingRequestQueueTimeoutandPollingRequestMaximumMessageProcessingTimeout, plusMachineHealthCheckPolicy.HealthCheckIntervalandMachineCleanupPolicy.DeleteMachinesElapsedTimeSpan.Callers who were compensating for the zero values will see real durations after this. I could not find any such workaround in this repository.
Upgrade note (please carry into the release notes)
This changes values consumers already read. Any day-scale duration on the fields above that previously came back as
0swill start coming back as its true value. In particular, tools that diff read state against stored state (such as the Terraform provider) may show a one-off diff on machine policy timeouts after upgrading. That diff is the correction, not drift, but it will generate questions if the release does not call it out.For whoever tags the release: goreleaser builds notes from commit subjects only, so this needs a manual note on the GitHub Release. A ready-to-paste version is in #435 (comment). The PR title already carries the
(changes parsed values)marker so the default squash subject signals it.Out of scope, tracked separately: negative time spans are mishandled in both
FromTimeSpanandToTimeSpanon both sides of this change. See #447 for details.