Skip to content

Prepare public site for SignPath review #202

Prepare public site for SignPath review

Prepare public site for SignPath review #202

# Compose syntax + Kubernetes client-side dry-run (no cluster required).
name: Validate deploy manifests
on:
push:
paths:
- "QSDM/deploy/**"
- "QSDM/docs/docs/runbooks/**"
- "scripts/check_runbook_coverage.py"
- "scripts/check_sitemap_freshness.py"
- "scripts/check_binary_strip.py"
- "scripts/gen_promtool_tests.py"
- "scripts/gen_grafana_dashboards.py"
- ".github/workflows/validate-deploy.yml"
- "QSDM/source/cmd/qsdm/**"
- "QSDM/source/go.mod"
- "QSDM/source/go.sum"
pull_request:
paths:
- "QSDM/deploy/**"
- "QSDM/docs/docs/runbooks/**"
- "scripts/check_runbook_coverage.py"
- "scripts/check_sitemap_freshness.py"
- "scripts/check_binary_strip.py"
- "scripts/gen_promtool_tests.py"
- "scripts/gen_grafana_dashboards.py"
- ".github/workflows/validate-deploy.yml"
- "QSDM/source/cmd/qsdm/**"
- "QSDM/source/go.mod"
- "QSDM/source/go.sum"
workflow_dispatch:
concurrency:
group: validate-deploy-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
compose-config:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker Compose config (cluster)
run: docker compose -f QSDM/deploy/docker-compose.cluster.yml config -q
- name: Docker Compose config (single / VPS)
run: docker compose -f QSDM/deploy/docker-compose.single.yml config -q
kubernetes-dry-run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# kubeconform validates Kubernetes manifests against the upstream
# OpenAPI schemas fully offline, so we don't need a running
# apiserver. The previous `kubectl apply --dry-run=client` step
# broke on GitHub-hosted runners because kubectl's client-side
# validator tries to fetch /openapi/v2 from a local apiserver
# that doesn't exist, producing
# "dial tcp [::1]:8080: connect: connection refused".
#
# kubeconform is tiny (~10 MB single static binary), has no
# dependencies, and catches strictly more regressions than
# `--validate=false` (which is the other escape hatch kubectl
# itself suggests in that error message).
- name: Install kubeconform
run: |
set -euo pipefail
VERSION="v0.6.7"
curl -fsSL -o /tmp/kubeconform.tgz \
"https://github.com/yannh/kubeconform/releases/download/${VERSION}/kubeconform-linux-amd64.tar.gz"
tar -xzf /tmp/kubeconform.tgz -C /tmp kubeconform
sudo mv /tmp/kubeconform /usr/local/bin/kubeconform
kubeconform -v
- name: Validate manifests with kubeconform
run: |
set -euo pipefail
# -strict : reject fields not in the schema
# -summary : human-readable summary at the end
# -schema-location default : upstream Kubernetes schemas
# -kubernetes-version: pin to a currently-supported minor
for f in \
namespace.yaml \
configmap.yaml \
secret.yaml \
pvc.yaml \
deployment.yaml \
service.yaml \
statefulset.yaml \
validator-statefulset.yaml \
miner-daemonset.yaml \
networkpolicy.yaml; do
echo "==> $f"
kubeconform -strict -summary \
-kubernetes-version 1.31.0 \
-schema-location default \
"QSDM/deploy/kubernetes/$f"
done
# promtool check rules guards against silent breakage of the
# Prometheus alert rules under QSDM/deploy/prometheus/. The file
# ships as the customer-facing example; if a contributor lands a
# rule with a malformed PromQL expression or an unbalanced template
# the operators would only find out at scrape time. promtool
# tokenises every expression at validation time, so syntax drift
# fails CI before a release tag.
prometheus-rules-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install promtool
run: |
set -euo pipefail
# Pin to a current Prometheus LTS — promtool semantics for
# rule validation have been stable since 2.30 but we still
# pin to keep the failure surface deterministic across CI
# runs.
VERSION="2.55.1"
curl -fsSL -o /tmp/prometheus.tgz \
"https://github.com/prometheus/prometheus/releases/download/v${VERSION}/prometheus-${VERSION}.linux-amd64.tar.gz"
tar -xzf /tmp/prometheus.tgz -C /tmp
sudo mv "/tmp/prometheus-${VERSION}.linux-amd64/promtool" /usr/local/bin/promtool
promtool --version
- name: Validate alert rule files
run: |
set -euo pipefail
for f in QSDM/deploy/prometheus/alerts_qsdm.example.yml; do
echo "==> $f"
promtool check rules "$f"
done
# promtool test rules takes the syntax check one layer deeper:
# alongside `check rules` (which only validates that PromQL parses
# and templates compile), `test rules` evaluates each rule against
# synthetic time series and asserts the alerts fire (or don't)
# at expected times. This guards against four classes of silent
# regression that `check rules` cannot catch:
#
# 1. Threshold drift — `> 0.5` tightened to `> 0.05` is valid
# PromQL but a 10× behavioural change.
# 2. `for:` window shrinkage — `for: 10m` shortened to `for: 1m`
# is valid YAML but a 10× firing-rate change.
# 3. Metric-name typos — `qsdm_typo_total` parses fine but
# evaluates to no-data; a check-rules-only CI would ship.
# 4. Annotation-template drift — editing a runbook anchor in
# the rule's `annotations.runbook_url` template without
# updating the runbook itself fails the test (because the
# test pins the rendered runbook_url verbatim), closing the
# loop with the runbook-coverage lint below.
#
# The test file at QSDM/deploy/prometheus/alerts_qsdm.test.yml is
# generated by scripts/gen_promtool_tests.py; see the CHANGELOG
# entry on the synthetic-time-series suite for the regeneration
# workflow.
- name: Run alert rule behavioural tests
run: |
set -euo pipefail
promtool test rules QSDM/deploy/prometheus/alerts_qsdm.test.yml
# amtool check-config guards against silent breakage of the
# Alertmanager example config under QSDM/deploy/alertmanager/.
# Catches three classes of regression that promtool cannot:
#
# 1. Routing-tree YAML structure drift (a misindented sibling
# that silently turns an `n` into a `0`-sibling route).
# 2. Template-reference rot (a receiver that calls
# `{{ template "qsdm.text" . }}` after the template has been
# renamed in templates/qsdm.tmpl).
# 3. URL-field syntax errors (api_url / pagerduty routing_key
# / SMTP host parse failures); amtool runs the same URL
# parser Alertmanager uses at startup.
#
# We additionally exercise the routing tree end-to-end with
# `amtool config routes test` for each severity level — a
# regression that breaks the `severity=critical -> pagerduty +
# slack` fan-out (e.g. someone collapsing the `continue: true`)
# would silently bypass PagerDuty without this check.
alertmanager-config-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install amtool
run: |
set -euo pipefail
# Pin AMTOOL_VERSION here; scripts/git_hook_pre_commit.py
# reads the same VERSION literal to warn when the local
# amtool version drifts from the CI pin.
VERSION="0.27.0"
curl -fsSL -o /tmp/alertmanager.tgz \
"https://github.com/prometheus/alertmanager/releases/download/v${VERSION}/alertmanager-${VERSION}.linux-amd64.tar.gz"
tar -xzf /tmp/alertmanager.tgz -C /tmp
sudo mv "/tmp/alertmanager-${VERSION}.linux-amd64/amtool" /usr/local/bin/amtool
amtool --version
- name: amtool check-config
run: |
set -euo pipefail
amtool check-config QSDM/deploy/alertmanager/alertmanager.example.yml
- name: Verify routing tree (amtool config routes test)
# amtool returns the comma-separated list of receivers that a
# set of labels would route to. We assert each severity routes
# to the expected receiver(s) — this catches breakage of the
# `continue: true` fan-out for critical alerts.
run: |
set -euo pipefail
CFG=QSDM/deploy/alertmanager/alertmanager.example.yml
assert_route() {
local labels="$1"
local expected="$2"
local actual
actual="$(amtool config routes test --config.file="$CFG" $labels)"
if [ "$actual" != "$expected" ]; then
echo "FAIL: labels=[$labels]"
echo " expected: $expected"
echo " actual: $actual"
exit 1
fi
echo "OK: [$labels] -> $expected"
}
assert_route "severity=critical alertname=ExampleCritical" "pagerduty-critical,slack-critical"
assert_route "severity=warning alertname=ExampleWarning" "slack-warning"
assert_route "severity=info alertname=ExampleInfo" "slack-info-quiet"
assert_route "alertname=ExampleNoSeverity" "slack-default"
# runbook-coverage lints the (alerts, runbooks) pair to ensure
# every Prometheus alert in alerts_qsdm.example.yml carries a
# resolvable `runbook_url` annotation. Three invariants are
# enforced (mirroring the operator-side promise made in
# QSDM/docs/docs/runbooks/README.md §5):
#
# 1. Every alert has a non-empty runbook_url.
# 2. Every runbook_url's filename component refers to an
# existing file under QSDM/docs/docs/runbooks/.
# 3. Every runbook_url's #anchor fragment matches an actual
# markdown heading in the target file (using GitHub's
# slugify rules).
#
# The sweep that landed across commits 9dd4a73..93acb1b reached
# 38/38 (100%) coverage; this lint job is the regression guard
# that prevents future PRs from silently undoing it.
runbook-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install PyYAML
run: |
set -euo pipefail
python -m pip install --upgrade pip
# PyYAML is the only runtime dep of check_runbook_coverage.py.
pip install "PyYAML>=6,<7"
- name: Lint runbook coverage (alerts ↔ runbook anchors)
run: |
set -euo pipefail
python scripts/check_runbook_coverage.py
# sitemap-freshness lints QSDM/deploy/landing/sitemap.xml against
# the source-of-truth files in QSDM/deploy/landing/, enforcing the
# contract documented in the sitemap header (lines 46-50):
#
# "the date here MUST be no older than the file's last meaningful
# content change, otherwise crawlers will skip the re-crawl and
# the change won't be re-indexed."
#
# The script ships two backends:
#
# --mode online HEAD each <loc> against https://qsdm.tech and
# compare to served Last-Modified. Designed for
# operator post-deploy verification; requires
# outbound network reach.
#
# --mode offline Map each <loc> to the source file in
# QSDM/deploy/landing/ and compare sitemap
# <lastmod> to `git log -1 --format=%cI -- <file>`.
# Designed for CI: deterministic, no network.
#
# We run --mode offline here. It catches the same drift class
# (sitemap claims an older <lastmod> than reality) before the
# commit even merges, closing the recurrence-class for the ad-hoc
# ops fix in commit 6927f9b. The online lint stays available for
# operators to run post-deploy, where Caddy's served Last-Modified
# may legitimately be newer than git (e.g. server-side `touch` for
# cache rotation, as in commit 29bbdff for /docs/).
#
# IMPORTANT: this job requires `fetch-depth: 0` on the checkout
# step. With the default shallow clone (depth=1), `git log -1`
# returns the HEAD commit for every file (because that's the only
# commit in the fetched history), which makes the lint pass
# vacuously. Full history is needed so each file's most-recent
# touching commit is correctly resolved.
sitemap-freshness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Lint sitemap freshness (offline mode, git-backed)
run: |
set -euo pipefail
python scripts/check_sitemap_freshness.py --mode offline
# binary-strip-lint enforces audit row infra-06: every Go binary
# shipped to /opt/qsdm/qsdm on BLR1 MUST be built with
# `-trimpath -ldflags='-s -w'` to strip both the symbol table
# (-s) and the DWARF debug info (-w). Functionally the unstripped
# form behaves identically, but it leaks function names + source
# paths into any pprof/debugger snapshot a researcher might later
# capture, and inflates the .bak rotation footprint on BLR1 by
# ~40% (~45.6 MB unstripped vs ~32 MB stripped).
#
# The CI guardrail follows the same release-flavored build pattern
# as release-container.yml lines 163-169 and the f8c1c90/299cb84
# BLR1 deploys: cross-compile cmd/qsdm with the canonical flags,
# then lint the resulting artifact with scripts/check_binary_strip.py.
# The Ubuntu runner has the libmagic-based `file` command
# preinstalled, so the canonical fast path is exercised here; the
# script's manual ELF-section-header fallback is exercised by the
# local smoke tests recorded in the audit row's Notes.
#
# Two negative tests run alongside the canonical positive test:
#
# 1. A second build without -ldflags is linted and MUST fail
# (exit 1) — proves the lint genuinely detects unstripped
# binaries rather than rubber-stamping anything that looks
# like an ELF.
# 2. A non-ELF input (the script itself) is linted and MUST
# pass (exit 0) — proves the documented non-ELF skip path
# still works on a fresh runner.
#
# Closes the recurrence-class for the failure mode authored
# manually at 2026-05-18 15:45 UTC during the infra-05 deploy
# cycle: the first qsdm cross-compile that day omitted
# `-ldflags='-s -w'` and was only caught by hand-comparing
# `ls -la /opt/qsdm/qsdm.bak.*` sizes before the swap.
binary-strip-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: QSDM/source/go.mod
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Cross-compile qsdm with canonical release flags (positive)
working-directory: QSDM/source
run: |
set -euo pipefail
# Mirrors release-container.yml lines 163-169 + the f8c1c90
# and 299cb84 BLR1 deploy commands. -trimpath strips the
# build cwd from runtime stack traces; -s drops the symbol
# table; -w drops the DWARF debug info. CGO_ENABLED=0
# keeps the binary statically linked so the lint result is
# not perturbed by libc-version drift.
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags='-s -w' \
-o "$GITHUB_WORKSPACE/qsdm.linux-amd64" \
./cmd/qsdm
ls -la "$GITHUB_WORKSPACE/qsdm.linux-amd64"
file "$GITHUB_WORKSPACE/qsdm.linux-amd64"
- name: Cross-compile qsdm WITHOUT strip flags (negative canary)
working-directory: QSDM/source
run: |
set -euo pipefail
# Same source, no -ldflags. Should produce a measurably
# larger binary that the lint's `file --brief` path
# detects as ", not stripped".
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -o "$GITHUB_WORKSPACE/qsdm.unstripped" \
./cmd/qsdm
ls -la "$GITHUB_WORKSPACE/qsdm.unstripped"
file "$GITHUB_WORKSPACE/qsdm.unstripped"
- name: Lint stripped artifact (MUST pass)
run: |
set -euo pipefail
python scripts/check_binary_strip.py "$GITHUB_WORKSPACE/qsdm.linux-amd64"
- name: Lint unstripped artifact (MUST fail — negative canary)
run: |
set -euo pipefail
# We expect exit 1 from the lint here; if it returns 0,
# the lint is broken (false-passing on an unstripped
# binary) and CI must fail. Wrap the call so a 0 exit
# becomes the failure signal, and a 1 exit becomes the
# pass signal.
if python scripts/check_binary_strip.py "$GITHUB_WORKSPACE/qsdm.unstripped"; then
echo "::error::check_binary_strip.py false-passed on an unstripped binary; the lint is broken"
exit 1
fi
echo "OK: lint correctly detected the unstripped negative canary"
- name: Lint non-ELF input (MUST pass via skip path)
run: |
set -euo pipefail
# The script itself is a valid non-ELF file. The
# documented contract is "report and skip; do not fail".
# Verifies the skip path still works on a fresh runner
# so a future contributor doesn't accidentally tighten
# it into a multi-platform-breaking rule.
python scripts/check_binary_strip.py scripts/check_binary_strip.py