Upload index.html with huggingface_hub
Browse files- index.html +113 -18
index.html
CHANGED
|
@@ -1,19 +1,114 @@
|
|
| 1 |
-
<!doctype html>
|
| 2 |
-
<
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html><html lang="en"><meta charset="utf-8">
|
| 2 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 3 |
+
<title>Workflow Expiry - dated GitHub Actions breakage</title>
|
| 4 |
+
<script defer data-auto-init src="https://cdn.jsdelivr.net/npm/@polar-sh/checkout@latest/dist/embed.global.js"></script>
|
| 5 |
+
<meta name="description" content="Finds the lines in .github/workflows that are valid today and fail on a date GitHub has already published.">
|
| 6 |
+
<style>
|
| 7 |
+
:root{color-scheme:light dark}
|
| 8 |
+
body{margin:0;font:16px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif;
|
| 9 |
+
background:#fbfbfa;color:#16161d}
|
| 10 |
+
@media(prefers-color-scheme:dark){body{background:#14141a;color:#e8e8ea}}
|
| 11 |
+
.wrap{max-width:880px;margin:0 auto;padding:28px 20px 64px}
|
| 12 |
+
h1{font-size:1.7rem;line-height:1.25;margin:.2em 0 .3em}
|
| 13 |
+
p.sub{margin:0 0 1.4em;opacity:.8}
|
| 14 |
+
textarea{width:100%;min-height:230px;font:13px/1.5 ui-monospace,Menlo,Consolas,monospace;
|
| 15 |
+
padding:12px;border:1px solid #c9c9d1;border-radius:10px;background:transparent;color:inherit;
|
| 16 |
+
box-sizing:border-box}
|
| 17 |
+
button{font:600 15px/1 inherit;padding:12px 22px;border:0;border-radius:10px;
|
| 18 |
+
background:#1d4ed8;color:#fff;cursor:pointer;margin:12px 0}
|
| 19 |
+
button:hover{background:#1a43bd}
|
| 20 |
+
table{border-collapse:collapse;width:100%;margin-top:14px;font-size:14px}
|
| 21 |
+
th,td{border-bottom:1px solid #d8d8e0;padding:8px 6px;text-align:left;vertical-align:top}
|
| 22 |
+
th{font-weight:600;opacity:.75}
|
| 23 |
+
td.n{width:64px;font:13px ui-monospace,monospace;opacity:.7}
|
| 24 |
+
.ok{padding:14px;border-radius:10px;background:#e8f5ec;color:#14532d;margin-top:14px}
|
| 25 |
+
@media(prefers-color-scheme:dark){.ok{background:#15321f;color:#bbf7d0}}
|
| 26 |
+
.next{margin-top:30px;padding:18px;border:1px solid #d8d8e0;border-radius:12px}
|
| 27 |
+
.next h2{font-size:1.05rem;margin:0 0 .5em}
|
| 28 |
+
.next ul{margin:.3em 0 1em 1.1em;padding:0}
|
| 29 |
+
.offer{display:none;margin-top:18px;padding:18px 20px;border:2px solid #1d4ed8;border-radius:12px}
|
| 30 |
+
.offer h2{font-size:1.1rem;margin:0 0 .5em}
|
| 31 |
+
.offer p{margin:.4em 0}
|
| 32 |
+
a.buy{display:inline-block;font:600 15px/1 inherit;padding:12px 22px;border-radius:10px;background:#1d4ed8;color:#fff;text-decoration:none}
|
| 33 |
+
.terms{font-size:13px;opacity:.75}
|
| 34 |
+
</style>
|
| 35 |
+
<div class="wrap">
|
| 36 |
+
<h1>Workflow Expiry - dated GitHub Actions breakage</h1>
|
| 37 |
+
<p class="sub">Finds the lines in .github/workflows that are valid today and fail on a date GitHub has already published.</p>
|
| 38 |
+
<textarea id="src" placeholder="Paste a workflow YAML file here (.github/workflows/ci.yml)"></textarea>
|
| 39 |
+
<button id="go">Check for expiry dates</button>
|
| 40 |
+
<div id="out"></div>
|
| 41 |
+
<div class="offer" id="offer"><h2>Every workflow in the repo, in one pass</h2><p>This page checks the one file you pasted. The full version scans every workflow in the repository at once, exports the dated audit as CSV/JSON/HTML, applies the replacement line for you, and emits JSON a CI step can fail on — so an expiring line cannot merge.</p><p><a class="buy" href="https://buy.polar.sh/polar_cl_JaA2n3akb6IlUIK0HRJCMrQCqJKruNoyMyBg00yLQZj" data-polar-checkout data-polar-checkout-theme="light">Get the full version — $29</a></p><p class="terms">$29 once · one licence key per person or team seat · Upwork lists DevOps engineers at $40-$100/hour, median $60.</p></div>
|
| 42 |
+
<div class="next"><h2>In the full version</h2><ul><li>Every workflow in the repo at once</li><li>Export the dated audit as a file</li><li>Machine-readable output for a CI gate</li><li>Apply the replacement line</li><li><a href="https://buy.polar.sh/polar_cl_JaA2n3akb6IlUIK0HRJCMrQCqJKruNoyMyBg00yLQZj">Get the full version</a></li></ul></div>
|
| 43 |
+
</div>
|
| 44 |
+
<script>
|
| 45 |
+
var RULES=[{"pattern": "ubuntu-22\\.04-arm", "flags": "", "sev": "error", "message": "ubuntu-22.04-arm: deprecation starts 2026-09-17, fully unsupported 2027-04-17. Brownouts fail the job before then. -> runs-on: ubuntu-24.04-arm", "fix": "runs-on: ubuntu-24.04-arm"}, {"pattern": "ubuntu-22\\.04(?!-arm)", "flags": "", "sev": "error", "message": "ubuntu-22.04: deprecation starts 2026-09-17, fully unsupported 2027-04-17. Still a valid label today, so no linter flags it. -> runs-on: ubuntu-24.04", "fix": "runs-on: ubuntu-24.04"}, {"pattern": "ubuntu-(16|18|20)\\.04", "flags": "", "sev": "error", "message": "This Ubuntu image is already retired (20.04 went fully unsupported 2025-04-15). The job cannot start. -> runs-on: ubuntu-24.04", "fix": "runs-on: ubuntu-24.04"}, {"pattern": "windows-2019", "flags": "", "sev": "error", "message": "windows-2019 was retired on 2025-06-30. The job cannot start. -> runs-on: windows-2022", "fix": "runs-on: windows-2022"}, {"pattern": "windows-2016", "flags": "", "sev": "error", "message": "windows-2016 has been retired for years. The job cannot start. -> runs-on: windows-2022", "fix": "runs-on: windows-2022"}, {"pattern": "macos-(10|11|12)\\b", "flags": "", "sev": "error", "message": "macos-12 was retired on 2024-12-03; 10.x and 11 earlier still. The job cannot start. -> runs-on: macos-14", "fix": "runs-on: macos-14"}, {"pattern": "ubuntu-latest", "flags": "", "sev": "info", "message": "ubuntu-latest already moved 22.04 -> 24.04 and will move again to 26.04. Fine if your build is OS-agnostic; pin if it is not.", "fix": "runs-on: ubuntu-24.04"}, {"pattern": "using:\\s*['\\\"]?node20", "flags": "i", "sev": "error", "message": "runs.using node20: Node 20 is removed from the runner on 2026-09-23. This action stops running that day. -> using: 'node24'", "fix": "using: 'node24'"}, {"pattern": "using:\\s*['\\\"]?node(12|16)", "flags": "i", "sev": "error", "message": "runs.using node12/node16: already removed from the runner. This action cannot execute. -> using: 'node24'", "fix": "using: 'node24'"}, {"pattern": "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION", "flags": "", "sev": "warn", "message": "This escape hatch only defers the Node 20 removal, and it stops working on 2026-09-23. It is not a fix. -> Move the action to node24 instead", "fix": "Move the action to node24 instead"}, {"pattern": "node-version:\\s*['\\\"]?(14|16|18)\\b", "flags": "i", "sev": "warn", "message": "Node 18 reached end of life on 2025-04-30; 16 and 14 long before. No security patches. -> node-version: '24'", "fix": "node-version: '24'"}, {"pattern": "actions/checkout@v[1-3]\\b", "flags": "", "sev": "error", "message": "actions/checkout v1-v3 run on Node 16, which is gone from the runner. -> uses: actions/checkout@v5", "fix": "uses: actions/checkout@v5"}, {"pattern": "actions/checkout@v4\\b", "flags": "", "sev": "warn", "message": "actions/checkout@v4 runs on Node 20, removed from the runner on 2026-09-23. -> uses: actions/checkout@v5", "fix": "uses: actions/checkout@v5"}, {"pattern": "actions/upload-artifact@v[1-3]\\b", "flags": "", "sev": "error", "message": "upload-artifact v3 was shut down on 2025-01-30. Jobs using it fail outright. -> uses: actions/upload-artifact@v6", "fix": "uses: actions/upload-artifact@v6"}, {"pattern": "actions/download-artifact@v[1-3]\\b", "flags": "", "sev": "error", "message": "download-artifact v3 was shut down on 2025-01-30. Jobs using it fail outright. -> uses: actions/download-artifact@v6", "fix": "uses: actions/download-artifact@v6"}, {"pattern": "actions/(upload|download)-artifact@v[45]\\b", "flags": "", "sev": "warn", "message": "artifact v4/v5 run on Node 20, removed from the runner on 2026-09-23. -> uses: actions/upload-artifact@v6", "fix": "uses: actions/upload-artifact@v6"}, {"pattern": "actions/cache@v[1-3]\\b", "flags": "", "sev": "error", "message": "actions/cache v1/v2 were shut down on 2025-02-01; v3 runs on a dead Node. -> uses: actions/cache@v4", "fix": "uses: actions/cache@v4"}, {"pattern": "actions/setup-node@v[1-3]\\b", "flags": "", "sev": "error", "message": "setup-node v1-v3 run on Node 16, which is gone from the runner. -> uses: actions/setup-node@v6", "fix": "uses: actions/setup-node@v6"}, {"pattern": "actions/setup-node@v[45]\\b", "flags": "", "sev": "warn", "message": "setup-node v4/v5 run on Node 20, removed from the runner on 2026-09-23. -> uses: actions/setup-node@v6", "fix": "uses: actions/setup-node@v6"}, {"pattern": "uses:\\s*(?!actions/)[\\w.-]+/[\\w.-]+@v[1-3]\\b", "flags": "", "sev": "warn", "message": "A third-party action still on v1-v3 almost certainly bundles Node 16 or 20. Check its runs.using before 2026-09-23. -> Upgrade to a major built on node24", "fix": "Upgrade to a major built on node24"}, {"pattern": "::set-output", "flags": "", "sev": "error", "message": "The ::set-output workflow command was removed. It is silently ignored, so the value is empty downstream. -> echo \"name=value\" >> \"$GITHUB_OUTPUT\"", "fix": "echo \"name=value\" >> \"$GITHUB_OUTPUT\""}, {"pattern": "::save-state", "flags": "", "sev": "error", "message": "The ::save-state workflow command was removed and is silently ignored. -> echo \"name=value\" >> \"$GITHUB_STATE\"", "fix": "echo \"name=value\" >> \"$GITHUB_STATE\""}, {"pattern": "::set-env", "flags": "", "sev": "error", "message": "The ::set-env workflow command was removed in 2020 for security reasons. -> echo \"NAME=value\" >> \"$GITHUB_ENV\"", "fix": "echo \"NAME=value\" >> \"$GITHUB_ENV\""}, {"pattern": "::add-path", "flags": "", "sev": "error", "message": "The ::add-path workflow command was removed in 2020 for security reasons. -> echo \"/some/path\" >> \"$GITHUB_PATH\"", "fix": "echo \"/some/path\" >> \"$GITHUB_PATH\""}, {"pattern": "\\$\\{\\{\\s*github\\.event\\.(issue\\.(title|body)|pull_request\\.(title|body)|comment\\.body|review\\.body|review_comment\\.body|discussion\\.(title|body)|head_commit\\.message)", "flags": "", "sev": "error", "message": "Attacker-controlled text. Interpolated into a run: block it is shell injection - anyone who can open an issue runs commands as your job. -> Pass it through env: then quote \"$VAR\" in the script", "fix": "Pass it through env: then quote \"$VAR\" in the script"}, {"pattern": "\\$\\{\\{\\s*github\\.(head_ref|event\\.pull_request\\.head\\.(ref|label))", "flags": "", "sev": "error", "message": "A branch name is attacker-chosen text. Interpolated into run: it is shell injection. -> Pass it through env: then quote \"$VAR\" in the script", "fix": "Pass it through env: then quote \"$VAR\" in the script"}, {"pattern": "pull_request_target", "flags": "", "sev": "warn", "message": "pull_request_target runs with a writable token and your secrets. Never check out the PR head under it. -> Use pull_request, or check out the base ref only", "fix": "Use pull_request, or check out the base ref only"}, {"pattern": "permissions:\\s*write-all", "flags": "", "sev": "error", "message": "write-all hands every job a token that can push code and publish packages. -> permissions: {contents: read}", "fix": "permissions: {contents: read}"}, {"pattern": "uses:\\s*(?!actions/)[\\w.-]+/[\\w.-]+@(main|master|HEAD|latest)\\b", "flags": "", "sev": "error", "message": "Pinned to a moving branch. A force-push into that branch runs in your pipeline with your secrets. -> Pin to the full 40-character commit SHA", "fix": "Pin to the full 40-character commit SHA"}];
|
| 46 |
+
function scan(t){var L=String(t).split(/\r?\n/),h=[],i,r,re;
|
| 47 |
+
for(i=0;i<L.length;i++){for(var k=0;k<RULES.length;k++){r=RULES[k];
|
| 48 |
+
try{re=new RegExp(r.pattern,r.flags||'')}catch(e){continue}
|
| 49 |
+
if(re.test(L[i]))h.push({line:i+1,msg:r.message})}}
|
| 50 |
+
return h}
|
| 51 |
+
document.getElementById('go').onclick=function(){
|
| 52 |
+
var t=document.getElementById('src').value,o=document.getElementById('out');
|
| 53 |
+
var of=document.getElementById('offer');if(!t.trim()){o.innerHTML='';of.style.display='none';return}of.style.display='block';
|
| 54 |
+
var h=scan(t);
|
| 55 |
+
if(!h.length){o.innerHTML='<div class="ok">No expiring lines found in this file.</div>';return}
|
| 56 |
+
var s='<table><tr><th>Line</th><th>Finding</th></tr>';
|
| 57 |
+
for(var i=0;i<h.length;i++){s+='<tr><td class="n">'+h[i].line+'</td><td>'+
|
| 58 |
+
String(h[i].msg).replace(/&/g,'&').replace(/</g,'<')+'</td></tr>'}
|
| 59 |
+
o.innerHTML=s+'</table>'};
|
| 60 |
+
</script>
|
| 61 |
</html>
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
<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://www.npmjs.com/package/@readystack/workflow-expiry-check" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'workflow-expiry-check',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/workflow-expiry-check" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'workflow-expiry-check',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/workflow-expiry-check <file>
|
| 65 |
+
docker run --rm -v "$PWD:/w" getreadystack/workflow-expiry-check /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://buy.polar.sh/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>
|
| 66 |
+
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind Workflow Expiry - dated GitHub Actions breakage every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"workflow-expiry-check",lang:"en",source:"rule_alert"})}).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></section>
|
| 67 |
+
|
| 68 |
+
<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 the lines in .github/workflows that are valid today and stop working on a date GitHub has already published.</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Upwork lists DevOps engineers at $40-$100/hour, median $60.</p><a href="https://buy.polar.sh/polar_cl_JaA2n3akb6IlUIK0HRJCMrQCqJKruNoyMyBg00yLQZj" 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>
|
| 69 |
+
<!--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><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-08">Updated 2026-09-08</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-08","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>There is a category of CI failure that no linter warns you about, because nothing is wrong yet.</p>
|
| 70 |
+
<p>On 2026-09-08, this is a completely valid workflow. <code>actionlint</code> passes it. The official GitHub Actions VS Code extension passes it. The schema is correct, the labels exist, the actions resolve.</p>
|
| 71 |
+
<pre><code>name: ci
|
| 72 |
+
on: [push]
|
| 73 |
+
jobs:
|
| 74 |
+
build:
|
| 75 |
+
runs-on: ubuntu-22.04
|
| 76 |
+
steps:
|
| 77 |
+
- uses: actions/checkout@v4
|
| 78 |
+
- uses: actions/setup-node@v4
|
| 79 |
+
- uses: actions/upload-artifact@v4</code></pre>
|
| 80 |
+
<p>Every line in it has a published expiry date.</p>
|
| 81 |
+
<h3>The two dates in September 2026</h3>
|
| 82 |
+
<p><strong>2026-09-17</strong> - <code>ubuntu-22.04</code> and <code>ubuntu-22.04-arm</code> begin deprecation. Full removal is 2027-04-17, and GitHub runs brownout windows before that in which jobs on the image simply fail. Right now the label is valid, so a validity check has nothing to report.</p>
|
| 83 |
+
<p><strong>2026-09-23</strong> - Node 20 is removed from the runner. Runners have defaulted to Node 24 since 2026-06-16. Any action whose <code>action.yml</code> declares <code>runs.using: 'node20'</code> stops executing that day, and that includes <code>actions/checkout@v4</code>, <code>actions/setup-node@v4</code> and <code>v5</code>, and the v4/v5 artifact actions. The <code>ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION</code> escape hatch expires on the same date.</p>
|
| 84 |
+
<h3>Why a linter cannot tell you this</h3>
|
| 85 |
+
<p>A linter answers "is this valid?" A calendar answers "is this still valid in three weeks?" Those are different questions and the second one is the one that pages you at 3am.</p>
|
| 86 |
+
<p><code>actionlint</code> does flag an action whose runner is *already* unsupported - node12 and node16 today. It validates runner labels against the list of labels that *currently* exist. Both checks are correct and both are blind to an announced future date, by design. The information you need is not in the file and not in the schema. It is in a changelog post from last year.</p>
|
| 87 |
+
<h3>What to change</h3>
|
| 88 |
+
<pre><code>name: ci
|
| 89 |
+
on: [push]
|
| 90 |
+
permissions:
|
| 91 |
+
contents: read
|
| 92 |
+
jobs:
|
| 93 |
+
build:
|
| 94 |
+
runs-on: ubuntu-24.04
|
| 95 |
+
steps:
|
| 96 |
+
- uses: actions/checkout@v5
|
| 97 |
+
- uses: actions/setup-node@v6
|
| 98 |
+
- uses: actions/upload-artifact@v6</code></pre>
|
| 99 |
+
<p>While you are in there, three more things carry dates or teeth:</p>
|
| 100 |
+
<ul><li><code>::set-output</code>, <code>::save-state</code>, <code>::set-env</code> and <code>::add-path</code> were removed. They do not error.</li></ul>
|
| 101 |
+
<p>They are ignored, so the value downstream is silently empty. Use <code>>> "$GITHUB_OUTPUT"</code>.</p>
|
| 102 |
+
<ul><li><code>uses: some-vendor/action@main</code> pins to a moving branch. Anyone who can force-push that branch</li></ul>
|
| 103 |
+
<p>runs code in your pipeline with your secrets. Pin the full 40-character commit SHA.</p>
|
| 104 |
+
<ul><li><code>${{ github.event.issue.title }}</code> inside a <code>run:</code> block is shell injection. The title is written</li></ul>
|
| 105 |
+
<p>by whoever opened the issue. Pass it through <code>env:</code> and quote <code>"$VAR"</code> in the script.</p>
|
| 106 |
+
<h3>Checking it</h3>
|
| 107 |
+
<p>I built a VS Code extension for this, because the check belongs where the file is being edited rather than in a report someone reads quarterly. It carries 29 rules, each one carrying its date and its replacement line, and checking the file you have open is free - all 29 rules, no limit.</p>
|
| 108 |
+
<p>There is also a single-page version that runs entirely in the browser: paste a workflow, get the line numbers back. Same rule set, same answers, nothing uploaded.</p>
|
| 109 |
+
<p>The thing worth internalising is smaller than the tool: <strong>"it passes the linter" and "it will still run next month" are two different claims</strong>, and almost nothing in the ecosystem checks the second one.</p></div><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 Workflow Expiry actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a GitHub Actions workflow file and flags the lines that are valid today but stop working on a date GitHub has already published. Each finding gives the line number, the reason, the date, and the replacement line. It covers 29 rules: retiring runner images, the Node 20 removal, shut-down action versions, deleted workflow commands, and script injection.</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">The engineer who owns .github/workflows for a team that ships to production, and anyone maintaining a repository with more workflows than they can read by hand. It is aimed at people who find out a pipeline broke when a deploy fails on a Monday morning, rather than when the deprecation notice was published a year earlier.</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">Does actionlint not already catch this?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">actionlint is excellent and checks whether a label is valid right now. That is a different question. On 2026-09-08 ubuntu-22.04 is still a perfectly valid label, so no validity check flags it, yet its deprecation starts 2026-09-17. Workflow Expiry checks the calendar, not the schema, and needs no Go binary installed.</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?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Checking the workflow file you have open is free and complete: every one of the 29 rules, no watermark, no limit on how many times you run it. A licence unlocks a different job, not a bigger one: scanning every workflow in the repository at once, exporting the dated audit as a file, applying fixes, and emitting JSON for a CI gate.</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 me otherwise?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Upwork lists DevOps engineers at $40 to $100 per hour, median $60. Reading forty workflow files against a deprecation calendar by hand is most of a working day, and it has to be redone every time GitHub publishes a new retirement date. The full version is $29 once.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Workflow Expiry actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a GitHub Actions workflow file and flags the lines that are valid today but stop working on a date GitHub has already published. Each finding gives the line number, the reason, the date, and the replacement line. It covers 29 rules: retiring runner images, the Node 20 removal, shut-down action versions, deleted workflow commands, and script injection."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "The engineer who owns .github/workflows for a team that ships to production, and anyone maintaining a repository with more workflows than they can read by hand. It is aimed at people who find out a pipeline broke when a deploy fails on a Monday morning, rather than when the deprecation notice was published a year earlier."}}, {"@type": "Question", "name": "Does actionlint not already catch this?", "acceptedAnswer": {"@type": "Answer", "text": "actionlint is excellent and checks whether a label is valid right now. That is a different question. On 2026-09-08 ubuntu-22.04 is still a perfectly valid label, so no validity check flags it, yet its deprecation starts 2026-09-17. Workflow Expiry checks the calendar, not the schema, and needs no Go binary installed."}}, {"@type": "Question", "name": "What is free and what needs a licence?", "acceptedAnswer": {"@type": "Answer", "text": "Checking the workflow file you have open is free and complete: every one of the 29 rules, no watermark, no limit on how many times you run it. A licence unlocks a different job, not a bigger one: scanning every workflow in the repository at once, exporting the dated audit as a file, applying fixes, and emitting JSON for a CI gate."}}, {"@type": "Question", "name": "What would this cost me otherwise?", "acceptedAnswer": {"@type": "Answer", "text": "Upwork lists DevOps engineers at $40 to $100 per hour, median $60. Reading forty workflow files against a deprecation calendar by hand is most of a working day, and it has to be redone every time GitHub publishes a new retirement date. The full version is $29 once."}}]}</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:15817,slug:"workflow-expiry-check",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/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/personal-data-map-dsar-audit"><span class="yj-rel-t">Personal Data Map: find every column a GDPR subject access request has to reac</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/engineer-resume-template"><span class="yj-rel-t">Engineer Resume Template — 6 US Resumes: Mid-Career, New Grad with EIT, PE-Tra</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/tax-preparer-website-template"><span class="yj-rel-t">Tax Preparer Website Template — 26 Ready-Written HTML Pages for a Tax Office, </span><span class="yj-rel-p">$49</span></a><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/webflow-saas-template"><span class="yj-rel-t">Webflow SaaS Template — a 16-Page SaaS Marketing Site with the Copy Already Wr</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>
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
<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>
|
| 113 |
+
|
| 114 |
+
<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>
|