You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of LeanerCloud/cloud-commitments-cli#1333 (commitment laddering automation). Phase 4 of 4: Azure implementation (Azure has both needed instruments: exchangeable Reservations and Savings Plans for Compute).
PRs:
Azure read side: list existing savings plans via Microsoft.BillingBenefits; savings-plan utilization via Cost Management benefitUtilizationSummaries; usage baseline from Cost Management series. Reservation utilization already exists (getRIUtilizationViaAPI).
Azure write side: new savings-plan purchase client (providers/azure/services/savingsplan/, Microsoft.BillingBenefits/savingsPlanOrders, validate-then-put, api-version confirmed against current docs) and reservation exchange client (POST /providers/Microsoft.Capacity/calculateExchange -> POST /providers/Microsoft.Capacity/exchange, async operation polling) with the same spend-cap guardrails as the AWS exchange path. All raw-HTTP clients MUST use httpclient.New() (SSRF/IMDS blocking).
AzureLadder capability + enablement: implements LadderCapability with 2 layers (Reservation = base+buffer, SavingsPlan = flex); pipeline enablement for Azure scopes.
A2 (residual basis): Azure stays on the unmerged guard. GetUsageBaseline returns ErrBaselineBasisUnresolved (providers/azure/ladder/ladder.go:309-311, tested by TestAzureLadder_GetUsageBaselineIsBlocked). No residual series is built, because Azure has no on-demand series source yet. The AWS residual basis (providers/aws/ladder/baseline.go:124) is not mirrored until a recorded response confirms the filter.
C2 (new-term exchanges):ReshapeBuffer refuses every run that is not a dry run, before any client call, with errExchangeNewTerm (providers/azure/ladder/reshape.go, checkReshapeAllowed). Reason: every exchange target is a new reservation purchase whose term starts when bought, so every exchange starts a new term. Dry runs still price through CalculateExchange and never reach ExecuteExchange. The per-process cap is accepted as-is and no new cap was added.
D1 (exchange enablement): unchanged, owner-only.
Root cause: no code change was needed for A2 (the guard already existed). C2 needed a new refusal, because the exchange path executed any priced exchange within the caps.
Change: commit 19b06a496fafe9da9044faf00e0d3d826c993d03 on main, Refs #28.
Tests: TestAzureLadder_ReshapeBuffer_NewTermExchangeNeverReachesExecute (regression, fails without the refusal) and TestAzureLadder_ReshapeBuffer_DryRunPricesTargetWithoutExecuting. Validation and scope tests (RefusesNilOrBadCaps, RejectsForeignScope) now use a dry run so they still reach their own checks. Removed the execute, halt, daily-cap and concurrency tests, which covered paths that are now unreachable.
Verification: mutation probe (refusal disabled) made the regression test fail; the file was restored and checked with cmp. In providers/azure: go test -count=1 ./..., go vet ./..., gofmt -l . and golangci-lint run ./... (0 issues) all passed. Adversarial review: two passes with the same reviewer; pass 1 found the two weakened validation and scope tests, fixed; pass 2 no actionable findings.
CI on 19b06a4: "CI - Build & Test" and "pre-commit" both succeeded, with all jobs run.
Known leftover: the execute, halt and daily-cap code in reshape.go is now unreachable. Remove it when D1 is decided. Issue stays open for D1.
A2 (usage baseline) closed by the owner's 2026-11-12 decision: Azure stays on the unmerged guard (ErrBaselineBasisUnresolved, providers/azure/ladder/ladder.go:309-311), so no residual series is built. Recorded by claude-go-cli-mcp, 2026-11-13. #28 stays open for D1, the Azure switch-on, which is owner-gated.
Non-D1 slices are landed or decided: C2 (refuse exchanges that start a new Azure term) is on main (providers/azure/ladder/reshape.go:101, errExchangeNewTerm). A2 stays unmerged until a recorded Basis response (2026-11-12 decision). D1 (the Azure switch-on) is owner-gated and remains open. Commented by claude-go-cli-mcp, 2026-11-14.
C2 is on main: ladder exchanges that start a new Azure term are refused (providers/azure/ladder/reshape.go:101, errExchangeNewTerm), landed in 19b06a4. A2 stays unmerged until a recorded Basis response (12 Nov decision). D1 remains owner-gated. Recorded by claude-go-cli-mcp, 2026-11-14.
Activity
3 remaining items
refactor(aws/ladder): use the shared ladder.RequireCap for reshape caps
feat(azure/ladder): read savings plan utilization from benefit utiliz…
feat(common): record the currency a commitment cost is denominated in
feat(azure/savingsplans): report the billing currency of a savings plan
feat(azure/ladder): implement the Azure LadderCapability read side
test(azure/ladder): cover the AzureLadder read side and its fail-loud…
feat(azure/ladder): purchase savings plan and reservation layers behi…
test(azure/ladder): cover the purchase guards and routing
feat(azure/ladder): reshape the buffer through exchanges, behind the …
test(azure/ladder): cover the reshape caps, halt and concurrency
fix(azure): refuse ladder exchanges that start a new term
cristim commentedon Nov 12, 2026
Owner decision (2026-11-12), recorded:
GetUsageBaselinereturnsErrBaselineBasisUnresolved(providers/azure/ladder/ladder.go:309-311, tested byTestAzureLadder_GetUsageBaselineIsBlocked). No residual series is built, because Azure has no on-demand series source yet. The AWS residual basis (providers/aws/ladder/baseline.go:124) is not mirrored until a recorded response confirms the filter.ReshapeBufferrefuses every run that is not a dry run, before any client call, witherrExchangeNewTerm(providers/azure/ladder/reshape.go,checkReshapeAllowed). Reason: every exchange target is a new reservation purchase whose term starts when bought, so every exchange starts a new term. Dry runs still price throughCalculateExchangeand never reachExecuteExchange. The per-process cap is accepted as-is and no new cap was added.Root cause: no code change was needed for A2 (the guard already existed). C2 needed a new refusal, because the exchange path executed any priced exchange within the caps.
Change: commit
19b06a496fafe9da9044faf00e0d3d826c993d03onmain,Refs #28.Tests:
TestAzureLadder_ReshapeBuffer_NewTermExchangeNeverReachesExecute(regression, fails without the refusal) andTestAzureLadder_ReshapeBuffer_DryRunPricesTargetWithoutExecuting. Validation and scope tests (RefusesNilOrBadCaps,RejectsForeignScope) now use a dry run so they still reach their own checks. Removed the execute, halt, daily-cap and concurrency tests, which covered paths that are now unreachable.Verification: mutation probe (refusal disabled) made the regression test fail; the file was restored and checked with
cmp. Inproviders/azure:go test -count=1 ./...,go vet ./...,gofmt -l .andgolangci-lint run ./...(0 issues) all passed. Adversarial review: two passes with the same reviewer; pass 1 found the two weakened validation and scope tests, fixed; pass 2 no actionable findings.CI on
19b06a4: "CI - Build & Test" and "pre-commit" both succeeded, with all jobs run.Known leftover: the execute, halt and daily-cap code in
reshape.gois now unreachable. Remove it when D1 is decided. Issue stays open for D1.cristim commentedon Nov 14, 2026
A2 (usage baseline) closed by the owner's 2026-11-12 decision: Azure stays on the unmerged guard (ErrBaselineBasisUnresolved, providers/azure/ladder/ladder.go:309-311), so no residual series is built. Recorded by claude-go-cli-mcp, 2026-11-13. #28 stays open for D1, the Azure switch-on, which is owner-gated.
cristim commentedon Nov 14, 2026
Non-D1 slices are landed or decided: C2 (refuse exchanges that start a new Azure term) is on main (providers/azure/ladder/reshape.go:101, errExchangeNewTerm). A2 stays unmerged until a recorded Basis response (2026-11-12 decision). D1 (the Azure switch-on) is owner-gated and remains open. Commented by claude-go-cli-mcp, 2026-11-14.
cristim commentedon Nov 14, 2026
C2 is on main: ladder exchanges that start a new Azure term are refused (providers/azure/ladder/reshape.go:101, errExchangeNewTerm), landed in 19b06a4. A2 stays unmerged until a recorded Basis response (12 Nov decision). D1 remains owner-gated. Recorded by claude-go-cli-mcp, 2026-11-14.