mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 03:43:10 +00:00
Add ci-static reusable workflow and content label rule (#56)
- ci-static.yaml: reusable CI for static HTML/CSS/JS sites (S3+CloudFront repos with no build framework). Job 'ci' emits the 'ci / ci' status context required by the org main-branch ruleset, which static sites previously could not satisfy (only ci-dotnet/python-sam/typescript-cdk existed). Checks: htmlhint, JSON-LD validity, sitemap well-formedness, internal-link/asset resolution, README/.gitignore conventions. - callable-labeler.yaml: add a 'content' rule (html/css/assets/sitemap/ robots) so static-site PRs get labeled instead of matching nothing.
This commit is contained in:
parent
06cab504be
commit
dea18763ee
2 changed files with 176 additions and 0 deletions
8
.github/workflows/callable-labeler.yaml
vendored
8
.github/workflows/callable-labeler.yaml
vendored
|
|
@ -56,6 +56,14 @@ jobs:
|
|||
- 'lambdas/**'
|
||||
- 'api/**'
|
||||
- 'services/**'
|
||||
content:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- '**/*.html'
|
||||
- '**/*.css'
|
||||
- 'assets/**'
|
||||
- 'sitemap.xml'
|
||||
- 'robots.txt'
|
||||
ci:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
|
|
|||
168
.github/workflows/ci-static.yaml
vendored
Normal file
168
.github/workflows/ci-static.yaml
vendored
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
name: CI — Static Site
|
||||
|
||||
# Reusable CI for static HTML/CSS/JS sites (S3 + CloudFront repos with no build
|
||||
# framework). Emits the `ci / ci` status context required by the org "main branch
|
||||
# protection" ruleset, which language-specific CI reusables already satisfy but
|
||||
# static sites previously could not.
|
||||
#
|
||||
# All checks are dependency-light: htmlhint via npx, everything else via the
|
||||
# python3 / xmllint preinstalled on ubuntu runners. No per-repo config needed.
|
||||
#
|
||||
# Caller example (.github/workflows/ci.yaml):
|
||||
# name: CI
|
||||
# on:
|
||||
# pull_request:
|
||||
# branches: [main]
|
||||
# jobs:
|
||||
# ci:
|
||||
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@main
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
html-glob:
|
||||
description: "Glob of HTML files to lint/validate"
|
||||
type: string
|
||||
default: "**/*.html"
|
||||
node-version:
|
||||
description: "Node.js version for htmlhint"
|
||||
type: string
|
||||
default: "24"
|
||||
run-htmlhint:
|
||||
description: "Run htmlhint structural validation"
|
||||
type: boolean
|
||||
default: true
|
||||
run-jsonld-check:
|
||||
description: "Validate every application/ld+json block parses as JSON"
|
||||
type: boolean
|
||||
default: true
|
||||
run-sitemap-check:
|
||||
description: "Validate sitemap.xml is well-formed XML (if present)"
|
||||
type: boolean
|
||||
default: true
|
||||
run-link-check:
|
||||
description: "Verify root-relative internal links and asset references resolve to files in the repo"
|
||||
type: boolean
|
||||
default: true
|
||||
run-conventions-check:
|
||||
description: "Require README.md and a .gitignore that covers .env"
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
concurrency:
|
||||
group: ci-static-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
if: ${{ inputs.run-htmlhint }}
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: HTMLHint
|
||||
if: ${{ inputs.run-htmlhint }}
|
||||
run: |
|
||||
cat > "${RUNNER_TEMP}/.htmlhintrc" <<'EOF'
|
||||
{
|
||||
"tagname-lowercase": true,
|
||||
"attr-lowercase": true,
|
||||
"attr-value-double-quotes": true,
|
||||
"doctype-first": true,
|
||||
"doctype-html5": true,
|
||||
"tag-pair": true,
|
||||
"spec-char-escape": false,
|
||||
"id-unique": true,
|
||||
"src-not-empty": true,
|
||||
"attr-no-duplication": true,
|
||||
"title-require": true,
|
||||
"alt-require": true
|
||||
}
|
||||
EOF
|
||||
npx --yes htmlhint --config "${RUNNER_TEMP}/.htmlhintrc" "${{ inputs.html-glob }}"
|
||||
|
||||
- name: Validate JSON-LD blocks
|
||||
if: ${{ inputs.run-jsonld-check }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import glob, json, re, sys
|
||||
errs = 0
|
||||
for path in sorted(glob.glob("**/*.html", recursive=True)):
|
||||
html = open(path, encoding="utf-8").read()
|
||||
for m in re.finditer(
|
||||
r'<script[^>]*type="application/ld\+json"[^>]*>(.*?)</script>', html, re.S
|
||||
):
|
||||
try:
|
||||
json.loads(m.group(1).strip())
|
||||
except Exception as e:
|
||||
print(f"::error file={path}::Invalid JSON-LD: {e}")
|
||||
errs += 1
|
||||
print("All JSON-LD blocks valid." if not errs else f"{errs} invalid JSON-LD block(s).")
|
||||
sys.exit(1 if errs else 0)
|
||||
PY
|
||||
|
||||
- name: Validate sitemap.xml
|
||||
if: ${{ inputs.run-sitemap-check }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import os, sys, xml.dom.minidom as M
|
||||
errs = 0
|
||||
if os.path.exists("sitemap.xml"):
|
||||
try:
|
||||
M.parse("sitemap.xml")
|
||||
print("sitemap.xml is well-formed.")
|
||||
except Exception as e:
|
||||
print(f"::error file=sitemap.xml::Malformed XML: {e}")
|
||||
errs += 1
|
||||
else:
|
||||
print("::warning::No sitemap.xml found.")
|
||||
sys.exit(1 if errs else 0)
|
||||
PY
|
||||
|
||||
- name: Check internal links and asset references
|
||||
if: ${{ inputs.run-link-check }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import glob, os, re, sys
|
||||
errs = 0
|
||||
for path in sorted(glob.glob("**/*.html", recursive=True)):
|
||||
html = open(path, encoding="utf-8").read()
|
||||
for attr in ("href", "src"):
|
||||
for m in re.finditer(rf'{attr}="([^"]+)"', html):
|
||||
url = m.group(1)
|
||||
if re.match(r'^(https?:|mailto:|tel:|#|data:|//|javascript:)', url):
|
||||
continue
|
||||
target = url.split("?")[0].split("#")[0]
|
||||
if not target.startswith("/"):
|
||||
continue # skip relative links; root-relative is the repo convention
|
||||
p = target.lstrip("/")
|
||||
if not any(os.path.exists(c) for c in (p, os.path.join(p, "index.html"))):
|
||||
print(f"::error file={path}::Broken internal reference: {url}")
|
||||
errs += 1
|
||||
print("All internal references resolve." if not errs else f"{errs} broken internal reference(s).")
|
||||
sys.exit(1 if errs else 0)
|
||||
PY
|
||||
|
||||
- name: Conventions check
|
||||
if: ${{ inputs.run-conventions-check }}
|
||||
run: |
|
||||
errors=0
|
||||
fail() { echo "::error::$1"; errors=$((errors + 1)); }
|
||||
[[ -f README.md ]] || fail "Missing README.md"
|
||||
if [[ -f .gitignore ]]; then
|
||||
grep -qE '^\.env$|^\.env\b' .gitignore || fail ".gitignore does not include .env"
|
||||
else
|
||||
fail "Missing .gitignore"
|
||||
fi
|
||||
if [[ $errors -gt 0 ]]; then
|
||||
echo "Conventions check failed with $errors error(s)."
|
||||
exit 1
|
||||
fi
|
||||
echo "Conventions check passed."
|
||||
Loading…
Add table
Reference in a new issue