Skip to content

Split the Recommendations app#2052

Open
flanakin wants to merge 2 commits intofeatures/hubs-recsfrom
flanakin/split-recs
Open

Split the Recommendations app#2052
flanakin wants to merge 2 commits intofeatures/hubs-recsfrom
flanakin/split-recs

Conversation

@flanakin
Copy link
Collaborator

🛠️ Description

Split the monolithic Recommendations app.bicep into three focused modules to improve maintainability and separation of concerns:

  • AzureResourceGraph — Azure Resource Graph query execution
  • IngestionQueries — Ingestion query definitions and management
  • Recommendations — Slimmed down to core recommendation logic only

Also added a new Build-HubIngestionQueries.ps1 build 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?

  • 🤏 Lint tests
  • 🤞 PS -WhatIf / az validate
  • 👍 Manually deployed + verified
  • 💪 Unit tests
  • 🙌 Integration tests

🙋‍♀️ Do any of the following that apply?

  • 🚨 This is a breaking change.
  • 🤏 The change is less than 20 lines of code.

📑 Did you update docs/changelog.md?

  • ✅ Updated changelog (required for dev PRs)
  • ➡️ Will add log in a future PR (feature branch PRs only)
  • ❎ Log not needed (small/internal change)

📖 Did you update documentation?

  • ✅ Public docs in docs (required for dev)
  • ✅ Public docs in docs-mslearn (required for dev)
  • ✅ Internal dev docs in docs-wiki (required for dev)
  • ✅ Internal dev docs in src (required for dev)
  • ➡️ Will add docs in a future PR (feature branch PRs only)
  • ❎ Docs not needed (small/internal change)

Copilot AI review requested due to automatic review settings March 12, 2026 10:19
@microsoft-github-policy-service microsoft-github-policy-service bot added the Needs: Review 👀 PR that is ready to be reviewed label Mar 12, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.bicep to wire new IngestionQueries and AzureResourceGraph modules and refactors inter-module dependencies/outputs.
  • Refactors Recommendations/app.bicep to focus on uploading query/schema assets, with query file entries generated at build time.
  • Adds Build-HubIngestionQueries.ps1 plus unit/integration tests, and extends Build-Toolkit.ps1 + template .build.config to 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.

Comment on lines 274 to +276
params: {
app: newApp(hub, 'Microsoft.CostManagement', 'Exports')
core: core.outputs.metadata
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
params: {
app: newApp(hub, 'Microsoft.CostManagement', 'Exports')
core: core.outputs.metadata
dependsOn: [
core
]
params: {
app: newApp(hub, 'Microsoft.CostManagement', 'Exports')

Copilot uses AI. Check for mistakes.

@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
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
output storageUrlForPowerBI string = core.outputs.metadata.storageUrlForPowerBI
output storageUrlForPowerBI string = core.outputs.storageUrlForPowerBI

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +7
import { AppMetadata as CoreMetadata } from '../Core/metadata.bicep'
import { AppMetadata as IngestionQueriesMetadata } from '../IngestionQueries/metadata.bicep'

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
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
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +6
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'
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
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' }
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
$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'
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +7
import { AppMetadata as CoreMetadata } from '../Core/metadata.bicep'
import { AppMetadata as IngestionQueriesMetadata } from './metadata.bicep'

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
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
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +33
@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
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.

## What it provides

- **`resourceGraph` dataset** — ADF REST dataset pointing to the ARG API (`/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01`)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
- **`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`)

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +58
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}')
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +284 to 288
core: core.outputs.metadata
exports: cmExports.outputs.metadata
}
}

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
core: core.outputs.metadata
exports: cmExports.outputs.metadata
}
}
}
}

Copilot uses AI. Check for mistakes.
… path, fix conditional output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

3 participants