From 603080bb815e21dde0d547c992f6ad4b80068d07 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Thu, 27 Aug 2026 20:12:48 +0000 Subject: [PATCH 1/5] fix: target node20 so GHES 3.16 runners can load the action --- AGENTS.md | 2 +- action.yaml | 2 +- dist/index.js | 47 +++++++++++++++------------------------------ src/comment.test.ts | 2 +- src/comment.ts | 11 +++++++++-- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b1e6f61..0c0bb9b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ **Key Difference from create-task-action**: This action targets the Coder Agents Chat API (`/api/experimental/chats`) instead of the Tasks API. Agents purposefully does NOT expose template selection. It either auto-provisions a workspace or uses an existing one. **Tech Stack**: -- **Action Runtime**: Node 24 +- **Action Runtime**: Node 20 (GHES 3.16 runners reject `using: node24`) - **Development Runtime**: Bun (JavaScript/TypeScript runtime & bundler) - **Language**: TypeScript with strict mode enabled - **Validation**: Zod for runtime schema validation diff --git a/action.yaml b/action.yaml index 957a8ca..e62c26e 100644 --- a/action.yaml +++ b/action.yaml @@ -123,5 +123,5 @@ outputs: description: "Human-readable error message when the chat fails." runs: - using: "node24" + using: "node20" main: "dist/index.js" diff --git a/dist/index.js b/dist/index.js index 36964a9..37020cb 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3,43 +3,25 @@ var __getProtoOf = Object.getPrototypeOf; var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; -function __accessProp(key) { - return this[key]; -} -var __toESMCache_node; -var __toESMCache_esm; var __toESM = (mod, isNodeMode, target) => { - var canCache = mod != null && typeof mod === "object"; - if (canCache) { - var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap; - var cached = cache.get(mod); - if (cached) - return cached; - } target = mod != null ? __create(__getProtoOf(mod)) : {}; const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; for (let key of __getOwnPropNames(mod)) if (!__hasOwnProp.call(to, key)) __defProp(to, key, { - get: __accessProp.bind(mod, key), + get: () => mod[key], enumerable: true }); - if (canCache) - cache.set(mod, to); return to; }; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); -var __returnValue = (v) => v; -function __exportSetter(name, newValue) { - this[name] = __returnValue.bind(null, newValue); -} var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, - set: __exportSetter.bind(all, name) + set: (newValue) => all[name] = () => newValue }); }; @@ -13094,7 +13076,7 @@ var require_fetch = __commonJS((exports2, module2) => { request.cache = "no-store"; } const newConnection = forceNewConnection ? "yes" : "no"; - if (request.mode === "websocket") {} + if (request.mode === "websocket") {} else {} let requestBody = null; if (request.body == null && fetchParams.processRequestEndOfBody) { queueMicrotask(() => fetchParams.processRequestEndOfBody()); @@ -17280,7 +17262,7 @@ var require_undici = __commonJS((exports2, module2) => { module2.exports.setGlobalDispatcher = setGlobalDispatcher; module2.exports.getGlobalDispatcher = getGlobalDispatcher; var fetchImpl = require_fetch().fetch; - module2.exports.fetch = async function fetch2(init, options = undefined) { + module2.exports.fetch = async function fetch(init, options = undefined) { try { return await fetchImpl(init, options); } catch (err) { @@ -18299,8 +18281,8 @@ function prepareKeyValueMessage(key, value) { var os4 = __toESM(require("os")); // node_modules/@actions/http-client/lib/index.js -var tunnel = __toESM(require_tunnel2(), 1); -var import_undici = __toESM(require_undici(), 1); +var tunnel = __toESM(require_tunnel2()); +var import_undici = __toESM(require_undici()); var HttpCodes; (function(HttpCodes2) { HttpCodes2[HttpCodes2["OK"] = 200] = "OK"; @@ -18623,8 +18605,8 @@ class Context { } // node_modules/@actions/github/lib/internal/utils.js -var httpClient = __toESM(require_lib(), 1); -var import_undici2 = __toESM(require_undici(), 1); +var httpClient = __toESM(require_lib()); +var import_undici2 = __toESM(require_undici()); var __awaiter2 = function(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function(resolve) { @@ -19107,7 +19089,7 @@ function withDefaults(oldDefaults, newDefaults) { var endpoint = withDefaults(null, DEFAULTS); // node_modules/@octokit/request/dist-bundle/index.js -var import_content_type = __toESM(require_dist(), 1); +var import_content_type = __toESM(require_dist()); // node_modules/json-with-bigint/json-with-bigint.js var intRegex = /^-?\d+$/; @@ -33673,7 +33655,7 @@ function finalize(ctx, schema) { result.$schema = "http://json-schema.org/draft-07/schema#"; } else if (ctx.target === "draft-04") { result.$schema = "http://json-schema.org/draft-04/schema#"; - } else if (ctx.target === "openapi-3.0") {} + } else if (ctx.target === "openapi-3.0") {} else {} if (ctx.external?.uri) { const id = ctx.external.registry.get(schema)?.id; if (!id) @@ -33917,7 +33899,7 @@ var literalProcessor = (schema, ctx, json, _params) => { if (val === undefined) { if (ctx.unrepresentable === "throw") { throw new Error("Literal `undefined` cannot be represented in JSON Schema"); - } + } else {} } else if (typeof val === "bigint") { if (ctx.unrepresentable === "throw") { throw new Error("BigInt literals cannot be represented in JSON Schema"); @@ -36793,10 +36775,13 @@ function sanitizeLabelToken(input) { // src/comment.ts var DEFAULT_GITHUB_SERVER_URL = "https://github.com"; var githubURLRegexCache = new Map; +function escapeRegExp(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} function githubURLRegex(serverURL) { let regex = githubURLRegexCache.get(serverURL); if (!regex) { - const base = RegExp.escape(normalizeBaseUrl(serverURL)); + const base = escapeRegExp(normalizeBaseUrl(serverURL)); regex = new RegExp(`^${base}/([^/]+)/([^/]+)/(?:issues|pull)/(\\d+)/?(?:[?#].*)?$`); githubURLRegexCache.set(serverURL, regex); } @@ -37643,6 +37628,6 @@ async function main() { } } } -if (require.main == module) { +if (require.main == require.module) { main(); } diff --git a/src/comment.test.ts b/src/comment.test.ts index 3c5ea8d..ad4ec2b 100644 --- a/src/comment.test.ts +++ b/src/comment.test.ts @@ -110,7 +110,7 @@ describe("parseGithubItemURL", () => { }); test("does not treat the server host as a regex pattern", () => { - // The dots in the host are metacharacters; RegExp.escape must keep + // The dots in the host are metacharacters; escapeRegExp must keep // them literal so a host that merely matches the pattern (dot as // wildcard) is still rejected. expect( diff --git a/src/comment.ts b/src/comment.ts index 13a1079..3bb5b4c 100644 --- a/src/comment.ts +++ b/src/comment.ts @@ -17,7 +17,7 @@ export const DEFAULT_GITHUB_SERVER_URL = "https://github.com"; // Anchored issue/PR URL matcher for `serverURL`, compiled once per server URL. // In production the server URL is fixed for the run, so this cache only ever // holds a single entry; it grows past one entry solely under tests that -// exercise multiple hosts. `RegExp.escape` neutralizes metacharacters in the +// exercise multiple hosts. `escapeRegExp` neutralizes metacharacters in the // host (e.g. the dots in `github.com`) so it matches literally. Anchored at // both ends so a non-server host or extra path segments // (e.g. `.../issues/123/files`, `https://attacker.example/owner/repo/issues/1`) @@ -27,10 +27,17 @@ export const DEFAULT_GITHUB_SERVER_URL = "https://github.com"; // comment). const githubURLRegexCache = new Map(); +// Escape regex metacharacters so the string matches literally. Hand-rolled +// because `RegExp.escape` requires Node 24+, while the action targets Node 20 +// (GHES 3.16 runners reject `using: node24` at job setup). +function escapeRegExp(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + function githubURLRegex(serverURL: string): RegExp { let regex = githubURLRegexCache.get(serverURL); if (!regex) { - const base = RegExp.escape(normalizeBaseUrl(serverURL)); + const base = escapeRegExp(normalizeBaseUrl(serverURL)); regex = new RegExp( `^${base}/([^/]+)/([^/]+)/(?:issues|pull)/(\\d+)/?(?:[?#].*)?$`, ); From b38b9f0363fd90793b88baf73b4bea6ef2dc03de Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Thu, 27 Aug 2026 20:33:33 +0000 Subject: [PATCH 2/5] docs(readme): explain the node20 runtime choice --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 7ed7eb7..84d812e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,15 @@ The chat owner is always the user the `coder-token` belongs to. Read [Security m - Coder deployment with Agents enabled (experimental). - Coder session token belonging to the user the chats should run as. Treat this token as a high-value secret: anyone holding it acts as that Coder user via the agent's tool plane (see [Security model](#security-model)). +## Node runtime + +The action declares `runs.using: node20` instead of `node24`. This is deliberate: + +- GitHub Enterprise Server ships older runners that reject `using: node24` at job setup, before the action even loads. +- Current GitHub.com runners run `node20` actions on Node 24 automatically. Node 20 itself reached end-of-life in April 2026 and is being removed from GitHub-hosted runners, but actions declaring `node20` still load and simply run on Node 24. + +So one release works everywhere: new runners execute the action with Node 24, older GHES runners execute it with Node 20. To keep that true, the code avoids APIs that need anything newer than Node 20. + ## Quickstart Triage every issue labeled `coder`: From 425d2f5e4791a05f33453aab33378480c6412120 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Thu, 27 Aug 2026 20:55:42 +0000 Subject: [PATCH 3/5] docs(readme): reference ecosystem precedent for node20 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 84d812e..a3d6c63 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ The action declares `runs.using: node20` instead of `node24`. This is deliberate So one release works everywhere: new runners execute the action with Node 24, older GHES runners execute it with Node 20. To keep that true, the code avoids APIs that need anything newer than Node 20. +This mirrors the wider ecosystem: [actions/create-github-app-token](https://github.com/actions/create-github-app-token) kept its v2 line on `node20` after `node24` broke GHES ([#274](https://github.com/actions/create-github-app-token/issues/274)) and shipped `node24` only as breaking major v3, and [actions/checkout@v4](https://github.com/actions/checkout/blob/v4/action.yml) and [actions/github-script@v7](https://github.com/actions/github-script/blob/v7/action.yml) still declare `node20` today, with `node24` variants released as new majors that require runner v2.327.1+. + ## Quickstart Triage every issue labeled `coder`: From 599f836d99d1beb8b5d776dfe76b882c24d56af7 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Thu, 27 Aug 2026 20:57:38 +0000 Subject: [PATCH 4/5] Revert "docs(readme): reference ecosystem precedent for node20" This reverts commit 425d2f5e4791a05f33453aab33378480c6412120. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index a3d6c63..84d812e 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,6 @@ The action declares `runs.using: node20` instead of `node24`. This is deliberate So one release works everywhere: new runners execute the action with Node 24, older GHES runners execute it with Node 20. To keep that true, the code avoids APIs that need anything newer than Node 20. -This mirrors the wider ecosystem: [actions/create-github-app-token](https://github.com/actions/create-github-app-token) kept its v2 line on `node20` after `node24` broke GHES ([#274](https://github.com/actions/create-github-app-token/issues/274)) and shipped `node24` only as breaking major v3, and [actions/checkout@v4](https://github.com/actions/checkout/blob/v4/action.yml) and [actions/github-script@v7](https://github.com/actions/github-script/blob/v7/action.yml) still declare `node20` today, with `node24` variants released as new majors that require runner v2.327.1+. - ## Quickstart Triage every issue labeled `coder`: From 06f0bf64458f8bc4d58dc6ee588365e939d0f81b Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Thu, 27 Aug 2026 23:48:38 +0000 Subject: [PATCH 5/5] chore: rebuild dist with bun 1.3.14 to match CI --- dist/index.js | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/dist/index.js b/dist/index.js index 37020cb..d85efbb 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3,25 +3,43 @@ var __getProtoOf = Object.getPrototypeOf; var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; +function __accessProp(key) { + return this[key]; +} +var __toESMCache_node; +var __toESMCache_esm; var __toESM = (mod, isNodeMode, target) => { + var canCache = mod != null && typeof mod === "object"; + if (canCache) { + var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap; + var cached = cache.get(mod); + if (cached) + return cached; + } target = mod != null ? __create(__getProtoOf(mod)) : {}; const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; for (let key of __getOwnPropNames(mod)) if (!__hasOwnProp.call(to, key)) __defProp(to, key, { - get: () => mod[key], + get: __accessProp.bind(mod, key), enumerable: true }); + if (canCache) + cache.set(mod, to); return to; }; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); +var __returnValue = (v) => v; +function __exportSetter(name, newValue) { + this[name] = __returnValue.bind(null, newValue); +} var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, - set: (newValue) => all[name] = () => newValue + set: __exportSetter.bind(all, name) }); }; @@ -13076,7 +13094,7 @@ var require_fetch = __commonJS((exports2, module2) => { request.cache = "no-store"; } const newConnection = forceNewConnection ? "yes" : "no"; - if (request.mode === "websocket") {} else {} + if (request.mode === "websocket") {} let requestBody = null; if (request.body == null && fetchParams.processRequestEndOfBody) { queueMicrotask(() => fetchParams.processRequestEndOfBody()); @@ -17262,7 +17280,7 @@ var require_undici = __commonJS((exports2, module2) => { module2.exports.setGlobalDispatcher = setGlobalDispatcher; module2.exports.getGlobalDispatcher = getGlobalDispatcher; var fetchImpl = require_fetch().fetch; - module2.exports.fetch = async function fetch(init, options = undefined) { + module2.exports.fetch = async function fetch2(init, options = undefined) { try { return await fetchImpl(init, options); } catch (err) { @@ -18281,8 +18299,8 @@ function prepareKeyValueMessage(key, value) { var os4 = __toESM(require("os")); // node_modules/@actions/http-client/lib/index.js -var tunnel = __toESM(require_tunnel2()); -var import_undici = __toESM(require_undici()); +var tunnel = __toESM(require_tunnel2(), 1); +var import_undici = __toESM(require_undici(), 1); var HttpCodes; (function(HttpCodes2) { HttpCodes2[HttpCodes2["OK"] = 200] = "OK"; @@ -18605,8 +18623,8 @@ class Context { } // node_modules/@actions/github/lib/internal/utils.js -var httpClient = __toESM(require_lib()); -var import_undici2 = __toESM(require_undici()); +var httpClient = __toESM(require_lib(), 1); +var import_undici2 = __toESM(require_undici(), 1); var __awaiter2 = function(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function(resolve) { @@ -19089,7 +19107,7 @@ function withDefaults(oldDefaults, newDefaults) { var endpoint = withDefaults(null, DEFAULTS); // node_modules/@octokit/request/dist-bundle/index.js -var import_content_type = __toESM(require_dist()); +var import_content_type = __toESM(require_dist(), 1); // node_modules/json-with-bigint/json-with-bigint.js var intRegex = /^-?\d+$/; @@ -33655,7 +33673,7 @@ function finalize(ctx, schema) { result.$schema = "http://json-schema.org/draft-07/schema#"; } else if (ctx.target === "draft-04") { result.$schema = "http://json-schema.org/draft-04/schema#"; - } else if (ctx.target === "openapi-3.0") {} else {} + } else if (ctx.target === "openapi-3.0") {} if (ctx.external?.uri) { const id = ctx.external.registry.get(schema)?.id; if (!id) @@ -33899,7 +33917,7 @@ var literalProcessor = (schema, ctx, json, _params) => { if (val === undefined) { if (ctx.unrepresentable === "throw") { throw new Error("Literal `undefined` cannot be represented in JSON Schema"); - } else {} + } } else if (typeof val === "bigint") { if (ctx.unrepresentable === "throw") { throw new Error("BigInt literals cannot be represented in JSON Schema"); @@ -37628,6 +37646,6 @@ async function main() { } } } -if (require.main == require.module) { +if (require.main == module) { main(); }