fix(powershell): treat Start-FinOpsCostExport dates as UTC calendar dates - #2261
Conversation
…ates Start-FinOpsCostExport converted -StartDate and -EndDate with ToUniversalTime() before truncating them to a day. For any positive UTC offset, local midnight falls on the previous UTC day, so .Date moved the requested period back by one day. The defect is asymmetric: at UTC and negative offsets local midnight converts to a later hour on the same UTC day, so .Date is unchanged and behavior is identical before and after this change. CI runs on UTC, which is why it never reproduced there. Cost Management export periods are UTC and day-granular, and the help text describes both parameters as days, so the parameters are now treated as calendar dates: the day the caller names is tagged as UTC rather than converted. The -Backfill default start date is derived from the current UTC month for the same reason; previously it was one day early, which made -Backfill run one extra month (at UTC+2, -Backfill 3 issued 5 export runs covering April through August instead of 4 covering May through August). The three tests that encoded the shifted behavior built their expected values with the same conversion on both sides, so they failed on any machine east of UTC. They now assert literal dates, which is what makes them time zone independent, and a new test covers each DateTimeKind the parameter binder can produce. Fixes #2255 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This pull request fixes Start-FinOpsCostExport date handling so -StartDate / -EndDate are treated as UTC calendar dates (day-granular) instead of being time zone converted before truncation, which previously shifted requested periods back by one day for positive UTC offsets.
Changes:
- Tag
-StartDate/-EndDateas UTC calendar dates viaSpecifyKind(..., Utc)to avoid offset-driven day shifts. - Update Pester unit tests to be time zone independent and tighten invocation assertions with
-Exactly. - Update Microsoft Learn reference docs and changelog to document the UTC calendar-date semantics and the bug fix.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/powershell/Public/Start-FinOpsCostExport.ps1 | Fixes date normalization to preserve requested calendar days across time zones and adjusts backfill defaulting to be UTC-month based. |
| src/powershell/Tests/Unit/Start-FinOpsCostExport.Tests.ps1 | Rewrites assertions to be time zone independent; adds coverage for different DateTimeKind inputs and uses -Exactly for call counts. |
| docs-mslearn/toolkit/powershell/cost/start-finopscostexport.md | Documents -StartDate / -EndDate as UTC calendar dates and updates ms.date. |
| docs-mslearn/toolkit/changelog.md | Adds a changelog entry describing the bug fix and updates ms.date. |
Suppressed comments (1)
src/powershell/Public/Start-FinOpsCostExport.ps1:123
- In the
-Backfilldefaulting logic,$EndDateis derived usingAddMilliseconds(-1)and then immediately truncated to.Datelater (line 135). Since-StartDate/-EndDateare now treated as UTC calendar dates (day-granular), the millisecond subtraction is unnecessary and makes the intent harder to follow. Consider defaulting the end date directly to the last calendar day of the month.
# If -EndDate is not set, assume 1 month
if (-not $EndDate)
{
$EndDate = $StartDate.AddMonths(1).AddMilliseconds(-1)
}
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
…ill end date The -Backfill default end date subtracted a millisecond to land on the last day of the window, then had the time truncated away by .Date a few lines later. For a midnight start date -- always the case here, since both -StartDate sources are midnight -- AddMonths(1).AddMilliseconds(-1).Date and AddMonths(1).AddDays(-1) are identical, so this is a no-op. Using AddDays(-1) also makes the two default-end-date paths read the same; the non-backfill branch below already computes it that way. Note this is not the same as 'the last day of the month': with an explicit -StartDate that is not the first (for example -StartDate '2026-03-15' -Backfill 2), the window stays one month from the start date and ends 2026-04-14, not 2026-03-31. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brett Wilson (MSBrett)
left a comment
There was a problem hiding this comment.
Reviewed this — the fix is correct and well-verified. I re-ran the unit tests under TZ=Asia/Tokyo and TZ=America/Los_Angeles to reproduce the exact asymmetric bug from #2255, and they pass on this branch in both (and fail as expected on old logic). Lint suite is clean too.
One non-blocking item: this branch currently shows as CONFLICTING against dev — looks like just the ms.date field in changelog.md (08/17 vs 08/19), which matches the repo's documented conflict pattern. A git merge origin/dev with ms.date set to today should clear it.
The two out-of-scope issues you called out in the description (missing--StartDate month-count blowup, and the millisecond- vs. day-truncated backfill boundary) both check out on my read — good catch calling those out separately rather than scope-creeping this fix.
…-cost-export-date-timezone # Conflicts: # docs-mslearn/toolkit/changelog.md
Brett Wilson (MSBrett)
left a comment
There was a problem hiding this comment.
Approving — my earlier review found only two Low/informational items (the pending ms.date merge conflict, now resolved via git merge origin/dev + this branch's own automated ms.date bot commit; and two pre-existing, explicitly out-of-scope issues unrelated to this fix). No Critical/High/Medium findings. Tests pass (verified under TZ=Asia/Tokyo and TZ=America/Los_Angeles to reproduce the original bug conditions), lint is clean, and CI is green.
🛠️ Description
Fixes #2255.
Start-FinOpsCostExportconverted-StartDateand-EndDatewithToUniversalTime()before truncating them to a day. For any positive UTC offset, local midnight falls on the previous UTC day, so.Datemoved the requested period back by one day.The parameters are now treated as UTC calendar dates: the day the caller names is tagged as UTC rather than converted.
This matches how the parameters are documented ("Day to start pulling the data for", "Last day to pull data for") and how Cost Management export periods actually work — they are UTC and day-granular, so there is no instant to convert.
The defect is asymmetric
2026-01-012026-01-012025-12-31❌2026-01-012025-12-31❌2026-01-012025-12-31❌2026-01-012026-01-012026-01-012026-01-012026-01-01At UTC and negative offsets, local midnight converts to a later hour on the same UTC day, so
.Datewas already unchanged. Behavior there is identical before and after this change. CI runs on UTC, which is why this never reproduced there.-Backfillran an extra monthThe default backfill start date used the same pattern, landing one day early — which pushed the computed month count up by one. On a UTC+02:00 machine,
-Backfill 3issued 5 export runs (April through August) instead of 4 (May through August). The default is now derived from the current UTC month, so the count is correct in every time zone.🧪 Testing
Three tests in
Start-FinOpsCostExport.Tests.ps1encoded the shifted behavior — they built their expected values with the sameToUniversalTime()conversion applied to both sides, so they failed on any machine east of UTC:Fixing only the tests would have cemented the product behavior, so the assertions were rewritten to be time zone independent instead:
'2024-01-01T00:00:00Z') rather than expressions that shift in step with the code under test.(Get-Date).ToUniversalTime().Dateso they hold at any offset.-Exactlyto the invocation counts, which is what catches the extra-Backfillmonth.DateTimeKindthe parameter binder can produce (Local,Unspecified, and theUtcpath via the existing tests).Results on a UTC+02:00 machine (PowerShell 7.6.3, Pester 6.1.0):
Tests Passed: 5, Failed: 3Tests Passed: 9, Failed: 0Tests Passed: 2373, Failed: 0, Skipped: 4Invoke-ScriptAnalyzerclean on both changed PowerShell filesBecause the change is a no-op at UTC, the existing CI run is the control: it should stay green.
📝 Notes
-Backfill nwill runn + 1exports instead ofn + 2. It is a no-op for UTC and negative offsets.-EndDatewithout-StartDateleaves$StartDateatDateTime.MinValue, so the month count becomes ~24,000 and the run loop iterates accordingly. It is unrelated to time zones and is not touched here — happy to open a separate issue.✅ Checklist
🤖 Generated with Claude Code