Trust transparency external probe #1107
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
| # External transparency probe. | |
| # | |
| # Every 30 minutes (and on demand via workflow_dispatch), build cmd/trustcheck | |
| # from the current main and point it at the live reference validator | |
| # (https://api.qsdm.tech by default). The binary validates every §8.5.x | |
| # contract on /api/v1/trust/attestations/{summary,recent}: | |
| # | |
| # * scope_note is verbatim | |
| # * fresh_within parses as a time.Duration | |
| # * ngc_service_status is in {healthy,degraded,outage} | |
| # * last_checked_at is RFC3339 and within 1h of the runner's clock | |
| # * ratio is consistent with attested/total_public (within 0.01) | |
| # * recent.count matches the slice length and is <= summary.attested | |
| # * every recent row redacts node_id_prefix with an ellipsis, uses a | |
| # valid region enum, and ages monotonically | |
| # | |
| # The intent is that if we ever silently break the transparency surface | |
| # (schema drift, accidental cache, aggregator deadlock), this job turns | |
| # red within 30 minutes without anyone having to remember to look at the | |
| # dashboard. Failures also page via GitHub's built-in watch/notification | |
| # plumbing, which is cheaper than standing up our own external cron. | |
| # | |
| # This workflow only READS the public endpoints; it has no VPS credentials | |
| # and is safe to run from any fork targeting any QSDM-shaped validator via | |
| # the TRUSTCHECK_BASE input. The default target is the reference node we | |
| # operate. | |
| name: Trust transparency external probe | |
| permissions: | |
| contents: read | |
| concurrency: | |
| # Serialize runs per ref so an in-flight probe never races a scheduled | |
| # one -- the last scheduled probe is the authoritative "is the surface | |
| # healthy right now" signal. | |
| group: trustcheck-external-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| on: | |
| # 30-minute cadence. Tight enough that a silent regression is seen within | |
| # the next redeploy window; loose enough that we don't spam the node with | |
| # ~50 probes/day just to catch the rare incident. | |
| schedule: | |
| - cron: "*/30 * * * *" | |
| workflow_dispatch: | |
| inputs: | |
| base: | |
| description: "Base URL to probe (default: https://api.qsdm.tech)" | |
| required: false | |
| default: "https://api.qsdm.tech" | |
| allow_warmup: | |
| description: "Treat '503 warming up' as pass (for right after a redeploy)" | |
| required: false | |
| default: "false" | |
| allow_disabled: | |
| description: "Treat '404 endpoint disabled' as pass" | |
| required: false | |
| default: "false" | |
| min_attested: | |
| description: "Minimum summary.attested floor (0 = disabled). Default 2 matches our deployed redundancy (DO BLR1 + OCI SGP1); lower to 0 during single-sidecar maintenance." | |
| required: false | |
| default: "2" | |
| push: | |
| # Self-edits to this file or to the tool itself should trigger a run | |
| # so regressions caused by refactors of cmd/trustcheck surface on the | |
| # PR that introduced them, not on the next cron tick. | |
| paths: | |
| - "QSDM/source/cmd/trustcheck/**" | |
| - ".github/workflows/trustcheck-external.yml" | |
| pull_request: | |
| paths: | |
| - "QSDM/source/cmd/trustcheck/**" | |
| - ".github/workflows/trustcheck-external.yml" | |
| jobs: | |
| probe: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| with: | |
| go-version-file: QSDM/source/go.mod | |
| # trustcheck is deliberately stdlib-only (see the package doc) so | |
| # CGO_ENABLED=0 keeps the build hermetic -- liboqs is not needed here. | |
| - name: Build trustcheck | |
| working-directory: QSDM/source | |
| env: | |
| CGO_ENABLED: "0" | |
| run: | | |
| go build -trimpath -ldflags="-s -w" -o "$GITHUB_WORKSPACE/trustcheck" ./cmd/trustcheck | |
| ls -l "$GITHUB_WORKSPACE/trustcheck" | |
| - name: Resolve inputs | |
| id: inputs | |
| run: | | |
| # schedule + push + pull_request events carry no `inputs.*`, so | |
| # fall back to the reference validator + strict defaults. | |
| base='${{ inputs.base }}' | |
| allow_warmup='${{ inputs.allow_warmup }}' | |
| allow_disabled='${{ inputs.allow_disabled }}' | |
| min_attested='${{ inputs.min_attested }}' | |
| [ -z "$base" ] && base="https://api.qsdm.tech" | |
| [ -z "$allow_warmup" ] && allow_warmup="false" | |
| [ -z "$allow_disabled" ] && allow_disabled="false" | |
| # Default floor when the workflow is triggered outside | |
| # workflow_dispatch (schedule, push, pull_request). 2 matches | |
| # the deployed "primary validator + second independent sidecar" | |
| # invariant we set up on 2026-04-23; if we ever intentionally | |
| # drop to one source, bump this back to 0 in the same PR so | |
| # the probe stops flagging the expected state. | |
| [ -z "$min_attested" ] && min_attested="2" | |
| echo "base=$base" >> "$GITHUB_OUTPUT" | |
| echo "allow_warmup=$allow_warmup" >> "$GITHUB_OUTPUT" | |
| echo "allow_disabled=$allow_disabled" >> "$GITHUB_OUTPUT" | |
| echo "min_attested=$min_attested" >> "$GITHUB_OUTPUT" | |
| - name: trustcheck (human-readable checklist) | |
| env: | |
| BASE: ${{ steps.inputs.outputs.base }} | |
| ALLOW_WARMUP: ${{ steps.inputs.outputs.allow_warmup }} | |
| ALLOW_DISABLED: ${{ steps.inputs.outputs.allow_disabled }} | |
| MIN_ATTESTED: ${{ steps.inputs.outputs.min_attested }} | |
| run: | | |
| set -e | |
| args=( --base "$BASE" --timeout 15s ) | |
| [ "$ALLOW_WARMUP" = "true" ] && args+=( --allow-warmup ) | |
| [ "$ALLOW_DISABLED" = "true" ] && args+=( --allow-disabled ) | |
| # Only forward --min-attested when the floor is a positive | |
| # integer; "0" or "" leaves the binary in its default | |
| # (floor-disabled) mode so the assertion row is absent | |
| # from the output entirely rather than emitting a no-op PASS. | |
| if [ -n "$MIN_ATTESTED" ] && [ "$MIN_ATTESTED" != "0" ]; then | |
| args+=( --min-attested "$MIN_ATTESTED" ) | |
| fi | |
| echo "::notice title=trustcheck target::$BASE (min_attested=${MIN_ATTESTED:-0})" | |
| # Human-readable run (renders nicely in the job log). | |
| "$GITHUB_WORKSPACE/trustcheck" "${args[@]}" | |
| - name: trustcheck (machine-readable, for artifact) | |
| if: always() | |
| env: | |
| BASE: ${{ steps.inputs.outputs.base }} | |
| ALLOW_WARMUP: ${{ steps.inputs.outputs.allow_warmup }} | |
| ALLOW_DISABLED: ${{ steps.inputs.outputs.allow_disabled }} | |
| MIN_ATTESTED: ${{ steps.inputs.outputs.min_attested }} | |
| run: | | |
| set +e | |
| args=( --base "$BASE" --timeout 15s --json ) | |
| [ "$ALLOW_WARMUP" = "true" ] && args+=( --allow-warmup ) | |
| [ "$ALLOW_DISABLED" = "true" ] && args+=( --allow-disabled ) | |
| if [ -n "$MIN_ATTESTED" ] && [ "$MIN_ATTESTED" != "0" ]; then | |
| args+=( --min-attested "$MIN_ATTESTED" ) | |
| fi | |
| "$GITHUB_WORKSPACE/trustcheck" "${args[@]}" > trustcheck.json | |
| code=$? | |
| echo "trustcheck-exit-code=$code" | |
| # Make the JSON visible in the log, truncated if huge. | |
| head -c 8192 trustcheck.json | |
| echo | |
| # Bubble the exit code up so the step still fails on a contract | |
| # violation even though we disabled errexit for the command. | |
| exit $code | |
| - name: Upload trustcheck JSON | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: trustcheck-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: trustcheck.json | |
| if-no-files-found: ignore | |
| retention-days: 30 |