Download index.html from getreadystack/gitlab-ci-break-lint: direct link, hf CLI and curl.
- Browser
- Download file 45.6 kB
-
https://huggingface.co/spaces/getreadystack/gitlab-ci-break-lint/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/gitlab-ci-break-lint/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/gitlab-ci-break-lint/resolve/main/index.html
45.6 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>GitLab CI retired keywords — check your pipeline file online</title> | |
| <meta name="description" content="This free tool finds retired GitLab CI keywords and GitHub Actions syntax in a .gitlab-ci.yml file."> | |
| <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-break-lint"></head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>GitLab CI retired keywords — check your pipeline file online</h1> | |
| <p class="lede">Finds the .gitlab-ci.yml keywords GitLab retired, plus the GitHub Actions syntax an AI assistant pastes in, before the job fails on your runner. Runs entirely in your browser — nothing is uploaded.</p> | |
| <textarea id="src" spellcheck="false"># Pipeline drafted by an AI assistant, then edited by hand. | |
| image: node:20 | |
| types: | |
| - build | |
| - test | |
| - deploy | |
| variables: | |
| NODE_ENV: "test" | |
| build: | |
| type: build | |
| script: | |
| - echo "Building $CI_BUILD_REF" | |
| - npm ci | |
| - npm run build | |
| test: | |
| script: | |
| - npm test -- --coverage | |
| artifacts: | |
| reports: | |
| cobertura: coverage/cobertura-coverage.xml | |
| deploy: | |
| script: | |
| - echo "$CI_JOB_JWT" > /tmp/vault-token | |
| - ./scripts/deploy.sh | |
| only: | |
| - main</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> Lint the .gitlab-ci.yml you have open and name every retired keyword, invalid job wiring and GitHub Actions leftover, with the replacement line for each.. <strong>In the editor:</strong> Scan every pipeline file in the repository in one pass and export a dated Markdown upgrade report you can attach to the change ticket. — 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 GLCI_RULES = [ | |
| {"id": "removed_ci_build_vars", "sev": "error", "title": "CI_BUILD_* variable", "fix": "Rename to the CI_JOB_* / CI_COMMIT_* equivalent (CI_BUILD_REF is CI_COMMIT_SHA, CI_BUILD_ID is CI_JOB_ID)."}, | |
| {"id": "removed_type_keyword", "sev": "error", "title": "type: / types: keyword", "fix": "Use stage: on a job and stages: at the top of the file."}, | |
| {"id": "removed_cobertura_report", "sev": "error", "title": "artifacts:reports:cobertura", "fix": "Use artifacts:reports:coverage_report with coverage_format: cobertura and path:."}, | |
| {"id": "removed_ci_job_jwt", "sev": "error", "title": "CI_JOB_JWT token", "fix": "Declare an id_tokens: block on the job and read that token instead."}, | |
| {"id": "retired_only_except", "sev": "warn", "title": "only: / except: job filter", "fix": "Move the condition into rules: with if:, changes: or exists:."}, | |
| {"id": "only_and_rules_conflict", "sev": "error", "title": "rules: together with only:/except:", "fix": "Keep rules: and delete only:/except: from the same job."}, | |
| {"id": "github_runs_on", "sev": "error", "title": "runs-on: from GitHub Actions", "fix": "GitLab picks runners by tags:. Replace runs-on: with tags: or drop it."}, | |
| {"id": "github_uses_steps", "sev": "error", "title": "steps: / uses: from GitHub Actions", "fix": "GitLab jobs run shell lines under script:. There is no uses: action."}, | |
| {"id": "github_expression", "sev": "error", "title": "${{ }} expression from GitHub Actions", "fix": "GitLab expands plain $VARIABLE. Rewrite the expression as a variable or a rules: if:."}, | |
| {"id": "github_top_level_key", "sev": "error", "title": "top-level on: / jobs: from GitHub Actions", "fix": "GitLab has no on: or jobs: mapping. Declare jobs at the top level and gate them with workflow:rules or rules:."}, | |
| {"id": "stage_not_declared", "sev": "error", "title": "stage: missing from stages:", "fix": "Add the stage name to the stages: list, or point the job at a declared stage."}, | |
| {"id": "needs_undefined_job", "sev": "error", "title": "needs: points at an undefined job", "fix": "Reference a job that exists in this file, or remove the needs: entry."}, | |
| {"id": "extends_undefined_key", "sev": "error", "title": "extends: points at an undefined key", "fix": "Define the hidden job (.name:) in this file or include the template that defines it."}, | |
| {"id": "start_in_without_delayed", "sev": "error", "title": "start_in: without when: delayed", "fix": "Add when: delayed to the same job, otherwise start_in: is rejected."}, | |
| {"id": "parallel_out_of_range", "sev": "error", "title": "parallel: outside 2-200", "fix": "Use a value between 2 and 200, or delete parallel: for a single job."}, | |
| {"id": "job_without_script", "sev": "error", "title": "job with no script:", "fix": "Give the job a script:, a trigger:, or an extends: that supplies one."} | |
| ] | |
| ;</script> | |
| <script>/* GitLab CI Break Lint - engine. Same file runs in Node (extension) and in the browser (web page). */ | |
| (function () { | |
| 'use strict'; | |
| var RULES = (typeof module !== 'undefined' && module.exports) | |
| ? require('./rules.json') | |
| : window.GLCI_RULES; | |
| var BY_ID = {}; | |
| for (var r = 0; r < RULES.length; r++) BY_ID[RULES[r].id] = RULES[r]; | |
| var RESERVED = { | |
| stages: 1, types: 1, variables: 1, include: 1, default: 1, workflow: 1, | |
| image: 1, services: 1, before_script: 1, after_script: 1, script: 1, | |
| cache: 1, on: 1, jobs: 1, run: 1, name: 1 | |
| }; | |
| function strip(line) { | |
| var m = /^(\s*)([\s\S]*)$/.exec(line); | |
| var body = m[2].replace(/(^|\s)#.*$/, '$1'); | |
| return m[1] + body.replace(/\s+$/, ''); | |
| } | |
| function indentOf(line) { return /^(\s*)/.exec(line)[1].length; } | |
| function unquote(v) { | |
| return String(v).trim().replace(/^['"]|['"]$/g, '').trim(); | |
| } | |
| function parse(text) { | |
| var lines = String(text == null ? '' : text).split(/\r?\n/).map(strip); | |
| var tops = []; | |
| for (var i = 0; i < lines.length; i++) { | |
| var l = lines[i]; | |
| if (!l.trim() || indentOf(l) !== 0) continue; | |
| var m = /^([A-Za-z_.][A-Za-z0-9_.\-]*):(\s[\s\S]*)?$/.exec(l); | |
| if (m) tops.push({ name: m[1], inline: (m[2] || '').trim(), start: i }); | |
| } | |
| for (var j = 0; j < tops.length; j++) { | |
| tops[j].end = (j + 1 < tops.length) ? tops[j + 1].start : lines.length; | |
| } | |
| return { lines: lines, tops: tops }; | |
| } | |
| function listUnder(lines, start, end, baseIndent) { | |
| var out = []; | |
| for (var i = start; i < end; i++) { | |
| if (!lines[i].trim()) continue; | |
| if (indentOf(lines[i]) <= baseIndent) break; | |
| var m = /^\s*-\s*(.+)$/.exec(lines[i]); | |
| if (!m) break; | |
| out.push({ value: unquote(m[1]), line: i }); | |
| } | |
| return out; | |
| } | |
| function inlineList(raw) { | |
| var m = /^\[([\s\S]*)\]$/.exec(raw.trim()); | |
| if (!m) return null; | |
| return m[1].split(',').map(unquote).filter(function (s) { return s; }); | |
| } | |
| function check(text, opts) { | |
| opts = opts || {}; | |
| var p = parse(text); | |
| var lines = p.lines, tops = p.tops; | |
| var findings = []; | |
| var seen = {}; | |
| function add(id, line, extra) { | |
| var rule = BY_ID[id] || { sev: 'error', title: id, fix: '' }; | |
| var key = id + '@' + line; | |
| if (seen[key]) return; | |
| seen[key] = 1; | |
| findings.push({ | |
| check: id, | |
| sev: rule.sev, | |
| msg: rule.title + (extra ? ' (' + extra + ')' : '') + ' - ' + rule.fix, | |
| line: line + 1 | |
| }); | |
| } | |
| /* ---- stages declared at the top of the file ---- */ | |
| var stages = []; | |
| var definedKeys = {}; | |
| for (var t = 0; t < tops.length; t++) { | |
| definedKeys[tops[t].name] = 1; | |
| if (tops[t].name === 'stages') { | |
| var inl = inlineList(tops[t].inline); | |
| if (inl) { for (var q = 0; q < inl.length; q++) stages.push(inl[q]); } | |
| else { | |
| var items = listUnder(lines, tops[t].start + 1, tops[t].end, 0); | |
| for (var s = 0; s < items.length; s++) stages.push(items[s].value); | |
| } | |
| } | |
| if (tops[t].name === 'on' || tops[t].name === 'jobs') { | |
| add('github_top_level_key', tops[t].start, tops[t].name + ':'); | |
| } | |
| } | |
| /* ---- line scans ---- */ | |
| for (var i = 0; i < lines.length; i++) { | |
| var l = lines[i]; | |
| if (!l.trim()) continue; | |
| var mv = /\bCI_BUILD_[A-Z0-9_]+/.exec(l); | |
| if (mv) add('removed_ci_build_vars', i, mv[0]); | |
| var mj = /\bCI_JOB_JWT(_V[12])?\b/.exec(l); | |
| if (mj) add('removed_ci_job_jwt', i, mj[0]); | |
| if (/\$\{\{/.test(l)) add('github_expression', i, null); | |
| if (/^\s*runs-on\s*:/.test(l)) add('github_runs_on', i, null); | |
| if (/^\s*(steps\s*:|-\s*uses\s*:|uses\s*:)/.test(l)) add('github_uses_steps', i, null); | |
| if (/^\s*cobertura\s*:/.test(l)) add('removed_cobertura_report', i, null); | |
| if (indentOf(l) === 0 && /^types\s*:/.test(l)) add('removed_type_keyword', i, 'types:'); | |
| if (indentOf(l) > 0 && /^\s+type\s*:\s*\S/.test(l)) add('removed_type_keyword', i, 'type:'); | |
| if (indentOf(l) > 0 && /^\s+(only|except)\s*:/.test(l)) add('retired_only_except', i, null); | |
| } | |
| /* ---- per job ---- */ | |
| for (var k = 0; k < tops.length; k++) { | |
| var job = tops[k]; | |
| if (RESERVED[job.name] || job.inline) continue; | |
| var hidden = job.name.charAt(0) === '.'; | |
| var hasScript = false, hasOnly = false, hasRules = false; | |
| var startIn = -1, whenDelayed = false; | |
| for (var n = job.start + 1; n < job.end; n++) { | |
| var jl = lines[n]; | |
| if (!jl.trim()) continue; | |
| if (/^\s+(script|trigger|extends|run)\s*:/.test(jl)) hasScript = true; | |
| if (/^\s+(only|except)\s*:/.test(jl)) hasOnly = true; | |
| if (/^\s+rules\s*:/.test(jl)) hasRules = true; | |
| if (/^\s+start_in\s*:/.test(jl)) startIn = n; | |
| if (/^\s+when\s*:\s*delayed\b/.test(jl)) whenDelayed = true; | |
| var ms = /^\s+stage\s*:\s*(\S.*)$/.exec(jl); | |
| if (ms && stages.length) { | |
| var st = unquote(ms[1]); | |
| if (st !== '.pre' && st !== '.post' && stages.indexOf(st) < 0) { | |
| add('stage_not_declared', n, st); | |
| } | |
| } | |
| var mp = /^\s+parallel\s*:\s*(\d+)\s*$/.exec(jl); | |
| if (mp) { | |
| var num = parseInt(mp[1], 10); | |
| if (num < 2 || num > 200) add('parallel_out_of_range', n, String(num)); | |
| } | |
| var me = /^\s+extends\s*:\s*(\S.*)$/.exec(jl); | |
| if (me) { | |
| var ex = inlineList(me[1]) || [unquote(me[1])]; | |
| for (var e = 0; e < ex.length; e++) { | |
| if (ex[e] && !definedKeys[ex[e]]) add('extends_undefined_key', n, ex[e]); | |
| } | |
| } else if (/^\s+extends\s*:\s*$/.test(jl)) { | |
| var exl = listUnder(lines, n + 1, job.end, indentOf(jl)); | |
| for (var e2 = 0; e2 < exl.length; e2++) { | |
| if (!definedKeys[exl[e2].value]) add('extends_undefined_key', exl[e2].line, exl[e2].value); | |
| } | |
| } | |
| var mn = /^\s+needs\s*:\s*(\S.*)?$/.exec(jl); | |
| if (mn) { | |
| var needs = []; | |
| if (mn[1]) { | |
| var il = inlineList(mn[1]); | |
| if (il) { for (var y = 0; y < il.length; y++) needs.push({ value: il[y], line: n }); } | |
| } else { | |
| needs = listUnder(lines, n + 1, job.end, indentOf(jl)); | |
| } | |
| for (var d = 0; d < needs.length; d++) { | |
| var nm = needs[d].value.replace(/^job\s*:\s*/, ''); | |
| nm = unquote(nm.split(/\s*,\s*/)[0]); | |
| if (!nm || /[:{]/.test(nm)) continue; | |
| if (!definedKeys[nm]) add('needs_undefined_job', needs[d].line, nm); | |
| } | |
| } | |
| } | |
| if (hasOnly && hasRules) add('only_and_rules_conflict', job.start, job.name); | |
| if (startIn >= 0 && !whenDelayed) add('start_in_without_delayed', startIn, job.name); | |
| if (!hidden && !hasScript) add('job_without_script', job.start, job.name); | |
| } | |
| findings.sort(function (a, b) { return a.line - b.line; }); | |
| return { findings: findings, today: opts.today || '' }; | |
| } | |
| 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.GLCIENGINE = api; | |
| })(); | |
| </script> | |
| <script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다. | |
| ; | |
| function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } | |
| 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 GLCIREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = GLCIREPORT; | |
| else window.GLCIREPORT = GLCIREPORT; | |
| </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.GLCIENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.GLCIREPORT.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-break-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'gitlab-ci-break-lint',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><a href="https://www.npmjs.com/package/@readystack/gitlab-ci-break-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'gitlab-ci-break-lint',src:'npm',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">npm</a><a href="https://www.npmjs.com/package/@readystack/gitlab-ci-break-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'gitlab-ci-break-lint',src:'mcp',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">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/gitlab-ci-break-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'gitlab-ci-break-lint',src:'docker',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">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">npx @readystack/gitlab-ci-break-lint <file> | |
| npx @readystack/gitlab-ci-break-lint --mcp | |
| docker run --rm -v "$PWD:/w" getreadystack/gitlab-ci-break-lint /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" 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 the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 16 rules GitLab CI Break Lint checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. 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">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</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-break-lint",lang:"en",source:"rule_table"+((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);})();</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 complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Names the .gitlab-ci.yml lines your runner will reject after the next GitLab major upgrade</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A freelance DevOps engineer's hour runs about $100, and reading a repository's pipelines by hand against a removals list is most of a morning.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_d7bGnhvRyjV580fnsaQ89C7TIU15QgC3kcWpO0NZLGc" 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 {"&":"&","<":"<",">":">",'"':"""}[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-20">Updated 2026-09-20</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-20","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/sku139792_result_card.jpg" alt="GitLab CI Break Lint" loading="lazy" style="max-width:100%;border-radius:6px"></p> | |
| <p>Six retired keywords in a 31-line .gitlab-ci.yml — that is what a DevOps engineer on self-managed GitLab gets back from a pipeline file an AI assistant drafted for them. The file is not broken. It runs green today. It stops running the day the instance crosses a major upgrade.</p> | |
| <p>That gap is the whole problem. GitLab removes CI keywords only in major releases, so a retired keyword lives in your repository for months in perfect health. Your runner accepts it. GitLab's own CI Lint accepts it, because CI Lint validates the file against the version your instance is running right now — the version where the keyword still works. Nothing in the loop is looking at the version you are about to move to.</p> | |
| <p>Here is the bundled dirty fixture, 31 lines, and the six lines that will not survive:</p> | |
| <table><thead><tr><th>Line in the file</th><th>What replaces it</th></tr></thead><tbody><tr><td><code>types:</code></td><td><code>stages:</code></td></tr><tr><td><code>type: build</code></td><td><code>stage: build</code></td></tr><tr><td><code>$CI_BUILD_REF</code></td><td><code>$CI_COMMIT_SHA</code></td></tr><tr><td><code>reports: cobertura:</code></td><td><code>reports: coverage_report:</code></td></tr><tr><td><code>$CI_JOB_JWT</code></td><td><code>id_tokens: VAULT_ID_TOKEN:</code></td></tr><tr><td><code>only: - main</code></td><td><code>rules: - if: $CI_COMMIT_BRANCH</code></td></tr></tbody></table> | |
| <p>Six findings, six line numbers, six replacement lines. The same pipeline rewritten the way GitLab accepts it returns nothing.</p> | |
| <h3>Where these lines come from</h3> | |
| <p>Two places, and neither of them is carelessness.</p> | |
| <p>The first is age. A pipeline written years ago picked up <code>types:</code> and <code>CI_BUILD_REF</code> when those were the documented keywords. Nobody rewrites a working pipeline. It gets copied into the next repository instead, and the removal notice lands in a release post nobody reads, because nothing was failing.</p> | |
| <p>The second is newer. Ask an assistant for "a CI pipeline that builds and tests this repo" and you get whichever CI syntax it saw most, usually GitHub Actions. That is how <code>runs-on:</code>, <code>steps:</code>, <code>- uses:</code> and <code>${{ }}</code> expressions end up in a file called <code>.gitlab-ci.yml</code>. GitLab does not reject the one bad line — it rejects the whole file, so the pipeline never starts.</p> | |
| <h3>What the linter checks</h3> | |
| <p>Sixteen rules, in three groups.</p> | |
| <p><strong>Keywords GitLab retired.</strong> <code>types:</code> and <code>type:</code>, the whole <code>CI_BUILD_*</code> variable family, <code>artifacts:reports:cobertura</code>, <code>CI_JOB_JWT</code> and its <code>_V1</code> / <code>_V2</code> siblings, and <code>only:</code> / <code>except:</code>. Each reports with its line number and replacement: <code>artifacts:reports:cobertura</code> becomes <code>artifacts:reports:coverage_report</code> with <code>coverage_format: cobertura</code>.</p> | |
| <p><strong>GitHub Actions syntax in a GitLab file.</strong> <code>runs-on:</code>, <code>steps:</code>, <code>- uses:</code>, <code>${{ ... }}</code>, and a top-level <code>on:</code> or <code>jobs:</code> mapping.</p> | |
| <p><strong>Job wiring GitLab will not accept.</strong> <code>rules:</code> and <code>only:</code> in the same job, a <code>stage:</code> that is missing from <code>stages:</code>, a <code>needs:</code> or <code>extends:</code> that points at a job or key which does not exist, <code>start_in:</code> without <code>when: delayed</code>, <code>parallel:</code> outside the 2–200 range, and a job with no <code>script:</code>, <code>trigger:</code> or <code>extends:</code>.</p> | |
| <h3>Free, and what a key buys</h3> | |
| <p>Free, in the editor or in the browser, with no key and no account: lint the pipeline file you have open. All sixteen rules, every line number, every replacement line. When the report is empty, that file is done — the whole job for one file, not a sample.</p> | |
| <p>The licence key changes the scope, not the checks. It scans every pipeline file in the repository in one pass and exports a dated Markdown upgrade report — file, line, keyword, replacement — as a single table you attach to the change ticket, so the upgrade window has an artefact instead of a promise.</p> | |
| <p>A freelance DevOps engineer's hour runs about $100, and reading one repository's pipelines by hand against a removals list is most of a morning.</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/sku139792_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku139792_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 Break Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a .gitlab-ci.yml as text and reports the lines GitLab no longer accepts, or will stop accepting at the next major release. Each finding gives the line number, the retired keyword or invalid wiring, and the replacement line. On the bundled 31-line dirty fixture it returns six findings; the clean rewrite returns none.</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 for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">DevOps and platform engineers who own .gitlab-ci.yml files, especially on self-managed GitLab where the instance upgrade date is yours to schedule. It also helps anyone reviewing a pipeline an AI assistant drafted, because assistants mix GitHub Actions keys such as runs-on: and steps: into GitLab files.</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 is GitLab's own CI Lint not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">GitLab's CI Lint validates the file against the version your instance runs right now. That is exactly the version where the retired keywords still work, so it returns a pass. The breakage appears on the first push after the upgrade, when those keywords are gone and the job that used them fails.</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">Free: lint the pipeline file you have open, with all 16 rules and every replacement line. The open file is finished when the report is empty. Paid: scan every pipeline file in the repository in one pass and export a dated Markdown upgrade report to attach to the change ticket.</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 would this cost done by hand?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A freelance DevOps engineer's hour runs about $100. Reading a repository's pipeline files line by line against a keyword removals list, then writing the replacements into a ticket, is most of a morning for one repository, and it has to be redone at every major release.</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 ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. GitLab CI Break Lint checks the file you open against 16 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files.</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 Break Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a .gitlab-ci.yml as text and reports the lines GitLab no longer accepts, or will stop accepting at the next major release. Each finding gives the line number, the retired keyword or invalid wiring, and the replacement line. On the bundled 31-line dirty fixture it returns six findings; the clean rewrite returns none."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "DevOps and platform engineers who own .gitlab-ci.yml files, especially on self-managed GitLab where the instance upgrade date is yours to schedule. It also helps anyone reviewing a pipeline an AI assistant drafted, because assistants mix GitHub Actions keys such as runs-on: and steps: into GitLab files."}}, {"@type": "Question", "name": "Why is GitLab's own CI Lint not enough?", "acceptedAnswer": {"@type": "Answer", "text": "GitLab's CI Lint validates the file against the version your instance runs right now. That is exactly the version where the retired keywords still work, so it returns a pass. The breakage appears on the first push after the upgrade, when those keywords are gone and the job that used them fails."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Free: lint the pipeline file you have open, with all 16 rules and every replacement line. The open file is finished when the report is empty. Paid: scan every pipeline file in the repository in one pass and export a dated Markdown upgrade report to attach to the change ticket."}}, {"@type": "Question", "name": "What would this cost done by hand?", "acceptedAnswer": {"@type": "Answer", "text": "A freelance DevOps engineer's hour runs about $100. Reading a repository's pipeline files line by line against a keyword removals list, then writing the replacements into a ticket, is most of a morning for one repository, and it has to be redone at every major release."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. GitLab CI Break Lint checks the file you open against 16 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files."}}]}</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:139792,slug:"gitlab-ci-break-lint",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/webflow-restaurant-template"><span class="yj-rel-t">Webflow Restaurant Template — delivered as self-contained HTML you own: 26 pag</span><span class="yj-rel-p">$49</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/angular-removed-api-lint"><span class="yj-rel-t">Angular Removed-API Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bulk-sender-lint"><span class="yj-rel-t">Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, checked in your mai</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/jdk25-upgrade-blocker-lint"><span class="yj-rel-t">JDK 25 Upgrade Blocker Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/latex-submission-lint"><span class="yj-rel-t">LaTeX Submission Lint: Desk-Reject Check</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> | |