How to Migrate from Jenkins to GitHub Actions (Step-by-Step)
DevOps / SRE Engineer & CEO, Cloud DevOps Solutions
Jenkins has served teams well for 15 years, but its operational overhead, managing controllers, agents, plugins, and upgrades, is a constant drain. GitHub Actions eliminates the controller entirely and runs where your code already lives. Here is how to migrate with minimal disruption.
Concept mapping
| Jenkins | GitHub Actions | Notes |
|---|---|---|
| Jenkinsfile | .github/workflows/*.yml | One workflow per concern |
| Pipeline | Workflow | Triggered by events |
| Stage | Job | Can run in parallel |
| Step | Step | Shell command or Action |
| Agent | Runner | GitHub-hosted or self-hosted |
| Credentials | Secrets | Org/repo/environment scoped |
| Shared Library | Reusable Workflow / Action | More composable |
| Parameter | Input / workflow_dispatch | Manual trigger inputs |
| Stash/Unstash | Artifacts | upload-artifact / download-artifact |
when { branch 'main' } | if: github.ref == 'refs/heads/main' | Conditional jobs |
| Blue Ocean | GitHub Actions UI | Built into GitHub |
Simple Jenkinsfile conversion
Before (Jenkins):
pipeline {
agent any
stages {
stage('Install') {
steps { sh 'npm ci' }
}
stage('Test') {
steps { sh 'npm test' }
post { always { junit 'test-results/*.xml' } }
}
stage('Build') {
steps { sh 'npm run build' }
}
stage('Deploy') {
when { branch 'main' }
steps { sh './deploy.sh' }
}
}
}
After (GitHub Actions):
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
test:
runs-on: ubuntu-latest
needs: install
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm test
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results/*.xml
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
Handling parallel stages
Jenkins parallel:
stage('Test') {
parallel {
stage('Unit') { steps { sh 'npm run test:unit' } }
stage('Integration') { steps { sh 'npm run test:integration' } }
stage('E2E') { steps { sh 'npm run test:e2e' } }
}
}
GitHub Actions (jobs run in parallel by default when they share the same needs):
test-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run test:unit
test-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run test:integration
test-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run test:e2e
deploy:
needs: [test-unit, test-integration, test-e2e] # waits for all three
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
Migrating Jenkins credentials
Jenkins stores credentials in its own encrypted store. Map them to GitHub secrets:
| Jenkins credential type | GitHub equivalent |
|---|---|
| Username/password | Two separate secrets (_USER and _PASS) |
| Secret text | Repository or org secret |
| SSH private key | Secret containing the key content |
| Certificate | Secret containing PEM content |
# Export from Jenkins (requires Jenkins CLI)
java -jar jenkins-cli.jar -s http://jenkins:8080 \
-auth admin:token \
list-credentials-as-xml system::system::jenkins _
# Add to GitHub via CLI
gh secret set AWS_ACCESS_KEY_ID --body "AKIA..."
gh secret set AWS_SECRET_ACCESS_KEY --body "..."
gh secret set KUBECONFIG --body "$(cat ~/.kube/config | base64)"
For environment-specific secrets (staging vs production), use GitHub Environments:
deploy-production:
environment: production # environment secrets override repo secrets
runs-on: ubuntu-latest
steps:
- run: echo "Using production DB_HOST=${{ secrets.DB_HOST }}"
Self-hosted runners
If your Jenkins agents have special requirements (private network access, GPU, specific tools), replace them with self-hosted GitHub Actions runners.
# On the machine that used to be a Jenkins agent
mkdir actions-runner && cd actions-runner
# Download runner (get latest URL from GitHub Settings > Actions > Runners)
curl -O -L https://github.com/actions/runner/releases/download/v2.319.1/actions-runner-linux-x64-2.319.1.tar.gz
tar xzf ./actions-runner-linux-x64-2.319.1.tar.gz
# Configure
./config.sh \
--url https://github.com/your-org \
--token YOUR_REGISTRATION_TOKEN \
--labels self-hosted,linux,x64,private-network
# Install as service
sudo ./svc.sh install && sudo ./svc.sh start
Use runner labels to route jobs:
jobs:
deploy:
runs-on: [self-hosted, private-network]
Matrix builds
Jenkins matrix (requires Matrix plugin):
matrix {
axes {
axis { name 'NODE'; values '18', '20', '22' }
}
stages {
stage('Test') { steps { sh "nvm use $NODE && npm test" } }
}
}
GitHub Actions matrix:
test:
strategy:
matrix:
node-version: [18, 20, 22]
fail-fast: false
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci && npm test
Jenkins Shared Libraries → Reusable Workflows
Jenkins shared libraries are Groovy; GitHub Actions equivalent is reusable workflows.
# .github/workflows/reusable-deploy.yml
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
KUBECONFIG:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- run: helm upgrade --install my-app ./helm/my-app -n ${{ inputs.environment }}
Calling it:
# In another workflow
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
secrets:
KUBECONFIG: ${{ secrets.KUBECONFIG }}
Common gotchas
npm cineeds apackage-lock.json, if you usednpm installin Jenkins,package-lock.jsonmay be.gitignored. Add it to git.- GitHub Actions has a 6-hour job timeout, long-running Jenkins jobs need to be split or optimised.
- No persistent workspace, each job starts clean. Use
actions/cacheoractions/upload-artifactto share state. - Secret masking differs, GitHub Actions masks secrets in logs, but multiline secrets need base64 encoding.
- Plugin parity, Jenkins has 1,800+ plugins; most have Action equivalents but check before assuming.
Migration checklist
- Inventory all Jenkinsfiles and shared libraries
- Map credentials to GitHub secrets (org vs repo vs environment)
- Identify self-hosted agent requirements
- Create workflow files in a branch; test against PRs before merging
- Run old and new pipelines in parallel for 2 weeks
- Decommission Jenkins after validating all jobs pass
Key takeaways
- Jobs in GitHub Actions run in parallel by default; use
needsto sequence them. - Replace Jenkins credentials with GitHub Secrets; use Environments for per-environment overrides.
- Self-hosted runners replace Jenkins agents with minimal configuration.
- Reusable Workflows replace Shared Libraries with better composability and type safety.
- The biggest migration risk is uncovered credentials and hard-coded paths, audit both before cutting over.
Planning the move
If you are still deciding whether to migrate, see how we approach a Jenkins migration. For a staged plan with a parallel run and rollback, read the Jenkins to GitHub Actions migration plan.