getreadystack's picture
Upload index.html with huggingface_hub
d37d9b8 verified
Raw History Blame Contribute Delete
47.7 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Github actions Docker login — check your ci config free</title>
<meta name="description" content="This free browser tool finds Docker Hub pulls in CI and configuration files that run without login, including service containers pulled before the login step.">
<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/docker-hub-pull-limit-lint"></head>
<body>
<div class="wrap">
<h1>Github actions Docker login — check your ci config free</h1>
<p class="lede">Finds Docker Hub pulls in GitHub Actions, docker-compose, GitLab CI, Kubernetes and Dockerfiles that run without login — incl. service containers pulled before your docker/login-action step. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">name: ci
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-24.04
container: node:20-bookworm
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
redis:
image: redis:7-alpine
steps:
- uses: actions/checkout@v4
- run: npm ci &amp;&amp; npm test
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Lint Dockerfile
run: docker run --rm -i hadolint/hadolint:v2.12.0 &lt; Dockerfile
- name: Shellcheck
uses: docker://koalaman/shellcheck:v0.10.0
with:
args: scripts/deploy.sh
- name: Pull build base
run: docker pull python:3.12-slim</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 open workflow, compose, GitLab CI, Kubernetes or Dockerfile: every anonymous Docker Hub pull, with line and fix. <strong>In the editor:</strong> Scan the whole workspace in one run and export a pull inventory report (Markdown) to attach to the CI ticket. — that part asks for a licence key. <a href="https://getreadystack.com/api/buy/cl/polar_cl_xEh5IeTeNYGk8do3WVOUYZxBSCx2BseojEiBQ38O9Ky">Get the full version — $29</a> · $29 once · one licence key per person or team seat. Docker Pro costs $108 a year per user and only covers pulls that log in.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var DH_RULES = [
{"id": "gha-service-no-credentials", "sev": "error", "kind": "workflow",
"title": "Service container pulled from Docker Hub without credentials",
"msg": "Service containers are pulled in 'Initialize containers', before any step runs — a docker/login-action step cannot authenticate this pull, so it counts against the anonymous per-IP quota.",
"fix": "Add credentials: {username, password} to this service, or pull it from a mirror registry.",
"limits": {"unauthenticated": "100 pulls per 6 hours per IPv4 address or IPv6 /64 subnet", "personal": "200 pulls per 6 hours", "paid": "unlimited (Pro, Team, Business)", "source": "https://docs.docker.com/docker-hub/usage/pulls/", "checked": "2026-09-27", "multi_arch": "a multi-arch image counts one pull per architecture"}},
{"id": "gha-container-no-credentials", "sev": "error", "kind": "workflow",
"title": "Job container pulled from Docker Hub without credentials",
"msg": "The job container is pulled before the first step — a docker/login-action step cannot authenticate it, so it counts against the anonymous per-IP quota.",
"fix": "Use the long form container: {image, credentials: {username, password}}, or a mirror registry."},
{"id": "gha-docker-uri-action", "sev": "error", "kind": "workflow",
"title": "uses: docker:// image pulled from Docker Hub at job setup",
"msg": "docker:// action images are pulled while the job is set up, before your login step, so the pull is anonymous.",
"fix": "Replace with a run: docker run step placed after docker/login-action, or reference the image on a mirror registry."},
{"id": "cli-pull-before-login", "sev": "error", "kind": "any",
"title": "docker pull / run / create of a Docker Hub image with no Docker Hub login before it",
"msg": "No docker login (or docker/login-action for Docker Hub) runs earlier in this job, so this pull is anonymous.",
"fix": "Add docker/login-action (no registry: = Docker Hub) with a Docker Hub access token before this step."},
{"id": "login-registry-not-hub", "sev": "warn", "kind": "workflow",
"title": "Login goes to another registry while the job pulls from Docker Hub",
"msg": "This login sets registry: to a non-Docker-Hub host; Docker Hub pulls in the job stay anonymous.",
"fix": "Add a second docker/login-action step without registry: for Docker Hub."},
{"id": "self-hosted-shared-ip", "sev": "warn", "kind": "workflow",
"title": "Self-hosted runner: anonymous Docker Hub pulls share one NAT IP",
"msg": "Self-hosted runners behind one NAT or proxy share one public IP, so every anonymous Docker Hub pull from the fleet draws on one per-IP quota.",
"fix": "Log every runner in to Docker Hub or put a pull-through cache registry in front of the fleet."},
{"id": "dockerfile-from-hub", "sev": "warn", "kind": "dockerfile",
"title": "FROM pulls a Docker Hub base image",
"msg": "This base image resolves to docker.io; the build pulls it anonymously unless the build host is logged in to Docker Hub.",
"fix": "Make sure the build step logs in to Docker Hub, or point FROM at a mirror registry."},
{"id": "compose-image-hub", "sev": "warn", "kind": "compose",
"title": "Compose service image resolves to Docker Hub",
"msg": "docker compose pull / up fetches this from docker.io with the host's credentials — anonymous on a fresh CI runner.",
"fix": "Run docker login before compose in CI, or use a mirror registry for this image."},
{"id": "k8s-image-hub-no-pull-secret", "sev": "warn", "kind": "k8s",
"title": "Kubernetes image from Docker Hub with no imagePullSecrets",
"msg": "Nodes pull this from docker.io anonymously; every node that starts the pod counts against its own IP's quota.",
"fix": "Add imagePullSecrets with a Docker Hub token, or use a pull-through cache registry."},
{"id": "gitlab-image-hub-no-auth", "sev": "warn", "kind": "gitlab",
"title": "GitLab CI image from Docker Hub without DOCKER_AUTH_CONFIG or Dependency Proxy",
"msg": "The runner pulls this image before the script runs; with no DOCKER_AUTH_CONFIG and no Dependency Proxy prefix the pull is anonymous.",
"fix": "Prefix with ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/ or set DOCKER_AUTH_CONFIG for the runner."}
]
;</script>
<script>(function () {
const RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.DH_RULES;
const BY = {};
RULES.forEach(r => { BY[r.id] = r; });
const HUB_HOSTS = ['docker.io', 'index.docker.io', 'registry-1.docker.io', 'registry.hub.docker.com'];
const VALUE_OPTS = ['-v', '--volume', '-e', '--env', '-w', '--workdir', '--name', '-p', '--publish', '--network', '--net',
'--entrypoint', '-u', '--user', '--mount', '--env-file', '--platform', '-l', '--label', '-h', '--hostname', '--add-host', '--pull'];
function clean(v) {
return String(v || '').replace(/\s+#.*$/, '').trim().replace(/^['"]|['"]$/g, '').trim();
}
// returns the image reference when it resolves to Docker Hub, else null
function hubImage(ref) {
ref = clean(ref).replace(/^docker:\/\//, '');
if (!ref || /\$/.test(ref) || ref === 'scratch') return null;
const parts = ref.split('/');
const first = parts[0].toLowerCase();
if (parts.length > 1 && (first.includes('.') || first.includes(':') || first === 'localhost')) {
return HUB_HOSTS.includes(first) ? ref : null;
}
return ref;
}
function indent(s) { return s.match(/^\s*/)[0].length; }
function blank(s) { return !s.trim() || /^\s*#/.test(s); }
// line index of the parent mapping key of line i (-1 if none)
function parentOf(lines, i) {
const n = indent(lines[i].replace(/^(\s*)- /, '$1 '));
for (let j = i - 1; j >= 0; j--) {
if (blank(lines[j])) continue;
if (indent(lines[j]) < n) return j;
}
return -1;
}
function keyOf(line) { const m = (line || '').match(/^\s*-?\s*([\w.-]+):/); return m ? m[1] : ''; }
// does the mapping that contains line i have sibling key `key`?
function siblingHas(lines, i, key) {
const n = indent(lines[i]);
const re = new RegExp('^\\s{' + n + '}' + key + ':');
for (let j = i - 1; j >= 0; j--) {
if (blank(lines[j])) continue;
if (indent(lines[j]) < n) break;
if (re.test(lines[j])) return true;
}
for (let j = i + 1; j < lines.length; j++) {
if (blank(lines[j])) continue;
if (indent(lines[j]) < n) break;
if (re.test(lines[j])) return true;
}
return false;
}
function cliImage(cmd) {
const m = cmd.match(/\bdocker\s+(?:container\s+|image\s+)?(pull|run|create)\s+(.*)$/);
if (!m) return null;
const toks = m[2].split(/\s+/).filter(Boolean);
for (let k = 0; k < toks.length; k++) {
const t = toks[k];
if (/^[<>|&;]/.test(t)) return null;
if (t.startsWith('-')) { if (VALUE_OPTS.includes(t)) k++; continue; }
return t;
}
return null;
}
function kindOf(text) {
if (/^jobs:\s*$/m.test(text) && /runs-on:/.test(text)) return 'workflow';
if (/^\s*apiVersion:/m.test(text) && /^\s*kind:\s*\w+/m.test(text)) return 'k8s';
if (/^services:\s*$/m.test(text)) return 'compose';
if (/^\s+script:/m.test(text) || /^stages:/m.test(text)) return 'gitlab';
if (/^\s*FROM\s+\S+/im.test(text)) return 'dockerfile';
return 'unknown';
}
function hit(out, id, line, image, extra) {
const r = BY[id];
out.push({ check: id, sev: r.sev, msg: r.title + ' — ' + image + '. ' + r.msg + (extra || ''), fix: r.fix, line: line + 1, image });
}
function check(text, opts) {
opts = opts || {};
const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
const kind = opts.kind || kindOf(text);
const out = [];
if (kind === 'workflow') {
let loggedIn = false, jobIndent = -1, nonHubLogin = null, selfHosted = -1;
lines.forEach((ln, i) => {
if (blank(ln)) return;
const n = indent(ln);
if (/^jobs:\s*$/.test(ln)) { jobIndent = -2; return; }
if (jobIndent === -2 && n > 0) jobIndent = n;
if (n === jobIndent && /^\s*[\w-]+:\s*$/.test(ln)) { loggedIn = false; return; }
if (/runs-on:.*self-hosted/.test(ln) && selfHosted < 0) selfHosted = i;
let m;
if ((m = ln.match(/uses:\s*['"]?docker\/login-action/))) {
let reg = '';
for (let j = i + 1; j < Math.min(lines.length, i + 10); j++) {
if (/^\s*- /.test(lines[j])) break;
const r = lines[j].match(/^\s*registry:\s*(.+)$/);
if (r) reg = clean(r[1]);
}
if (!reg || HUB_HOSTS.includes(reg.toLowerCase())) loggedIn = true; else if (!nonHubLogin) nonHubLogin = { i, reg };
return;
}
if ((m = ln.match(/\bdocker\s+login\b(.*)$/))) {
const host = (m[1].split(/\s+/).filter(t => t && !t.startsWith('-') && !/^\$|^['"]?\$/.test(t)).pop() || '');
if (!host || !host.includes('.') || HUB_HOSTS.includes(host.toLowerCase())) loggedIn = true;
return;
}
if ((m = ln.match(/^\s*-?\s*uses:\s*['"]?docker:\/\/(\S+)/))) {
const img = hubImage(m[1]); if (img) hit(out, 'gha-docker-uri-action', i, img); return;
}
if ((m = ln.match(/^\s*container:\s*(\S.*)$/)) && clean(m[1])) {
const img = hubImage(m[1]); if (img) hit(out, 'gha-container-no-credentials', i, img); return;
}
if ((m = ln.match(/^\s*image:\s*(\S.*)$/))) {
const img = hubImage(m[1]); if (!img) return;
if (siblingHas(lines, i, 'credentials')) return;
const p = parentOf(lines, i);
if (keyOf(lines[p]) === 'container') { hit(out, 'gha-container-no-credentials', i, img); return; }
if (keyOf(lines[parentOf(lines, p)]) === 'services') { hit(out, 'gha-service-no-credentials', i, img); return; }
return;
}
const ci = cliImage(ln);
if (ci && !loggedIn) { const img = hubImage(ci); if (img) hit(out, 'cli-pull-before-login', i, img); }
});
const hubPulls = out.length;
if (nonHubLogin && hubPulls) {
hit(out, 'login-registry-not-hub', nonHubLogin.i, nonHubLogin.reg);
}
if (selfHosted >= 0 && hubPulls) hit(out, 'self-hosted-shared-ip', selfHosted, hubPulls + ' anonymous pull line(s) in this file');
} else if (kind === 'dockerfile') {
const stages = new Set();
lines.forEach((ln, i) => {
const m = ln.match(/^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)(?:\s+AS\s+(\S+))?/i);
if (!m) return;
const ref = m[1];
if (!stages.has(ref.toLowerCase())) { const img = hubImage(ref); if (img) hit(out, 'dockerfile-from-hub', i, img); }
if (m[2]) stages.add(m[2].toLowerCase());
});
} else if (kind === 'compose' || kind === 'k8s' || kind === 'gitlab') {
const id = kind === 'compose' ? 'compose-image-hub' : kind === 'k8s' ? 'k8s-image-hub-no-pull-secret' : 'gitlab-image-hub-no-auth';
const covered = kind === 'k8s' ? /imagePullSecrets:/.test(text)
: kind === 'gitlab' ? /DOCKER_AUTH_CONFIG|CI_DEPENDENCY_PROXY/.test(text) : false;
let loggedIn = false;
lines.forEach((ln, i) => {
if (/\bdocker\s+login\b/.test(ln)) loggedIn = true;
let m = ln.match(/^\s*-?\s*(?:image|name):\s*(\S.*)$/);
if (m && (/image:/.test(ln) || keyOf(lines[parentOf(lines, i)]) === 'image')) {
const img = hubImage(m[1]);
if (img && !covered && !(kind === 'gitlab' && /^\s*-?\s*name:/.test(ln) && !img.includes(':') && !img.includes('/'))) hit(out, id, i, img);
return;
}
const ci = cliImage(ln);
if (ci && !loggedIn && !covered) { const img = hubImage(ci); if (img) hit(out, 'cli-pull-before-login', i, img); }
});
}
return { kind, findings: out, pulls: out.filter(f => f.check !== 'self-hosted-shared-ip' && f.check !== 'login-registry-not-hub').length };
}
const api = { engine: { check, hubImage, kindOf }, RULES, RULE_COUNT: RULES.length };
if (typeof module !== 'undefined') module.exports = api;
if (typeof window !== 'undefined') window.DHENGINE = 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 DHREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = DHREPORT;
else window.DHREPORT = DHREPORT;
</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.DHENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.DHREPORT.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/docker-hub-pull-limit-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'docker-hub-pull-limit-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/docker-hub-pull-limit-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'docker-hub-pull-limit-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://hub.docker.com/r/getreadystack/docker-hub-pull-limit-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'docker-hub-pull-limit-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/docker-hub-pull-limit-lint &lt;file&gt;
docker run --rm -v &quot;$PWD:/w&quot; getreadystack/docker-hub-pull-limit-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 10 rules Docker Hub Pull Limit 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:"docker-hub-pull-limit-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">Finds every anonymous Docker Hub pull in GitHub Actions, compose, GitLab CI, Kubernetes and Dockerfiles, including the service containers your login step never covers. Docker Hub limit: 100 </p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Docker Pro costs $9 per user per month billed yearly ($108 a year), and it only lifts the limit for pulls that log in; this lint costs $29 once.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_xEh5IeTeNYGk8do3WVOUYZxBSCx2BseojEiBQ38O9Ky" 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-27">Updated 2026-09-27</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-27","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/sku324640_result_card.jpg" alt="Docker Hub Pull Limit Lint" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Six anonymous Docker Hub pulls sat in one two-job GitHub Actions workflow we use as a sample (ci.yml), and for a CI engineer running Node tests against Postgres and Redis, three of them could not have been fixed by adding a login step.</p>
<p>Docker Hub&#x27;s usage page (docs.docker.com/docker-hub/usage/pulls, checked 2026-09-27) sets the anonymous limit at 100 pulls per 6 hours per IPv4 address or IPv6 /64 subnet. A signed-in Personal account gets 200 pulls per 6 hours; Pro, Team and Business are unlimited. When the quota runs out, the pull fails with toomanyrequests and the job fails with it.</p>
<p>The usual advice is &quot;add docker/login-action&quot;. That fixes part of the problem. Here is where the sample&#x27;s six pulls happen:</p>
<ol><li>container: node:20-bookworm. The job container is pulled before the first step.</li><li>services.postgres image: postgres:16. Service containers are pulled in the &quot;Initialize containers&quot; phase, before any step.</li><li>services.redis image: redis:7-alpine. Same phase, same problem.</li><li>docker run hadolint/hadolint:v2.12.0 in a run: step with no Docker Hub login earlier in the job.</li><li>uses: docker://koalaman/shellcheck:v0.10.0. docker:// action images are pulled while the job is set up.</li><li>docker pull python:3.12-slim in a run: step, again with no login before it.</li></ol>
<p>A login step placed first in the lint job fixes items 4 and 6. It cannot reach items 1, 2, 3 and 5, because GitHub has pulled those images before step one runs. The job container and each service need their own credentials: block (username and password, usually a Docker Hub access token from secrets). The docker:// action is better rewritten as a docker run step after the login, or pointed at a mirror registry.</p>
<p>Docker Hub Pull Limit Lint checks exactly this. It decides whether each image resolves to Docker Hub (no registry host, or docker.io), then checks the position of the pull against the login:</p>
<ul><li>services and job containers: is there a credentials: sibling?</li><li>docker:// actions: always pulled at setup, so always flagged when they resolve to Docker Hub</li><li>docker pull / run / create in run: steps: did a Docker Hub login run earlier in the same job?</li><li>docker/login-action with registry: set to another host while the job still pulls from Docker Hub</li><li>runs-on: self-hosted: every runner behind one NAT shares one per-IP quota</li></ul>
<p>Beyond GitHub Actions it covers docker-compose image: lines, GitLab CI image: lines without DOCKER_AUTH_CONFIG or a Dependency Proxy prefix, Kubernetes images with no imagePullSecrets, and Dockerfile FROM lines. That is 10 rules in total.</p>
<p>On the clean version of the same sample (credentials on the container and postgres, redis moved to public.ecr.aws/docker/library, docker/login-action before the run steps) the lint reports 0 findings. On a docker-compose file with postgres:16, redis:7-alpine and a ghcr.io app image it reports 2; the ghcr.io image does not count against Docker Hub. On a Kubernetes Deployment running nginx:1.27 with no imagePullSecrets it reports 1.</p>
<p>Why not just pay Docker? Docker Pro is $9 per user per month billed yearly ($108 a year) and Team is $15. A paid plan removes the limit only for pulls that authenticate. The service container with no credentials: block still pulls anonymously, and still hits the per-IP quota shared with every other job on that runner&#x27;s address.</p>
<p>The free tier lints the open file in VS Code, or a pasted file on the web page, using all 10 rules, and shows each line and its fix. The licence adds a whole-workspace scan in one run and a Markdown pull inventory to attach to the CI ticket.</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/sku324640_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku324640_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 Docker Hub Pull Limit Lint check?</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, docker-compose file, GitLab CI file, Kubernetes manifest or Dockerfile and flags every image that resolves to Docker Hub and is pulled without a login: service containers, job containers, docker:// actions, docker pull/run steps and FROM lines. Each finding gives the line, the image and the fix.</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 Docker Hub rate limit lint for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It is for DevOps and CI engineers, platform teams and open-source maintainers who run GitHub Actions, GitLab CI or self-hosted runners and have seen 429 toomanyrequests errors from Docker Hub. It suits teams whose runners share one NAT IP, because every anonymous pull from the fleet draws on the same per-IP quota.</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 doesn&#x27;t adding docker/login-action fix Docker Hub rate limits?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">GitHub Actions pulls service containers, the job container and docker:// action images during job setup, before any step runs, so a docker/login-action step cannot authenticate those pulls. They need credentials on the service or container itself. Generic YAML linters and chatbots do not flag this, which is why the lint checks each pull&#x27;s position against the login.</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 does the licence add?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free: lint the open file in VS Code or paste it into the web page, and see every anonymous Docker Hub pull with its line and fix, using all 10 rules. The $29 licence adds a whole-workspace scan in one run and a Markdown pull-inventory report you can attach to the CI ticket. It is one payment with one licence key per person or team seat.</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 fixing Docker Hub pull limits cost otherwise?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Docker Pro is $9 per user per month billed yearly, or $108 a year, and Team is $15 per user per month billed yearly. A paid plan lifts the limit only for pulls that actually log in, so service containers without credentials stay anonymous. The lint is $29 once and shows which lines to change first.</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. Docker Hub Pull Limit Lint checks the file you open against 10 rules from a rule set dated 2026-09-27, 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 Docker Hub Pull Limit Lint check?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a GitHub Actions workflow, docker-compose file, GitLab CI file, Kubernetes manifest or Dockerfile and flags every image that resolves to Docker Hub and is pulled without a login: service containers, job containers, docker:// actions, docker pull/run steps and FROM lines. Each finding gives the line, the image and the fix."}}, {"@type": "Question", "name": "Who is this Docker Hub rate limit lint for?", "acceptedAnswer": {"@type": "Answer", "text": "It is for DevOps and CI engineers, platform teams and open-source maintainers who run GitHub Actions, GitLab CI or self-hosted runners and have seen 429 toomanyrequests errors from Docker Hub. It suits teams whose runners share one NAT IP, because every anonymous pull from the fleet draws on the same per-IP quota."}}, {"@type": "Question", "name": "Why doesn't adding docker/login-action fix Docker Hub rate limits?", "acceptedAnswer": {"@type": "Answer", "text": "GitHub Actions pulls service containers, the job container and docker:// action images during job setup, before any step runs, so a docker/login-action step cannot authenticate those pulls. They need credentials on the service or container itself. Generic YAML linters and chatbots do not flag this, which is why the lint checks each pull's position against the login."}}, {"@type": "Question", "name": "What is free and what does the licence add?", "acceptedAnswer": {"@type": "Answer", "text": "Free: lint the open file in VS Code or paste it into the web page, and see every anonymous Docker Hub pull with its line and fix, using all 10 rules. The $29 licence adds a whole-workspace scan in one run and a Markdown pull-inventory report you can attach to the CI ticket. It is one payment with one licence key per person or team seat."}}, {"@type": "Question", "name": "What does fixing Docker Hub pull limits cost otherwise?", "acceptedAnswer": {"@type": "Answer", "text": "Docker Pro is $9 per user per month billed yearly, or $108 a year, and Team is $15 per user per month billed yearly. A paid plan lifts the limit only for pulls that actually log in, so service containers without credentials stay anonymous. The lint is $29 once and shows which lines to change first."}}, {"@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. Docker Hub Pull Limit Lint checks the file you open against 10 rules from a rule set dated 2026-09-27, 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:324640,slug:"docker-hub-pull-limit-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/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/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/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/about-page-template"><span class="yj-rel-t">About Page Template — 6 Finished About Pages for Small Service Businesses (Sel</span><span class="yj-rel-p">$49</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/portfolio-website-template"><span class="yj-rel-t">Portfolio Website Template — Freelance Designer Portfolio &amp; Case Study Site (8</span><span class="yj-rel-p">$49</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>