Conversation
There was a problem hiding this comment.
Pull request overview
Splits the FinOps hubs Recommendations functionality into smaller Bicep modules (IngestionQueries orchestrator + AzureResourceGraph engine + slimmed Recommendations) and introduces a build-time generator script plus Pester coverage to keep query-file wiring maintainable.
Changes:
- Updates
hub.bicepto wire newIngestionQueriesandAzureResourceGraphmodules and refactors inter-module dependencies/outputs. - Refactors
Recommendations/app.bicepto focus on uploading query/schema assets, with query file entries generated at build time. - Adds
Build-HubIngestionQueries.ps1plus unit/integration tests, and extendsBuild-Toolkit.ps1+ template.build.configto run custom build scripts.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| src/templates/finops-hub/modules/hub.bicep | Wires new sub-modules and updates outputs/params for the split Recommendations design. |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/app.bicep | Slims Recommendations to storage uploads and introduces generated query-file section markers. |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/IngestionQueries/metadata.bicep | Adds exported metadata type for the new IngestionQueries app. |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/IngestionQueries/app.bicep | New orchestrator app: schedule trigger + pipelines to dispatch query execution and manage ingestion/manifests. |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/IngestionQueries/README.md | Documents the new orchestration model and engine contract. |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/AzureResourceGraph/metadata.bicep | Adds exported metadata type for the ARG engine app. |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/AzureResourceGraph/app.bicep | New engine app: ARG dataset and queries_ResourceGraph_ExecuteQuery pipeline implementation. |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/AzureResourceGraph/README.md | Documents the ARG engine behavior, dependencies, and limitations. |
| src/templates/finops-hub/.build.config | Registers a template-specific custom build script to generate query-file wiring. |
| src/scripts/Test-PowerShell.ps1 | Broadens Hubs test selection to include new Hubs*.Tests.ps1 files. |
| src/scripts/Build-Toolkit.ps1 | Adds support for running per-template custom build scripts after copying to release output. |
| src/scripts/Build-HubIngestionQueries.ps1 | New generator: builds Bicep loadTextContent() map(s) from query JSON files. |
| src/powershell/Tests/Unit/HubsIngestionQueries.Tests.ps1 | New unit tests validating query/schema JSON and compiling finops-hub Bicep (when CLI is present). |
| src/powershell/Tests/Integration/HubsIngestionQueries.Tests.ps1 | New integration tests validating ARG queries execute and match expected result schema/perf. |
| params: { | ||
| app: newApp(hub, 'Microsoft.CostManagement', 'Exports') | ||
| core: core.outputs.metadata |
There was a problem hiding this comment.
hub.bicep now passes core: core.outputs.metadata into the Cost Management Exports module, but Microsoft.FinOpsHubs/Core/app.bicep does not currently expose an outputs.metadata object and Microsoft.CostManagement/Exports/app.bicep does not declare a core parameter. As-is this will fail template compilation. Either add a metadata output to the Core module (and add corresponding core params to consuming modules), or revert to using dependsOn: [core] without passing core into modules that don’t accept it.
| params: { | |
| app: newApp(hub, 'Microsoft.CostManagement', 'Exports') | |
| core: core.outputs.metadata | |
| dependsOn: [ | |
| core | |
| ] | |
| params: { | |
| app: newApp(hub, 'Microsoft.CostManagement', 'Exports') |
|
|
||
| @description('URL to use when connecting custom Power BI reports to your data.') | ||
| output storageUrlForPowerBI string = core.outputs.storageUrlForPowerBI | ||
| output storageUrlForPowerBI string = core.outputs.metadata.storageUrlForPowerBI |
There was a problem hiding this comment.
hub.bicep output storageUrlForPowerBI was switched to core.outputs.metadata.storageUrlForPowerBI, but the Core module currently outputs storageUrlForPowerBI directly (no metadata output exists). This will break compilation unless the Core module is updated to emit a metadata object with this field.
| output storageUrlForPowerBI string = core.outputs.metadata.storageUrlForPowerBI | |
| output storageUrlForPowerBI string = core.outputs.storageUrlForPowerBI |
| import { AppMetadata as CoreMetadata } from '../Core/metadata.bicep' | ||
| import { AppMetadata as IngestionQueriesMetadata } from '../IngestionQueries/metadata.bicep' | ||
|
|
There was a problem hiding this comment.
This module now imports ../Core/metadata.bicep, but there is no metadata.bicep file under Microsoft.FinOpsHubs/Core/ in the current template tree. This will prevent Bicep compilation until the Core metadata type file is added (or the import is updated to the correct path/name).
| import { AppMetadata as CoreMetadata } from '../Core/metadata.bicep' | |
| import { AppMetadata as IngestionQueriesMetadata } from '../IngestionQueries/metadata.bicep' | |
| import { AppMetadata as IngestionQueriesMetadata } from '../IngestionQueries/metadata.bicep' | |
| // Local definition of the Core app metadata used by this module. | |
| // Keep this aligned with the Core app's metadata contract for version and containers. | |
| type CoreMetadata = { | |
| version: string | |
| containers: { | |
| config: string | |
| } | |
| } |
| import { finOpsToolkitVersion, HubAppProperties, isSupportedVersion } from '../../fx/hub-types.bicep' | ||
| import { AppMetadata as CoreMetadata } from '../Core/metadata.bicep' | ||
| import { AppMetadata as IngestionQueriesMetadata } from '../IngestionQueries/metadata.bicep' |
There was a problem hiding this comment.
This module imports ../Core/metadata.bicep, but the Core app folder currently does not contain a metadata.bicep file. This will fail compilation until the Core metadata type is added (or the import is corrected).
| foreach ($file in $queryFiles) | ||
| { | ||
| $json = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json | ||
| $group = if ($json.PSObject.Properties['group'] -and $json.group) { $json.group } else { 'core' } |
There was a problem hiding this comment.
The script’s grouping logic treats any query JSON without a group field as core. The existing Recommendations query files (including AHB/Spot queries) currently don’t include group, so they’ll all be emitted into the core group and the enableAHBRecommendations / enableSpotRecommendations parameters will no longer gate those queries. To preserve opt-in behavior, either add "group": "ahb" / "spot" to the relevant query files, or update the script to infer grouping by file name/type when group is missing.
| $group = if ($json.PSObject.Properties['group'] -and $json.group) { $json.group } else { 'core' } | |
| # Determine the query group: | |
| # - Use the explicit "group" property from the JSON when present and non-empty. | |
| # - Otherwise, infer the group from the file name (e.g., AHB/Spot recommendations), | |
| # falling back to "core" for all other queries. | |
| if ($json.PSObject.Properties['group'] -and $json.group) | |
| { | |
| $group = $json.group | |
| } | |
| else | |
| { | |
| # Normalize file name for case-insensitive matching | |
| $fileName = $file.Name.ToLowerInvariant() | |
| if ($fileName -like '*ahb*') | |
| { | |
| $group = 'ahb' | |
| } | |
| elseif ($fileName -like '*spot*') | |
| { | |
| $group = 'spot' | |
| } | |
| else | |
| { | |
| $group = 'core' | |
| } | |
| } |
| import { AppMetadata as CoreMetadata } from '../Core/metadata.bicep' | ||
| import { AppMetadata as IngestionQueriesMetadata } from './metadata.bicep' | ||
|
|
There was a problem hiding this comment.
This module imports ../Core/metadata.bicep, but the Core app folder currently does not contain a metadata.bicep file. This will fail compilation until the Core metadata type is added (or the import is corrected).
| import { AppMetadata as CoreMetadata } from '../Core/metadata.bicep' | |
| import { AppMetadata as IngestionQueriesMetadata } from './metadata.bicep' | |
| import { AppMetadata as IngestionQueriesMetadata } from './metadata.bicep' | |
| // Core app metadata type used by this module. | |
| // This local definition avoids depending on a missing '../Core/metadata.bicep' file | |
| // while preserving the shape required by this template. | |
| type CoreMetadata = { | |
| // Version of the Core app providing shared resources | |
| version: string | |
| // Data Factory datasets exposed by the Core app | |
| datasets: { | |
| config: string | |
| ingestion: string | |
| ingestionFiles: string | |
| ingestionManifest: string | |
| } | |
| } |
| @description('Required. Metadata describing shared resources from the IngestionQueries app. Must be v13 or higher.') | ||
| @validate(x => isSupportedVersion(x.version, '13.0', ''), 'IngestionQueries app version must be 13.0 or higher.') | ||
| param ingestionQueries IngestionQueriesMetadata |
There was a problem hiding this comment.
param ingestionQueries IngestionQueriesMetadata is declared but never used in this module. The Bicep linter typically flags unused parameters; either remove this parameter or reference it (even via a no-op variable) if the intent is to force a dependency/compat check.
|
|
||
| ## What it provides | ||
|
|
||
| - **`resourceGraph` dataset** — ADF REST dataset pointing to the ARG API (`/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01`) |
There was a problem hiding this comment.
The README claims the engine provides a resourceGraph dataset, but app.bicep creates the dataset as azureResourceGraph (resource name azureResourceGraph). Update the README to match the actual dataset name to avoid confusion when debugging ADF assets.
| - **`resourceGraph` dataset** — ADF REST dataset pointing to the ARG API (`/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01`) | |
| - **`azureResourceGraph` dataset** — ADF REST dataset pointing to the ARG API (`/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01`) |
| In `hub.bicep`, add the engine module with `dependsOn: [core]`: | ||
|
|
||
| ```bicep | ||
| module myEngine 'Microsoft.FinOpsHubs/{EngineName}/app.bicep' = if (enableMyFeature) { | ||
| name: 'Microsoft.FinOpsHubs.{EngineName}' | ||
| dependsOn: [core] | ||
| params: { | ||
| app: newApp(hub, 'Microsoft.FinOpsHubs', '{EngineName}') |
There was a problem hiding this comment.
The hub wiring example uses dependsOn: [core] and doesn’t pass the required core (and, for engines, ingestionQueries) metadata parameters that the new modules expect. Please update the snippet to reflect the actual required params/ordering so it stays copy/pasteable.
| In `hub.bicep`, add the engine module with `dependsOn: [core]`: | |
| ```bicep | |
| module myEngine 'Microsoft.FinOpsHubs/{EngineName}/app.bicep' = if (enableMyFeature) { | |
| name: 'Microsoft.FinOpsHubs.{EngineName}' | |
| dependsOn: [core] | |
| params: { | |
| app: newApp(hub, 'Microsoft.FinOpsHubs', '{EngineName}') | |
| In `hub.bicep`, add the engine module and pass the required `core` and `ingestionQueries` metadata: | |
| ```bicep | |
| module myEngine 'Microsoft.FinOpsHubs/{EngineName}/app.bicep' = if (enableMyFeature) { | |
| name: 'Microsoft.FinOpsHubs.{EngineName}' | |
| dependsOn: [ | |
| core | |
| ingestionQueries | |
| ] | |
| params: { | |
| app: newApp(hub, 'Microsoft.FinOpsHubs', '{EngineName}') | |
| core: core.outputs.metadata | |
| ingestionQueries: ingestionQueries.outputs.metadata |
| core: core.outputs.metadata | ||
| exports: cmExports.outputs.metadata | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
cmManagedExports is being passed core: core.outputs.metadata and exports: cmExports.outputs.metadata, but the Exports module does not output metadata (it outputs exportContainer, schemaFilesUploaded, etc.) and the ManagedExports module does not declare core/exports parameters. This will fail compilation unless those modules are updated to support the new metadata wiring, or these params are removed and the previous wiring restored.
| core: core.outputs.metadata | |
| exports: cmExports.outputs.metadata | |
| } | |
| } | |
| } | |
| } |
… path, fix conditional output Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🛠️ Description
Split the monolithic Recommendations app.bicep into three focused modules to improve maintainability and separation of concerns:
Also added a new
Build-HubIngestionQueries.ps1build script with corresponding unit and integration tests, and updated the hub module to wire up the new sub-modules.📋 Checklist
🔬 How did you test this change?
🙋♀️ Do any of the following that apply?
📑 Did you update
docs/changelog.md?📖 Did you update documentation?