
Migrate GitHub Actions to Node.js 24 Before the Deadline
Jenkins still runs more enterprise CI pipelines than any other system, but the momentum has shifted. GitHub Actions offers tighter integration with your repositories, a massive ecosystem of community actions, and a declarative YAML syntax that most developers pick up in an afternoon. The real question isn't whether to migrate. It's how to do it without losing the conventions your team has built over years of Jenkinsfile grooming.
This guide walks through each major Jenkins concept, shows its GitHub Actions equivalent with working code, and covers the pitfalls that trip up teams mid-migration. If you're standing up Actions for the first time, you're also in the best position to add automated code review as a required status check before legacy patterns get locked in.
A Jenkins declarative pipeline and a GitHub Actions workflow express the same ideas in different shapes. Here's how the core constructs map across:
pipeline becomes the workflow file itself. Each .yml file under .github/workflows/ is a self-contained pipeline.
agent becomes runs-on. Where Jenkins uses agent { label 'linux' }, Actions uses runs-on: ubuntu-latest (or a custom runner label).
stages / stage become jobs. Each Jenkins stage maps to a separate job. Sequential ordering uses needs to express dependencies between jobs.
steps stay as steps. The concept is nearly identical, but Actions steps can reference marketplace actions with uses: instead of just running shell commands.
triggers become on:. Jenkins pollSCM and webhook triggers translate to on: push, on: pull_request, or on: schedule with cron syntax.
environment becomes env:. You can set environment variables at the workflow, job, or step level.
post conditions (always, success, failure) map to if: always(), if: success(), and if: failure() on individual steps.
Here's a minimal Jenkins declarative pipeline and its Actions equivalent side by side.
Jenkins:
pipeline {
agent { label 'linux' }
stages {
stage('Build') {
steps {
sh 'npm ci'
sh 'npm run build'
}
}
stage('Test') {
steps {
sh 'npm test'
}
}
stage('Deploy') {
when { branch 'main' }
steps {
sh './deploy.sh'
}
}
}
post {
failure {
mail to: 'team@example.com', subject: 'Build failed'
}
}
}GitHub Actions equivalent:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
notify-failure:
needs: [build, test, deploy]
if: failure()
runs-on: ubuntu-latest
steps:
- uses: dawidd6/action-send-mail@v3
with:
to: team@example.com
subject: Build failedA few things jump out. Actions requires an explicit actions/checkout step because each job starts with a clean workspace. Jenkins implicitly checks out the repo. Each job in Actions runs on a fresh VM, so you'll need to either re-run setup in each job or pass artifacts between them (more on that later).
Jenkins parallel stages look like this:
stage('Test') {
parallel {
stage('Unit Tests') { steps { sh 'npm run test:unit' } }
stage('Integration') { steps { sh 'npm run test:integration' } }
stage('E2E') { steps { sh 'npm run test:e2e' } }
}
}You have two options in GitHub Actions. The first is separate jobs without needs dependencies between them. Jobs without needs run in parallel by default:
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run test:unit
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run test:integration
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run test:e2eThe second option is a matrix strategy, which is cleaner when the jobs differ only by a single variable:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
suite: [unit, integration, e2e]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:${{ matrix.suite }}Matrix strategies are also useful for cross-platform testing. If your Jenkins pipeline uses multiple agents to test on different OS versions, a matrix with os: [ubuntu-latest, macos-latest, windows-latest] replaces that cleanly. Set fail-fast: false if you want all matrix legs to finish even when one fails, which matches Jenkins's default parallel behavior.
Jenkins Shared Libraries are the standard way to extract reusable pipeline logic into a separate repository. Teams use them for custom build steps, deployment functions, and notification helpers. GitHub Actions has two equivalents, and choosing the right one depends on what you're sharing.
Composite actions bundle multiple steps into a single reusable action. Think of them as the equivalent of a Shared Library function that runs a sequence of shell commands or other actions. You define them in a repository with an action.yml:
# .github/actions/setup-node-and-build/action.yml
name: Setup Node and Build
description: Install deps and build
inputs:
node-version:
required: false
default: '20'
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
shell: bash
- run: npm run build
shell: bashThen reference it from any workflow with uses: ./.github/actions/setup-node-and-build for local actions, or uses: your-org/shared-actions/setup-node-and-build@v1 if you host them in a separate repo.
Reusable workflows are the heavier equivalent. They let you share entire multi-job pipelines, complete with their own triggers, secrets, and output values. If your Jenkins Shared Library defines a full deployment pipeline that multiple repos call, a reusable workflow is the better fit:
# In your shared workflow repo:
# .github/workflows/deploy.yml
on:
workflow_call:
inputs:
environment:
type: string
required: true
secrets:
DEPLOY_KEY:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}Calling repos reference it with uses: your-org/shared-workflows/.github/workflows/deploy.yml@main. One limitation: reusable workflows can be nested up to four levels deep, and a calling workflow can invoke a maximum of twenty reusable workflows.
Jenkins stores credentials in its built-in credential store (or a plugin like HashiCorp Vault). GitHub Actions has two mechanisms, and you should use both.
GitHub Secrets are the direct replacement for Jenkins credentials. You store them at the repository, environment, or organization level, and reference them with ${{ secrets.MY_SECRET }}. They're encrypted at rest, masked in logs, and never exposed to forked pull requests by default.
OIDC federation is the better option for cloud provider access. Instead of storing an AWS access key or GCP service account JSON as a secret, you configure your cloud provider to trust GitHub's OIDC token. The workflow requests a short-lived token at runtime, and no long-lived credential ever touches your repo:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/deploy-role
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucketIf you're currently storing AWS keys in Jenkins Credentials, migrating to OIDC federation is the single biggest security improvement you'll get from this migration. Azure and GCP support the same pattern through their respective credential actions.
For credentials that can't use OIDC (API tokens for third-party services, npm publish tokens, etc.), use repository or organization secrets. Scope them with GitHub Environments when you need per-environment values and approval gates.
Migration is the ideal time to add automated code review. You're already rewriting CI configurations, and your branch protection rules are likely being rebuilt from scratch. Adding Tenki Code Reviewer takes about two minutes and doesn't require any workflow YAML changes.
The setup:
Tenki reviews every PR automatically, posting comments with severity-tagged findings organized from critical to low. At $1.00 per review, the pricing is predictable regardless of how long a review takes. That matters because it means your CI bill doesn't spike when the reviewer finds something complex to analyze.
The reason to do this during migration rather than after: once your new GitHub Actions workflows are established and branch protection rules are locked in, adding a new required check means coordinating across teams. During migration, you're already touching those settings.
Jenkins keeps a persistent workspace on the agent between stages. All stages within the same pipeline share the same filesystem. GitHub Actions doesn't. Each job runs on a fresh VM. If your build job produces artifacts that your test or deploy jobs need, you must explicitly pass them.
Use actions/upload-artifact and actions/download-artifact to transfer build outputs between jobs:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: dist
- run: ./deploy.shFor small data (a version string, a commit SHA), use job outputs instead. They're lighter than artifacts and don't count toward storage limits.
Jenkins keeps build artifacts forever by default (or until you configure a discard policy). GitHub Actions artifacts have a default retention of 90 days, and they count against your storage quota. Set retention-days explicitly on upload-artifact to avoid surprise storage bills. If your team relies on keeping every build's artifacts for compliance, you'll need to push them to an external store (S3, GCS) as part of the workflow.
Jenkins agents typically run on dedicated VMs or containers with resources you control. GitHub-hosted runners give you a fixed 4-core, 16GB RAM machine. That's fine for most builds, but if your Jenkins agents have 8+ cores or you're running memory-heavy test suites, the standard GitHub runner will be slower.
This is where Tenki Runners make the transition easier. They run on bare metal with varying runner sizes, so you can match the compute to your workload. In benchmarks, Tenki runners finish builds 30% faster than GitHub-hosted runners and cost up to 60% less. For a team migrating from beefy Jenkins agents, the ability to pick a runner size that matches your current agent specs means you don't have to re-tune your build for weaker hardware.
Switching is a one-line YAML change. Replace runs-on: ubuntu-latest with the Tenki runner label from your dashboard, and your workflows run on Tenki's infrastructure with zero other changes.
If your Jenkins pipeline uses Docker agents (agent { docker { image 'node:20' } }), the equivalent in Actions is a container job:
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:20
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run buildFor service containers (databases, Redis, message brokers that your tests need), use the services key on the job. This replaces Jenkins's approach of running Docker Compose or starting containers in a shell step.
Jenkins agents keep caches on disk between runs. That's gone in Actions since each run starts fresh. Use actions/cache to persist node_modules, Maven's .m2 directory, or Gradle caches between runs. The setup actions (actions/setup-node, actions/setup-java) have built-in caching options that handle this with a single cache: parameter.
Before you start converting Jenkinsfiles, audit your current setup:
Most teams complete a full migration in two to four weeks when they follow this order. The ones that struggle are the ones that try to convert everything at once, or that skip the parallel-run period and discover gaps in production.
Tags
Recommended for you
What's next in your stack.