Jenkins works. It has worked for over a decade. But maintaining Jenkins — the server, the plugins, the security patches, the Groovy scripts — is a full-time job that adds zero business value.
We have migrated 8 teams from Jenkins to GitHub Actions. Some had 10 pipelines. One had 200+. This is the process we follow, the problems we hit, and the patterns that work.
Why Teams Move Away from Jenkins
Every migration we have done started with the same complaints:
Maintenance overhead. Jenkins is self-hosted. You manage the server, the JVM, plugin updates, security patches, backups, and scaling. One client had a dedicated engineer spending 30% of their time maintaining Jenkins infrastructure.
Plugin compatibility hell. Jenkins has 1,800+ plugins, but updating one can break three others. Teams freeze plugin versions to avoid breakage, then accumulate security vulnerabilities.
Groovy complexity. Jenkins Pipelines use Groovy — a powerful but complex language that most developers do not know well. Debugging shared libraries and @NonCPS annotations is a specialised skill.
Scaling challenges. Jenkins agents need provisioning, monitoring, and lifecycle management. Dynamic agent provisioning helps but adds another layer of complexity.
GitHub Actions advantages:
- Fully managed — no servers, no JVM, no plugin updates
- YAML-based workflows — simpler to read and review than Groovy
- Native GitHub integration — PR checks, deployments, environments, OIDC
- Marketplace with 20,000+ actions
- Self-hosted runners when you need them
The Migration Process
Step 1: Audit Your Jenkins Environment (Week 1)
Before migrating a single pipeline, understand what you are working with:
Jenkins Audit Checklist:
├── Pipelines
│ ├── Total count
│ ├── Declarative vs Scripted
│ ├── Multi-branch vs standalone
│ └── Trigger types (webhook, cron, upstream)
├── Plugins
│ ├── List all installed plugins
│ ├── Identify which are used in pipelines
│ └── Check GitHub Actions equivalents
├── Shared Libraries
│ ├── Custom functions and classes
│ ├── Usage across pipelines
│ └── Rewrite strategy (reusable workflows vs composite actions)
├── Credentials
│ ├── Jenkins credential store entries
│ ├── Types (username/password, SSH key, token, secret file)
│ └── Map to GitHub Secrets and OIDC
├── Agents
│ ├── Static vs dynamic
│ ├── Labels and capabilities
│ └── Self-hosted runner requirements
└── Integrations
├── Artifact storage (Nexus, Artifactory, S3)
├── Deployment targets
├── Notification channels (Slack, email, Teams)
└── Code quality tools (SonarQube, etc.)
GitHub provides the Actions Importer tool to automate this audit:
# Install
gh extension install github/gh-actions-importer
# Audit your Jenkins instance
gh actions-importer audit jenkins \
--jenkins-instance-url https://jenkins.example.com \
--jenkins-username admin \
--jenkins-access-token $JENKINS_TOKEN \
--output-dir audit-results
The audit report shows conversion rates — what percentage of your pipelines can be automatically converted, partially converted, or need manual rewriting.
Step 2: Map Jenkins Concepts to GitHub Actions
| Jenkins | GitHub Actions | Notes |
|---|---|---|
| Pipeline | Workflow (.yml file) | One workflow per file |
| Stage | Job | Jobs run in parallel by default |
| Step | Step | Sequential within a job |
| Agent | Runner | ubuntu-latest or self-hosted |
| Node label | runs-on | Runner selection |
when / if | if condition | Expression syntax differs |
parameters | workflow_dispatch inputs | Manual trigger inputs |
cron trigger | schedule | Same cron syntax |
| Shared Library | Reusable workflow / composite action | Different patterns |
| Credentials | GitHub Secrets + OIDC | Prefer OIDC for cloud access |
post { always } | if: always() | Cleanup steps |
| Multibranch | Branch filter in on: | Simpler configuration |
| Upstream trigger | workflow_run | Chain workflows |
Step 3: Convert Pipelines
Automated conversion:
# Dry run — see the converted YAML without creating a PR
gh actions-importer dry-run jenkins \
--jenkins-instance-url https://jenkins.example.com \
--pipeline-id my-pipeline \
--output-dir converted
# Full migration — creates a PR with the converted workflow
gh actions-importer migrate jenkins \
--jenkins-instance-url https://jenkins.example.com \
--pipeline-id my-pipeline \
--target-url https://github.com/org/repo
Manual conversion example:
Jenkins Declarative Pipeline:
pipeline {
agent { label 'docker' }
environment {
AWS_REGION = 'eu-west-1'
}
stages {
stage('Build') {
steps {
sh 'docker build -t myapp:${BUILD_NUMBER} .'
}
}
stage('Test') {
steps {
sh 'docker run myapp:${BUILD_NUMBER} npm test'
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
withCredentials([string(credentialsId: 'aws-key', variable: 'AWS_ACCESS_KEY_ID')]) {
sh '''
aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REGISTRY
docker push $ECR_REGISTRY/myapp:${BUILD_NUMBER}
kubectl set image deployment/web web=$ECR_REGISTRY/myapp:${BUILD_NUMBER}
'''
}
}
}
}
post {
failure {
slackSend channel: '#deploys', message: "Build failed: ${env.JOB_NAME}"
}
}
}
Equivalent GitHub Actions workflow:
name: Build and Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
AWS_REGION: eu-west-1
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.run_number }} .
- name: Run tests
run: docker run myapp:${{ github.run_number }} npm test
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-arn: arn:aws:iam::123456789:role/github-actions
aws-region: ${{ env.AWS_REGION }}
- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build, push, and deploy
run: |
docker build -t $ECR_REGISTRY/myapp:${{ github.run_number }} .
docker push $ECR_REGISTRY/myapp:${{ github.run_number }}
kubectl set image deployment/web web=$ECR_REGISTRY/myapp:${{ github.run_number }}
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v2
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
payload: '{"text": "Deploy failed: ${{ github.repository }}"}'
Key improvements:
- OIDC authentication instead of stored AWS keys — no long-lived credentials
- PR checks — tests run on every pull request, not just pushes
- Native GitHub context —
github.run_number,github.ref, etc. replace Jenkins variables - Declarative YAML instead of Groovy
Step 4: Replace Jenkins Plugins
Common plugin replacements:
| Jenkins Plugin | GitHub Actions Replacement |
|---|---|
| Docker Pipeline | docker/build-push-action |
| AWS Steps | aws-actions/configure-aws-credentials + OIDC |
| Slack Notification | slackapi/slack-github-action |
| SonarQube | SonarSource/sonarcloud-github-action |
| JUnit | Built-in test reporting with actions/upload-artifact |
| Git | Built-in (actions/checkout) |
| Credentials Binding | GitHub Secrets + OIDC |
| Parameterised Build | workflow_dispatch with inputs |
| Build Discarder | Automatic (GitHub manages retention) |
| Email Extension | GitHub notifications or custom action |
| Artifactory/Nexus | actions/upload-artifact or direct API calls |
| Kubernetes Deploy | azure/k8s-deploy or kubectl directly |
Step 5: Replace Shared Libraries
Jenkins shared libraries are the hardest part of any migration. They contain custom Groovy functions used across many pipelines.
Option A: Reusable Workflows (for pipeline-level reuse)
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image_tag:
required: true
type: string
secrets:
KUBE_CONFIG:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/web web=myapp:${{ inputs.image_tag }}
kubectl rollout status deployment/web
# Called from another workflow
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
image_tag: ${{ github.sha }}
secrets:
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG_STAGING }}
Option B: Composite Actions (for step-level reuse)
# .github/actions/notify/action.yml
name: Notify
inputs:
status:
required: true
channel:
required: true
runs:
using: composite
steps:
- name: Send notification
shell: bash
run: |
curl -X POST ${{ inputs.webhook }} \
-d '{"channel": "${{ inputs.channel }}", "text": "Deploy ${{ inputs.status }}"}'
The 3 Hardest Migrations
Migration #2: 200+ Scripted Pipelines
A fintech client had 200+ Jenkins pipelines, most using scripted (not declarative) syntax. The Actions Importer could only automatically convert about 60%. The remaining 40% used custom Groovy logic, dynamic stage generation, and shared libraries heavily. This took 6 weeks with two engineers.
Migration #5: Air-gapped Environment
A client in a regulated industry could not use GitHub-hosted runners. We set up self-hosted runners in their private network with auto-scaling using the actions-runner-controller on their EKS cluster.
Migration #7: Complex Shared Libraries
A platform team had built 15 shared libraries with 100+ functions covering deployment, testing, notifications, and compliance checks. We mapped each function to either a reusable workflow, composite action, or inline step. The library conversion alone took 2 weeks.
Migration Timeline
| Pipeline Count | Complexity | Estimated Timeline |
|---|---|---|
| 1-10 | Simple (declarative, few plugins) | 1-2 days |
| 10-50 | Medium (mix of declarative/scripted) | 1-2 weeks |
| 50-100 | Complex (shared libraries, custom plugins) | 3-4 weeks |
| 100+ | Enterprise (air-gapped, heavy customisation) | 6-8 weeks |
When NOT to Migrate
Stay on Jenkins if:
- You are not on GitHub — GitLab CI or Bitbucket Pipelines are better matches for those platforms
- You need extreme customisation — Jenkins’ plugin architecture is still unmatched for niche use cases
- You have massive parallelism needs — Jenkins can scale to thousands of concurrent builds with proper agent infrastructure
- Your pipelines are deeply tied to on-premise systems that cannot be reached from GitHub-hosted runners (though self-hosted runners solve this)
Ready to Migrate Off Jenkins?
We have migrated 8 teams from Jenkins to GitHub Actions — from 10-pipeline startups to 200+ pipeline enterprises with complex shared libraries.
Our CI/CD consulting services cover:
- Pipeline audit — inventory your Jenkins setup and estimate conversion effort
- Automated + manual conversion — use Actions Importer for what it handles, hand-convert the rest
- Shared library migration — convert Groovy libraries to reusable workflows and composite actions
- Self-hosted runners — set up auto-scaling runners on EKS for private network access
- OIDC setup — replace stored credentials with keyless AWS/Azure/GCP authentication
We also help teams set up ArgoCD for GitOps-based Kubernetes deployments alongside GitHub Actions.