Prepare public site for SignPath review #202
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
| # 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 |