mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 05:53:12 +00:00
Merge pull request #57 from Sea-Haven-Industries/feature/ci-static-build-support
ci-static: add build mode for templated static sites
This commit is contained in:
commit
e4e4b42ce6
1 changed files with 58 additions and 29 deletions
87
.github/workflows/ci-static.yaml
vendored
87
.github/workflows/ci-static.yaml
vendored
|
|
@ -1,31 +1,38 @@
|
||||||
name: CI — Static Site
|
name: CI — Static Site
|
||||||
|
|
||||||
# Reusable CI for static HTML/CSS/JS sites (S3 + CloudFront repos with no build
|
# Reusable CI for static HTML/CSS/JS sites (S3 + CloudFront repos). Emits the
|
||||||
# framework). Emits the `ci / ci` status context required by the org "main branch
|
# `ci / ci` status context required by the org "main branch protection" ruleset.
|
||||||
# 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
|
# Supports two modes:
|
||||||
# python3 / xmllint preinstalled on ubuntu runners. No per-repo config needed.
|
# - Source mode (default): validates HTML in place at the repo root.
|
||||||
|
# - Build mode: set `build-command` (e.g. an Eleventy build) + `check-dir`
|
||||||
|
# (e.g. "_site") so the checks validate the BUILT output that actually
|
||||||
|
# ships — not the source templates. Without this, a templated site's
|
||||||
|
# source has no plain HTML and the checks would pass vacuously.
|
||||||
#
|
#
|
||||||
# Caller example (.github/workflows/ci.yaml):
|
# All checks are dependency-light: htmlhint via npx, the rest via python3.
|
||||||
# name: CI
|
#
|
||||||
# on:
|
# Caller example (build mode):
|
||||||
# pull_request:
|
|
||||||
# branches: [main]
|
|
||||||
# jobs:
|
# jobs:
|
||||||
# ci:
|
# ci:
|
||||||
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@main
|
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@main
|
||||||
|
# with:
|
||||||
|
# build-command: "npx @11ty/eleventy"
|
||||||
|
# check-dir: "_site"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
html-glob:
|
check-dir:
|
||||||
description: "Glob of HTML files to lint/validate"
|
description: "Directory the checks run against (repo root in source mode, build output dir in build mode)"
|
||||||
type: string
|
type: string
|
||||||
default: "**/*.html"
|
default: "."
|
||||||
|
build-command:
|
||||||
|
description: "Optional build command to run before checks (implies `npm ci` first). Leave empty for source mode."
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
node-version:
|
node-version:
|
||||||
description: "Node.js version for htmlhint"
|
description: "Node.js version for build / htmlhint"
|
||||||
type: string
|
type: string
|
||||||
default: "24"
|
default: "24"
|
||||||
run-htmlhint:
|
run-htmlhint:
|
||||||
|
|
@ -41,11 +48,11 @@ on:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
run-link-check:
|
run-link-check:
|
||||||
description: "Verify root-relative internal links and asset references resolve to files in the repo"
|
description: "Verify root-relative internal links and asset references resolve to files"
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
run-conventions-check:
|
run-conventions-check:
|
||||||
description: "Require README.md and a .gitignore that covers .env"
|
description: "Require README.md and a .gitignore that covers .env (always run against repo root)"
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
|
|
@ -59,14 +66,28 @@ jobs:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ci-static-${{ github.workflow }}-${{ github.ref }}
|
group: ci-static-${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
env:
|
||||||
|
CHECK_DIR: ${{ inputs.check-dir }}
|
||||||
|
BUILD_COMMAND: ${{ inputs.build-command }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
if: ${{ inputs.run-htmlhint }}
|
if: ${{ inputs.run-htmlhint || inputs.build-command != '' }}
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
if: ${{ inputs.build-command != '' }}
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
# build-command passed via env to avoid expression injection into the script body
|
||||||
|
eval "$BUILD_COMMAND"
|
||||||
|
if [[ ! -d "$CHECK_DIR" ]]; then
|
||||||
|
echo "::error::build-command did not produce check-dir '$CHECK_DIR'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: HTMLHint
|
- name: HTMLHint
|
||||||
if: ${{ inputs.run-htmlhint }}
|
if: ${{ inputs.run-htmlhint }}
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -86,15 +107,16 @@ jobs:
|
||||||
"alt-require": true
|
"alt-require": true
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
npx --yes htmlhint --config "${RUNNER_TEMP}/.htmlhintrc" "${{ inputs.html-glob }}"
|
npx --yes htmlhint --config "${RUNNER_TEMP}/.htmlhintrc" "${CHECK_DIR%/}/**/*.html"
|
||||||
|
|
||||||
- name: Validate JSON-LD blocks
|
- name: Validate JSON-LD blocks
|
||||||
if: ${{ inputs.run-jsonld-check }}
|
if: ${{ inputs.run-jsonld-check }}
|
||||||
run: |
|
run: |
|
||||||
python3 - <<'PY'
|
python3 - <<'PY'
|
||||||
import glob, json, re, sys
|
import glob, json, os, re, sys
|
||||||
|
base = os.environ.get("CHECK_DIR", ".")
|
||||||
errs = 0
|
errs = 0
|
||||||
for path in sorted(glob.glob("**/*.html", recursive=True)):
|
for path in sorted(glob.glob(os.path.join(base, "**/*.html"), recursive=True)):
|
||||||
html = open(path, encoding="utf-8").read()
|
html = open(path, encoding="utf-8").read()
|
||||||
for m in re.finditer(
|
for m in re.finditer(
|
||||||
r'<script[^>]*type="application/ld\+json"[^>]*>(.*?)</script>', html, re.S
|
r'<script[^>]*type="application/ld\+json"[^>]*>(.*?)</script>', html, re.S
|
||||||
|
|
@ -113,16 +135,18 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
python3 - <<'PY'
|
python3 - <<'PY'
|
||||||
import os, sys, xml.dom.minidom as M
|
import os, sys, xml.dom.minidom as M
|
||||||
|
base = os.environ.get("CHECK_DIR", ".")
|
||||||
|
p = os.path.join(base, "sitemap.xml")
|
||||||
errs = 0
|
errs = 0
|
||||||
if os.path.exists("sitemap.xml"):
|
if os.path.exists(p):
|
||||||
try:
|
try:
|
||||||
M.parse("sitemap.xml")
|
M.parse(p)
|
||||||
print("sitemap.xml is well-formed.")
|
print("sitemap.xml is well-formed.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"::error file=sitemap.xml::Malformed XML: {e}")
|
print(f"::error file={p}::Malformed XML: {e}")
|
||||||
errs += 1
|
errs += 1
|
||||||
else:
|
else:
|
||||||
print("::warning::No sitemap.xml found.")
|
print(f"::warning::No sitemap.xml found in {base}")
|
||||||
sys.exit(1 if errs else 0)
|
sys.exit(1 if errs else 0)
|
||||||
PY
|
PY
|
||||||
|
|
||||||
|
|
@ -131,8 +155,9 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
python3 - <<'PY'
|
python3 - <<'PY'
|
||||||
import glob, os, re, sys
|
import glob, os, re, sys
|
||||||
|
base = os.environ.get("CHECK_DIR", ".")
|
||||||
errs = 0
|
errs = 0
|
||||||
for path in sorted(glob.glob("**/*.html", recursive=True)):
|
for path in sorted(glob.glob(os.path.join(base, "**/*.html"), recursive=True)):
|
||||||
html = open(path, encoding="utf-8").read()
|
html = open(path, encoding="utf-8").read()
|
||||||
for attr in ("href", "src"):
|
for attr in ("href", "src"):
|
||||||
for m in re.finditer(rf'{attr}="([^"]+)"', html):
|
for m in re.finditer(rf'{attr}="([^"]+)"', html):
|
||||||
|
|
@ -141,9 +166,13 @@ jobs:
|
||||||
continue
|
continue
|
||||||
target = url.split("?")[0].split("#")[0]
|
target = url.split("?")[0].split("#")[0]
|
||||||
if not target.startswith("/"):
|
if not target.startswith("/"):
|
||||||
continue # skip relative links; root-relative is the repo convention
|
continue # root-relative is the repo convention
|
||||||
p = target.lstrip("/")
|
rel = target.lstrip("/")
|
||||||
if not any(os.path.exists(c) for c in (p, os.path.join(p, "index.html"))):
|
cands = (
|
||||||
|
os.path.join(base, rel),
|
||||||
|
os.path.join(base, rel, "index.html"),
|
||||||
|
)
|
||||||
|
if not any(os.path.exists(c) for c in cands):
|
||||||
print(f"::error file={path}::Broken internal reference: {url}")
|
print(f"::error file={path}::Broken internal reference: {url}")
|
||||||
errs += 1
|
errs += 1
|
||||||
print("All internal references resolve." if not errs else f"{errs} broken internal reference(s).")
|
print("All internal references resolve." if not errs else f"{errs} broken internal reference(s).")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue