Skip to content
compiler.dev

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

JenkinsGitHub Actions
Jenkinsfile.github/workflows/*.yml
Multibranch pipelineWorkflow on: push / pull_request (runs per branch automatically)
agent { label 'x' }runs-on: [label]
agent { docker { image 'x' } }container: x on the job
stages / stagejobs (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, archiveArtifactsactions/upload-artifact / download-artifact
input (manual approval)Environment with required reviewers
triggers { cron() }on.schedule
parameters { }workflow_dispatch.inputs
Shared librariesComposite actions, reusable workflows
PluginsMarketplace actions, or plain scripts
Jenkins controller + agentsGitHub-hosted runners or self-hosted runners
Workspace persists on agentFresh 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 build and test do not share a workspace. Either rebuild in each job (cheap with the Gradle cache) or pass outputs with artifacts. Merging build into the test jobs is often simpler.
  • parallel stages become a matrix or separate jobs. See matrix builds.
  • post { failure { mail } } becomes a notification step with if: 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 an if: condition. For manual approval, add required reviewers to the production environment.
  • 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:

  1. Replaced by a built-in or marketplace action (SCM, JUnit, Slack notify, credentials, artifact archiving).
  2. Replaced by a script (anything that wrapped a CLI).
  3. 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:

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

  1. Inventory jobs, plugins, credentials, triggers and who depends on each.
  2. Audit with the Importer; pick the simplest pipeline first.
  3. Port and run in parallel with Jenkins for one or two weeks.
  4. Move credentials and deployment stages.
  5. Switch required checks in branch protection.
  6. 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.

Run this workflow on compiler.dev 200 free minutes

Start free