Repository navigation
Regenerate app-map.html from APP_MAP.md; fix malformed Section 7 list #103
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy to GitHub Pages | |
| on: | |
| push: | |
| branches: ["main"] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| concurrency: | |
| group: "pages" | |
| cancel-in-progress: false | |
| jobs: | |
| validate: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Validate OpenAPI specs | |
| run: | | |
| echo "=== OpenAPI Spec Validation ===" | |
| errors=0 | |
| total=0 | |
| # Validate all JSON specs are parseable | |
| for dir in swagger-*-model/api swagger-*-model/api; do | |
| if [ -d "$dir" ]; then | |
| for f in "$dir"/*.json; do | |
| [ -f "$f" ] || continue | |
| total=$((total + 1)) | |
| if ! python3 -c "import json; json.load(open('$f'))" 2>/dev/null; then | |
| echo "❌ Invalid JSON: $f" | |
| errors=$((errors + 1)) | |
| fi | |
| done | |
| fi | |
| done | |
| echo "Validated $total JSON files, $errors errors" | |
| if [ $errors -gt 0 ]; then | |
| echo "::error::$errors spec files have invalid JSON" | |
| exit 1 | |
| fi | |
| echo "✅ All specs valid" | |
| - name: Validate manifests | |
| run: | | |
| echo "=== Manifest Validation ===" | |
| errors=0 | |
| for manifest in swagger-*-model/api/manifest.json; do | |
| [ -f "$manifest" ] || continue | |
| dir=$(dirname "$manifest") | |
| model=$(basename $(dirname "$dir")) | |
| # Check manifest module count matches file count. | |
| # Exclude manifest.json itself and underscore-prefixed helper | |
| # indexes (_paths_index.json, etc.) that aren't modules. | |
| declared=$(python3 -c "import json; print(json.load(open('$manifest'))['total_modules'])") | |
| actual=$(ls "$dir"/*.json 2>/dev/null | grep -v -E '/(manifest\.json|_[^/]+\.json)$' | wc -l) | |
| if [ "$declared" != "$actual" ]; then | |
| echo "❌ $model: manifest declares $declared modules but found $actual files" | |
| errors=$((errors + 1)) | |
| else | |
| echo "✅ $model: $declared modules OK" | |
| fi | |
| done | |
| if [ $errors -gt 0 ]; then | |
| echo "::error::Manifest count mismatches found" | |
| exit 1 | |
| fi | |
| - name: Validate search index | |
| run: | | |
| echo "=== Search Index Validation ===" | |
| python3 -c " | |
| import json | |
| with open('search-index.json', encoding='utf-8') as f: | |
| data = json.load(f) | |
| modules = data['modules'] | |
| stats = data['stats'] | |
| print(f'Search index: {len(modules)} modules, {stats[\"total_endpoints\"]} endpoints') | |
| # Check no exact duplicates (same name+version+category) | |
| keys = [m['name'] + ':' + m.get('version','v1') + ':' + m.get('category','') for m in modules] | |
| dupes = [n for n in set(keys) if keys.count(n) > 1] | |
| if dupes: | |
| print(f'❌ Exact duplicates found: {dupes[:5]}') | |
| exit(1) | |
| print('✅ No duplicates') | |
| " | |
| - name: G-6 API count regression guard | |
| run: | | |
| echo "=== API / Operation / Module Count Regression ===" | |
| python3 -X utf8 scripts/release_counts.py --check | |
| validate-releases: | |
| # Per-release validation gate (VERSIONING.md §9). Runs only if releases/ | |
| # is populated. Each release is checked independently; planned releases | |
| # are skipped automatically by validate_release.py. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: validate | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| version: ["17.9.x", "17.12.x", "17.15.x", "17.18.1", "26.1.1"] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Validate release ${{ matrix.version }} | |
| run: | | |
| if [ ! -d "releases/${{ matrix.version }}" ]; then | |
| echo "::notice::releases/${{ matrix.version }} not present yet — skipping." | |
| exit 0 | |
| fi | |
| python3 scripts/validate_release.py --version "${{ matrix.version }}" | |
| deploy: | |
| needs: [validate, validate-releases] | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Prepare deployment directory | |
| run: | | |
| # Create a clean deployment directory | |
| mkdir -p deploy | |
| # Copy only necessary files for the website (exclude heavy references and scripts) | |
| echo "Copying model directories..." | |
| cp -r swagger-*-model deploy/ 2>/dev/null || true | |
| echo "Copying YANG trees..." | |
| cp -r yang-trees deploy/ 2>/dev/null || true | |
| echo "Copying docs..." | |
| cp -r docs deploy/ 2>/dev/null || true | |
| echo "Copying root files..." | |
| # Copy root HTML files and config | |
| cp *.html deploy/ 2>/dev/null || true | |
| cp *.js deploy/ 2>/dev/null || true | |
| cp *.json deploy/ 2>/dev/null || true | |
| cp README.md deploy/ 2>/dev/null || true | |
| cp FAQ.md deploy/ 2>/dev/null || true | |
| cp CHANGELOG.md deploy/ 2>/dev/null || true | |
| cp .nojekyll deploy/ 2>/dev/null || true | |
| # SEO surface (robots.txt + sitemap.xml refreshed below) | |
| cp robots.txt deploy/ 2>/dev/null || true | |
| # PWA manifest (.webmanifest is not matched by *.json above) | |
| cp site.webmanifest deploy/ 2>/dev/null || true | |
| # Copy tools directory (Postman collection, etc.) | |
| echo "Copying tools..." | |
| cp -r tools deploy/ 2>/dev/null || true | |
| # Copy shared front-end assets (CSS/JS used by all HTML pages) | |
| echo "Copying assets..." | |
| cp -r assets deploy/ 2>/dev/null || true | |
| # Refresh sitemap.xml with today's <lastmod> values | |
| echo "Refreshing sitemap.xml..." | |
| python3 scripts/generate_sitemap.py >/dev/null | |
| cp sitemap.xml deploy/ 2>/dev/null || true | |
| # Regenerate app-map.html from APP_MAP.md (canonical arch doc) | |
| echo "Regenerating app-map.html from APP_MAP.md..." | |
| python3 -X utf8 scripts/build_app_map_html.py | |
| cp app-map.html deploy/ 2>/dev/null || true | |
| # build_app_map_html also (re)generates docs/app_mindmap.png when | |
| # Pillow is available; copy any fresh artifact over the earlier | |
| # `cp -r docs deploy/` snapshot so the deployed image matches the | |
| # current APP_MAP.md. | |
| mkdir -p deploy/docs | |
| cp docs/app_mindmap.png deploy/docs/ 2>/dev/null || true | |
| # Copy releases tree (multi-version artifacts: per-release specs, | |
| # yang-trees, exports, telemetry-index, accountability, etc.) | |
| echo "Copying releases/..." | |
| cp -r releases deploy/ 2>/dev/null || true | |
| # Exclude heavy directories | |
| echo "Excluding references, archive, generators, scripts, internal docs from deployment..." | |
| - name: Set up Node (for minifiers) | |
| uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | |
| with: | |
| node-version: "20" | |
| - name: Minify CSS and JS in deploy/ | |
| run: | | |
| # Minify only the small shared front-end files we control. | |
| # We deliberately skip the bulky per-release JSON specs and | |
| # third-party search-index data — those are JSON, not JS/CSS. | |
| npm install --no-save --silent terser@5.x clean-css-cli@5.x | |
| before_total=0 | |
| after_total=0 | |
| minify_js() { | |
| local f="$1" | |
| local before after | |
| before=$(stat -c%s "$f") | |
| npx --no-install terser "$f" \ | |
| --compress --mangle \ | |
| --output "$f.tmp" 2>/dev/null || { echo "skip $f"; return; } | |
| mv "$f.tmp" "$f" | |
| after=$(stat -c%s "$f") | |
| before_total=$((before_total + before)) | |
| after_total=$((after_total + after)) | |
| echo " js ${f#deploy/}: ${before} -> ${after}" | |
| } | |
| minify_css() { | |
| local f="$1" | |
| local before after | |
| before=$(stat -c%s "$f") | |
| npx --no-install cleancss -o "$f.tmp" "$f" 2>/dev/null \ | |
| || { echo "skip $f"; return; } | |
| mv "$f.tmp" "$f" | |
| after=$(stat -c%s "$f") | |
| before_total=$((before_total + before)) | |
| after_total=$((after_total + after)) | |
| echo " css ${f#deploy/}: ${before} -> ${after}" | |
| } | |
| echo "=== Minifying deploy/assets/ ===" | |
| for f in deploy/assets/css/*.css; do [ -f "$f" ] && minify_css "$f"; done | |
| for f in deploy/assets/js/*.js; do [ -f "$f" ] && minify_js "$f"; done | |
| echo "=== Minifying deploy/ top-level JS ===" | |
| # Only minify our hand-written JS, skip search-index and similar data files | |
| for f in deploy/*.js; do | |
| [ -f "$f" ] || continue | |
| case "$(basename "$f")" in | |
| search-index*.js) echo " skip $f (data)";; | |
| *) minify_js "$f";; | |
| esac | |
| done | |
| if [ $before_total -gt 0 ]; then | |
| pct=$(( (before_total - after_total) * 100 / before_total )) | |
| echo "" | |
| echo "=== Minification summary ===" | |
| echo " before: ${before_total} bytes" | |
| echo " after: ${after_total} bytes" | |
| echo " savings: ${pct}% (-$((before_total - after_total)) bytes)" | |
| fi | |
| - name: Minify JSON data files in deploy/ | |
| run: | | |
| # Strip whitespace from top-level JSON data files. These are | |
| # large (search-index, accountability dumps) and the page never | |
| # consumes the indented form. Per-release spec JSONs are left | |
| # alone — they live under releases/ and are not in scope here. | |
| python3 - <<'PY' | |
| import json, os, sys | |
| targets = [ | |
| "search-index.json", | |
| "accountability_compare.json", | |
| "yang_accountability.json", | |
| "yang-prefix-map.json", | |
| "version-stats.json", | |
| ] | |
| total_before = total_after = 0 | |
| for name in targets: | |
| path = os.path.join("deploy", name) | |
| if not os.path.isfile(path): | |
| continue | |
| before = os.path.getsize(path) | |
| with open(path, "r", encoding="utf-8") as fh: | |
| data = json.load(fh) | |
| compact = json.dumps(data, separators=(",", ":"), ensure_ascii=False) | |
| with open(path, "w", encoding="utf-8") as fh: | |
| fh.write(compact) | |
| after = os.path.getsize(path) | |
| total_before += before | |
| total_after += after | |
| pct = (before - after) * 100 // before if before else 0 | |
| print(f" json {name}: {before} -> {after} ({pct}% saved)") | |
| if total_before: | |
| pct = (total_before - total_after) * 100 // total_before | |
| print() | |
| print("=== JSON minification summary ===") | |
| print(f" before: {total_before} bytes") | |
| print(f" after: {total_after} bytes") | |
| print(f" savings: {pct}% (-{total_before - total_after} bytes)") | |
| PY | |
| - name: Deployment summary | |
| run: | | |
| echo "=== Deployment Summary ===" | |
| du -sh deploy | |
| echo "Total files to deploy:" | |
| find deploy -type f | wc -l | |
| echo "Top-level deployment contents:" | |
| ls -lah deploy/ | |
| - name: Setup Pages | |
| uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 | |
| - name: Upload artifact | |
| uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 | |
| with: | |
| path: 'deploy' | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 |