From 1c541f3350b29f086c4acea2783871eb70eb4d4e Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Mon, 13 Jul 2026 14:13:03 +0530 Subject: [PATCH] build: enforce complete private release workflow --- .publish/ai/package.json | 2 +- .publish/authz/package.json | 2 +- .publish/cli/package.json | 22 +-- .publish/compiler/package.json | 2 +- .publish/core/package.json | 2 +- .publish/csr/package.json | 4 +- .publish/db/package.json | 2 +- .publish/dev-server/package.json | 26 ++-- .publish/encryption/package.json | 2 +- .publish/helpers/package.json | 4 +- .publish/i18n/package.json | 4 +- .publish/jwt/package.json | 2 +- .publish/mobile/package.json | 4 +- .publish/native/package.json | 2 +- .publish/oauth/package.json | 2 +- .publish/pubsub/package.json | 2 +- .publish/queue/package.json | 2 +- .publish/reactive/package.json | 2 +- .publish/router/package.json | 6 +- .publish/ssr/package.json | 4 +- .publish/styles/package.json | 4 +- .publish/test/package.json | 2 +- .publish/tracking/package.json | 2 +- .publish/ui/package.json | 4 +- .publish/uploader/package.json | 4 +- .publish/validation/package.json | 2 +- docs/PUBLISHING.md | 68 ++++++++- package.json | 2 + packages/ai/package.json | 2 +- packages/authz/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/update.ts | 21 +++ packages/cli/test/update.test.ts | 25 ++++ packages/compiler/package.json | 2 +- packages/core/package.json | 2 +- packages/csr/package.json | 2 +- packages/db/package.json | 2 +- packages/dev-server/package.json | 2 +- packages/encryption/package.json | 2 +- packages/helpers/package.json | 2 +- packages/i18n/package.json | 2 +- packages/jwt/package.json | 2 +- packages/mobile/package.json | 2 +- packages/native/package.json | 2 +- packages/oauth/package.json | 2 +- packages/pubsub/package.json | 2 +- packages/queue/package.json | 2 +- packages/reactive/package.json | 2 +- packages/router/package.json | 2 +- packages/ssr/package.json | 2 +- packages/styles/package.json | 2 +- packages/test/package.json | 2 +- packages/tracking/package.json | 2 +- packages/ui/package.json | 2 +- packages/uploader/package.json | 2 +- packages/validation/package.json | 2 +- scripts/release.ts | 246 +++++++++++++++++++++++++++++++ 57 files changed, 438 insertions(+), 92 deletions(-) create mode 100644 scripts/release.ts diff --git a/.publish/ai/package.json b/.publish/ai/package.json index e3d44324..78887f5b 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index 6a8e772e..895695b8 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index 26dd58d0..140a16ac 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,16 +28,16 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.2.17", - "@wrnexus/router": "^0.2.17", - "@wrnexus/csr": "^0.2.17", - "@wrnexus/compiler": "^0.2.17", - "@wrnexus/styles": "^0.2.17", - "@wrnexus/dev-server": "^0.2.17", - "@wrnexus/ui": "^0.2.17", - "@wrnexus/validation": "^0.2.17", - "@wrnexus/i18n": "^0.2.17", - "@wrnexus/db": "^0.2.17" + "@wrnexus/core": "^0.2.18", + "@wrnexus/router": "^0.2.18", + "@wrnexus/csr": "^0.2.18", + "@wrnexus/compiler": "^0.2.18", + "@wrnexus/styles": "^0.2.18", + "@wrnexus/dev-server": "^0.2.18", + "@wrnexus/ui": "^0.2.18", + "@wrnexus/validation": "^0.2.18", + "@wrnexus/i18n": "^0.2.18", + "@wrnexus/db": "^0.2.18" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 4e07ba73..f22ab6f5 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/core/package.json b/.publish/core/package.json index 3cf50c51..c7a20a3a 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index a6b9ad2c..2b5ef12d 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.17" + "@wrnexus/core": "^0.2.18" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index ea2bb3e1..505b2809 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index 3a7c4b3c..198608d8 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.17", - "@wrnexus/router": "^0.2.17", - "@wrnexus/ssr": "^0.2.17", - "@wrnexus/csr": "^0.2.17", - "@wrnexus/compiler": "^0.2.17", - "@wrnexus/styles": "^0.2.17", - "@wrnexus/ui": "^0.2.17", - "@wrnexus/validation": "^0.2.17", - "@wrnexus/i18n": "^0.2.17", - "@wrnexus/db": "^0.2.17", - "@wrnexus/pubsub": "^0.2.17", - "@wrnexus/uploader": "^0.2.17" + "@wrnexus/core": "^0.2.18", + "@wrnexus/router": "^0.2.18", + "@wrnexus/ssr": "^0.2.18", + "@wrnexus/csr": "^0.2.18", + "@wrnexus/compiler": "^0.2.18", + "@wrnexus/styles": "^0.2.18", + "@wrnexus/ui": "^0.2.18", + "@wrnexus/validation": "^0.2.18", + "@wrnexus/i18n": "^0.2.18", + "@wrnexus/db": "^0.2.18", + "@wrnexus/pubsub": "^0.2.18", + "@wrnexus/uploader": "^0.2.18" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 88919843..dbbb5487 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index b93c830d..04f8ef0f 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.17" + "@wrnexus/core": "^0.2.18" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index 64a1e5ca..b7c77cca 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.17" + "@wrnexus/core": "^0.2.18" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index 16ba3517..d25533f4 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index a46842e3..fa454978 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.17" + "@wrnexus/native": "^0.2.18" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index 76a27870..b203cf6a 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index d128dcf8..e93ef3a0 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index 001a2a20..fe75fe97 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index 0f9bde00..732142e5 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index 5b4d7912..e9fce405 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 7f7471b2..886d3de8 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.17", - "@wrnexus/core": "^0.2.17" + "@wrnexus/compiler": "^0.2.18", + "@wrnexus/core": "^0.2.18" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index 81768fdf..99a48ac3 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.17" + "@wrnexus/core": "^0.2.18" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 4a83b780..61fac9aa 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.17" + "@wrnexus/uploader": "^0.2.18" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index f11afbf7..da25dc1d 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 8db57371..2901320c 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index b58089b5..2a6abc59 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -22,7 +22,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.2.17" + "@wrnexus/core": "^0.2.18" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index 37218bc8..62966d87 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.17" + "@wrnexus/core": "^0.2.18" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index 8fc78f86..cca2e0a0 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index fe373a92..ae4b5fe9 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -21,7 +21,58 @@ The repo `.npmrc` points the scope at public npm: @wrnexus:registry=https://registry.npmjs.org/ ``` -## Publish +## Mandatory release workflow + +Do not publish packages with ad-hoc `npm publish` commands during a normal release. The +repository release commands enforce the complete framework + migration + documentation +workflow. + +Every release must have an entry in `packages/cli/src/update.ts`'s `MIGRATIONS` list. The +entry must be idempotent and conservative. If a release requires no project file change, +add a documented no-op migration entry so that the decision is explicit and reviewable. + +### 1. Prepare + +Align every `packages/*/package.json` version, add the update migration and tests, then run: + +```bash +bun run release:prepare +``` + +This command refuses to prepare a release when versions differ or migration coverage is +missing. It runs the complete framework check, builds all publish artifacts, and verifies +that every staged manifest uses the aligned version and `restricted` access. + +Review the generated `.publish` changes, then commit and push the framework repository. +The publish step will refuse to run from an uncommitted or unpushed framework checkout. + +### 2. Publish privately and update documentation + +```bash +bun run release:private +``` + +The command performs the remaining release as one guarded workflow: + +1. Rebuild and validate all private package artifacts from the pushed commit. +2. Publish every missing `@wrnexus/*` package in dependency order. Already-published + packages are verified and skipped, so an interrupted release can be resumed safely. +3. Confirm every package is private and run the newly published CLI updater, including its + migrations, against `D:\Company\wrnexusjs`. +4. Align every docs-site dependency, regenerate package pages twice, update + `public/llms.txt`, `public/llms-full.txt`, the sitemap, and the documentation index. +5. Run the docs-site check and production build, verify that `public/llms.txt` contains the + current version and every framework package, then commit and push the docs repository. + +If the documentation checkout lives elsewhere, set `WRNEXUS_DOCS_ROOT` before running the +publish command. Both repositories must start clean and synchronized with their active +remote branches. + +Only use the individual commands printed by `scripts/publish-packages.ts` for diagnosed +recovery. Finish recovery by rerunning `bun run release:private`; it is the authoritative +completion check. + +## Publishing implementation The packages ship raw TypeScript in the repo (`main`/`exports` → `src/*.ts`), so they are **built** for publishing. `scripts/publish-packages.ts` builds each @@ -36,13 +87,14 @@ package with [tsup](https://tsup.egoist.dev) (bundled JS + a single `.d.ts`) and The dev `package.json` (pointing at `src/`) is **never modified**, so in-repo dev and tests keep working. -### 1. Private-first (test before the world sees it) +### Private staging details -By default the script stages packages as **`restricted`** (private). Requires the -`wrnexus` org on a paid Teams plan. +By default the lower-level staging script creates **`restricted`** (private) manifests. +It is called by the mandatory release commands and requires the `wrnexus` org on a paid +Teams plan. ```bash -# Build + stage all packages (access: restricted) +# Lower-level recovery/debug command only bun run scripts/publish-packages.ts # Publish (deps first). The script prints the exact order + --access flag; e.g. @@ -52,8 +104,8 @@ npm publish .publish/compiler --access restricted npm publish .publish/cli --access restricted ``` -Test the private packages (see below). When satisfied, **flip every package to -public** (no republish needed): +When an approved release is ready to become public, **flip every package to public** (no +republish needed): ```bash npm access public @wrnexus/core @@ -61,7 +113,7 @@ npm access public @wrnexus/compiler # … the script prints the full list … ``` -### 2. Public directly +### Public staging for an explicitly approved public release To stage public manifests from the start (free org is enough): diff --git a/package.json b/package.json index 9155b1dc..1486eba1 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "test": "bun test packages", "test:coverage": "bun test --coverage packages", "security:audit": "bun audit", + "release:prepare": "bun run scripts/release.ts prepare", + "release:private": "bun run scripts/release.ts publish", "test:db:live": "bash scripts/test-live-db.sh", "typecheck": "tsc --noEmit", "lint": "eslint .", diff --git a/packages/ai/package.json b/packages/ai/package.json index 831f8ade..8e4fed92 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", diff --git a/packages/authz/package.json b/packages/authz/package.json index cecbdb8e..2e57a6cc 100644 --- a/packages/authz/package.json +++ b/packages/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index f86daf7b..709abc86 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts index 5a9eae8a..a5662f7e 100644 --- a/packages/cli/src/update.ts +++ b/packages/cli/src/update.ts @@ -156,8 +156,29 @@ const MIGRATIONS: Migration[] = [ if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8"); }, }, + { + version: "0.2.18", + id: "add-helpers-package", + description: "Add @wrnexus/helpers to existing runnable applications", + apply(ctx) { + // Workspace roots do not consume application Context helpers directly. + if (!existsSync(join(ctx.appRoot, "app", "pages"))) return; + const file = join(ctx.appRoot, "package.json"); + const pkg = JSON.parse(readFileSync(file, "utf8")) as Record; + const dependencies = (pkg.dependencies ??= {} as Record); + if (dependencies["@wrnexus/helpers"]) return; + dependencies["@wrnexus/helpers"] = `^${ctx.to}`; + ctx.log(`+ @wrnexus/helpers ^${ctx.to}`); + if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8"); + }, + }, ]; +/** Release tooling uses this to require an explicit migration entry per version. */ +export function updateMigrationVersions(): string[] { + return [...new Set(MIGRATIONS.map((migration) => migration.version))]; +} + /** Bump every `@wrnexus/*` range to `^target`. Returns the human-readable changes. */ function bumpDeps(pkg: Record, target: string): string[] { const changed: string[] = []; diff --git a/packages/cli/test/update.test.ts b/packages/cli/test/update.test.ts index d751cae5..e9e9bc3b 100644 --- a/packages/cli/test/update.test.ts +++ b/packages/cli/test/update.test.ts @@ -36,3 +36,28 @@ test("updateApp migrates project files without marking an unverified update comp rmSync(root, { recursive: true, force: true }); } }); + +test("0.2.18 migration adds helpers to existing apps and is idempotent", () => { + const root = mkdtempSync(join(tmpdir(), "wrnexus-update-")); + mkdirSync(join(root, "app", "pages"), { recursive: true }); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ + name: "existing-app", + dependencies: { "@wrnexus/core": "^0.2.17" }, + wrnexus: { version: "0.2.17" }, + }), + ); + + try { + updateApp(root, "0.2.18", false); + updateApp(root, "0.2.18", false); + const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); + expect(pkg.dependencies["@wrnexus/helpers"]).toBe("^0.2.18"); + expect( + Object.keys(pkg.dependencies).filter((name) => name === "@wrnexus/helpers"), + ).toHaveLength(1); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 04e5a7ee..1eb26f61 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/core/package.json b/packages/core/package.json index bdfaf7e2..79ca1ee0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index 1228a92a..444d96d3 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/db/package.json b/packages/db/package.json index 0129a34b..541fc3ee 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 4c6dcbec..42017e87 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/encryption/package.json b/packages/encryption/package.json index fa8d17c7..5189e521 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 3dec7d23..f549cc34 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 49aa007c..b56bda94 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index f2898640..5996535e 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index ae6ffddd..9d134e45 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index fa4f5ed9..01188db0 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index 6b2b28f0..c10437c5 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 0effae08..341def4e 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index 344e5358..5f793572 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index 13b4c48a..f9cfa726 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index 89e9d77f..c2c4a0f6 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index da2e12e3..a4222f1e 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index 7aec660c..e93c06bd 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.17", + "version": "0.2.18", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index bd4a7508..a4d9f814 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 7843eff7..8e02a8b7 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 5f7cef63..9458e237 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index d907a8d1..6a6e4d2f 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index f0f040a1..ba991f34 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.17", + "version": "0.2.18", "private": true, "type": "module", "main": "src/index.ts", diff --git a/scripts/release.ts b/scripts/release.ts new file mode 100644 index 00000000..a2addd1f --- /dev/null +++ b/scripts/release.ts @@ -0,0 +1,246 @@ +/** + * Mandatory WRNexusJS private release workflow. + * + * bun run release:prepare # verify, enforce migration coverage, stage packages + * # review, commit, and push the framework repository + * bun run release:private # publish/resume, update docs, verify llms, commit + push docs + * + * Set WRNEXUS_DOCS_ROOT when the docs repository is not D:\\Company\\wrnexusjs. + */ + +import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; +import { updateMigrationVersions } from "../packages/cli/src/update.ts"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const docsRoot = resolve(process.env.WRNEXUS_DOCS_ROOT ?? "D:\\Company\\wrnexusjs"); +const npm = process.platform === "win32" ? "npm.cmd" : "npm"; +const mode = process.argv[2]; + +interface PackageInfo { + dir: string; + name: string; + version: string; + dependencies: Record; +} + +function run( + command: string, + args: string[], + cwd = root, + options: { capture?: boolean; allowFailure?: boolean } = {}, +) { + const result = spawnSync(command, args, { + cwd, + encoding: options.capture ? "utf8" : undefined, + stdio: options.capture ? "pipe" : "inherit", + env: process.env, + }); + if (result.error) throw result.error; + if (result.status !== 0 && !options.allowFailure) { + const detail = options.capture ? `\n${result.stderr || result.stdout || ""}` : ""; + throw new Error(`${command} ${args.join(" ")} failed with ${result.status}.${detail}`); + } + return result; +} + +function output(command: string, args: string[], cwd = root): string { + return String(run(command, args, cwd, { capture: true }).stdout ?? "").trim(); +} + +function packages(): PackageInfo[] { + return readdirSync(join(root, "packages"), { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => { + const file = join(root, "packages", entry.name, "package.json"); + if (!existsSync(file)) return null; + const manifest = JSON.parse(readFileSync(file, "utf8")); + if (!manifest.name?.startsWith("@wrnexus/")) return null; + return { + dir: entry.name, + name: manifest.name as string, + version: manifest.version as string, + dependencies: (manifest.dependencies ?? {}) as Record, + }; + }) + .filter((value): value is PackageInfo => value !== null); +} + +function alignedVersion(all: PackageInfo[]): string { + const versions = new Set(all.map((pkg) => pkg.version)); + if (versions.size !== 1) { + throw new Error(`Package versions are not aligned: ${[...versions].join(", ")}`); + } + const version = all[0]?.version; + if (!version || !/^\d+\.\d+\.\d+$/.test(version)) { + throw new Error(`Invalid release version: ${version ?? "missing"}`); + } + return version; +} + +function sortedPackages(all: PackageInfo[]): PackageInfo[] { + const byName = new Map(all.map((pkg) => [pkg.name, pkg])); + const seen = new Set(); + const result: PackageInfo[] = []; + const visit = (pkg: PackageInfo) => { + if (seen.has(pkg.name)) return; + seen.add(pkg.name); + for (const dependency of Object.keys(pkg.dependencies)) { + const internal = byName.get(dependency); + if (internal) visit(internal); + } + result.push(pkg); + }; + all.forEach(visit); + return result; +} + +function requireMigration(version: string) { + if (!updateMigrationVersions().includes(version)) { + throw new Error( + `Release ${version} has no update migration entry. Add an idempotent migration (a documented no-op is acceptable) to packages/cli/src/update.ts.`, + ); + } +} + +function validateStaging(all: PackageInfo[], version: string) { + for (const pkg of all) { + const stage = join(root, ".publish", pkg.dir); + const manifestPath = join(stage, "package.json"); + const dist = join(stage, "dist", "index.js"); + if (!existsSync(manifestPath) || !existsSync(dist)) { + throw new Error(`Missing staged artifact for ${pkg.name}; run release:prepare again.`); + } + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + if (manifest.version !== version || manifest.publishConfig?.access !== "restricted") { + throw new Error(`${pkg.name} staging is not restricted ${version}.`); + } + } +} + +function requireCleanAndPushed(repo: string, label: string) { + if (output("git", ["status", "--porcelain"], repo)) { + throw new Error(`${label} repository has uncommitted changes.`); + } + run("git", ["fetch", "origin"], repo); + const branch = output("git", ["branch", "--show-current"], repo); + const head = output("git", ["rev-parse", "HEAD"], repo); + const remote = output("git", ["rev-parse", `origin/${branch}`], repo); + if (head !== remote) throw new Error(`${label} HEAD is not synchronized with origin/${branch}.`); +} + +function registryHas(pkg: PackageInfo, version: string): boolean { + const result = run(npm, ["view", `${pkg.name}@${version}`, "version", "--json"], root, { + capture: true, + allowFailure: true, + }); + return result.status === 0 && String(result.stdout).includes(version); +} + +function verifyPrivate(pkg: PackageInfo, version: string) { + const status = output(npm, ["access", "get", "status", pkg.name]); + if (!status.endsWith(": private")) throw new Error(`${pkg.name}@${version} is not private.`); +} + +function syncDocsManifest(all: PackageInfo[], version: string) { + const file = join(docsRoot, "package.json"); + const manifest = JSON.parse(readFileSync(file, "utf8")); + manifest.dependencies ??= {}; + manifest.devDependencies ??= {}; + for (const pkg of all) { + if (pkg.name === "@wrnexus/cli") { + manifest.devDependencies[pkg.name] = `^${version}`; + delete manifest.dependencies[pkg.name]; + } else { + manifest.dependencies[pkg.name] = `^${version}`; + delete manifest.devDependencies[pkg.name]; + } + } + manifest.wrnexus = { ...(manifest.wrnexus ?? {}), version }; + writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n"); +} + +function verifyDocs(all: PackageInfo[], version: string) { + const manifest = JSON.parse(readFileSync(join(docsRoot, "package.json"), "utf8")); + if (manifest.wrnexus?.version !== version) throw new Error("Docs version marker is stale."); + const llms = readFileSync(join(docsRoot, "public", "llms.txt"), "utf8"); + if (!llms.includes(`# WRNexusJS documentation ${version}`)) { + throw new Error("Docs public/llms.txt has a stale or missing release header."); + } + for (const pkg of all) { + if (!llms.includes(`## ${pkg.name}`)) { + throw new Error(`Docs public/llms.txt is missing ${pkg.name}.`); + } + } + const full = readFileSync(join(docsRoot, "public", "llms-full.txt"), "utf8"); + if (!full.includes(`WRNexusJS ${version}`) || !full.includes("/packages/helpers")) { + throw new Error("Docs public/llms-full.txt is stale or incomplete."); + } +} + +function prepare(all: PackageInfo[], version: string) { + requireMigration(version); + console.log(`\nPreparing WRNexusJS ${version} (${all.length} packages)…\n`); + run(process.execPath, ["run", "check"]); + run(process.execPath, ["run", "scripts/publish-packages.ts"]); + validateStaging(all, version); + console.log( + `\n✓ Release ${version} is prepared. Review changes, commit and push the framework repository, then run: bun run release:private\n`, + ); +} + +function publish(all: PackageInfo[], version: string) { + if (!existsSync(docsRoot)) throw new Error(`Docs repository not found: ${docsRoot}`); + requireMigration(version); + requireCleanAndPushed(root, "Framework"); + requireCleanAndPushed(docsRoot, "Docs"); + + // Rebuild from the committed source and refuse publication if staging drifts. + run(process.execPath, ["run", "scripts/publish-packages.ts"]); + validateStaging(all, version); + requireCleanAndPushed(root, "Framework after staging"); + output(npm, ["whoami"]); + + for (const pkg of sortedPackages(all)) { + if (registryHas(pkg, version)) { + console.log(` = ${pkg.name}@${version} already published; verifying`); + } else { + console.log(` + publishing ${pkg.name}@${version}`); + run(npm, ["publish", join(root, ".publish", pkg.dir), "--access", "restricted"]); + } + verifyPrivate(pkg, version); + } + + console.log(`\nUpdating docs repository at ${docsRoot}…\n`); + run( + process.execPath, + ["x", "--package", `@wrnexus/cli@${version}`, "wrnexus", "update", `--version=${version}`], + docsRoot, + ); + syncDocsManifest(all, version); + run(process.execPath, ["install"], docsRoot); + run(npm, ["install", "--package-lock-only", "--ignore-scripts"], docsRoot); + run(process.execPath, ["run", "docs:generate"], docsRoot); + run(process.execPath, ["run", "docs:generate"], docsRoot); + run(process.execPath, ["run", "check"], docsRoot); + run(process.execPath, ["run", "build"], docsRoot); + verifyDocs(all, version); + + run("git", ["add", "--all"], docsRoot); + if (output("git", ["status", "--porcelain"], docsRoot)) { + run("git", ["commit", "-m", `docs: update portal for WRNexusJS ${version}`], docsRoot); + } + run("git", ["push", "origin", "HEAD"], docsRoot); + requireCleanAndPushed(docsRoot, "Docs"); + console.log(`\n✓ WRNexusJS ${version} published privately and docs are updated and pushed.\n`); +} + +const all = packages(); +const version = alignedVersion(all); +if (mode === "prepare") prepare(all, version); +else if (mode === "publish") publish(all, version); +else { + throw new Error("Usage: bun scripts/release.ts prepare | publish"); +}