ci: add site pipeline
Some checks failed
site / test (push) Failing after 5m23s
site / build (push) Has been skipped

This commit is contained in:
rezayaghobi-dev
2026-10-20 17:57:04 +03:30
parent f74a3e8a11
commit 12e72d0418
6 changed files with 137 additions and 0 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
.git
.gitea
images
tools

View File

@@ -0,0 +1,30 @@
name: site
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check file references and external requests
run: python3 tools/check_site.py
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Check tooling
run: docker version --format '{{.Server.Version}}' && docker compose version
- name: Build image
run: docker build -t rezayaghobi-site:${GITHUB_SHA::7} .
- name: Validate nginx config
run: docker run --rm rezayaghobi-site:${GITHUB_SHA::7} nginx -t
- name: Check site files are in the image
run: |
docker run --rm --entrypoint sh rezayaghobi-site:${GITHUB_SHA::7} \
-c 'test -f /usr/share/nginx/html/index.html && test -d /usr/share/nginx/html/assets'

5
Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM nginxinc/nginx-unprivileged:1.28-alpine
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
COPY nginx/security-headers.conf /etc/nginx/snippets/security-headers.conf
COPY index.html favicon.svg robots.txt /usr/share/nginx/html/
COPY assets/ /usr/share/nginx/html/assets/

34
nginx/nginx.conf Normal file
View File

@@ -0,0 +1,34 @@
server {
listen 8080;
server_name _;
server_tokens off;
root /usr/share/nginx/html;
gzip on;
gzip_min_length 1024;
gzip_types text/css application/javascript image/svg+xml application/json;
include /etc/nginx/snippets/security-headers.conf;
# Only these paths are served; .git, tools/, README.md, images/ etc. return 404.
location = / {
try_files /index.html =404;
add_header Cache-Control "no-cache" always;
include /etc/nginx/snippets/security-headers.conf;
}
location = /index.html {
add_header Cache-Control "no-cache" always;
include /etc/nginx/snippets/security-headers.conf;
}
location ~ ^/(favicon\.svg|robots\.txt)$ {
add_header Cache-Control "public, max-age=86400" always;
include /etc/nginx/snippets/security-headers.conf;
}
location ^~ /assets/ {
add_header Cache-Control "public, max-age=3600" always;
include /etc/nginx/snippets/security-headers.conf;
}
location / {
return 404;
}
}

View File

@@ -0,0 +1,5 @@
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; manifest-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

59
tools/check_site.py Normal file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
import re
import sys
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urlparse
ROOT = Path(__file__).resolve().parent.parent
INDEX = ROOT / "index.html"
SKIP = ("#", "mailto:", "tel:", "data:", "javascript:")
FETCH_RELS = {"stylesheet", "icon", "preload", "modulepreload", "manifest", "apple-touch-icon"}
errors = []
def check(ref, base, where, fetched):
if not ref or ref.startswith(SKIP):
return
if ref.startswith(("http://", "https://", "//")):
if fetched:
errors.append(f"{where}: external request {ref}")
return
path = urlparse(ref).path
target = ROOT / path.lstrip("/") if path.startswith("/") else base.parent / path
if not target.exists():
errors.append(f"{where}: missing file {ref}")
class Page(HTMLParser):
def handle_starttag(self, tag, attrs):
a = dict(attrs)
refs = []
if tag in ("script", "img", "source", "iframe", "video", "audio"):
refs.append(a.get("src"))
for part in (a.get("srcset") or "").split(","):
if part.strip():
refs.append(part.split()[0])
elif tag == "link" and FETCH_RELS & set((a.get("rel") or "").split()):
refs.append(a.get("href"))
for ref in refs:
check(ref, INDEX, "index.html", True)
if tag == "a":
check(a.get("href"), INDEX, "index.html", False)
for name in ("index.html", "favicon.svg", "robots.txt"):
if not (ROOT / name).is_file():
errors.append(f"missing required file: {name}")
if INDEX.is_file():
Page().feed(INDEX.read_text(encoding="utf-8"))
for css in ROOT.glob("assets/*.css"):
for ref in re.findall(r"url\(\s*['\"]?([^'\")]+)", css.read_text(encoding="utf-8")):
check(ref, css, str(css.relative_to(ROOT)), True)
if errors:
print("\n".join(errors))
sys.exit(1)
print("site check passed")