From fd111407e973cbb74352a51bae9fe41de7b17d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LiShuai=20=28=E9=98=BF=E6=9C=A8=29?= Date: Wed, 25 Feb 2026 21:40:27 +0800 Subject: [PATCH 1/5] docs: remove dead Job Board links (#1926) ## Checklist - [x] I have ensured my pull request is not behind the main or master branch of the original repository. - [x] I have rebased all commits where necessary so that reviewing this pull request can be done without having to merge it first. - [x] I have written a commit message that passes commitlint linting. - [x] I have ensured that my code changes pass linting tests. - [x] I have ensured that my code changes pass unit tests. - [x] I have described my pull request and the reasons for code changes along with context if necessary. ## Summary - Remove the "Job Board" section from README as all links are dead (404) ## Details The `astro.netlify.com` service was added by TJ Holowaychuk in April 2017 as a private job board sponsorship service for open source projects. This service is no longer operational - all URLs return 404 errors. The affected links pointed to: - Automattic - Segment - Auth0 The Backers and Sponsors sections using OpenCollective remain functional and are preserved. ## Test plan - [x] Verified all `astro.netlify.com` URLs return 404 - [x] Confirmed OpenCollective links still work Issues: [[docs] Job Board three link and image not show in readme #1911](https://github.com/koajs/koa/issues/1911) --- Readme.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Readme.md b/Readme.md index 92d600365..689d4abeb 100644 --- a/Readme.md +++ b/Readme.md @@ -189,14 +189,6 @@ See [AUTHORS](AUTHORS). - [中文文档 v2.x](https://github.com/demopark/koa-docs-Zh-CN) - __[#koajs]__ on freenode -## Job Board - -Looking for a career upgrade? - - - - - ## Backers Support us with a monthly donation and help us continue our activities. From 3b0508e8d000fdab6be845255f337f7db4644aab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:40:47 +0800 Subject: [PATCH 2/5] build(deps-dev): bump qs from 6.14.1 to 6.14.2 (#1927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
Changelog

Sourced from qs's changelog.

6.14.2

  • [Fix] parse: mark overflow objects for indexed notation exceeding arrayLimit (#546)
  • [Fix] arrayLimit means max count, not max index, in combine/merge/parseArrayValue
  • [Fix] parse: throw on arrayLimit exceeded with indexed notation when throwOnLimitExceeded is true (#529)
  • [Fix] parse: enforce arrayLimit on comma-parsed values
  • [Fix] parse: fix error message to reflect arrayLimit as max index; remove extraneous comments (#545)
  • [Robustness] avoid .push, use void
  • [readme] document that addQueryPrefix does not add ? to empty output (#418)
  • [readme] clarify parseArrays and arrayLimit documentation (#543)
  • [readme] replace runkit CI badge with shields.io check-runs badge
  • [meta] fix changelog typo (arrayLengtharrayLimit)
  • [actions] fix rebase workflow permissions
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=qs&package-manager=npm_and_yarn&previous-version=6.14.1&new-version=6.14.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/koajs/koa/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b5e60cb9..a62586064 100644 --- a/package-lock.json +++ b/package-lock.json @@ -431,7 +431,6 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1448,7 +1447,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -1656,7 +1654,6 @@ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -1724,7 +1721,6 @@ "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "builtins": "^5.0.1", "eslint-plugin-es": "^4.1.0", @@ -1751,7 +1747,6 @@ "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -1768,7 +1763,6 @@ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -3910,9 +3904,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { From d3ea8bf9649d164b40c448b5ce0b40306b4dca90 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:53:24 +0800 Subject: [PATCH 3/5] chore: Add workflow_dispatch trigger to npm-publish workflow (#1930) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables manual triggering of the npm publish workflow with a specific tag input, so tags created from non-default branches (e.g. `v2.x`) can be published. - Added `workflow_dispatch` trigger with a required `tag` string input - Updated checkout step to use `inputs.tag || github.ref` — manual runs check out the specified tag, tag-push runs retain existing behavior Note: GitHub Actions `workflow_dispatch` does not support dynamic dropdowns, so the tag is a free-text input field. --- 🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. [Learn more about Advanced Security.](https://gh.io/cca-advanced-security) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fengmk2 <156269+fengmk2@users.noreply.github.com> --- .github/workflows/npm-publish.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index b639d8d3d..8e9dda380 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,6 +1,8 @@ name: NPM Publish -# Trigger only when tags matching semver format are pushed +# Trigger when tags matching semver format are pushed, or manually via workflow_dispatch. +# Manual triggers allow selecting a specific tag to publish (e.g. tags from the v2.x branch). +# # Patterns match common semver formats: # - v1.0.0 (standard) # - v1.0.0-alpha (pre-release) @@ -16,6 +18,12 @@ name: NPM Publish - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-[a-zA-Z0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-[a-zA-Z0-9]+.[0-9]+' + workflow_dispatch: + inputs: + tag: + description: 'Git tag to checkout and publish (e.g. v2.15.4)' + required: true + type: string # Permissions for NPM trusted publishing with provenance permissions: @@ -27,6 +35,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + with: + ref: ${{ inputs.tag || github.ref }} - name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 #v6 From 2503a1fb14ff3dbed9b55ea1cd6419be739ff150 Mon Sep 17 00:00:00 2001 From: killa Date: Sat, 28 Mar 2026 13:37:53 +0800 Subject: [PATCH 4/5] feat: defer AsyncLocalStorage creation for v8 startup snapshots (#1946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Defer `AsyncLocalStorage` creation when `v8.startupSnapshot.isBuildingSnapshot()` is true, making Koa compatible with Node.js startup snapshots - Register a `v8.startupSnapshot.addDeserializeCallback` to properly initialize `ctxStorage` after snapshot restoration - Extract `getAsyncLocalStorage()` helper to consolidate creation logic ## Test plan - [x] All 429 existing tests pass with 0 failures - [x] `currentContext` tests verify both `asyncLocalStorage: true` and custom `AsyncLocalStorage` instance paths work correctly - [x] Normal (non-snapshot) code path is unchanged — `v8.startupSnapshot?.isBuildingSnapshot?.()` returns `undefined` in regular execution 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- __tests__/application/currentContext.test.js | 84 +++++++++++++++++++- lib/application.js | 17 +++- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/__tests__/application/currentContext.test.js b/__tests__/application/currentContext.test.js index 78f6fd315..962878733 100644 --- a/__tests__/application/currentContext.test.js +++ b/__tests__/application/currentContext.test.js @@ -1,6 +1,7 @@ 'use strict' -const { describe, it } = require('node:test') +const { describe, it, beforeEach, afterEach } = require('node:test') +const v8 = require('node:v8') const request = require('supertest') const assert = require('node:assert/strict') const Koa = require('../..') @@ -113,4 +114,85 @@ describe('app.currentContext', () => { await request(app.callback()).get('/').expect('ok') assert(app.currentContext === undefined) }) + + describe('v8 startup snapshot', () => { + let originalStartupSnapshot + + beforeEach(() => { + originalStartupSnapshot = v8.startupSnapshot + }) + + afterEach(() => { + v8.startupSnapshot = originalStartupSnapshot + }) + + it('should defer AsyncLocalStorage creation when building snapshot', () => { + let deserializeCallback + v8.startupSnapshot = { + isBuildingSnapshot: () => true, + addDeserializeCallback: (cb, data) => { + deserializeCallback = { cb, data } + } + } + + const app = new Koa({ asyncLocalStorage: true }) + assert.strictEqual(app.ctxStorage, null) + assert(deserializeCallback, 'deserialize callback should be registered') + + // simulate snapshot deserialization + deserializeCallback.cb(deserializeCallback.data) + assert(app.ctxStorage instanceof AsyncLocalStorage) + }) + + it('should defer with custom AsyncLocalStorage when building snapshot', () => { + const customStorage = new AsyncLocalStorage() + let deserializeCallback + v8.startupSnapshot = { + isBuildingSnapshot: () => true, + addDeserializeCallback: (cb, data) => { + deserializeCallback = { cb, data } + } + } + + const app = new Koa({ asyncLocalStorage: customStorage }) + assert.strictEqual(app.ctxStorage, null) + + // simulate snapshot deserialization + deserializeCallback.cb(deserializeCallback.data) + assert(app.ctxStorage instanceof AsyncLocalStorage) + assert.strictEqual(app.ctxStorage, customStorage) + }) + + it('should work normally after deserialization', async () => { + let deserializeCallback + v8.startupSnapshot = { + isBuildingSnapshot: () => true, + addDeserializeCallback: (cb, data) => { + deserializeCallback = { cb, data } + } + } + + const app = new Koa({ asyncLocalStorage: true }) + + // simulate snapshot deserialization + deserializeCallback.cb(deserializeCallback.data) + + app.use(async ctx => { + assert(ctx === app.currentContext) + ctx.body = 'ok' + }) + + await request(app.callback()).get('/').expect('ok') + assert(app.currentContext === undefined) + }) + + it('should not defer when not building snapshot', () => { + v8.startupSnapshot = { + isBuildingSnapshot: () => false + } + + const app = new Koa({ asyncLocalStorage: true }) + assert(app.ctxStorage instanceof AsyncLocalStorage) + }) + }) }) diff --git a/lib/application.js b/lib/application.js index cd6504496..5a0a97208 100644 --- a/lib/application.js +++ b/lib/application.js @@ -4,6 +4,7 @@ * Module dependencies. */ const util = require('node:util') +const v8 = require('node:v8') const debug = util.debuglog('koa:application') const Emitter = require('node:events') const Stream = require('node:stream') @@ -40,6 +41,13 @@ const only = require('./only.js') * Inherits from `Emitter.prototype`. */ +function getAsyncLocalStorage (options) { + if (options.asyncLocalStorage instanceof AsyncLocalStorage) { + return options.asyncLocalStorage + } + return new AsyncLocalStorage() +} + module.exports = class Application extends Emitter { /** * Initialize a new `Application`. @@ -81,10 +89,13 @@ module.exports = class Application extends Emitter { this[util.inspect.custom] = this.inspect } if (options.asyncLocalStorage) { - if (options.asyncLocalStorage instanceof AsyncLocalStorage) { - this.ctxStorage = options.asyncLocalStorage + if (v8.startupSnapshot?.isBuildingSnapshot?.()) { + this.ctxStorage = null + v8.startupSnapshot.addDeserializeCallback(({ app, options }) => { + app.ctxStorage = getAsyncLocalStorage(options) + }, { app: this, options }) } else { - this.ctxStorage = new AsyncLocalStorage() + this.ctxStorage = getAsyncLocalStorage(options) } } } From e0ba8ef39d27fe5dae5492f9fe753d155124f994 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 28 Mar 2026 13:39:25 +0800 Subject: [PATCH 5/5] 3.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a62586064..5d3826354 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "koa", - "version": "3.1.2", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "koa", - "version": "3.1.2", + "version": "3.2.0", "license": "MIT", "dependencies": { "accepts": "^1.3.8", diff --git a/package.json b/package.json index ee869841a..81db52f43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "koa", - "version": "3.1.2", + "version": "3.2.0", "description": "Koa web app framework", "main": "lib/application.js", "exports": {