# Workflow Templates Complete, production-ready templates for the most common GitHub Actions use cases. Use these as starting points and adapt to the user's project. --- ## Node.js CI ```yaml name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci - run: npm run lint - run: npm run typecheck if: hashFiles('tsconfig.json') != '' test: runs-on: ubuntu-latest timeout-minutes: 15 strategy: matrix: node-version: [18, 20, 22] fail-fast: false steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npm ci - run: npm test - uses: codecov/codecov-action@v4 if: matrix.node-version == 20 with: token: ${{ secrets.CODECOV_TOKEN }} ``` --- ## Python CI ```yaml name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip' - run: pip install ruff mypy - run: ruff check . - run: ruff format --check . test: runs-on: ubuntu-latest timeout-minutes: 15 strategy: matrix: python-version: ['3.10', '3.11', '3.12'] fail-fast: false steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' - run: pip install -e ".[dev]" - run: pytest --cov --cov-report=xml - uses: codecov/codecov-action@v4 if: matrix.python-version == '3.12' with: token: ${{ secrets.CODECOV_TOKEN }} ``` --- ## Go CI ```yaml name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.22' - uses: golangci/golangci-lint-action@v6 with: version: latest test: runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.22' - run: go test -race -coverprofile=coverage.out ./... - uses: codecov/codecov-action@v4 with: files: coverage.out token: ${{ secrets.CODECOV_TOKEN }} build: runs-on: ubuntu-latest timeout-minutes: 10 needs: [lint, test] steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.22' - run: go build ./... ``` --- ## Rust CI ```yaml name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: check: runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - uses: actions/cache@v4 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - run: cargo fmt --all -- --check - run: cargo clippy --all-targets -- -D warnings test: runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: actions/cache@v4 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - run: cargo test --all ``` --- ## Docker Build & Push ```yaml name: Docker on: push: branches: [main] tags: ['v*'] pull_request: branches: [main] permissions: contents: read packages: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 if: github.event_name != 'pull_request' with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/metadata-action@v5 id: meta with: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha - uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max ``` --- ## Deploy to Vercel ```yaml name: Deploy on: push: branches: [main] pull_request: branches: [main] permissions: contents: read pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 15 environment: name: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }} url: ${{ steps.deploy.outputs.url }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci - run: npm test - name: Deploy to Vercel id: deploy run: | npm i -g vercel if [ "${{ github.ref }}" = "refs/heads/main" ]; then url=$(vercel --prod --token=${{ secrets.VERCEL_TOKEN }} 2>&1 | tail -1) else url=$(vercel --token=${{ secrets.VERCEL_TOKEN }} 2>&1 | tail -1) fi echo "url=$url" >> $GITHUB_OUTPUT env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - name: Comment PR with preview URL if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: `Preview deployed: ${{ steps.deploy.outputs.url }}` }) ``` --- ## Deploy to AWS (ECS) ```yaml name: Deploy to AWS on: push: branches: [main] permissions: id-token: write contents: read concurrency: group: deploy-production cancel-in-progress: false jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 20 environment: production steps: - uses: actions/checkout@v4 - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: us-east-1 - uses: aws-actions/amazon-ecr-login@v2 id: ecr - name: Build and push image run: | IMAGE=${{ steps.ecr.outputs.registry }}/${{ github.repository }}:${{ github.sha }} docker build -t $IMAGE . docker push $IMAGE echo "image=$IMAGE" >> $GITHUB_OUTPUT id: build - name: Deploy to ECS run: | aws ecs update-service \ --cluster production \ --service app \ --force-new-deployment ``` --- ## npm Publish on Release ```yaml name: Publish on: push: tags: ['v*'] permissions: contents: write id-token: write jobs: publish: runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm test - run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: softprops/action-gh-release@v2 with: generate_release_notes: true ``` --- ## PyPI Publish ```yaml name: Publish to PyPI on: push: tags: ['v*'] permissions: contents: write id-token: write jobs: publish: runs-on: ubuntu-latest timeout-minutes: 15 environment: pypi steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - run: pip install build - run: python -m build - uses: pypa/gh-action-pypi-publish@release/v1 - uses: softprops/action-gh-release@v2 with: generate_release_notes: true ``` --- ## Scheduled Dependency Updates Check ```yaml name: Dependency Review on: pull_request: branches: [main] schedule: - cron: '0 9 * * 1' # Monday 9am UTC workflow_dispatch: permissions: contents: read pull-requests: write jobs: audit: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/dependency-review-action@v4 if: github.event_name == 'pull_request' - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci - run: npm audit --audit-level=high continue-on-error: ${{ github.event_name == 'schedule' }} ``` --- ## Monorepo CI (with path filters) ```yaml name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: changes: runs-on: ubuntu-latest timeout-minutes: 5 outputs: frontend: ${{ steps.filter.outputs.frontend }} backend: ${{ steps.filter.outputs.backend }} infra: ${{ steps.filter.outputs.infra }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 id: filter with: filters: | frontend: - 'packages/frontend/**' backend: - 'packages/backend/**' infra: - 'terraform/**' - 'docker/**' frontend: needs: changes if: needs.changes.outputs.frontend == 'true' runs-on: ubuntu-latest timeout-minutes: 15 defaults: run: working-directory: packages/frontend steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' cache-dependency-path: packages/frontend/package-lock.json - run: npm ci - run: npm run lint - run: npm test - run: npm run build backend: needs: changes if: needs.changes.outputs.backend == 'true' runs-on: ubuntu-latest timeout-minutes: 15 defaults: run: working-directory: packages/backend steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' cache-dependency-path: packages/backend/package-lock.json - run: npm ci - run: npm test ``` --- ## Release Please (Automated Versioning) ```yaml name: Release on: push: branches: [main] permissions: contents: write pull-requests: write jobs: release: runs-on: ubuntu-latest timeout-minutes: 10 outputs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} steps: - uses: googleapis/release-please-action@v4 id: release with: release-type: node # or python, go, etc. publish: needs: release if: needs.release.outputs.release_created runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ``` --- ## Cloudflare Workers Deploy ```yaml name: Deploy Worker on: push: branches: [main] pull_request: branches: [main] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci - name: Deploy uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} command: ${{ github.ref == 'refs/heads/main' && 'deploy' || 'deploy --dry-run' }} ``` --- ## E2E Tests with Playwright ```yaml name: E2E Tests on: push: branches: [main] pull_request: branches: [main] schedule: - cron: '0 6 * * *' # Daily at 6am UTC permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: e2e: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci - run: npx playwright install --with-deps chromium - run: npm run build - run: npx playwright test env: CI: true - uses: actions/upload-artifact@v4 if: failure() with: name: playwright-report path: playwright-report/ retention-days: 7 ``` --- ## Security Scanning (CodeQL + Trivy) ```yaml name: Security on: push: branches: [main] pull_request: branches: [main] schedule: - cron: '0 0 * * 1' # Weekly on Monday permissions: contents: read security-events: write jobs: codeql: runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: github/codeql-action/init@v3 with: languages: javascript-typescript # or python, go, java, etc. - uses: github/codeql-action/analyze@v3 trivy: runs-on: ubuntu-latest timeout-minutes: 10 if: hashFiles('Dockerfile') != '' steps: - uses: actions/checkout@v4 - uses: aquasecurity/trivy-action@master with: scan-type: 'fs' format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' ``` --- ## Terraform Plan & Apply ```yaml name: Infrastructure on: push: branches: [main] paths: ['terraform/**'] pull_request: branches: [main] paths: ['terraform/**'] permissions: contents: read pull-requests: write id-token: write concurrency: group: terraform-${{ github.ref }} cancel-in-progress: false jobs: plan: runs-on: ubuntu-latest timeout-minutes: 15 defaults: run: working-directory: terraform steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: terraform_version: '1.7' - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: us-east-1 - run: terraform init - run: terraform validate - run: terraform plan -out=tfplan id: plan - uses: actions/github-script@v7 if: github.event_name == 'pull_request' with: script: | const plan = `${{ steps.plan.outputs.stdout }}`; github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: `### Terraform Plan\n\`\`\`\n${plan}\n\`\`\`` }) apply: needs: plan if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 30 environment: production defaults: run: working-directory: terraform steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: terraform_version: '1.7' - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: us-east-1 - run: terraform init - run: terraform apply -auto-approve ```