Skip to content
Open
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
36 changes: 36 additions & 0 deletions .github/workflows/actionlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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 + checksum-verified)
run: |
set -euo pipefail
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
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[@]}"
28 changes: 28 additions & 0 deletions .github/workflows/properties-json-schema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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 --only-binary :all: "jsonschema==4.26.0"
- name: Validate workflow-templates/*.properties.json
run: python3 ops/validate-properties-json.py
23 changes: 23 additions & 0 deletions .github/workflows/sha-pin-audit.yml
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions ops/audit-sha-pins.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/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() {
local ref="$1"
[[ "$ref" =~ ^[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)."
27 changes: 27 additions & 0 deletions ops/schemas/workflow-template-properties.schema.json
Original file line number Diff line number Diff line change
@@ -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 }
}
}
}
60 changes: 60 additions & 0 deletions ops/validate-properties-json.py
Original file line number Diff line number Diff line change
@@ -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 '<root>'})")
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())
Loading