惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
Jina AI
Jina AI
罗磊的独立博客
V
Visual Studio Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
J
Java Code Geeks
U
Unit 42
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
爱范儿
爱范儿
酷 壳 – CoolShell
酷 壳 – CoolShell
Last Week in AI
Last Week in AI
T
The Blog of Author Tim Ferriss
腾讯CDC
Hugging Face - Blog
Hugging Face - Blog
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
I
InfoQ
月光博客
月光博客
博客园_首页
Vercel News
Vercel News
P
Proofpoint News Feed
GbyAI
GbyAI
Y
Y Combinator Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Your runtimes have an expiry date. I baked the EOL calend...
ひとし 田畑 · 2026-06-19 · via DEV Community

ひとし 田畑

Every runtime and piece of middleware you run has a quiet expiry date. PHP 8.0
went EOL in late 2023. Node 16 in 2023. Postgres 12 in 2024. After that: no
security patches. The problem is never "we didn't know EOL was a thing" — it's that
the EOL calendar lives in a dozen vendor pages and nobody checks them until an
auditor does.

I wanted my self-hosted ops tool to flag this automatically. The obvious move is to
call an API like endoflife.date. But I made the default
offline: the EOL calendar is baked into the app, and the external fetch is
strictly opt-in. Here's why, and the part that was actually fiddly — version
matching.

Offline by default: a self-hosted tool shouldn't phone home

If you ship a tool people run inside their own infra, "calls an external API on
startup" is a liability, not a feature. Air-gapped environments break. Security
reviewers ask why your inventory tool is making outbound requests. So the EOL data
is a plain dict compiled into the code:

# {canonical_name: {cycle: eol_date_or_None}}   None means "actively supported"
_EOL = {
    'python': {
        '3.7': date(2023, 6, 27),
        '3.8': date(2024, 10, 7),
        '3.9': date(2025, 10, 5),
        '3.11': None,
        '3.12': None,
    },
    'postgresql': {
        '12': date(2024, 11, 14),
        '13': date(2025, 11, 13),
        '16': None,
    },
    # redis, php, nodejs, nginx, mysql, ruby, go, java, ...
}

It works the moment you docker compose up, no network required. If you want
fresher data, you flip EOL_REFRESH_ENABLED=true and a daily job pulls from
endoflife.date and overlays the result on top of the baked-in dict:

def _effective() -> dict:
    eff = {k: dict(v) for k, v in _EOL.items()}   # start from the offline baseline
    snap = _load_snapshot_data()                  # latest fetched snapshot, if any
    if snap:
        for product, cycles in snap.items():
            eff.setdefault(product, {}).update(cycles)
    return eff

Offline data is the floor; the network is an enhancement, never a dependency. (The
fetch side validates the product slug against a regex and only ever talks https to
endoflife.date — an inventory tool making arbitrary outbound requests is exactly the
SSRF footgun you don't want to ship.)

The fiddly part: matching a version string to a cycle

Here's where it stops being a lookup table. A dependency reports its version as a
string — "3.11.4", "8", "1.24.2" — but the EOL calendar is keyed by release
cycle
: "3.11" for Python, "8" for MySQL. You can't just dict[version].

The rule that works: try the longest cycle key first (major.minor), then fall back
to major only.

for n_parts in (2, 1):                      # "3.11.4" → try "3.11", then "3"
    cycle = _major_minor(version, n_parts)
    if cycle not in cycles:
        continue
    eol_date = _parse_date(cycles[cycle])
    if eol_date is None:
        return 'ok'                         # cycle exists, no EOL date = supported
    if eol_date < today:
        return 'eol'
    if eol_date < today + timedelta(days=180):
        return 'warning'                    # within 6 months
    return 'ok'
return 'unknown'

Python is tracked at major.minor (3.11 ≠ 3.12), but MySQL "8.0.36" should match
the "8.0" cycle and Node "20.11.1" should match "20". Longest-match-first gets
both right without per-product special-casing. The four states — eol / warning /
ok / unknown — matter too: unknown is not ok. "I have no data for this"
and "this is supported" are different answers, and collapsing them is how you ship a
green dashboard that's quietly lying.

The "this has no EOL" case

Not everything has an end-of-life. Gunicorn, Celery, Composer — there's no vendor
EOL cycle to track. Naively those show up as unknown forever, which is noise. The
trick is an alias table that can map a name to None meaning "no EOL concept":

_ALIASES = {
    'node':     'nodejs',        # normalize spelling
    'postgres': 'postgresql',
    'gunicorn': None,            # explicitly: no formal EOL — don't nag about it
    'celery':   None,
}

def canonical(name):
    return _ALIASES.get(_normalize(name), _normalize(name))

So canonical() does double duty: it normalizes spellings (nodenodejs) and
encodes "we deliberately don't track this" as None, which short-circuits to
unknown and keeps it out of the warnings. Suppressing a check on purpose is a
feature; the alias table is where that intent lives.

Takeaways

  • For a self-hosted tool, make EOL data offline-first: bake in a baseline dict, make the external refresh opt-in, and overlay fetched data rather than depend on it. It runs air-gapped and doesn't surprise a security reviewer.
  • Don't index the EOL table by raw version. Match longest cycle first (major.minor → major) so 3.11.4, 8.0.36, and 20.11.1 all land on the right cycle.
  • Keep unknown distinct from ok, and give yourself a way to say "this has no EOL" on purpose so the signal stays clean.

This EOL check is one layer of a self-hosted tool that inventories your AWS assets
and the apps/middleware running on each environment, then flags drift and aging
runtimes. Open source (MIT), one docker compose up: syncvey.com.
How do you track middleware/runtime EOL today — a spreadsheet, Dependabot, or hope?