Pixel

Pixel Updates & Events

Server Updates & Events

Announcements

Optimize Unity WebGL with a Faster JavaScript Bridge

DEV Community

Engineered in Japan GCC: Optimizing Unity WebGL / Emscripten JS Bridge (Zero-Copy FS & Branchless Lookup)

بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ Algorithmic Fractal Art ("New Math" Series) Engineered in Japan. Minted on Ethereum Blockchain as a Private Collection (Not Listed / Invitation & Offer Only). 🔗 View Collection on OpenSea 🇸🇦 🇦🇪 🇶🇦 🇧🇭 🇴🇲 ⸺ Arabic Version (العربية - لغة تقنية فصحى) هذا المشروع هو تقرير بحثي تقني يركز على تحليل برمجيات الجسر (JavaScript Bridge) بين المتصفح ومحرك الألعاب في ألعاب WebGL التجارية (Unity WebGL)، مما أدى إلى تقليل استهلاك الذاكرة العشوائية (RAM) وتحسين أداء المعالجة (CPU). التزاماً بالشروط والأحكام وحماية لحقوق النشر، تم استبعاد الشيفرة المصدرية الفعلية للمنتج بالكامل. يشرح هذا المستند التحديات الهيكلية وطرق المعالجة المحددة. 🇬🇧 English Version This project is a technical research report focusing on analyzing JavaScript bridge code between the browser and the game engine in commercial WebGL games (Unity WebGL), resulting in reduced memory consumption (RAM) and CPU workload optimization. To comply with terms of service and protect copyrights, the actual product source code is completely excluded. This document explains the architectural challenges and specific remediation approaches. 🇯🇵 日本語版 (Japanese) 本プロジェクトは、商用WebGLゲーム(Unity WebGL)におけるブラウザとゲームエンジン間のJavaScriptブリッジコードを解析し、メモリ消費量(RAM)の削減およびCPU負荷の最適化を行った技術研究レポートです。 ※ 規約および著作権保護のため、製品のソースコード自体は含んでいません。アーキテクチャの課題と、それに対する具体的な修正アプローチを解説します。 🚀 نظرة عامة (Overview) يعتبر جسر JavaScript (خاصة حول نظام الملفات الافتراضي وإدارة الأجهزة) في ألعاب Unity WebGL منطقة معرضة بشدة للتسبب في تسريب الذاكرة وتقطعات الأداء الناتجة عن المعالجة المكثفة لآلية تنظيف الذاكرة (Garbage Collection)، لا سيما على متصفحات الهواتف المحمولة. يقضي هذا البحث على "نسخ البيانات المتكرر" و"خوارزميات البحث غير الفعالة" الموجودة في التطبيقات التجارية القياسية، مما يعيد هيكلة الكود ليعمل بسلاسة حتى على الأجهزة ذات المواصفات المنخفضة. 🚀 Overview The JavaScript bridge (specifically around the virtual file system and device management) in Unity WebGL games is highly prone to causing memory leaks and framerate hitches (spikes) due to heavy Garbage Collection (GC), particularly on mobile browsers. This research eliminates the "redundant data copying" and "inefficient lookup algorithms" found in standard commercial implementations, refactoring the codebase to run smoothly even on lower-end devices. 🚀 概要 (Overview) Unity WebGLゲームにおけるJavaScriptブリッジ(仮想ファイルシステムおよびデバイス管理周辺)は、特にモバイルブラウザ(スマホ)においてメモリリークやガベージコレクション(GC)によるカクつき(スパイク)の原因になりやすい領域です。 本研究では、既存の一般的な実装に見られる「冗長なデータコピー」と「非効率な探索アルゴリズム」を解消し、低スペック端末でも安定して動作するコードへのリファクタリングを行いました。 🛠 محاور التحسين الرئيسية (Key Optimizations) ١. نقل البيانات ذو النسخ الصفري عبر إلغاء النسخ المتكرر للذاكرة 🔴 التحدي (Before) عند كتابة ملفات الأصول الثنائية المستلمة من جهة المتصفح (البيانات الواردة من Web Workers، إلخ) داخل نظام ملفات WebGL الداخلي (FS)، كان يتم إنشاء نسخة جديدة من Uint8Array في كل مرة، مما يتسبب في تخصيص مزدوج للذاكرة. أدى ذلك إلى استدعاء مفرط لآلية تنظيف الذاكرة أثناء تحميل الأصول الكبيرة، مما تسبب في تجمد مؤقت للشاشة وتقطعات دقيقة. 🟢 حل المعالجة (After) الانتقال من "النسخ بالقيمة" إلى "التمرير بالمؤشر (المرجع)"، مع تفعيل خيار تفويض ملكية الأصول (canOwn: true) ضمن إعدادات نظام ملفات Emscripten. Zero-Copy Transfer via Elimination of Redundant Memory Copying 🔴 Challenge (Before) When writing asset binaries retrieved on the browser side (data returned from Web Workers, etc.) into the WebGL internal file system (FS), a new Uint8Array instance was instantiated every time, causing double allocation of memory. This triggered massive Garbage Collection during large asset loads, leading to micro-stutters and screen freezes. 🟢 Remediation Approach (After) Shifted from "copy-by-value" to "pass-by-pointer (reference)," utilizing the asset ownership delegation option (canOwn: true) within Emscripten's file system configuration. 🛠 主な最適化ポイント (Key Optimizations) メモリの重複コピー廃止によるゼロコピー化 (Zero-Copy Transfer) 🔴 課題 (Before) ブラウザ側で取得したアセットバイナリ(Web Worker等から返されたデータ)をWebGL内のファイルシステム(FS)に書き込む際、毎回新しい Uint8Array のインスタンスを生成(メモリの二重確保)していました。これにより大量のファイル...

Powered by Readybot.io

Optimize Unity WebGL with a Faster JavaScript Bridge

AI Agent Runs 187 FEM/CFD Solves: Accuracy Results

DEV Community

An AI Agent Ran 187 FEM/CFD Solves Over MCP. Here's How Many Matched the Textbook

From 6 to 12 November 2026, using dates as recorded in the logs, an AI agent (Claude Code) drove an open-source simulation app, NovaSolver Studio, through its MCP server. Structural, modal, buckling, thermal, flow, electrostatic and DC current problems. Every call and every reply was written to a log. The headline numbers, all from those logs: MCP calls 3,355 (49 log files) Solves started 187 Accepted comparisons with a reference 136 (from 118 analyses) Median difference 0.13% Within 1% / within 3% 105 / 130 Above 5% 2 The accepted set has 132 external-reference comparisons (formulas, series, correlations or independent 2D FEM) and 4 comparisons with another mesh of the same Studio model; 140 comparisons were recorded including reference-only results. The interesting part is not the median. It is what got thrown away before those 136, and two types of setup mistake that passed a mesh or reference check. What "the agent ran it" means here It was not a chat where the model clicked around freely. Claude Code wrote Python scripts that act as an MCP client. Each script starts Studio's MCP server over stdio, calls the tools in a fixed order, and appends every call and reply to a .jsonl file. Claude Code also wrote the STEP geometry, the scripts that compute the reference values, and the scripts that compare the two. The client is small. This is the core of mcpdrv.py, the shared driver: async def call(self, name, args):
t0 = time.perf_counter()
r = await self.s.call_tool(name, {"language": "ja",
args})
...
with io.open(self.log, "a", encoding="utf-8") as f:
f.write(json.dumps({"tool": name, "args": args, "reply": text,
"s": round(time.perf_counter() - t0, 2),
"data": getattr(r, "structuredContent", None)},
ensure_ascii=False, default=str) + "\n")
return text

async def wait(self, text):
m = re.search(r"Running as (\S+?).", text)
if not m:
return text
while True:
await asyncio.sleep(5)
rep = await self.call("job_status", job_id=m.group(1))
if rep.startswith(("done", "failed", "stopped", "error")):
return rep
A typical case script then reads like a recipe: create_case or import_geometry, update_case with materials and boundary conditions, validate_case, check_convergence, get_results, verify_case, probe_results. 58% of the calls were waiting job_status was called 1,949 times out of 3,355. Solves run as background jobs. run_simulation and check_convergence return a job id if the job is not done in about 20 seconds, and job_status waits up to 20 seconds per call before it answers. So most of the traffic is the client asking "done yet?". The calls that carry engineering content are an almost flat band of about 170–200 each: set up, check, solve on three meshes, read results, compare. The sum of all call durations is 42,086 seconds (about 11.7 hours). That is not wall time: it does not include the scripts' own time, and jobs that queued behind each other are counted in parallel. From 206 cases to 171 results Of 187 solves started: Outcome Count Out of memory (Studio's pre-check or during the solve) 11 Iterative solver did not converge 3 Tool error (the disk filled up) 1 Result not recorded 1 Results obtained 171 ...with a completed three-mesh test 167 ...mesh tests passed / failed 145 / 22 ...single-mesh pipe calculations 4 Before any solve, validate_case returned RED 8 times. All eight were DC current cases where Joule heating was on by default and no face set a temperature. Four plate updates were rejected; the client continued, and one started job has no recorded completion. Eleven cases were only opened to read the geometry. The accepted set excludes the 800 mm column, which failed the mesh test. Re-solves often covered a whole set after one case failed, to keep the mesh settings consistent. Out of 23 topics, 13 needed at least one redo, design change or abandon...

Powered by Readybot.io

AI Agent Runs 187 FEM/CFD Solves: Accuracy Results

Migrate from Depot Runners Without Losing Cache

DEV Community

Migrate from Depot runners without losing the cache

Teams that migrate from Depot runners in 2026 are usually reacting to one number: Depot lists its GitHub Actions runners at $0.006 a minute, which is GitHub-hosted list price to the cent. The move itself is a label per job, and the two things that do not move with it are the remote BuildKit builders and a cache that behaves differently from every other cache in this market. The cost of 1,000 jobs at five job lengths, at the rates published on 2026-09-20. Per-second tracking wins below about 25 seconds a job and loses above it. Start with what you are giving up, because it is specific and some of it is unusual. Depot tracks usage by the second and bills whole minutes at the end of the month, with no one-minute minimum, so a wide matrix of short jobs does not pay for rounding. Its runner documentation publishes 100 GB of disk on the smallest Linux runner, rising to 250 GB, with a slice of memory reserved as a RAM-disk accelerator in front of it. It publishes no concurrency limit, no cache size limit and no network limit. And its cache is not isolated by branch: its documentation says entries from main and from other branches share one namespace, scoped by repository. That is the opposite of actions/cache, where an entry is restorable only from the branch that wrote it or from the default branch. It is the single change most likely to make a migrated pipeline slower without any error appearing. The alternatives and their rates are in Depot alternatives. This page is the cutover: the label map, the cache work, and the arithmetic for whether per-second tracking was worth the rate. The label map, with the disk in it Depot labels carry the Ubuntu version and a vCPU suffix, so depot-ubuntu-22.04-4 is a 4 vCPU runner on 22.04. Sizes match Latchkey up to 16 vCPU and the memory matches at every step; the disk does not. Depot publishes 100 GB at 2 vCPU and 130 GB at 4, while a Latchkey runner reports 43 GB free, measured on 2026-09-20 and quoted in migrate from Blacksmith. Depot label What it is Closest Latchkey label What changes depot-ubuntu-24.04 2 vCPU, 8 GB, 100 GB disk latchkey-small 58% cheaper a minute, 43 GB of disk, no RAM-disk accelerator depot-ubuntu-22.04-4 4 vCPU, 16 GB, 130 GB disk latchkey-medium Same cores, 15.3 GB usable measured, a third of the disk depot-ubuntu-24.04-8 8 vCPU, 32 GB, 150 GB disk latchkey-large Same cores and memory, 43 GB of disk depot-ubuntu-24.04-16 16 vCPU, 64 GB, 180 GB disk latchkey-xlarge The largest size Latchkey publishes depot-ubuntu-24.04-32 and -64 32 or 64 vCPU, 128 or 256 GB None No equivalent; these jobs stay on Depot or go to Blacksmith at 32 depot-ubuntu-24.04-arm and depot-windows-2025 Graviton4 arm64, or Windows None Linux x86_64 only on the other side, so these stay put depot-macos-latest 8 CPU, 24 GB, 400 GB disk None No macOS runner at any price; the destinations are priced separately One constraint is identical on both sides and catches personal accounts: Depot requires the repository to be owned by a GitHub organization, and so does Latchkey. Ubicloud, Blacksmith and Namespace each have their own connection step, listed in the roundup. The pre-flight checklist Seven items. The cache ones are the two that decide whether this migration feels neutral or feels like a regression. Labels. Grep the workflow directory for runs-on:.depot- to find every job, including the Windows and macOS ones you may not be able to move. Depot actions. Grep for depot/. The build actions and the CLI talk to Depot builders and have no meaning on another runner. Cache scope. List the jobs that restore a cache written by another branch. On Depot that works; on actions/cache it does not, and those jobs will start cold on every feature branch. Cache size. Depot sells 25 GB on its $20 Developer plan and 250 GB on Startup. GitHub gives 10 GB per repository with 7-day eviction; Latchkey publishes no size and keeps entries 14 days. If you are above your destination's allowance, decide what stops being cached ...

Powered by Readybot.io*

Migrate from Depot Runners Without Losing Cache

Fix GitHub Actions Cache 503 and 429 Errors

DEV Community

Cache service responded with 503 or 429 in GitHub Actions

Cache service responded with 503 in GitHub Actions is the Actions cache backend refusing one request, and a 429 is the same backend throttling you. Neither is a problem with your key: the step warns, the job carries on without a cache, and the only thing that turns it into a red build is a workflow that treats a cache miss as fatal. The restore failing is a warning. A red build needs something else in the workflow to insist that the cache was essential. What this error means A cache step logs a warning rather than an error, and the job keeps going: "Warning: Failed to restore: Cache service responded with 503". A 429 appears in the same shape and means the opposite thing, that the service is working and has decided you are asking too often. Either way the restore did not happen, so the steps after it run cold: the install downloads everything, the build has no incremental state, and the job takes the long route. The save at the end of the job can fail the same way, which is worse in one specific sense, because the next run then starts cold too. The block below is a sample from the pattern library rather than from a run of ours, and the section on reproduction explains why there is no run on this page. ```Sample log from the pattern library Warning: getCacheEntry failed: Cache service responded with 503

Common causes

The cache service had a bad few minutes

The ordinary case. A 503 is the backend declining one request during an incident, a deploy or a load spike, and it clears on its own. Nothing in the workflow caused it and nothing in the workflow will prevent it; the only useful response is to make sure it costs a slower build rather than a failed one.

You set continue-on-error and the job still failed

The wasted fix, because the tolerance went on the step that was already tolerant. The cache step warns; the failure is in whatever ran next on the assumption that the cache was restored. Look for a step gated on the cache-hit output, a fail-on-cache-miss, or a test command that expects a directory the restore was supposed to create.

Your own concurrency earned the 429

A wide matrix, several workflows triggered by one push, or a monorepo running twelve jobs that each save a large cache will produce a burst of cache traffic in a short window. The service answers that with a throttle. It is transient, and it is also a design signal: the same caches are being written many times over.

The caches are large enough to make every operation fragile

A multi-gigabyte cache takes a long time to upload and download, which widens the window in which something can go wrong, and it pushes other entries out under the eviction policy. In our experience the repositories that see cache-service errors most often are the ones caching a built tree rather than a package store.

How to fix it

Let a cache miss be a cache miss

  1. Remove fail-on-cache-miss unless the job genuinely cannot run without the cache.
  2. Make sure the install step runs whether or not the restore succeeded, rather than being skipped on a cache hit.
  3. Re-run the workflow once the service recovers, and expect the first run after the incident to be slow.

```.github/workflows/ci.yml
- uses: actions/cache@v6
id: cache
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
- run: npm ci --no-audit --no-fund
Put the tolerance where the failure is If something downstream really does need the cache, guard that step rather than making the whole job depend on a service you do not control. A conditional fallback is a few lines and it turns an outage into a slow build. - uses: actions/cache@v6
id: deps
with:
path: ~/.cache/pip
key: pip-${{ hashFiles('requirements.txt') }}
- if: steps.deps.outputs.cache-hit != 'true'
run: pip install -r requirements.txt
Reduce the cache traffic you generate Cap matrix parallelism, share one warm cache across legs with restore-keys, and cache the package store instead of the...

Powered by Readybot.io

Fix GitHub Actions Cache 503 and 429 Errors

Connect Google Reviews to Claude with MCP

DEV Community

Ask your Google reviews from Claude: a hosted MCP server walkthrough

If you manage Google reviews for more than one location, the day usually starts the same way: open the Business Profile for each location, scroll, find what's new, find what's still unanswered. I wanted to ask one question instead: "which of my locations has the most unanswered reviews, and what are people complaining about?" That's what the Model Context Protocol (MCP) is good at. Here's how we wired Google reviews into Claude, Claude Code and Cursor with a hosted MCP server, and the design decisions that mattered. The setup in one command For Claude Code: claude mcp add --transport http reputemap https://api.reputemap.com/mcp \
- -header "Authorization: Bearer rmk_YOUR_KEY"
Then: claude "which of my locations has the most unanswered reviews?"
For Cursor, add this to ~/.cursor/mcp.json: {
"mcpServers": {
"reputemap": {
"url": "https://api.reputemap.com/mcp",
"headers": { "Authorization": "Bearer rmk_YOUR_KEY" }
}
}
}
In claude.ai or Claude Desktop you don't need a key at all: add it as a connector and sign in through OAuth. The tools The server is Streamable HTTP and exposes seven tools: Tool What it does list_locations Every location you manage, with ids list_reviews Reviews filtered by location, rating, unanswered, last N days, with the published reply get_review_stats Counts, averages, 1–5 breakdown, unanswered, per location and in total draft_reply Reply variants, saved as a draft. Never publishes publish_reply Publishes a reply you explicitly approved send_review_request Sends a real review-request email get_usage This month's API credits Three design decisions worth copying 1. Read tools run freely, write tools always ask. Every tool carries MCP annotations: the four read tools are marked read-only, while draft_reply, send_review_request and publish_reply are not. Well-behaved clients ask before running them, and in Claude you can block the write tools entirely to keep the assistant read-only. An AI that answers customers on its own is the most common complaint about review software, so the default here is "the model drafts, a person approves". 2. Writes are idempotent where it matters. A second send_review_request for the same email at the same location returns status: "already_requested". No second email goes out and no credit is spent. Models retry; customers shouldn't get two emails because of it. 3. Surface the platform's verdict, not just "success". Google now moderates owner replies before showing them. So publish_reply returns google_state: pending while Google checks, or rejected_by_google with the policy reason. list_reviews carries the same state, and a rejected reply counts as unanswered. Without that, an agent would happily report "replied!" to a reply nobody can see. Prompts that work well "Summarize what customers complained about at the Austin location in the last 30 days." "Draft replies to every unanswered 1–3 star review this week. Don't publish anything." "Compare average rating and unanswered count across all locations, worst first." "Send a review request to [email protected] for the Round Rock location." (Claude asks first.) Costs and limits Each tool call costs one API credit (get_usage is free), and every paid ReputeMap plan, including the 14-day trial, includes 1,000 credits a month, shared with the REST API. Links Setup reference with Claude Desktop, Cursor and troubleshooting: reputemap.com/docs/mcp Copy-paste configs and scripts: github.com/reputemap-com/examples Agent skills for review triage and replies: npx skills add reputemap-com/review-skills If you build something on top of it, I'd love to hear what you ask your reviews.

Powered by Readybot.io

Connect Google Reviews to Claude with MCP

Fake Ryzen CPU: How a Laptop Hid Its Real Chip

DEV Community

The Fake CPU Booted Fine. That's the Real Scandal.

A laptop that lies about its own processor should not work. It should crash, refuse to boot, or at least throw an error somewhere. The Chuwi CoreBook X did none of that. Sold as carrying an AMD Ryzen 5 7430U, it actually carried the older Ryzen 5 5500U — and it booted, ran Windows, and displayed the fancier name everywhere a buyer would look. That is what made the story stick when it broke in March 2026. A teardown by NotebookCheck found the mismatch the only two ways it could be found: by reading the chip's own ordering part number, and by running benchmarks that did not match the advertised processor. The BIOS, the operating system, and the packaging all repeated the wrong name. Nothing in the machine objected. So the interesting question is not only how one small brand got caught. It is why the lie was so easy to tell. The answer sits one level up the supply chain. AMD's mobile lineup reuses architectures across generations in ways that make several model numbers close cousins: the 5500U, 5600U, 5625U, and 7430U share enough silicon DNA, microcode, and driver support that a system can accept one name in place of another without tripping. Rebadging is not a bug in this world. It is the business model. When a chipmaker stretches one architecture across many price points and model years, it saves engineering cost — and it also creates a gap between what a model number promises and what the silicon actually is. A downstream seller only has to step into that gap. This is not unique to AMD, and it is not new. Intel has spent decades moving sockets and sharing chipset microcode across product tiers, and every one of those transitions created the same kind of confusion: parts that fit, boot, and mostly work, while meaning something slightly different from what the box says. The PC industry has trained buyers to read model numbers as if they were precise technical descriptions. In practice, they are often marketing labels pasted over a shared platform. That is the argument a March 2026 Chinese commentary piece made about the Chuwi case, and it is worth taking seriously even if you strip away its sharper language: the fraud was downstream, but the conditions that made it possible were upstream. A giant's cost-saving habit — reuse, rename, repeat — became a small brand's opportunity. The commentary called it greed. A cooler word would be negligence about what reuse does to trust. Either way, the mechanism is the same: when the top of the chain stops policing the meaning of its own labels, the bottom of the chain gets to rewrite them. None of this excuses the mislabeling. The facts of the case are plain. The teardown evidence was the chip number and the benchmark scores, not a rumor. AMD denied involvement. Chuwi's OEM was identified as the Shenzhen assembler Emdoor. A recall was offered with narrow conditions: units bought new, with full original packaging, before the end of May 2026 — and at least one popular reseller channel was left outside it. Buyers who checked early, with a free tool like CPU-Z and a benchmark run, could catch it. Buyers who trusted the box could not. There are three lessons here, and they apply well beyond laptops. First, a spec sheet is a claim, not a measurement. For anything where performance is the product — a processor, a battery, a motor — the only numbers that count are the ones you can reproduce yourself. A five-minute check beats a five-star listing. Second, reuse is a trust decision, not just an engineering one. Every company reuses platforms; every company that does so owes buyers a clear map of what changed and what did not. When model numbers multiply faster than real differences, the map is being hidden, not drawn. Third, the penalty for getting caught has to land on the right floor. A recall window that excludes ordinary resale channels and demands pristine packaging protects the seller more than the buyer. If the industry wants the benefit of cheap reuse, it should pay for verification: publ...

Powered by Readybot.io

Fake Ryzen CPU: How a Laptop Hid Its Real Chip

Weekly CVE Triage with KEV, EPSS and CVSS

DEV Community

Build a weekly CVE triage workflow with CISA KEV, EPSS and CVSS

This article was first published on the Rokkhe blog on Hashnode. It was drafted with AI assistance and reviewed and edited by the Rokkhe team; all commands and sources were checked against CISA, FIRST and NVD documentation. A one-hour operational pass you can run every Monday, using official feeds only. 15 November 2026, Asia/Dhaka. By the Rokkhe research team. Most teams do not fail at “finding CVEs.” They fail at deciding which ones to do this week. Three public signals help, and they do not measure the same thing: CISA’s Known Exploited Vulnerabilities (KEV) catalog records vulnerabilities with reliable evidence of exploitation in the wild, and a clear remediation action. EPSS, from FIRST, estimates the probability that a CVE will be exploited in the next 30 days. CVSS describes severity (how bad a successful exploit can be), not whether anyone is exploiting it. This post is a weekly workflow: fetch, join, ticket. It complements the existing Rokkhe briefing on how to prioritize vulnerabilities with CISA KEV, EPSS and CVSS; it does not replace that briefing, and it does not invent a success rate for the method. Do not use this workflow to discuss or publish embargoed research. If a draft is under vendor embargo, it stays out of the queue. TL;DR If a CVE is in KEV and you run the product, treat it as a priority this week, even when EPSS is low. FIRST says that explicitly. Use EPSS for the large majority of CVEs that are not in KEV. Use CVSS to understand impact once you already know a CVE is relevant, not as the only sort key. The commands below only read public APIs. They do not patch anything by themselves. After you patch a KEV item, hunt for signs it was used before the fix. A patch does not prove the box was clean. Three signals, three different questions CISA maintains the KEV catalog so organisations can prioritize vulnerabilities that have already been used. CISA also publishes BOD 26-04, Prioritizing Security Updates Based on Risk (issued 10 June 2026), which carries the KEV criteria forward and tells federal civilian agencies to prioritize using exposure, KEV status, exploit automation and technical impact. You do not have to be a federal agency for the catalog to be useful. CISA encourages every organisation to watch it. FIRST’s “Using EPSS” guidance is careful about what the score is: a calibrated probability of observed exploitation in the next 30 days, not a severity score and not a complete risk score. A low EPSS on a KEV-listed CVE is not a contradiction. KEV says exploitation has already been seen. EPSS says the current signal looks like CVEs that are rarely exploited in the next month. FIRST’s rule of thumb: when a CVE is on KEV, treat it as a priority regardless of EPSS. Use EPSS for everything KEV does not cover. CVSS remains useful for impact: a network-exploitable, no-user-interaction vulnerability is a different change window than a local issue that needs an existing account. It is a poor only-sort, because a “critical” CVE with no exploitation evidence and no instance in your estate should not beat a “high” KEV item you actually run. A fourth, unglamorous signal sits underneath all three: do we have this product, in this version, on a reachable network? If the answer is no, log it and stop. If the answer is “we do not know,” the first ticket is inventory, not a patch. Pull this week’s KEV additions CISA publishes the catalog as a JSON feed. The following command only downloads a public file: curl -fsSL \
'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json' \
- o /tmp/kev.json

Newest dateAdded values first. Adjust the slice as you like.

jq -r '
.vulnerabilities
| sort_by(.dateAdded)
| reverse
| .[:20]
| .[]
| [.dateAdded, .cveID, .vendorProject, .product, .shortDescription]
| @tsv
' /tmp/kev.json
For a weekly pass, filter to additions since last Monday (use your date): SINCE=2026-11-09 # replace with last week's cutoff, in UTC dates as the feed ...

Powered by Readybot.io

Weekly CVE Triage with KEV, EPSS and CVSS

GitHub Actions working-directory Error Explained

DEV Community

GitHub Actions working-directory on a uses step is rejected, not ignored

GitHub Actions working-directory on a uses step is refused by the workflow schema before anything runs, so the belief that the key is accepted and quietly ignored is backwards. There is no wrong directory to debug, because the file never compiled and no job was ever created. A step is one of two shapes. Reading uses narrows the reader to the shape that has no working-directory property, and the next key has nowhere to go. What this error means You added a directory to a step that calls an action, pushed, and the workflow did not run. In the Actions tab the file is flagged rather than the job, and the annotation names a line and a column and quotes the key you added. If you are reading a stale answer that says the key is silently ignored, the thing to check is whether your run exists at all: an ignored key produces a green run that did the wrong work, and a rejected key produces no run. ```Message text from TemplateStrings.UnexpectedValue in actions/runner Invalid workflow file: .github/workflows/ci.yml#L12 (Line: 12, Col: 9): Unexpected value 'working-directory'

Common causes

The key was copied from a run step onto a uses step

Overwhelmingly the commonest origin. Somebody had a working run step with a directory on it, replaced the command with an action, and kept the surrounding keys. The diff looks harmless because only the middle line changed.

An answer elsewhere said the key is ignored rather than refused

The claim that a uses step accepts working-directory and quietly does nothing with it is widespread and wrong. It matters because the two behaviors need opposite responses: an ignored key means hunting for why the action used the wrong path, and a refused key means the run you are looking for does not exist.

The action really has no way to take a path

Some actions assume the repository root and offer no input for anything else. Reaching for working-directory is the natural next move, and the schema refuses it, which leaves the reader convinced the key is broken rather than absent.

The directory belonged on the job, not the step

When every step in a job works in the same subdirectory, per-step keys accumulate until one of them lands on an action. In our experience this is the version that survives review, because each individual line looks like the line above it.

How to fix it

Pass the path as an input the action declares

Open the action's action.yml and read the inputs block. Most actions that touch the filesystem expose a path, a working-directory or a source input. Passing it under with is the only mechanism the step shape offers, and it is the one the action author tested.

.github/workflows/ci.yml (illustrative) - uses: some-org/build-action@v1 with: path: packages/app Set the directory once for the job, for run steps Move the directory to defaults.run.working-directory on the job. It applies to every run step in the job and to none of the actions, which is exactly the boundary the schema draws, so nothing is left ambiguous for the next reader..github/workflows/ci.yml (illustrative) jobs: build: defaults: run: working-directory: packages/app

Wrap the action in a run step when it takes no path

  1. Check the action's action.yml once more for any input that accepts a path.
  2. If there is none, replace the action with the command it wraps, in a run step that carries the directory.
  3. If the action does real work you do not want to reimplement, prepare the directory in a run step before it, so the action still sees the layout it expects at the repository root.

Split the job when two directories are genuinely involved

A job whose steps work in two places is asking one defaults block to be two values. Two jobs, each with its own default, expand into a clearer run page and let you give them different conditions later. The cost is one extra checkout, which a cache usually absorbs.

How to prevent it

  • Validate workflow files in the editor, wh...

    Powered by Readybot.io
GitHub Actions working-directory Error Explained

Dockerize a FastAPI App: Beginner's Guide

DEV Community

📝 How to Dockerize a FastAPI Application (A Beginner's Guide)

Have you ever heard a developer say, "But it works on my machine!" That is the exact problem Docker was built to solve. If you are building a FastAPI application, you need a way to package it with all its dependencies, environment variables, and OS libraries so it runs exactly the same way on your laptop, your teammate's laptop, and the production server. Here is how to Dockerize a FastAPI app in three simple steps. Step 1: Create your requirements.txt First, make sure your project has a requirements.txt file listing all your Python packages. fastapi==0.100.0
uvicorn==0.23.2
Step 2: Write the Dockerfile A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. Create a file named Dockerfile in the root of your project (no file extension). # Use the official lightweight Python image
FROM python:3.10-slim

Set the working directory in the container

WORKDIR /code

Copy the requirements file first (this helps with caching)

COPY ./requirements.txt /code/requirements.txt

Install the dependencies

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

Copy the rest of the application code

COPY ./app /code/app

Command to run the application using Uvicorn

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
Step 3: Build and Run Now that you have your Dockerfile, you can build the image and run the container. Open your terminal and run: docker build -t my-fastapi-app .
docker run -d --name my-running-app -p 8080:80 my-fastapi-app
- -d runs the container in the background. · -p 8080:80 maps port 8080 on your machine to port 80 inside the container. Why this is a game-changer If you look closely at the Dockerfile, the --host 0.0.0.0 flag is crucial. If you leave it as 127.0.0.1, the server will only listen to traffic inside the container, and your browser won't be able to reach it. Once you start using Docker, you never have to worry about dependency conflicts again. Have you started using Docker for your Python projects yet? Let me know!

Powered by Readybot.io

Dockerize a FastAPI App: Beginner's Guide

Tinybox: Affordable Hosting Built by a Teen Developer

DEV Community

I'm 16 and built a tiny hosting platform to earn from my hobby. It has 0 users so far

I'm 16, and coding is my hobby. I wanted to see if I could turn it into a little income, so I built tinybox: tiny servers you can start in a minute. What it does Deploy from a GitHub repo (Node.js, Python or static sites) A web terminal in your browser Your own name.tinybox.online address with HTTPS 2 custom domains Free for 7 days, no card needed Paid plans start at $1/month (crypto accepted, or pay manually) The hard parts Connecting my backend to the VPS so one click creates a working server for each user Building the web terminal Setting up subdomains and HTTPS automatically Each of these took me longer than I expected, and I learned a lot doing them. Where it stands I launched a few days ago. Number of users: zero. 😅 I'm sharing it now because I'd rather learn from real feedback than keep polishing alone. What I'd love to know Would you use something like this for a side project or a bot? Is 256MB RAM enough for what you build? What's missing? Try it https://tinybox.online The first 10 testers get a free month of the Starter plan (512MB RAM) on top of the 7-day trial. Sign up, then comment "tester" below and I'll upgrade your slot. I'll reply to every comment. Thanks for reading! 🙏

Powered by Readybot.io

Tinybox: Affordable Hosting Built by a Teen Developer

HKDF Info: Bind Protocol Context to Derived Keys

DEV Community

HKDF info is where protocol context becomes key identity

HKDF info is where protocol context becomes key identity A key derivation function turns one secret into several keys. HKDF separates that work into an extract step and an expand step. RFC 5869 defines an optional info input for the expand step and describes it as context and application-specific information. The field is optional in the API. Treating it as optional in a shared-key deployment is a different decision. The extract step computes a pseudorandom key from input keying material and an optional salt. The expand step uses that pseudorandom key, the info value, and an output length. If two applications use the same input secret and the same output length but different info values, they receive different output keys. The info value is not secret. Its job is to make the intended use unambiguous. Make the context a fixed grammar A useful info value identifies the protocol version, key purpose, sender or receiver role, algorithm, and any identifier that changes the key's meaning. Encode fixed fields with lengths or a canonical serialization. Do not concatenate user-controlled strings in a way that lets ("ab", "c") and ("a", "bc") produce the same input. Examples include myproto/v2/ingress/session-key and a structured binary context with a protocol label, a 32-bit version, a direction byte, and a bounded identifier. The exact format matters less than consistency across languages and versions. Publish test vectors for the language implementations and keep old contexts readable during migration. Do not put a password, token, or other secret into info. It is a public domain label. If the deployment needs to bind a key to a secret identifier, derive a separate value or use a construction designed for that purpose. Keep extraction and expansion deliberate RFC 5869 warns against skipping the extract step when the input is a Diffie-Hellman value. The details belong to the protocol design. The operational review is simpler: identify every place that calls an HKDF API and confirm that it follows the protocol specification rather than a copied helper with different defaults. Check the output length against the hash function. HKDF-Expand has a limit related to the hash output size. A caller that asks for an excessive length should receive a deterministic error, not a truncated key. Enforce the same limit in every language binding. Salt values also need a policy. They can be public, but a missing salt and a fixed salt are not the same deployment choice. Record whether the protocol uses a random salt, a transcript-derived salt, or an explicit zero value. Do not let a retry silently choose a different salt if the protocol expects a reproducible derivation. Test key separation Build a small matrix with two contexts that share the same input secret. Verify that the output keys differ, that each context produces the same key on every implementation, and that a context from one service is rejected by the other service. Include a context-version change and a key-rotation case. Logging should record the protocol, context label, algorithm, and output length, not the secret or the derived key. That information is enough to compare configuration and diagnose a mismatch without creating another data store full of key material. HKDF does not authenticate a peer, encrypt a message, or choose an application identifier. It provides a controlled expansion from a secret. The info field is the place where the team states what that secret is for. References RFC 5869, HMAC-based Extract-and-Expand Key Derivation Function RFC 8619, Algorithm Identifiers for HKDF

Powered by Readybot.io

HKDF Info: Bind Protocol Context to Derived Keys

Fix Docker TLS Handshake Timeouts in CI

DEV Community

Docker net/http: TLS handshake timeout in CI

A Docker TLS handshake timeout in CI means the pull opened a connection to the registry and then ran out its deadline before the certificate exchange finished. It is a path problem rather than an image problem, which is why the same docker pull works on the next run, and why the first thing worth changing is how many pulls your job starts at once. The message names the stage the pull died in. Each stage has a different set of causes, and only one of them is worth pointing at the registry. What this error means The pull fails in seconds rather than minutes, with a single line from the daemon quoting the URL it was trying and a Go networking error. Three wordings mean the same thing at three different points: a handshake that started and did not finish, a request canceled while waiting for headers, and a context deadline exceeded. All three are the client giving up rather than the registry refusing you, which is the difference between this and a rate limit or an authentication failure. This page carries no recorded run. The reproduction available to us is a listener that accepts the connection and never completes the handshake, which produces the message but models a registry that is permanently unreachable rather than the transient this failure actually is, so the block below is the sample text the Latchkey pattern library matches on, labeled as such, and the causes come from the daemon issues in the references. ```Sample log from the Latchkey pattern library Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: TLS handshake timeout Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) Error response from daemon: failed to resolve reference "docker.io/library/node:lts-slim": failed to do request: Head "https://registry-1.docker.io/v2/library/node/manifests/lts-slim": net/http: TLS handshake timeout

Common causes

The runner was pulling several things at once

The most common and the most fixable. Handshakes are latency sensitive and small; a link busy moving layer blobs will delay them enough to trip a deadline measured in seconds. A job that fails in the step where compose starts six containers, and passes when it pulls one image, has said what the problem is.

The registry edge was having a bad minute

Public registries are shared infrastructure and they have slow periods. The moby issue tracker carries this one continuously, with the same shape each time: intermittent, not reproducible on demand, gone on the next attempt. Nothing in your workflow caused it and nothing in your workflow will prevent it, so the answer is a retry and a mirror.

Something is inspecting TLS between the runner and the registry

A corporate proxy, an egress appliance or a service mesh sidecar terminating TLS adds work and latency to every handshake, and sometimes rewrites the certificate chain. If the failures are constant rather than occasional, and only from one network, this is the first place to look rather than the last.

The path itself is lossy, or IPv6 is a black hole

Packet loss and a broken path MTU both show up as handshakes that stall, because the handshake needs several round trips before anything useful has happened. An environment that advertises IPv6 without a working route is the same failure with a cleaner cause: the daemon tries the address it was given, waits, and reports a timeout.

How to fix it

Retry the pull, with a bounded loop

  1. Pull explicitly before the step that needs the image, so a failure is attributed to the pull rather than to a build or a test.
  2. Retry three times with a short backoff. A handshake that fails twice in a row is usually a path problem rather than a busy minute.
  3. Fail the step on the last attempt rather than continuing into a build that has no image.

```Terminal
for attempt in 1 2 3; do
docker pull "$IMAGE" && break
...

Powered by Readybot.io

Fix Docker TLS Handshake Timeouts in CI

Intl.DurationFormat: Fix Duration Formatting for French

DEV Community

My Duration Formatter Broke in French. Intl.DurationFormat Doesn't.

Here's a function that ships in more codebases than anyone wants to admit: function formatDuration(hours, minutes, seconds) {
const parts = [];
if (hours > 0) parts.push(${hours} hour${hours !== 1 ? "s" : ""});
if (minutes > 0) parts.push(${minutes} minute${minutes !== 1 ? "s" : ""});
if (seconds > 0) parts.push(${seconds} second${seconds !== 1 ? "s" : ""});
return parts.length ? parts.join(", ") : "0 seconds";
}

formatDuration(1, 46, 40); // "1 hour, 46 minutes, 40 seconds"
It works. It ships. Code review doesn't blink — the ternary is a little ugly, but it's correct, and everyone's seen worse. Then the product adds a French locale, and formatDuration keeps returning "1 hour, 46 minutes, 40 seconds" to someone whose UI is in French everywhere else. Nobody wrote a bug here. formatDuration does exactly what it was written to do. The bug is that it was only ever written to do one thing: speak English. The ternary that only counts to two The !== 1 ? "s" : "" trick works in English because English plurals are binary — one thing, or not-one things. Pick any other language and that assumption quietly stops holding: French still needs the trailing "s" logic, but also reorders the sentence with "et" before the last item: 1 h, 46 min et 40 s. Russian and Polish have three plural categories, chosen by the last digit of the number, with exceptions for 11–14. Arabic has up to six — zero, one, two, few, many, other — each a genuinely different word shape, not just a suffix. A two-way ternary can express exactly one rule: English's. It's not almost right for everything else; it's correct for one language and silently wrong for the rest, in a way that never throws, never logs, and never shows up until someone files "the app is broken in French" with no repro steps that mean anything to you. This is also why the npm ecosystem has half a dozen duration-formatting packages, most of them shipping a locale-data JSON file to work around exactly this. The browser has had equivalent locale data loaded since the day it started, for Intl.NumberFormat and Intl.RelativeTimeFormat. Duration formatting was the gap. As of 2025, it isn't anymore. Intl.DurationFormat, tested for real 🎮 Try it yourself ▶️ Open the interactive playground → Runs right in your browser — poke at it and watch the concept react live. Intl.DurationFormat takes a locale and a style, and formats a duration object built from plural field names — hours, minutes, seconds, up through years: const dtf = new Intl.DurationFormat("en", { style: "long" });
dtf.format({ hours: 1, minutes: 46, seconds: 40 });
// "1 hour, 46 minutes, 40 seconds"

new Intl.DurationFormat("fr-FR", { style: "short" })
.format({ hours: 1, minutes: 46, seconds: 40 });
// "1 h, 46 min et 40 s"
Same duration, same call shape, correct output in both languages — word order, conjunction, and all. Nothing about your code changes between locales; only the string you pass as the first argument does. Four style values cover the usual cases, and short is the default if you omit it: const d = { hours: 1, minutes: 46, seconds: 40 };
new Intl.DurationFormat("en", { style: "long" }).format(d); // "1 hour, 46 minutes, 40 seconds"
new Intl.DurationFormat("en", { style: "short" }).format(d); // "1 hr, 46 min, 40 sec"
new Intl.DurationFormat("en", { style: "narrow" }).format(d); // "1h 46m 40s"
new Intl.DurationFormat("en", { style: "digital" }).format(d); // "1:46:40"
I ran all four of those in a real browser before putting them in this post, rather than trust a blog post that did the same thing eight months before this API's behavior settled — and they're exactly what's above, character for character. Three sharp edges, found by actually calling it The surface API is simple. A few of its rules only show up when you poke at it, and they're worth knowing before they poke back. Field names are strict, and wrong ones don't warn you. The object is keyed by plural unit names only — hours, not hour or h. Pass t...

Powered by Readybot.io

Intl.DurationFormat: Fix Duration Formatting for French

Forced Colors Test Reveals SVG Accessibility Gaps

DEV Community

I turned forced colours on for all 76 of our pages, and 39,617 drawing colours ignored it completely

Forced colours mode is the setting behind Windows High Contrast. When it is on, the browser throws away most of your colour decisions and repaints the page in a palette the user chose: one colour for text, one for background, one for links, one for buttons. It is not a dark theme. It is a user saying "I will decide what things look like, thank you". What makes it interesting to test is that the override is partial. The browser forces some properties and leaves others alone, and the ones it leaves alone are exactly the ones a drawn interface relies on. Nakodo is a drawn interface. Its public pages contain no photographs and no raster images, just line art. So I turned forced colours on for all 76 pages and counted what changed. Running it Chrome can emulate the mode, which means you can test it without a Windows machine: await page.cdp("Emulation.setEmulatedMedia",
{ features: [{ name: "forced-colors", value: "active" }] });
By hand it is DevTools, the Rendering panel, "Emulate CSS media feature forced-colors", set to active. Readers can follow along on any page of the site. Then on each page I walked every element and recorded its computed color, backgroundColor, fill and stroke, and whether each had become a system colour or kept its own. The counts Across 76 pages: Measure Count Elements 59,700 of which SVG elements 34,175, or 57 per cent color values replaced by the system palette 57,607 color values that kept their own value 2,093 SVG strokes untouched 23,733 SVG fills untouched 15,884 Declarations of forced-color-adjust on the site 0 The first line of the result is good news. Text is the thing forced colours is for, and 57,607 of 59,700 elements hand their text colour straight over. Paragraphs go white, links go yellow, buttons become outlined boxes. The pricing page in this mode is entirely readable: three plan cards as black boxes with white borders, every feature line legible, every price legible. The one thing it loses there is emphasis. In normal colours the recommended plan is an inverted dark card that pulls your eye across the row. In forced colours all three cards are identical, and the only reason you can still tell which one is recommended is that the word "RECOMMENDED" is text rather than a colour. That is the rule this mode teaches, and it is worth writing on a wall: any distinction carried by colour alone does not survive, and any distinction also carried by a word or a shape does. The 57 per cent the mode does not touch More than half of every page here is SVG, and forced colours mode does not override fill or stroke. That is per spec, not a Chrome bug: the forced properties are things like color, background-color, border-color and outline-color. Paint on a shape is left to the author. So 39,617 colour values across the site carry on exactly as they were, on top of a background the user has just changed to black. Whether that is fine or catastrophic depends entirely on which colour each shape was drawn in. Measured against the forced background: Drawing colour Contrast against the forced canvas paper 0 (the fill inside drawings) 20.63:1 ochre 400 (the spot colour) 11.46:1 cyano 800 (the brand mark) 2.23:1 ink 900 (every outline) 1.33:1 The top two are better in forced colours than they are in daylight. The bottom two are the problem, and the bottom one is the whole line system. Every outline in every drawing on the site is --illo-line, which is ink 900, which lands at 1.33:1 against black. A line at 1.33:1 is not a faint line. It is not there. What saves the screenshots from being empty is that the drawings are not outlines alone. They are shapes filled with paper and spotted with ochre, with ink outlines on top. The birds on the home page are still perfectly clear in forced colours: white bodies, ochre wings, and outlines you cannot see but do not need. The autumn leaves in the hero, which are mostly ink with a little ochre, are mostly gone. The clearest single failure is the brand mark in the heade...

Powered by Readybot.io

Forced Colors Test Reveals SVG Accessibility Gaps

Verify AI Agents with zkVerify Receipts

DEV Community

Trust the Agent. Verify It My Agent Games Receipts

Don't Trust the Agent. Verify It — My Agent Games Receipts My agent makes claims all day. "I checked the balance." "The recipient matches." "The transfer is safe." A claim is not evidence. Crypto already solved this for money: don't trust, verify. zkVerify does the same for agents. Agent Games Season 01 (21 Oct – 21 Nov 2026) asks Creator Track entrants to show their work. Here is mine — 9 things I actually did, with receipts. 01 The problem → an agent can misread a number, mix up two addresses, or approve what it should reject. It can be buggy or compromised. A proof is valid or it isn't. There is no "mostly correct." 02 Registration → I registered @rbby24 on agentgames.zkverify.io without a browser or wallet extension. Asked for a sign-in nonce, built the message, signed it with a fresh wallet, sent it back. Session valid to 15 Dec 2026. User ID 32a8225a-4920-4402-8633-a5b62b3d00ae. 03 The wire format → the hard part wasn't the signature. The handle is claimed through a server action that sends its arguments as an array in a plain-text body — not form data. My first 5 tries sent the wrong shape. The server answered every one with the same useless error: just a code, no explanation. 04 The rate-limit trap → then "Too many registration attempts." It was a rate limit — and it was hiding the real problem. The format was still wrong. Two different failures, same blank response. I only figured it out by reading the site's own JavaScript. 05 The fix → once the format was right, it worked. @rbby24, 0 points, 0 verified proofs, not ranked. Ugly process. But the failure is the point: if agents are going to use the web, the web has to work for agents too. 06 The circuit → registration proves an agent exists, not that it decides correctly. So I used the tournament's own gatekeeper circuit — the zero-knowledge circuit that decides whether a transfer gets approved. Downloaded the official files, installed snarkjs 0.7.6, ran it locally. 07 The proof → clean case: balance 1460 vs minimum 1000, amount 500 vs max 1000, recipient matches, expiry block 8,430,000 ahead of 8,420,000, verification-key hash matches the registered one. Decision: approve. The proof came out 805 bytes, 13 public signals. Verification returned OK. Anyone can rerun it: snarkjs groth16 verify verification_key.json public.json proof.json
[INFO] snarkJS: OK!
08 The negative test → the real test is whether the circuit lets a wrong decision through. It doesn't. Approve a transfer to the wrong recipient? The proof can't even be built — witness generation fails. Reject a valid request? Same. A wrong decision doesn't produce a bad proof. It produces no proof at all. 09 What it doesn't prove → no live score, no endpoint built, 0 points, not ranked. The live track needs a public endpoint answering signed tasks in 60 seconds, and I didn't build one. What it proves is narrower: the verification layer works as advertised, an outsider can use it end to end, and it fails in the right direction — it refuses wrong decisions instead of rubber-stamping them. Don't trust the agent. Verify it. The tools are public, the circuits are downloadable, and the proof is 805 bytes. Try it yourself. Agent Games Season 01 runs to 21 November 2026, judging 22–30 November. #AgentGames @ZKVProtocol

Powered by Readybot.io

Verify AI Agents with zkVerify Receipts

Fix GitHub Workflow File Push Permission Errors

DEV Community

Refusing to allow a GitHub App to create or update workflow files

The message refusing to allow a GitHub App to create or update workflow files comes from GitHub's git server during a push, not from Actions, and it names a grant that is deliberately separate from repository write access. Which grant depends on what pushed: an App installation needs a repository permission, and an OAuth token needs a scope with a different name. Three identities, three names for the same privilege, and one of them cannot be granted from a workflow file at all. What this error means A push is rejected and nothing else about the branch is unusual. The same token pushed every other file in the same commit range without complaint, and the only difference in the failing change is that it touches something under .github/workflows. The rejection is printed by git, so it appears in a bot's log in the middle of a checkout or push step rather than in any Actions annotation, and the branch simply does not appear on GitHub. `Quoted from Fragment256/githatch#60, a push rejection reported verbatim ! [remote rejected] ci/fix -> ci/fix (refusing to allow a GitHub App to create or update workflow.github/workflows/ci.ymlwithoutworkflows permission)

Common causes

The App installation does not hold the Workflows permission

The commonest cause for bot pushes. An app configured for contents and pull requests can do everything else it needs and is refused on any push that touches .github/workflows, because the workflow file privilege is a separate repository permission.

A personal access token is missing the workflow scope

Classic tokens list workflow as its own checkbox, separate from repo. A token created for general automation usually does not have it, and the failure only appears the first time the automation edits a workflow rather than when the token was created.

The push is using the token the workflow run was given

That token cannot be granted this privilege at all. There is no key for it in the permissions block, so no amount of editing the workflow will help, and contents: write is not a substitute.

The grant was added but the installation was never re-approved

A new repository permission on an existing GitHub App installation has to be accepted before it takes effect. In our experience this is the version that produces the longest debugging session, because the app's settings page shows the permission the whole time.

How to fix it

Read the rejection and identify which grant it named

  1. Look for the words GitHub App or OAuth App in the message.
  2. A GitHub App means the workflows repository permission on the app.
  3. An OAuth App means the workflow scope on the token.
  4. Then confirm the grant on the credential in hand rather than on the settings page you edited.

Push with an App installation token that holds Workflows

Mint a short-lived installation token in the job and hand it to checkout, so the push is authenticated as the app rather than as the run. Keep the app's permission set minimal and add Workflows only to the installation that needs it.

```.github/workflows/sync.yml (illustrative)
- uses: actions/checkout@v7
with:
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
Re-approve the installation after changing permissions Adding a permission to an app does not update installations that already exist. Accept the new permission for the installation on the organization or account, then mint a fresh token, because a token issued before the acceptance carries the old set. Split the change when only part of it touches a workflow If a bot generates both code and workflow files, let it push the code with the credential it already has and open a pull request for the workflow change, or keep the workflow file identical to one already on another branch. The scopes reference records that an identical file at the same path on another branch may be committed without the scope, which is a narrow exemption but occasionally the cheapest rou...

Powered by Readybot.io

Fix GitHub Workflow File Push Permission Errors

The New Senior Hire Has No Ramp-Up Time

DEV Community

You're the new senior hire, and there's no ramp for that

👋 I'm Parsa Jiravand — I work in IT, and this is Best Practice. One article every day, one soft-skills episode every week. It all lives at bestpractic.org — come join us. It's your second week, and someone's just asked what you think the team should do about the deploy pipeline that everyone agrees is held together with duct tape. You don't know whose duct tape. You don't know if the last three people who tried to fix it got overruled, or burned out, or promoted somewhere else for trying. You have an opinion forming anyway, because that's the job you were hired to do, and you can feel the gap between how much you actually know and how much weight your answer is about to carry. That gap is the whole first month now, and it didn't used to be this wide. The job changed shape before you noticed For a long time, "new senior hire" meant someone who'd clear a short, forgiving runway — a few weeks to read the code, sit in on standups, get a feel for who actually makes the calls versus who just talks in meetings — before anyone expected an opinion that mattered. That runway is the thing that's gone missing. Hiring has leaned harder toward senior roles this year, and junior postings have thinned out alongside it, which means a lot of teams aren't backfilling a ladder anymore — they're hiring one person to be the judgment, directly, because there's no one two rungs down to grow into it. If you got the senior title, you weren't hired to eventually know how the system breaks. You were hired already knowing it, and the first month is everyone — including you — finding out whether that's actually true here, on this particular pile of decisions someone else made before you arrived. That's a different test than the one the title implies. "Senior" sounds like it's testing your judgment. What it's actually testing, for the first several weeks, is your judgment about a system you have none of the history for. Those aren't the same skill, and the gap between them is exactly the thing nobody warns you about when they hand you the title. What's actually happening underneath Here's the part that's uncomfortable to say out loud: the people around you are reading your first few answers as a referendum on whether the hire was right. Not consciously, mostly. But the first opinion you give in a room gets weighted more than the fortieth will, because it's the only data point anyone has yet. And the thing most people do with that pressure is try to prove the title immediately — have a strong take in week two, fix the thing everyone's been complaining about, show you were worth it. It reads as confidence. It's actually the single easiest way to spend credibility you haven't earned on a decision you don't have the context to be right about. The quieter problem is that nobody tells you this is happening, so you don't know you're being read at all. You think you're just answering a question honestly. The room thinks you're declaring your operating style. What works The move that buys you the most room, and costs you nothing, is asking the people who've been there what's already broken that nobody's fixed. Not "what should I know," which gets you a tour. Specifically: "What's the thing people here already agree is a problem, that just hasn't gotten prioritized?" That question does three things at once. It gets you the map, for free, from someone who's already drawn it — which is faster than re-deriving it from the codebase. It signals that you're looking for consensus before you push, which reads as judgment rather than ignorance, even though you're asking because you genuinely don't know yet. And it tells you, by what people say and what they visibly don't say, where the actual landmines are — the thing everyone agrees is broken but is also someone's project, or someone's old decision, or tied to a person who's still in the room. The second move is narrower, and it's about the first opinion you're asked for in a room full of people who don't know you yet. ...

Powered by Readybot.io

The New Senior Hire Has No Ramp-Up Time

Why the Second AI Deployment Is the Real Product

DEV Community

The Second Deployment Is the Product

Originally published at Trida AI. The most expensive sentence in our line of work is "we've done this before." Said loosely, it means somebody on the team remembers a similar project. Said properly, it should mean something you can point to: a shelf of artifacts left over from the last deployment, which the next one starts from. The distance between those two meanings is where forward deployed engineering either becomes a business or stays a run of heroic one-offs. A first deployment is discovery at full price. The team learns what the client's data actually looks like, which approvals really matter, where the workflow breaks on a bad Tuesday. That learning is the most valuable thing the engagement produces, and the most perishable. If it lives in the heads of the engineers who did the work, it depreciates on a schedule nobody controls: people rotate, context fades, someone leaves. The second deployment starts close to zero, and the client pays again for lessons already bought once. Our position is simple. Judge a deployment team by what the first deployment leaves behind. The residue is the product. But residue does not curate itself, and most of what a project leaves behind is not reusable. Knowing which parts are, and doing the unglamorous work of making them so, is the discipline this post is about. What carries over Failure catalogues carry furthest. The ways an AI system fails in production repeat across clients because they come from the machinery, not from the business: a source replaced after indexing, quoted as if current; an ambiguous instruction resolved silently instead of asked about; a tool result trusted after the thing it described had changed; an approval requested after the action, not before. A catalogue entry names the shape, says how the system detects it, and says what happens instead. The detection and the response transfer almost intact. The examples inside the entry do not — they get redrawn from the new client's own data, because the old client's data is not ours to carry, and because a failure seen only in someone else's examples is one you will not recognise in yours. Eval sets transfer as skeletons. The question classes, the scoring rubric, the taxonomy of hard cases: those carry. The gold answers do not — what counts as correct is a business judgment, and it belongs to the business being judged. Starting from a skeleton still changes the second deployment's shape: a regression gate exists from the first week, in form, waiting for the new client's labels, instead of being invented under deadline pressure near the end, which is when eval sets usually get built, and why they so often get built badly. Connector patterns transfer; mappings do not. How a class of systems behaves — its authentication flow, its pagination, how it signals rate limits, whether its writes are idempotent — is knowledge that survives the move to the next client running the same class of system. What a field means inside one company's estate does not survive, and pretending otherwise is how a second deployment imports the first client's data model by accident. Review and tracing shapes transfer for a duller reason: the accountability questions are the same everywhere. What did the system do, on what evidence, who approved it, how do we undo it. The records that answer those questions can be rebuilt from a pattern, at a fraction of the cost of designing them from a blank page. What the first deployment hands over. Artifacts move through a curated library; judgments stay in the room. One catalogue entry, carried across. The check and the hold behaviour moved unchanged; the stamp and the staleness window were rewritten. Example content. What has to stay bespoke Data semantics stay. What "active customer" means, when a ticket counts as closed, which of three conflicting records wins: no library knows this, because it is not knowledge about systems. It is knowledge about one business, and it has to be learned in the room, every...

Powered by Readybot.io

Why the Second AI Deployment Is the Real Product

One Variable Font Replaces Five Font Files

DEV Community

I Was Shipping Five Font Files. One Variable Font Replaced Them All.

Open the network tab on a site with a half-decent type system and count the font requests. Four is normal. Six isn't rare. Every one of them is a separate .woff2 file for a separate weight — Regular, Medium, Semibold, Bold — sitting in the critical path before the browser can paint text in anything but a fallback font. Our design system had five: four weights plus a bold italic for pull-quotes. Nobody had decided that on purpose. It accumulated — a designer added Semibold for one component, Bold Italic followed for another, and eighteen months later the font folder looked like a weight had been added every time someone liked how it looked in Figma. The fix everyone reaches for first When this shows up in a performance audit, the fix is almost always the same: cut weights. Drop from five down to three. Merge Semibold into Medium. Ship Bold Italic as a font-style: oblique of regular Bold instead of a dedicated file. @font-face {
font-family: "Inter";
src: url("/fonts/inter-regular.woff2") format("woff2");
font-weight: 400;
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter-medium.woff2") format("woff2");
font-weight: 500;
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter-bold.woff2") format("woff2");
font-weight: 700;
}
This genuinely helps — three requests beat five. It also doesn't touch the actual shape of the problem: every weight you keep is still its own file, its own request, its own round trip. Cut too aggressively and you lose the weight your design actually needs; cut too little and you're back where you started with a slightly smaller number. Before you keep reading — guess how many of those weights the browser actually downloads on a page that only uses Regular body text and one Bold heading. The honest (and slightly surprising) answer is: two. Browsers don't fetch an @font-face file the moment it's declared — the CSS Fonts spec requires user agents to download only the faces a page's rendered text actually calls for, so Medium, Semibold, and Bold Italic never leave the server for this particular page. That's the one thing "cut weights" already gets right, mostly by accident. What it doesn't fix: those two downloads are still two separate round trips, and the moment any other page in the same design system reaches for a third weight — a Semibold stat callout, a Bold Italic pull-quote — that page pays for a third file, one request at a time, because nothing about a static setup can serve more than the single weight baked into each one. It's not about how many weights — it's about how many files Here's what the "cut weights" fix never addresses: a static font file is a fixed snapshot. One file draws letterforms at exactly 700 and nothing else. Want 650 for a hover state that should feel "almost bold but not quite"? You can't — 650 doesn't exist unless you shipped a sixth file for it. The weight scale looks continuous in your design tool and is actually a handful of hardcoded stops in production. A variable font isn't a set of snapshots. It's one file that encodes the whole range as interpolation data — the letterforms at 100 and the letterforms at 900, with the browser computing everything in between on the fly. Ask for font-weight: 538 and you get a real, rendered 538, not a browser fallback or a rounded guess. @font-face {
font-family: "Inter";
src: url("/fonts/inter-variable.woff2") format("woff2-variations");
font-weight: 100 900; / a range, not a single value /
font-stretch: 75% 125%;
}
That's the whole type system. One @font-face block. One network request. Every weight from thin to black, plus every stop in between, available from the same file: h1 { font-weight: 700; }
h2 { font-weight: 600; }
body { font-weight: 400; }
.btn:hover { font-weight: 538; } / yes, this is a valid value now /
Five requests become one. The five-weight design system from the top of this post could have shipped as a single file from day one — nobody needed to decide anything ab...

Powered by Readybot.io

One Variable Font Replaces Five Font Files

Handle API Rate Limits in Python with Backoff

DEV Community

How to Handle API Rate Limits in Python Like a Pro (with Exponential Backoff)

If you are building applications that rely on external APIs (like OpenAI, Gemini, GitHub, or even just fetching job data), you have probably run into the dreaded 429 Too Many Requests error. When I was building my AI backend, my server kept crashing because I was hammering the Gemini API too fast. If you just write a try/except block and instantly try again, you will get banned. You need Exponential Backoff. Here is how it works and how to build it in Python. What is Exponential Backoff? Instead of retrying a failed request immediately, you wait a little bit. If it fails again, you wait twice as long. If it fails again, you wait twice as long as that. · Attempt 1 fails -> Wait 1 second. · Attempt 2 fails -> Wait 2 seconds. · Attempt 3 fails -> Wait 4 seconds. · Attempt 4 fails -> Wait 8 seconds. This gives the API server time to breathe and clear its queue, making your request much more likely to succeed on the next try. The Python Code (Using the tenacity Library) You could write this logic from scratch using a while loop, but why do that when Python has an incredible library called tenacity? First, install it: pip install tenacity
Now, let's build a robust API request function: import httpx
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

We only want to retry if we hit a 429 (Rate Limit) or 500 (Server Error)

def is_rate_limit_error(exception):
return isinstance(exception, httpx.HTTPStatusError) and exception.response.status_code in [429, 500]

@retry(
retry=retry_if_exception_type(httpx.HTTPStatusError),
wait=wait_exponential(multiplier=1, min=2, max=10),
stop=stop_after_attempt(5)
)
async def fetch_external_data(url: str):
print(f"Attempting to fetch: {url}")
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=10.0)

    # Raise an exception if the status code is 4xx or 5xx
    response.raise_for_status() 
    return response.json()

What does this code do? · wait_exponential(multiplier=1, min=2, max=10): This is the magic. It will wait 2s, then 4s, then 8s, capping at 10s so you don't wait forever. · stop_after_attempt(5): It will try 5 times before finally giving up and raising the error to your main application. Bonus Tip: Respect the Retry-After Header If an API is really nice, it will send a Retry-After header in the 429 response telling you exactly how many seconds to wait. Tenacity can be configured to read this, but even with a standard exponential backoff, your server will stop crashing and start handling failures gracefully. Have you ever had your server crash from an API rate limit? Let me know below!

Powered by Readybot.io

Handle API Rate Limits in Python with Backoff

Supabase Security Check for AI-Built Apps

DEV Community

24,664 AI-built apps sent database records to any visitor. I built a one-command check that proves yours does not.

24,664 AI-built apps sent database records to any visitor. I built a one-command check that proves yours does not. In November 2026, German security firm Crissy Field (Hamburg), working with the newspaper Handelsblatt, published research on AI-built apps. The numbers are the reason I built this tool, so here they are first, with the caveats attached. The researchers scanned 159,041 Lovable projects found in Common Crawl (apps created December 2025 to September 2026). Of those, 34,545 used Supabase as their backend. 24,664 websites sent database records to the browser as soon as the page loaded, without any login or interaction. With further read-only requests using the same public key the app itself ships, the researchers found records containing email addresses in 10,560 tables across 7,403 sites (30%). Worst cases, each a separate app: one service provider exposed more than 1,000 identity records (passport numbers, expiry dates, nationalities); one lifestyle forum exposed more than 200,000 private chat messages and 80,000 profiles; one app exposed about 280,000 customer emails. Read those numbers honestly, because the researchers are explicit about the limits: 30% is NOT a confirmed leak rate for all Lovable apps. The sample is not representative, some email addresses are public contact or test data, and records without an email address were not counted. The researchers could not establish whether any exposed data was misused. Record counts are a floor, not a total: only part of each dataset was retrieved. This was not a hack of Lovable's systems. The researchers used read-only requests with the same public key the app itself ships. The root cause is Row Level Security left off in the app's Supabase database, with the public anon key doing the rest. The same class of bug had already been disclosed eight months earlier as CVE-2025-48757 (March 2025, CVSS 9.3): researchers scanned 1,645 Lovable showcase sites and found 170 with tables open to anonymous reads. Lovable added a Security Scanner and an AI Security Reviewer after that, and the exposure repeated at a larger scale in November. The lesson is not "run a scanner once". It is "prove, on every deploy, that an anonymous request returns zero rows". I built a small tool that does exactly that. It is called vibeleak, and it is a zero-dependency TypeScript CLI. MIT licensed. It starts from only the deployed app URL, finds the Supabase project URL and anon key in the JavaScript the app already ships, then probes each table and storage bucket as an anonymous visitor. One command, one verdict: CLEAN, LEAK, or REVIEW, with a JSON report and a baseline diff so a closed table that opens again fails CI. The problem: the key is already public, and nobody checked the lock When an AI tool builds an app on Supabase, it ships a public anon key in the frontend JavaScript. That is by design: the browser needs the key to talk to the database. The only thing standing between a stranger and your tables is Row Level Security. If RLS is off, or a policy says USING (true), the anon key reads the whole table, and the app helpfully sends the rows to any visitor on page load. Nobody checks this because the app works. The login page looks right, the dashboard loads, the data is there for the right user. The failure is invisible from inside the app. You only see it when you make the same request the app makes, without logging in, and count the rows that come back. That is what vibeleak does. It fetches the app the way any visitor does, extracts the Supabase URL and anon key from the shipped JavaScript (the same code every browser downloads), enumerates the tables through the PostgREST OpenAPI document, and probes each one with an anonymous GET limited to one row. It stores column names and a 0/1 rows-visible flag only. It never stores row values. It never writes anything: GET and HEAD only, no POST, no RPC calls, no writes of any kind. What it does Five commands: vibeleak scan [--i-own-t...

Powered by Readybot.io

Supabase Security Check for AI-Built Apps

Why SongUp AI Offers a 30-Second Free Song Preview

DEV Community

Pay after you hear it: why our AI song maker sings 30 seconds free first

I build SongUp AI, so this is a maker's build note, not a neutral review. When you sell something people have never bought before, the hardest question is not "how much?" It is "what am I actually getting?" A custom song is a strange purchase. Nobody can picture it in advance. You type a few lines about your partner, your dad or your dog, and you are asked to pay before you know if the voice, the style or even the words will work. So on SongUp AI anyone can hear the first 30 seconds of a song made from their own lyrics before paying anything. Here is the thinking behind that design. 1. The preview has to be their song, not a demo A sample in the chosen style only tells you what "acoustic pop" sounds like. What people want to know is how their chorus sounds with their person's name in it. So the free preview is generated from the user's own words. It costs more to run than playing a sample, but it is the only preview that answers the real question. 2. Thirty seconds is a design decision, not an accident Too short and the song never reaches the chorus. Too long and there is no reason to keep the full song. Thirty seconds usually covers an intro and the first verse or hook, which is where the name and the first memory land. That moment, hearing a name sung, is what people react to. The full song after that is one payment of $2.99, and plans start at $9.99 a month for people who make a lot of songs. Both are on one page so nobody has to dig for the price: songupai.com/pricing. 3. Show the price before the button, not after "Unlock full song" buttons that only reveal the price on the checkout page are common, and every surprise there costs trust. A price printed next to the button may get fewer clicks, but the clicks that happen come from people who already decided. 4. Be honest about what AI makes Some requests get refused by the music model (certain names, long descriptions), and some lines come out sung oddly. We would rather a user hear that in a free preview than after paying. It also means support tickets start with "can you fix this line?" instead of "I want a refund." The short version If your product's value is a feeling, let people have the feeling before they pay. In our case that is 30 seconds of a song built from their own words. Here is a 16-second clip of the idea (the presenter is AI-generated, not a customer): What do you preview for free in your product, and how did you choose where the free part ends? Pricing details: songupai.com/pricing

Powered by Readybot.io

Why SongUp AI Offers a 30-Second Free Song Preview

Latchkey vs WarpBuild: GitHub Actions Runner Costs

DEV Community

Latchkey vs WarpBuild for GitHub Actions runners

On managed runners Latchkey is $0.0025 a minute at 2 vCPU against WarpBuild at $0.004, but that is not the comparison most WarpBuild users are making: its bring-your-own-cloud mode runs in your own AWS, GCP or Azure account at $0.002 a minute, which is cheaper than both. The question is whether you want to own the cloud account the jobs run in. One month of 10,000 Linux minutes at 2 vCPU on a private repository at each published rate. The BYOC bar is the WarpBuild orchestration charge only; the instances are billed by your own cloud provider on top. Rates read 20 September 2026. WarpBuild sells managed GitHub Actions runners at $0.004 a minute for 2 vCPU and 8 GB, adopted by installing its bot from the WarpBuild dashboard rather than the GitHub marketplace and setting runs-on to a runner ID such as warp-ubuntu-latest-x64-4x. It also sells a bring-your-own-cloud mode at $0.002 a minute where the machines run in your own cloud account, publishes arm64 at $0.003, and ships Windows and macOS runners. Its cache is metered separately at $0.20 per GB-month plus a charge per write or restore. Latchkey sells managed runners only, at $0.0025 a minute for 2 vCPU and 8 GB, in its own AWS estate, with a self-healing agent that repairs transient failures inside the run. There is no BYOC mode, no arm64, no Windows and no macOS, and the cache is not metered separately. Every cell below was read from the vendor's own public pricing or documentation page on 20 September 2026. Where a vendor does not publish a number, the cell says "not published" instead of carrying a guess. Comparison WarpBuild Latchkey Price per minute, 2 vCPU Linux x64 $0.004 (2 vCPU, 8 GB RAM) $0.0025 (2 vCPU, 8 GB RAM, 100 GB disk) Price per minute, 4 vCPU Linux x64 $0.008 (4 vCPU, 16 GB RAM) $0.005 (4 vCPU, 16 GB RAM, 100 GB disk) Bring your own cloud $0.002/min running in your own AWS, GCP or Azure account Not offered Free tier Not published. Sign-up needs no card until you reach a usage or credit limit 30-day trial, then 2,000 included minutes a month on Developer, 4,000 on Launch, 6,000 on Scale Billing granularity Per minute, in its own words: "CI runners are billed on a per-minute basis" Per minute Runner sizes and OS 2 to 32 vCPU on Linux x64 and arm64; Windows from 4 to 32 vCPU; macOS M4 Pro at 6 and 12 vCPU; Linux on Apple silicon at 6 and 12 vCPU 2, 4, 8 and 16 vCPU, Ubuntu 24.04 on x86_64 only. arm64, Windows and macOS are not offered Cache: type, limit, persistence Metered: $0.20 per GB-month of storage plus $0.0001 per write or restore; snapshot restore $0.04 a job and $0.025 an hour of snapshot storage Size not published. latchkey-dev/cache-action@v1 replaces actions/cache with a restore step and a save step, keeping path and key (no restore-keys); entries are isolated per organization, versioned by OS, kept 14 days; a failed restore warns rather than failing the job Docker build acceleration Remote BuildKit builders on their own meter, 16 vCPU at $0.06/min to 192 vCPU at $0.88, driven by Warpbuilds/build-push-action@v7; the shared cache is eventually consistent with a 10-day TTL latchkey-dev/docker-cache-action@v1, layers held in a Latchkey-managed private registry per organization Concurrency limits Advertised as unlimited job concurrency; the no-queue guarantee and an SLA behind it are Enterprise features Developer 20, Launch 40, Scale 80 busy runners per workspace; idle warm runners do not count Where jobs run WarpBuild's own cloud, or your AWS, GCP or Azure account under BYOC Latchkey's own AWS estate on m6a-class hosts, one job per runner, no inbound access, destroyed after the job Self-healing or retries Not offered Built in: transient failures are diagnosed and retried inside the run Setup change required Sign up, install the WarpBuild bot from its dashboard rather than the GitHub marketplace, then set runs-on to a runner ID such as warp-ubuntu-latest-x64-4x A runs-on label change. GitHub organization accounts only The BYOC rate is the real compar...

Powered by Readybot.io

Latchkey vs WarpBuild: GitHub Actions Runner Costs

GitHub Actions Matrix Exclude: Partial Match Explained

DEV Community

GitHub Actions matrix exclude not working, and the partial match

GitHub Actions matrix exclude not working is most often the opposite complaint from the one people arrive with: the entry matched more combinations than intended, not fewer. An exclude entry is a partial match, so naming one key removes every combination that agrees on that key, whatever the other keys hold. An exclude entry naming one key of two constrains only that key. Both combinations that agree on it are removed. What this error means The job count is wrong and nothing is annotated. Either several legs you wanted are gone, which is the partial match doing exactly what it is specified to do, or a leg you tried to remove is still there, which usually means it was not in the cross product to begin with. The tell in both directions is that the run succeeds and the job list is simply a different length than the arithmetic in your head. There is no message to search for, because nothing went wrong from the builder point of view. ```Illustrative matrix fragment, not a log line: this outcome produces no message matrix: os: [ubuntu-latest, windows-latest] node: [18, 20] exclude: - node: 18 two jobs remain, not three

Common causes

The exclude entry names fewer keys than the combination has

The dominant cause. One key named means one comparison, and every combination agreeing on that key is removed regardless of the others, which is usually a whole slice rather than a single job.

The entry names a value that is not in the cross product

A filter that matches nothing removes nothing, and it is not an error. This happens when a matrix value changes and the exclude entry describing it is not updated in the same change.

The job being excluded was created by an include entry

Configurations that include adds are produced after the exclusion pass has finished, so they are never tested against exclude at all. The only way to stop one is to change the include entry.

The matrix was changed and the count was never rechecked

In our experience this is how a wrong count survives for months. Adding one value to a dimension multiplies the product and can move which combinations an existing partial entry removes.

How to fix it

Name every key needed to identify the combination

To remove exactly one job, the entry has to constrain every dimension. Anything less is a slice, and the number of keys you name is the number of comparisons the builder creates.

```.github/workflows/ci.yml
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20]
exclude:
- os: windows-latest
node: 18
Print the matrix context and count the legs Add a step that serializes the matrix context to the log. Set fail fast to false so a single failure does not hide the rest of the expansion. Run it and compare the number of legs against the number you expected. Leave the step in place for whoever edits the matrix next. Change the include entry rather than excluding what it adds A configuration that only exists because include created it cannot be removed by exclude, because it is produced after the exclusion pass. Edit or delete the include entry instead. Build the matrix from include entries when the set is small If you want five specific combinations, list five include entries and define no dimensions to cross. Nothing has to be subtracted, so none of the subtleties on this page applies. strategy:
matrix:
include:
- os: ubuntu-latest
node: 20
- os: windows-latest
node: 22
How to prevent it Treat the key count in an exclude entry as the width of the filter, and check it. Keep a step that prints the matrix context permanently in any workflow with a matrix. Recheck the job count whenever a dimension gains or loses a value. Prefer an explicit list of include entries when the wanted set is small. An exclude entry constrains only the keys it names The published schema says it in one line: an excluded configuration only has to be a partial match for it to be excluded. The builder implements ...

Powered by Readybot.io

GitHub Actions Matrix Exclude: Partial Match Explained

Why C/C++ volatile Is Not Atomic in Multithreading

DEV Community

The Most Dangerous Keyword in C/C++: Why volatile Is Not Atomic

The Most Dangerous Keyword in C/C++: Why volatile Is Not Atomic One of the most persistent myths in systems programming is the belief that declaring a shared variable as volatile makes it safe for concurrent multi-threaded access. Consider the following multi-threaded counter: volatile int counter = 0;

void worker() {
for (int i = 0; i < 100000; i++) {
counter++; // DATA RACE & LOST UPDATES!
}
}
Running two threads executing worker() concurrently will almost never result in 200,000. Increments will be silently lost. To understand why, we must examine the difference between Compiler Optimizations and CPU Hardware Coherence. 1. What volatile Actually Does The volatile keyword was introduced into the C standard in 1989 for a single specific purpose: interacting with Memory-Mapped I/O (MMIO) hardware registers and asynchronous signal handlers. It communicates one instruction to the compiler: "Do not cache this variable inside a CPU register. Do not optimize away repeated reads or writes. Every read must emit a fresh load from memory." // Without volatile:
while (!flag) {} // Compiler may optimize to: if (!flag) while(1){}

// With volatile:
while (!flag) {} // Compiler emits a CMP instruction reading RAM every loop iteration
2. What volatile Fails to Provide Failure 1: Zero Atomicity The operation counter++ is not a single atomic hardware instruction. It decomposes into a 3-step sequence in assembly: mov eax, dword ptr [counter] ; 1. Read from memory into register
add eax, 1 ; 2. Increment register value
mov dword ptr [counter], eax ; 3. Write back to memory
If Thread 1 reads counter = 5, and is preempted before Step 3, Thread 2 can read 5, increment to 6, and write back. When Thread 1 resumes, it writes 6, completely obliterating Thread 2's increment. Failure 2: Zero Memory Barriers Modern superscalar CPUs (x86, ARM64) execute instructions out of order to maximize execution pipeline throughput. volatile imposes constraints on the compiler, but it emits zero hardware memory barrier instructions (like MFENCE or DMB). The CPU is free to reorder reads and writes around volatile variables across cache lines. 3. The Modern Solution: std::atomic For thread-safe operations in C and C++, never use volatile. Use the standard atomic types: #include

std::atomic counter{0};

void worker() {
for (int i = 0; i < 100000; i++) {
counter.fetch_add(1, std::memory_order_relaxed); // Hardware Atomic!
}
}
On x86, this compiles to the hardware instruction lock inc [counter], asserting the CPU bus lock and guaranteeing atomic execution. Read the full systems engineering breakdown at Binary Dose.

Powered by Readybot.io

Why C/C++ volatile Is Not Atomic in Multithreading