diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3389af..ae004e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,12 +80,9 @@ jobs: docker compose --env-file .env.example up -d db - # The historical production reset is a one-shot operational migration, - # not a bootstrap migration: it requires production data/configuration - # that cannot exist at its timestamp in a database rebuilt from zero. - # Prove the clean chain reaches that exact guard, then mark only that - # one-shot migration as already applied and continue the reproducible - # schema chain. The historical migration itself remains untouched. + # Historical production reset is a one-shot migration that expects the + # production admin. Prove the clean chain reaches that exact guard, + # mark only that historical reset as applied, then continue the chain. bootstrap_log="$(mktemp)" set +e docker compose --env-file .env.example --profile tools run --build --rm migrate 2>&1 | tee "$bootstrap_log" @@ -104,17 +101,14 @@ jobs: docker compose --env-file .env.example exec -T db \ psql -v ON_ERROR_STOP=1 -U dhv2_owner -d dhv2 <<'SQL' DO $$ - DECLARE - reset_rows integer; + DECLARE reset_rows integer; BEGIN SELECT COUNT(*) INTO reset_rows FROM typeorm_migrations WHERE name = 'ResetProductionOperationalData1788652800000'; - IF reset_rows <> 0 THEN RAISE EXCEPTION 'CI one-shot bypass expected reset migration to be pending, found % rows', reset_rows; END IF; - INSERT INTO typeorm_migrations ("timestamp", name) VALUES (1788652800000, 'ResetProductionOperationalData1788652800000'); END $$; @@ -122,17 +116,23 @@ jobs: docker compose --env-file .env.example --profile tools run --rm migrate + # F5.1 intentionally ends with zero operational/domain instances. The + # technical family and finding masters remain, but territory preload, + # imports, applicability links and old audits are deliberately gone. docker compose --env-file .env.example exec -T db \ psql -v ON_ERROR_STOP=1 -U dhv2_owner -d dhv2 <<'SQL' DO $$ DECLARE f5_migrations integer; - real_inventory integer; - source_areas integer; - source_yacimientos integer; + domain_assets integer; + audits integer; + applicability integer; + territory_sources integer; source_installations integer; source_subinstallations integer; source_findings integer; + department_types integer; + area_department_rules integer; BEGIN SELECT COUNT(*) INTO f5_migrations FROM typeorm_migrations @@ -141,28 +141,33 @@ jobs: 'F5CanonicalInventoryHierarchy1790087150000', 'F5AuthoritativeTerritory1790087200000', 'F5OperationalContextCompatibility1790087250000', - 'F5AuthoritativeInventoryCatalog1790087300000' + 'F5AuthoritativeInventoryCatalog1790087300000', + 'F51CleanManualInventory1790087400000' ); - IF f5_migrations <> 5 THEN - RAISE EXCEPTION 'Expected 5 F5 migrations, got %', f5_migrations; + IF f5_migrations <> 6 THEN + RAISE EXCEPTION 'Expected 6 F5/F5.1 migrations, got %', f5_migrations; END IF; - SELECT COUNT(*) INTO real_inventory - FROM assets WHERE is_inventory_instance=true AND information_status<>'INACTIVE'; - IF real_inventory <> 0 THEN - RAISE EXCEPTION 'Fresh F5 database must start with 0 real Inventory instances, got %', real_inventory; + SELECT COUNT(*) INTO domain_assets FROM assets; + IF domain_assets <> 0 THEN + RAISE EXCEPTION 'F5.1 clean start must contain 0 Assets, got %', domain_assets; END IF; - SELECT COUNT(DISTINCT asset.id) FILTER (WHERE type.operational_role='AREA'), - COUNT(DISTINCT asset.id) FILTER (WHERE lower(type.code)='yacimiento') - INTO source_areas,source_yacimientos - FROM source_documents document - JOIN asset_source_documents link ON link.document_id=document.id - JOIN assets asset ON asset.id=link.asset_id - JOIN asset_types type ON type.id=asset.asset_type_id - WHERE document.document_number='DH-F5-TERRITORY'; - IF source_areas <> 64 OR source_yacimientos <> 230 THEN - RAISE EXCEPTION 'F5 territory preload mismatch: areas %, yacimientos %', source_areas,source_yacimientos; + SELECT COUNT(*) INTO audits FROM audit_events; + IF audits <> 0 THEN + RAISE EXCEPTION 'F5.1 clean start must contain 0 audit events, got %', audits; + END IF; + + SELECT COUNT(*) INTO applicability FROM finding_catalog_item_inventory_families; + IF applicability <> 0 THEN + RAISE EXCEPTION 'F5.1 clean start must contain 0 finding applicability links, got %', applicability; + END IF; + + SELECT COUNT(*) INTO territory_sources + FROM source_documents + WHERE document_number='DH-F5-TERRITORY'; + IF territory_sources <> 0 THEN + RAISE EXCEPTION 'F5.1 must remove the old territory source preload, got % rows', territory_sources; END IF; SELECT COUNT(*) FILTER (WHERE level='INSTALLATION'), @@ -171,7 +176,7 @@ jobs: FROM inventory_families WHERE is_active=true AND source_reference LIKE 'F5:final_modelov2.xlsx%'; IF source_installations <> 14 OR source_subinstallations <> 109 THEN - RAISE EXCEPTION 'F5 family preload mismatch: installations %, subinstallations %', source_installations,source_subinstallations; + RAISE EXCEPTION 'F5.1 must preserve technical family masters: installations %, subinstallations %', source_installations,source_subinstallations; END IF; SELECT COUNT(*) INTO source_findings @@ -179,42 +184,38 @@ jobs: JOIN finding_categories category ON category.id=item.category_id WHERE lower(category.code)='f5model' AND item.is_active=true; IF source_findings <> 177 THEN - RAISE EXCEPTION 'F5 finding preload mismatch: %', source_findings; + RAISE EXCEPTION 'F5.1 must preserve finding master catalog, got %', source_findings; + END IF; + + SELECT COUNT(*) INTO department_types + FROM asset_types + WHERE lower(code)='departamento' AND can_be_root=true AND is_active=true; + IF department_types <> 1 THEN + RAISE EXCEPTION 'Expected one active root Departamento type, got %', department_types; + END IF; + + SELECT COUNT(*) INTO area_department_rules + FROM asset_type_parent_rules rule + JOIN asset_types child ON child.id=rule.child_type_id + JOIN asset_types parent ON parent.id=rule.parent_type_id + WHERE lower(child.code)='area' AND lower(parent.code)='departamento'; + IF area_department_rules <> 1 THEN + RAISE EXCEPTION 'Expected Area → Departamento canonical rule, got %', area_department_rules; END IF; END $$; SQL - # Prove the five F5 migrations are actually reversible on a clean state. - for _ in 1 2 3 4 5; do - docker compose --env-file .env.example --profile tools run --rm migrate npm run migration:revert - done - - docker compose --env-file .env.example exec -T db \ - psql -v ON_ERROR_STOP=1 -U dhv2_owner -d dhv2 <<'SQL' - DO $$ - DECLARE f5_migrations integer; instance_column integer; - BEGIN - SELECT COUNT(*) INTO f5_migrations - FROM typeorm_migrations - WHERE name LIKE 'F5%1790087%'; - IF f5_migrations <> 0 THEN - RAISE EXCEPTION 'F5 rollback left % migration rows behind', f5_migrations; - END IF; - SELECT COUNT(*) INTO instance_column - FROM information_schema.columns - WHERE table_schema='public' AND table_name='assets' AND column_name='is_inventory_instance'; - IF instance_column <> 0 THEN - RAISE EXCEPTION 'F5 rollback left is_inventory_instance behind'; - END IF; - END $$; - SQL - - # Reapply them once more. Each F5 migration performs its own source/cardinality checks. - docker compose --env-file .env.example --profile tools run --rm migrate - docker compose --env-file .env.example exec -T db \ - psql -v ON_ERROR_STOP=1 -U dhv2_owner -d dhv2 -Atc \ - "SELECT CASE WHEN COUNT(*)=5 THEN 'F5_REAPPLY_OK' ELSE 'F5_REAPPLY_FAILED:'||COUNT(*) END FROM typeorm_migrations WHERE name IN ('F5InventoryPhysicalInstance1790087100000','F5CanonicalInventoryHierarchy1790087150000','F5AuthoritativeTerritory1790087200000','F5OperationalContextCompatibility1790087250000','F5AuthoritativeInventoryCatalog1790087300000');" \ - | grep -Fx 'F5_REAPPLY_OK' + # F5.1 is intentionally one-way: production rollback is the PRE database + # backup, not migration:revert. Prove instead that the completed chain is + # idempotent and has no pending migration on a second run. + rerun_log="$(mktemp)" + docker compose --env-file .env.example --profile tools run --rm migrate 2>&1 | tee "$rerun_log" + grep -Eq 'No pending migrations|Applied migrations: 0' "$rerun_log" || { + echo "ERROR: F5.1 migration chain is not idempotent." >&2 + cat "$rerun_log" >&2 + exit 1 + } + rm -f "$rerun_log" - name: VPS-equivalent isolated API preflight run: | set -Eeuo pipefail