Skip to content

feat: add initial ci.yml workflow #5

feat: add initial ci.yml workflow

feat: add initial ci.yml workflow #5

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# Discover all projects in the repository and filter by changed files
discover:
runs-on: ubuntu-latest
outputs:
node-projects: ${{ steps.filter-projects.outputs.node-projects }}
poetry-projects: ${{ steps.filter-projects.outputs.poetry-projects }}
uv-projects: ${{ steps.filter-projects.outputs.uv-projects }}
pip-projects: ${{ steps.filter-projects.outputs.pip-projects }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed files
id: changed-files
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
# For PRs, compare against the base branch
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | tr '\n' ' ')
else
# For pushes, compare against the previous commit
CHANGED_FILES=$(git diff --name-only HEAD~1...HEAD 2>/dev/null | tr '\n' ' ' || echo "")
fi
echo "Changed files: $CHANGED_FILES"
echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT
- name: Find all projects
id: find-projects
run: |
# Find Node.js projects (have package.json but not in node_modules)
NODE_PROJECTS=$(find . -name "package.json" -not -path "*/node_modules/*" -exec dirname {} \; | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "node-projects=$NODE_PROJECTS" >> $GITHUB_OUTPUT
# Find Poetry projects (have poetry.lock)
POETRY_PROJECTS=$(find . -name "poetry.lock" -exec dirname {} \; | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "poetry-projects=$POETRY_PROJECTS" >> $GITHUB_OUTPUT
# Find uv projects (have uv.lock)
UV_PROJECTS=$(find . -name "uv.lock" -exec dirname {} \; | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "uv-projects=$UV_PROJECTS" >> $GITHUB_OUTPUT
# Find pip projects (have requirements.txt but no poetry.lock or uv.lock)
PIP_PROJECTS=$(find . -name "requirements.txt" -exec dirname {} \; | while read dir; do
if [ ! -f "$dir/poetry.lock" ] && [ ! -f "$dir/uv.lock" ]; then
echo "$dir"
fi
done | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "pip-projects=$PIP_PROJECTS" >> $GITHUB_OUTPUT
- name: Filter projects by changed files
id: filter-projects
run: |
CHANGED_FILES="${{ steps.changed-files.outputs.files }}"
# If no changed files detected (e.g., first commit), run all projects
if [ -z "$CHANGED_FILES" ]; then
echo "No changed files detected, running all projects"
echo "node-projects=${{ steps.find-projects.outputs.node-projects }}" >> $GITHUB_OUTPUT
echo "poetry-projects=${{ steps.find-projects.outputs.poetry-projects }}" >> $GITHUB_OUTPUT
echo "uv-projects=${{ steps.find-projects.outputs.uv-projects }}" >> $GITHUB_OUTPUT
echo "pip-projects=${{ steps.find-projects.outputs.pip-projects }}" >> $GITHUB_OUTPUT
exit 0
fi
# Check if CI workflow itself changed - if so, run all projects
if echo "$CHANGED_FILES" | grep -q ".github/workflows/ci.yml"; then
echo "CI workflow changed, running all projects"
echo "node-projects=${{ steps.find-projects.outputs.node-projects }}" >> $GITHUB_OUTPUT
echo "poetry-projects=${{ steps.find-projects.outputs.poetry-projects }}" >> $GITHUB_OUTPUT
echo "uv-projects=${{ steps.find-projects.outputs.uv-projects }}" >> $GITHUB_OUTPUT
echo "pip-projects=${{ steps.find-projects.outputs.pip-projects }}" >> $GITHUB_OUTPUT
exit 0
fi
# Function to filter projects based on changed files
filter_projects() {
local projects_json="$1"
local result=""
# Parse JSON array and check each project
for project in $(echo "$projects_json" | jq -r '.[]'); do
# Remove leading ./ for comparison
project_path="${project#./}"
# Check if any changed file starts with this project path
for changed_file in $CHANGED_FILES; do
if [[ "$changed_file" == "$project_path"* ]] || [[ "./$changed_file" == "$project"* ]]; then
if [ -z "$result" ]; then
result="$project"
else
result="$result"$'\n'"$project"
fi
break
fi
done
done
# Convert to JSON array
if [ -z "$result" ]; then
echo "[]"
else
echo "$result" | jq -R -s -c 'split("\n") | map(select(length > 0))'
fi
}
# Filter each project type
NODE_FILTERED=$(filter_projects '${{ steps.find-projects.outputs.node-projects }}')
POETRY_FILTERED=$(filter_projects '${{ steps.find-projects.outputs.poetry-projects }}')
UV_FILTERED=$(filter_projects '${{ steps.find-projects.outputs.uv-projects }}')
PIP_FILTERED=$(filter_projects '${{ steps.find-projects.outputs.pip-projects }}')
echo "node-projects=$NODE_FILTERED" >> $GITHUB_OUTPUT
echo "poetry-projects=$POETRY_FILTERED" >> $GITHUB_OUTPUT
echo "uv-projects=$UV_FILTERED" >> $GITHUB_OUTPUT
echo "pip-projects=$PIP_FILTERED" >> $GITHUB_OUTPUT
# Debug output
echo "Filtered Node.js projects: $NODE_FILTERED"
echo "Filtered Poetry projects: $POETRY_FILTERED"
echo "Filtered uv projects: $UV_FILTERED"
echo "Filtered pip projects: $PIP_FILTERED"
# Build and lint Node.js projects
node:
needs: discover
if: ${{ needs.discover.outputs.node-projects != '[]' && needs.discover.outputs.node-projects != '' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project: ${{ fromJson(needs.discover.outputs.node-projects) }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Get npm cache directory
id: npm-cache-dir
shell: bash
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles(format('{0}/package-lock.json', matrix.project), format('{0}/package.json', matrix.project)) }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
working-directory: ${{ matrix.project }}
run: npm install
- name: Build
working-directory: ${{ matrix.project }}
run: |
if npm run build --if-present 2>/dev/null; then
echo "Build completed successfully"
else
echo "No build script found, skipping..."
fi
- name: Lint
working-directory: ${{ matrix.project }}
run: |
if npm run lint --if-present 2>/dev/null; then
echo "Lint completed successfully"
else
echo "No lint script found, skipping..."
fi
# Build and lint Poetry projects
poetry:
needs: discover
if: ${{ needs.discover.outputs.poetry-projects != '[]' && needs.discover.outputs.poetry-projects != '' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project: ${{ fromJson(needs.discover.outputs.poetry-projects) }}
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
- name: Cache Poetry dependencies
uses: actions/cache@v4
with:
path: ${{ matrix.project }}/.venv
key: ${{ runner.os }}-poetry-${{ hashFiles(format('{0}/poetry.lock', matrix.project)) }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Install dependencies
working-directory: ${{ matrix.project }}
run: poetry install --no-interaction
- name: Lint with ruff (if available)
working-directory: ${{ matrix.project }}
run: |
if poetry run ruff check . 2>/dev/null; then
echo "Ruff lint completed"
elif poetry run flake8 . 2>/dev/null; then
echo "Flake8 lint completed"
else
echo "No Python linter found, skipping..."
fi
continue-on-error: true
- name: Type check with mypy (if available)
working-directory: ${{ matrix.project }}
run: |
if poetry run mypy . 2>/dev/null; then
echo "Type check completed"
else
echo "No type checker found, skipping..."
fi
continue-on-error: true
# Build and lint uv projects
uv:
needs: discover
if: ${{ needs.discover.outputs.uv-projects != '[]' && needs.discover.outputs.uv-projects != '' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project: ${{ fromJson(needs.discover.outputs.uv-projects) }}
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Install dependencies
working-directory: ${{ matrix.project }}
run: uv sync
- name: Lint with ruff (if available)
working-directory: ${{ matrix.project }}
run: |
if uv run ruff check . 2>/dev/null; then
echo "Ruff lint completed"
elif uv run flake8 . 2>/dev/null; then
echo "Flake8 lint completed"
else
echo "No Python linter found, skipping..."
fi
continue-on-error: true
- name: Type check with mypy (if available)
working-directory: ${{ matrix.project }}
run: |
if uv run mypy . 2>/dev/null; then
echo "Type check completed"
else
echo "No type checker found, skipping..."
fi
continue-on-error: true
# Build and lint pip projects
pip:
needs: discover
if: ${{ needs.discover.outputs.pip-projects != '[]' && needs.discover.outputs.pip-projects != '' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project: ${{ fromJson(needs.discover.outputs.pip-projects) }}
steps:
- uses: actions/checkout@v4
- name: Detect Python version
id: python-version
working-directory: ${{ matrix.project }}
run: |
if [ -f ".python-version" ]; then
VERSION=$(cat .python-version | tr -d '[:space:]')
echo "version=$VERSION" >> $GITHUB_OUTPUT
else
echo "version=3.12" >> $GITHUB_OUTPUT
fi
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ steps.python-version.outputs.version }}
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles(format('{0}/requirements.txt', matrix.project)) }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
working-directory: ${{ matrix.project }}
run: |
python -m pip install --upgrade pip
# Use project-level pip.conf if it exists
if [ -f "pip.conf" ]; then
export PIP_CONFIG_FILE="$(pwd)/pip.conf"
fi
pip install -r requirements.txt
- name: Install linters
run: pip install ruff flake8
continue-on-error: true
- name: Lint with ruff
working-directory: ${{ matrix.project }}
run: ruff check . || echo "Ruff lint skipped or failed"
continue-on-error: true
# Summary job to ensure all checks passed
ci-success:
needs: [node, poetry, uv, pip]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all jobs passed
run: |
# Check each job result - 'skipped' is OK (no projects to build)
# 'success' is OK, 'failure' is not OK
NODE_RESULT="${{ needs.node.result }}"
POETRY_RESULT="${{ needs.poetry.result }}"
UV_RESULT="${{ needs.uv.result }}"
PIP_RESULT="${{ needs.pip.result }}"
echo "Node job result: $NODE_RESULT"
echo "Poetry job result: $POETRY_RESULT"
echo "uv job result: $UV_RESULT"
echo "pip job result: $PIP_RESULT"
if [ "$NODE_RESULT" == "failure" ] || \
[ "$POETRY_RESULT" == "failure" ] || \
[ "$UV_RESULT" == "failure" ] || \
[ "$PIP_RESULT" == "failure" ]; then
echo "One or more jobs failed"
exit 1
fi
echo "All CI checks passed!"