Validates YAML/required fields/duplicate ids/requirement types with yq+jq against the exact shape content.rs parses (not a full schema check - that would mean building portal just to lint content), then publishes portal.content.reload over NATS so every running portal instance picks up the change without a restart (portal's own content::watch_for_reload, added alongside this).
This commit is contained in:
@@ -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"
|
||||||
@@ -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 <input type>
|
||||||
|
# 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
|
||||||
Reference in New Issue
Block a user