diff --git a/3-practice/YAML-POLICY.adoc b/3-practice/YAML-POLICY.adoc index 73e3d3fb5..5664078fd 100644 --- a/3-practice/YAML-POLICY.adoc +++ b/3-practice/YAML-POLICY.adoc @@ -258,7 +258,14 @@ These issues are now booked, in this dependency order: an executed-step marker on both the KYAML arm and its plain-YAML control. This settles the *parser* dimension only. * Step 2 — `hyperpolymath/standards#1021` — comment-preservation proof. -* Step 3 — `hyperpolymath/standards#1022` — formatter/linter for arbitrary YAML. +* Step 3 — `hyperpolymath/standards#1022` — formatter/linter for arbitrary YAML: + `scripts/kyaml-format.sh [--check] FILE...`. It runs `yq -o kyaml` verbatim, + the one rewriter step 2's proof covers, and it refuses (exit 2) a file that + does not parse, has no final newline, or would change its data. + *Not yet a gate:* `gh actions-lock` v0.1.6 does not see a `uses:` inside a + KYAML workflow. A planted unlocked SHA stayed `valid: true`, while the same + plant in block YAML went `stale`. So workflows stay out of scope until that + tool reads flow syntax (see step 6). * Step 4 — `hyperpolymath/standards#1023` — owner ruling on scope. * Step 5 — `hyperpolymath/standards#1024` — migrate non-bot estate YAML. * Step 6 — `hyperpolymath/standards#1025` — workflows, if step 4 rules them in. diff --git a/scripts/kyaml-format.sh b/scripts/kyaml-format.sh new file mode 100755 index 000000000..145b157d7 --- /dev/null +++ b/scripts/kyaml-format.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# kyaml-format.sh — format arbitrary YAML as KYAML, or check that it already is +# (YAML-POLICY Y-3; KYAML adoption step 3, standards#1022). +# +# The rewriter is `yq -p yaml -o kyaml '.'`, verbatim — the one rewriter the +# step-2 comment-preservation proof (tools/yaml-comment-proof, standards#1021) +# covers. That proof does not generalise to any other rewriter, so this script +# deliberately adds none: no post-processing, no re-indenting, no sed. +# +# Usage: +# kyaml-format.sh [--check] FILE... +# +# (default) rewrite each FILE in place as KYAML when it is not already. +# --check rewrite nothing; exit 1 if any FILE is not already KYAML. +# +# Exit contract: +# 0 = every FILE is KYAML (after the rewrite, in format mode) +# 1 = --check: at least one FILE is not KYAML (each one is named) +# 2 = refused: no FILE given, a FILE is missing or unparseable, a FILE has no +# final newline, or the rewrite would change the parsed data. A refusal +# never rewrites that FILE and never counts as clean. +# +# Why the final-newline refusal: a `|` block scalar that ends at EOF with no +# line break is read differently by go-yaml (yq) and eemeli/yaml — measured on +# k9-contractile.yml, the trailing "\n" of its last run: body exists for one +# parser and not the other. Formatting such a file would silently pick one +# reading, so the script refuses it and names the fix instead. +set -uo pipefail + +MODE=format +if [ "${1:-}" = "--check" ]; then + MODE=check + shift +fi + +if [ "$#" -eq 0 ]; then + echo "kyaml-format: no files given (usage: kyaml-format.sh [--check] FILE...)" >&2 + exit 2 +fi + +command -v yq >/dev/null 2>&1 || { echo "kyaml-format: yq is not on PATH" >&2; exit 2; } + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# canonical_data — prints the file's parsed value as key-sorted JSON; +# fails when the file does not parse. +canonical_data() { + yq -p yaml -o json -I 0 'sort_keys(..)' "$1" +} + +# kyaml_of — writes the KYAML rendering of to ; +# fails when does not parse. +kyaml_of() { + yq -p yaml -o kyaml '.' "$1" > "$2" +} + +# refuse — reports a file this script will not judge. +refuse() { + echo "::error file=$1::kyaml-format: REFUSED $1 — $2" >&2 + refused=$((refused + 1)) +} + +refused=0 drift=0 clean=0 rewritten=0 +i=0 +for f in "$@"; do + i=$((i + 1)) + out="$TMP/$i.kyaml" + if [ ! -f "$f" ]; then + refuse "$f" "no such file"; continue + fi + if [ -s "$f" ] && [ -n "$(tail -c1 "$f")" ]; then + refuse "$f" "no final newline; add one first (a trailing block scalar is ambiguous without it)"; continue + fi + if ! kyaml_of "$f" "$out" 2>"$TMP/err"; then + refuse "$f" "does not parse: $(head -1 "$TMP/err")"; continue + fi + if cmp -s "$f" "$out"; then + clean=$((clean + 1)); continue + fi + # The rewrite is only acceptable when it preserves the data exactly. + if ! before=$(canonical_data "$f") || ! after=$(canonical_data "$out") || [ "$before" != "$after" ]; then + refuse "$f" "the KYAML rendering does not parse to the same data"; continue + fi + if [ "$MODE" = check ]; then + echo "::error file=$f::kyaml-format: $f is not KYAML — run scripts/kyaml-format.sh $f" >&2 + drift=$((drift + 1)) + else + cat "$out" > "$f" + echo "kyaml-format: rewrote $f" + rewritten=$((rewritten + 1)) + fi +done + +total=$# +if [ "$refused" -gt 0 ]; then + echo "kyaml-format: $refused of $total file(s) REFUSED — not judged, not clean" >&2 + exit 2 +fi +if [ "$drift" -gt 0 ]; then + echo "kyaml-format: $drift of $total file(s) are not KYAML" >&2 + exit 1 +fi +if [ "$MODE" = check ]; then + echo "kyaml-format: all $total file(s) are KYAML" +else + echo "kyaml-format: $total file(s) KYAML ($rewritten rewritten, $clean already clean)" +fi +exit 0 diff --git a/scripts/tests/kyaml-format-test.sh b/scripts/tests/kyaml-format-test.sh new file mode 100755 index 000000000..137a0a77a --- /dev/null +++ b/scripts/tests/kyaml-format-test.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# kyaml-format-test.sh — fixture suite for scripts/kyaml-format.sh (standards#1022). +# +# A checker that only ever says "clean" proves nothing, so every accepting case +# here has a rejecting twin: a block-YAML file and a hand-mangled KYAML file +# must fail --check, an unparseable or newline-less file must be REFUSED (exit +# 2, never 0), and format mode must preserve the pin comment and be idempotent. +# +# Run: bash scripts/tests/kyaml-format-test.sh +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FMT="${FMT:-$SCRIPT_DIR/../kyaml-format.sh}" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +SHA=3d3c42e5aac5ba805825da76410c181273ba90b1 +pass=0 fail=0 + +# expect