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

推荐订阅源

T
Threat Research - Cisco Blogs
博客园 - 聂微东
小众软件
小众软件
P
Proofpoint News Feed
Security Archives - TechRepublic
Security Archives - TechRepublic
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
TaoSecurity Blog
TaoSecurity Blog
博客园 - 司徒正美
罗磊的独立博客
N
News and Events Feed by Topic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security Affairs
S
Security @ Cisco Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
月光博客
月光博客
S
Secure Thoughts
P
Proofpoint News Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Forbes - Security
Forbes - Security
H
Heimdal Security Blog
W
WeLiveSecurity
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
L
LangChain Blog
T
The Blog of Author Tim Ferriss
NISL@THU
NISL@THU
Google DeepMind News
Google DeepMind News
Cloudbric
Cloudbric
H
Hacker News: Front Page
The Last Watchdog
The Last Watchdog
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cisco Blogs
博客园 - 三生石上(FineUI控件)
博客园_首页
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Schneier on Security
Project Zero
Project Zero
SecWiki News
SecWiki News
爱范儿
爱范儿
The Register - Security
The Register - Security
AI
AI
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Y
Y Combinator Blog
L
Lohrmann on Cybersecurity
Application and Cybersecurity Blog
Application and Cybersecurity Blog
P
Privacy International News Feed
J
Java Code Geeks
S
Securelist
C
Cyber Attacks, Cyber Crime and Cyber Security
V
Visual Studio Blog

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
Ship a Changelog Without Writing One: Conventional Commits + Semantic Release
The Practica · 2026-06-27 · via The Practical Developer

Every project starts with a CHANGELOG.md handwritten by the author. Six months and three maintainers later, the changelog says “various bug fixes and improvements” and nobody knows what was actually released. Version numbers get bumped by whoever remembers to do it, and half the time the tag doesn’t match what’s on npm.

This is not sustainable. It’s also completely solvable.

Conventional commits plus semantic-release automates all of it. Write commits in a structured format, push to your main branch, and let CI handle version calculation, changelog generation, git tags, and npm publication. No human judgment calls, no forgotten steps, no “we’ll write the changelog later.”

The real problem with manual releases

Before we get to the setup, let’s be honest about why most release processes collapse.

Version bumps are arbitrary. Did that last PR fix a bug or add a feature? Is it a patch or a minor? Without a shared definition, two maintainers will answer differently. The package.json version drifts, and suddenly ^1.4.2 resolves differently on different machines.

Changelogs rot fast. A well-meaning CHANGELOG.md with release dates and bullet points turns into “Various improvements and bug fixes” by the third entry. Nobody wants to write it, nobody enforces it, and eventually it becomes noise.

Tags drift from reality. The v1.5.0 tag doesn’t match what’s published to npm. The latest tag points to something three releases old. Production is running 1.4.2 but nobody can tell you what 1.5.0 actually contains.

Context switching kills momentum. Releasing should be a push-button event. Instead, it’s a 20-minute manual process that involves checking diffs, writing release notes, running tests again just in case, and typing npm version minor && npm publish && git push --tags while hoping nothing breaks.

A formalized commit convention and an automated release tool removes every one of these failure modes.

What is a conventional commit?

The Conventional Commits specification is simple. Every commit message follows this structure:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

The most common types are:

  • feat:: a new feature (triggers a minor version bump)
  • fix:: a bug fix (triggers a patch version bump)
  • feat!: or fix!: with a BREAKING CHANGE footer: a breaking change (triggers a major version bump)
  • chore:, docs:, refactor:, style:, test:, ci:, perf:: no version bump, excluded from changelog

A breaking change can also be indicated with BREAKING CHANGE: in the footer or by adding ! after the type.

Here’s what real commits look like:

feat(api): add pagination headers to list endpoint

Implements RFC 5988 Link headers for cursor-based pagination.
Closes #142.

feat(web)!: redesign user dashboard

BREAKING CHANGE: The dashboard API no longer returns deprecated
`user.profile` fields. Use `user.preferences` instead.

fix(cache): handle null TTL values in Redis adapter

The adapter now defaults to 300 seconds when TTL is null
instead of throwing an error.

The key insight: these messages are parseable. A tool can read the commit log between two tags, classify every change by type, and determine the next version number automatically based on semver rules.

Setting up commitlint

The hardest part of adopting conventional commits is getting everyone on the team to write them. You need enforcement at the commit level, not just in PR descriptions.

Commitlint validates commit messages against the conventional commit format. Hook it into husky and it rejects non-conforming messages before they reach the repository.

Install the dependencies:

npm install --save-dev @commitlint/cli @commitlint/config-conventional husky

Create a commitlint.config.js in your project root:

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'scope-enum': [2, 'always', ['api', 'web', 'cache', 'cli', 'docs']],
    'header-max-length': [2, 'always', 100],
  },
};

The scope-enum rule is optional but useful. It restricts scopes to a known list, which keeps the changelog organized. Without it, you get scopes like "misc fixes" and "stuff" that defeat the purpose.

Set up the husky hook:

npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

Test it. Try committing a message like "fixed stuff":

⧗   input: fixed stuff
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]
✖   found 2 problems, 0 warnings

Now try a valid message:

feat(cli): add --dry-run flag to deploy command
✔   found 0 problems, 0 warnings

That’s it. From this point, every commit message in your repository conforms to a parseable format. The data you need for automated releases is guaranteed to exist.

A note on adoption. Adding commitlint to an existing project mid-cycle is disruptive. Do it at a natural boundary (after a release, or at the start of a sprint). Communicate the change in standup. The friction lasts about three days, then it becomes muscle memory.

Semantic-release: the automation layer

Commitlint enforces the input. Semantic-release consumes that input and produces the output: version numbers, changelogs, tags, and publish events.

It works by looking at the git log since the last release tag. It classifies every commit using conventional commit rules, calculates the next semver version, generates a changelog entry, creates a git tag, and optionally publishes to npm, GitHub Releases, or Docker registries.

Installation and base config

npm install --save-dev semantic-release @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm @semantic-release/github @semantic-release/git

Create release.config.js:

// release.config.js
module.exports = {
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    '@semantic-release/npm',
    [
      '@semantic-release/git',
      {
        assets: ['package.json', 'package-lock.json'],
        message: 'chore(release): ${nextRelease.version}\n\n${nextRelease.notes}',
      },
    ],
    '@semantic-release/github',
  ],
};

Here’s what each plugin does:

  • commit-analyzer: reads the commit log since the last tag and determines the next version (major, minor, or patch).
  • release-notes-generator: builds a formatted changelog from commit messages, grouped by type.
  • @semantic-release/npm: updates package.json version and publishes to npm.
  • @semantic-release/git: commits the version bump and changelog back to the repository.
  • @semantic-release/github: creates a GitHub Release with the changelog text.

The CI workflow

Semantic-release should never run locally. It needs access to secrets (npm tokens, GitHub tokens) and it modifies the repository. Run it only in CI, on the main branch.

Here’s a GitHub Actions workflow that does everything:

# .github/workflows/release.yml
name: Release
on:
  push:
    branches: [main]

permissions:
  contents: write
  issues: write
  pull-requests: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci

      - run: npm test

      - name: Semantic Release
        uses: cycjimmy/semantic-release-action@v4
        with:
          semantic_version: 24
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Three things worth calling out:

fetch-depth: 0 is mandatory. Semantic-release needs the full git history including tags to calculate the correct version. With the default shallow clone, it sees only the latest commit and can’t determine what changed since the last release.

persist-credentials: false prevents the default token from interfering with semantic-release’s own git operations.

secrets.NPM_TOKEN: if you’re publishing to npm, create an automation token in your npm account (not a publish token) and add it to GitHub Secrets. Automation tokens can’t be used to log into the npm website, which limits the blast radius if leaked.

First release

The first time you push to main with this workflow, semantic-release will look for a git tag. If there isn’t one, it starts from commit 1. The version will be 1.0.0 by default.

Run git tag v1.0.0 && git push origin v1.0.0 to set a starting point if you want to begin from a known version. After that, every push to main with a feat or fix commit triggers a release.

What the output looks like

After the workflow runs, several things happen automatically.

The package.json version gets updated:

{
  "name": "my-package",
  "version": "1.2.0"
}

A git tag is created:

v1.2.0

A GitHub Release appears with markdown (this came from actual commit messages):

## 1.2.0 (2026-06-27)

### Features
* **api:** add pagination headers to list endpoint (#142)
* **cli:** add --dry-run flag to deploy command (#138)

### Bug Fixes
* **cache:** handle null TTL values in Redis adapter (#140)
* **api:** validate email format before sending confirmation (#136)

And the commit message about the version bump itself:

chore(release): 1.2.0

# Auto-generated by semantic-release. Do not edit.

The release notes stay in GitHub Releases. You can also generate a CHANGELOG.md file if you want one committed to the repo, but GitHub Releases serves the same purpose for most teams and avoids merge conflicts on the changelog file during concurrent PRs.

Handling pre-release and maintenance branches

Not everything ships from main. Bug fix releases for an older major version, or pre-release channels like next or beta, need a separate configuration.

Add additional branches to the config:

// release.config.js
module.exports = {
  branches: [
    'main',
    {name: 'next', channel: 'next', prerelease: true},
    {name: '1.x', range: '1.x', channel: '1.x'},
  ],
  // plugins unchanged
};

With this config:

  • Pushing to main releases to the latest npm tag with a full semver bump.
  • Pushing to next releases to the next npm tag with a pre-release version like 2.0.0-next.1.
  • Pushing to 1.x (with a fix commit) releases a patch on the 1.x channel.

The prerelease flag appends a counter. The range flag restricts the version range for maintenance branches.

Common pitfalls and how to avoid them

”It published a major version for a typo fix”

This happens when a commit message includes a breaking change annotation accidentally (usually a ! in the scope or a BREAKING CHANGE footer that was meant for the body).

# This looks like a feature but might be parsed incorrectly
git commit -m "feat!: fix typo in readme"   # Wrong! The ! means breaking change

The ! goes right after the type/scope, not after the colon. If feat!: is a breaking change, fix!: is also a breaking change. Use them deliberately. In practice, you won’t see many breaking changes per month. If you’re writing feat!: daily, you’re misusing the convention.

Merge commits and squash merges

GitHub’s default merge commit messages are verbose and not formatted as conventional commits. This causes semantic-release to see commits like Merge pull request #142 from user/feature-x and classify them as non-feature/non-fix (which is fine, they’re ignored). But it can also cause duplicate entries in the changelog.

The fix: use squash merges on your repository. Set the default commit message for squash merges to the PR title. If your PR titles follow conventional commit format, the squashed commit will be a single well-formed message. This keeps the git history clean and the changelog deduplicated.

Configure this in your repository settings: Settings > Pull Requests > Allow squash merging > Default commit message: PR title.

npm two-factor authentication

If your npm account has 2FA enabled for publish, semantic-release will fail because it can’t prompt for a code. Create an automation token in your npm account settings (not a publish token). Automation tokens bypass 2FA for publish operations but cannot be used to log in to the website.

# Create automation token at https://www.npmjs.com/settings/<user>/tokens
# Add to GitHub: Settings > Secrets and variables > Actions > NPM_TOKEN

Monorepos

Semantic-release works with monorepos, but you need one configuration per package. The @semantic-release/npm plugin accepts a pkgRoot option to point at a specific workspace:

'@semantic-release/npm': {
  pkgRoot: 'packages/core',
},

For larger monorepos with many interdependent packages, consider tools like multi-semantic-release or nx-integrated release management. The basic semantic-release setup starts to struggle when packages share release cycles.

When you might not need this

Semantic-release adds complexity. If any of these apply to you, a manual release process is probably fine:

  • Solo project with one user. You don’t need automation for a single-person project. The coordination problems don’t exist.
  • Infrequent releases. If you ship a new version once a quarter, the overhead of setting up commitlint and semantic-release exceeds the manual effort.
  • Dual npm and Docker publication. Semantic-release can handle both, but the configuration gets more involved. Consider whether the automation saves you more time than it costs to maintain.

For any project with two or more developers shipping weekly or more often, the automation pays for itself in about two release cycles.

A note from Yojji

Automating the release pipeline removes a major source of human error from the software delivery process. The kind of CI/CD discipline that treats versioning, changelogs, and publication as code-managed concerns rather than manual chores is exactly the engineering practice that Yojji’s teams bring to their client projects. Yojji is an international custom software development company with teams across Europe, the US, and the UK, specializing in the JavaScript ecosystem, cloud platforms, and full-cycle product engineering, including the release automation workflows that keep deployments predictable and traceable.