Migrating from Jenkins to GitHub Actions: what survives and what breaks
A pipeline migration is not a YAML translation exercise. It is a re-examination of every step in the pipeline and a decision about whether that step still belongs there. Most Jenkins pipelines carry years of accumulated logic that nobody remembers adding, and the migration is the first opportunity in years to delete it.
Start with the inventory
Before writing a single workflow file, list every pipeline and what it does:
# Dump all job names from Jenkins
jenkins-cli list-jobs
# For each job, get its configuration
jenkins-cli get-job <job-name>
You do not need to understand every line. You need to know which jobs exist, which repos they serve, which environments they deploy to and which approvals they wait on. The inventory will already show you jobs that nobody has touched in months — those are candidates for deletion, not migration.
Secrets: the thing that breaks first
Jenkins stores secrets in its credential store. GitHub Actions stores them in repository or organisation secrets. The secrets themselves are the same values — API keys, tokens, certificates — but the injection mechanism is different.
In Jenkins, a secret is a credential binding in a pipeline step. In GitHub Actions, a secret is accessed as ${{ secrets.SECRET_NAME }} and is not printed to logs automatically:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Authenticate to AWS
run: |
aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }}
The security upgrade you should make during migration: replace long-lived credentials with OIDC federation. Instead of storing AWS access keys as secrets, configure GitHub Actions as an OIDC identity provider in AWS and use short-lived tokens:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
aws-region: us-east-1
No stored credentials. No rotated keys. No key that leaks when a developer adds echo $AWS_SECRET_ACCESS_KEY to a workflow. I wrote about the full transition in detail:
Artifacts: the thing nobody thought about
Jenkins stores build artifacts on the master or on a dedicated artifact server. GitHub Actions stores them as workflow artifacts with a 90-day retention period by default.
The migration question is: do your pipelines produce artifacts that need to outlive 90 days? If they do — release binaries, container images, documentation builds — the artifact needs a destination that is not GitHub Actions:
- Container images: push to a registry (ECR, ACR, Docker Hub). The workflow is the builder, not the store.
- Release artifacts: upload to S3 or Azure Blob Storage with a lifecycle policy.
- Temporary artifacts: use
actions/upload-artifactandactions/download-artifactwithin the workflow.
Do not migrate the pattern of "the CI server stores the build output." That pattern was born from Jenkins having a local filesystem. GitHub Actions does not have one, and the artifact storage is not designed for it.
Approvals: what pushes to production
Jenkins has a manual approval step. GitHub Actions has environments with protection rules:
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- run: ./deploy.sh production
The environment key triggers the configured protection rules — required reviewers, wait timers, deployment branches. Configure these in the repository settings, not in the workflow file.
The feature difference that catches teams: Jenkins approvals can be conditional — "approve if the change is to /docs, auto-approve otherwise." GitHub Actions environment protections are per-environment, not per-workflow. If you need conditional approvals, implement them as a custom step that calls the GitHub API.
The shared library problem
Many Jenkins installations have a shared library — a Groovy repo with custom pipeline steps. This is the hardest part of the migration, because GitHub Actions does not have the same concept.
The replacement is reusable workflows:
# .github/workflows/deploy.yml — the reusable workflow
name: Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- run: ./deploy.sh ${{ inputs.environment }}
# In each repo's workflow
jobs:
deploy-staging:
uses: my-org/.github/.github/workflows/deploy.yml@main
with:
environment: staging
A reusable workflow is not a shared library. It is a workflow template that can be called from other repositories. The migration from a Groovy shared library to reusable workflows is not a one-to-one mapping; it is a redesign of how shared pipeline logic is packaged and distributed.
What to delete
A migration is also a cleanup. These are the Jenkins patterns you can delete during the move:
- Build matrices that expanded into hundreds of jobs — GitHub Actions has matrix builds natively. One workflow definition, not a parameterised job that generates a hundred others.
- Cron schedules for nightly builds — move them to
on: schedulein the workflow. Same behaviour, fewer lines. - The plugin that nobody owns — Jenkins plugins multiply over time. If a pipeline step depends on a plugin that has no maintainer, find out what the step does and whether it is still needed. Most are not.
- The script that polls for a downstream job — replace with
workflow_runorrepository_dispatch. The polling was a workaround for Jenkins's event model; GitHub Actions has native event triggers.
The migration timeline
A full Jenkins-to-GitHub Actions migration for a team of 20-50 repos takes 2-4 weeks of focused work, broken into:
- Week 1: Inventory and secret migration. Run both CI systems in parallel.
- Week 2-3: Migrate pipelines, starting with the simplest repos. The pattern you develop on repo three will carry through to repo twenty.
- Week 4: Decommission Jenkins. Do not leave it running "just in case" — a decommissioned Jenkins that is still running becomes a permanent shadow CI system that nobody maintains and everyone occasionally uses.
Migrating CI/CD or building pipelines from scratch? I take on CI/CD pipeline engineering and fractional DevOps retainers as freelance and contract work — independently, for teams in the US, EU and APAC.
