Skip to content

Render and deploy Quarto docs #299

Render and deploy Quarto docs

Render and deploy Quarto docs #299

Workflow file for this run

name: Render and deploy Quarto docs
on:
push:
branches: [main]
paths:
- "docs/quarto/**"
- ".github/workflows/docs.yml"
pull_request:
branches: [main]
paths:
- "docs/quarto/**"
- ".github/workflows/docs.yml"
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
# Only one deployment at a time; keep in-progress runs to avoid partial churn.
concurrency:
group: "pages-multi-monitor"
cancel-in-progress: false
jobs:
pr_render:
name: Render docs for PR
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Quarto
uses: quarto-dev/quarto-actions/setup@v2
with:
version: "1.6.39"
- name: Render site
working-directory: docs/quarto
run: quarto render
prepare_monitor_matrix:
name: Prepare monitor matrix
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.prepare.outputs.matrix }}
enabled_slugs: ${{ steps.prepare.outputs.enabled_slugs }}
all_slugs: ${{ steps.prepare.outputs.all_slugs }}
env:
H17_ENABLED: ${{ vars.MONITOR_H17_ENABLED }}
steps:
- name: Build enabled monitor matrix
id: prepare
shell: bash
run: |
set -euo pipefail
python - <<'PY'
import json
import os
from pathlib import Path
def as_bool(value: str) -> bool:
return str(value).strip().lower() == "true"
monitors = [
{"monitor_slug": "dsacamera", "runner_label": "dsacamera", "source_root": "/data/incoming"},
]
if as_bool(os.getenv("H17_ENABLED", "false")):
monitors.append({"monitor_slug": "h17", "runner_label": "h17", "source_root": "/data/incoming"})
matrix = {"include": monitors}
enabled_slugs = ",".join(m["monitor_slug"] for m in monitors)
all_slugs = "dsacamera,h17"
out = Path(os.environ["GITHUB_OUTPUT"])
with out.open("a", encoding="utf-8") as f:
f.write(f"matrix={json.dumps(matrix)}\n")
f.write(f"enabled_slugs={enabled_slugs}\n")
f.write(f"all_slugs={all_slugs}\n")
PY
build_monitors:
name: Build ${{ matrix.monitor_slug }} monitor
runs-on: [self-hosted, linux, "${{ matrix.runner_label }}"]
needs: prepare_monitor_matrix
if: github.event_name != 'pull_request'
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.prepare_monitor_matrix.outputs.matrix) }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
MONITOR_OUT_DIR: .monitor_site
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dsacamera-monitor scanner
shell: bash
run: |
set -euo pipefail
python -m pip install ./tools/dsacamera-monitor
- name: Build monitor output
shell: bash
run: |
set -euo pipefail
rm -rf "${MONITOR_OUT_DIR}"
mkdir -p "${MONITOR_OUT_DIR}"
dsacamera-incoming-scan --root "${{ matrix.source_root }}" --out "${MONITOR_OUT_DIR}" --pointing-timeseries --pointing-timeseries-max-files 5000
- name: Validate monitor artifact contract
shell: bash
run: |
set -euo pipefail
[[ -f "${MONITOR_OUT_DIR}/index.html" ]] || { echo "Missing index.html"; exit 1; }
[[ -f "${MONITOR_OUT_DIR}/manifest.json" ]] || { echo "Missing manifest.json"; exit 1; }
[[ -d "${MONITOR_OUT_DIR}/css" ]] || { echo "Missing css/"; exit 1; }
[[ -d "${MONITOR_OUT_DIR}/js" ]] || { echo "Missing js/"; exit 1; }
python - <<'PY'
import json
from pathlib import Path
manifest_path = Path(".monitor_site/manifest.json")
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
required = {
"schema_version",
"generated_at",
"source_root",
"totals",
"by_day",
"by_beam",
"gaps",
"freshness",
"options",
}
missing = sorted(required - set(manifest.keys()))
if missing:
raise SystemExit(f"manifest missing keys: {missing}")
if not isinstance(manifest.get("schema_version"), int):
raise SystemExit("manifest schema_version must be int")
if not isinstance(manifest.get("by_day"), list):
raise SystemExit("manifest by_day must be list")
if manifest.get("options", {}).get("no_stat", True):
raise SystemExit("manifest options.no_stat must be false for KPI size/mtime support")
PY
- name: Upload monitor artifact
uses: actions/upload-artifact@v4
with:
name: monitor-${{ matrix.monitor_slug }}
path: ${{ env.MONITOR_OUT_DIR }}
# MONITOR_OUT_DIR is a dot-prefixed path (.monitor_site); upload-artifact@v4
# treats dot-prefixed paths as hidden and skips them by default, yielding an
# empty artifact and a silent "No files were found" warning. Include hidden
# files so the whole output tree ships.
include-hidden-files: true
aggregate_site:
name: Render docs and aggregate monitors
needs: [prepare_monitor_matrix, build_monitors]
# Run whenever the matrix prepared successfully, even if some per-host legs
# failed. Failed legs render a placeholder so the rest of the site still
# publishes -- one bad host should not take down the whole inventory page.
if: always() && needs.prepare_monitor_matrix.result == 'success' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
SITE_OUT_DIR: docs/quarto/_site
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Quarto
uses: quarto-dev/quarto-actions/setup@v2
with:
version: "1.6.39"
- name: Render site
working-directory: docs/quarto
run: quarto render
- name: Download monitor artifacts
uses: actions/download-artifact@v4
with:
pattern: monitor-*
path: .monitor_artifacts
merge-multiple: false
- name: Mount monitor artifacts under _site
env:
ENABLED_SLUGS: ${{ needs.prepare_monitor_matrix.outputs.enabled_slugs }}
ALL_SLUGS: ${{ needs.prepare_monitor_matrix.outputs.all_slugs }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
shell: bash
run: |
set -euo pipefail
for slug in ${ALL_SLUGS//,/ }; do
src=".monitor_artifacts/monitor-${slug}"
mkdir -p "${SITE_OUT_DIR}/${slug}-live-monitor"
if [[ -f "${src}/index.html" && -f "${src}/manifest.json" ]]; then
cp -r "${src}/." "${SITE_OUT_DIR}/${slug}-live-monitor/"
elif [[ ",${ENABLED_SLUGS}," == *",${slug},"* ]]; then
# Enabled slug but no artifact: the per-host build leg failed.
# Emit a placeholder instead of hard-failing so the rest of the
# site still deploys; flag the failure via a workflow warning so
# it surfaces in the run summary.
echo "::warning title=Monitor leg failed::No artifact for enabled slug '${slug}'; rendering placeholder. See build_monitors job for details."
cat > "${SITE_OUT_DIR}/${slug}-live-monitor/index.html" <<EOF
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>${slug} live monitor unavailable (build failed)</title></head>
<body style="font-family: sans-serif; padding: 2rem;">
<h1>${slug} live monitor unavailable</h1>
<p>The ${slug} monitor build failed for this run; no fresh artifact is available.</p>
<p>See the <a href="${RUN_URL}">Actions run</a> (job "Build ${slug} monitor") for the specific failure.</p>
<p>This page will self-heal on the next successful run (scheduled every 5 minutes).</p>
</body>
</html>
EOF
else
cat > "${SITE_OUT_DIR}/${slug}-live-monitor/index.html" <<EOF
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>${slug} live monitor unavailable</title></head>
<body style="font-family: sans-serif; padding: 2rem;">
<h1>${slug} live monitor unavailable</h1>
<p>This monitor is not enabled yet in CI. Set repository variable MONITOR_${slug^^}_ENABLED=true and ensure a matching self-hosted runner label exists.</p>
</body>
</html>
EOF
fi
done
- name: Validate mounted monitor output
env:
ENABLED_SLUGS: ${{ needs.prepare_monitor_matrix.outputs.enabled_slugs }}
ALL_SLUGS: ${{ needs.prepare_monitor_matrix.outputs.all_slugs }}
shell: bash
run: |
set -euo pipefail
# Every slug must have an index.html (real artifact or placeholder).
# manifest.json presence is no longer required for enabled slugs:
# enabled+mounted legs already passed the per-leg contract check
# upstream; enabled+placeholder legs legitimately have no manifest.
for slug in ${ALL_SLUGS//,/ }; do
dest="${SITE_OUT_DIR}/${slug}-live-monitor"
[[ -f "${dest}/index.html" ]] || { echo "Mounted site missing ${slug} index.html"; exit 1; }
if [[ -f "${dest}/manifest.json" ]]; then
echo " ${slug}: real artifact mounted"
elif [[ ",${ENABLED_SLUGS}," == *",${slug},"* ]]; then
echo " ${slug}: placeholder (enabled but build leg failed)"
else
echo " ${slug}: placeholder (not enabled)"
fi
done
- name: Upload Pages artifact
# Only upload on main branch (never on PRs or non-main dispatches)
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
with:
path: ${{ env.SITE_OUT_DIR }}
deploy:
name: Deploy to GitHub Pages
needs: aggregate_site
# Lead with always() so the implicit transitive success() check does not cascade
# from a failed build_monitors leg and skip us; then explicitly require
# aggregate_site to have succeeded (it runs under always() too, so this is the
# real gate). Without always() here, GitHub still skips deploy whenever any
# upstream job -- including transitive build_monitors legs -- failed.
if: always() && needs.aggregate_site.result == 'success' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4