From e8bb8e4380d9cfcbde2f5d00d2d18211de7f61f0 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:46:46 +0200 Subject: [PATCH 1/3] fix(deps): migrate validation to Zod 4 Upgrade Zod to 4.6.7 to resolve GHSA-mrhx-c7xc-m3w5. Adapt object schemas, records and diagnostics while preserving optional fields and accepted Sandbox timeout values. Add parser and HTML callback regressions. npm run check passes 2523 tests on Node 22 and 24. Installed tarballs pass consumer builds, strict declaration checks and clean audits on both versions. --- package-lock.json | 8 +++---- package.json | 2 +- .../entrypoint/parser/AbstractParser.test.ts | 2 +- src/cli/entrypoint/parser/AbstractParser.ts | 4 ++-- src/cli/entrypoint/parser/ContentParser.ts | 2 +- .../entrypoint/parser/SandboxParser.test.ts | 18 +++++++++++++++ src/cli/entrypoint/parser/SandboxParser.ts | 6 +++-- src/cli/entrypoint/parser/ViewParser.test.ts | 19 +++++++++++++++- src/cli/entrypoint/parser/schemas/html.ts | 18 ++++++--------- .../sandbox/invalid/negative-timeout.ts | 1 + .../sandbox/invalid/string-timeout.ts | 1 + .../fixtures/sandbox/invalid/zero-timeout.ts | 1 + .../options/infinite-timeouts/sandbox.ts | 4 ++++ .../fixtures/view/invalid/static-add-hash.ts | 1 + .../fixtures/view/options/sparse/page.ts | 9 ++++++++ src/cli/plugins/meta/SpecificSettings.test.ts | 14 ++++++++++++ .../build/html/fixture/html-options.ts | 16 ++++++++++++-- .../build/html/html.integration.test.ts | 22 +++++++++++++++++++ .../scenarios/options-invalid-callback.ts | 4 ++++ .../scenarios/options-throwing-callback.ts | 8 +++++++ 20 files changed, 135 insertions(+), 25 deletions(-) create mode 100644 src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/negative-timeout.ts create mode 100644 src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/string-timeout.ts create mode 100644 src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/zero-timeout.ts create mode 100644 src/cli/entrypoint/parser/tests/fixtures/sandbox/options/infinite-timeouts/sandbox.ts create mode 100644 src/cli/entrypoint/parser/tests/fixtures/view/invalid/static-add-hash.ts create mode 100644 src/cli/entrypoint/parser/tests/fixtures/view/options/sparse/page.ts create mode 100644 tests/integration/build/html/scenarios/options-invalid-callback.ts create mode 100644 tests/integration/build/html/scenarios/options-throwing-callback.ts diff --git a/package-lock.json b/package-lock.json index 3e91f337..2b49a0fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "typescript": "^5.7.3", "utility-types": "^3.11.0", "webpack-merge": "^6.0.1", - "zod": "^3.24.2" + "zod": "^4.6.7" }, "bin": { "adnbn": "bin/adnbn.js" @@ -19351,9 +19351,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 32f7bad8..bf672d98 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,7 @@ "typescript": "^5.7.3", "utility-types": "^3.11.0", "webpack-merge": "^6.0.1", - "zod": "^3.24.2" + "zod": "^4.6.7" }, "devDependencies": { "@commitlint/cli": "^20.1.0", diff --git a/src/cli/entrypoint/parser/AbstractParser.test.ts b/src/cli/entrypoint/parser/AbstractParser.test.ts index 32021344..ebad7c79 100644 --- a/src/cli/entrypoint/parser/AbstractParser.test.ts +++ b/src/cli/entrypoint/parser/AbstractParser.test.ts @@ -23,7 +23,7 @@ class CommonParser extends AbstractParser { return "defineBackground"; } - protected schema(): z.AnyZodObject { + protected schema(): z.ZodObject { return this.CommonPropertiesSchema; } } diff --git a/src/cli/entrypoint/parser/AbstractParser.ts b/src/cli/entrypoint/parser/AbstractParser.ts index c65dd457..2a4175f4 100644 --- a/src/cli/entrypoint/parser/AbstractParser.ts +++ b/src/cli/entrypoint/parser/AbstractParser.ts @@ -21,7 +21,7 @@ export default abstract class AbstractParser implem manifestVersion: z.union([z.literal(2), z.literal(3)]).optional(), }); - protected abstract schema(): z.AnyZodObject; + protected abstract schema(): z.ZodObject; protected abstract definition(): string | string[]; @@ -59,7 +59,7 @@ export default abstract class AbstractParser implem const {success, error, data} = this.schema().safeParse(this.getOptions(file)); if (!success) { - const e = error?.errors[0]; + const e = error.issues[0]; if (e) { throw new Error(`Invalid options ${e.path.join(", ")} in "${file.file}": ${e.message}`); diff --git a/src/cli/entrypoint/parser/ContentParser.ts b/src/cli/entrypoint/parser/ContentParser.ts index 73a596de..2056d2f9 100644 --- a/src/cli/entrypoint/parser/ContentParser.ts +++ b/src/cli/entrypoint/parser/ContentParser.ts @@ -23,7 +23,7 @@ export default class ContentParser< return ["defineContentScript", "defineContentScriptAppend"]; } - protected schema(): z.AnyZodObject { + protected schema(): z.ZodObject { const frameSchema = z .object({ type: z.literal(ContentScriptIsolation.Iframe), diff --git a/src/cli/entrypoint/parser/SandboxParser.test.ts b/src/cli/entrypoint/parser/SandboxParser.test.ts index 0978d9ff..d6b80813 100644 --- a/src/cli/entrypoint/parser/SandboxParser.test.ts +++ b/src/cli/entrypoint/parser/SandboxParser.test.ts @@ -31,6 +31,24 @@ describe("SandboxParser", () => { expect(parseOptions("options", "defaults", "sandbox.ts")).toEqual({}); }); + test("preserves positive infinite timeout values accepted by existing configurations", () => { + expect(parseOptions("options", "infinite-timeouts", "sandbox.ts")).toStrictEqual({ + readyTimeout: Infinity, + requestTimeout: Infinity, + }); + }); + + test.each([ + ["zero-timeout", "readyTimeout"], + ["negative-timeout", "requestTimeout"], + ["string-timeout", "readyTimeout"], + ])("rejects %s and identifies its field and source", (scenario, field) => { + const source = file("invalid", `${scenario}.ts`); + + expect(() => parser.options(source)).toThrow(`Invalid options ${field}`); + expect(() => parser.options(source)).toThrow(source.file); + }); + test("parses full sandbox options from a real entrypoint file", () => { expect(parseOptions("options", "full", "sandbox.ts")).toEqual({ as: "unsafe-parser-frame", diff --git a/src/cli/entrypoint/parser/SandboxParser.ts b/src/cli/entrypoint/parser/SandboxParser.ts index e2b72dbb..fa872683 100644 --- a/src/cli/entrypoint/parser/SandboxParser.ts +++ b/src/cli/entrypoint/parser/SandboxParser.ts @@ -18,6 +18,8 @@ export default class extends ViewParser { protected schema(): typeof this.CommonPropertiesSchema { const sources = z.array(z.enum(sandboxSourceValues)).optional(); + // Preserve the positive-number contract from Zod 3, including positive infinity. + const timeout = z.union([z.number().positive(), z.literal(Infinity)]).optional(); return super.schema().extend({ name: z @@ -30,8 +32,8 @@ export default class extends ViewParser { "Key must start with a Unicode letter, `$` or `_`, and may only contain letters, digits, `$` or `_`", }) .optional(), - readyTimeout: z.number().positive().optional(), - requestTimeout: z.number().positive().optional(), + readyTimeout: timeout, + requestTimeout: timeout, removeOnRequestTimeout: z.boolean().optional(), csp: z .object({ diff --git a/src/cli/entrypoint/parser/ViewParser.test.ts b/src/cli/entrypoint/parser/ViewParser.test.ts index 8076d8cb..46c72e08 100644 --- a/src/cli/entrypoint/parser/ViewParser.test.ts +++ b/src/cli/entrypoint/parser/ViewParser.test.ts @@ -57,6 +57,17 @@ describe("ViewParser", () => { expect(parseOptions("options", "defaults", "page.ts")).toEqual({}); }); + test("preserves optional values and falsy attributes while stripping unknown nested options", () => { + expect(parseOptions("options", "sparse", "page.ts")).toStrictEqual({ + append: undefined, + metas: { + attributes: {name: "description", content: "", hidden: false, priority: 0}, + glob: undefined, + }, + scripts: [{path: "extra.js", attributes: {defer: true}}], + }); + }); + test("reads a meta object", () => { expect(parseOptions("options", "object", "page.ts")).toEqual({metas: {attributes: {charset: "utf-8"}}}); }); @@ -79,7 +90,7 @@ describe("ViewParser", () => { ["meta-missing", /meta object with nonempty attributes/], ["meta-null", /meta object with nonempty attributes/], ["meta-invalid-attribute", /meta object with nonempty attributes/], - ["meta-nonfinite-attribute", /Number must be finite/], + ["meta-nonfinite-attribute", /meta object with nonempty attributes/], ])("rejects %s and identifies its field and source file", (scenario, message) => { expect(() => parseOptions("invalid", `${scenario}.ts`)).toThrow("Invalid options metas"); expect(() => parseOptions("invalid", `${scenario}.ts`)).toThrow(`${scenario}.ts`); @@ -96,6 +107,12 @@ describe("ViewParser", () => { ); }); + test("retains the configuration hint for a forbidden static callback option", () => { + expect(() => parseOptions("invalid", "static-add-hash.ts")).toThrow( + /Invalid options addHash.*static-add-hash.ts.*Configure addHash in config.html/ + ); + }); + test.each([ ["add-hash", "addHash"], ["add-public-path", "addPublicPath"], diff --git a/src/cli/entrypoint/parser/schemas/html.ts b/src/cli/entrypoint/parser/schemas/html.ts index 2433c459..b304a71a 100644 --- a/src/cli/entrypoint/parser/schemas/html.ts +++ b/src/cli/entrypoint/parser/schemas/html.ts @@ -1,14 +1,14 @@ import {z} from "zod"; -const AttributesSchema = z.record(z.union([z.string(), z.boolean(), z.number().finite()])); +const AttributesSchema = z.record(z.string(), z.union([z.string(), z.boolean(), z.number().finite()])); const CommonSchema = z.object({ append: z.boolean().optional(), useHash: z.boolean().optional(), - addHash: z.never({invalid_type_error: "Configure addHash in config.html"}).optional(), + addHash: z.never({error: "Configure addHash in config.html"}).optional(), hash: z.union([z.boolean(), z.string()]).optional(), usePublicPath: z.boolean().optional(), - addPublicPath: z.never({invalid_type_error: "Configure addPublicPath in config.html"}).optional(), + addPublicPath: z.never({error: "Configure addPublicPath in config.html"}).optional(), publicPath: z.union([z.boolean(), z.string()]).optional(), }); @@ -22,7 +22,7 @@ const BaseTagSchema = CommonSchema.extend({ const LinkSchema = BaseTagSchema.extend({ path: z.string(), attributes: AttributesSchema.optional(), - external: z.never({invalid_type_error: "external is only supported for scripts"}).optional(), + external: z.never({error: "external is only supported for scripts"}).optional(), }); const ScriptSchema = LinkSchema.extend({ @@ -34,7 +34,7 @@ const MetaSchema = BaseTagSchema.extend({ attributes: AttributesSchema.refine(attributes => Object.keys(attributes).length > 0, { message: "A meta tag requires a nonempty attributes object", }), - external: z.never({invalid_type_error: "external is only supported for scripts"}).optional(), + external: z.never({error: "external is only supported for scripts"}).optional(), }); const TagSchema = ScriptSchema.extend({type: z.enum(["css", "js"]).optional()}); @@ -46,17 +46,13 @@ export const HtmlEntrypointOptionsSchema = CommonSchema.extend({ prependExternals: z.boolean().optional(), jsExtensions: z.union([z.string(), z.array(z.string())]).optional(), cssExtensions: z.union([z.string(), z.array(z.string())]).optional(), - files: z - .never({invalid_type_error: "The entrypoint selects its HTML file; configure files in config.html"}) - .optional(), + files: z.never({error: "The entrypoint selects its HTML file; configure files in config.html"}).optional(), links: z.union([LinkValueSchema, z.array(LinkValueSchema)]).optional(), scripts: z.union([ScriptValueSchema, z.array(ScriptValueSchema)]).optional(), tags: z.union([TagValueSchema, z.array(TagValueSchema)]).optional(), metas: z .union([MetaSchema, z.array(MetaSchema)], { - errorMap: () => ({ - message: "Expected a meta object with nonempty attributes, or an array of such objects", - }), + error: "Expected a meta object with nonempty attributes, or an array of such objects", }) .optional(), }); diff --git a/src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/negative-timeout.ts b/src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/negative-timeout.ts new file mode 100644 index 00000000..56e8bcd1 --- /dev/null +++ b/src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/negative-timeout.ts @@ -0,0 +1 @@ +export const requestTimeout = -1; diff --git a/src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/string-timeout.ts b/src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/string-timeout.ts new file mode 100644 index 00000000..8417ba59 --- /dev/null +++ b/src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/string-timeout.ts @@ -0,0 +1 @@ +export const readyTimeout = "1000"; diff --git a/src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/zero-timeout.ts b/src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/zero-timeout.ts new file mode 100644 index 00000000..de192aa3 --- /dev/null +++ b/src/cli/entrypoint/parser/tests/fixtures/sandbox/invalid/zero-timeout.ts @@ -0,0 +1 @@ +export const readyTimeout = 0; diff --git a/src/cli/entrypoint/parser/tests/fixtures/sandbox/options/infinite-timeouts/sandbox.ts b/src/cli/entrypoint/parser/tests/fixtures/sandbox/options/infinite-timeouts/sandbox.ts new file mode 100644 index 00000000..9a817b17 --- /dev/null +++ b/src/cli/entrypoint/parser/tests/fixtures/sandbox/options/infinite-timeouts/sandbox.ts @@ -0,0 +1,4 @@ +export const readyTimeout = 1e309; +export const requestTimeout = 1e309; + +export const init = () => ({}); diff --git a/src/cli/entrypoint/parser/tests/fixtures/view/invalid/static-add-hash.ts b/src/cli/entrypoint/parser/tests/fixtures/view/invalid/static-add-hash.ts new file mode 100644 index 00000000..777c2258 --- /dev/null +++ b/src/cli/entrypoint/parser/tests/fixtures/view/invalid/static-add-hash.ts @@ -0,0 +1 @@ +export const addHash = "invalid"; diff --git a/src/cli/entrypoint/parser/tests/fixtures/view/options/sparse/page.ts b/src/cli/entrypoint/parser/tests/fixtures/view/options/sparse/page.ts new file mode 100644 index 00000000..27f72da5 --- /dev/null +++ b/src/cli/entrypoint/parser/tests/fixtures/view/options/sparse/page.ts @@ -0,0 +1,9 @@ +export const append = undefined; + +export const metas = { + attributes: {name: "description", content: "", hidden: false, priority: 0}, + glob: undefined, + unused: "ignored", +}; + +export const scripts = [{path: "extra.js", attributes: {defer: true}, unused: "ignored"}]; diff --git a/src/cli/plugins/meta/SpecificSettings.test.ts b/src/cli/plugins/meta/SpecificSettings.test.ts index 0902db30..f87a8831 100644 --- a/src/cli/plugins/meta/SpecificSettings.test.ts +++ b/src/cli/plugins/meta/SpecificSettings.test.ts @@ -16,6 +16,20 @@ describe("SpecificSettings meta", () => { const makeConfig = (specific: any): any => ({specific}); + test.each([{}, {gecko: {}}, {gecko: {dataCollectionPermissions: {}}}, {gecko: undefined}])( + "preserves missing and explicitly undefined optional fields in %p", + specific => { + expect(new SpecificSettings(makeConfig(specific)).getResolved()).toStrictEqual(specific); + } + ); + + test.each([{unknown: true}, {gecko: null}, {gecko: {dataCollectionPermissions: {required: null}}}])( + "rejects unknown keys and null optional fields in %p", + specific => { + expect(new SpecificSettings(makeConfig(specific)).getResolved()).toBeUndefined(); + } + ); + test("returns direct valid object", () => { const specific = { gecko: { diff --git a/tests/integration/build/html/fixture/html-options.ts b/tests/integration/build/html/fixture/html-options.ts index 136ceb5f..faf41b90 100644 --- a/tests/integration/build/html/fixture/html-options.ts +++ b/tests/integration/build/html/fixture/html-options.ts @@ -6,7 +6,19 @@ export const htmlOptions: HtmlOptions = { scripts: { path: "global.js", sourcePath: "assets/global.js", - publicPath: assetPath => `/global/${assetPath}`, + publicPath: (assetPath, publicPath) => { + if (assetPath !== "global.js" || typeof publicPath !== "string") { + throw new Error("Unexpected publicPath callback arguments"); + } + + return `/global/${assetPath}`; + }, + }, + hash: (assetPath, hash) => { + if (assetPath !== "/global/global.js" || typeof hash !== "string" || hash.length === 0) { + throw new Error("Unexpected hash callback arguments"); + } + + return `${assetPath}?build=${hash}`; }, - hash: (assetPath, hash) => `${assetPath}?build=${hash}`, }; diff --git a/tests/integration/build/html/html.integration.test.ts b/tests/integration/build/html/html.integration.test.ts index a65474fe..888cf530 100644 --- a/tests/integration/build/html/html.integration.test.ts +++ b/tests/integration/build/html/html.integration.test.ts @@ -79,6 +79,28 @@ test("reports invalid metas returned by the html factory during a real build", a } }); +test.each([ + ["invalid-callback", /addHash.*function/], + ["throwing-callback", /HTML hash callback failed/], +])("reports %s from config.html during a real build", async (scenario, message) => { + const fixture = await createIntegrationFixture(rootDir, fixtureDir); + + try { + await writeFile( + path.join(fixture.directory, "adnbn.config.ts"), + await readFile(path.join(scenarios, "config-function-array.ts")) + ); + await writeFile( + path.join(fixture.directory, "html-options.ts"), + await readFile(path.join(scenarios, `options-${scenario}.ts`)) + ); + + await expect(fixture.build()).rejects.toThrow(message); + } finally { + await fixture.dispose(); + } +}); + test.each([ ["invalid-metas", /Invalid options metas.*object\.page\.ts.*attributes/], ["conflicting-hash", /metas\.hash/], diff --git a/tests/integration/build/html/scenarios/options-invalid-callback.ts b/tests/integration/build/html/scenarios/options-invalid-callback.ts new file mode 100644 index 00000000..9d20a9c9 --- /dev/null +++ b/tests/integration/build/html/scenarios/options-invalid-callback.ts @@ -0,0 +1,4 @@ +export const htmlOptions = { + scripts: "extra.js", + addHash: 42, +}; diff --git a/tests/integration/build/html/scenarios/options-throwing-callback.ts b/tests/integration/build/html/scenarios/options-throwing-callback.ts new file mode 100644 index 00000000..ba3073c4 --- /dev/null +++ b/tests/integration/build/html/scenarios/options-throwing-callback.ts @@ -0,0 +1,8 @@ +import type {HtmlOptions} from "adnbn"; + +export const htmlOptions: HtmlOptions = { + scripts: "extra.js", + hash: () => { + throw new Error("HTML hash callback failed"); + }, +}; From ac7d8a5623aeb2b5d5d6832132b6deb019cb2199 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:25:03 +0200 Subject: [PATCH 2/3] fix(deps): resolve basic-ftp security advisory Override basic-ftp to 6.2.3 within get-uri to remove six high audit findings. Verified proxy behavior, release previews and npm run check on Node 22 and 24. --- package-lock.json | 6 +++--- package.json | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b49a0fb..28b37444 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8885,9 +8885,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-6.2.3.tgz", + "integrity": "sha512-T7GYtQ2eKJUN5YORG7A8u2WfB9JMN0a4/wl3MI64qq2qTRfa3FXzoNq0DDojZDkDyStcL+TkfKsz4JHEZx5kGQ==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index bf672d98..18da66c3 100644 --- a/package.json +++ b/package.json @@ -237,6 +237,9 @@ } }, "overrides": { + "get-uri": { + "basic-ftp": "6.2.3" + }, "glob": "$glob", "jsdom": "27.4.0", "test-exclude": "^7.0.1", From ed2cc29ebcac3f5198c3eb214d116cf4f0339f78 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:48:59 +0200 Subject: [PATCH 3/3] ci: run regular checks and releases on Node 24 Keep the full Linux/Windows Node 22/24 matrix for PRs to main and before release. Move coverage producers, artifacts and publication to Node 24. --- .github/workflows/ci.yml | 30 +++++++++++++++++------------- .github/workflows/release.yml | 2 +- AGENTS.md | 2 +- tests/README.md | 6 ++++-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36b28bb9..963fd4cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,12 +27,16 @@ jobs: name_suffix: ${{ steps.set.outputs.name_suffix }} steps: - id: set + env: + EVENT_NAME: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + FULL_MATRIX: ${{ inputs.full }} run: | - if [[ "${{ github.event_name }}" != "workflow_call" || "${{ inputs.full }}" == "true" ]]; then + if [[ "$FULL_MATRIX" == "true" || ( "$EVENT_NAME" == "pull_request" && "$BASE_REF" == "main" ) ]]; then echo 'matrix={"os":["ubuntu-24.04","windows-latest"],"node":[22,24]}' >> "$GITHUB_OUTPUT" echo 'name_suffix=(full matrix)' >> "$GITHUB_OUTPUT" else - echo 'matrix={"os":["ubuntu-24.04"],"node":[22]}' >> "$GITHUB_OUTPUT" + echo 'matrix={"os":["ubuntu-24.04","windows-latest"],"node":[24]}' >> "$GITHUB_OUTPUT" echo 'name_suffix=' >> "$GITHUB_OUTPUT" fi @@ -60,13 +64,13 @@ jobs: - name: Verify test groups run: npm run test:inventory - name: Units with coverage (no package build) - if: matrix.os == 'ubuntu-24.04' && matrix.node == 22 + if: matrix.os == 'ubuntu-24.04' && matrix.node == 24 run: npm run test:run -- --selectProjects unit-node unit-dom --ci --coverage --coverageDirectory=coverage/unit - name: Units (no package build) - if: matrix.os != 'ubuntu-24.04' || matrix.node != 22 + if: matrix.os != 'ubuntu-24.04' || matrix.node != 24 run: npm run test:run -- --selectProjects unit-node unit-dom --ci - name: Upload unit coverage - if: matrix.os == 'ubuntu-24.04' && matrix.node == 22 + if: matrix.os == 'ubuntu-24.04' && matrix.node == 24 uses: actions/upload-artifact@v7.0.1 with: name: coverage-unit-${{ matrix.os }}-node${{ matrix.node }} @@ -94,17 +98,17 @@ jobs: - name: Build package run: npm run build - name: Compiler, CLI and declaration integrations with coverage - if: matrix.os == 'ubuntu-24.04' && matrix.node == 22 + if: matrix.os == 'ubuntu-24.04' && matrix.node == 24 run: npm run test:run -- --selectProjects build types --ci --maxWorkers=2 --coverage --coverageDirectory=coverage/build - name: Compiler, CLI and declaration integrations - if: matrix.os != 'ubuntu-24.04' || matrix.node != 22 + if: matrix.os != 'ubuntu-24.04' || matrix.node != 24 run: npm run test:run -- --selectProjects build types --ci --maxWorkers=2 - name: Override builds in isolated sessions run: npm run test:run -- --selectProjects build --testPathPatterns=tests/integration/build/override --ci --maxWorkers=2 env: ADNBN_OVERRIDE_BUILD_MODE: session - name: Upload build coverage - if: matrix.os == 'ubuntu-24.04' && matrix.node == 22 + if: matrix.os == 'ubuntu-24.04' && matrix.node == 24 uses: actions/upload-artifact@v7.0.1 with: name: coverage-build-${{ matrix.os }}-node${{ matrix.node }} @@ -183,7 +187,7 @@ jobs: retention-days: 7 coverage: - name: Coverage (ubuntu-24.04, Node 22) + name: Coverage (ubuntu-24.04, Node 24) needs: [compute-matrix, unit, build] runs-on: ubuntu-24.04 permissions: @@ -194,26 +198,26 @@ jobs: - name: Use Node.js uses: actions/setup-node@v7.0.0 with: - node-version: 22 + node-version: 24 cache: npm - name: Install dependencies run: npm ci - name: Download unit coverage uses: actions/download-artifact@v8.0.1 with: - name: coverage-unit-ubuntu-24.04-node22 + name: coverage-unit-ubuntu-24.04-node24 path: coverage/unit - name: Download build coverage uses: actions/download-artifact@v8.0.1 with: - name: coverage-build-ubuntu-24.04-node22 + name: coverage-build-ubuntu-24.04-node24 path: coverage/build - name: Merge source coverage from both groups run: node scripts/merge-coverage.mjs coverage/unit/coverage-final.json coverage/build/coverage-final.json - name: Upload coverage uses: actions/upload-artifact@v7.0.1 with: - name: coverage-ubuntu-24.04-node22 + name: coverage-ubuntu-24.04-node24 path: coverage/merged # Keep the existing required check names while the work runs in independent jobs. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60fde19e..aedef28e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,7 +82,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v7.0.0 with: - node-version: 22 + node-version: 24 cache: 'npm' - name: Update npm diff --git a/AGENTS.md b/AGENTS.md index 4d60b31e..f217e10b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ ## Cross-platform tests -- The full CI matrix in `.github/workflows/ci.yml` runs on Linux and Windows with Node.js 22 and 24. New and changed non-browser tests must work on both operating systems. Chrome and Firefox browser integrations currently run on Linux; consult the workflow for the current matrix. +- Regular CI in `.github/workflows/ci.yml` runs on Linux and Windows with Node.js 24. Pull requests targeting `main` and pre-release validation use the full Node.js 22/24 matrix. New and changed non-browser tests must work on both operating systems and supported Node.js versions. Chrome and Firefox browser integrations run on Linux for each selected Node.js version; consult the workflow for the current matrix. Coverage and package publication use Node.js 24. - Build filesystem paths with `path.resolve` and `path.join`, including paths used with in-memory filesystems. Do not hardcode Unix roots such as `/dist` or Windows drive letters. Configure the compiler and read, write, or assert its output using the same resolved directory. Keep filesystem paths distinct from URLs and manifest asset paths, which use forward slashes. - In watch tests, simulate edits with `writeFile(destination, await readFile(source))`. Do not use `copyFile` to update watched files: on Windows it can preserve an old `mtime`, preventing the watcher from detecting the change. Copying initial fixtures before starting watch is fine. - Wait for the expected compilation or observable output with a descriptive timeout. Do not rely on fixed sleeps or assume that emitted files mean the watcher has finished reconnecting. Preserve exact filename casing and account for platform line endings when they are not part of the tested contract. diff --git a/tests/README.md b/tests/README.md index 053a1dd3..7e5d40e2 100644 --- a/tests/README.md +++ b/tests/README.md @@ -169,9 +169,11 @@ Pre-push runs `npm run check`, without browser launches or coverage. It includes ## CI and coverage -CI runs units, compiler/build/declaration tests, fixture typechecks, Chrome and Firefox in separate jobs. All non-browser groups retain the Linux/Windows × Node 22/24 matrix; both browsers run on Linux for each Node version. Linux jobs use `ubuntu-24.04`. Existing `Test and Build` check names retain their `ubuntu-latest` label for compatibility and aggregate the entire matrix, not only the OS/Node named in each check; they fail if any required job fails, is cancelled or is skipped. A full workflow contains 22 jobs including matrix computation and aggregation, with at most 16 test jobs ready together before coverage/aggregation dependencies finish. +CI runs units, compiler/build/declaration tests, fixture typechecks, Chrome and Firefox in separate jobs. Regular pushes and pull requests targeting `develop` or `feature/**` use Node 24 on Linux and Windows. Pull requests targeting `main` use the full Linux/Windows × Node 22/24 matrix. The release workflow also requests this full matrix with `full: true` and waits for it before publishing on Node 24. Reusable calls without `full: true` use the regular matrix unless the triggering event is a pull request targeting `main`. Both browsers run on Linux for each selected Node version. Linux jobs use `ubuntu-24.04`. -Babel coverage is collected in the Ubuntu/Node 22 unit and build jobs, then merged once with `scripts/merge-coverage.mjs`. The other matrix cells run the same tests without coverage instrumentation. The published report is `coverage-ubuntu-24.04-node22`; platform-specific coverage reports are no longer produced. Moving compiler tests between groups must not remove their source coverage. Keep both `coverage-final.json` inputs when merging locally: +`Test and Build` check names retain their `ubuntu-latest` label and the `(full matrix)` suffix for full runs. They aggregate the entire selected matrix, not only the OS/Node named in each check, and fail if any required job fails, is cancelled or is skipped. A regular workflow contains 12 jobs, including 8 test jobs; a full workflow contains 22 jobs, including 16 test jobs. These counts include matrix computation, coverage merging and aggregation. Node 22 remains supported by the package. + +Babel coverage is collected in the Ubuntu/Node 24 unit and build jobs, then merged once with `scripts/merge-coverage.mjs`. The other matrix cells run the same tests without coverage instrumentation. Both regular and full runs publish `coverage-ubuntu-24.04-node24`; platform-specific coverage reports are no longer produced. Moving compiler tests between groups must not remove their source coverage. Use Node 24 and keep both `coverage-final.json` inputs when merging locally: ```sh npm run test:run -- --selectProjects unit-node unit-dom --coverage --coverageDirectory=coverage/unit