feat: adding new skills, including testing patterns and methodologies, along with bundled resources for better usability.

This commit is contained in:
duthaho
2026-03-30 12:18:00 +07:00
parent 0ff5ae4082
commit 7fa9a48c6c
89 changed files with 25808 additions and 923 deletions
+745 -29
View File
@@ -1,20 +1,33 @@
---
name: github-actions
description: >
Use this skill whenever setting up or modifying GitHub Actions CI/CD workflows, automating tests, builds, or deployments on GitHub. Trigger on keywords like GitHub Actions, workflow YAML, CI/CD pipeline, actions/checkout, matrix builds, workflow_dispatch, or .github/workflows. Also applies when configuring caching in workflows, managing GitHub secrets, or troubleshooting failed workflow runs.
---
# GitHub Actions
## Description
GitHub Actions CI/CD workflows including testing, building, and deployment.
## When to Use
- Setting up CI/CD pipelines
- Automating tests and builds
- Deployment automation
## When NOT to Use
- GitLab CI projects using `.gitlab-ci.yml` configuration
- Jenkins pipelines using Jenkinsfile or Groovy-based configuration
- CircleCI, Travis CI, or other non-GitHub CI/CD systems
---
## Core Patterns
### Basic CI Workflow
### 1. CI Pipeline
Complete CI workflow covering checkout, setup, install, lint, test, and build for
both Python and Node.js projects.
#### Node.js CI Pipeline
```yaml
name: CI
@@ -25,64 +38,767 @@ on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
test:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
node-version: "20"
cache: "pnpm"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
test:
name: Test
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm test -- --coverage
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
build:
name: Build
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 5
```
#### Python CI Pipeline
```yaml
name: CI - Python
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements-dev.txt
- run: ruff check .
- run: ruff format --check .
- run: mypy src/
test:
name: Test
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements.txt -r requirements-dev.txt
- name: Run tests
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
run: pytest -v --cov=src --cov-report=xml
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-xml
path: coverage.xml
retention-days: 7
```
---
### 2. Matrix Strategy
Matrix builds run the same job across multiple combinations of OS, language
version, or other variables.
#### OS and version matrix
```yaml
jobs:
test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: "npm"
- run: npm ci
- run: npm test
```
### Matrix Builds
#### Include and exclude
```yaml
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: [18, 20]
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.11", "3.12"]
exclude:
# Skip Python 3.11 on Windows
- os: windows-latest
python: "3.11"
include:
# Add a specific combination with extra env
- os: ubuntu-latest
python: "3.13"
experimental: true
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental || false }}
steps:
- uses: actions/setup-node@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
node-version: ${{ matrix.node }}
python-version: ${{ matrix.python }}
- run: pip install -r requirements.txt
- run: pytest
```
### Caching
---
### 3. Caching
Caching avoids re-downloading dependencies on every run. Use `hashFiles` to
generate cache keys from lockfiles so the cache invalidates when dependencies
change.
#### npm cache
```yaml
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: npm-
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
```
### Secrets
#### pnpm cache
```yaml
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: deploy --key "$API_KEY"
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "store=$(pnpm store path)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.store }}
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
```
#### pip cache
```yaml
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
pip-${{ runner.os }}-
```
#### Docker layer cache
```yaml
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
```
---
### 4. Reusable Workflows
Reusable workflows let you define a workflow once and call it from other
workflows, reducing duplication across repositories.
#### Defining a reusable workflow (`.github/workflows/reusable-test.yml`)
```yaml
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
description: "Node.js version to use"
required: false
type: string
default: "20"
working-directory:
description: "Directory to run commands in"
required: false
type: string
default: "."
secrets:
NPM_TOKEN:
required: false
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: "npm"
registry-url: "https://registry.npmjs.org"
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm test
```
#### Calling a reusable workflow
```yaml
name: CI
on:
push:
branches: [main]
jobs:
test-app:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "20"
working-directory: "packages/app"
secrets: inherit # Pass all secrets to the called workflow
test-lib:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "20"
working-directory: "packages/lib"
secrets: inherit
```
---
### 5. Composite Actions
Composite actions package multiple steps into a single reusable action. Unlike
reusable workflows, they run inline within the calling job.
#### Action definition (`.github/actions/setup-project/action.yml`)
```yaml
name: "Setup Project"
description: "Install Node.js, enable corepack, and install dependencies"
inputs:
node-version:
description: "Node.js version"
required: false
default: "20"
install-command:
description: "Command to install dependencies"
required: false
default: "pnpm install --frozen-lockfile"
runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Enable corepack
shell: bash
run: corepack enable
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "store=$(pnpm store path)" >> "$GITHUB_OUTPUT"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.store }}
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- name: Install dependencies
shell: bash
run: ${{ inputs.install-command }}
```
#### Using the composite action
```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
with:
node-version: "20"
- run: pnpm build
```
---
### 6. Deployment
Deployment workflows with environment protection rules, manual approval gates,
and multi-stage promotion.
```yaml
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
type: choice
options:
- staging
- production
permissions:
contents: read
deployments: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: corepack enable && pnpm install --frozen-lockfile
- run: pnpm build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Deploy to staging
env:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
run: |
echo "Deploying to staging..."
# Replace with your actual deploy command
# e.g., aws s3 sync, rsync, wrangler publish, etc.
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: deploy-staging
if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'
environment:
name: production
url: https://example.com
# Production environment should have required reviewers configured
# in GitHub Settings > Environments > production > Protection rules
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Deploy to production
env:
DEPLOY_TOKEN: ${{ secrets.PRODUCTION_DEPLOY_TOKEN }}
run: |
echo "Deploying to production..."
```
---
### 7. Artifacts
Artifacts let you share data between jobs in the same workflow or persist build
outputs for later download.
#### Upload artifact
```yaml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always() # Upload even if tests fail
with:
name: test-results-${{ matrix.os }}-${{ matrix.node }}
path: |
test-results/
coverage/
retention-days: 14
if-no-files-found: warn # warn, error, or ignore
```
#### Download artifact in another job
```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- run: ls -la dist/
```
#### Download all artifacts
```yaml
- uses: actions/download-artifact@v4
with:
path: all-artifacts/
# Each artifact is placed in a subdirectory named after the artifact
```
---
### 8. Conditional Execution
Control when jobs and steps run using `if` expressions, job dependencies, and
path filters.
#### Path filters on triggers
```yaml
on:
push:
branches: [main]
paths:
- "src/**"
- "package.json"
- "pnpm-lock.yaml"
paths-ignore:
- "docs/**"
- "*.md"
```
#### Conditional jobs
```yaml
jobs:
changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'src/api/**'
- 'requirements*.txt'
frontend:
- 'src/web/**'
- 'package.json'
test-backend:
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt && pytest
test-frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
```
#### Conditional steps with if expressions
```yaml
steps:
- name: Run only on main branch
if: github.ref == 'refs/heads/main'
run: echo "On main"
- name: Run only on pull requests
if: github.event_name == 'pull_request'
run: echo "PR event"
- name: Run only when previous step failed
if: failure()
run: echo "Something failed"
- name: Always run (cleanup)
if: always()
run: echo "Cleanup"
- name: Run only when a label is present
if: contains(github.event.pull_request.labels.*.name, 'deploy')
run: echo "Deploy label found"
- name: Skip for dependabot
if: github.actor != 'dependabot[bot]'
run: npm test
```
#### Job dependencies
```yaml
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo "Linting..."
test:
runs-on: ubuntu-latest
steps:
- run: echo "Testing..."
# Runs after both lint and test succeed
deploy:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- run: echo "Deploying..."
# Runs even if test fails, but only after it completes
notify:
runs-on: ubuntu-latest
needs: [test]
if: always()
steps:
- run: echo "Test job status: ${{ needs.test.result }}"
```
---
## Best Practices
1. Use caching for dependencies
2. Run jobs in parallel when possible
3. Use environment secrets
4. Pin action versions
5. Add proper triggers
1. **Pin action versions with SHA** -- Use the full commit SHA instead of a
mutable tag: `actions/checkout@b4ffde65f...` (or at minimum a major version
tag like `@v4`). This prevents supply-chain attacks where a tag is moved.
2. **Use caching aggressively** -- Cache package manager stores (`~/.npm`,
pnpm store, `~/.cache/pip`) and Docker layers. A well-cached pipeline can
cut run times by 50-80%.
3. **Set minimal permissions** -- Add a top-level `permissions` block and grant
only what is needed. Default permissions are overly broad and pose a security
risk, especially for pull requests from forks.
4. **Run jobs in parallel** -- Structure independent jobs (lint, test, typecheck)
to run concurrently. Use `needs` only when there is a real dependency between
jobs.
5. **Use `fail-fast: false` in matrix builds** -- By default a failing matrix
combination cancels all others. Setting `fail-fast: false` lets all
combinations complete so you get the full picture of what is broken.
6. **Use environment protection rules** -- Configure required reviewers and wait
timers on production environments in GitHub Settings. This adds a human gate
before production deploys.
7. **Extract reusable workflows and composite actions** -- If the same steps
appear in multiple workflows, factor them into a reusable workflow
(`workflow_call`) or composite action to keep things DRY.
8. **Keep secrets out of logs** -- Never `echo` a secret. GitHub masks known
secrets, but dynamically constructed values may leak. Use `::add-mask::` for
runtime values that should be hidden.
---
## Common Pitfalls
- **Slow pipelines**: Add caching
- **Secret exposure**: Never echo secrets
- **Unpinned versions**: Use @v4 not @main
1. **Unpinned action versions** -- Using `actions/checkout@main` means your
workflow pulls whatever is on main today. A bad push to that action
repository could break or compromise your builds. Pin to a tag (`@v4`) or
SHA.
2. **Missing caching** -- Running `npm ci` or `pip install` from scratch on
every run wastes minutes. Always configure caching for your package manager,
or use the built-in `cache` option in setup actions (e.g.,
`actions/setup-node` has a `cache` input).
3. **Overly broad triggers** -- Triggering on every push to every branch floods
the queue. Restrict triggers to `main` and pull requests. Use `paths` or
`paths-ignore` to skip runs when only docs or unrelated files change.
4. **Secret exposure in pull requests from forks** -- Secrets are NOT available
in workflows triggered by `pull_request` from forks (by design). If your
workflow needs secrets for fork PRs, use `pull_request_target` carefully and
never check out untrusted code in that context.
5. **Large artifacts without retention limits** -- Uploading artifacts without
setting `retention-days` uses the repository default (90 days), consuming
storage quota. Set short retention for transient artifacts like test results
and coverage reports.
6. **Ignoring `if: always()` for cleanup** -- Steps after a failure are skipped
by default. If you need to upload test results, send notifications, or run
cleanup regardless of prior step results, use `if: always()` or
`if: failure()`.
---
## Related Skills
- `devops/docker` - Container patterns for building and deploying Dockerized applications in workflows
- `testing/pytest` - Python test configuration for CI pipeline integration
- `testing/vitest` - TypeScript/JavaScript test configuration for CI pipeline integration
@@ -0,0 +1,250 @@
# GitHub Actions Syntax Quick Reference
## Workflow File Structure
```yaml
name: CI # Workflow name (shown in GitHub UI)
on: # Triggers
push:
branches: [main]
pull_request:
branches: [main]
permissions: # Workflow-level permissions
contents: read
env: # Workflow-level environment variables
NODE_ENV: test
jobs:
build: # Job ID
runs-on: ubuntu-latest # Runner
steps:
- uses: actions/checkout@v4 # Action step
- run: echo "Hello" # Shell step
```
## Triggers (on:)
### Common Events
```yaml
on:
push:
branches: [main, "release/**"]
paths: ["src/**", "!src/**/*.test.ts"] # Path filtering
tags: ["v*"]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
workflow_dispatch: # Manual trigger
inputs:
environment:
type: choice
options: [staging, production]
schedule:
- cron: "0 6 * * 1" # Every Monday at 6am UTC
release:
types: [published]
workflow_call: # Reusable workflow
inputs:
node-version: { type: string, default: "22" }
secrets:
NPM_TOKEN: { required: true }
```
## Jobs
```yaml
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- run: npm test
deploy:
needs: [lint, test] # Runs after lint AND test succeed
runs-on: ubuntu-latest
steps: [...]
```
### Matrix Strategy
```yaml
jobs:
test:
strategy:
fail-fast: false # Don't cancel other jobs on failure
matrix:
os: [ubuntu-latest, macos-latest]
node: [20, 22]
exclude:
- os: macos-latest
node: 20
include:
- os: ubuntu-latest
node: 22
coverage: true
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
```
## Steps
### Action Step
```yaml
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history (needed for some tools)
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
```
### Shell Step
```yaml
- name: Run tests
run: npm test
working-directory: ./packages/api
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
shell: bash
continue-on-error: true # Don't fail the job
timeout-minutes: 10
```
### Multi-line Commands
```yaml
- run: |
echo "Line 1"
echo "Line 2"
npm run build
```
## Conditionals (if:)
```yaml
# Run only on main branch
- if: github.ref == 'refs/heads/main'
# Run only on pull requests
- if: github.event_name == 'pull_request'
# Run only when previous step failed
- if: failure()
# Always run (even if previous steps failed)
- if: always()
# Run only when a matrix variable is set
- if: matrix.coverage == true
# Run based on changed files (requires dorny/paths-filter or similar)
- if: steps.filter.outputs.backend == 'true'
# Run on specific actor
- if: github.actor != 'dependabot[bot]'
```
## Environment and Secrets
```yaml
jobs:
deploy:
environment:
name: production
url: https://example.com
env:
APP_VERSION: ${{ github.sha }}
steps:
- run: deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }} # Repository secret
DEPLOY_TOKEN: ${{ vars.DEPLOY_TOKEN }} # Repository variable
```
## Caching
### Built-in Cache (setup actions)
```yaml
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm" # Automatic pnpm cache
```
### Manual Cache
```yaml
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
.mypy_cache
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
```
## Artifacts
### Upload
```yaml
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
```
### Download (in another job)
```yaml
- uses: actions/download-artifact@v4
with:
name: coverage-report
path: ./coverage
```
## Services (Containers)
Define `services:` under a job with `image`, `env`, `ports`, and `options` (for health checks). Common: postgres, redis, mysql.
## Outputs (Passing Data Between Steps/Jobs)
```yaml
# Step output: echo "key=value" >> "$GITHUB_OUTPUT"
# Read in later step: ${{ steps.<step-id>.outputs.key }}
# Job output: declare under jobs.<job>.outputs, read via needs.<job>.outputs.key
```
## Permissions
Common values: `contents: read`, `pull-requests: write`, `issues: write`, `packages: write`, `id-token: write` (OIDC).
## Reusable Workflows
Caller uses `uses: ./.github/workflows/reusable.yaml` with `with:` and `secrets: inherit`. Callee triggers on `workflow_call:` with `inputs:` and `secrets:` definitions.
## Useful Expressions
| Expression | Result |
|-----------|--------|
| `${{ github.sha }}` | Full commit SHA |
| `${{ github.ref_name }}` | Branch or tag name |
| `${{ github.event.pull_request.number }}` | PR number |
| `${{ runner.os }}` | `Linux`, `macOS`, `Windows` |
| `${{ hashFiles('**/lockfile') }}` | SHA256 of files |
| `${{ contains(github.event.head_commit.message, '[skip ci]') }}` | Check commit message |
@@ -0,0 +1,176 @@
# =============================================================================
# Node.js CI Pipeline
# Runs: lint (eslint), type check (tsc), test (vitest), build
# =============================================================================
name: Node CI
on:
push:
branches: [main]
paths:
- "**.ts"
- "**.tsx"
- "**.js"
- "**.jsx"
- "package.json"
- "pnpm-lock.yaml"
- "tsconfig.json"
- ".github/workflows/ci-node.yaml"
pull_request:
branches: [main]
paths:
- "**.ts"
- "**.tsx"
- "**.js"
- "**.jsx"
- "package.json"
- "pnpm-lock.yaml"
- "tsconfig.json"
- ".github/workflows/ci-node.yaml"
permissions:
contents: read
env:
NODE_VERSION: "22"
jobs:
# ---------------------------------------------------------------------------
# Install dependencies (shared across jobs via cache)
# ---------------------------------------------------------------------------
install:
name: Install
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
# ---------------------------------------------------------------------------
# Lint with ESLint
# ---------------------------------------------------------------------------
lint:
name: Lint
needs: install
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm lint
# ---------------------------------------------------------------------------
# Type check with TypeScript compiler
# ---------------------------------------------------------------------------
type-check:
name: Type Check
needs: install
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm tsc --noEmit
# ---------------------------------------------------------------------------
# Test with Vitest
# ---------------------------------------------------------------------------
test:
name: Test
needs: install
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- name: Run tests with coverage
run: pnpm vitest run --coverage --reporter=junit --outputFile=junit.xml
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: junit.xml
retention-days: 7
# ---------------------------------------------------------------------------
# Build
# ---------------------------------------------------------------------------
build:
name: Build
needs: [lint, type-check, test]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
@@ -0,0 +1,164 @@
# =============================================================================
# Python CI Pipeline
# Runs: lint (ruff), type check (mypy), test (pytest), coverage upload
# =============================================================================
name: Python CI
on:
push:
branches: [main]
paths:
- "**.py"
- "requirements*.txt"
- "pyproject.toml"
- ".github/workflows/ci-python.yaml"
pull_request:
branches: [main]
paths:
- "**.py"
- "requirements*.txt"
- "pyproject.toml"
- ".github/workflows/ci-python.yaml"
permissions:
contents: read
env:
PYTHON_VERSION: "3.12"
jobs:
# ---------------------------------------------------------------------------
# Lint with Ruff
# ---------------------------------------------------------------------------
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install ruff
run: pip install ruff
- name: Ruff check (lint)
run: ruff check .
- name: Ruff format (formatting)
run: ruff format --check .
# ---------------------------------------------------------------------------
# Type check with mypy
# ---------------------------------------------------------------------------
type-check:
name: Type Check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install mypy
- name: Run mypy
run: mypy src/ --ignore-missing-imports
# ---------------------------------------------------------------------------
# Test with pytest
# ---------------------------------------------------------------------------
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 15
# Uncomment to add a PostgreSQL service for integration tests.
# services:
# postgres:
# image: postgres:17-alpine
# env:
# POSTGRES_USER: test
# POSTGRES_PASSWORD: test
# POSTGRES_DB: test_db
# ports:
# - 5432:5432
# options: >-
# --health-cmd pg_isready
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run tests with coverage
run: |
pytest \
--cov=src \
--cov-report=xml:coverage.xml \
--cov-report=term-missing \
--junitxml=junit.xml \
-v
env:
PYTHONPATH: ${{ github.workspace }}
# DATABASE_URL: postgresql://test:test@localhost:5432/test_db
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml
retention-days: 7
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: junit.xml
retention-days: 7
# Uncomment to upload coverage to Codecov.
# - name: Upload to Codecov
# if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# uses: codecov/codecov-action@v4
# with:
# files: coverage.xml
# token: ${{ secrets.CODECOV_TOKEN }}