Skip to content

Release FaceID 6.0.0 product interface (#19) #70

Release FaceID 6.0.0 product interface (#19)

Release FaceID 6.0.0 product interface (#19) #70

Workflow file for this run

name: CI
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Build commercial React UI
working-directory: frontend
run: |
npm install --ignore-scripts
npm run build
git diff --exit-code -- ../static
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
# httpx is required by Starlette's TestClient, not by the FaceID runtime.
run: pip install -r requirements.txt httpx==0.28.1
- name: Import smoke test (would have caught issue #1)
run: python -c "import app.main; import app.backfill; import app.enroll"
- name: Byte-compile all sources
run: python -m compileall -q app
- name: Unit tests
run: python -m unittest discover -s tests -v
- name: Import-consistency check (local imports must exist in their module)
run: |
python - << 'EOF'
import ast, sys
from pathlib import Path
defined = {}
for f in Path("app").glob("*.py"):
tree = ast.parse(f.read_text())
defined[f.stem] = {n.name for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.ClassDef))} | \
{t.id for n in ast.walk(tree) if isinstance(n, ast.Assign) for t in n.targets if isinstance(t, ast.Name)}
errors = []
for f in Path("app").glob("*.py"):
for n in ast.walk(ast.parse(f.read_text())):
if isinstance(n, ast.ImportFrom) and n.level == 1 and n.module in defined:
errors += [f"{f.name}: imports {a.name} from {n.module} — not defined there"
for a in n.names if a.name not in defined[n.module]]
if errors:
print("IMPORT CHECK FAILED:")
[print(" ", e) for e in errors]
sys.exit(1)
print("import consistency OK")
EOF
- name: App build context in sync (faceid-addon/ mirrors app/, static/, requirements)
run: |
python - << 'EOF'
import filecmp, sys
from pathlib import Path
def assert_synced(src, dst):
dc = filecmp.dircmp(src, dst, ignore=["__pycache__"])
problems = dc.diff_files + dc.left_only + dc.right_only
for sub in dc.subdirs.values():
problems += sub.diff_files + sub.left_only + sub.right_only
if problems:
print(f"{dst} out of sync with {src}: {problems}")
print("Run scripts/sync-addon.sh and commit the result.")
sys.exit(1)
assert_synced("app", "faceid-addon/app")
assert_synced("static", "faceid-addon/static")
if Path("requirements.txt").read_text() != Path("faceid-addon/requirements.txt").read_text():
print("faceid-addon/requirements.txt out of sync"); sys.exit(1)
print("app build context in sync")
EOF
- name: Version consistency (manifest version has a changelog entry)
run: |
python - << 'EOF'
import re, sys, yaml
from pathlib import Path
version = yaml.safe_load(open("faceid-addon/config.yaml"))["version"]
changelog = open("CHANGELOG.md").read()
if f"## {version}" not in changelog:
print(f"version {version} has no CHANGELOG.md entry"); sys.exit(1)
manifest = yaml.safe_load(open("faceid-addon/config.yaml"))
# Supervisor appends this value to an ingress URL that already ends
# in '/'. A leading slash creates '//ui-…' and FastAPI returns 404.
expected_entry = f"ui-{version}"
if manifest.get("ingress_entry") != expected_entry:
print(f"ingress_entry must be {expected_entry}"); sys.exit(1)
backend = Path("app/__init__.py").read_text()
ui = Path("static/index.html").read_text()
if f'VERSION = "{version}"' not in backend:
print("backend version is out of sync"); sys.exit(1)
if f"const UI_VERSION='{version}'" not in ui:
print("embedded UI version is out of sync"); sys.exit(1)
print(f"version {version} OK")
EOF
- name: Web UI script parses
run: |
python - << 'EOF'
import re, subprocess, sys
from pathlib import Path
html = Path("static/index.html").read_text()
blocks = re.findall(r"<script>(.*?)</script>", html, re.S)
if not blocks:
print("no inline script found in static/index.html"); sys.exit(1)
# Ein SyntaxError laesst die GANZE Seite leer bleiben, nicht nur den Teil,
# der ihn enthaelt — deshalb hier hart pruefen.
for i, b in enumerate(blocks):
Path(f"/tmp/ui{i}.js").write_text(b)
r = subprocess.run(["node", "--check", f"/tmp/ui{i}.js"],
capture_output=True, text=True)
if r.returncode != 0:
print(f"script block {i} does not parse:\n{r.stderr}"); sys.exit(1)
print(f"{len(blocks)} script block(s) parse")
EOF
- name: Released tag still matches shipped code
if: github.event_name == 'push'
run: |
version=$(python -c 'import yaml; print(yaml.safe_load(open("faceid-addon/config.yaml"))["version"])')
tag="v${version}"
if git rev-parse -q --verify "refs/tags/${tag}^{commit}" >/dev/null; then
tagged=$(git rev-parse "${tag}^{commit}")
current=$(git rev-parse HEAD)
if [ "${tagged}" != "${current}" ]; then
changed=$(git diff --name-only "${tag}"..HEAD -- \
app static requirements.txt faceid-addon/app faceid-addon/static \
faceid-addon/config.yaml faceid-addon/run.sh \
faceid-addon/Dockerfile faceid-addon/requirements.txt)
if [ -n "${changed}" ]; then
echo "${tag} no longer points at the code shipped as ${version}:"
echo "${changed}"
echo "Bump the version and publish a new tag instead of changing a released build."
exit 1
fi
fi
fi