Skip to content

Pages (landing + nightly dashboard) #168

Pages (landing + nightly dashboard)

Pages (landing + nightly dashboard) #168

Workflow file for this run

name: Pages (landing + nightly dashboard)
# Single owner of the GitHub Pages deployment (repo Pages source must be set to
# "GitHub Actions"). A repo has exactly one Pages site, so this one build serves
# BOTH the nightly CI dashboard (at the site root) AND the project landing page
# (Jekyll render of README.md, under /docs/ alongside the rest of the rendered
# docs). It runs on main pushes, after each "Nightly Evaluation" completes (pick
# up fresh history from the ci-results branch), after each "Sanitizers Nightly"
# completes (pick up fresh history from the sanitizer-results branch), after each
# "Latest ROCm canary" completes (also ci-results, under results/canary/), and on
# demand.
#
# Every workflow that WRITES a data branch this page renders has to be listed
# below, or its rows sit on the branch unpublished until some unrelated later
# deploy happens to pick them up. The canary is the newest such writer.
on:
push:
branches: [main]
workflow_run:
workflows: ["Nightly Evaluation", "Sanitizers Nightly", "Latest ROCm canary"]
types: [completed]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
# Never cancel an in-progress deploy; queue them so a landing-page push and a
# nightly refresh don't race to publish.
concurrency:
group: pages
cancel-in-progress: false
jobs:
build-deploy:
name: build + deploy pages
# Only ever deploy trusted content: a push to main, a manual dispatch, or a
# data-branch refresh triggered by a source workflow that itself ran on main.
# A workflow_run fired by a feature-branch run (e.g. a workflow_dispatch of
# Sanitizers Nightly off a PR branch) must NOT trigger an official Pages
# deploy -- that would let unreviewed branch output reach the public site.
if: >-
github.event_name != 'workflow_run' ||
github.event.workflow_run.head_branch == 'main'
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
# Always deploy the landing page + dashboard generator from main, even for a
# manual workflow_dispatch off another ref, so Pages content is deterministic.
- name: Check out main
uses: actions/checkout@v5
with:
ref: main
- name: Configure Pages
id: pages
uses: actions/configure-pages@v5
# Renders README.md (+ any repo docs) to ./_site with the same github-pages
# gem the legacy Pages build used, so the rendered docs are unchanged.
- name: Build landing page (Jekyll)
uses: actions/jekyll-build-pages@v1
with:
source: ./
destination: ./_site
# jekyll-build-pages is a container action running as root, so ./_site comes
# back root-owned and the later steps (which run as the unprivileged runner
# user) cannot write inside it.
- name: Take ownership of the Jekyll output
run: sudo chown -R "$(id -u):$(id -g)" _site
# Jekyll renders README.md to _site/index.html and docs/*.md to _site/docs/*.
# Move that landing page under /docs/ (which had no index of its own) so the
# CI dashboard can own the site root. Two rewrites are needed:
#
# 1. The rendered README mixes root-absolute links with relative ones;
# going one directory deeper breaks only the relative ones, so they are
# re-pointed with a ../ prefix. Fragments (#x), root-absolute (/x) and
# absolute (https://x, which all contain a colon) are left alone.
# 2. Jekyll's SEO tags hard-code the page's own URL, which was the site
# root. Left alone they would tell crawlers and link unfurlers that this
# page lives at the dashboard's URL, so they are re-pointed at /docs/.
- name: Move the landing page to /docs/
env:
DOCS_URL: ${{ steps.pages.outputs.base_url }}/docs/
run: |
set -euo pipefail
test -f _site/index.html
mkdir -p _site/docs
sed -E \
-e 's%(href|src)="([^":#/][^":]*)"%\1="../\2"%g' \
-e "s%(<link rel=\"canonical\" href=\")[^\"]*\"%\1${DOCS_URL}\"%" \
-e "s%(<meta property=\"og:url\" content=\")[^\"]*\"%\1${DOCS_URL}\"%" \
-e "s%(\"url\":\")[^\"]*\"%\1${DOCS_URL}\"%" \
_site/index.html > _site/docs/index.html
rm _site/index.html
echo "landing page -> /docs/ ($(wc -c < _site/docs/index.html) bytes)"
# History lives on the ci-results data branch (written by nightly-eval's
# publish job); it may not exist until the first nightly runs. Fail closed:
# a genuinely-absent branch (ls-remote exit 2) deploys the docs only, but a
# transport/auth error must NOT be swallowed into "no dashboard" -- that
# would silently drop the dashboard while history exists.
- name: Generate the nightly dashboard at the site root
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
repo_url="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
set +e
git ls-remote --exit-code --heads "$repo_url" ci-results >/dev/null 2>&1
lsr=$?
set -e
# Both result directories are OPTIONAL inputs, resolved independently.
# An absent one falls back to an empty directory, which gen_dashboard
# renders as its documented "no runs yet" state.
#
# Two lanes write to ci-results from two different workflows, so
# neither may gate the other: the canary publishes on its own schedule
# and can legitimately land first, and previously the whole render was
# conditional on a top-level gated results/*.json existing, so canary
# history stayed invisible until the first gated nightly (and the
# empty-state fallback dropped --canary-results-dir entirely). Same
# class as the "/sanitizers/ 404s before the first nightly" rule.
empty="${RUNNER_TEMP}/empty-results"
mkdir -p "$empty"
results_dir="$empty"
canary_dir="$empty/absent" # deliberately non-existent; tolerated
if [ "$lsr" -eq 2 ]; then
echo "ci-results branch absent; publishing the empty state"
elif [ "$lsr" -ne 0 ]; then
echo "::error::cannot reach ci-results (git ls-remote exit ${lsr}); failing closed"
exit 1
else
tmp="$(mktemp -d)"
git clone --depth 1 --branch ci-results "$repo_url" "$tmp"
if [ -d "$tmp/results" ]; then
results_dir="$tmp/results"
fi
if [ -d "$tmp/results/canary" ]; then
canary_dir="$tmp/results/canary"
fi
fi
# Rendered unconditionally, so / never 404s. The canary cannot affect
# the gated page: results/*.json is globbed non-recursively and the
# canary rows take a separate render path that feeds neither
# status.json nor data.json.
python3 scripts/ci/gen_dashboard.py \
--results-dir "$results_dir" --out-dir _site --max-builds 180 \
--canary-results-dir "$canary_dir"
# Counted with a directory guard, not `2>/dev/null`: redirecting
# stderr hides find's message but not its exit code, and under
# `set -euo pipefail` a failing command substitution in an assignment
# aborts the step. `canary_dir` is deliberately non-existent on the
# normal pre-first-run path, so that would have failed the Pages job
# right after the dashboard was generated -- never deploying the empty
# state this block exists to publish.
count_json() {
if [ -d "$1" ]; then
find "$1" -maxdepth 1 -name '*.json' | wc -l
else
echo 0
fi
}
echo "dashboard at / ($(count_json "$results_dir") gated build(s), $(count_json "$canary_dir") canary run(s))"
if [ ! -f _site/index.html ]; then
echo "::error::dashboard generation produced no index.html"
exit 1
fi
# History lives on the sanitizer-results data branch (written by
# sanitizers-nightly's publish job); it may not exist until the first
# sanitizer nightly runs. Same fail-closed ls-remote contract as ci-results.
# The /sanitizers/ route must NEVER 404: the root nav and docs link to it
# unconditionally, so when there is no (trusted) snapshot yet we publish the
# generator's own "no runs yet" empty state instead of leaving it absent.
- name: Publish the sanitizer dashboard at /sanitizers/
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
baselines="recipes/sanitizers/fixtures/expected/verdict_baselines.json"
mkdir -p _site/sanitizers
publish_placeholder() {
empty="$(mktemp -d)"
python3 scripts/sanitizers/gen_sanitizer_dashboard.py \
--history-root "$empty" \
--baselines "$baselines" \
--out-dir _site/sanitizers
}
repo_url="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
set +e
git ls-remote --exit-code --heads "$repo_url" sanitizer-results >/dev/null 2>&1
lsr=$?
set -e
if [ "$lsr" -eq 2 ]; then
echo "sanitizer-results branch absent; publishing the empty-state page"
publish_placeholder
elif [ "$lsr" -ne 0 ]; then
echo "::error::cannot reach sanitizer-results (git ls-remote exit ${lsr}); failing closed"
exit 1
else
tmp="$(mktemp -d)"
git clone --depth 1 --branch sanitizer-results "$repo_url" "$tmp"
src_ref=""
if [ -f "$tmp/dashboard/status.json" ]; then
src_ref="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("ref",""))' \
"$tmp/dashboard/status.json" 2>/dev/null || echo "")"
fi
if [ -f "$tmp/dashboard/status.json" ] && [ "$src_ref" != "refs/heads/main" ]; then
# Defence in depth for the main-only publish guard: never serve
# sanitizer data whose recorded source ref is not main.
echo "::warning::sanitizer-results recorded ref '${src_ref}' is not main; publishing empty state instead"
publish_placeholder
elif [ -d "$tmp/dashboard" ] && [ -f "$tmp/dashboard/index.html" ]; then
cp -r "$tmp/dashboard/"* _site/sanitizers/
echo "sanitizer dashboard at /sanitizers/ (source ref ${src_ref:-unknown})"
else
echo "sanitizer-results exists but has no dashboard yet; publishing empty state"
publish_placeholder
fi
fi
test -s _site/sanitizers/index.html
# The dashboard used to live at /ci/, the docs advertised that URL, and
# /ci/data.json is a machine-readable endpoint something may already poll.
# A Pages deploy replaces the whole site, so moving the dashboard to the
# root would 404 both the moment it ships. Keep the data endpoint where it
# was and redirect the page to its new home.
- name: Keep the old /ci/ routes working
env:
SITE_URL: ${{ steps.pages.outputs.base_url }}
run: |
set -euo pipefail
site="${SITE_URL%/}"
mkdir -p _site/ci
cp _site/data.json _site/ci/data.json
printf '%s\n' \
'<!doctype html>' \
'<html lang="en"><head><meta charset="utf-8">' \
'<title>Moved — aorta nightly CI</title>' \
'<meta http-equiv="refresh" content="0; url=../">' \
"<link rel=\"canonical\" href=\"${site}/\">" \
'</head><body>' \
'<p>The nightly CI dashboard moved to <a href="../">the site root</a>.' \
'The history feed is unchanged at <a href="data.json">/ci/data.json</a>.</p>' \
'</body></html>' \
> _site/ci/index.html
# Fail before deploying rather than publishing a site that 404s a route we
# promised. Every path below is one an existing bookmark or consumer uses.
- name: Verify the published routes
env:
SITE_URL: ${{ steps.pages.outputs.base_url }}
run: |
set -euo pipefail
# /sanitizers/ is always published (a real snapshot or the empty state),
# so the route is required unconditionally -- it must never 404.
routes=(
_site/index.html
_site/docs/index.html
_site/ci/index.html
_site/data.json
_site/ci/data.json
_site/status.json
_site/sanitizers/index.html
)
for f in "${routes[@]}"; do
if [ ! -s "$f" ]; then
echo "::error::$f is missing or empty; refusing to deploy"
exit 1
fi
done
cmp _site/data.json _site/ci/data.json
# The moved docs page must not still identify itself as the site root.
site="${SITE_URL%/}"
if grep -E 'rel="canonical"|property="og:url"|"url":"' \
_site/docs/index.html | grep -qF "${site}/\""; then
echo "::error::/docs/ still claims the dashboard's URL as its own"
exit 1
fi
echo "routes ok: / /docs/ /ci/ /data.json /ci/data.json /status.json /sanitizers/"
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
with:
path: _site
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@v4