getreadystack's picture
Upload index.html with huggingface_hub
b6c3c6c verified
Raw History Blame Contribute Delete
42.8 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GitLab GitHub Actions Migration — check your workflow for free</title>
<meta name="description" content="This free tool flags GitHub Actions workflow lines GitLab cannot run as-is or that require GitLab Premium.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/gitlab-ci-migration-check"><style data-yj-mfit="1">#out{max-width:100%}#out td,#out th{overflow-wrap:anywhere}#out pre,#out code{white-space:pre-wrap;overflow-wrap:anywhere}@media (max-width:640px){table{display:block;max-width:100%;overflow-x:auto}pre{max-width:100%;overflow-x:auto}code,a{overflow-wrap:anywhere}img,svg,video,canvas,iframe{max-width:100%}textarea,input,select{max-width:100%;box-sizing:border-box}.yj-lgbar{position:static!important;justify-content:flex-end;margin:0 0 10px}#out table:not(:has(tr>:nth-child(4))){display:table;table-layout:fixed;width:100%}#out table:has(tr>:nth-child(4)),#out table:has(tr>:nth-child(4)) tbody,#out table:has(tr>:nth-child(4)) tr,#out table:has(tr>:nth-child(4)) td{display:block;width:auto;overflow:visible}#out table:has(tr>:nth-child(4)) thead{display:none}#out table:has(tr>:nth-child(4)) tr{padding:10px 0;border-bottom:1px solid rgba(128,128,128,.28)}#out table:has(tr>:nth-child(4)) td{border:0;padding:1px 0}#out table:has(tr>:nth-child(4)) td:nth-child(-n+3){display:inline-block;margin-right:10px;font-size:12px;opacity:.78}}</style>
</head>
<body>
<div class="wrap">
<h1>GitLab GitHub Actions Migration — check your workflow for free</h1>
<p class="lede">GitLab CI/CD migration check: flags every line of a GitHub Actions workflow that GitLab cannot run as-is (Marketplace actions, pull_request_target, matrix exclude) or that needs GitLab Premium. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">name: release
on:
pull_request_target:
branches: [main]
schedule:
- cron: "0 3 * * 1"
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20, 22]
os: [linux, alpine]
exclude:
- node: 18
os: alpine
steps:
- uses: actions/checkout@v4
- run: npm ci &amp;&amp; npm test
ios:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- run: xcodebuild -scheme App -sdk iphonesimulator build
deploy:
needs: [test, ios]
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/deploy
aws-region: eu-west-1
- run: ./scripts/deploy.sh</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Checks one GitHub Actions workflow file and lists every line GitLab CI/CD cannot run as-is, with the GitLab keyword to use instead and a Premium-tier flag.. <strong>In the editor:</strong> Scans every workflow in the workspace in one pass and exports one migration report per repository, with the Premium-only lines totalled for the licence decision. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var GLM_RULES = [
{"id": "marketplace-action", "sev": "error", "tier": "no-equivalent", "re": "^\\s*-?\\s*uses:\\s*['\"]?(?!actions/|\\./|docker://)[\\w.-]+/[\\w./-]+@", "msg": "Third-party Marketplace action: GitLab runners have no runtime for `uses:` actions.", "fix": "Rewrite the step as `script:` lines, or use a GitLab CI/CD Catalog component via `include: - component:`."},
{"id": "first-party-cache-artifact", "sev": "warning", "tier": "free", "re": "^\\s*-?\\s*uses:\\s*['\"]?actions/(cache|upload-artifact|download-artifact)@", "msg": "actions/cache or actions/*-artifact step: GitLab has no step for this, it is a job keyword.", "fix": "Use the job keywords `cache:` (paths + key) and `artifacts:` (paths + expire_in); downstream jobs get artifacts through `needs:`."},
{"id": "setup-toolchain-action", "sev": "warning", "tier": "free", "re": "^\\s*-?\\s*uses:\\s*['\"]?actions/setup-(node|python|go|java|dotnet)@", "msg": "actions/setup-* toolchain step: GitLab jobs pick the toolchain through the container image.", "fix": "Set `image:` to the matching tag (for example `image: node:20` or `image: python:3.12`) and drop the step."},
{"id": "reusable-workflow", "sev": "warning", "tier": "free", "re": "^\\s*uses:\\s*['\"]?[\\w./-]*\\.github/workflows/[\\w.-]+\\.ya?ml", "msg": "Job calls a reusable workflow: GitLab has no `workflow_call` target.", "fix": "Move the shared jobs into a YAML file pulled in with `include:`, or run it as a child pipeline with `trigger: include:`."},
{"id": "schedule-trigger", "sev": "warning", "tier": "free", "re": "^\\s*schedule:\\s*$", "msg": "`on: schedule` cron: GitLab does not read schedules from .gitlab-ci.yml.", "fix": "Create a pipeline schedule under Build \u003e Pipeline schedules (or the Pipeline schedules API) and gate jobs with `rules: - if: $CI_PIPELINE_SOURCE == \"schedule\"`."},
{"id": "pull-request-target", "sev": "error", "tier": "no-equivalent", "re": "^\\s*-?\\s*pull_request_target\\s*:?", "msg": "`pull_request_target`: GitLab has no trigger that runs fork code with the target project's secrets.", "fix": "Fork merge request pipelines run in the fork by default; a maintainer can run one in the parent project from the merge request. Redesign the job around that."},
{"id": "workflow-run-trigger", "sev": "warning", "tier": "free", "re": "^\\s*-?\\s*workflow_run\\s*:?", "msg": "`workflow_run` trigger: GitLab pipelines cannot listen for another pipeline finishing.", "fix": "Chain the work inside one pipeline with `needs:`/stages, or start it from the upstream job with `trigger:` (multi-project or child pipeline)."},
{"id": "macos-runner", "sev": "error", "tier": "premium", "re": "^\\s*runs-on:.*macos", "msg": "macOS runner: GitLab-hosted macOS runners are offered on Premium and Ultimate only.", "fix": "Budget GitLab Premium for this project, or register your own Mac as a self-managed runner and target it with `tags:`."},
{"id": "self-hosted-runner", "sev": "warning", "tier": "free", "re": "^\\s*runs-on:.*self-hosted", "msg": "Self-hosted runner label: GitHub runner registrations do not carry over.", "fix": "Install GitLab Runner on the same host, register it to the project or group, and replace the labels with `tags:`."},
{"id": "environment-approval", "sev": "warning", "tier": "premium", "re": "^\\s*environment:", "msg": "Job deploys to a GitHub environment: if it has required reviewers, the GitLab equivalent (deployment approvals on a protected environment) is Premium and Ultimate only.", "fix": "Keep `environment: name:` (works on Free); add approvals only after the Premium decision, or replace the reviewer gate with a `when: manual` job."},
{"id": "permissions-block", "sev": "warning", "tier": "no-equivalent", "re": "^\\s*permissions:", "msg": "`permissions:` block: GitLab has no per-job token scope in the YAML.", "fix": "The job uses CI_JOB_TOKEN with its fixed scope; configure the job token allowlist in project settings, or use a project access token stored as a masked variable."},
{"id": "github-token", "sev": "warning", "tier": "free", "re": "secrets\\.GITHUB_TOKEN|github\\.token", "msg": "GITHUB_TOKEN reference: this token does not exist on GitLab.", "fix": "Use $CI_JOB_TOKEN for registry/API calls it supports, or a project access token saved as a masked CI/CD variable."},
{"id": "repo-secret", "sev": "info", "tier": "free", "re": "secrets\\.(?!GITHUB_TOKEN)[A-Za-z_][\\w]*", "msg": "Repository secret: GitHub secrets cannot be exported and must be re-created.", "fix": "Re-enter the value as a masked (and, for deploys, protected) CI/CD variable and reference it as $NAME."},
{"id": "matrix-exclude", "sev": "error", "tier": "no-equivalent", "re": "^\\s*exclude:\\s*$", "msg": "`strategy.matrix.exclude`: GitLab `parallel:matrix` has no exclude key.", "fix": "Write the combinations you want as separate entries in the `parallel:matrix` list (one hash per kept combination)."},
{"id": "hashfiles-key", "sev": "warning", "tier": "free", "re": "hashFiles\\(", "msg": "hashFiles() cache key: expressions are not evaluated by GitLab.", "fix": "Use `cache: key: files:` with at most two files (for example package-lock.json)."},
{"id": "job-outputs", "sev": "warning", "tier": "free", "re": "^\\s*outputs:\\s*$", "msg": "Job `outputs:` map: GitLab jobs pass values through a dotenv report.", "fix": "Write KEY=value lines to a file and publish it with `artifacts: reports: dotenv:`; later jobs read $KEY after `needs:`."},
{"id": "github-output-file", "sev": "warning", "tier": "free", "re": "\\$\\{?(GITHUB_OUTPUT|GITHUB_ENV|GITHUB_STEP_SUMMARY|GITHUB_PATH)\\b", "msg": "Writes to $GITHUB_OUTPUT/ENV/PATH/STEP_SUMMARY: these files are not set on GitLab.", "fix": "Export variables in the same `script:` or write a dotenv report; for a summary, publish an artifact."},
{"id": "github-context", "sev": "warning", "tier": "free", "re": "\\$\\{\\{\\s*github\\.(?!token)", "msg": "`${{ github.* }}` expression: GitLab does not evaluate GitHub contexts.", "fix": "Swap for predefined variables: github.sha -\u003e $CI_COMMIT_SHA, github.ref_name -\u003e $CI_COMMIT_REF_NAME, github.repository -\u003e $CI_PROJECT_PATH, github.actor -\u003e $GITLAB_USER_LOGIN."},
{"id": "concurrency-group", "sev": "warning", "tier": "free", "re": "^\\s*concurrency:", "msg": "`concurrency:` group: GitLab splits this into two features.", "fix": "Use `resource_group:` to serialise deploy jobs, and `interruptible: true` plus the auto-cancel setting to cancel superseded pipelines."},
{"id": "workflow-dispatch-inputs", "sev": "info", "tier": "free", "re": "^\\s*workflow_dispatch:\\s*$", "msg": "`workflow_dispatch` manual trigger: GitLab starts manual pipelines from Build \u003e Pipelines \u003e Run pipeline.", "fix": "Declare the inputs as variables with defaults (or `spec: inputs:`) and gate jobs with `rules: - if: $CI_PIPELINE_SOURCE == \"web\"`."}
]
;</script>
<script>/* GitLab CI/CD migration check — one engine for the extension and the web page. */
(function () {
var DOC = (typeof module !== 'undefined' && typeof require !== 'undefined') ? require('./rules.json') : window.GLM_RULES;
var RULES = Array.isArray(DOC) ? DOC : (DOC && DOC.rules) || [];
var COMPILED = RULES.map(function (r) { return { r: r, re: new RegExp(r.re, r.flags || '') }; });
var TIER = { 'premium': 'GitLab Premium', 'no-equivalent': 'no GitLab equivalent', 'free': 'GitLab Free' };
function stripComment(line) {
if (/^\s*#/.test(line)) return '';
return line.replace(/\s+#[^'"]*$/, '');
}
function check(text, opts) {
opts = opts || {};
var lines = String(text || '').split(/\r?\n/);
var findings = [];
var premium = 0;
lines.forEach(function (raw, i) {
var line = stripComment(raw);
if (!line.trim()) return;
COMPILED.forEach(function (c) {
if (!c.re.test(line)) return;
if (c.r.tier === 'premium') premium++;
findings.push({ check: c.r.id, sev: c.r.sev, tier: c.r.tier, line: i + 1,
msg: c.r.msg + ' [' + TIER[c.r.tier] + '] Fix: ' + c.r.fix, text: raw.trim() });
});
});
var m = String(opts.today || '').match(/(\d{4})-(\d{2})-(\d{2})/);
var summary = findings.length + ' findings, ' + premium + ' need GitLab Premium' + (m ? ' (checked ' + m[0] + ')' : '');
return { findings: findings, premium: premium, summary: summary };
}
var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
if (typeof module !== 'undefined' && module.exports) module.exports = API;
if (typeof window !== 'undefined') window.GLMENGINE = API;
})();
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var GLMREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = GLMREPORT;
else window.GLMREPORT = GLMREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.GLMENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.GLMREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://open-vsx.org/extension/readystack/gitlab-ci-migration-check" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'gitlab-ci-migration-check',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind GitLab CI/CD Migration Check for GitHub Actions every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"gitlab-ci-migration-check",lang:"en",source:"rule_alert"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);document.addEventListener("click",function(ev){var b=ev.target&&ev.target.closest?ev.target.closest("button,input[type=submit]"):null;if(b&&!b.closest("[data-yj],[data-yj-cta],[data-yj-install],[data-yj-alert],.yj-find,.yj-related,#yj-ask,.yjp"))mv(ev);},true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the full version <span class="yj-price">$29</span></h2><p class="yj-cta-paid" style="margin:.25rem 0 .6rem;color:#1f2a37">Scans every workflow in the workspace in one pass and exports one migration report per repository, with the Premium-only lines totalled for the licence decision.</p><p class="yj-cta-pay" style="margin:.25rem 0 1rem;color:#5b6b7c;font-size:.94rem">One payment, one licence key for this tool. The key is shown right after payment.</p><p class="yj-cta-anchor" style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">GitLab Premium lists at 29 US dollars per user per month, billed annually (3,480 dollars a year for 10 seats).</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_Ovf2Gas57uoPWUDKcDeNlqeGkEbUFLZ7bh5vi1osoR9" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-29">Updated 2026-09-29</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-29","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku390244_result_card.jpg" alt="GitLab CI/CD Migration Check for GitHub Actions" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Migrate from GitHub Actions to GitLab CI/CD: the 6 lines a conversion silently drops</p>
<p>GitLab CI/CD migration: 6 findings in one 35-line GitHub Actions release workflow, measured before a single job ran on GitLab, and 2 of them decide whether a DevOps team moving to GitLab can stay on the Free tier.</p>
<p>The workflow: a Node test matrix, an iOS build on a macOS runner, a deploy job to a production environment, a weekly cron, and a pull_request_target trigger.</p>
<h3>What GitLab does differently</h3>
<p>Most GitHub Actions steps translate one to one into .gitlab-ci.yml. The trouble is the handful of features with no counterpart, that live outside the YAML on GitLab, or that sit behind a paid tier. A chatbot conversion parses; it does not say which lines stopped doing anything.</p>
<h3>The 6 findings</h3>
<ol><li>pull_request_target (line 3, no GitLab equivalent). GitHub runs the base branch&#x27;s workflow with repository secrets for fork pull requests. GitLab has no trigger with that behaviour: fork merge request pipelines run in the fork, and a maintainer can choose to run one in the parent project.</li></ol>
<ol><li>on: schedule (line 5, GitLab Free). GitLab does not read cron lines from .gitlab-ci.yml. Schedules are created under Build &gt; Pipeline schedules or through the API, and jobs are gated with rules: - if: $CI_PIPELINE_SOURCE == &quot;schedule&quot;. A conversion that keeps the cron block produces a pipeline that simply never runs on Monday.</li></ol>
<ol><li>strategy.matrix.exclude (line 14, no GitLab equivalent). parallel:matrix accepts a list of variable hashes but has no exclude key. The fix is to write out only the combinations you keep.</li></ol>
<ol><li>runs-on: macos-14 (line 21, GitLab Premium). GitLab-hosted macOS runners are offered on Premium and Ultimate only. The alternative is registering your own Mac as a runner and targeting it with tags.</li></ol>
<ol><li>environment: production (line 28, GitLab Premium). Environments themselves work on Free. If the GitHub environment had required reviewers, the GitLab version of that gate, deployment approvals on a protected environment, is Premium and Ultimate only.</li></ol>
<ol><li>uses: aws-actions/configure-aws-credentials@v4 (line 31, no GitLab equivalent). GitLab runners do not execute Marketplace actions. The step becomes script lines, or a GitLab CI/CD Catalog component pulled in with include: - component:.</li></ol>
<h3>Why the tier tag matters</h3>
<p>GitLab Premium lists at 29 US dollars per user per month, billed annually. For a 10-seat team that is 3,480 dollars a year, and it is decided by lines like finding 4 and finding 5. Seen before cutover, the team can move the iOS build to its own Mac runner and swap the reviewer gate for a when: manual job.</p>
<h3>Two more workflows, two different answers</h3>
<p>A package publish workflow using actions/setup-node, actions/cache with a hashFiles() key and secrets.NPM_TOKEN gives 4 findings, all on GitLab Free: image: node:20, cache: key: files:, and a masked CI/CD variable. A deploy workflow with workflow_dispatch, concurrency, permissions, a reusable workflow, a self-hosted runner, ${{ github.sha }} and $GITHUB_OUTPUT gives 7 findings, one of them (permissions) with no GitLab equivalent.</p>
<h3>Try it on your own file</h3>
<p>The web version runs the same 20 rules in the browser and uploads nothing. Paste one workflow, read the findings, fix them. A licence key only adds whole-workspace scans and an exported migration report; checking a single file stays free.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku390244_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku390244_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does GitLab CI/CD Migration Check do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a GitHub Actions workflow file and lists every line GitLab CI/CD cannot run as-is: Marketplace actions, pull_request_target, matrix exclude, schedules, GITHUB_OUTPUT writes and 15 more patterns, 20 rules in all. Each finding names the GitLab keyword to use instead and tags the tier it needs: GitLab Free, GitLab Premium, or no GitLab equivalent.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this migration check for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">DevOps and platform engineers moving a company&#x27;s GitHub Actions workflows to GitLab CI/CD, and the engineering lead who must decide whether the team can stay on GitLab Free. It fits teams with release, iOS or deploy workflows, because macOS runners and deployment approvals are the lines that push a GitLab project onto Premium.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask a chatbot to convert the workflow?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A chatbot produces a .gitlab-ci.yml that parses, but it does not flag what silently disappears. On GitLab an on: schedule cron is not read from YAML, parallel:matrix has no exclude key, and pull_request_target has no counterpart. In our sample release workflow the check found 6 findings, 2 of them Premium-only, that a plain conversion would carry over unnoticed.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Checking one workflow file is free and complete: every finding, its line number, the GitLab fix and the tier tag, in the editor or on the web page. A licence key adds a whole-workspace scan of every workflow in one pass and exports one migration report per repository with the Premium-only lines totalled for the plan decision.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does getting the plan decision wrong cost?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">GitLab Premium lists at 29 US dollars per user per month, billed annually, so a 10-seat team pays 3,480 dollars a year. One macOS runner or approval-gated deploy left in a workflow decides that line. The check shows those lines before cutover, so the team can keep Premium features only where they are worth it.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does GitLab CI/CD Migration Check do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a GitHub Actions workflow file and lists every line GitLab CI/CD cannot run as-is: Marketplace actions, pull_request_target, matrix exclude, schedules, GITHUB_OUTPUT writes and 15 more patterns, 20 rules in all. Each finding names the GitLab keyword to use instead and tags the tier it needs: GitLab Free, GitLab Premium, or no GitLab equivalent."}}, {"@type": "Question", "name": "Who is this migration check for?", "acceptedAnswer": {"@type": "Answer", "text": "DevOps and platform engineers moving a company's GitHub Actions workflows to GitLab CI/CD, and the engineering lead who must decide whether the team can stay on GitLab Free. It fits teams with release, iOS or deploy workflows, because macOS runners and deployment approvals are the lines that push a GitLab project onto Premium."}}, {"@type": "Question", "name": "Why not just ask a chatbot to convert the workflow?", "acceptedAnswer": {"@type": "Answer", "text": "A chatbot produces a .gitlab-ci.yml that parses, but it does not flag what silently disappears. On GitLab an on: schedule cron is not read from YAML, parallel:matrix has no exclude key, and pull_request_target has no counterpart. In our sample release workflow the check found 6 findings, 2 of them Premium-only, that a plain conversion would carry over unnoticed."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Checking one workflow file is free and complete: every finding, its line number, the GitLab fix and the tier tag, in the editor or on the web page. A licence key adds a whole-workspace scan of every workflow in one pass and exports one migration report per repository with the Premium-only lines totalled for the plan decision."}}, {"@type": "Question", "name": "What does getting the plan decision wrong cost?", "acceptedAnswer": {"@type": "Answer", "text": "GitLab Premium lists at 29 US dollars per user per month, billed annually, so a 10-seat team pays 3,480 dollars a year. One macOS runner or approval-gated deploy left in a workflow decides that line. The check shows those lines before cutover, so the team can keep Premium features only where they are worth it."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:390244,slug:"gitlab-ci-migration-check",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/cron-schedule-lint"><span class="yj-rel-t">Cron Schedule Lint for crontab, Kubernetes CronJob, GitHub Actions, Spring and</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/actions-deprecation-lint-2026"><span class="yj-rel-t">GitHub Actions Deprecation Lint: the 2026 runner and action EOL dates</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/ssdf-attestation-workflow-audit"><span class="yj-rel-t">SSDF Attestation Audit for GitHub Actions</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/almalinux-migration-lint"><span class="yj-rel-t">AlmaLinux Migration Lint — CentOS 7/8</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/brave-search-api-migration-check"><span class="yj-rel-t">Brave Search API Migration Check — Google Custom Search &amp; Bing</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>