Intermediaire 16 min de lecture · 3 377 mots

CI/CD avec GitHub Actions : Tests, Build, et Deploiement Automatique

Estimated reading time: 15 minutes

Introduction

GitHub Actions permet de créer des pipelines CI/CD puissants directement dans votre repository. Ce guide couvre l’implémentation complète d’un pipeline moderne avec tests, build, et déploiement multi-environnements.

1. Concepts Fondamentaux

Architecture GitHub Actions

Workflow (fichier .yml)
  └── Jobs (s'exécutent en parallèle par défaut)
      └── Steps (s'exécutent séquentiellement)
          └── Actions (actions réutilisables)

# Exemple de structure
.github/
  └── workflows/
      ├── ci.yml           # Integration continue
      ├── cd.yml           # Deploiement continu
      ├── security.yml     # Scans de sécurité
      └── release.yml      # Gestion des releases

Premier Workflow Simple

# .github/workflows/hello.yml
name: Hello World

# Déclencheurs
on:
  push:
    branches: [main]
  pullrequest:
    branches: [main]

# Jobs
jobs:
  greet:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Say hello
        run: echo "Hello, GitHub Actions!"

      - name: Show environment
        run: |
          echo "Runner OS: ${{ runner.os }}"
          echo "GitHub ref: ${{ github.ref }}"
          echo "GitHub actor: ${{ github.actor }}"

2. Pipeline CI Complet

Application Node.js/React

# .github/workflows/ci.yml
name: Continuous Integration

on:
  push:
    branches: [main, develop]
  pullrequest:
    branches: [main, develop]

env:
  NODEVERSION: '18'
  CACHEVERSION: 'v1'

jobs:
  # Job 1: Lint et validation
  lint:
    name: Lint Code
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Nécessaire pour certains linters

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODEVERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npm run lint

      - name: Run Prettier check
        run: npm run format:check

      - name: TypeScript check
        run: npm run type-check

      - name: Check commit messages
        if: github.eventname == 'pullrequest'
        run: |
          npm install -g @commitlint/cli @commitlint/config-conventional
          npx commitlint --from ${{ github.event.pullrequest.base.sha }} --to HEAD

  # Job 2: Tests unitaires
  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    timeout-minutes: 15

    strategy:
      matrix:
        node-version: [16, 18, 20]

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm run test:unit -- --coverage --maxWorkers=2

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
          flags: unit-tests
          name: node-${{ matrix.node-version }}
          failciiferror: false

      - name: Archive test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-results-${{ matrix.node-version }}
          path: |
            coverage/
            test-results/
          retention-days: 7

  # Job 3: Tests d'integration
  integration-tests:
    name: Integration Tests
    runs-on: ubuntu-latest
    timeout-minutes: 20

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRESDB: testdb
          POSTGRESUSER: testuser
          POSTGRESPASSWORD: testpass
        options: >-
          --health-cmd pgisready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379

    env:
      DATABASEURL: postgresql://testuser:testpass@localhost:5432/testdb
      REDISURL: redis://localhost:6379

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODEVERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run database migrations
        run: npm run db:migrate

      - name: Seed test data
        run: npm run db:seed

      - name: Run integration tests
        run: npm run test:integration

      - name: Upload test artifacts
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: integration-test-logs
          path: logs/

  # Job 4: Tests E2E avec Playwright
  e2e-tests:
    name: E2E Tests
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODEVERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps

      - name: Build application
        run: npm run build

      - name: Start application
        run: |
          npm run start:test &
          npx wait-on http://localhost:3000 --timeout 60000

      - name: Run E2E tests
        run: npm run test:e2e

      - name: Upload Playwright report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

      - name: Upload test videos
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: e2e-videos
          path: test-results//.webm
          retention-days: 3

  # Job 5: Build
  build:
    name: Build Application
    needs: [lint, unit-tests]
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODEVERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build
        env:
          CI: true
          NODEENV: production

      - name: Check bundle size
        run: |
          npm run analyze:bundle
          if [ -f bundle-size.json ]; then
            SIZE=$(jq '.totalSize' bundle-size.json)
            if [ "$SIZE" -gt 500000 ]; then
              echo "::warning::Bundle size ($SIZE bytes) exceeds 500KB"
            fi
          fi

      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-${{ github.sha }}
          path: |
            build/
            dist/
          retention-days: 7

  # Job 6: Security scans
  security:
    name: Security Scans
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v3

      - 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:
          sariffile: 'trivy-results.sarif'

      - name: Run npm audit
        run: npm audit --audit-level=moderate

      - name: Check for secrets
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.defaultbranch }}
          head: HEAD

  # Job 7: Code quality
  code-quality:
    name: Code Quality Analysis
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Full history for SonarCloud

      - name: SonarCloud Scan
        uses: SonarSource/sonarcloud-github-action@master
        env:
          GITHUBTOKEN: ${{ secrets.GITHUBTOKEN }}
          SONARTOKEN: ${{ secrets.SONARTOKEN }}
        with:
          args: >
            -Dsonar.organization=my-org
            -Dsonar.projectKey=my-project
            -Dsonar.sources=src
            -Dsonar.tests=tests
            -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info

  # Job final: Status check
  ci-success:
    name: CI Success
    needs: [lint, unit-tests, integration-tests, e2e-tests, build, security]
    runs-on: ubuntu-latest
    if: always()

    steps:
      - name: Check all jobs status
        run: |
          if [[ "${{ needs.lint.result }}" != "success" ]] || 
             [[ "${{ needs.unit-tests.result }}" != "success" ]] || 
             [[ "${{ needs.integration-tests.result }}" != "success" ]] || 
             [[ "${{ needs.e2e-tests.result }}" != "success" ]] || 
             [[ "${{ needs.build.result }}" != "success" ]] || 
             [[ "${{ needs.security.result }}" != "success" ]]; then
            echo "One or more CI jobs failed"
            exit 1
          fi
          echo "All CI jobs passed successfully!"

3. Pipeline CD Multi-Environnements

Deploiement avec Environments

# .github/workflows/cd.yml
name: Continuous Deployment

on:
  push:
    branches: [main]
  workflowdispatch:
    inputs:
      environment:
        description: 'Environment to deploy'
        required: true
        type: choice
        options:
          - staging
          - production

env:
  DOCKERREGISTRY: ghcr.io
  IMAGENAME: ${{ github.repository }}

jobs:
  # Job 1: Build Docker image
  build-image:
    name: Build Docker Image
    runs-on: ubuntu-latest

    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to Container Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.DOCKERREGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUBTOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ env.DOCKERREGISTRY }}/${{ env.IMAGENAME }}
          tags: |
            type=ref,event=branch
            type=sha,prefix={{branch}}-
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}

      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            BUILDDATE=${{ github.event.headcommit.timestamp }}
            VCSREF=${{ github.sha }}
            VERSION=${{ steps.meta.outputs.version }}

      - name: Scan image for vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.DOCKERREGISTRY }}/${{ env.IMAGENAME }}:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-image-results.sarif'

      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sariffile: 'trivy-image-results.sarif'

  # Job 2: Deploy to Staging
  deploy-staging:
    name: Deploy to Staging
    needs: build-image
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' || github.event.inputs.environment == 'staging'

    environment:
      name: staging
      url: https://staging.example.com

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWSACCESSKEYID }}
          aws-secret-access-key: ${{ secrets.AWSSECRETACCESSKEY }}
          aws-region: us-east-1

      - name: Deploy to ECS
        run: |
          # Update task definition
          TASKDEFINITION=$(aws ecs describe-task-definition 
            --task-definition staging-app 
            --query 'taskDefinition' 
            --output json)

          NEWTASKDEF=$(echo $TASKDEFINITION | jq 
            --arg IMAGE "${{ env.DOCKERREGISTRY }}/${{ env.IMAGENAME }}:${{ github.sha }}" 
            '.containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)')

          NEWTASKINFO=$(aws ecs register-task-definition 
            --cli-input-json "$NEWTASKDEF")

          NEWREVISION=$(echo $NEWTASKINFO | jq -r '.taskDefinition.revision')

          # Update service
          aws ecs update-service 
            --cluster staging-cluster 
            --service staging-app-service 
            --task-definition staging-app:${NEWREVISION} 
            --force-new-deployment

          # Wait for deployment
          aws ecs wait services-stable 
            --cluster staging-cluster 
            --services staging-app-service

      - name: Run smoke tests
        run: |
          npm ci
          npm run test:smoke -- --baseUrl=https://staging.example.com

      - name: Notify Slack
        if: always()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: |
            Deployment to staging: ${{ job.status }}
            Commit: ${{ github.event.headcommit.message }}
            Author: ${{ github.actor }}
          webhookurl: ${{ secrets.SLACKWEBHOOK }}

  # Job 3: Deploy to Production
  deploy-production:
    name: Deploy to Production
    needs: deploy-staging
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' || github.event.inputs.environment == 'production'

    environment:
      name: production
      url: https://example.com

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWSACCESSKEYIDPROD }}
          aws-secret-access-key: ${{ secrets.AWSSECRETACCESSKEYPROD }}
          aws-region: us-east-1

      - name: Create deployment snapshot
        run: |
          # Backup current production state
          aws ecs describe-services 
            --cluster production-cluster 
            --services production-app-service 
            > deployment-backup-${{ github.sha }}.json

      - name: Blue/Green Deployment
        run: |
          # Deploy to green environment
          # ... (similar to staging deployment)

          # Run health checks
          for i in {1..30}; do
            if curl -f https://green.example.com/health; then
              echo "Green environment healthy"
              break
            fi
            sleep 10
          done

          # Switch traffic (update load balancer)
          aws elbv2 modify-listener 
            --listener-arn ${{ secrets.PRODLISTENERARN }} 
            --default-actions Type=forward,TargetGroupArn=${{ secrets.GREENTARGETGROUP }}

          # Monitor for 5 minutes
          sleep 300

          # Check error rate
          ERRORRATE=$(aws cloudwatch get-metric-statistics 
            --namespace AWS/ApplicationELB 
            --metric-name HTTPCodeTarget5XXCount 
            --start-time $(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%S) 
            --end-time $(date -u +%Y-%m-%dT%H:%M:%S) 
            --period 300 
            --statistics Sum 
            --query 'Datapoints[0].Sum' 
            --output text)

          if [ "$ERRORRATE" -gt 10 ]; then
            echo "High error rate detected, rolling back"
            # Rollback logic
            aws elbv2 modify-listener 
              --listener-arn ${{ secrets.PRODLISTENERARN }} 
              --default-actions Type=forward,TargetGroupArn=${{ secrets.BLUETARGETGROUP }}
            exit 1
          fi

      - name: Run production smoke tests
        run: |
          npm run test:smoke -- --baseUrl=https://example.com

      - name: Create GitHub Release
        if: success()
        uses: actions/create-release@v1
        env:
          GITHUBTOKEN: ${{ secrets.GITHUBTOKEN }}
        with:
          tagname: v${{ github.runnumber }}
          releasename: Release ${{ github.runnumber }}
          body: |
            Deployed commit: ${{ github.sha }}
            Deployment time: ${{ github.event.headcommit.timestamp }}

      - name: Notify deployment
        if: always()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: |
            Production deployment: ${{ job.status }}
            Version: v${{ github.runnumber }}
            URL: https://example.com
          webhookurl: ${{ secrets.SLACKWEBHOOKPROD }}

4. Workflows Avances

Matrix Strategy pour Multi-Platform

# .github/workflows/multi-platform.yml
name: Multi-Platform Build

on: [push, pullrequest]

jobs:
  build:
    name: Build on ${{ matrix.os }} - ${{ matrix.node }}
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [16, 18, 20]
        include:
          - os: ubuntu-latest
            node: 20
            experimental: true
        exclude:
          - os: windows-latest
            node: 16

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js ${{ matrix.node }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node }}

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build

Deploiement avec Approval Manual

# .github/workflows/production-deploy.yml
name: Production Deployment with Approval

on:
  workflowdispatch:
    inputs:
      version:
        description: 'Version to deploy'
        required: true
      reason:
        description: 'Deployment reason'
        required: true

jobs:
  request-approval:
    name: Request Deployment Approval
    runs-on: ubuntu-latest

    steps:
      - name: Create approval issue
        uses: actions/github-script@v6
        with:
          script: |
            const issue = await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: Deployment Approval: v${{ github.event.inputs.version }},
              body: `
              ## Deployment Request

              Version: ${{ github.event.inputs.version }}
              Reason: ${{ github.event.inputs.reason }}
              Requested by: @${{ github.actor }}
              Time: ${new Date().toISOString()}

              ### Approval Process

              Team leads, please review and approve by commenting:
              /approve

              Or reject with:
              /reject [reason]
              `,
              labels: ['deployment', 'approval-required']
            });

            core.setOutput('issuenumber', issue.data.number);

  wait-for-approval:
    name: Wait for Approval
    needs: request-approval
    runs-on: ubuntu-latest
    timeout-minutes: 1440  # 24 hours

    steps:
      - name: Wait for approval comment
        uses: actions/github-script@v6
        with:
          script: |
            const issuenumber = ${{ needs.request-approval.outputs.issuenumber }};

            while (true) {
              const comments = await github.rest.issues.listComments({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issuenumber: issuenumber
              });

              for (const comment of comments.data) {
                if (comment.body.includes('/approve')) {
                  console.log('Deployment approved!');
                  return;
                }
                if (comment.body.includes('/reject')) {
                  throw new Error('Deployment rejected');
                }
              }

              await new Promise(resolve => setTimeout(resolve, 30000));  // Wait 30s
            }

  deploy:
    name: Deploy to Production
    needs: wait-for-approval
    runs-on: ubuntu-latest

    environment:
      name: production
      url: https://example.com

    steps:
      - name: Deploy
        run: |
          echo "Deploying version ${{ github.event.inputs.version }}"
          # Deployment logic here

Workflow avec Cache Optimise

# .github/workflows/optimized-cache.yml
name: Optimized Build with Caching

on: [push, pullrequest]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      # Cache npm dependencies
      - name: Cache npm dependencies
        uses: actions/cache@v3
        id: npm-cache
        with:
          path: |
            ~/.npm
            nodemodules
          key: ${{ runner.os }}-npm-${{ hashFiles('/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-

      - name: Install dependencies
        if: steps.npm-cache.outputs.cache-hit != 'true'
        run: npm ci

      # Cache build output
      - name: Cache build
        uses: actions/cache@v3
        id: build-cache
        with:
          path: |
            dist/
            .next/cache
          key: ${{ runner.os }}-build-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-build-

      - name: Build
        if: steps.build-cache.outputs.cache-hit != 'true'
        run: npm run build

      # Cache Playwright browsers
      - name: Cache Playwright browsers
        uses: actions/cache@v3
        with:
          path: ~/.cache/ms-playwright
          key: ${{ runner.os }}-playwright-${{ hashFiles('/package-lock.json') }}

      - name: Test
        run: npm test

5. Reusable Workflows

Workflow Reutilisable

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy Workflow

on:
  workflowcall:
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
    secrets:
      aws-access-key:
        required: true
      aws-secret-key:
        required: true
      slack-webhook:
        required: false

jobs:
  deploy:
    name: Deploy to ${{ inputs.environment }}
    runs-on: ubuntu-latest

    environment:
      name: ${{ inputs.environment }}

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure AWS
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.aws-access-key }}
          aws-secret-access-key: ${{ secrets.aws-secret-key }}
          aws-region: us-east-1

      - name: Deploy
        run: |
          echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"
          # Deployment commands

      - name: Notify
        if: secrets.slack-webhook != ''
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          webhookurl: ${{ secrets.slack-webhook }}

Utilisation du Workflow Reutilisable

# .github/workflows/main-deploy.yml
name: Main Deployment Pipeline

on:
  push:
    branches: [main]

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      image-tag: ${{ github.sha }}
    secrets:
      aws-access-key: ${{ secrets.AWSKEYSTAGING }}
      aws-secret-key: ${{ secrets.AWSSECRETSTAGING }}
      slack-webhook: ${{ secrets.SLACKWEBHOOK }}

  deploy-production:
    needs: deploy-staging
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
      image-tag: ${{ github.sha }}
    secrets:
      aws-access-key: ${{ secrets.AWSKEYPROD }}
      aws-secret-key: ${{ secrets.AWSSECRETPROD }}
      slack-webhook: ${{ secrets.SLACKWEBHOOKPROD }}

6. Actions Personnalisees

Composite Action

# .github/actions/setup-app/action.yml
name: 'Setup Application'
description: 'Setup Node.js and install dependencies'

inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '18'
  cache-key:
    description: 'Custom cache key'
    required: false

outputs:
  cache-hit:
    description: 'Whether cache was hit'
    value: ${{ steps.cache.outputs.cache-hit }}

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: ${{ inputs.node-version }}

    - name: Cache dependencies
      id: cache
      uses: actions/cache@v3
      with:
        path: nodemodules
        key: ${{ inputs.cache-key || format('deps-{0}-{1}', runner.os, hashFiles('/package-lock.json')) }}

    - name: Install dependencies
      if: steps.cache.outputs.cache-hit != 'true'
      shell: bash
      run: npm ci

    - name: Verify installation
      shell: bash
      run: |
        node --version
        npm --version

Utilisation de l’Action Composite

# .github/workflows/use-composite.yml
name: Use Composite Action

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup application
        uses: ./.github/actions/setup-app
        with:
          node-version: '18'

      - name: Run tests
        run: npm test

7. Monitoring et Debugging

Workflow avec Debug Logging

# .github/workflows/debug.yml
name: Debug Workflow

on:
  workflowdispatch:
    inputs:
      debugenabled:
        description: 'Enable debug logging'
        type: boolean
        default: false

jobs:
  debug-job:
    runs-on: ubuntu-latest

    steps:
      - name: Enable debug logging
        if: ${{ inputs.debugenabled }}
        run: |
          echo "ACTIONSSTEPDEBUG=true" >> $GITHUBENV
          echo "ACTIONSRUNNERDEBUG=true" >> $GITHUBENV

      - name: Checkout
        uses: actions/checkout@v3

      - name: Show context
        run: |
          echo "GitHub context:"
          echo "${{ toJSON(github) }}"

          echo -e "nRunner context:"
          echo "${{ toJSON(runner) }}"

          echo -e "nJob context:"
          echo "${{ toJSON(job) }}"

      - name: Debug environment
        run: |
          echo "Environment variables:"
          env | sort

          echo -e "nSystem info:"
          uname -a
          df -h
          free -h

      - name: Test with debug
        run: |
          if [ "${{ inputs.debugenabled }}" == "true" ]; then
            npm test -- --verbose --debug
          else
            npm test
          fi

Monitoring Workflow Performance

# .github/workflows/performance-monitoring.yml
name: Workflow Performance Monitoring

on:
  workflowrun:
    workflows: ["CI", "CD"]
    types: [completed]

jobs:
  analyze:
    runs-on: ubuntu-latest

    steps:
      - name: Get workflow run data
        uses: actions/github-script@v6
        with:
          script: |
            const run = await github.rest.actions.getWorkflowRun({
              owner: context.repo.owner,
              repo: context.repo.repo,
              runid: context.payload.workflowrun.id
            });

            const jobs = await github.rest.actions.listJobsForWorkflowRun({
              owner: context.repo.owner,
              repo: context.repo.repo,
              runid: context.payload.workflowrun.id
            });

            const duration = new Date(run.data.updatedat) - new Date(run.data.createdat);
            const durationMinutes = Math.floor(duration / 60000);

            console.log(Workflow: ${run.data.name});
            console.log(Duration: ${durationMinutes} minutes);
            console.log(Status: ${run.data.conclusion});

            for (const job of jobs.data.jobs) {
              const jobDuration = new Date(job.completedat) - new Date(job.startedat);
              console.log(Job ${job.name}: ${Math.floor(jobDuration / 1000)}s);
            }

            // Alert if workflow is too slow
            if (durationMinutes > 30) {
              github.rest.issues.create({
                owner: context.repo.owner,
                repo: context.repo.repo,
                title: Slow workflow detected: ${run.data.name},
                body: Workflow took ${durationMinutes} minutes to complete.,
                labels: ['performance', 'ci']
              });
            }

8. Best Practices

Secrets Management

# Bonne pratique pour les secrets
steps:
  - name: Use secrets securely
    env:
      # NEVER: echo ${{ secrets.APIKEY }}
      APIKEY: ${{ secrets.APIKEY }}
      DBPASSWORD: ${{ secrets.DBPASSWORD }}
    run: |
      # Les secrets sont maintenant dans l'environnement
      ./deploy.sh

  - name: Mask sensitive data
    run: |
      echo "::add-mask::$SENSITIVEVALUE"
      echo "Value: $SENSITIVEVALUE"  # Sera masqué dans les logs

Configuration des Environments

# Via GitHub CLI
gh api repos/:owner/:repo/environments/production -X PUT --input - << EOF
{
  "waittimer": 30,
  "reviewers": [
    {
      "type": "User",
      "id": 12345
    },
    {
      "type": "Team",
      "id": 67890
    }
  ],
  "deploymentbranchpolicy": {
    "protectedbranches": true,
    "custombranchpolicies": false
  }
}
EOF

Cost Optimization

# Optimisations pour réduire les coûts
jobs:
  optimize:
    runs-on: ubuntu-latest
    # Timeout pour éviter les jobs bloqués
    timeout-minutes: 30

    steps:
      # Checkout superficiel
      - uses: actions/checkout@v3
        with:
          fetch-depth: 1

      # Installer seulement les dépendances de production
      - name: Install production dependencies
        run: npm ci --production

      # Utiliser des caches agressifs
      - uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('/package-lock.json') }}

      # Paralléliser quand possible
      - name: Run parallel tests
        run: npm test -- --maxWorkers=4

      # Annuler les runs redondants
      - name: Cancel redundant runs
        uses: styfle/cancel-workflow-action@0.11.0
        with:
          accesstoken: ${{ github.token }}

9. Troubleshooting

Debug d’un Workflow qui Echoue

# 1. Activer le debug logging
# Settings > Secrets > New repository secret
# Name: ACTIONSSTEPDEBUG
# Value: true

# 2. Re-run le workflow avec debug
gh run rerun  --debug

# 3. Voir les logs détaillés
gh run view  --log

# 4. Télécharger les logs
gh run download 

# 5. Tester localement avec act
# Installation: https://github.com/nektos/act
act -j build -s GITHUBTOKEN=

# 6. Dry-run
act -n

# 7. Lister les jobs
act -l

# 8. Runner spécifique job
act -j test

Problemes Communs

# Problème: Timeout sur les tests
# Solution: Augmenter le timeout
jobs:
  test:
    timeout-minutes: 60  # Au lieu de défaut (360)

# Problème: Cache non utilisé
# Solution: Vérifier les clés
  • uses: actions/cache@v3
  • with: path: nodemodules key: deps-${{ hashFiles('package-lock.json') }} # Pas / si fichier à la racine # Problème: Secrets non disponibles dans PR de fork # Solution: Utiliser pullrequesttarget avec précaution on: pullrequesttarget: # A accès aux secrets types: [labeled] jobs: build: if: contains(github.event.pullrequest.labels..name, 'safe-to-test') # ... # Problème: Artifacts expirés # Solution: Augmenter la rétention
  • uses: actions/upload-artifact@v3
  • with: name: build path: dist/ retention-days: 30 # Au lieu de défaut (90)

10. Templates de Configuration Complete

package.json Scripts

{
  "scripts": {
    "test": "jest",
    "test:unit": "jest --testPathPattern=unit",
    "test:integration": "jest --testPathPattern=integration",
    "test:e2e": "playwright test",
    "test:smoke": "playwright test --grep @smoke",
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix",
    "format": "prettier --write src/",
    "format:check": "prettier --check src/",
    "type-check": "tsc --noEmit",
    "build": "webpack --mode production",
    "analyze:bundle": "webpack-bundle-analyzer dist/stats.json",
    "db:migrate": "knex migrate:latest",
    "db:seed": "knex seed:run"
  }
}

Dockerfile Multi-Stage pour CI/CD

# Build stage
FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production && 
    npm cache clean --force

COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine

WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S nodejs && 
    adduser -S nodejs -u 1001

COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/nodemodules ./nodemodules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 
  CMD node healthcheck.js

CMD ["node", "dist/server.js"]

Conclusion

GitHub Actions offre une plateforme CI/CD puissante et flexible. Les points clés :

  • Commencez simple puis optimisez
  • Utilisez les caches pour la performance
  • Parallélisez les jobs indépendants
  • Monitorer les coûts et la performance
  • Sécurisez les secrets et les déploiements
  • Testez à tous les niveaux (unit, integration, E2E)
  • Automatisez mais gardez le contrôle
  • Avec ces patterns, vous pouvez construire des pipelines robustes et efficaces pour tout type de projet.

    Une remarque, un retour ?

    Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.