# ============================================================================= # CI Workflow — Continuous Integration # ============================================================================= # Triggers on push and pull_request. # # Detection logic: # 1. Python: if requirements.txt exists → install deps, lint, test. # 2. Node/JS: if package.json exists → npm ci, lint, test, build. # 3. Neither detected → print a message and exit 0 (never fail). # # Controlled by .ci/config.env: # ENABLE_CI — master switch (default: true) # CI_STRICT — if true, lint/test failures fail the workflow # if false, failures are warnings only # # See docs/CI.md for full details. # ============================================================================= name: CI on: push: pull_request: jobs: ci: runs-on: ubuntu-latest steps: # ----------------------------------------------------------------------- # Step 1: Checkout the code # ----------------------------------------------------------------------- - name: Checkout uses: actions/checkout@v4 # ----------------------------------------------------------------------- # Step 2: Load configuration from .ci/config.env # ----------------------------------------------------------------------- - name: Load config id: config run: | # Source config.env to get ENABLE_CI, CI_STRICT, etc. if [ -f .ci/config.env ]; then # Export all non-comment, non-empty lines set -a source .ci/config.env set +a echo "Config loaded from .ci/config.env" else echo "WARNING: .ci/config.env not found, using defaults" ENABLE_CI=true CI_STRICT=true fi # Pass values to subsequent steps via environment file echo "ENABLE_CI=${ENABLE_CI:-true}" >> "$GITHUB_ENV" echo "CI_STRICT=${CI_STRICT:-true}" >> "$GITHUB_ENV" # ----------------------------------------------------------------------- # Step 3: Check master switch # ----------------------------------------------------------------------- - name: Check if CI is enabled run: | if [ "$ENABLE_CI" != "true" ]; then echo "CI is disabled (ENABLE_CI=$ENABLE_CI). Exiting." exit 0 fi # ----------------------------------------------------------------------- # Step 4: Detect project type # ----------------------------------------------------------------------- - name: Detect project type id: detect run: | HAS_PYTHON=false HAS_NODE=false if [ -f requirements.txt ] || [ -f setup.py ] || [ -f pyproject.toml ]; then HAS_PYTHON=true echo "Detected: Python project" fi if [ -f package.json ]; then HAS_NODE=true echo "Detected: Node.js project" fi if [ "$HAS_PYTHON" = "false" ] && [ "$HAS_NODE" = "false" ]; then echo "No Python or Node.js project detected. CI will skip gracefully." fi echo "HAS_PYTHON=${HAS_PYTHON}" >> "$GITHUB_ENV" echo "HAS_NODE=${HAS_NODE}" >> "$GITHUB_ENV" # ----------------------------------------------------------------------- # Step 5: Python — Setup & Install # ----------------------------------------------------------------------- - name: Set up Python if: env.HAS_PYTHON == 'true' uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install Python dependencies if: env.HAS_PYTHON == 'true' run: | python -m pip install --upgrade pip if [ -f requirements.txt ]; then pip install -r requirements.txt fi # Install optional dev tools (won't fail if not needed) pip install ruff black flake8 pytest 2>/dev/null || true # ----------------------------------------------------------------------- # Step 6: Python — Lint # Runs ruff, then black --check, then flake8. Each is optional: # only runs if the tool is installed AND relevant config exists. # ----------------------------------------------------------------------- - name: Python lint if: env.HAS_PYTHON == 'true' run: | EXIT_CODE=0 # --- ruff --- if command -v ruff >/dev/null 2>&1; then echo ">>> ruff check ." ruff check . || EXIT_CODE=$? else echo "SKIP: ruff not installed" fi # --- black --- if command -v black >/dev/null 2>&1; then echo ">>> black --check ." black --check . || EXIT_CODE=$? else echo "SKIP: black not installed" fi # --- flake8 --- if command -v flake8 >/dev/null 2>&1; then echo ">>> flake8 ." flake8 . || EXIT_CODE=$? else echo "SKIP: flake8 not installed" fi if [ "$EXIT_CODE" -ne 0 ]; then if [ "$CI_STRICT" = "true" ]; then echo "ERROR: Lint failed (CI_STRICT=true)" exit 1 else echo "WARNING: Lint issues found (CI_STRICT=false, continuing)" fi fi # ----------------------------------------------------------------------- # Step 7: Python — Test # Runs pytest if a tests/ directory or pytest config is detected. # ----------------------------------------------------------------------- - name: Python test if: env.HAS_PYTHON == 'true' run: | # Check if tests exist HAS_TESTS=false if [ -d tests ] || [ -d test ]; then HAS_TESTS=true fi # Check for pytest config in pyproject.toml or pytest.ini if [ -f pytest.ini ] || [ -f setup.cfg ]; then HAS_TESTS=true fi if [ -f pyproject.toml ] && grep -q '\[tool\.pytest' pyproject.toml 2>/dev/null; then HAS_TESTS=true fi if [ "$HAS_TESTS" = "true" ]; then echo ">>> pytest" if ! pytest; then if [ "$CI_STRICT" = "true" ]; then echo "ERROR: Tests failed (CI_STRICT=true)" exit 1 else echo "WARNING: Tests failed (CI_STRICT=false, continuing)" fi fi else echo "SKIP: No tests detected (no tests/ dir or pytest config)" fi # ----------------------------------------------------------------------- # Step 8: Node.js — Setup & Install # ----------------------------------------------------------------------- - name: Set up Node.js if: env.HAS_NODE == 'true' uses: actions/setup-node@v4 with: node-version: "lts/*" - name: Install Node dependencies if: env.HAS_NODE == 'true' run: npm ci # ----------------------------------------------------------------------- # Step 9: Node.js — Lint (only if "lint" script exists in package.json) # ----------------------------------------------------------------------- - name: Node lint if: env.HAS_NODE == 'true' run: | if grep -q '"lint"' package.json 2>/dev/null; then echo ">>> npm run lint" if ! npm run lint; then if [ "$CI_STRICT" = "true" ]; then echo "ERROR: Lint failed (CI_STRICT=true)" exit 1 else echo "WARNING: Lint issues found (CI_STRICT=false, continuing)" fi fi else echo "SKIP: no 'lint' script in package.json" fi # ----------------------------------------------------------------------- # Step 10: Node.js — Test (only if "test" script exists) # ----------------------------------------------------------------------- - name: Node test if: env.HAS_NODE == 'true' run: | if grep -q '"test"' package.json 2>/dev/null; then echo ">>> npm test" if ! npm test; then if [ "$CI_STRICT" = "true" ]; then echo "ERROR: Tests failed (CI_STRICT=true)" exit 1 else echo "WARNING: Tests failed (CI_STRICT=false, continuing)" fi fi else echo "SKIP: no 'test' script in package.json" fi # ----------------------------------------------------------------------- # Step 11: Node.js — Build (only if "build" script exists) # ----------------------------------------------------------------------- - name: Node build if: env.HAS_NODE == 'true' run: | if grep -q '"build"' package.json 2>/dev/null; then echo ">>> npm run build" npm run build else echo "SKIP: no 'build' script in package.json" fi # ----------------------------------------------------------------------- # Step 12: Summary # ----------------------------------------------------------------------- - name: CI Summary if: always() run: | echo "==============================" echo " CI Complete" echo " Python detected: $HAS_PYTHON" echo " Node detected: $HAS_NODE" echo " Strict mode: $CI_STRICT" echo "=============================="