Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ jobs:
with:
path: '.' # Directory to scan (default: repo root)
strict: 'false' # Promote warnings to errors (default: false)
# paths-ignore: defaults to vendored / fixture patterns; override
# via newline-separated string. Use '' to disable.
----

=== Inputs
Expand All @@ -57,8 +59,27 @@ jobs:
| `strict`
| `false`
| When `true`, warnings become errors and the action fails on any issue

| `paths-ignore`
| _vendored & fixture defaults_
| Newline-separated path fragments to skip. Substring match against each
file path. Default set: `vendor/`, `vendored/`, `verified-container-spec/`,
`.audittraining/`, `integration/fixtures/`, `test/fixtures/`,
`tests/fixtures/`. Pass an empty string (`paths-ignore: ''`) to disable
and scan everything. See https://github.com/hyperpolymath/hypatia/pull/243
for the architectural rationale (content-pattern validators must
distinguish targets from fixtures / vendored / training-corpus files
that legitimately contain the very pattern being checked).
|===

==== Why default-on path exemptions?

K9 files inside vendored projects (e.g. `verified-container-spec/`) carry
their own pedigree declarations in their upstream context — flagging every
such file as "Pedigree block missing 'name' field" is provenance noise.
The defaults match the canonical RSR vendored-content paths; override for
project-specific carve-outs.

=== Outputs

[cols="1,3"]
Expand Down
20 changes: 20 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ inputs:
will fail on any validation issue. Defaults to false.
required: false
default: 'false'
paths-ignore:
description: >-
Newline-separated path fragments to skip. Each line is matched as a
substring against the file's path. Defaults to common vendored /
training-corpus / fixture patterns so consumers don't have to repeat
this carve-out in every repo. Pass an empty string to disable.
Pattern follows hyperpolymath/hypatia#243 — validators that scan
content patterns must distinguish a target file from a fixture /
vendored / training-corpus file that legitimately contains the
pattern being checked.
required: false
default: |
vendor/
vendored/
verified-container-spec/
.audittraining/
integration/fixtures/
test/fixtures/
tests/fixtures/

outputs:
files-scanned:
Expand All @@ -51,5 +70,6 @@ runs:
env:
INPUT_PATH: ${{ inputs.path }}
INPUT_STRICT: ${{ inputs.strict }}
INPUT_PATHS_IGNORE: ${{ inputs.paths-ignore }}
run: |
"${GITHUB_ACTION_PATH}/validate-k9.sh"
54 changes: 51 additions & 3 deletions validate-k9.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@ set -euo pipefail

SCAN_PATH="${INPUT_PATH:-.}"
STRICT="${INPUT_STRICT:-false}"
PATHS_IGNORE_RAW="${INPUT_PATHS_IGNORE:-}"

# Parse paths-ignore: newline-separated fragments, blank lines and # comments
# stripped. Each fragment is a substring match against the file path. Pattern
# adopted from hyperpolymath/hypatia#243 — content-pattern validators must
# distinguish a target from a vendored / fixture file that legitimately
# contains the very pattern being checked.
PATHS_IGNORE=()
while IFS= read -r _frag; do
# Strip leading and trailing whitespace (canonical bash idiom).
_frag="${_frag#"${_frag%%[![:space:]]*}"}"
_frag="${_frag%"${_frag##*[![:space:]]}"}"
[[ -z "$_frag" || "$_frag" == \#* ]] && continue
PATHS_IGNORE+=("$_frag")
done <<< "$PATHS_IGNORE_RAW"

# Returns 0 if path should be skipped (matches any ignore fragment)
path_ignored() {
local p="$1" frag
for frag in "${PATHS_IGNORE[@]}"; do
[[ "$p" == *"$frag"* ]] && return 0
done
return 1
}

# Counters
FILES_SCANNED=0
Expand Down Expand Up @@ -144,12 +168,21 @@ validate_k9() {
while IFS= read -r line; do
line_num=$((line_num + 1))

# Detect pedigree block start
# Detect pedigree block start. Note: do NOT `continue` here — the
# `pedigree = {` line itself contains the opening brace that
# establishes the block. Falling through to the brace counter
# below makes depth start at 1, so a subsequent `security = {…},`
# closing brace correctly takes depth to 1 (not 0), keeping us
# inside the pedigree block when later fields (name/version/leash)
# are checked. Previously the `continue` skipped this opening
# brace, depth started at 0, and the first nested block's close
# prematurely terminated the validator's view of the pedigree —
# making `pedigree.metadata.name` invisible.
if [[ "$line" =~ ^[[:space:]]*pedigree[[:space:]]*= ]]; then
has_pedigree=true
in_pedigree=true
pedigree_depth=0
continue
# fall through
fi

if [[ "$in_pedigree" == "true" ]]; then
Expand Down Expand Up @@ -250,7 +283,22 @@ echo "Scanning ${SCAN_PATH} for K9 files (.k9, .k9.ncl)..."
echo ""

# Find all K9 files, excluding .git directory
mapfile -t k9_files < <(find "$SCAN_PATH" \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path '*/.git/*' -type f | sort)
mapfile -t k9_candidates < <(find "$SCAN_PATH" \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path '*/.git/*' -type f | sort)

# Apply paths-ignore filter
k9_files=()
SKIPPED=0
for _f in "${k9_candidates[@]}"; do
if path_ignored "$_f"; then
SKIPPED=$((SKIPPED + 1))
continue
fi
k9_files+=("$_f")
done

if [[ $SKIPPED -gt 0 ]]; then
echo "::notice::Skipped ${SKIPPED} file(s) matching paths-ignore"
fi

if [[ ${#k9_files[@]} -eq 0 ]]; then
echo "::notice::No K9 files found in ${SCAN_PATH}"
Expand Down
Loading