Continue integratie & Git pre-commit hooks
Om een hoge softwarekwaliteit te waarborgen, regressies te voorkomen en mergeconflicten te vermijden, maakt het TimeManagement-project gebruik van een verificatiearchitectuur met twee lagen:
- Lokale Git pre-commit hook: Snelle, alleen-lezen controles aan de clientzijde die direct worden uitgevoerd bij
git commitom syntaxis en imports in staged bestanden te valideren. - Continue integratie (CI) workflow: Een externe GitHub Actions-pipeline die testsuites en codebase-verificatie uitvoert wanneer wijzigingen worden gepusht naar de
main- ofrelease-takken, evenals bij pull-aanvragen.
1. Continue integratie (GitHub Actions)
Er is een GitHub Actions Continuous Integration (CI)-workflow geconfigureerd voor de projectrepository. Deze voert automatisch tests uit en verifieert de integriteit van de code onder de volgende voorwaarden:
- Triggers:
- Elk
push-evenement naar demain-branch. - Elk
push-evenement naar derelease-branch. - Pull-aanvragen gericht op
mainofrelease.
- Elk
- Geautomatiseerde taken:
- Uitvoering van testsuites: Voert backend- en moduletestsuites uit om ervoor te zorgen dat bestaande functionaliteit intact blijft.
- Syntaxis- en importvalidatie: Voert lintingcontroles uit op alle QML-, JavaScript- en Python-bestanden om verbroken importpaden, syntaxisfouten of niet-geïmporteerde services op te sporen.
- Build-integriteit: Bevestigt dat pakketten schoon bouwen zonder ontbrekende afhankelijkheden of assets.
- PR-beleid: Alle tests en CI-controles moeten slagen voordat pull-aanvragen kunnen worden beoordeeld en gemerged in beveiligde branches.
2. Git pre-commit hook
Het uitvoeren van CI op GitHub is essentieel, maar wachten op externe controles na het pushen vertraagt het ontwikkelproces. De lokale Git pre-commit hook wordt binnen een seconde uitgevoerd en detecteert fouten direct tijdens het committen.
Belangrijkste kenmerken
- Alleen staged bestanden: Controleert uitsluitend bestanden die momenteel zijn klaargezet (
git add) voor commit (.qml,.jsen.py). - Alleen-lezen: De hook inspecteert bestanden zonder ze te formatteren of aan te passen, waardoor onverwachte bestandswijzigingen of mergeconflicten worden voorkomen.
- Snel & lichtgewicht: Slaat de controle netjes over als er alleen documentatie, markdown- of configuratiebestanden zijn klaargezet.
- Noodomzeiling: In zeldzame noodgevallen waarin een commit toch moet worden vastgelegd ondanks een lintprobleem, gebruikt u:
git commit --no-verify
3. Installatie en configuratie
Vereisten
Zorg ervoor dat het volgende op uw ontwikkelwerkstation is geïnstalleerd:
- Git:
git --version - Python 3:
python3 --version - qmllint: Geleverd door Qt-ontwikkelpakketten:
# Ubuntu / Debiansudo apt install qtdeclarative5-dev-tools# Of voor Qt6-gebaseerde omgevingen:sudo apt install qml-tools
Het installatiescript uitvoeren
Voer in de hoofdmap van de projectrepository het hook-installatiescript uit:
chmod +x scripts/setup_hooks.sh
./scripts/setup_hooks.sh
Na succesvolle installatie ziet u:
[SUCCESS] Git pre-commit hook successfully installed at: .git/hooks/pre-commit
4. Verificatiescripts in detail
Het verificatiesysteem vertrouwt op drie scripts in de map scripts/ van het project:
Script 1: scripts/setup_hooks.sh
Dit installatiescript schrijft het hookbestand .git/hooks/pre-commit en maakt het uitvoerbaar.
#!/usr/bin/env bash
# ==============================================================================
# Git Pre-commit Hook Setup Script
# Installs local pre-commit hook to lint only staged files before committing.
# ==============================================================================
set -e
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HOOK_DEST="$PROJECT_ROOT/.git/hooks/pre-commit"
if [ ! -d "$PROJECT_ROOT/.git" ]; then
echo "[ERROR] Not a git repository."
exit 1
fi
cat << 'EOF' > "$HOOK_DEST"
#!/usr/bin/env bash
# ==============================================================================
# Git Pre-commit Hook: Lint Staged Files Only (Read-Only)
# Blocks commit if syntax or import errors are detected in staged files.
# ==============================================================================
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
LINT_SCRIPT="$PROJECT_ROOT/scripts/lint.sh"
if [ ! -x "$LINT_SCRIPT" ]; then
echo "[WARN] $LINT_SCRIPT not found or not executable. Skipping pre-commit lint."
exit 0
fi
# Get staged QML, JS, and Python files that were added/copied/modified
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(qml|js|py)$' || true)
if [ -z "$STAGED_FILES" ]; then
# No relevant files staged
exit 0
fi
echo "[INFO] Running pre-commit syntax and import checks on staged files..."
if ! "$LINT_SCRIPT" $STAGED_FILES; then
echo ""
echo "[ERROR] Commit aborted: Linting errors detected in staged files."
echo "[HINT] Fix the issues above, stage your changes, and commit again."
echo "[HINT] Emergency bypass if required: git commit --no-verify"
exit 1
fi
EOF
chmod +x "$HOOK_DEST"
echo "[SUCCESS] Git pre-commit hook successfully installed at: $HOOK_DEST"
Script 2: scripts/check_imports.py
Dit zelfstandige Python-hulpprogramma voert een diepe validatie uit op QML- en JavaScript-bestanden. Het voert vier essentiële controles uit:
- Basisinvoer van modules: Controleert of
.qml-bestanden die visuele of logische componenten definiëren de juiste importdeclaraties bevatten (bijv.QtQuickofLomiri). - Relatieve padresolutie (QML): Bevestigt dat relatieve bestanden waarnaar wordt verwezen in
import "path"ofimport "../path" as Aliasdaadwerkelijk op de schijf bestaan. - JavaScript Pragma-imports: Bevestigt dat
.import "path" as Alias-instructies in.pragma libraryJavaScript-bestanden verwijzen naar geldige paden. - Gebruik van bekende services: Detecteert wanneer singleton-services (zoals
TimerService,Utils,Logger,DraftManager,MainModel,NavigationRoutes) worden aangeroepen zonder expliciet te zijn geïmporteerd of gedeclareerd.
#!/usr/bin/env python3
"""
QML and JavaScript Import & Symbol Validator.
Validates:
1. Required module imports in .qml files (ensures file is not missing QtQuick/Lomiri).
2. Existence of relative imported files and folders (e.g. import "../../models/foo.js").
3. Usage of common namespace aliases (TimerService, Utils, Logger, etc.) without import.
"""
import sys
import os
import re
KNOWN_SERVICES = [
"TimerService",
"Utils",
"Logger",
"DraftManager",
"MainModel",
"NavigationRoutes"
]
def validate_file(filepath):
errors = []
if not os.path.isfile(filepath):
return [f"File not found: {filepath}"]
try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
except Exception as e:
return [f"Could not read file: {e}"]
file_dir = os.path.dirname(os.path.abspath(filepath))
# Check 1: Missing base imports in .qml files
if filepath.endswith(".qml"):
import_lines = re.findall(r"^\s*import\s+.+$", content, re.MULTILINE)
has_root_object = re.search(r"^\s*(?:[A-Z]\w+|Item|Rectangle|Page|Component|QtObject|Column|Row)\s*\{", content, re.MULTILINE)
if not import_lines and has_root_object:
errors.append("File contains QML objects but has NO import statements (missing QtQuick/Lomiri imports).")
# Check 2: Relative file/folder imports in QML
# Matches: import "path" or import "path" as Alias or import "../path"
qml_imports = re.findall(r"^\s*import\s+[\"']([^\"']+)[\"'](?:\s+as\s+(\w+))?", content, re.MULTILINE)
for rel_path, alias in qml_imports:
target_path = os.path.normpath(os.path.join(file_dir, rel_path))
if not os.path.exists(target_path):
errors.append(f"Import path not found: \"{rel_path}\" (resolved to: {target_path})")
# Check 3: Relative .import in JavaScript files (.pragma library)
# Matches: .import "path" as Alias
js_imports = re.findall(r"^\s*\.import\s+[\"']([^\"']+)[\"']\s+as\s+(\w+)", content, re.MULTILINE)
for rel_path, alias in js_imports:
target_path = os.path.normpath(os.path.join(file_dir, rel_path))
if not os.path.exists(target_path):
errors.append(f"JS pragma import not found: \"{rel_path}\" (resolved to: {target_path})")
# Check 4: Unimported service aliases
for service in KNOWN_SERVICES:
# Check if service is called (e.g. TimerService.start())
if re.search(r"\b" + service + r"\.", content):
# Check if imported in QML or JS
qml_alias = re.search(r"import\s+.*?as\s+" + service + r"\b", content)
js_alias = re.search(r"\.import\s+.*?as\s+" + service + r"\b", content)
# Or defined in the file itself (e.g. var TimerService = ...)
local_decl = re.search(r"\b(?:var|let|const|function|property\s+var)\s+" + service + r"\b", content)
if not qml_alias and not js_alias and not local_decl:
# Exclude file if it's the actual implementation file of that service
base_name = os.path.splitext(os.path.basename(filepath))[0]
if base_name.lower() not in service.lower():
errors.append(f"Identifier \"{service}\" is used, but missing \"import ... as {service}\"")
return errors
def main():
if len(sys.argv) < 2:
print("Usage: check_imports.py <file1> [file2...]")
sys.exit(1)
has_errors = False
for filepath in sys.argv[1:]:
if not filepath.endswith((".qml", ".js")):
continue
errs = validate_file(filepath)
if errs:
has_errors = True
print(f"[FAIL] Import check failed in {filepath}:")
for err in errs:
print(f" - {err}")
if has_errors:
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()
Script 3: scripts/lint.sh
De primaire testuitvoerder die wordt aangeroepen door de pre-commit hook en CI-pipelines. Het script voert de volgende stappen uit:
qmllintmet de juiste import-includevlaggen (-I qml -I models -I qml/components/system).scripts/check_imports.pyop QML- en JavaScript-bestanden.python3 -m py_compilevoor validatie van de Python-syntaxis.
#!/usr/bin/env bash
# ==============================================================================
# Local Linting & Syntax Verification Script
# Read-only verification for QML, JavaScript, and Python files.
# Never modifies files or causes merge conflicts.
# ==============================================================================
set -o pipefail
# Text formatting
if [ -t 1 ]; then
BOLD="\033[1m"
GREEN="\033[32m"
RED="\033[31m"
YELLOW="\033[33m"
BLUE="\033[34m"
RESET="\033[0m"
else
BOLD=""
GREEN=""
RED=""
YELLOW=""
BLUE=""
RESET=""
fi
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PROJECT_ROOT"
ERRORS=0
CHECKED=0
FAILED_FILES=()
# Check for qmllint
if ! command -v qmllint &> /dev/null; then
echo -e "${RED}[ERROR] 'qmllint' is not installed or not in PATH.${RESET}"
echo "Install it via your Qt/Ubuntu development packages (e.g. qtdeclarative5-dev-tools or qml-tools)."
exit 1
fi
IMPORT_CHECKER="$PROJECT_ROOT/scripts/check_imports.py"
lint_qml_js() {
local file="$1"
((CHECKED++))
local file_failed=0
# 1. Grammar & Bracket Syntax check via qmllint
local qml_out
qml_out=$(qmllint -I qml -I models -I qml/components/system "$file" 2>&1)
local qml_status=$?
if [ $qml_status -ne 0 ] || [ -n "$qml_out" ]; then
echo -e "${RED}[FAIL] Syntax Error:${RESET} $file"
if [ -n "$qml_out" ]; then
echo "$qml_out" | sed 's/^/ /'
fi
file_failed=1
fi
# 2. Deep Import & Symbol validation via check_imports.py
if [ -x "$IMPORT_CHECKER" ]; then
local imp_out
imp_out=$("$IMPORT_CHECKER" "$file" 2>&1)
local imp_status=$?
if [ $imp_status -ne 0 ]; then
echo -e "${RED}[FAIL] Import Error:${RESET} $file"
if [ -n "$imp_out" ]; then
echo "$imp_out" | sed 's/^/ /'
fi
file_failed=1
fi
fi
if [ $file_failed -eq 1 ]; then
((ERRORS++))
FAILED_FILES+=("$file")
fi
}
lint_python() {
local file="$1"
((CHECKED++))
local output
output=$(python3 -m py_compile "$file" 2>&1)
local status=$?
if [ $status -ne 0 ]; then
echo -e "${RED}[FAIL] Python Syntax Error:${RESET} $file"
if [ -n "$output" ]; then
echo "$output" | sed 's/^/ /'
fi
((ERRORS++))
FAILED_FILES+=("$file")
fi
}
echo -e "${BOLD}${BLUE}[INFO] Starting codebase linting...${RESET}\n"
# If specific files were passed as arguments, check only those
if [ "$#" -gt 0 ]; then
for file in "$@"; do
if [ ! -f "$file" ]; then
echo -e "${YELLOW}[WARN] Skipping non-existent file: $file${RESET}"
continue
fi
case "$file" in
*.qml|*.js)
lint_qml_js "$file"
;;
*.py)
lint_python "$file"
;;
*)
# Non-lintable file, skip silently
;;
esac
done
else
# Full codebase scan (excluding build, .git, .clickable, .agent)
while IFS= read -r -d '' file; do
lint_qml_js "$file"
done < <(find qml models -type f \( -name "*.qml" -o -name "*.js" \) -not -path "*/build/*" -print0)
while IFS= read -r -d '' file; do
lint_python "$file"
done < <(find . -maxdepth 2 -type f -name "*.py" -not -path "*/build/*" -not -path "*/.git/*" -not -path "*/.clickable/*" -not -path "*/.agent/*" -print0)
if [ -d "src" ]; then
while IFS= read -r -d '' file; do
lint_python "$file"
done < <(find src -type f -name "*.py" -not -path "*/__pycache__/*" -print0)
fi
fi
echo ""
echo "------------------------------------------------------------"
if [ $ERRORS -eq 0 ]; then
echo -e "${GREEN}${BOLD}[SUCCESS] All $CHECKED files passed syntax and import validation.${RESET}"
exit 0
else
echo -e "${RED}${BOLD}[ERROR] Linting failed: $ERRORS error(s) found across $CHECKED checked files.${RESET}"
echo -e "${RED}Failed files:${RESET}"
for failed in "${FAILED_FILES[@]}"; do
echo -e " - $failed"
done
exit 1
fi
5. Handmatige uitvoering
U kunt lintingcontroles op elk gewenst moment lokaal uitvoeren zonder een commit te activeren:
Specifieke bestanden controleren
./scripts/lint.sh qml/Main.qml src/backend.py
De volledige codebase controleren
./scripts/lint.sh
Imports direct valideren
python3 scripts/check_imports.py qml/Main.qml models/TaskModel.js
6. Probleemoplossing
[!WARNING] Als
qmllintontbreekt, wordtscripts/lint.shafgebroken met een foutmelding. Zorg ervoor dat uqtdeclarative5-dev-tools(of een vergelijkbaar Qt-ontwikkelpakket) op uw Linux-systeem installeert.
- Hook wordt niet uitgevoerd bij
git commit: Controleer of.git/hooks/pre-commitbestaat en uitvoeringsrechten heeft:chmod +x .git/hooks/pre-commit - Linting mislukt op staged bestanden:
Lees de foutmelding zorgvuldig. Corrigeer de syntaxis of de ontbrekende import, zet de gecorrigeerde bestanden klaar met
git adden herhaalgit commit. - Noodomzeiling:
Als u toch moet committen terwijl een probleem offline wordt opgelost, voegt u
--no-verifytoe:Houd er rekening mee dat externe CI nog steeds wordt uitgevoerd en het mergen naargit commit -m "WIP: noodherstel" --no-verifymain- enrelease-branches blokkeert totdat alle controles slagen.