diff --git a/.gitea/workflows/lint-and-reload.yml b/.gitea/workflows/lint-and-reload.yml new file mode 100644 index 0000000..be06bdf --- /dev/null +++ b/.gitea/workflows/lint-and-reload.yml @@ -0,0 +1,21 @@ +name: Lint and reload + +on: + push: + branches: [main] + +jobs: + lint-and-reload: + runs-on: bare + steps: + - uses: actions/checkout@v4 + + - name: Lint questions + run: ./lint.sh questions + + # Tells every running portal instance to re-fetch and atomically + # swap in the new content - see content::watch_for_reload in the + # portal repo. Fire and forget: no reply expected, no payload + # needed (the subject alone is the whole message). + - name: Tell portal to reload content + run: nats pub portal.content.reload '' --server "nats://infra:${{ secrets.NATS_TOKEN }}@127.0.0.1:4222" diff --git a/lint.sh b/lint.sh new file mode 100755 index 0000000..d8059d9 --- /dev/null +++ b/lint.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Validates every question YAML file's shape against what +# portal/src/content.rs actually requires at parse time - not a full +# schema check (that would mean building portal itself just to lint +# content, real coupling for no real benefit here), but the mistakes +# that would actually break a load: bad YAML, missing `id`/`name`, +# duplicate `id`s, and unknown requirement `type`s. Uses `yq` (the +# jq-wrapping kislyuk/yq, not the Go mikefarah/yq) so every check is a +# real jq filter, not bespoke parsing. +set -euo pipefail + +dir="${1:-questions}" +fail=0 +declare -A seen_ids + +shopt -s nullglob +for f in "$dir"/*.yaml; do + if ! json=$(yq . "$f" 2>&1); then + echo "FAIL $f: invalid YAML: $json" >&2 + fail=1 + continue + fi + + id=$(jq -r '.id // empty' <<<"$json") + name=$(jq -r '.name // empty' <<<"$json") + + if [[ -z "$id" ]]; then + echo "FAIL $f: missing required 'id'" >&2 + fail=1 + elif [[ -n "${seen_ids[$id]:-}" ]]; then + echo "FAIL $f: duplicate id '$id' (also used by ${seen_ids[$id]})" >&2 + fail=1 + else + seen_ids[$id]="$f" + fi + + if [[ -z "$name" ]]; then + echo "FAIL $f: missing required 'name'" >&2 + fail=1 + fi + + # "textarea" and "file" are the two values content.rs special-cases; + # anything else is passed straight through as an HTML + # attribute, so this is the real set of valid values, not a guess. + bad_types=$(jq -r ' + [.alternatives[]?.features[]?.requirements[]? + | select(.type != null and (.type | IN( + "text", "textarea", "file", "email", "tel", "url", "number", + "password", "date", "datetime-local", "time", "month", "week", + "color", "range", "checkbox", "radio", "hidden", "search" + ) | not)) + | .type] + | unique | .[] + ' <<<"$json") + if [[ -n "$bad_types" ]]; then + echo "FAIL $f: unknown requirement type(s): $(tr '\n' ' ' <<<"$bad_types")" >&2 + fail=1 + fi +done + +if [[ $fail -eq 0 ]]; then + echo "OK: all question files valid" +fi +exit $fail