1. Tool landscape #
Choosing without dogma.
Comparison #
| Tool | Model | Strengths | Points of attention |
|---|---|---|---|
| GitHub Actions | YAML in .github/workflows, hosted or self-hosted runners | GitHub integration, actions marketplace, OIDC | pin third-party actions, cost of minutes |
| GitLab CI | .gitlab-ci.yml, GitLab runners | all-in-one platform (registry, security, environments), self-hostable | complex files at scale (use include and components) |
| Jenkins | Jenkinsfile (Groovy), agents | highly extensible, self-hosted, large legacy | plugin maintenance, controller security |
| Argo CD / Flux | GitOps: the cluster pulls the state from Git | continuous sync, drift detection, rollback through Git | Kubernetes only, does not replace CI |
| Others | CircleCI, Azure Pipelines, Tekton, Dagger, Buildkite | depending on the ecosystem | — |
Push or pull #
In push mode, the pipeline connects to the target and deploys (kubectl, Helm, SSH): simple, but CI holds access to production. In pull mode (GitOps), an agent in the cluster reads the repository and applies the changes: CI only needs to write to Git.
2. A complete pipeline with GitHub Actions #
From commit to production.
The workflow #
name: delivery
on:
push: { branches: [main] }
pull_request: {}
permissions: { contents: read }
concurrency: { group: delivery-${{ github.ref }}, cancel-in-progress: true }
jobs:
test:
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.txt && ruff check . && pytest -q
build:
needs: test
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write, id-token: write }
outputs: { image: "${{ steps.meta.outputs.image }}" }
steps:
- uses: actions/checkout@v4
- id: meta
run: echo "image=ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- uses: docker/build-push-action@v6
with: { push: true, tags: "${{ steps.meta.outputs.image }}" }
- uses: aquasecurity/trivy-action@0.28.0
with: { image-ref: "${{ steps.meta.outputs.image }}", severity: CRITICAL, exit-code: "1" }
deploy-staging:
needs: build
environment: staging
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh staging "${{ needs.build.outputs.image }}"
- run: ./smoke-test.sh https://staging.example.com
deploy-production:
needs: [build, deploy-staging]
environment: production # règle d'approbation configurée sur l'environnement
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh production "${{ needs.build.outputs.image }}"
- run: ./smoke-test.sh https://shop.example.comReusing #
# workflow réutilisable appelé par plusieurs dépôts
jobs:
ci:
uses: mon-org/pipelines/.github/workflows/python-ci.yml@v3
with: { python-version: "3.12" }
secrets: inherit3. The same pipeline with GitLab CI and Jenkins #
The concepts are identical.
GitLab CI #
stages: [test, build, deploy]
variables: { IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" }
test:
stage: test
image: python:3.12
cache: { paths: [.cache/pip] }
script: [pip install -r requirements.txt, pytest -q]
build:
stage: build
image: docker:27
services: [docker:27-dind]
rules: [{ if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' }]
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE . && docker push $IMAGE
deploy-staging:
stage: deploy
environment: { name: staging, url: https://staging.example.com }
script: [./deploy.sh staging $IMAGE]
deploy-production:
stage: deploy
environment: { name: production }
when: manual
script: [./deploy.sh production $IMAGE]Jenkins #
// Jenkinsfile (pipeline déclaratif)
pipeline {
agent { docker { image 'python:3.12' } }
environment { IMAGE = "registry.example.com/shop/api:${env.GIT_COMMIT.take(7)}" }
stages {
stage('Test') { steps { sh 'pip install -r requirements.txt && pytest -q' } }
stage('Build') { agent any
steps { sh 'docker build -t $IMAGE . && docker push $IMAGE' } }
stage('Staging') { steps { sh './deploy.sh staging $IMAGE' } }
stage('Production') {
input { message 'Déployer en production ?' }
steps { sh './deploy.sh production $IMAGE' } }
}
post { failure { echo 'Pipeline en échec : prévenir l\'équipe' } }
}4. Pipeline security and health #
CI is a prime target.
Securing #
- Minimal permissions by default (
permissions: contents: read), widened job by job. - Third-party actions pinned by commit SHA, reviewed before adoption.
- No secrets exposed to pull requests coming from forks.
- Ephemeral self-hosted runners (one job, one machine), never shared between projects of different trust levels.
- OIDC to the cloud rather than long-lived keys.
Common mistake: A persistent self-hosted runner attached to a public repository lets anyone run code on your network through a pull request.
Measuring the pipeline #
| Indicator | Target |
|---|---|
| Commit pipeline duration | < 10 min |
| Success rate on main | > 90% |
| Time to fix a broken build | < 1 h |
| Rerun rate (flaky tests) | decreasing |
| Commit → production lead time | see the DORA metrics |
