Skip to content

fix(powershell): treat Start-FinOpsCostExport dates as UTC calendar dates - #2261

Open
Roland Krummenacher (RolandKrummenacher) wants to merge 2 commits into
devfrom
RolandKrummenacher/fix-cost-export-date-timezone
Open

fix(powershell): treat Start-FinOpsCostExport dates as UTC calendar dates#2261
Roland Krummenacher (RolandKrummenacher) wants to merge 2 commits into
devfrom
RolandKrummenacher/fix-cost-export-date-timezone

Conversation

@RolandKrummenacher

Copy link
Copy Markdown
Collaborator

🛠️ Description

Fixes #2255.

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.

# machine in UTC+01:00
$in = [datetime]'2026-01-01'   # how the binder parses the parameter: local midnight
$in.ToUniversalTime().Date     # -> 2025-12-31

The parameters are now treated as UTC calendar dates: the day the caller names is tagged as UTC rather than converted.

[datetime]::SpecifyKind($StartDate.Date, [DateTimeKind]::Utc)

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

Local offset Old behavior New behavior
UTC 2026-01-01 2026-01-01
UTC+01:00 (Europe) 2025-12-31 2026-01-01
UTC+05:30 (India) 2025-12-31 2026-01-01
UTC+09:00 (Japan) 2025-12-31 2026-01-01
UTC-05:00 (US East) 2026-01-01 2026-01-01
UTC-08:00 (US West) 2026-01-01 2026-01-01

At UTC and negative offsets, local midnight converts to a later hour on the same UTC day, so .Date was already unchanged. Behavior there is identical before and after this change. CI runs on UTC, which is why this never reproduced there.

-Backfill ran an extra month

The 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 3 issued 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.ps1 encoded the shifted behavior — they built their expected values with the same ToUniversalTime() conversion applied to both sides, so they failed on any machine east of UTC:

[-] Should call /run with default end date
[-] Should call /run for backfill
[-] Should adjust end date if it would be in the future

Fixing only the tests would have cemented the product behavior, so the assertions were rewritten to be time zone independent instead:

  • Date expectations are now literal strings ('2024-01-01T00:00:00Z') rather than expressions that shift in step with the code under test.
  • Date-derived tests compute from (Get-Date).ToUniversalTime().Date so they hold at any offset.
  • Added -Exactly to the invocation counts, which is what catches the extra -Backfill month.
  • Added a test covering every DateTimeKind the parameter binder can produce (Local, Unspecified, and the Utc path via the existing tests).

Results on a UTC+02:00 machine (PowerShell 7.6.3, Pester 6.1.0):

  • Before: Tests Passed: 5, Failed: 3
  • After: Tests Passed: 9, Failed: 0
  • Full unit suite: Tests Passed: 2373, Failed: 0, Skipped: 4
  • Invoke-ScriptAnalyzer clean on both changed PowerShell files

Because the change is a no-op at UTC, the existing CI run is the control: it should stay green.


📝 Notes

  • This is a behavior change for users at positive UTC offsets — they will start exporting the day they actually asked for, and -Backfill n will run n + 1 exports instead of n + 2. It is a no-op for UTC and negative offsets.
  • Parameter help and the command reference page now state the UTC calendar-date semantics explicitly.
  • Out of scope, noted while tracing this: passing -EndDate without -StartDate leaves $StartDate at DateTime.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

  • Bug fix (non-breaking change which fixes an issue)
  • Unit tests added/updated and passing
  • Documentation updated
  • Changelog updated

🤖 Generated with Claude Code

…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) <[email protected]>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / -EndDate as UTC calendar dates via SpecifyKind(..., 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 -Backfill defaulting logic, $EndDate is derived using AddMilliseconds(-1) and then immediately truncated to .Date later (line 135). Since -StartDate/-EndDate are 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) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Review 👀 PR that is ready to be reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[PowerShell] Start-FinOpsCostExport shifts the export period by one day in positive UTC offsets

4 participants