Migrate Jenkins to GitHub Actions: Step by Step
Last updated
Migrating Jenkins to GitHub Actions means translating a Jenkinsfile into workflow YAML: stages become jobs or steps, agent becomes runs-on, credentials become secrets, and shared libraries become composite actions or reusable workflows. Declarative pipelines translate well. Scripted pipelines with heavy Groovy logic are the part that takes time. This guide gives the mapping, a worked translation, what to do about agents and plugins, and a cutover plan.
Related pages: Jenkins vs GitHub Actions and Jenkins alternatives.
Concept mapping
| Jenkins | GitHub Actions |
|---|---|
Jenkinsfile | .github/workflows/*.yml |
| Multibranch pipeline | Workflow on: push / pull_request (runs per branch automatically) |
agent { label 'x' } | runs-on: [label] |
agent { docker { image 'x' } } | container: x on the job |
stages / stage | jobs (parallel by default) or sequential steps |
parallel { } | Separate jobs, or a matrix |
steps { sh '...' } | steps: - run: ... |
environment { } | env: at workflow, job or step level |
credentials('id') | ${{ secrets.NAME }} |
when { branch 'main' } | if: github.ref == 'refs/heads/main' |
post { always { } } | Step with if: always() |
stash / unstash, archiveArtifacts | actions/upload-artifact / download-artifact |
input (manual approval) | Environment with required reviewers |
triggers { cron() } | on.schedule |
parameters { } | workflow_dispatch.inputs |
| Shared libraries | Composite actions, reusable workflows |
| Plugins | Marketplace actions, or plain scripts |
| Jenkins controller + agents | GitHub-hosted runners or self-hosted runners |
| Workspace persists on agent | Fresh VM per job unless you cache or use a persistent runner |
That last row is the one that surprises people. A Jenkins agent often keeps its workspace, Maven repository and Docker layers between builds. A hosted GitHub runner starts empty every job, so you need explicit caching to get the speed back.
A realistic pipeline, side by side
A declarative Jenkinsfile for a Java service:
pipeline {
agent { label 'linux' }
options { timeout(time: 30, unit: 'MINUTES') }
environment {
REGISTRY = 'registry.example.com'
DEPLOY_KEY = credentials('deploy-key')
}
stages {
stage('Build') {
steps { sh './gradlew assemble' }
}
stage('Test') {
parallel {
stage('Unit') { steps { sh './gradlew test' } }
stage('Integration') { steps { sh './gradlew integrationTest' } }
}
post { always { junit '**/build/test-results/**/*.xml' } }
}
stage('Deploy') {
when { branch 'main' }
steps { sh './scripts/deploy.sh' }
}
}
post { failure { mail to: 'team@example.com', subject: 'Build failed' } }
}
The GitHub Actions version:
name: CI
on:
push:
branches: [main]
pull_request:
env:
REGISTRY: registry.example.com
jobs:
build:
runs-on: ubuntu-latest # or [self-hosted, Linux, compiler-ci]
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v6
- run: ./gradlew assemble
test:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
task: [test, integrationTest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v6
- run: ./gradlew ${{ matrix.task }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: results-${{ matrix.task }}
path: "**/build/test-results/**/*.xml"
deploy:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v6
- run: ./scripts/deploy.sh
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
Notes on the translation:
- Each GitHub job runs on a fresh machine, so
buildandtestdo not share a workspace. Either rebuild in each job (cheap with the Gradle cache) or pass outputs with artifacts. Mergingbuildinto the test jobs is often simpler. parallelstages become a matrix or separate jobs. See matrix builds.post { failure { mail } }becomes a notification step withif: failure(), or rely on GitHub's built-in workflow failure notifications.credentials('deploy-key')becomes a secret. Create it in the repo, organisation or an environment.when { branch 'main' }becomes anif:condition. For manual approval, add required reviewers to theproductionenvironment.- Test results are uploaded as artifacts. For a summary in the pull request, use a JUnit reporter action.
Agents, labels and self-hosting
Many Jenkins shops run their own agents for a reason: private network access, special hardware, or cost. GitHub Actions supports self-hosted runners with labels, which map onto Jenkins labels. Hosted runners cannot reach private networks without a tunnel or an approach such as OIDC plus a public endpoint. If you need private access, plan self-hosted runners and read the security notes: never run untrusted fork code on a long-lived runner.
If you moved to GitHub-hosted runners and builds got slower than on your Jenkins agents, that is usually the 2 vCPU default plus a cold cache. compiler.dev runs the same jobs on 8 vCPU / 16 GB machines with dependency and Docker layer caches ready, at $0.012 per minute, through runs-on: [self-hosted, Linux, compiler-ci], with automatic fallback to your original runners; see pricing and faster runners.
Plugins
Audit your installed plugins, then sort them into three groups:
- Replaced by a built-in or marketplace action (SCM, JUnit, Slack notify, credentials, artifact archiving).
- Replaced by a script (anything that wrapped a CLI).
- No equivalent (rare plugins that extend the Jenkins UI). Decide whether you still need them.
Check marketplace actions before adopting: pin by commit SHA for third-party actions, as covered in securing GitHub Actions.
Credentials and secrets
Jenkins stores credentials in the controller, and you cannot export secret values through the UI. Source them from your password manager or vault, then add them as GitHub secrets. Replace long-lived cloud keys with OIDC where possible: GitHub Actions OIDC with AWS. Secrets from the controller's global scope become organisation secrets; per-folder credentials become repository or environment secrets.
Caching
Jenkins agents often cached by being long-lived. In Actions, add caching explicitly:
- Gradle: cache-gradle-github-actions (the
setup-gradleaction above does it). - npm, pnpm, Yarn: npm, pnpm, Yarn.
- Docker: Docker layer caching and buildx cache.
- General rules and the 10 GB limit: caching guide.
Automated conversion: GitHub Actions Importer
GitHub's Importer is a gh extension. Per GitHub's docs (checked October 2026) it supports Jenkins, CircleCI, GitLab CI, Azure DevOps, Bamboo, Bitbucket Pipelines and Travis CI, and it requires Docker and the GitHub CLI.
gh extension install github/gh-actions-importer
gh actions-importer configure
gh actions-importer audit jenkins --output-dir audit --source-url https://jenkins.example.com
The documented commands are audit, forecast, dry-run and migrate. Start with audit against your Jenkins server for a report of what converts and what does not, then dry-run to write workflows locally. Expect to hand-convert shared libraries, custom Groovy and plugin-specific steps. Check GitHub's current Importer docs for exact flags for Jenkins, since credentials and URLs are passed differently from other platforms.
Cutover plan
- Inventory jobs, plugins, credentials, triggers and who depends on each.
- Audit with the Importer; pick the simplest pipeline first.
- Port and run in parallel with Jenkins for one or two weeks.
- Move credentials and deployment stages.
- Switch required checks in branch protection.
- Freeze Jenkins jobs (read-only), keep the controller for a release cycle for history, then retire it.
FAQ
Can GitHub Actions replace Jenkins fully?
For most build, test and deploy pipelines, yes. Keep Jenkins where you depend on a plugin without an equivalent or need controller-level features.
How do I replace Jenkins shared libraries?
Composite actions for reusable steps, reusable workflows for whole pipelines. Convert the Groovy logic to shell or a small script.
What about Jenkins agents on private networks?
Use self-hosted runners in that network with matching labels.
Is the Jenkins workspace persistence a problem?
It can be. Add caching, or use runners that keep caches warm, instead of relying on leftover files.
How long does it take?
A simple declarative pipeline: hours. Dozens of scripted pipelines and plugins: plan weeks, mostly for validation.
Related
- Migrate CircleCI to GitHub Actions: Step by StepMigrate CircleCI to GitHub Actions with a concept mapping table, side-by-side config translation, secrets, caching, the Importer and a cutover plan.
- Migrate GitLab CI to GitHub Actions: Step by StepMigrate GitLab CI to GitHub Actions with a .gitlab-ci.yml mapping table, side-by-side translation, variables, cache, services, Importer and cutover.
- Jenkins vs GitHub Actions with compiler.dev runnersJenkins vs GitHub Actions on compiler.dev runners: zero license cost vs real ops cost, plugin flexibility, and a Jenkinsfile to workflow migration.
- Best Jenkins Alternatives in 2026Best Jenkins alternatives in 2026: GitHub Actions, GitLab CI, Buildkite, CircleCI, Semaphore and compiler.dev, with migration notes from Jenkinsfile.