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 <noreply@anthropic.com>
This commit is contained in:
Nubenetes Bot
2026-06-19 13:29:10 +02:00
parent a355889969
commit 1dd0dab308
2 changed files with 48 additions and 13 deletions

View File

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

View File

@@ -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__":