From 047eae3d92eb4f62adb51ff9ca8706f3a89a2047 Mon Sep 17 00:00:00 2001 From: Igor Beylin Date: Mon, 20 Jul 2026 11:40:26 -0400 Subject: [PATCH 1/2] ci(dogfood): add actionlint, repo-wide SHA-pin audit, properties.json schema Quantum-L9/.github only ran 2 checks (SonarCloud + validate-starters.sh, the latter still pending in PR #8) because workflow-templates/*.yml never executes in this repo -- those 9 (soon 12) files are GitHub's starter-workflow gallery, only ever run when copied into OTHER repos. This adds three real self-CI gates that dogfood the repo's own template/pack surface: - actionlint on workflow-templates/ + l9-ci-pack/workflows/ (glob is nullglob-safe, so it lints the 9 legacy templates today and picks up l9-ci-pack/ automatically once PR #8 merges). Pinned to actionlint v1.7.12 by the download script's commit SHA. - ops/audit-sha-pins.sh: repo-wide SHA-pin audit generalizing validate-starters.sh's l9-ci-pack-only @main check to every uses: line in workflow-templates/, l9-ci-pack/workflows/, and .github/workflows/. Allows only full 40-char SHAs or the documented frozen Quantum-L9/l9-ci-core tags (@v1 legacy, @v2/@v2.0.0 current). Verified against real content with both positive (33/39 refs clean) and negative (injected @main + @v4, both caught) tests; an initial regex bug that missed the common "- uses:" list-item syntax was found and fixed during testing. - ops/validate-properties-json.py + ops/schemas/workflow-template-properties.schema.json: structural JSON-Schema validation for every workflow-templates/*.properties.json. categories is intentionally left without an enum -- verified the SchemaStore community schema for this exact file type encodes only github/linguist's 865 language names and omits GitHub's 11 official fixed category buckets (Automation, continuous-integration, etc.), so a strict enum check would immediately false-flag values this repo's own templates already use. Verified against all 9 existing files (12 once PR #8 merges) plus a negative test (missing required field + wrong type, both caught). Documented all three in CONTRIBUTING.md under a new "This Repo's Own CI" section, alongside the existing SonarCloud + validate-starters.sh gates. User-approved: add more CI checks to this repo (actionlint, SHA-pin audit, properties.json schema were the three selected of six options presented). Co-authored-by: Cursor --- .github/workflows/actionlint.yml | 34 +++++++++ .github/workflows/properties-json-schema.yml | 27 +++++++ .github/workflows/sha-pin-audit.yml | 23 ++++++ CONTRIBUTING.md | 31 ++++++++ ops/audit-sha-pins.sh | 72 +++++++++++++++++++ .../workflow-template-properties.schema.json | 27 +++++++ ops/validate-properties-json.py | 60 ++++++++++++++++ 7 files changed, 274 insertions(+) create mode 100644 .github/workflows/actionlint.yml create mode 100644 .github/workflows/properties-json-schema.yml create mode 100644 .github/workflows/sha-pin-audit.yml create mode 100755 ops/audit-sha-pins.sh create mode 100644 ops/schemas/workflow-template-properties.schema.json create mode 100644 ops/validate-properties-json.py diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml new file mode 100644 index 0000000..b02c881 --- /dev/null +++ b/.github/workflows/actionlint.yml @@ -0,0 +1,34 @@ +name: actionlint (workflow-templates + l9-ci-pack) +on: + pull_request: + push: + branches: + - main + workflow_dispatch: +permissions: + contents: read +concurrency: + group: actionlint-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + actionlint: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Download actionlint (pinned) + run: | + set -euo pipefail + curl -sSf "https://raw.githubusercontent.com/rhysd/actionlint/914e7df21a07ef503a81201c76d2b11c789d3fca/scripts/download-actionlint.bash" \ + | bash -s -- 1.7.12 + - name: Lint workflow-templates/ and l9-ci-pack/workflows/ + run: | + set -euo pipefail + shopt -s nullglob + files=(workflow-templates/*.yml l9-ci-pack/workflows/*.yml) + if [ "${#files[@]}" -eq 0 ]; then + echo "No workflow files found to lint." >&2 + exit 1 + fi + ./actionlint -color "${files[@]}" diff --git a/.github/workflows/properties-json-schema.yml b/.github/workflows/properties-json-schema.yml new file mode 100644 index 0000000..550afec --- /dev/null +++ b/.github/workflows/properties-json-schema.yml @@ -0,0 +1,27 @@ +name: properties.json schema validation +on: + pull_request: + push: + branches: + - main + workflow_dispatch: +permissions: + contents: read +concurrency: + group: properties-json-schema-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.12" + - name: Install jsonschema + run: pip install --quiet jsonschema + - name: Validate workflow-templates/*.properties.json + run: python3 ops/validate-properties-json.py diff --git a/.github/workflows/sha-pin-audit.yml b/.github/workflows/sha-pin-audit.yml new file mode 100644 index 0000000..33962fe --- /dev/null +++ b/.github/workflows/sha-pin-audit.yml @@ -0,0 +1,23 @@ +name: SHA-pin audit (repo-wide) +on: + pull_request: + push: + branches: + - main + workflow_dispatch: +permissions: + contents: read +concurrency: + group: sha-pin-audit-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + audit: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Run ops/audit-sha-pins.sh + run: | + chmod +x ops/audit-sha-pins.sh + ops/audit-sha-pins.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be1fd66..7267777 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,6 +62,37 @@ All pull requests must pass: --- +## This Repo's Own CI + +`Quantum-L9/.github` validates itself on every PR/push to `main` — note that +none of the 12 files under `workflow-templates/` ever run as CI *in this +repo*; they only appear as starter-workflow choices in other repos' Actions +tab. What actually executes here: + +- **`validate-starters.sh`** — workflow-templates + `l9-ci-pack/` completeness + and `@main`-ref check (existing). +- **`actionlint`** — lints every file in `workflow-templates/` and + `l9-ci-pack/workflows/` for YAML/expression/shellcheck errors + ([`.github/workflows/actionlint.yml`](.github/workflows/actionlint.yml)). +- **`SHA-pin audit`** — repo-wide: every `uses:` ref in `workflow-templates/`, + `l9-ci-pack/workflows/`, and `.github/workflows/` must be pinned by full + 40-char commit SHA, except the documented frozen `Quantum-L9/l9-ci-core` + tags (`@v1` legacy, `@v2`/`@v2.0.0` current) + ([`ops/audit-sha-pins.sh`](ops/audit-sha-pins.sh)). +- **`properties.json schema validation`** — every + `workflow-templates/*.properties.json` against + [`ops/schemas/workflow-template-properties.schema.json`](ops/schemas/workflow-template-properties.schema.json). + This is a structural schema only — `categories` is deliberately + unconstrained by an enum. GitHub's real category vocabulary is an open + union (11 fixed buckets + any [linguist](https://github.com/github/linguist/blob/main/lib/linguist/languages.yml) + language + tech-stack names) with no single closed list; the community + SchemaStore schema for this file type encodes only the linguist-language + list and would false-flag legitimate buckets like `Automation` or + `continuous-integration`. +- **SonarCloud** — external GitHub App, not a workflow file, always runs. + +--- + ## Kernel Authoring (l9-ci-core contributors only) - Kernels must use `on: workflow_call` only diff --git a/ops/audit-sha-pins.sh b/ops/audit-sha-pins.sh new file mode 100755 index 0000000..67e6801 --- /dev/null +++ b/ops/audit-sha-pins.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# ops/audit-sha-pins.sh +# +# Repo-wide SHA-pin audit. Every `uses:` reference in every workflow YAML file +# in this repo (workflow-templates/, l9-ci-pack/workflows/, .github/workflows/) +# must be pinned by a full 40-character commit SHA, EXCEPT the documented, +# intentionally-frozen Quantum-L9/l9-ci-core major-version tags (@v1 = frozen +# legacy kernel set; @v2 / @v2.0.0 = current major once published). `@main`, +# `@master`, `@latest`, or any other floating tag is a failure. +# +# This generalizes the l9-ci-pack-only pin check already in +# ops/validate-starters.sh to every workflow file in the repo, including the +# workflow-templates/ gallery (which validate-starters.sh does not scan for +# pins beyond the @main check). +set -euo pipefail + +FAILED=0 +CHECKED=0 + +# Directories that can contain `uses:` lines. +SEARCH_DIRS=("workflow-templates" "l9-ci-pack/workflows" ".github/workflows") + +is_sha() { + [[ "$1" =~ ^[0-9a-f]{40}$ ]] +} + +is_frozen_core_tag() { + local repo="$1" ref="$2" + [[ "$repo" == "Quantum-L9/l9-ci-core"* ]] && [[ "$ref" =~ ^v(1|2|2\.0\.0)$ ]] +} + +for dir in "${SEARCH_DIRS[@]}"; do + [ -d "$dir" ] || continue + while IFS= read -r -d '' f; do + line_no=0 + while IFS= read -r line; do + line_no=$((line_no + 1)) + # Strip leading whitespace and an optional YAML list-item dash + # (" - uses: ..." is the common steps: syntax; "uses: ..." is the + # job-level reusable-workflow-call syntax). + trimmed="$(echo "$line" | sed -E 's/^[[:space:]]*(-[[:space:]]+)?//')" + # Skip full-line comments; only inspect live `uses:` keys. + [[ "$trimmed" == \#* ]] && continue + [[ "$trimmed" =~ ^uses:[[:space:]]*(.+)$ ]] || continue + spec="${BASH_REMATCH[1]}" + # Strip a trailing inline comment (e.g. "... # v4.2.2"). + spec="$(echo "$spec" | sed -E 's/[[:space:]]+#.*$//')" + # Local/relative actions (./.github/actions/...) have no ref to pin. + [[ "$spec" == .* ]] && continue + [[ "$spec" == *"@"* ]] || continue + ref="${spec##*@}" + repo="${spec%@*}" + CHECKED=$((CHECKED + 1)) + if is_sha "$ref"; then + continue + fi + if is_frozen_core_tag "$repo" "$ref"; then + continue + fi + echo "FAIL: ${f}:${line_no}: floating ref '${ref}' on '${repo}' — pin by full 40-char commit SHA" + FAILED=$((FAILED + 1)) + done < "$f" + done < <(find "$dir" -type f \( -name "*.yml" -o -name "*.yaml" \) -print0) +done + +echo "---" +echo "Checked ${CHECKED} uses: references." +if [ "$FAILED" -gt 0 ]; then + echo "${FAILED} floating (non-SHA, non-frozen-tag) reference(s) found." + exit 1 +fi +echo "All references pinned by SHA or a documented frozen Core tag (@v1/@v2/@v2.0.0)." diff --git a/ops/schemas/workflow-template-properties.schema.json b/ops/schemas/workflow-template-properties.schema.json new file mode 100644 index 0000000..ed8fe0c --- /dev/null +++ b/ops/schemas/workflow-template-properties.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "Structural schema for GitHub Actions workflow-template *.properties.json files (https://docs.github.com/en/actions/using-workflows/creating-starter-workflows-for-your-organization). categories.items is intentionally type:string with NO enum: GitHub accepts an open union of 11 fixed category buckets (continuous-integration, deployment, testing, code-quality, code-review, dependency-management, monitoring, Automation, utilities, Pages, Hugo) PLUS any github/linguist language name PLUS any known tech-stack name -- there is no single published closed list. A strict enum here would false-flag legitimate values (the SchemaStore community schema for this file type only encodes the linguist-language list and omits the 11 fixed buckets entirely, which is exactly this trap). This schema enforces structure/type-correctness only.", + "type": "object", + "additionalProperties": false, + "required": ["name", "description"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 }, + "iconName": { "type": "string", "minLength": 1 }, + "creator": { "type": "string", "minLength": 1 }, + "categories": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + }, + "filePatterns": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "labels": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + } +} diff --git a/ops/validate-properties-json.py b/ops/validate-properties-json.py new file mode 100644 index 0000000..07997e5 --- /dev/null +++ b/ops/validate-properties-json.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Validate every workflow-templates/*.properties.json against +ops/schemas/workflow-template-properties.schema.json. + +See that schema file's $comment for why `categories` is intentionally +type:string with no enum (GitHub's real category vocabulary is an open +union of 11 fixed buckets + linguist languages + tech stacks; there is no +single closed list to validate against, and the community SchemaStore +schema for this exact file type gets this wrong by encoding only the +linguist-language list). +""" +import json +import sys +from pathlib import Path + +try: + import jsonschema +except ImportError: + print("jsonschema package not available; install with: pip install jsonschema", file=sys.stderr) + sys.exit(1) + +REPO_ROOT = Path(__file__).resolve().parent.parent +SCHEMA_PATH = REPO_ROOT / "ops" / "schemas" / "workflow-template-properties.schema.json" + + +def main() -> int: + schema = json.loads(SCHEMA_PATH.read_text()) + files = sorted((REPO_ROOT / "workflow-templates").glob("*.properties.json")) + if not files: + print("No workflow-templates/*.properties.json files found.", file=sys.stderr) + return 1 + + failed = 0 + for path in files: + rel = path.relative_to(REPO_ROOT) + try: + data = json.loads(path.read_text()) + except json.JSONDecodeError as exc: + print(f"FAIL {rel}: invalid JSON: {exc}") + failed += 1 + continue + try: + jsonschema.validate(data, schema) + except jsonschema.ValidationError as exc: + print(f"FAIL {rel}: {exc.message} (at {'/'.join(str(p) for p in exc.absolute_path) or ''})") + failed += 1 + continue + print(f"OK {rel}") + + print("---") + print(f"Checked {len(files)} properties.json file(s).") + if failed: + print(f"{failed} file(s) failed schema validation.") + return 1 + print("All files valid.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From e4308cc91c28d0e3161cb1b5c6a13c16463259e4 Mon Sep 17 00:00:00 2001 From: Igor Beylin Date: Mon, 20 Jul 2026 11:44:22 -0400 Subject: [PATCH 2/2] fix(ci): resolve SonarCloud findings on the new self-CI workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - actionlint.yml: download the actionlint install script to a file and verify its sha256 before executing (was curl|bash with no verification — BLOCKER githubactions:S8482). - properties-json-schema.yml: pin jsonschema==4.26.0 and add --only-binary :all: to the pip install (MAJOR githubactions:S8541/S8544); wrapped in a run: | block since the bare "--only-binary :all:" plain scalar tripped actionlint's YAML parser (colon-space ambiguity). - ops/audit-sha-pins.sh: assign $1 to a local var in is_sha() before use, and use [[ instead of [ for the two remaining conditionals (MAJOR shelldre:S7679/S7688 x2). Re-verified with shellcheck (clean) and the existing positive/negative test suite (unchanged: 20/39 refs clean depending on pre/post PR #8 merge, injected violations still caught). Co-authored-by: Cursor --- .github/workflows/actionlint.yml | 8 +++++--- .github/workflows/properties-json-schema.yml | 3 ++- ops/audit-sha-pins.sh | 7 ++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml index b02c881..f4d310d 100644 --- a/.github/workflows/actionlint.yml +++ b/.github/workflows/actionlint.yml @@ -17,11 +17,13 @@ jobs: steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Download actionlint (pinned) + - name: Download actionlint (pinned + checksum-verified) run: | set -euo pipefail - curl -sSf "https://raw.githubusercontent.com/rhysd/actionlint/914e7df21a07ef503a81201c76d2b11c789d3fca/scripts/download-actionlint.bash" \ - | bash -s -- 1.7.12 + curl -sSf -o /tmp/download-actionlint.bash \ + "https://raw.githubusercontent.com/rhysd/actionlint/914e7df21a07ef503a81201c76d2b11c789d3fca/scripts/download-actionlint.bash" + echo "72fa3e45ac20f3c3a512d6747b4fcf719e21f890e8c43e78d48a41fdfb900c4e /tmp/download-actionlint.bash" | sha256sum -c - + bash /tmp/download-actionlint.bash 1.7.12 - name: Lint workflow-templates/ and l9-ci-pack/workflows/ run: | set -euo pipefail diff --git a/.github/workflows/properties-json-schema.yml b/.github/workflows/properties-json-schema.yml index 550afec..4cc86b5 100644 --- a/.github/workflows/properties-json-schema.yml +++ b/.github/workflows/properties-json-schema.yml @@ -22,6 +22,7 @@ jobs: with: python-version: "3.12" - name: Install jsonschema - run: pip install --quiet jsonschema + run: | + pip install --quiet --only-binary :all: "jsonschema==4.26.0" - name: Validate workflow-templates/*.properties.json run: python3 ops/validate-properties-json.py diff --git a/ops/audit-sha-pins.sh b/ops/audit-sha-pins.sh index 67e6801..019bfc2 100755 --- a/ops/audit-sha-pins.sh +++ b/ops/audit-sha-pins.sh @@ -21,7 +21,8 @@ CHECKED=0 SEARCH_DIRS=("workflow-templates" "l9-ci-pack/workflows" ".github/workflows") is_sha() { - [[ "$1" =~ ^[0-9a-f]{40}$ ]] + local ref="$1" + [[ "$ref" =~ ^[0-9a-f]{40}$ ]] } is_frozen_core_tag() { @@ -30,7 +31,7 @@ is_frozen_core_tag() { } for dir in "${SEARCH_DIRS[@]}"; do - [ -d "$dir" ] || continue + [[ -d "$dir" ]] || continue while IFS= read -r -d '' f; do line_no=0 while IFS= read -r line; do @@ -65,7 +66,7 @@ done echo "---" echo "Checked ${CHECKED} uses: references." -if [ "$FAILED" -gt 0 ]; then +if [[ "$FAILED" -gt 0 ]]; then echo "${FAILED} floating (non-SHA, non-frozen-tag) reference(s) found." exit 1 fi