Continuous Integration & Git Pre-commit Hooks
To ensure high software quality, prevent regressions, and avoid merge conflicts, the TimeManagement project employs a two-tier verification architecture:
- Local Git Pre-commit Hook: Fast, client-side, read-only checks run immediately upon
git committo validate syntax and imports on staged files. - Continuous Integration (CI) Workflow: Remote GitHub Actions pipeline running test suites and codebase validation whenever changes are pushed to
mainorreleasebranches, as well as on pull requests.
1. Continuous Integration (GitHub Actions)โ
A GitHub Actions Continuous Integration (CI) workflow is configured for the project repository. It automatically executes tests and verifies code integrity under the following conditions:
- Triggers:
- Any
pushevent to themainbranch. - Any
pushevent to thereleasebranch. - Pull requests targeting
mainorrelease.
- Any
- Automated Tasks:
- Test Suite Execution: Runs backend and module test suites to ensure existing functionality remains intact.
- Syntax & Import Validation: Executes linting checks across all QML, JavaScript, and Python files to catch broken import paths, syntax errors, or unimported services.
- Build Integrity: Confirms that packages build cleanly without missing dependencies or assets.
- PR Policy: All tests and CI checks must pass before pull requests can be reviewed and merged into protected branches.
2. Git Pre-commit Hookโ
Running CI on GitHub is essential, but waiting for remote checks after pushing slows down development. The local Git pre-commit hook runs in less than a second and catches errors right at commit time.
Key Featuresโ
- Staged Files Only: Only checks files currently staged (
git add) for commit (.qml,.js, and.py). - Read-Only: The hook inspects files without formatting or altering them, ensuring no unexpected file mutations or merge conflicts.
- Fast & Lightweight: Skips cleanly if only docs, markdown, or config files are staged.
- Emergency Bypass: In rare emergencies where a commit must be recorded despite a lint issue, use:
git commit --no-verify
3. Setup and Installationโ
Prerequisitesโ
Ensure you have the following installed on your development workstation:
- Git:
git --version - Python 3:
python3 --version - qmllint: Provided by Qt development packages:
# Ubuntu / Debiansudo apt install qtdeclarative5-dev-tools# Or for Qt6-based environments:sudo apt install qml-tools
Running the Setup Scriptโ
In the root of the project repository, run the hook setup script:
chmod +x scripts/setup_hooks.sh
./scripts/setup_hooks.sh
Upon success, you will see:
[SUCCESS] Git pre-commit hook successfully installed at: .git/hooks/pre-commit
4. Verification Scripts in Detailโ
The verification system relies on three scripts located in the scripts/ directory of the project:
Script 1: scripts/setup_hooks.shโ
This installation script writes the .git/hooks/pre-commit hook file and makes it executable.
#!/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โ
This standalone Python utility conducts deep validation on QML and JavaScript files. It performs four vital checks:
- Root Module Imports: Verifies that
.qmlfiles defining visual or logical components include proper import declarations (e.g.QtQuickorLomiri). - Relative Path Resolution (QML): Confirms that relative files referenced in
import "path"orimport "../path" as Aliasactually exist on disk. - JavaScript Pragma Imports: Confirms that
.import "path" as Aliasstatements in.pragma libraryJavaScript files resolve to valid paths. - Known Service Usage: Detects when singleton services (such as
TimerService,Utils,Logger,DraftManager,MainModel,NavigationRoutes) are invoked without being explicitly imported or declared.
#!/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โ
The primary test runner called by the pre-commit hook and CI pipelines. It executes:
qmllintwith appropriate import include flags (-I qml -I models -I qml/components/system).scripts/check_imports.pyon QML and JavaScript files.python3 -m py_compilefor Python syntax verification.
#!/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. Manual Executionโ
You can run linting checks locally at any time without triggering a commit:
Lint Specific Filesโ
./scripts/lint.sh qml/Main.qml src/backend.py
Lint the Entire Codebaseโ
./scripts/lint.sh
Validate Imports Directlyโ
python3 scripts/check_imports.py qml/Main.qml models/TaskModel.js
6. Troubleshootingโ
[!WARNING] If
qmllintis missing,scripts/lint.shwill exit with an error. Ensure you installqtdeclarative5-dev-tools(or equivalent Qt development package) on your Linux machine.
- Hook does not execute upon
git commit: Check that.git/hooks/pre-commitexists and has execute permissions:chmod +x .git/hooks/pre-commit - Linting failed on staged files:
Read the error message output carefully. Fix the syntax or missing import, stage the corrected files with
git add, and repeatgit commit. - Emergency bypass:
If you must commit while an issue is being resolved offline, append
--no-verify:Note that remote CI will still run and block merge intogit commit -m "WIP: emergency fix" --no-verifymainandreleasebranches until all checks succeed.