Subchapter 28.8
references/phases/discover/discover-assemble.mdMarkdown5 KBView on GitHub
Assembler unit. Runs after the discover fragments (
discover-terraform.md,discover-live.md,discover-billing.md) have produced their contributions. It combines them into the single artifact and owns that artifact’s final contract. See for how this unit is composed into the phase.
heroku-resource-inventory.jsondiscover.mdAfter all sub-discoveries complete, assemble heroku-resource-inventory.json in $MIGRATION_DIR/.
Schema reference: shared/schema-discover-heroku.md — consult for complete field definitions, per-type config schemas, and validation checklist.
resource_id, resource_type, heroku_app, config.heroku_app field. Unassociable resources (spaces, pipelines) get heroku_app: "unassociated".metadata section: discovery_timestamp, total_apps_discovered, discovery_sources, confidence.apps[] section with per-app entries containing:
app_name, app_id, discovery_status (success/discovery_failed), failure_reasonheroku_generation (cedar/fir/unknown), generation_action (always detect_only), generation_diagnostics (array of diagnostic reasons)space (Private Space name or null)procfile_parse_warning, app_json_parse_warning (per-app parse warnings or null)billing_profile section (if billing data available, with available, total_monthly_cost, currency, billing_period, line_items).terraform_metadata section only when .tf files with heroku_* resources were actually FOUND (the terraform fragment always runs but may exit empty — an empty run contributes no section and no "terraform" discovery source).live_metadata section (if the live fragment ran, with found, captured_at, apps_captured, apps_failed, capture_warnings, limitations — and drift per the Merge & Drift Rules below).Resource identity is resource_id (both fragments use the same deterministic ID
formats). Merge into ONE entry per resource_id. Never resolve a disagreement
silently — every conflict is recorded as drift.
resource_id from both: keep one entry. Live values win field-by-field
in config (live reflects current account state; Terraform may be stale). Keep
Terraform provenance fields (tf_file, tf_resource_name). Set
source: "live+terraform". If any config field disagreed, record it in
live_metadata.drift.config_conflicts[] as
{ "resource_id", "field", "terraform_value", "live_value" }.
1a. Add-on plan changes are conflicts, not add/remove pairs. Add-on
resource_ids embed the plan, so before applying rules 2–3, pair any live-only
and terraform-only addon entries that share the same heroku_app +
addon_service. Treat such a pair as ONE resource with a plan config conflict
(rule 1): keep the live entry, record
{ "resource_id": <live id>, "field": "plan", "terraform_value", "live_value" }
in config_conflicts[], and do not count the pair in resources_live_only /
resources_terraform_only.source: "live") and set
unmanaged_by_terraform: true — this is click-ops drift the migration plan must
include.source: "terraform") and set
not_found_live: true — defined but not deployed (or not in the selected app
set). Detect-only: downstream phases decide how to treat it.quantity: 0) but Terraform/Procfile declares are kept from
the Terraform contribution — this is the expected complement, not a conflict. Do
NOT set not_found_live on them and do NOT count them in
resources_terraform_only.apps[] entries win (they carry the real app_id UUID);
merge in Terraform-only apps with not_found_live: true.live_metadata.drift to
{ "resources_live_only": N, "resources_terraform_only": M, "config_conflicts": [...] }.When only ONE of the two fragments contributed, no merge occurs and no drift key
is written.
If assembly fails (no valid resources from any source after sub-discoveries ran):
this is an unrecoverable error (INTERPRETER.md § _on_error — _unrecoverable).
STOP and output: “Discovery ran but produced no valid resources. Check that your
input files contain valid Heroku resources and try again.”
(The phase’s _postconditions separately enforce that no forbidden clustering
fields — cluster_id, creation_order_depth, edges, dependencies,
must_migrate_together — appear in the assembled artifact.)