From 1bfbbf0c5d35d2d58e9f78974c5ed3af50c0bca2 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 18 Oct 2026 20:00:18 +0900 Subject: [PATCH] fix(release): validate managed changepack paths Refs #683 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...ack_log_conditional_spread_precedence.json | 3 +- .../changepack_log_css_output_edges.json | 3 +- .../changepack_log_docs_accuracy.json | 7 -- .../changepack_log_docs_breakpoints.json | 3 +- packages/__tests__/changepack-paths.test.ts | 95 +++++++++++++++++++ 5 files changed, 98 insertions(+), 13 deletions(-) delete mode 100644 .changepacks/changepack_log_docs_accuracy.json create mode 100644 packages/__tests__/changepack-paths.test.ts diff --git a/.changepacks/changepack_log_conditional_spread_precedence.json b/.changepacks/changepack_log_conditional_spread_precedence.json index 2512093ae..31261720a 100644 --- a/.changepacks/changepack_log_conditional_spread_precedence.json +++ b/.changepacks/changepack_log_conditional_spread_precedence.json @@ -1,7 +1,6 @@ { "changes": { - "bindings/devup-ui-wasm/package.json": "Patch", - "apps/landing/package.json": "Patch" + "bindings/devup-ui-wasm/package.json": "Patch" }, "note": "Respect source-order property replacement across conditional spreads in styled rule objects, and document that styled callbacks cannot return runtime class names. No new build errors; existing located Cannot compose errors remain unchanged.", "date": "2026-10-01T00:00:00.000Z" diff --git a/.changepacks/changepack_log_css_output_edges.json b/.changepacks/changepack_log_css_output_edges.json index 5d5645a5f..92c183449 100644 --- a/.changepacks/changepack_log_css_output_edges.json +++ b/.changepacks/changepack_log_css_output_edges.json @@ -1,7 +1,6 @@ { "changes": { - "bindings/devup-ui-wasm/package.json": "Patch", - "apps/landing/package.json": "Patch" + "bindings/devup-ui-wasm/package.json": "Patch" }, "note": "Preserve unitless legacy vendor numbers, classify vendor time values as milliseconds and emit the Opera vendor prefix correctly; reject nested responsive arrays and known dead unprefixed CSS property names with source-located build errors, while preserving supported experimental CSS, unknown properties and documented shorthands.", "date": "2026-10-01T00:00:00.000Z" diff --git a/.changepacks/changepack_log_docs_accuracy.json b/.changepacks/changepack_log_docs_accuracy.json deleted file mode 100644 index 47222273b..000000000 --- a/.changepacks/changepack_log_docs_accuracy.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "changes": { - "apps/landing/package.json": "Patch" - }, - "note": "Documentation examples now preserve Button event handlers and show the CSS actually emitted for values, colors, keyframes, fonts and atomic rule reuse. Documentation only: no new build errors.", - "date": "2026-10-01T00:00:00.000Z" -} diff --git a/.changepacks/changepack_log_docs_breakpoints.json b/.changepacks/changepack_log_docs_breakpoints.json index 2f71eb081..3918c419f 100644 --- a/.changepacks/changepack_log_docs_breakpoints.json +++ b/.changepacks/changepack_log_docs_breakpoints.json @@ -1,7 +1,6 @@ { "changes": { - "bindings/devup-ui-wasm/package.json": "Patch", - "apps/landing/package.json": "Patch" + "bindings/devup-ui-wasm/package.json": "Patch" }, "note": "Typography documents the emitted default breakpoints, and source-backed integration tests keep the public breakpoint lists and range bounds synchronized with the production defaults. No runtime changes or new build errors.", "date": "2026-10-01T00:00:00.000Z" diff --git a/packages/__tests__/changepack-paths.test.ts b/packages/__tests__/changepack-paths.test.ts new file mode 100644 index 000000000..09372d220 --- /dev/null +++ b/packages/__tests__/changepack-paths.test.ts @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict' +import { resolve } from 'node:path' + +import { describe, expect, it } from 'bun:test' + +const root = resolve(import.meta.dir, '../..') +// Bun's filesystem API avoids the node:fs spies used by the plugin suites. +const config: unknown = await Bun.file( + resolve(root, '.changepacks/config.json'), +).json() +assert(typeof config === 'object' && config !== null && 'ignore' in config) +assert(Array.isArray(config.ignore)) + +// This is deliberately scoped to the asserted config, not a gitignore parser. +const managedManifests = new Set( + config.ignore.slice(1).flatMap((pattern: unknown) => { + assert(typeof pattern === 'string') + return [ + ...new Bun.Glob(pattern.slice(1)).scanSync({ + cwd: root, + onlyFiles: true, + }), + ] + .map((path) => path.replaceAll('\\', '/')) + .filter((path) => + /^(packages|bindings)\/[^/]+\/package\.json$/.test(path), + ) + }), +) + +function unmanagedPaths(paths: readonly string[]): string[] { + // Do not normalize log keys: aliases must not silently resolve as canonical keys. + return paths.filter((path) => !managedManifests.has(path)).sort() +} + +describe('changepack manifest paths', () => { + it('uses the supported managed-package allowlist when reading current config', () => { + expect(config.ignore).toEqual([ + '*', + '!packages/*/*', + '!bindings/*/package.json', + ]) + }) + + it.each([ + 'packages/react/package.json', + 'bindings/devup-ui-wasm/package.json', + ])('accepts %s when it is an existing managed manifest', (path) => { + const rejected = unmanagedPaths([path]) + expect(rejected).toEqual([]) + }) + + it.each([ + 'apps/landing/package.json', + 'package.json', + 'benchmark/tailwindcss/package.json', + 'packages/__tests__/package.json', + 'bindings/__missing_changepack_manifest__/package.json', + 'packages/react/src/package.json', + 'packages/react/README.md', + './packages/react/package.json', + 'packages//react/package.json', + 'packages/react/../react/package.json', + 'packages\\react\\package.json', + 'Packages/react/package.json', + 'packages/react/package.json/', + ])('rejects %s when it is unmanaged, missing or noncanonical', (path) => { + const rejected = unmanagedPaths([path]) + expect(rejected).toEqual([path]) + }) + + it('resolves every changes key when reading all changepack logs', async () => { + const logs = [ + ...new Bun.Glob('.changepacks/changepack_log_*.json').scanSync({ + cwd: root, + onlyFiles: true, + }), + ] + .map((path) => path.replaceAll('\\', '/')) + .sort() + const diagnostics: string[] = [] + for (const path of logs) { + const log: unknown = await Bun.file(resolve(root, path)).json() + assert(typeof log === 'object' && log !== null && 'changes' in log, path) + assert(typeof log.changes === 'object' && log.changes !== null, path) + assert(!Array.isArray(log.changes), path) + diagnostics.push( + ...unmanagedPaths(Object.keys(log.changes)).map( + (key) => `${path}: ${key}`, + ), + ) + } + expect(diagnostics.sort()).toEqual([]) + }) +})