GitHub Actions has revolutionized how developers implement CI/CD pipelines. Integrated directly into GitHub, it provides a powerful platform for automating builds, tests, and deployments. Let's explore how to build robust CI/CD workflows with GitHub Actions.
Understanding GitHub Actions Fundamentals
GitHub Actions uses a YAML-based workflow definition that runs in response to events. Workflows contain jobs, which contain steps that execute commands or actions.
yaml# .github/workflows/ci.yml name: CI Pipeline on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Run tests run: npm test
Building Your First Workflow
Multi-Stage CI Pipeline
yamlname: CI Pipeline on: push: branches: [ main, develop ] pull_request: branches: [ main ] env: NODE_VERSION: '20.x' CACHE_VERSION: 'v1' jobs: lint: name: Lint Code runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run ESLint run: npm run lint test: name: Run Tests runs-on: ubuntu-latest needs: lint strategy: matrix: node-version: [18.x, 20.x, 21.x] steps: - uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests run: npm test -- --coverage - name: Upload coverage uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} build: name: Build Application runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install dependencies run: npm ci - name: Build application run: npm run build env: CI: true - name: Upload build artifacts uses: actions/upload-artifact@v3 with: name: build path: dist/ retention-days: 7
Advanced CI/CD Patterns
Caching Strategy
yamljobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Cache node modules uses: actions/cache@v3 id: npm-cache with: path: | ~/.npm node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install dependencies if: steps.npm-cache.outputs.cache-hit != 'true' run: npm ci
Matrix Builds
yamljobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] node-version: [18.x, 20.x, 21.x] # Exclude specific combinations exclude: - os: windows-latest node-version: 21.x steps: - uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm test
Deployment Workflows
Deploy to Production
yamlname: Deploy to Production on: push: branches: [ main ] jobs: deploy: runs-on: ubuntu-latest environment: name: production url: https://example.com steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Build application run: npm run build env: CI: false - name: Deploy to Vercel uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.ORG_ID }} vercel-project-id: ${{ secrets.PROJECT_ID }} vercel-args: '--prod' - name: Run smoke tests run: | curl -f https://example.com/health || exit 1
Blue-Green Deployment
yamljobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-1 - name: Deploy to blue environment run: | # Deploy new version to blue environment aws elasticbeanstalk update-environment \ --environment-name myapp-blue \ --version-label $GITHUB_SHA - name: Run smoke tests on blue run: | # Test blue environment curl -f https://blue.example.com/health - name: Switch traffic to blue run: | # Swap DNS to point to blue aws route53 change-resource-record-sets \ --hosted-zone-id Z1234567890ABC \ --change-batch file://route53-switch.json
Docker and Kubernetes
Build and Push Docker Image
yamljobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max
Deploy to Kubernetes
yamljobs: deploy-k8s: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Configure kubectl uses: azure/k8s-set-context@v4 with: method: kubeconfig kubeconfig: ${{ secrets.KUBE_CONFIG }} - name: Deploy to Kubernetes run: | kubectl set image deployment/myapp \ myapp=ghcr.io/username/repo:${{ github.sha }} \ --namespace=production - name: Verify deployment run: | kubectl rollout status deployment/myapp --namespace=production kubectl get pods -n production
Monitoring and Notifications
Slack Notifications
yamlname: CI with Notifications on: push: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm test notify: needs: test runs-on: ubuntu-latest if: always() steps: - name: Send Slack notification uses: slackapi/slack-github-action@v1.24.0 with: payload: | { "text": "CI Status: ${{ job.status }}", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "Build ${{ github.sha }} completed with status *${{ job.status }}*" } } ] } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Best Practices
Use Reusable Workflows
yaml# .github/workflows/reusable-ci.yml name: Reusable CI Workflow on: workflow_call: inputs: node-version: required: true type: string outputs: test-result: value: ${{ jobs.test.outputs.result }} jobs: test: runs-on: ubuntu-latest outputs: result: ${{ steps.test.outputs.result }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} - run: npm test - id: test run: echo "result=success" >> $GITHUB_OUTPUT
Security Scanning
yamljobs: security: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: scan-type: 'fs' scan-ref: '.' format: 'sarif' output: 'trivy-results.sarif' - name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif'
Conclusion
GitHub Actions provides a powerful, flexible platform for CI/CD. Start with simple workflows, iterate based on your needs, and leverage the rich ecosystem of actions to build automation that saves time and improves quality.
Focus on making workflows fast, reliable, and secure. Use caching, parallel jobs, and smart deployment strategies to keep your development workflow smooth and efficient.