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

推荐订阅源

WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
P
Proofpoint News Feed
小众软件
小众软件
Recent Announcements
Recent Announcements
博客园 - 三生石上(FineUI控件)
Security Archives - TechRepublic
Security Archives - TechRepublic
W
WeLiveSecurity
Cloudbric
Cloudbric
博客园 - 司徒正美
美团技术团队
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
PCI Perspectives
PCI Perspectives
宝玉的分享
宝玉的分享
H
Help Net Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
Help Net Security
Help Net Security
Last Week in AI
Last Week in AI
S
Schneier on Security
N
News | PayPal Newsroom
B
Blog RSS Feed
L
LINUX DO - 最新话题
T
Troy Hunt's Blog
S
Secure Thoughts
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
L
Lohrmann on Cybersecurity
G
Google Developers Blog
Microsoft Azure Blog
Microsoft Azure Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tenable Blog
S
Securelist
L
LangChain Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
I
InfoQ
H
Heimdal Security Blog
Cisco Talos Blog
Cisco Talos Blog
F
Full Disclosure
Y
Y Combinator Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
K
Kaspersky official blog
T
Tailwind CSS Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
C
Cisco Blogs

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Publishing One Package to Five Registries with GitHub Actions
Iurii Rogulia · 2026-06-03 · via DEV Community

Every developer building for European markets hits the same wall eventually. EU VAT rates are public data — the European Commission publishes them — but getting that data into your codebase reliably is a mess. You hardcode the rates, they change, your invoices are wrong. You scrape a website, the HTML structure changes, your scraper breaks. You pay $50–200/month for vatstack or vatlayer to access data the EC gives away for free.

I built eu-vat-rates-data to fix this: a free, open-source dataset of EU VAT rates, published as native packages for npm, PyPI, Go Module, RubyGems, and Packagist — updated daily from the official source, with zero manual steps in the publishing pipeline. The full project overview is in the eu-vat-rates-data project card.

This is how it works, and what I learned building a single dataset across five language ecosystems.

Why Five Registries

The first question I get: why not just publish JSON and let developers fetch it themselves?

Because that turns every consumer into a maintenance burden. They have to write the fetch logic, handle errors, decide on caching, deal with network failures at build time, and parse the response format. A native package means import { getRate } from "eu-vat-rates-data" — the data is bundled, typed, and versioned. No network calls, no parsing, no surprises.

The second question: why not just npm? Because not every EU-facing project is JavaScript. The finance team's tooling might be PHP (common in European accounting software). The microservice doing VAT calculation might be Go. The data pipeline might be Python. Publishing to one registry excludes everyone else.

So: five registries. Same data, same logical API, each package idiomatic for its ecosystem.

The Source of Truth

The data comes from the European Commission TEDB — Taxes in Europe Database — a SOAP web service at ec.europa.eu/taxation_customs/tedb/ws/. Each day, a Python script sends a typed XML request for all 28 countries (EU-27 plus UK) with situationOn set to today's date:

soap_body = f"""<v1:retrieveVatRatesReqMsg>
  <types:memberStates>
    {''.join(f'<types:isoCode>{c}</types:isoCode>' for c in COUNTRY_CODES)}
  </types:memberStates>
  <types:situationOn>{today}</types:situationOn>
</v1:retrieveVatRatesReqMsg>"""

Enter fullscreen mode Exit fullscreen mode

The response is parsed into a canonical JSON structure stored in data/eu-vat-rates-data.json. This file is the single source of truth — all five packages read from it.

A few non-obvious edge cases the script handles:

Greece uses EL in TEDB, not the ISO standard GR. The response comes back with EL codes. Explicit mapping: TEDB_TO_ISO = {"EL": "GR"}. Miss this and your Greek VAT lookups will silently return nothing.

The UK is not in TEDB after Brexit. GB rates (20% standard, 5% reduced) are hardcoded as a static fallback in the script and updated manually when Westminster changes them.

TEDB returns non-numeric rate types. EXEMPTED, OUT_OF_SCOPE, NOT_APPLICABLE — all filtered out. Only positive floats make it into the dataset.

France, Portugal, and Spain have territorial special rates that appear as duplicates in the SOAP response. Aggregated into a Python set() before writing.

SOAP namespace stripping is manual. XML tags arrive as {urn:ec.europa.eu:taxud:tedb:services:types}vatRateResults. Stripped with el.tag.split("}")[-1] — no XML library that handles namespaces automatically because we want to keep the script dependency-free.

Repository Architecture

The JavaScript/TypeScript package is the primary repo: vatnode/eu-vat-rates-data. It owns the Python ingestion script, the canonical JSON file, and the npm publishing workflow.

The other four language packages are separate repositories that each pull the canonical JSON directly from the primary repo:

vatnode/eu-vat-rates-data          ← source of truth, npm
vatnode/eu-vat-rates-data-python   ← PyPI
vatnode/eu-vat-rates-data-php      ← Packagist
vatnode/eu-vat-rates-data-go       ← Go Module (pkg.go.dev)
vatnode/eu-vat-rates-data-ruby     ← RubyGems

Enter fullscreen mode Exit fullscreen mode

I considered a monorepo but rejected it. The Go and Ruby toolchains expect certain directory structures and version tagging conventions that conflict with each other and with the JavaScript toolchain. Separate repos with separate workflows is more work to set up but far less brittle to maintain.

The Publishing Workflow

Primary Repo (JavaScript, runs at 07:00 UTC)

# .github/workflows/update-rates.yml
name: Update VAT Rates

on:
  schedule:
    - cron: "0 7 * * *"
  workflow_dispatch:

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Fetch rates from EC TEDB
        run: python scripts/fetch-rates.py

      - name: Check for rate changes
        id: diff
        run: |
          # Compare rates only, not the version date field
          python scripts/compare-rates.py
          echo "changed=$?" >> $GITHUB_OUTPUT

      - name: Bump version and publish
        if: steps.diff.outputs.changed == '1'
        run: |
          npm version patch --no-git-tag-version
          npm run build
          npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Commit and push
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add data/eu-vat-rates-data.json package.json
          git diff --staged --quiet || git commit -m "chore: update VAT rates $(date +%Y-%m-%d)"
          git push

Enter fullscreen mode Exit fullscreen mode

The key design decision: the workflow checks whether the actual rates changed, not just whether the date changed. compare-rates.py extracts the rates object from both the old and new JSON and does a deep comparison — if the numbers are identical, no version bump happens and the downstream repos won't trigger either. This prevents a flood of patch versions on days when nothing changed.

Dependent Repos (run at 08:00 UTC, one hour later)

Each dependent repo has a simple workflow that fetches the canonical JSON from the primary repo via curl and runs the publish script:

# .github/workflows/sync.yml (Python example)
- name: Fetch canonical data
  run: |
    curl -sL https://raw.githubusercontent.com/vatnode/eu-vat-rates-data/main/data/eu-vat-rates-data.json \
      -o eu_vat_rates_data/data.json

- name: Check for changes
  id: diff
  run: python scripts/compare_versions.py

- name: Publish to PyPI
  if: steps.diff.outputs.changed == '1'
  run: |
    python -m build
    python -m twine upload dist/*
  env:
    TWINE_USERNAME: __token__
    TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}

Enter fullscreen mode Exit fullscreen mode

The one-hour delay is intentional: the primary repo's workflow needs to commit the updated JSON before the dependent repos try to fetch it. GitHub Actions schedule times are approximate (can be up to 15 minutes late), so one hour gives comfortable margin.

Language-Specific Implementation Details

All five packages expose the same logical API: getRate(countryCode), getStandardRate(countryCode), isEUMember(countryCode), dataVersion. But the implementation differs by ecosystem idioms.

TypeScript — Overloaded Signatures for Type Safety

The TypeScript package uses overloaded function signatures so the return type narrows based on whether you pass a known CountryCode or a plain string:

// packages/js/src/index.ts
export type CountryCode =
  | "AT"
  | "BE"
  | "BG"
  | "CY"
  | "CZ"
  | "DE"
  | "DK"
  | "EE"
  | "ES"
  | "FI"
  | "FR"
  | "GB"
  | "GR"
  | "HR"
  | "HU"
  | "IE"
  | "IT"
  | "LT"
  | "LU"
  | "LV"
  | "MT"
  | "NL"
  | "PL"
  | "PT"
  | "RO"
  | "SE"
  | "SI"
  | "SK";

export function getRate(countryCode: CountryCode): VatRate;
export function getRate(countryCode: string): VatRate | undefined;
export function getRate(countryCode: string): VatRate | undefined {
  return data.rates[countryCode as CountryCode];
}

// isEUMember is a type guard — narrows string to CountryCode
export function isEUMember(code: string): code is CountryCode {
  return code in data.rates;
}

Enter fullscreen mode Exit fullscreen mode

This means if you've already called isEUMember, TypeScript knows the subsequent getRate call returns VatRate, not VatRate | undefined. The user doesn't need a null check they're going to forget anyway.

Published as dual CJS + ESM with .d.ts declarations so it works in both Node.js and browser bundlers.

Go — Embedded Data, No File I/O at Runtime

Go uses //go:embed to bundle the JSON into the binary at compile time:

// eu_vat_rates.go
import _ "embed"

//go:embed data/eu-vat-rates-data.json
var rawData []byte

var dataset Dataset

func init() {
    if err := json.Unmarshal(rawData, &dataset); err != nil {
        panic("eu-vat-rates-data: failed to parse embedded data: " + err.Error())
    }
}

// Nullable fields use pointer types — standard Go pattern
type VatRate struct {
    Country      string    `json:"country"`
    Currency     string    `json:"currency"`
    Standard     float64   `json:"standard"`
    Reduced      []float64 `json:"reduced"`
    SuperReduced *float64  `json:"super_reduced"` // nil if not applicable
    Parking      *float64  `json:"parking"`        // nil if not applicable
}

Enter fullscreen mode Exit fullscreen mode

The init() panic on parse failure is intentional. If the embedded JSON is somehow corrupted at build time, you want to find out immediately — not at the moment a production service tries to look up a VAT rate at 2am.

Python — importlib.resources for Wheel Compatibility

The Python package uses importlib.resources.files() to locate the bundled JSON, not __file__-relative path construction:

# eu_vat_rates_data/__init__.py
import importlib.resources
import json
from typing import TypedDict, Optional

class VatRate(TypedDict):
    country: str
    currency: str
    standard: float
    reduced: list[float]
    super_reduced: Optional[float]
    parking: Optional[float]

def _load_data() -> dict:
    ref = importlib.resources.files("eu_vat_rates_data") / "data.json"
    with importlib.resources.as_file(ref) as path:
        with open(path) as f:
            return json.load(f)

_dataset = _load_data()

Enter fullscreen mode Exit fullscreen mode

The __file__-relative approach (os.path.join(os.path.dirname(__file__), "data.json")) breaks inside zip archives and certain wheel installations. importlib.resources is the correct modern approach — it works in all packaging scenarios including frozen executables.

slug="automation-workflows"
text="Need a zero-touch publish pipeline, scheduled data sync, or multi-registry release automation? I build these as part of the automation work I do for EU-facing products."
/>

Versioning Strategy

Version format: YYYY.M.D — for example 2026.3.15. If the workflow runs twice on the same day (manual trigger plus scheduled), a counter suffix: 2026.3.15.1, 2026.3.15.2.

I deliberately chose date-based versioning rather than semantic versioning for this package. The "change" is always data, never API. There are no breaking changes in the API across versions — the function signatures are stable. So semver's major.minor.patch distinction is meaningless here. A date version tells users exactly when the data was last updated without them having to read a changelog.

One consequence: the dependent repos need to match the version before publishing. Each dependent workflow parses the version string from the primary repo's package.json, checks if it already published that version, and skips if so.

Gotchas From Production

The TEDB SOAP endpoint is occasionally unavailable. Around EU public holidays, it returns 503. The fetch script has a retry loop — three attempts with 60-second sleep between them — before it fails the workflow. A failed workflow generates a GitHub Actions email notification. The data from the previous successful run remains valid; most EU VAT rate changes happen on January 1st or a country-specific date, not randomly.

GitHub Actions schedule is not precise. The 07:00 UTC trigger might fire at 07:12. The 08:00 UTC trigger for dependent repos might fire at 07:58. When I originally set the gap at 30 minutes, I got intermittent failures where the Python workflow pulled the old JSON. One hour is the safe minimum.

Packagist (PHP) requires a composer.json version field that matches the git tag. The other registries accept a version number in a config file without a corresponding tag. For Packagist, I have to tag the commit — v2026.3.15 — before the composer.json version bump is visible. The workflow creates the tag first, then commits.

PyPI and npm reject re-publishing the same version. This means if the publish step fails for a transient reason (network timeout, registry hiccup), re-running the workflow will fail again at publish because the version already exists. I handle this by catching the specific "version already exists" error and treating it as success — if it published, the goal is achieved.

Results

Metric Value
Packages published 5 (npm, PyPI, Packagist, Go, RubyGems)
Countries covered 28 (EU-27 + UK)
Rate types 4 (standard, reduced, super_reduced, parking)
Update frequency Daily (automated)
Manual intervention Zero (unless EC TEDB changes its API)
Publishing cost €0/month

The git history of data/eu-vat-rates-data.json is a side-effect that turned out to be genuinely useful: it's a complete, automatically maintained audit trail of every EU VAT rate change since the project launched. Finland's rate changed from 24% to 25.5% in September 2024 — there's a commit for it with the exact date.


If you're building a product that handles EU pricing, VAT calculation, or tax compliance, the package is free to use. github.com/vatnode/eu-vat-rates-data.

If you need a developer who understands EU VAT compliance at the code level — from VIES validation to multi-country rate handling to accurate invoice generation — the production VAT SaaS is vatnode.dev. For automation workflows or API integrations that feed data between systems reliably — get in touch. I'm available for freelance projects and long-term engagements.


Related projects: eu-vat-rates-data open source dataset — the project described in this article. vatnode.dev VAT Validation API — production SaaS built on top of this dataset.

Related reading: Data Format Comparison: JSON, YAML, TOML, CSV, Protobuf — the format decisions that underpin a multi-ecosystem package. | UUID v7 in Production: Why Your Database Hates v4 — the identifier strategy used in the vatnode SaaS that consumes this dataset.