This file covers the cloud-commitments-go modules (pkg/, providers/, ci_cd_sanity_tests/) only.
The format is based on Keep a Changelog.
-
Azure search reservations are upfront-only (#38).
searchpurchases and offering quotes withmonthlyorno-upfrontare refused before any request, because the Capacity catalog does not recogniseSearchService. Monthly stays allowed for cache, managedredis and synapse, whose catalog rows list it. -
Expiry adjustment functions return an error (#450).
recommendations.AdjustExistingCoverageForExpiringCommitments,...WithCoverageand...WithCoverageOptionsnow returnerroras their last value. An unknown service, or an EC2 size with no normalization factor in a size-flexible family, fails the call and leaves the recommendations untouched. Non-RDS expiry pools now carry the service name, and EC2 pools in size-flexible families net per family in normalized units. -
EC2 re-drives now refuse when several RIs carry the idempotency token. If more than one active or payment-pending RI has the token (#724), the purchase is refused instead of adopting the first one: no purchase is sent and the error wraps
common.ErrRefusedBeforePurchase, names the count and the masked token, and needs a human to reconcile. -
Azure savings plan inventory fails closed for Shared plans billed to a billing account (#272).
savingsplans.Client.GetExistingCommitmentsandGetCoveringCommitments(the one the duplicate check reads) list plans billed to/subscriptions/<client subscription>, plus billing-account-billed Single-scope plans whose applied scope names exactly the client subscription. A billing-account plan with Shared, management group or unresolvable scope cannot be attributed to one subscription, so the whole call returns an error starting "incomplete savings plan inventory for subscription ..., page N: row M: " (for example "billing-account plan is not single-subscription scoped", or a "single-scope plan ..." message when a Single scope cannot be resolved) instead of a partial list. Plans billed to a subscription are not affected by this scope check, andGetCoveringCommitmentsalso returns the plans billed to other subscriptions that may cover this one. The duplicate check therefore sees an error, not a silently incomplete inventory, and no recommendation is purchased on top of a plan it could not see. This affects EA and MCA tenants that buy savings plans at billing-account scope with Shared applied scope. Workaround: buy Single-scope plans attributed to the subscription. A consumer that catches the error and skips Azure savings plan inventory also removes the duplicate-purchase guard for that subscription, so it can buy on top of the Shared plan it could not see; only do that where no Shared billing-account plan can cover the subscription. Tracked in #272; revisit when a tenant needs Shared attribution. -
Azure Redis purchases must go through managedredis (
ServiceMemoryDB). The Azure recommendations adapter still labels Redis recommendationsServiceCache, and the cache client now refuses every purchase (#538), so route Azure Redis purchases to the managedredis client. The refusal wrapscommon.ErrRefusedBeforePurchase(nothing was sent), not an upfront-approval failure, so approving and retrying does not help. -
Removed (
aws/azure/gcpservice clients): the unusedValidateOfferingandGetValidResourceTypesmethods, and the exported test seamsRPValidateAPI/SetRPValidateClient(azure savingsplans),SQLAdminService/SetSQLAdminService(gcp cloudsql),MachineTypesService/MachineTypesIterator/SetMachineTypesService(gcp computeengine),RedisCachesPager/SetRedisCachesPager(azure cache and managedredis),ServicesPager/SetSearchServicesPager(azure search). None of these were part ofpkg/providerinterfaces; callers that type-asserted the concrete clients must drop those calls.GetOfferingDetailsis unchanged. (#11) -
Removed (gcp
cloudstorageandmemorystoreclients): the unused exported test seamsStorageService/BucketIterator/BucketHandle/SetStorageServiceandRedisService/RedisIterator/CreateInstanceOperation/SetRedisService. Purchases are a no-op, so nothing read them; callers that type-asserted the concrete clients must drop those calls. The gcp module no longer requirescloud.google.com/go/storageorcloud.google.com/go/redis. (#6)
-
ladder.LadderPlan.ValidateRunCaps: checks an assembled plan against the per-run caps: the summed immediate purchase USD/hr must not exceedmax_hourly_commit_per_run, and purchases plus reshapes must not exceedmax_actions_per_run(holds are not counted). Pure, no caller yet; the per-run execution gate in #27 is the intended caller. (#27) -
ladder.BuildPlan: assembles a validatedLadderPlanfrom anAllocateResultand itsBuildTranchesoutput. Each immediate tranche becomes one purchase action with the tranche's own commitment amount; an allocation with no immediate tranche becomes a hold stating when its first tranche fires. Pure, no I/O, no existing caller changes. (#27) -
aws/recommendations:recommendations.GetRICommitmentRatederives the commitment dollars per covered on-demand dollar of a new Convertible EC2 Reserved Instance from Cost Explorer's reservation purchase recommendations: used share of the effective monthly RI cost (recurring plus upfront amortized over the term) over the covered on-demand cost, filtered by account and region, withErrNoRICommitmentRatewhen no usable detail exists. (#732) -
ladder/aws:GetCommitmentRatesalso returns aconvertible-risizing rate fromGetRICommitmentRate. A missing RI rate omits only that layer; any other error fails the call. Costs one extra billed Cost Explorer request (plus pages) per call. (#732) -
ladder/aws:AWSLadder.WithBufferFractionand a stable-usage estimator: when the fraction is set,GetUsageBaselinefillsStableUSDPerHourwith the lowest of the low-water mark, the 10th percentile and the last 3 days, times1 - fraction. It stays nil when the option is unset, the series has fewer than 14 days, or the 10th percentile is under half the median (#31). -
AWS/Redshift: when the
AttemptJournalalso implementscommon.ClaimReader, the reserved-node dedupe skips nodes another purchase recorded as bought, before and after the send. A claim read error refuses before the send or leaves the outcome unknown after it; if the skip leaves one of several matches and the token has no sending or outcome_unknown record, the purchase is refused as ambiguous (#700). -
pkg: optionalcommon.ClaimReader(ClaimedByOthers(ctx, token, ids)), which anAttemptJournalmay implement so a provider can skip commitments another purchase recorded as bought (#700). -
pkg:common.Commitment.CostKnownreports whetherCostholds a price the provider reported. The zero value means unknown, so aCostof 0 withoutCostKnownmust not be read as free. Producers set it in a follow-up (#74). -
common:AttemptJournal,AttemptRecordand an optionalPurchaseOptions.AttemptJournal, a caller-backed durable record of purchase attempts per idempotency token (#107). Nothing reads it yet. -
pkg:provider.PurchaseReconciler, an optional read-onlyServiceClientinterface withcommon.ReconcileRequest,common.ReconcileResultandcommon.ReconcileVerdict(ReconcileFound/ReconcileUnknown, never NotFound), so a purchase that returnedErrOutcomeUnknowncan later be checked against the provider. Types only; AWS EC2 and Redshift implementations follow (#298). -
aws: theec2andredshiftservice clients now implementprovider.PurchaseReconciler: a read-only check, after a purchase returnedErrOutcomeUnknown, of whether the purchase landed. The verdict isFoundorUnknown, neverNotFound. It reportsFoundonly for exactly one full attribute match inside the attempt window, andUnknownotherwise. The other AWS clients (rds, elasticache, memorydb, opensearch, savingsplans) have an idempotency token and do not implement it. The platform consumes it in a follow-up (#298). -
ladder/aws:ladder.ObservedCommitmentRateCapabilityand its AWS implementation report commitment dollars per covered on-demand dollar measured on the account's existing Savings Plans and convertible RIs (account-scoped Cost Explorer utilization). Layers with no usable observation are absent. -
azure:PartialResultError.SkipKinds() []SkipKind, index-aligned withSkipReasons(), so consumers can tell skip causes apart without matching the reason text.SkipUnpriceableis a row with no usable price, a non-USD currency, no pricer or a malformed row.SkipPriceLookupUnavailableis a price lookup that failed to complete: the failing Advisor row and every one after it.SkipSourceIncompleteis a truncated Advisor walk. The kinds survive the multi-subscription merge. APartialResultErrorbuilt with onlySkippedreads asSkipUnpriceable. Existing fields and methods are unchanged (#670). -
azure.NewPartialSubscriptionFailureWithKinds: builds aPartialSubscriptionFailureErrorwith each skip's kind, so a consumer test can build a failure whoseSkipKinds()reports a price-lookup outage or a truncated source.SkipKinds()now also reads an empty kind asSkipUnpriceable(#680). -
common.ErrRefusedBeforePurchase: a sentinel for a purchase refused before the call sent any purchase request, detectable witherrors.Is. The AWS EC2, RDS, OpenSearch, Redshift and MemoryDB approved-upfront guard refusals wrap it. -
Azure
NewPartialSubscriptionFailureconstructor: builds aPartialSubscriptionFailureError, optionally with skip reasons readable throughSkips(), so code outside the module can construct one, for example in tests. -
Azure currency refusals wrap
common.ErrCurrencyNotAccepted: reservation purchase, savings plan purchase and reservation exchange refusals can now be detected witherrors.Is; each existing message gains a: currency not acceptedsuffix. -
Typed accessors on partial recommendation results: the Azure and GCP
PartialResultErrortypes now exposeSkippedCount() intandSkipReasons() []string(a copy), so callers can match one interface througherrors.Asinstead of parsing the message text. -
common.ErrCurrencyNotAccepted: a sentinel error for operations refused because the billing currency is missing or not the requested one, so callers can tell a refusal from an API failure witherrors.Is. -
ladder.AllocateResult.NetExistingUSDPerHour: the net existing commitment (existing minus expiring, summed over layers) thatAllocatesized the gap against, set on every successful result including the hold-only exits. Callers can report it instead of recomputing a gross figure from the layer states, so a plan's existing figure reconciles with its gap.Allocatenow checks the snapshot before the baseline-unavailable hold, so expiring above existing is an error there too (#83). -
exchange.WithOfferedInstanceTypes/RunAutoExchangeParams.OfferedInstanceTypes: restrict RI reshape targets to the instance types the region offers.AnalyzeReshapingandAnalyzeReshapingWithRecstake it as a variadicReshapeOption; an empty list keeps the previous size ladder, and a family missing from a non-empty list gets a recommendation with the reason "instance family not offered in region". Auto exchange now reports the recommendation's own reason for every skipped RI without a target (#563). -
RecommendationParams.RIAccountScope: selects the Cost Explorer account scope (linkeddefault, orpayer) for Reserved Instance recommendations, mirroringSPAccountScope. Empty keeps the previous linked behaviour; an unknown value is rejected (#508). -
recommendations.RederiveFamilyCoverage: recomputes the family-wide RDSProjectedCoveragethat family-NU sizing wrote, after a caller changedCount(--max-instances, count override, duplicate-RI dedupe). It shares its formula with sizing, returns a new slice and leaves non-RDS, SQL Server and unsized recs untouched (#511). -
recommendations.AdjustExistingCoverageForExpiringCommitmentsWithCoverageOptions: the per-pool (EC2 and other non-family) expiry deduction, plus credit for replacements already bought. Coverage is a lookback average, so RIs bought insideCoverageLookbackDaysare not in it yet; with R the count of active, non-expiring RIs in the same pool that started inside the lookback, the deduction ismax(0, expiring - R). It can only buy less, never more. A growth purchase in the pool counts as a replacement. Size-flexible EC2 families net in normalized units (#450), so a replacement of another size in the same family is credited.CoverageLookbackDays <= 0credits none. Additive; the existing functions are unchanged (#346). -
recommendations.Client.GetSPCommitmentRate: commitment dollars per on-demand dollar (r) of the used part of a new Savings Plan, derived from CE purchase recommendations as(utilization/100) * EstimatedSPCost / (EstimatedSPCost + EstimatedSavingsAmount)(ratio of sums over matching details). Strict raw-detail parsing; an absent field or no matching detail isErrNoSPCommitmentRate, never a 1:1 fallback. Nothing consumes it yet (go#349, groundwork for converting on-demand ladder sizing to commitment dollars). -
recommendations.ApplyFamilyNUSizingRDSWithExpiryOptions: the rebuy-window RDS family sizing, plus credit for replacements already bought. Coverage is a lookback average, so RIs bought insideCoverageLookbackDaysare not in it yet; with R the NU of visible active, non-expiring RIs that started inside the lookback, post-expiry covered ismax(0, max(min(C, C - E + R), min(C, A - E))). It is never below the R = 0 formula, so it can only buy less, and a partial inventory still never removes more than expires.FamilyExpiryReport.ReplacementNUrecords R.CoverageLookbackDays <= 0credits none. Additive; the other functions are unchanged (LeanerCloud/cloud-commitments-cli#2132, #2175). -
common.SavingsPlanDetails.Currency: the ISO 4217 code the hourly commitment is denominated in. Additive and unused until the Azure Savings Plan client starts requiring it (#46). -
recommendations.ApplyFamilyNUSizingRDSWithExpiry: family-NU sizing that deducts RDS RIs expiring inside the rebuy window from family coverage in normalized units (max(covered - expiring, min(covered, active - expiring)), so a partial RI inventory never removes more than expires), and reports expiring RIs it could not use (FamilyExpiryReport.Unusable). Additive;ApplyFamilyNUSizingRDSis unchanged. Consumers that pass a rebuy window will see higher RDS counts and costs for families with expiring RIs (LeanerCloud/cloud-commitments-cli#2132). -
insurance.ContractTerms(): returns the documented Archeracontract_termvalues thatComparisonaccepts and the decoder returns, as a new sorted slice on every call (#316). The order is lexicographic, not by duration. Consumers should call it instead of copying the list. -
common.ErrOutcomeUnknown: exported sentinel for a purchase whose response was lost and whose commitment may exist (#301). Consumers can useerrors.Isto tell it from a definite failure. The AWS EC2 and Redshift clients switch to it in a follow-up once the aws module pins this commit. -
pkg/insurance: a read-only Archera insured-commitment comparison contract (#284, #285).NewClientreturns a client withComparisonandPlanfor Archera's documentedGET /v1/org/{org_id}/commitment-plans/{plan_id}/comparisonandGET /v1/org/{org_id}/commitment-plans/{plan_id}endpoints (DecodeComparisonandDecodePlanparse already-fetched bodies). The origin is fixed athttps://api.archera.ai, the key is sent only in thex-api-keyheader, and the client never follows redirects and never retries. It has no purchase or binding capability and nothing in the repo calls it yet. Contract points consumers must keep:- Money is
*big.Rat; nil means unknown, never zero.archera_premiumis already insidecommitment_cost.total, so do not add it again. Monthly rates are 730-hour rates and the one-time upfront cost is a separate field; do not sum them. - The vendor schema has no currency or expiry field, so both stay nil
(unknown).
FetchedAtrecords retrieval time and freshness policy is the consumer's. - Nullable vendor identity (term, payment option, commitment type) stays nullable. Each comparison row keeps its current offer and all candidate offers.
- A comparison is a hypothetical rollup, not a bindable quote. An insured target of 100% is a requested target subject to Archera underwriting, not a guarantee.
AssessProductSupportreturnssupportedonly for the two AWS rows quoted from Archera's Supported Reservable Services page (aws/AmazonEC2andaws/savingsplan/Compute); every other product isunknown. Support is not underwriting eligibility.- The API key is redacted from
fmtoutput forConfigandClient, except%p(and%Ton a non-pointer value), whichfmthandles before redaction. - Verified with offline tests only; no live Archera call has been made, so response shapes come from the published schema.
- Money is
-
BREAKING
azure: Azure Redis recommendations now come only from managedredis and are labelledmemorydb(ServiceMemoryDB), both from Consumption and from Advisor rows. The cache client'sGetRecommendationsreturns none, soGetRecommendationsForService(ServiceCache)is empty for Azure, and consumers that stored Azure Redis plans or settings undercachemust also matchmemorydb. Azure Redis recommendations keepCacheDetailsEngineandNodeTypebut no longer reportShards(0, unknown) (#692). -
azure: the cache service no longer purchases Azure Redis reservations; managedredis (ServiceMemoryDB) is the only service that buysRedisCache, so one Redis recommendation cannot be bought once through each service.cache.PurchaseCommitmentnow refuses before any API call with an error wrappingcommon.ErrRefusedBeforePurchase. Consumers that buy an AzureServiceCacherecommendation must route it to managedredis (#538). -
Ladder run and tranche statuses now write the US spelling
canceled(RunStatusCanceled,TrancheStatusCanceled). Rows holding the legacycancelledstill validate, count as terminal, and parse to the new constants throughParseRunStatusandParseTrancheStatus. TheRunStatusCancelledandTrancheStatusCancellednames remain as deprecated aliases with the new value. Consumers that compare raw stored strings or filter oncancelledin SQL must accept both spellings. -
ListTargetOfferingsnow walks up to 50 pages (about 5000 offerings) instead of 10, and returns an error wrapping the newec2.ErrTargetOfferingsTruncated(with no list) when that bound is hit, instead of silently returning a partial list. -
GCP Cloud Storage pricing lists the SKU catalog once per client, like Cloud SQL and Memorystore.
-
Azure multi-subscription collection keeps skipped recommendations when a subscription also fails. They are returned by the new
PartialSubscriptionFailureError.Skips()(a*PartialResultError, or nil) and are deliberately not in the error chain, so a partial-result check never matches a subscription failure. Fields,Unwrapand message are unchanged. -
GCP reports the recommendations it skips. Compute Engine recommendations dropped for a non-USD amount, a malformed amount or an unrecognized term are now returned as a partial-result error (
Partial() bool) alongside the recommendations that succeeded, mirroring Azure, instead of only being logged. -
GCP: one strict commitment term parser (#15). Compute Engine now uses
skumatch.TermYearslike Cloud SQL, Memorystore and Cloud Storage; it accepts1yr/1/12mo/3yr/3/36mocase-insensitively. An unrecognized term now errors inGetOfferingDetailsand skips pricing inenrichRecWithPricinginstead of being priced as one year. -
aws/recommendations: the four Cost Explorer coverage and utilization page fetchers share one genericfetchWithRetryhelper (rate limiter plus concurrency cap); behaviour and error messages are unchanged (#120). -
aws/rds: the RDS purchase path uses the sharedpurchaseguardidempotency guard and AlreadyExists recovery, like ElastiCache. Behavior for valid purchases is unchanged (#10). -
aws/memorydb: the MemoryDB purchase path uses the sharedpurchaseguardidempotency guard and AlreadyExists recovery, like ElastiCache. Behavior for valid purchases is unchanged; only the lookup-failure error wording changes to "before MemoryDB reservation purchase" (#10). -
aws/opensearch: the OpenSearch purchase path uses the sharedpurchaseguardidempotency guard and AlreadyExists recovery, like ElastiCache. Behavior for valid purchases is unchanged (#10). -
azure/cache:GetValidResourceTypesnow returns an error when the Redis cache list reaches the page cap (#249) instead of logging a warning and returning a partial SKU set. -
scripts/setup-git-secrets.sh: the database DSN detector now fires onuser:password@host(the old pattern ended at@, whichgit-secrets' word-match scan rejected before a hostname), and also coverspostgresql,mariadb, driver-suffixed schemes and empty usernames (#125). -
provider:ProviderConfigis renamed toConfig(#39).ProviderConfigstays as a type alias markedDeprecated: use Config., so existing consumers keep compiling, but consumers that run staticcheck with fatal warnings (cloud-commitments-mcp, ~86 uses, and cloud-commitments-platform, ~36 uses) fail lint with SA1019 on their nextpkgbump until they migrate toprovider.Config. Theaws,azureandgcpprovider modules andci_cd_sanity_testsalready useprovider.Config. -
httpclient: dials to the RFC 8215 local-use NAT64 prefix64:ff9b:1::/48are refused when any RFC 6052 layout (/48, /56, /64, /96) embeds a metadata IPv4 address; Teredo2001::/32stays allowed (#238). -
CI: the security-scan job runs a pinned gitleaks CLI (v8.30.1, sha256-verified) over the checkout with
.gitleaks.tomland fails on any finding (#101). -
CI: the AWS and Azure sanity workflows now fail when their secrets are missing instead of skipping and passing; fork and Dependabot pull requests are skipped explicitly. The AWS OIDC session name is now
cc-go-sanity(#2). -
common.HoursPerMonth: exported 730-hour average month (HoursPerYear / 12) next toHoursPerYear; the remaining bare 730 and 8760 literals in the aws and gcp providers use the shared constants (#109). -
gcp: region-lookup permission errors are now classified by type only (googleapi 403 or gRPCPermissionDenied) through the sharedIsPermissionError. gRPCPermissionDeniedis now skipped with a warning like a 403, and an untyped error whose text merely contains "403" and "permission" now propagates instead of being skipped (#6). -
BREAKING
gcp: stored contexts removed (#6).computeengine.NewClientno longer takes acontext.Context(it was stored and never read), andcomputeengine.BillingService.ListSKUsnow takesctxas its first parameter, so custom implementations must add it.ProviderandRecommendationsClientAdapterno longer store a context (unexported fields; no API change). Downstream call sites to update: mcptools/gcp_computeengine_cud_safeguards_test.go:76,125,tools/search_dedupe_test.go:344,cmd/cudly-mcp/gcp_purchase_protocol_test.go:102; platforminternal/purchase/gcp_history_cost_test.go:48. No cli, mcp or platform code referencesBillingService,ListSKUsor agcp.Providerliteral. -
azure: managedredis and synapse reservations now get the same generated display name as the other services (redis-<region>-<sku>-<n>x-<term>-<payment>-...andsynapse-...) instead of the fixedAzure Cache for Redis Reservation - <sku>/Synapse SQL Pool Reservation - <sku>. This also removes the stray space before the idempotency marker on managedredis. Idempotency lookup keys on the tag or the marker suffix, not the base name (#538). -
BREAKING
gcp: Cloud SQL and Memorystore no longer return CUD recommendations (#294). They came from the project-scopedPerformanceRecommenders, which give per-instance tuning advice, and were relabeled as 1yr CUDs with made-up or zero savings. Cloud SQL and Memorystore CUDs are spend-based, and Google recommends them only at Cloud Billing account scope (seedocs/design/gcp-spend-commitments.md).GetSupportedServicesnow returns onlyServiceCompute, the default recommendation fan-out queries only the Compute Engine commitment recommender, and an explicit Cloud SQL or Memorystore request returns an error wrappingcommon.ErrCommitmentPurchaseNotSupportedwithout calling the Recommender API.SetRecommenderClienton both clients is now a deprecated no-op. Pricing (GetOfferingDetails,ValidateOffering) is unchanged. -
BREAKING
ladder.Allocateconverts the on-demand-equivalent gap to commitment dollars (go#349). Tranches were bought 1:1 against an on-demand-priced gap, over-buying by 1/rate (a 30% discount bought 43% too much).AllocationInput.InFlightUSDPerHour *float64is replaced byInFlight []InFlightTranche(commitment USD/hr plus the rate stored with each tranche, netted as commitment/rate) and a requiredCommitmentRatesmap (a partial map is fine: a layer that receives an allocation without a rate is held, never bought 1:1; a rate outside (0, 1] or non-finite is an error).Allocation.GapUSDPerHouris now commitment USD/hr, withOnDemandEquivalentUSDPerHourandRatealongside; the per-run cap applies to commitment dollars. The base split now also counts in-flight base commitment against the stable level.BuildTranchesrejects anAllocationwithout a rate in (0, 1] and stamps it onTranche.CommitmentRate. The amounts are exact; the purchase boundary must round commitment DOWN. Tranches persisted before this change are in the old unit and carry no rate. Consumer: the platform'sinternal/server/handler_ladder.goonly. -
BREAKING
ladder.TrancheResult.BuyNowremoved;BuildTranchesnow emits aTranchefor every ramp step, includingAfterDays == 0(itsFireAfterequalsTrancheInput.Now). The immediate step was returned as aBuyNowaction that the platform never persisted, so in-flight netting could not see it (#36). The only consumer is the platform'sinternal/server/handler_ladder.go; cloud-commitments-cli and cloud-commitments-mcp do not reference it. Buy-now rationale and data sources are no longer carried on the immediate step (tranches have no such fields).Tranche.AmountUSDPerHourandAllocationInput.InFlightUSDPerHournow document their unit (#349). -
Breaking (source):
insurance.Config.APIKeyis now an opaqueinsurance.Secret(build it withinsurance.NewSecret(key)) instead of astring. The key lives in a closure, so aConfigheld in an unexported struct field no longer prints it under any fmt verb, json, xml, yaml or slog.Config.Formatis removed (%vof aConfignow prints{[redacted] <org>}), and two copies of oneConfigare no longerreflect.DeepEqual.Configis also no longer comparable (==stops compiling and aConfigmap key panics as unhashable), andjson.Unmarshalinto aConfigcarrying anAPIKeynow errors where the oldjson:"-"tag ignored it (#330) -
Breaking (source):
azure.PartialResultErrorgained aKindsfield (#670). Downstream code that builds it with unkeyed composite literals no longer compiles; keyed literals such asPartialResultError{Skipped: ...}are unaffected and read asSkipUnpriceable. -
Breaking (source):
common.PurchaseResultgained anExistingCommitmentfield. Downstream code that builds it with unkeyed composite literals no longer compiles; keyed literals are unaffected. The field is true when a provider adopted a commitment that already existed for the same idempotency token (#211) -
Breaking (source):
common.PurchaseResultgained anOfferingClassfield. Downstream code that builds it with unkeyed composite literals no longer compiles; keyed literals are unaffected. On an adopted re-drive it carries the offering class AWS reports for the existing RI (the EC2 provider sets it in a follow-up change); empty otherwise (#211). -
aws/EC2: an adopted re-drive now reports the existing RI's offering class inPurchaseResult.OfferingClass(#211). -
GCP Compute Engine adoption now returns a nil
Costinstead of a pointer to 0; a fresh purchase is unchanged (#211).
-
Duplicate check: Azure Advisor SQL Database reservation rows now carry
Enginesqlserverand the SKU asInstanceClass, like the SQL client's own recommendations. They had no details, so their dedupe key never matched a held SQL commitment and the held reservation did not reduce them. Savings Plan rows keep no details, as before, and the MemoryDB details are now attached only to reservation rows (no real Advisor row is affected) (#735). -
Duplicate check: the Azure SQL Database dedupe key no longer includes the zone-redundancy slot, so a held reservation stamped
zoneRedundantnow covers a recommendation stampednoneor empty, and the reverse. AvCorereservation covers standard compute either way; the slot only split one pool in two and let duplicates through when the two sides disagreed. It can only over-filter: a heldvCore ZRadd-on reservation now counts toward the same pool (#715). -
Duplicate check: a recent purchase now reduces every term and payment variant of a recommendation, not only the first one in input order, for EC2 as well as every other service. Each
term|paymentgroup starts from the full held quantity and consumes it in input order, so one held unit against four variants of 2 yields 1, 1, 1, 1 (was 1, 2, 2, 2). Providers pick this up on their nextpkgpin bump. Fixes #703. -
providers/gcp:m4-ultramem-224now matches its ownM4Ultramem224catalog rows instead of the plainM4rates, which understated RAM cost by about 25%. The commitment SKU text (Commitment v1: M4Ultramem224 Cpu/Ram in ...) is assumed by analogy with other families and not verified against the live catalog; a wrong text fails closed with "no commitment pricing found". Other M4 ultramem shapes (m4n-*and unknown sizes) are refused with an unknown-catalog-family error. Them1,m2andm3ultramem shapes are unchanged. Fixes #719. -
common: aRecommendation, and aPurchaseResultcarrying one (with noError), now decodes from JSON whendetailsholds an object.detailsis decoded into the typed pointer for the recommendation's service throughDecodeServiceDetailsFor; absentdetailsleave the field unchanged and null sets it nil, as encoding/json does, and details for a service with no typed details fail withErrUntypedServiceDetails. Before this, encoding/json could not decode an object into theServiceDetailsinterface (#211). -
CostKnownproducers (#74): providers now setcommon.Commitment.CostKnownwhere the price is reported, and stop inventing inventory fields.- AWS ladder RI costs:
CostKnownis true only when EC2 reported the price (FixedPrice plus a recurring price) and either a Duration is present or the upfront is 0. - AWS ladder SP costs:
CostKnownis true. - Azure savings plans: a reported
Commitment.Amount, including 0, is marked known. - Azure inventory: the cache and search consumption inventory no longer invent
RegionorState. Both are now empty, meaning unknown. Cosmos DB, SQL and Synapse moved to the Reservations API in #717. - Known log noise: an empty
Statestill logs one recfilter WARNING per such commitment.
- AWS ladder RI costs:
-
azurereservation purchases: compute, cosmosdb, database, managedredis, search and synapse now refuse a purchase before any API call, wrappingcommon.ErrRefusedBeforePurchase, when the recommendation region (empty, or not the same region as the client after lowercasing and removing spaces) does not match the client region. Previously the reservation was bought in the client region regardless ofrec.Region(#704). -
Breaking (source):
managedredis.ReservationsDetailsPagerand(*managedredis.Client).SetReservationsPagerare removed; tests inject reservation inventory withSetInventoryFactoriesinstead (#699). -
azureRedis:managedredis.GetExistingCommitmentsnow reads held reservations from the Reservations API (purchase date, quantity, location, state), so a Redis reservation bought in the last day reduces the matching recommendation instead of being invisible to the duplicate guard (#699). An incomplete live reservation row (missing SKU, location, quantity or purchase date, an unattributable scope, or an expiry falling today) now fails the lookup with an error instead of being skipped, asazurecompute already does. -
Breaking (source):
cosmosdb.ReservationsDetailsPagerand(*cosmosdb.Client).SetReservationsPagerare removed; tests inject reservation inventory withSetInventoryFactoriesinstead (#705). -
azureCosmos DB:cosmosdb.GetExistingCommitmentsnow reads held reservations from the Reservations API (purchase date, quantity, location, state), so a Cosmos DB reservation bought in the last day reduces the matching recommendation instead of being invisible to the duplicate guard.Regionis now the reservation location, not the client region. An incomplete live reservation row fails the lookup with an error instead of being skipped (#705). -
Breaking (source):
synapse.ReservationsDetailsPagerand(*synapse.Client).SetReservationsPagerare removed; tests inject reservation inventory withSetInventoryFactoriesinstead (#705). -
azureSynapse:synapse.GetExistingCommitmentsnow reads held reservations from the Reservations API (purchase date, quantity, location, state), so a Synapse reservation bought in the last day reduces the matching recommendation instead of being invisible to the duplicate guard.Regionis now the reservation location, not the client region. An incomplete live reservation row fails the lookup with an error instead of being skipped (#705). -
Breaking (source):
database.ReservationsDetailsPagerand(*database.Client).SetReservationsPagerare removed; tests inject reservation inventory withSetInventoryFactoriesinstead (#705). -
azureSQL Database:database.GetExistingCommitmentsnow reads held reservations from the Reservations API (purchase date, quantity, location, state), so a SQL Database reservation bought in the last day reduces the matching recommendation instead of being invisible to the duplicate guard. Held reservations carry enginesqlserverand the subscription's dominant zone-redundancy setting, as recommendations do.Regionis now the reservation location, not the client region. An incomplete live reservation row fails the lookup with an error instead of being skipped (#705). -
awsRI recommendations: a fractional Cost Explorer quantity now rounds down instead of to the nearest integer, so a purchase never exceeds CE's quantity. The upfront, recurring monthly cost, monthly savings and savings percentage scale bycount / quantity, so they describe the instances actually bought. Before, 2.5 became 3 with the upfront for 2.5, and the approved-upfront check refused the purchase. The on-demand baseline and break-even months are unchanged (#635). -
AWS: purchase counts outside [1, MaxInt32] are refused before any API call in EC2, ElastiCache, MemoryDB, OpenSearch, RDS and Redshift, with
ErrRefusedBeforePurchaseandofferingprice.ErrPurchaseCountOutOfRange(#683). -
azure: an error from a reservation purchase retry (attempt 2 or later) now also wrapscommon.ErrOutcomeUnknown. That covers a currency refusal from the retry's calculatePrice, a failed idempotency re-check, a non-retryable purchase failure, a cancelled retry wait and exhausted attempts. A retry runs only after a purchase that failed retryably and may have committed, and the re-check can miss that order, so none of these errors may read as "nothing was bought". The cause stays in the chain, soerrors.Is(err, common.ErrCurrencyNotAccepted)still matches. TheErrCurrencyNotAccepteddoc now says it is pre-purchase only withoutErrOutcomeUnknown(#671). -
Azure purchases check the recommendation currency. Azure reservation purchases compare the calculatePrice billing currency with the recommendation currency (empty means USD) and refuse before purchasing when it is missing or differs. Azure Savings Plan purchases refuse when the recommendation currency (empty means USD) differs from the plan commitment currency.
-
Azure reservation purchases and compute exchanges for accounts billed in a currency other than USD are now refused (recommendations are USD-only). The purchase check also runs again when a purchase is retried after a session timeout.
CalculateExchangerefuses, so no exchange session is returned to execute, when the net payable currency is missing or is not USD. -
gcp: the Cloud SQL, Memorystore and Cloud Storage price extractors keep 1-year and 3-year commitment SKUs in separate slots and return the price for the requested term, so a catalog that lists both terms no longer fails as ambiguous (#254). Two SKUs of the same term at different prices still fail closed, and a recommendation term other than 1yr or 3yr (for example "2yr") now errors instead of being priced as one year. -
ladder: a run no longer emits a buffer-layer purchase for a layer it also recommends reshaping. The buffer allocation becomes an informational Hold naming the pending reshape; the reshape and all other allocations are unchanged. A layer that also carries the base or flex role (merged Azure reservation) is not held (#30). -
ladder: a run no longer emits a buffer-layer purchase for a layer it also recommends reshaping. The buffer allocation becomes an informational Hold naming the pending reshape; the reshape stays. Other allocations are not edited, but with the held buffer removed a bindingMaxHourlyCommitPerRuncan give base/flex a larger share of the cap (total spend never higher), and a freed max-actions slot can let a deferred purchase through. A layer that also carries the base role (merged Azure reservation) is not held (#30). -
azure: recommendations from the Consumption path (compute, database, cache, cosmosdb, managedredis, synapse) and the Savings Plan benefit path now setCommitmentCurrencytoUSD, because their commitment amounts come from sources already required to be USD. The legacy path carries no checked commitment amounts and stays empty (#623); Advisor reservation rows are priced separately, see below. -
azure: Advisor reservation recommendations (virtual machines, SQL databases, Redis cache) are now priced from the Retail Prices reservation rows, so they carryCommitmentCostorRecurringMonthlyCostandCommitmentCurrencyUSDon both payment legs. A row that cannot be priced is skipped and counted in the partial-result error. An Advisor reservation row is dropped when the Consumption path returned a reservation for the same account, service, region and term, and Advisor regions are normalized to ARM form (eastus). Duplicate Advisor reservation rows for the same account, service, region and term collapse to the cheapest priced total. Advisor Savings Plan rows stay unpriced, and CosmosDb and SqlDataWarehouse Advisor rows are still ignored because their quantity unit is unproven (#655). -
azure: Advisor compute reservation rows for a different VM series are no longer dropped by a Consumption row or collapsed into a cheaper Advisor row for the same region and term; the series (D,E,B...) is now part of the match. A row whose series cannot be read keeps the previous per-region, per-term match on both sides, so one need never shows as two rows. Series that share several size flexibility groups (D,E,M) still merge (#674). -
AWS/OpenSearch: the STS account-ID lookup used for post-purchase tagging caches only a successful result, so one throttled or timed-out
GetCallerIdentityno longer disables tagging for the client's lifetime (#53). -
AWS/recommendations:
normalizeRegionNamemaps the currentEurope (Ireland|Frankfurt|London|Paris|Stockholm)Cost Explorer display names to region codes, so coverage lookups keyed by region code match those recommendations (#53). -
AWS/recommendations:
normalizeRegionNamealso mapsAsia Pacific (Malaysia|Thailand),Canada West (Calgary)andMexico (Central)toap-southeast-5,ap-southeast-7,ca-west-1andmx-central-1(#53). -
AWS/recommendations:
normalizeRegionNamealso mapsAsia Pacific (Taipei)(ap-east-2),Asia Pacific (New Zealand)(ap-southeast-6) and the China, GovCloud, ISO and European Sovereign Cloud region descriptions from the SDK partitions file (#53). The alias table is now a package-levelregionAliasesmap, the no-op region-code prefix branch is removed (output unchanged for every input), and the collision test covers the whole map. -
AWS/Savings Plans:
GetExistingCommitmentsreturns an error naming the plan ID and value when a plan has an unparseable start or end date, instead of silently leaving the date zero (#53). -
AWS/Savings Plans:
GetExistingCommitmentsreturns an error naming the plan ID when a plan has no start or end date; a zero start date was never treated as recent by the purchase dedupe, so a duplicate purchase could slip through (#53). -
AWS/RDS:
PurchaseCommitmentrefuses an offering whose upfront (FixedPricex count) exceeds the approved recommendation upfront by more than 5%, or that is priced in a non-USD currency; a zero approved upfront is not checked (#53). -
Bump Go to 1.26.10 to pick up the fix for GO-2026-6809 (crypto/x509), which
govulnchecknow reports against 1.26.9. -
AWS/MemoryDB, OpenSearch, Redshift:
PurchaseCommitmentrefuses an offering whose upfront (FixedPricex count) exceeds the approved recommendation upfront by more than 5%, or that is priced in a non-USD currency (MemoryDB offerings carry no currency, so only the recommendation's currency is checked there), or, for MemoryDB, an upfront offering with a zero price or one bought in a China region (cn-north-1, cn-northwest-1), where the assumed USD price is wrong; a zero approved upfront is not checked. All AWS RI price guards (EC2, RDS, MemoryDB, OpenSearch, Redshift) also refuse a NaN or infinite approved upfront (#53). -
AWS ElastiCache purchases refuse a recommendation with no engine instead of sending an empty
ProductDescriptionfilter, send the engine lowercased as ElastiCache reports it, and verify the chosen offering's node type, duration and engine before buying it (#53). -
AWS EC2 RI purchases refuse an offering whose total upfront exceeds the recommendation's approved upfront by more than 5% (USD only; a recommendation without an upfront figure is not checked), so a repriced or differently-priced offering can no longer bypass the approved cap (#53).
-
ReshapeBufferrejects nil payment caps instead of mapping them tomath.MaxFloat64:MaxPaymentPerExchangeUSDandMaxPaymentDailyUSDare now required, so an omitted cap can no longer mean unlimited spend on the auto-exchange path (#83). -
aws/recommendations:GetRecommendationsfor RI services now rejects an unrecognized term, payment option or lookback period instead of letting Cost Explorer apply its own defaults under the requested label; the deprecated silent converters are removed (#53). -
aws/recommendations:GetRecommendationsfor RI services now rejects a non-empty unrecognized term, payment option or lookback period instead of letting Cost Explorer apply its own defaults under the requested label; an unset (empty) value still means the Cost Explorer default. The deprecated silent converters are removed (#53). -
Azure savings plan recommendations use a 730 hour month (was 730.5), matching
common.HoursPerMonthand the 8760 hour year; monthly recurring cost, on-demand cost and savings shift by about 0.07% (#583). -
exchange.AnalyzeReshaping: no longer proposes a same-capacity reshape (1x m5.24xlarge to 2x m5.12xlarge); it frees no units and AWS needs an equal-or-greater exchange value, so such RIs now get no recommendation and no quote (#563). -
common.EC2NormalizationFactor: knows the 32xlarge, 48xlarge, 56xlarge, 96xlarge and 112xlarge sizes (256 to 896), so RI exchange analysis no longer skips RIs of those sizes with "No normalization factor known" (#563). -
exchange:
RunAutoExchangenow stops and returns the error when cancelling stale pending exchanges fails, instead of logging it and creating new exchanges while a stale pending one may still be approvable. -
exchange:
RunAutoExchangenow rejects any mode other thanautoormanual(including empty) with an error before any store or exchange call; previously every non-manualvalue auto-executed exchanges. -
common:
BuildReservationNameno longer drops its random uniqueness suffix when a long SKU overflows the 60-char cap (it dropped it first, so long-SKU purchases got deterministic, colliding names). The timestamp and payment segments drop first, then the SKU is truncated; the suffix is always kept (#123). -
logging:
SetOutputno longer races withLogger.Withand concurrent log calls: the unsynchronized mirror of the writer is gone and the destination lives only in the mutex-guardedlog.Logger(#123). -
common:
DecodeServiceDetailsFornow rejects a non-empty details payload for a service with no typed details instead of silently returning nil details; MemoryDB (whose recommendations carryCacheDetails) now decodes asCacheDetails(#123). -
common:
DecodeServiceDetailsFornow rejects a non-empty details payload for a service with no typed details instead of silently returning nil details; MemoryDB (whose recommendations carryCacheDetails) and NoSQL (Azure Cosmos DB,NoSQLDetails) now decode to their typed details, so MemoryDB rows without a payload decode to a zero*CacheDetailsinstead of nil (#123). -
common:
DecodeServiceDetailsFornow rejects a non-empty details payload for a service with no typed details instead of silently returning nil details; MemoryDB (whose recommendations carryCacheDetails) and NoSQL (Azure Cosmos DB,NoSQLDetails) now decode to their typed details, so MemoryDB rows without a payload decode to a zero*CacheDetailsinstead of nil. The rejection wrapsErrUntypedServiceDetailsforerrors.Is(#123). -
retry: jittered backoff is now clamped to
MaxDelayafter jitter is applied (upward jitter could exceed it), and theDo/PerAttemptTimeoutdocs now say the per-attempt context is derived from, and cancelled with, the outer context (#123). -
aws/ec2:
GetOfferingDetailsalways priced the convertible offering and its doc comment wrongly said the class does not change prices. AddedGetOfferingDetailsForClassto price the class the purchase uses;GetOfferingDetailskeeps the convertible default (#570). -
A Multi-AZ size-flexible RDS reservation now covers Single-AZ demand in the duplicate-purchase guard (#323): it counts as twice the size units of its family, so a recent Multi-AZ db.r6g.large PostgreSQL reservation suppresses two Single-AZ db.r6g.large recommendations. Single-AZ never covers Multi-AZ, and exact-class engines such as SQL Server keep their key.
-
Size-flexible RDS reservations now cover Single-AZ and Multi-AZ demand in the duplicate-purchase guard (#323): Single-AZ and Multi-AZ share one family budget in size units, and a Multi-AZ node costs twice the units. A recent Multi-AZ db.r6g.large PostgreSQL reservation suppresses two Single-AZ db.r6g.large recommendations, and a Single-AZ db.r6g.xlarge suppresses one Multi-AZ db.r6g.large. Exact-class engines such as SQL Server keep their key.
-
The duplicate-purchase guard now treats regional Linux/Unix default-tenancy EC2 reservations as size-flexible (#323): a recent m5.xlarge reservation suppresses two m5.large recommendations in the same region, counted in normalized units. Zonal, Windows/RHEL/SUSE, dedicated, wildcard and unknown-size entries stay exact-type, as do the families AWS excludes from size flexibility (G4ad, G4dn, G5, G5g, G6, G6e, G6f, Gr6, Gr6f, hpc7a, P5, Inf1, Inf2, u7i-6tb, u7i-8tb). A partly spent reservation is never credited as a whole node.
-
Unbounded AWS pagination loops now stop at a page cap and honour cancellation (#561): the existing-commitment and valid-resource-type scans for ElastiCache, RDS, OpenSearch, Redshift and EC2 check the context before each page and fail with an error after 50 pages instead of looping forever. The Savings Plans existing-commitment scan now errors at its page cap instead of returning a partial list to the duplicate-purchase guards.
-
Duplicate guard counts an existing RDS reservation against other sizes of the same family (#323): an AWS RDS reservation now pays for recommendations of any size in the same instance family, engine, region and deployment, by normalized units, instead of matching only the exact instance class. Only MySQL, MariaDB, PostgreSQL and Aurora are size-flexible; SQL Server, Oracle (until both paths carry the license model, #342), Db2 and unknown engines still match the exact class.
-
aws/savingsplans: Savings Plans offering lookup, validation and purchase now fail withErrUnsupportedPartitionbefore any AWS call forcn-*(aws-cn) regions, whose plans are priced in CNY while the purchase path is USD-only;awsandaws-us-govare unchanged (#7). -
MemoryDB reserved-node and offering listings are page-capped and cancellable (#556):
GetExistingCommitmentsandGetValidResourceTypesnow check the context per page and fail loudly after 50 pages instead of looping on a NextToken that never ends. -
exchange: reshape no longer treatsmetalas a fixed 192-unit size: it is removed from the size table, soNormalizationFactorForSize("metal")returns 0 (unknown), no recommendation targets a*.metaltype, and a metal RI without a caller-suppliedNormalizationFactoris reported as skipped with a reason instead of being sized as 192 units or dropped silently (#552). -
exchange: reshape recommendations no longer propose a target with more normalized capacity than the source RI owns (3xm5.xlargeat 80% used to become 2xm5.2xlarge, 32 units against 24 owned);findBestFitnow steps down to a smaller size when rounding up would exceed the owned units, and skips the RI when nothing fits (#551). -
azure: computePurchaseCommitmentnow checks the term before the payment option and returns the term error unwrapped (nofailed to marshal request:prefix), like the other six services (#538). -
azure: synapse's zero-quantity purchase error now includes the quantity (..., got 0), like the other six services (#538). -
azure:PurchaseCommitmentfor cache, compute, cosmosdb, database, managedredis and search now rejects a blank resource type before any HTTP call, as synapse already did, instead of sending an emptysku.name(#538). -
azure: The sharedCUDLY_MAX_PARALLELISMslot is now held per outbound call (page fetch, pricing page, SKU or capability request) instead of across a whole per-service sweep, so a long Azure sweep no longer starves AWS and GCP calls (#8). -
scorer:MinCountno longer applies to Savings Plans. TheirCountis always 1 (the commitment is dollar-denominated), so--min-count 2dropped every Savings Plan recommendation. -
aws/recommendations: RDS family-NU sizing and the expiry deduction no longer pool SQL Server by instance family (its RIs are never size-flexible): those recs go to the per-size path and expiring SQL Server RIs count asUnusable. Oracle stays pooled until the license model is carried (#342). -
aws/recommendations:FamilyExpiryReport.ExpiringNUnow reports the NU actually removed from family covered NU (before minus after) instead of the expiring RIs' own NU, so it is 0 when underutilized RIs absorb the expiry (#342). -
awsandazure/savingsplansrecommendations: a Cost Explorer RI or Savings Plan detail with an explicit non-USDCurrencyCode, and an Azure benefit recommendation whosecurrencyCodeis not USD, are skipped instead of passing non-USDOnDemandCost/EstimatedSavingsthrough as USD. AWS reports each skip inIncompleteRecommendationsError(wrapsrecommendations.ErrNotUSD); Azure records it in the collection's skip log, surfacing asPartialResultError. An absent AWSCurrencyCodeis unchanged (#124) -
aws/recommendations: the Savings Plans coverage and utilization and on-demand Cost Explorer fetchers now hold the shared concurrency slot around each call, so they count towardCUDLY_MAX_PARALLELISM(#8). -
azure: the Retail Prices$filtervalues built from SKU and region in the search, compute, cosmosdb and savingsplans clients are now validated withpricing.ODataString, so an injected value is rejected before any request is built (#526). -
reporter.RenderSummarynow lists filtered-reason categories in sorted order instead of random map order (#110). -
gcp/computeengine: a Recommender cost projection with a non-zero cost and a non-USD or missing currency code is now skipped with a logged warning instead of being reported as USDEstimatedSavings. -
azure/compute: reservation exchange targets are no longer always billed Upfront.ExchangeTarget.BillingPlan(new, optional) is sent as each target'sbillingPlanin the CalculateExchange body, so the session thatExecuteExchangecommits carries it. Unset keeps the previous Upfront request byte for byte; anything other thanUpfrontorMonthly(exact spelling) is a validation error before any request (#89). -
ladder.Allocateno longer subtracts existing commitment from a residual baseline a second time (#282). NewUsageBaseline.Basis(BaselineBasisTotal, the default including the empty value, orBaselineBasisResidual). A residual baseline counts only usage existing commitments left uncovered, so Allocate sizeslow_water * target% - in_flight(never above the residual) instead oflow_water - existing, which held a $4/hr residual with a $6/hr Savings Plan as "target already met". A residual baseline with a target below 100% and existing commitment is held (the gap needs the covered usage the residual excludes). Expiring commitment is not added back on that basis. Unknown basis values are an error. Additive: total-basis results are unchanged. The AWS ladder baseline now declares the residual basis (its series counts only uncovered On Demand Instances usage), so a partly covered AWS account is no longer held as "target already met"; with a target below 100% and existing commitment it is held ("residual baseline cannot size a partial target") instead of being undersized by subtracting that commitment from the residual again. Part of #282. -
azure/savingsplans: the commitment currency is no longer hardcoded to USD.SavingsPlanDetails.Currency(new, additive) is sent in the purchase and validate bodies and returned byGetOfferingDetails; empty or malformed values (anything but 3 ASCII letters; format-only, not ISO 4217 membership) fail closed (#46). -
pkg/insurance: hardened vendor-text handling inClienterrors (#286). The sanitizer now also strips format, line and paragraph separator characters (Unicode Cf, Zl, Zp) from vendor text; other invisible characters (for example U+034F, variation selectors, U+3164) are not stripped and can still split an echoed key, which is whyNewClientnow rejects any API key that is not printable ASCII (0x21-0x7E).ComparisonandPlandecode errors go through the same strip, redact and truncate pipeline asHTTPError.Message, and aRetry-Afterbeyond the int range is capped at 24 h instead of ignored. -
pkg/insurance: decode errors no longer echo an uncapped vendor value (#336). A rejected enum value is shown only when it is a short identifier ([A-Za-z0-9_.-], up to 32 bytes); anything else is reported as(N bytes, not shown), so a hostile response cannot flood logs or inject text into an error that a consumer prints or returns to an LLM. -
Units change (Azure Consumption recommendations): the cost fields were amounts over the API's lookback window but were treated as monthly figures and term totals (#267). For recommendations from the Consumption ReservationRecommendations API (compute, database, cache, cosmosdb, synapse, managedredis; the Advisor path is unchanged):
OnDemandCostandEstimatedSavingsare now monthly run-rates (window amount x 30.4375 / lookback days; savings is net of overage);CommitmentCostis now Count x the reservation retail price for the term (upfront variant) or 0 (monthly variant);RecurringMonthlyCostis now 0 (upfront) or Count x price / term months (monthly), never the window total. The lookback is requested explicitly (Last7Days) and read from the response. A recommendation with a missing or unknown lookback or term, a non-USD modern currency, or no resolvable reservation price is skipped with an error log instead of being returned with guessed figures. A price lookup that fails to complete (transport error, HTTP error status, cancelled or expired context) instead fails the whole service'sGetRecommendationscall. Consumers that cache or compare these fields (caps, payback, savings thresholds) will see different magnitudes after upgrading. In this repo,pkg/reporter/reporter.go(lines 44, 96) andpkg/common/audit.go(line 301) useCommitmentCostand will now see 0 for monthly variants; the platform'srecTotalCommitmentmust addRecurringMonthlyCostx term months to get the true total commitment of a monthly variant. Those consumers are not changed here -
Align pre-commit gocyclo threshold (10) with CI pipeline
-
Pin tool versions in GitHub Actions for reproducible builds
-
The local git-secrets setup script aborted on its PEM pattern and, when patched past that, made every scan fail on an invalid regex; its keyword allowlist whitelisted whole lines containing common Go/Terraform tokens. Allowlisting now lives in
.gitallowedas literal entries and the PEM detector covers PKCS#8 keys (#1972)
- BREAKING:
providers/azure/services/cacheno longer exportsRecommendationsPager,(*Client).SetRecommendationsPageror(*Client).ReservationUnitPrice. The cache client no longer collects or prices recommendations; managedredis does. No cli, mcp or platform code references them. (#692) - BREAKING:
common.Matches(pkg/common) is removed. It compared only Provider, Region, Service, ResourceType and engine and had no callers in this repo, the CLI, the MCP server or the platform; usepkg/recfilterfor dedupe. (#123) - BREAKING: the
pkg/errorspackage is removed (no importers). Removed exported symbols:NotFoundError,ValidationError,AuthenticationError,AuthorizationError,ConflictError,RateLimitError,ServiceError, theirNew*/Is*helpers (NewNotFoundError,NewNotFoundErrorWithMessage,IsNotFoundError,NewValidationError,IsValidationError,NewAuthenticationError,IsAuthenticationError,NewAuthorizationError,IsAuthorizationError,NewConflictError,IsConflictError,NewRateLimitError,IsRateLimitError,NewServiceError,IsServiceError) and the sentinelsErrNotFound,ErrUnauthorized,ErrForbidden,ErrInvalidInput,ErrConflict,ErrInternalError. (#123) - BREAKING: the
pkg/configpackage is removed (no importers). Removed exported symbols:Load,DefaultScorerConfig,Config,AWSConfig,AzureConfig,GCPConfigandServerConfig. Thepkgmodule no longer depends ongithub.com/spf13/pflag. (#123)
Releases before the split of this repository (0.9.0 and earlier) are not listed here; they described the combined product and live in the history of cloud-commitments-cli.