Migrating from Jenkins to GitHub Actions: A Decision Framework and Comparison

Migrating from Jenkins to GitHub Actions: A Decision Framework and Comparison

This is the fourth and final article in the “Unified CI/CD Pipeline Governance” series. The first three articles covered why unified management matters, how to implement it with Jenkins, and how to implement it with GitHub Actions. The purpose of this article is to help you make an informed decision: whether to migrate, and if so, how. This article draws from a production environment covering 500+ repositories with over two years of operation — the migration decision is not theoretical, but a real set of tradeoffs that actually played out.


1. Conclusion First: Jenkins Shared Library Is Not Wrong

Before discussing migration, one thing must be made clear: Jenkins Shared Library is a mature, battle-tested solution. A large number of organizations have run stably on it for years. It addresses the core problem of unified CI/CD governance and has a complete ecosystem behind it.

Migration carries real costs:

  • Rewrite cost: Jenkins Groovy logic needs to be translated into GitHub Actions YAML + shell — this is not a simple format conversion
  • Training cost: Teams need to re-learn GitHub Actions concepts and debugging approaches
  • Validation cost: You need to run both pipelines in parallel and compare results to ensure there are no behavioral differences
  • Historical data: Jenkins build history, artifacts, and deployment records cannot be migrated
  • Migration scale of 500+ repositories: Migrating 500 repositories is not something that can be done in a single sprint — it requires planning in units of months to years

Do not migrate for the sake of migrating. The purpose of this article is to provide an evidence-based decision framework, not to sell you on migration.


2. The Fundamental Differences Between the Two Approaches (In Depth)

2.1 Pipeline Structure: Dynamic vs. Static

This is the most fundamental technical difference between the two approaches.

Jenkins: Runtime Dynamic Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
// JenkinsStageGenerator: stage structure is determined at runtime
void run(Map config, String vaultToken) {
stage('Lint') { ... } // always runs

if (config.jobs.find { it.name == 'unit-test' }) {
stage('Unit Test') { ... } // conditionally runs
}

// Business repos can declare any number of custom stages in config.yaml
config.jobs.findAll { it.name.startsWith('custom-') }.each { job ->
stage(job.name) { runCustomJob(job) } // dynamic stage
}
}

In the Jenkins UI, a repository without unit tests simply does not display a “Unit Test” stage.

GitHub Actions: Compile-Time Static Structure

1
2
3
4
5
# The workflow YAML fixes the job structure at parse time
jobs:
unit-test:
if: ${{ needs.config.outputs.has_unit_test == 'true' }} # can only skip, not "not exist"
...

GitHub Actions can skip a job, but the job always appears in the workflow definition. You cannot declare “this job only exists if a condition is met” in YAML.

Practical Impact at 500+ Repository Scale: For 95% of business scenarios, “can be skipped” and “does not exist” make no practical difference. But at 500+ repository scale, if dozens of repositories need to declare an arbitrary number of custom stages (database migrations, E2E tests, multi-stage deployments), Jenkins natively supports this, while GitHub Actions requires matrix or other workarounds, with a noticeable gap in the UI experience.

Actual Data: In a platform with 500+ repositories, approximately 5-10% of repositories use custom stages. This subset represents the most complex migration scenario — not because migration is impossible, but because the resulting YAML will grow significantly in size and dynamism will be reduced.

2.2 Credential Security Model: Static vs. Dynamic

DimensionJenkins AppRoleGitHub Actions JWT/OIDC
Static credentialsSecretID stored in Jenkins Credential StoreNo static credentials
Credential scopeAll stages share the same Vault tokenEach sub-workflow authenticates independently
Token lifetimeTTL 1 hour (configurable)5-minute batch token, non-renewable
Leak traceabilityYes (traceable via Vault audit log)Batch tokens do not appear in accessors
Groovy code reading credentialsTheoretically possible (sh 'printenv')Not applicable
Branch isolationNone (any branch can use the same SecretID)@refs/heads/main branch lock

Security Amplification Effect at 500+ Repository Scale:

The core risk of AppRole is the globally shared SecretID. When the platform covers 500 repositories:

  • Any repository’s Groovy Pipeline can theoretically access the shared SecretID
  • SecretID rotation requires coordinating all 500 repositories to be simultaneously idle during authentication — in a high-frequency CI environment, the rotation window is extremely difficult to control
  • When 500 repositories concurrently trigger AppRole logins, Vault rate limiting causes widespread authentication failures (see Section 3)

GitHub Actions’ job_workflow_ref + @refs/heads/main suffix enforces that all workflow modifications must pass code review before Vault access is granted. This security boundary is enforced at the authentication layer by Vault’s JWT bound_claims, and cannot be bypassed by workflow code itself:

1
2
3
4
5
6
7
8
9
10
An attacker modifies a workflow file in a feature branch of the .github repo


job_workflow_ref = "OrgA/.github/.github/workflows/platform-ci-build.yml@refs/heads/attacker-branch"


Vault verifies: bound_claims requires @refs/heads/main


✗ No match → 403 → Cannot obtain any secrets

500 repositories, not a single one storing any credentials — this is the core security value of the JWT/OIDC approach at 500+ scale.

2.3 Infrastructure Burden

DimensionJenkinsGitHub Actions
Primary node maintenanceRequired (JVM tuning, OOM troubleshooting, plugin management)Not required (GitHub-hosted)
Executor node maintenanceKubernetes cluster (pod scheduling, image updates)Self-hosted runner (if internal resources needed)
Plugin ecosystemHundreds of plugins, version compatibility requires testingActions Marketplace, versions are independent
Upgrade impactJenkins version upgrades may break PipelinesGitHub API is backward-compatible, breaking changes are rare

Real Operational Cost at 500+ Repository Scale:

Typical Jenkins operational incidents (actually occurred at 500+ repository scale):

  • 1-2 times per quarter: Kubernetes Plugin upgrades causing pod scheduling failures, affecting all CI (2-4 hours to diagnose, but all 500 repositories are halted)
  • 1-2 times per year: Jenkins primary node OOM (100-200 concurrent Pipelines at peak, JVM heap exhausted, 4-8 hours to diagnose and recover)
  • 1-2 times per month: A plugin becomes incompatible with a new Jenkins version, requiring a downgrade (1-2 hours to diagnose)
  • Vault rate limiting: When 500+ repositories push simultaneously (morning peak hours), concurrent AppRole authentication may trigger Vault rate limits, causing hundreds of Pipeline authentication failures

Typical GitHub Actions self-hosted runner operational incidents:

  • Once per quarter: Runner software update (5-minute automated script, no impact on running jobs)
  • Occasional: Runner network timeouts (re-running the job usually resolves the issue)
  • Runner capacity: 500+ repositories require planning sufficient runner counts to prevent queuing at peak times

2.4 Developer Experience

This dimension has the greatest impact on engineers’ daily work and, at 500+ repository scale, directly affects support costs:

Jenkins:

  • CI results are in the Jenkins UI; the PR page only shows “succeeded/failed”
  • Viewing failure details requires navigating to Jenkins (which may require an additional login)
  • Logs are in Jenkins’ Blue Ocean or Classic interface, disconnected from the code review workflow
  • Failure reasons are often “some stage failed,” requiring drilling down through multiple layers to see the specific error

GitHub Actions:

  • CI results are displayed directly in the PR’s Checks tab
  • Logs for each step can be expanded directly on the PR page
  • Fully integrated with code review, comments, and assignees
  • Failed steps have a direct “Re-run” button
  • The Job Summary page can display structured reports (config parsing results, list of covered modules, etc.)

Quantified Impact (500+ Repository Scale):

A platform team survey found the average time to diagnose a CI failure:

  • Jenkins: 8-12 minutes (including switching platforms, finding the relevant build, locating the stage)
  • GitHub Actions: 3-5 minutes (expand directly on the PR page)

At 500+ repositories with hundreds of CI runs per day, if each CI failure takes an average of 6 fewer minutes to diagnose, a 100-person engineering team saves approximately 50-100 person-hours per week. This figure directly supports the ROI calculation for migration investment.

2.5 Configuration Merging Capability

DimensionJenkins Groovy MergerGitHub Actions shell + yq
Type safetyYes (Groovy strong typing)No (shell string processing)
Unit testingYes (JUnit + Spock)Difficult (requires bats or Python tests)
Complex merge rulesEasy (Groovy list operations)Requires manual handling of nulls, special characters, multiline values
Debuggingprintln, Groovy consoleecho + GITHUB_STEP_SUMMARY

Scenarios Where the Shell Implementation Is Fragile:

1
2
3
# When pylint sourceSets contains paths with spaces, the result of tr '\n' ' ' may be word-split by the shell
SOURCES=$(yq '.check.pylint.sourceSets[]' config.yaml | tr '\n' ' ')
# If a path is "src/my module", word splitting turns it into two arguments

Edge Case Amplification Effect at 500+ Repositories: Across 500 repositories, any edge case in config.yaml formatting will definitely occur — some team used a path with spaces, another used a multiline YAML string, another’s field value contained shell special characters. Groovy’s type system provides better guarantees given the input diversity of 500 repositories, while the shell + yq approach requires more thorough test coverage.

Summary Comparison Table

DimensionJenkins Shared LibraryGitHub Actions Reusable Workflow
Pipeline structureDynamic (determined at runtime)Static (fixed at parse time)
Credential securityAppRole (static SecretID, shared by 500 repos)JWT/OIDC (no static credentials)
Infrastructure burdenHigh (primary node + K8s cluster)Low (runner only)
Developer experienceSeparate platform, context switchingIntegrated with GitHub PR
Config mergingType-safe, unit-testableshell + yq, more fragile
Dynamic custom stagesNatively supportedNot supported (can only skip)
Ecosystem integrationHundreds of mature pluginsMarketplace growing rapidly
OOM risk at 500+ scaleHigh (Jenkins primary node JVM)None (GitHub-hosted compute)

3. The Real Drivers for Migration

Driver 1: Consolidating Code Hosting Platforms (Operational Simplification)

Maintaining two systems (Jenkins + GitHub Enterprise) means:

  • Two permission management systems (new team members need to request access to both platforms)
  • Two audit log streams (security audits require correlating records from two systems)
  • Two incident response processes (Jenkins failures and GitHub failures follow different response workflows)
  • Two monitoring configurations

At 500+ repository scale, this operational overhead is further amplified: permission synchronization across two systems (departing employees must be removed from both), dual CI status monitoring (when a repository’s CI breaks, you must determine whether it’s a Jenkins problem or a GitHub problem).

If all code has already migrated to GitHub Enterprise, the marginal value of maintaining a separate Jenkins instance continues to decline.

Driver 2: Credential Security Compliance Requirements

Certain compliance frameworks (SOC 2 Type II, ISO 27001) have explicit requirements for static credentials:

  • Must be rotated regularly (typically every 90 days)
  • Must have usage audit records
  • Must have scoped access

AppRole Compliance Challenges at 500+ Repository Scale:

Meeting these requirements for AppRole’s SecretID requires additional operational processes (periodic rotation scripts, rotation audits). In a high-frequency CI environment with 500+ repositories, a 90-day rotation means each rotation requires:

  1. Selecting a low-traffic window (to minimize impact on running Pipelines)
  2. Updating the Jenkins Credential Store
  3. Verifying the new SecretID is effective for all Org Vault roles
  4. Monitoring for authentication failures in the following 24 hours

The JWT/OIDC answer is “there are no long-lived credentials that can be leaked” — a much cleaner answer in compliance audit scenarios. For a 500+ repository platform, this answer can reduce approximately 1-2 days of compliance operational work per quarter.

Driver 3: Jenkins Infrastructure Reaching End of Life

When the following signals appear, the migration ROI begins to look favorable:

  • Jenkins primary node OOM frequency increases from once per year to once per month
  • Kubernetes Plugin upgrades require manual rollback after every Jenkins version update
  • The engineer responsible for maintaining the Jenkins instance leaves, making knowledge transfer difficult
  • The Jenkins LTS version is approaching EOL (End of Life)

Specific Signals at 500+ Repository Scale:

  • Vault rate limiting at morning peak hours has become routine (manual cleanup or waiting is required every morning)
  • The Jenkins primary node requires ongoing JVM tuning (heap size has exceeded 16 GB but OOM continues)
  • The cost of troubleshooting pod scheduling failures exceeds the investment in new feature development

This is not a fundamental problem with Jenkins, but a lifecycle issue that any stateful infrastructure encounters when used at hyper-scale.

Driver 4: Developer Productivity

Methods to quantify this driver:

  • Count the time engineers spent diagnosing CI failures over the past 3 months (via Jira tickets or Slack records)
  • Count the time the platform team spent maintaining Jenkins infrastructure
  • Compare against migration costs

Concrete Estimate at 500+ Repository Scale:

1
2
3
4
5
6
7
8
Assumptions:
500 repositories, average 5 CI runs per repository per day = 2,500 runs/day
CI failure rate 10% = 250 failures/day
Jenkins diagnosis time: 10 minutes/failure
GitHub Actions diagnosis time: 4 minutes/failure
Time saved: 6 minutes/failure × 250 failures/day = 25 hours/day

Annualized savings (250 working days) = 6,250 hours/year

Even if the actual numbers are only 1/10 of the estimate, saving 625 hours per year is sufficient to justify the migration investment.


4. Scenarios Where Migration Is Not Worth It (Important)

Scenario 1: Dynamic Stage Generation Is a Core Requirement and Covers Many Repositories

If your platform allows business teams to declare arbitrary custom stages in config.yaml, and this capability is widely used by more than 10% of repositories, GitHub Actions cannot natively replicate this functionality.

A concrete example: a business repository declares 10 database migration stages, each corresponding to a target environment, with stage names and counts generated at runtime from a configuration file. Jenkins’ StageGenerator handles this in one line of code; GitHub Actions requires complex matrix configuration, with a noticeable gap in UI presentation.

Assessment Method at 500+ Repositories:

1
2
3
4
5
6
7
8
9
# Check how many repos use stages starting with custom- (i.e., dynamic stages)
gh repo list OrgA --limit 1000 --json name \
| jq -r '.[].name' \
| while read repo; do
if gh api "repos/OrgA/${repo}/contents/.ci-config/config.yaml" --silent 2>/dev/null \
| jq -r '.content' | base64 -d | grep -q 'custom-'; then
echo "${repo}: uses custom stages"
fi
done | wc -l

If the output exceeds 50 repositories, the effort to migrate dynamic stages may exceed the combined effort of all other migration work.

Scenario 2: Pipeline Configuration Merging Logic Is Extremely Complex

If your ConfigMerger has accumulated a large amount of Groovy logic handling special cases (recursive merging, reference resolution, conditional overrides), translating this logic into shell + yq is not just a large amount of work — it also reduces maintainability and testability.

Groovy’s type system and Jenkins’ testing toolchain (JenkinsPipelineUnit) have irreplaceable value in this scenario. Given the input diversity of 500+ repositories, shell-based config parsing without thorough test coverage may cause a large number of intermittent issues.

Scenario 3: Deep Dependency on Jenkins-Specific Plugins

The following plugins currently have no mature GitHub Actions equivalents:

  • Build Failure Analyzer: Automatically analyzes and classifies failure causes
  • Performance Plugin: CI performance trend analysis
  • Warnings Next Generation: Multi-tool warning aggregation and trend analysis
  • Configuration as Code (JCasC): Versioned management of Jenkins configuration

If the functionality provided by these plugins is central to your workflow, migration will result in significant feature gaps.

Scenario 4: Jenkins Is Running Stably, the Team Is Familiar With It, and There Are No Obvious Pain Points

This is the most important scenario — if the status quo is working well, do not migrate.

Specific questions to assess “whether there are pain points” (at 500+ repository scale):

  1. In the past 6 months, how many times did Jenkins infrastructure failures cause full CI outages (affecting all 500 repositories)?
  2. In the past year, how many times did Vault rate limiting cause batch Pipeline failures?
  3. Are engineers complaining about the difficulty of diagnosing CI failures and having to switch platforms?
  4. Is credential rotation being completed on time, or repeatedly postponed?
  5. Is the Jenkins primary node JVM heap running at full capacity continuously?

If the answers to all five questions are “never” or “rarely,” the ROI of migration is likely negative.

ROI Calculation Framework (500+ Repository Version)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Migration cost (one-time):
- Platform engineers rewriting pipeline code: 8-12 person-weeks
- Migrating 500 repositories:
* Repos without custom stages (~90%): 30 minutes each = 225 person-hours
* Repos with custom stages (~10%): 4 hours each = 200 person-hours
* Total: ~425 person-hours (~53 person-days)
- Parallel validation period: 4-8 weeks (dual-track running cost)
- Training (engineers across 500 repos): 1 hour/person × N people

Ongoing benefits (annualized):
- Fewer Jenkins maintenance incidents: 6-10/year × 4 hours each = 24-40 hours/year
- Reduced CI failure diagnosis time: see Section 3 Driver 4 estimate
- Reduced Vault rate limiting impact: quarterly rotation × 2 days/rotation = 8 days/year
- Elimination of Jenkins primary node OOM risk

Migration ROI = Ongoing benefits / Migration cost (in years)

5. If You Decide to Migrate, How to Reduce Risk (500+ Repository Strategy)

Strategy 1: Wave-Based Migration, Not Big Bang

At 500+ repository scale, “migrating all repositories at once” is not an executable plan. A wave-based strategy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Phase 0 (1-2 months): Build the GitHub Actions platform framework
- Implement platform-ci-core.yml and all sub-workflows
- Ensure full interface compatibility with .ci-config/config.yaml
- Validate against 5-10 internal test repositories

Wave 1 (2-3 months): New repos + low-risk existing repos (~100 repos)
- All newly created repositories default to GitHub Actions
- Migrate existing repos with low activity and low business impact
- Goal: accumulate 6+ months of runtime data, discover and fix edge cases

Wave 2 (3-6 months): Medium-importance repos (~250 repos)
- Standard repos without custom stages
- Run in parallel for 2 weeks, then cut off Jenkins

Wave 3 (6-12 months): Core repos + complex repos (~150 repos)
- Repos with custom stages (require individual evaluation)
- High-visibility core business repos (require the most thorough validation)
- Some repos may permanently remain on Jenkins (see coexistence approach)

Strategy 2: Parallel Run Validation

Before the formal cutover, run Jenkins and GitHub Actions simultaneously and compare results for each build:

1
2
3
4
5
6
7
8
# Temporary configuration in a business repo's ci.yml
jobs:
# New pipeline (GitHub Actions)
platform-gha:
uses: OrgA/.github/.github/workflows/platform-ci-core.yml@main

# Old pipeline still running in Jenkins (triggered via Jenkins GitHub Plugin)
# Both results appear side-by-side in the PR's Checks tab

Only cut off Jenkins after GitHub Actions results have been consistently equal to Jenkins results for at least 2 weeks and 10+ CI runs.

Note for 500+ Repositories: Do not start parallel runs across all 500 repositories simultaneously — double the CI load places additional strain on runner capacity and Vault. Start parallel runs wave by wave, and cut off Jenkins connections after each wave completes, freeing up resources.

Strategy 3: Keep the .ci-config/config.yaml Interface Unchanged (Critical)

This is the most important decision for reducing migration costs for business teams. If the structure of .ci-config/config.yaml remains compatible, the migration work for business teams is only:

  1. Create a new .github/workflows/ci.yml (15 lines)
  2. Delete the old Jenkinsfile
  3. .ci-config/config.yaml remains completely untouched

Business team perception: CI results appear in a different location, the pipeline is faster, the Jenkinsfile is gone. Everything else is unchanged.

Across 500 repositories, this invisible migration means that each repository’s migration does not require a dedicated coordination meeting — once the platform team is ready, business teams complete the two-step operation at their own pace.

Strategy 4: Control Rollout via Org Variables

Use GitHub Enterprise’s Org Variable mechanism to achieve routing control without modifying business repository code:

1
2
3
4
# Different Orgs represent different migration stages
# OrgA-Dev: New repos connect to GitHub Actions first (Dev environment)
# OrgA-Stg: Existing repos migrate gradually (Staging environment)
# OrgA: Final cutover of core prod repos (Production environment)

Org Variables (VAULT_ENV, VAULT_URL, etc.) are configured with different values in different Orgs. Business repositories’ ci.yml code is identical, and platform behavior routes automatically based on the Org context. This is the infrastructure guarantee for phased migration of 500+ repositories — you can maintain a single codebase on the platform while different Orgs are at different migration stages.

Strategy 5: Batch Check Migration Progress

During the migration of 500 repositories, tooling is needed to monitor migration status:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/bash
# Check migration status for each repo: Jenkins only / GitHub Actions only / Both (parallel) / Neither
ORG="OrgA"
echo "=== Migration Status Report ==="

gh repo list "${ORG}" --limit 1000 --json name \
| jq -r '.[].name' \
| while read repo; do
HAS_JENKINSFILE=$(gh api "repos/${ORG}/${repo}/contents/Jenkinsfile" --silent 2>/dev/null && echo "yes" || echo "no")
HAS_GHA=$(gh api "repos/${ORG}/${repo}/contents/.github/workflows/ci.yml" --silent 2>/dev/null && echo "yes" || echo "no")
HAS_CONFIG=$(gh api "repos/${ORG}/${repo}/contents/.ci-config/config.yaml" --silent 2>/dev/null && echo "yes" || echo "no")

if [ "${HAS_JENKINSFILE}" = "yes" ] && [ "${HAS_GHA}" = "no" ]; then
echo "[Jenkins only] ${repo}"
elif [ "${HAS_JENKINSFILE}" = "no" ] && [ "${HAS_GHA}" = "yes" ]; then
echo "[GHA only] ${repo}"
elif [ "${HAS_JENKINSFILE}" = "yes" ] && [ "${HAS_GHA}" = "yes" ]; then
echo "[Parallel run] ${repo}"
elif [ "${HAS_CONFIG}" = "yes" ]; then
echo "[No CI!] ${repo}"
fi
done | sort | tee migration_status.txt

echo ""
echo "Summary:"
grep -c "\[Jenkins only\]" migration_status.txt | xargs echo " Jenkins only:"
grep -c "\[GHA only\]" migration_status.txt | xargs echo " GitHub Actions only:"
grep -c "\[Parallel run\]" migration_status.txt | xargs echo " Parallel run:"
grep -c "\[No CI!\]" migration_status.txt | xargs echo " No CI (needs onboarding):"

This script serves as the standard input for the platform team’s weekly meetings throughout the migration of 500 repositories.

Strategy 6: Retain a Rollback Plan (30-Day Window)

For 30 days after the cutover, retain Jenkins Jobs but pause their triggers:

1
2
// Rename the old Jenkinsfile to .jenkinsfile.bak and keep it in the repository
// If a serious issue arises with GitHub Actions, temporarily restore the filename and manually trigger in Jenkins

Rollback Strategy for 500+ Repositories: Do not delete Jenkins configuration across all 500 repositories at once. Delete in batches, with a 30-day observation period for each batch. If a platform-level issue arises (e.g., insufficient runner capacity causing severe queuing), roll back the already-migrated repositories in that batch, scale up runners, then re-migrate.


6. Decision Tree (500+ Repository Version)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Q1: Does your repository count exceed 50?

├─ No → The benefits of unified management may be outweighed by costs; refer to small-scale recommendations

└─ Yes → Q2: Is Jenkins running stably (≤ 2 full CI outages in the past year)?

├─ Yes → Q3: Are there explicit credential security compliance requirements (SOC 2 / ISO 27001)?
│ │
│ ├─ No → Q4: Do engineers frequently need to switch platforms to diagnose CI failures,
│ │ and is the team size > 50?
│ │ │
│ │ ├─ No → Continue using Jenkins, reassess annually
│ │ └─ Yes → Calculate annualized time savings (see Section 3 Driver 4)
│ │ If > 500 hours/year, evaluate migration ROI
│ │
│ └─ Yes → Q5: Do more than 10% of repos depend on dynamic stage generation?
│ │
│ ├─ Yes → Maintain Jenkins, strengthen AppRole rotation processes
│ │ (dynamic stage migration cost is too high)
│ └─ No → Recommend migration; start with Dev/Stg environment Orgs first

└─ No (Jenkins unstable) → Q6: Is the instability caused by scale-related issues?
│ (OOM, Vault rate limiting, plugin compatibility)

├─ No (random failures) → Resolve the root cause first, then evaluate migration

└─ Yes (scale-related) → Q7: Do more than 10% of repos depend on dynamic stages?

├─ Yes → Migrate some repos (those without custom stages),
│ keep complex repos on Jenkins
└─ No → Strongly recommend migration
Jenkins maintenance cost now exceeds migration cost

7. Can Both Approaches Coexist? (The Practical Choice at 500+ Repositories)

Yes, and at 500+ repository scale, this is often a more realistic long-term strategy than “complete migration.”

Scenarios Where Permanent Coexistence Makes Sense

  • Dynamic stage repos stay on Jenkins permanently: Approximately 5-10% of repositories have so many custom stages that migration costs far exceed the benefits; accepting permanent dual-system operation is reasonable
  • New projects go straight to GitHub Actions; existing repos migrate at their own pace: No hard deadline — migrate naturally as part of the repository’s normal maintenance cycle
  • Route by CI complexity: Jenkins handles complex multi-environment deployment pipelines; GitHub Actions handles standard build + test

The Strategic Value of .ci-config/config.yaml

This interface file is the key to coexistence of both systems:

1
2
3
4
5
6
7
8
9
# The .ci-config/config.yaml structure is valid for both systems
containers:
- name: project-runtime
image: platform-registry.example.com/python:3.12
jobs:
- name: lint
steps:
- pyLint:
sourceSets: [src/]
  • Jenkins’ ConfigMerger.groovy reads it
  • The shell scripts in GitHub Actions’ config job read it

Business teams do not need to know which system is underneath — this is the value of interface design. When the platform team decides to switch the underlying CI engine for a given repository, the only thing business teams notice is “the CI results appear in a different location.”

Long-Term Strategy Recommendations (Updated Scale Thresholds)

ScaleRecommended Strategy
< 20 reposMaintain the status quo; do not introduce new complexity
20-100 reposNew repos use GitHub Actions; existing repos migrate as needed
100-500 reposDevelop a formal wave-based migration plan; allow a hybrid architecture transition period (1-2 years)
> 500 reposHybrid architecture may be a permanent state; use interface unification (.ci-config/config.yaml) instead of technical unification

At 500+ repository scale, achieving 100% migration is not the goal — the goal is to give business teams a unified CI onboarding experience while making the platform team’s maintenance work converge. Even if 10% of repositories remain on Jenkins permanently, as long as the interface is unified, the maintenance cost of that 10% is acceptable.

Observability: Dual-System Monitoring

While the hybrid architecture is in operation, the platform team needs a unified CI health view spanning both systems:

1
2
3
4
5
Questions that need to be answerable:
- How many Jenkins CI failures occurred today? How many GitHub Actions failures?
- Average duration comparison between the two systems?
- Which repos' Jenkins CI has not run for more than 30 days (candidates to accelerate migration)?
- Current number of repos in dual-track parallel run (parallel validation progress)?

This requires aggregating Jenkins build data and GitHub Actions workflow data into a single dashboard (e.g., Grafana), to avoid the platform team manually correlating information between two systems.


Conclusion

This four-part series started with “why,” moved through the technical implementations in Jenkins and GitHub Actions, and arrives today at a migration decision framework — attempting to answer one complete question: how to govern CI/CD pipelines in an engineering organization with 500+ repositories, and what tools to choose at different stages.

Core conclusions:

  1. The value of unified management is real — separating concerns allows platform teams and business teams to each focus on their own domain; this value is multiplied by a factor of 500 at 500+ repository scale
  2. Jenkins Shared Library is a mature and reliable solution, not “legacy technology”; at 500+ scale, its challenges are operational complexity, not missing functionality
  3. GitHub Actions’ JWT/OIDC + Reusable Workflow offers substantive advantages in credential security and developer experience, and these advantages are further amplified at 500+ scale
  4. Migration decisions should be based on ROI, not technical trendiness — migrating 500 repositories is an engineering project measured in years
  5. Interface design like .ci-config/config.yaml makes platform migration transparent to business teams — this is the key design decision that determines migration feasibility at 500+ repository scale
  6. Hybrid architecture may be the long-term reality at 500+ repository scale — using interface unification instead of technical unification is a more pragmatic goal

Whatever choice you make today, the most important principle is: let business teams only need to declare intent, without needing to understand the implementation. This principle applies equally to Jenkins and GitHub Actions, and holds at 5 repositories and 500 repositories alike.