diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..37d3d66 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI +on: + pull_request: + push: + branches: [main] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + check: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1 + - run: make install check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17f9665 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.artifacts/ +__pycache__/ diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..15db865 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,3 @@ +[tools] +python = "3.13.11" +actionlint = "1.7.12" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..29b77dc --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +SHELL := /bin/bash +.SHELLFLAGS := -eu -o pipefail -c +.DEFAULT_GOAL := help +.PHONY: help install lint test build check dev stop clean +help: + @echo "make check: validate and build the static redirect" +install: + mise trust .mise.toml + mise install python actionlint +lint: + mise exec -- python3 scripts/check.py + mise exec -- actionlint +build: + mise exec -- python3 scripts/check.py --build +check: lint build +test dev stop: + @echo "$@: unsupported: static redirect configuration; lint checks all redirect mechanisms" +clean: + python3 -c 'import shutil; shutil.rmtree(".artifacts", ignore_errors=True)' diff --git a/engineering-profile.json b/engineering-profile.json new file mode 100644 index 0000000..43a3e27 --- /dev/null +++ b/engineering-profile.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "profile": "configuration", + "commands": { + "install": [ + "make", + "install" + ], + "lint": [ + "make", + "lint" + ], + "build": [ + "make", + "build" + ], + "check": [ + "make", + "check" + ], + "clean": [ + "make", + "clean" + ], + "test": { + "unsupported": "Static redirect configuration has no application test suite or persistent development service; lint validates all redirect mechanisms." + }, + "dev": { + "unsupported": "Static redirect configuration has no application test suite or persistent development service; lint validates all redirect mechanisms." + }, + "stop": { + "unsupported": "Static redirect configuration has no application test suite or persistent development service; lint validates all redirect mechanisms." + } + } +} diff --git a/scripts/check.py b/scripts/check.py new file mode 100644 index 0000000..9511f93 --- /dev/null +++ b/scripts/check.py @@ -0,0 +1,44 @@ +"""Validate the existing public redirect and optionally stage its static artifact.""" +from html.parser import HTMLParser +from pathlib import Path +import re +import shutil +import sys +from urllib.parse import urlsplit + +class Redirect(HTMLParser): + def __init__(self): + super().__init__() + self.canonical = [] + self.refresh = [] + self.in_script = False + self.scripts = [] + def handle_starttag(self, tag, attrs): + values = dict(attrs) + if tag == "link" and values.get("rel") == "canonical": + self.canonical.append(values.get("href")) + if tag == "meta" and values.get("http-equiv", "").lower() == "refresh": + self.refresh.append(values.get("content", "")) + if tag == "script": + self.in_script = True + def handle_endtag(self, tag): + if tag == "script": + self.in_script = False + def handle_data(self, data): + if self.in_script: + self.scripts.append(data) + +root = Path(__file__).resolve().parents[1] +parser = Redirect() +parser.feed((root / "index.html").read_text()) +target = "https://www.avior.studio/" +assert urlsplit(target).scheme == "https" +assert parser.canonical in ([target], [target.rstrip("/")]), "canonical redirect target changed" +assert len(parser.refresh) == 1 and re.fullmatch(r"0;\s*url=" + re.escape(target), parser.refresh[0]), "refresh must immediately use the canonical destination" +assert len(parser.scripts) == 1 and re.fullmatch(r"\s*location\.replace\([\"']" + re.escape(target) + r"[\"']\);?\s*", parser.scripts[0]), "script redirect must use the canonical destination" +assert sys.argv[1:] in ([], ["--build"]), "usage: check.py [--build]" +if sys.argv[1:]: + output = root / ".artifacts/dist" + output.mkdir(parents=True, exist_ok=True) + shutil.copy2(root / "index.html", output / "index.html") +print("Canonical, refresh and script redirect targets agree")