CI/CD hardening: lint, secret scan, coverage gate, auto CVE-fix PRs, GHCR + GitHub mirror publishing (#34)
CI / build-and-scan (push) Successful in 2m43s

- release changelog: commits rendered as description (link), divider lines dropped
- gitleaks secret scan and hadolint on every push/PR
- ruff lint/format gate (Python repos) with a pytest --cov-fail-under gate
- scheduled CRITICAL Trivy failures attempt an apk upgrade rebuild and open a follow-up PR if it clears the finding, instead of just failing red
- images also published to ghcr.io/djeex/<repo>
- a matching GitHub Release is created on the GitHub mirror, with a notice pointing back to this repo as the source of truth
This commit was merged in pull request #34.
This commit is contained in:
2026-08-26 15:38:54 +02:00
parent edd39febd1
commit f22634e9d9
18 changed files with 354 additions and 111 deletions
+9 -3
View File
@@ -1,7 +1,7 @@
import logging
from pathlib import Path
from shutil import copyfile
def generate_css_variables(colors_dict, output_path):
"""Generate css variables for theme colors"""
css_lines = [":root {"]
@@ -13,6 +13,7 @@ def generate_css_variables(colors_dict, output_path):
f.write("\n".join(css_lines))
logging.info(f"[✓] CSS variables written to {output_path}")
def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
"""Generate css variables fonts"""
font_files = list(fonts_dir.glob("*"))
@@ -35,7 +36,7 @@ def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
css_lines = []
for font_name, sources in font_faces.items():
css_lines.append(f"@font-face {{")
css_lines.append("@font-face {")
css_lines.append(f" font-family: '{font_name}';")
srcs = [f"url('../fonts/{file}') format('{fmt}')" for file, fmt in sorted(sources)]
css_lines.append(f" src: {', '.join(srcs)};")
@@ -58,6 +59,7 @@ def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
logging.info(f"[✓] Generated fonts CSS: {output_path}")
return preload_links
def generate_google_fonts_link(fonts):
"""Generate src link for Google fonts"""
if not fonts:
@@ -70,5 +72,9 @@ def generate_google_fonts_link(fonts):
families.append(f"{family}:wght@{';'.join(weights)}")
else:
families.append(family)
href = "https://fonts.googleapis.com/css2?" + "&".join(f"family={f}" for f in families) + "&display=swap"
href = (
"https://fonts.googleapis.com/css2?"
+ "&".join(f"family={f}" for f in families)
+ "&display=swap"
)
return f'<link href="{href}" rel="stylesheet">'
+17 -17
View File
@@ -1,7 +1,8 @@
import yaml
import os
from pathlib import Path
import yaml
# YAML file paths
GALLERY_YAML = "config/gallery.yaml"
@@ -9,31 +10,37 @@ GALLERY_YAML = "config/gallery.yaml"
GALLERY_DIR = Path("config/photos/gallery")
HERO_DIR = Path("config/photos/hero")
def load_yaml(path):
"""Load gallery config .yaml file"""
print(f"[→] Loading {path}...")
if not os.path.exists(path):
print(f"[✗] File not found: {path}")
return {}
with open(path, "r", encoding="utf-8") as f:
with open(path, encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
images = data.get("images", []) or []
print(f"[✓] Loaded {len(images)} image(s) from {path}")
return data
def save_yaml(data, path):
"""Save modified gallery config .yaml file"""
with open(path, "w", encoding="utf-8") as f:
yaml.dump(data, f, sort_keys=False, allow_unicode=True)
print(f"[✓] Saved updated YAML to {path}")
def get_all_image_paths(directory):
"""Get the path to record for builded site"""
return sorted([
str(p.relative_to(directory.parent)).replace("\\", "/")
for p in directory.rglob("*")
if p.suffix.lower() in [".jpg", ".jpeg", ".png", ".webp"]
])
return sorted(
[
str(p.relative_to(directory.parent)).replace("\\", "/")
for p in directory.rglob("*")
if p.suffix.lower() in [".jpg", ".jpeg", ".png", ".webp"]
]
)
def update_gallery():
"""Update the gallery photo list"""
@@ -50,11 +57,7 @@ def update_gallery():
known_images = {img["src"] for img in gallery_images}
# Add new images
new_images = [
{"src": path, "tags": []}
for path in all_images
if path not in known_images
]
new_images = [{"src": path, "tags": []} for path in all_images if path not in known_images]
if new_images:
gallery_images.extend(new_images)
print(f"[✓] Added {len(new_images)} new image(s) to gallery.yaml (gallery)")
@@ -74,6 +77,7 @@ def update_gallery():
if not new_images and not deleted_images:
print("[✓] No changes to gallery.yaml (gallery)")
def update_hero():
"""Update the hero photo list"""
print("\n=== Updating gallery.yaml (hero section) ===")
@@ -89,11 +93,7 @@ def update_hero():
known_images = {img["src"] for img in hero_images}
# Add new images
new_images = [
{"src": path}
for path in all_images
if path not in known_images
]
new_images = [{"src": path} for path in all_images if path not in known_images]
if new_images:
hero_images.extend(new_images)
print(f"[✓] Added {len(new_images)} new image(s) to gallery.yaml (hero)")
+7 -2
View File
@@ -2,6 +2,7 @@ import json
import logging
from pathlib import Path
def render_template(template_path, context):
"""Render html templates"""
with open(template_path, encoding="utf-8") as f:
@@ -11,6 +12,7 @@ def render_template(template_path, context):
content = content.replace(placeholder, str(value) if value is not None else "")
return content
def render_gallery_images(images):
"""Render the photo gallery"""
html = ""
@@ -20,11 +22,12 @@ def render_gallery_images(images):
html += f"""
<div class="section" data-tags="{tags}">
<div class="tags">{tag_html}</div>
<img class="fade-in-img lazyload" data-src="/img/{img['src']}" alt="{img.get('alt', '')}" loading="lazy">
<img class="fade-in-img lazyload" data-src="/img/{img["src"]}" alt="{img.get("alt", "")}" loading="lazy">
</div>
"""
return html
def generate_gallery_json_from_images(images, output_dir):
"""Generte the hero carrousel photo list"""
try:
@@ -37,6 +40,7 @@ def generate_gallery_json_from_images(images, output_dir):
except Exception as e:
logging.error(f"[✗] Error generating gallery JSON: {e}")
def generate_robots_txt(canonical_url, allowed_paths, output_dir):
"""Generate the robot.txt"""
robots_lines = ["User-agent: *"]
@@ -65,10 +69,11 @@ def generate_robots_txt(canonical_url, allowed_paths, output_dir):
except Exception as e:
logging.error(f"[✗] Failed to write robots.txt: {e}")
def generate_sitemap_xml(canonical_url, allowed_paths, output_dir):
"""Generate the sitemap"""
urlset_start = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
urlset_end = '</urlset>\n'
urlset_end = "</urlset>\n"
urls = ""
for path in allowed_paths:
loc = canonical_url.rstrip("/") + path
+15 -4
View File
@@ -1,8 +1,10 @@
import logging
from pathlib import Path
from PIL import Image, features
from shutil import copyfile
from PIL import Image, features
def convert_and_resize_image(input_path, output_path, resize=True, max_width=1140):
"""Convert an image to WebP (or JPEG fallback) and optionally resize it."""
try:
@@ -37,6 +39,7 @@ def convert_and_resize_image(input_path, output_path, resize=True, max_width=114
except Exception as e:
logging.error(f"[✗] Error processing image {input_path}: {e}")
def process_images(images, resize_images, img_dir, build_dir):
"""Process a list of image references and update paths to optimized versions."""
for img in images:
@@ -52,6 +55,7 @@ def process_images(images, resize_images, img_dir, build_dir):
if jpg_path.exists():
img["src"] = str(Path(img["src"]).with_suffix(".jpg"))
def copy_original_images(images, img_dir, build_dir):
"""Copy original image files without processing."""
for img in images:
@@ -70,6 +74,7 @@ def copy_original_images(images, img_dir, build_dir):
except Exception as e:
logging.error(f"[✗] Error copying {src_path}: {e}")
def get_favicon_path(theme_vars, theme_dir):
"""Retrieve the favicon path from theme variables, ensuring it exists."""
fav_path = theme_vars.get("favicon", {}).get("path")
@@ -87,6 +92,7 @@ def get_favicon_path(theme_vars, theme_dir):
return path
def generate_favicons_from_logo(theme_vars, theme_dir, output_dir):
"""Generate multiple PNG favicons from a single source image."""
logo_path = get_favicon_path(theme_vars, theme_dir)
@@ -97,9 +103,13 @@ def generate_favicons_from_logo(theme_vars, theme_dir, output_dir):
try:
output_dir.mkdir(parents=True, exist_ok=True)
specs = [
(32, "favicon-32.png"), (96, "favicon-96.png"), (128, "favicon-128.png"),
(192, "favicon-192.png"), (196, "favicon-196.png"),
(152, "favicon-152.png"), (180, "favicon-180.png")
(32, "favicon-32.png"),
(96, "favicon-96.png"),
(128, "favicon-128.png"),
(192, "favicon-192.png"),
(196, "favicon-196.png"),
(152, "favicon-152.png"),
(180, "favicon-180.png"),
]
img = Image.open(logo_path).convert("RGBA")
for size, name in specs:
@@ -110,6 +120,7 @@ def generate_favicons_from_logo(theme_vars, theme_dir, output_dir):
except Exception as e:
logging.error(f"[✗] Error generating PNG favicons: {e}")
def generate_favicon_ico(theme_vars, theme_dir, output_path):
"""Generate a multi-size favicon.ico from a source image."""
logo_path = get_favicon_path(theme_vars, theme_dir)
+34 -13
View File
@@ -2,14 +2,27 @@ import logging
from datetime import datetime
from pathlib import Path
from shutil import copyfile
from PIL import Image
from .utils import ensure_dir, copy_assets, load_yaml, load_theme_config
from .css_generator import generate_css_variables, generate_fonts_css, generate_google_fonts_link
from .image_processor import process_images, copy_original_images, convert_and_resize_image, generate_favicons_from_logo, generate_favicon_ico
from .html_generator import render_template, render_gallery_images, generate_gallery_json_from_images, generate_robots_txt, generate_sitemap_xml
from .html_generator import (
generate_gallery_json_from_images,
generate_robots_txt,
generate_sitemap_xml,
render_gallery_images,
render_template,
)
from .image_processor import (
copy_original_images,
generate_favicon_ico,
generate_favicons_from_logo,
process_images,
)
from .utils import copy_assets, ensure_dir, load_theme_config, load_yaml
# Configure logging to display only the messages
logging.basicConfig(level=logging.INFO, format='%(message)s')
logging.basicConfig(level=logging.INFO, format="%(message)s")
# Define key directories used throughout the script
SRC_DIR = Path.cwd()
@@ -22,9 +35,10 @@ GALLERY_FILE = SRC_DIR / "config/gallery.yaml"
SITE_FILE = SRC_DIR / "config/site.yaml"
THEMES_DIR = SRC_DIR / "config/themes"
VERSION_FILE = SRC_DIR / "VERSION"
with open(VERSION_FILE, "r") as vf:
with open(VERSION_FILE) as vf:
build_version = vf.read().strip()
def build():
logging.info("\n")
logging.info("=" * 24)
@@ -33,7 +47,7 @@ def build():
logging.info("\n === Starting build === ")
ensure_dir(BUILD_DIR)
copy_assets(JS_DIR, STYLE_DIR, BUILD_DIR)
# Defining build vars
build_date = datetime.now().strftime("%Y%m%d%H%M%S")
build_date_version = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -57,9 +71,13 @@ def build():
logging.info(f"[✓] Theme CSS found, copied to build folder: {dest_theme_css}")
else:
theme_css = ""
logging.warning(f"[~] No theme.css found in {theme_css_path}, skipping theme CSS injection.")
logging.warning(
f"[~] No theme.css found in {theme_css_path}, skipping theme CSS injection."
)
preload_links = generate_fonts_css(fonts_dir, BUILD_DIR / "style" / "fonts.css", fonts_cfg=theme_vars.get("fonts"))
preload_links = generate_fonts_css(
fonts_dir, BUILD_DIR / "style" / "fonts.css", fonts_cfg=theme_vars.get("fonts")
)
generate_css_variables(theme_vars.get("colors", {}), BUILD_DIR / "style" / "colors.css")
generate_favicons_from_logo(theme_vars, theme_dir, BUILD_DIR / "img" / "favicon")
generate_favicon_ico(theme_vars, theme_dir, BUILD_DIR / "favicon.ico")
@@ -94,7 +112,7 @@ def build():
# Adding Google fonts if existing
google_fonts_link = generate_google_fonts_link(theme_vars.get("google_fonts", []))
logging.info(f"[✓] Google Fonts link generated")
logging.info("[✓] Google Fonts link generated")
# Generating thumbnail
thumbnail_path = site_vars.get("social", {}).get("thumbnail")
@@ -124,11 +142,13 @@ def build():
head_vars["theme_css"] = theme_css
head_vars["build_date"] = build_date
head_vars["canonical"] = canonical_home
# Render the home page
head = render_template(TEMPLATE_DIR / "head.html", head_vars)
hero = render_template(TEMPLATE_DIR / "hero.html", {**site_vars["hero"], **head_vars})
footer = render_template(TEMPLATE_DIR / "footer.html", {**site_vars.get("footer", {}), **head_vars})
footer = render_template(
TEMPLATE_DIR / "footer.html", {**site_vars.get("footer", {}), **head_vars}
)
gallery_html = render_gallery_images(gallery_images)
gallery = render_template(TEMPLATE_DIR / "gallery.html", {"gallery_images": gallery_html})
@@ -185,7 +205,8 @@ def build():
generate_robots_txt(canonical_url, allowed_pages, BUILD_DIR)
generate_sitemap_xml(canonical_url, allowed_pages, BUILD_DIR)
else:
logging.warning("[~] No canonical URL found in site.yaml info section, skipping robots.txt and sitemap.xml generation.")
logging.warning(
"[~] No canonical URL found in site.yaml info section, skipping robots.txt and sitemap.xml generation."
)
logging.info("✅ Build complete.")
+10 -4
View File
@@ -1,26 +1,30 @@
import yaml
import logging
from pathlib import Path
from shutil import copytree, rmtree, copyfile
from shutil import copytree, rmtree
import yaml
def load_yaml(path):
"""Load gallery and site .yaml conf"""
if not path.exists():
logging.warning(f"[!] YAML file not found: {path}")
return {}
with open(path, "r", encoding="utf-8") as f:
with open(path, encoding="utf-8") as f:
return yaml.safe_load(f)
def load_theme_config(theme_name, themes_dir):
"""Load theme.yaml"""
theme_dir = themes_dir / theme_name
theme_config_path = theme_dir / "theme.yaml"
if not theme_config_path.exists():
raise FileNotFoundError(f"[✗] Theme config not found: {theme_config_path}")
with open(theme_config_path, "r", encoding="utf-8") as f:
with open(theme_config_path, encoding="utf-8") as f:
theme_vars = yaml.safe_load(f)
return theme_vars, theme_dir
def clear_dir(path: Path):
"""Clear the output dir"""
if not path.exists():
@@ -32,6 +36,7 @@ def clear_dir(path: Path):
elif child.is_dir():
rmtree(child)
def ensure_dir(path: Path):
"""Create the output dir if it does not exist"""
if not path.exists():
@@ -39,6 +44,7 @@ def ensure_dir(path: Path):
else:
clear_dir(path)
def copy_assets(js_dir, style_dir, build_dir):
"""Copy public assets to output dir"""
for folder in [js_dir, style_dir]: