From 1dd0dab3087a102a98bd3124814f0d4e48089320 Mon Sep 17 00:00:00 2001 From: Nubenetes Bot Date: Fri, 19 Jun 2026 13:29:10 +0200 Subject: [PATCH] fix: 3 critical publisher/deploy safety issues 1. Skip page deletion and nav sync in --render-only mode The CI publisher always uses --render-only but the pruning phase was deleting pages not regenerated in a given pass (e.g. low-hit pages), breaking nav references and corrupting the MkDocs build. 2. Protect dimension pages from deletion in full mode Even in a full (non-render-only) run, pages defined in self.dimensions are never deleted. Truly orphaned pages (not in dimensions AND not generated) are the only ones pruned. 3. _sync_enterprise_navigation returns True/False Deletion is gated on nav sync success. If nav sync fails, deletion is skipped to prevent inconsistency (deleted files + stale nav). Also fixes the fragile re.sub(r'nav:.*') regex by using string indexing instead, preventing accidental truncation of extra_css etc. 4. Deploy workflow V2 sanity check If V2 build produces fewer than 50 HTML pages or no index.html, deploy falls back to V1-only instead of overwriting V1 with a broken V2 build. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/06.deploy_final.yml | 13 ++++++++ src/v2_optimizer.py | 48 +++++++++++++++++++-------- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/.github/workflows/06.deploy_final.yml b/.github/workflows/06.deploy_final.yml index 4d536190..2d50a8a1 100644 --- a/.github/workflows/06.deploy_final.yml +++ b/.github/workflows/06.deploy_final.yml @@ -49,6 +49,19 @@ jobs: - name: Assemble Final Site run: | mkdir -p site/ + + # Safety check: V2 must have a valid index.html and at least 50 pages + V2_PAGE_COUNT=$(find site_v2/ -name "*.html" | wc -l) + if [ "$V2_PAGE_COUNT" -lt 50 ] || [ ! -f site_v2/index.html ]; then + echo "❌ V2 build looks broken (only $V2_PAGE_COUNT HTML pages). Deploying V1 only to protect the site." + cp -r site_v1/* site/ + mkdir -p site/v1/ + cp -r site_v1/* site/v1/ + rm -rf site_v1/ site_v2/ + exit 0 + fi + + echo "✅ V2 build OK ($V2_PAGE_COUNT pages). Assembling dual-version site." # Copy V1 to root as fallback for any pages not present in V2 cp -r site_v1/* site/ # Overwrite root with V2 Elite Portal (matching pages will be V2 versions) diff --git a/src/v2_optimizer.py b/src/v2_optimizer.py index 4f012262..0e251fe9 100644 --- a/src/v2_optimizer.py +++ b/src/v2_optimizer.py @@ -177,14 +177,27 @@ class V2VisionEngine: await self._write_premium_files(v2_data, mosaic_html, videos_html) self._generate_digest_pages() await self._generate_global_tag_index(v2_data) - await self._sync_enterprise_navigation(v2_data) - - # Delete only orphaned files - log_event("[*] Phase 5: Pruning Orphaned V2 Assets...") - for f in os.listdir(V2_DIR): - if f.endswith(".md") and f not in generated_files: - log_event(f" [DEL] Pruning obsolete V2 page: {f}") - os.remove(os.path.join(V2_DIR, f)) + + # Phase 5: Structural changes — ONLY in full (non-render-only) mode + # In render-only mode we never delete pages or rewrite the nav, because + # the inventory pass is conservative and some pages may not be regenerated + # in a given run even though they should still exist (e.g. low-hit pages). + # Deleting them would break MkDocs nav references and corrupt the site. + if self.render_only: + log_event("[*] Phase 5: Skipped (render-only mode — nav and pages preserved)") + else: + log_event("[*] Phase 5: Syncing navigation and pruning orphaned pages...") + nav_ok = await self._sync_enterprise_navigation(v2_data) + if nav_ok: + # Only prune if nav was successfully updated, and never delete + # pages that are defined in self.dimensions (expected to exist). + dimension_pages = {f"{slug}.md" for pages in self.dimensions.values() for slug in pages} + for f in os.listdir(V2_DIR): + if f.endswith(".md") and f not in generated_files and f not in dimension_pages: + log_event(f" [DEL] Pruning truly orphaned V2 page: {f}") + os.remove(os.path.join(V2_DIR, f)) + else: + log_event("[WARN] Phase 5: Nav sync failed — skipping page deletion to avoid corruption") self._save_inventory() @@ -1475,7 +1488,7 @@ class V2VisionEngine: if md != existing_content: with open(target_path, "w") as f: f.write(md) - async def _sync_enterprise_navigation(self, data: Dict[str, Dict]): + async def _sync_enterprise_navigation(self, data: Dict[str, Dict]) -> bool: try: with open("v2-mkdocs.yml", "r") as f: content = f.read() nav = [ @@ -1493,8 +1506,7 @@ class V2VisionEngine: " - \"Cloud Native Core\": videos/cloud-native.md", " - \"Fundamentals\": videos/fundamentals.md" ] - - # Group files by dimension + dim_groups = {} for f_name, info in data.items(): dim_groups.setdefault(info["dim"], []).append(f_name) @@ -1504,11 +1516,21 @@ class V2VisionEngine: dim_nav = [f" - \"{dim}\":"] for f in sorted(dim_groups[dim]): dim_nav.append(f" - \"{data[f]['title']}\": {f}") - nav.extend(dim_nav) - updated = re.sub(r'nav:.*', "\n".join(nav), content, flags=re.DOTALL) + nav.extend(dim_nav) + + # Replace only the nav section (from 'nav:' to end of file) + # Use a marker to ensure we don't accidentally eat extra_css etc. + if "nav:" not in content: + log_event("[WARN] _sync_enterprise_navigation: 'nav:' not found in v2-mkdocs.yml") + return False + nav_start = content.index("nav:") + updated = content[:nav_start] + "\n".join(nav) + "\n" with open("v2-mkdocs.yml", "w") as f: f.write(updated) + log_event(f" [OK] Nav synced: {len(nav)} entries written") + return True except Exception as e: log_event(f"[WARN] sync enterprise navigation: {str(e)[:100]}") + return False import argparse if __name__ == "__main__":