From db8066fc779525d5dd7e451dc53503a2014e5357 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:01:54 +0800 Subject: [PATCH] chore(distribution): remove unused npm channel Fixes #253 --- .github/workflows/ci.yml | 14 - .github/workflows/release-please.yml | 2 - .github/workflows/release.yml | 172 --------- dist/README.md | 48 +-- dist/npm/wright-darwin-arm64/README.md | 9 - dist/npm/wright-darwin-arm64/package.json | 27 -- dist/npm/wright-darwin-x64/README.md | 9 - dist/npm/wright-darwin-x64/package.json | 27 -- dist/npm/wright-linux-x64/README.md | 9 - dist/npm/wright-linux-x64/package.json | 27 -- dist/npm/wright-win32-x64/README.md | 9 - dist/npm/wright-win32-x64/package.json | 27 -- dist/npm/wright/README.md | 43 --- dist/npm/wright/bin/wright-lsp.js | 27 -- dist/npm/wright/bin/wright.js | 27 -- dist/npm/wright/index.d.ts | 24 -- dist/npm/wright/index.js | 81 ----- dist/npm/wright/package.json | 36 -- docs/release.md | 38 +- scripts/package-npm.py | 406 ---------------------- scripts/test-npm.py | 297 ---------------- scripts/tests/test_npm_packaging.py | 230 ------------ scripts/update-dist-manifests.py | 117 +------ scripts/verify-dist.py | 77 +--- 24 files changed, 16 insertions(+), 1767 deletions(-) delete mode 100644 dist/npm/wright-darwin-arm64/README.md delete mode 100644 dist/npm/wright-darwin-arm64/package.json delete mode 100644 dist/npm/wright-darwin-x64/README.md delete mode 100644 dist/npm/wright-darwin-x64/package.json delete mode 100644 dist/npm/wright-linux-x64/README.md delete mode 100644 dist/npm/wright-linux-x64/package.json delete mode 100644 dist/npm/wright-win32-x64/README.md delete mode 100644 dist/npm/wright-win32-x64/package.json delete mode 100644 dist/npm/wright/README.md delete mode 100755 dist/npm/wright/bin/wright-lsp.js delete mode 100755 dist/npm/wright/bin/wright.js delete mode 100644 dist/npm/wright/index.d.ts delete mode 100644 dist/npm/wright/index.js delete mode 100644 dist/npm/wright/package.json delete mode 100755 scripts/package-npm.py delete mode 100755 scripts/test-npm.py delete mode 100644 scripts/tests/test_npm_packaging.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52d75a4..af6b383 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -648,26 +648,12 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} cache-on-failure: false - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version: 22 - - name: Build wright CLI and LSP run: cargo build --locked -p wright-cli -p wright-lsp - name: Validate package-manager metadata and install script run: python scripts/verify-dist.py - # #183 moved the npm tarball determinism/executable-bit regressions - # (#123) to scripts/tests and declared them part of distribution - # validation; this step is that wiring. - - name: Run npm packaging unit regressions (#123) - run: python -m unittest discover -s scripts/tests - - name: Run installer functional tests if: runner.os != 'Windows' run: scripts/test-install.sh - - - name: Run npm distribution smoke tests - run: python scripts/test-npm.py --binaries-dir target/debug diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 598f676..3a46321 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -86,5 +86,3 @@ jobs: secrets: inherit permissions: contents: write - id-token: write - packages: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f542b55..57abe23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,14 +19,9 @@ on: GH_TOKEN: description: Dedicated cross-repository token with write access to wrightkit/homebrew-tap required: true - NPM_TOKEN: - description: Optional npmjs.org publication token - required: false permissions: contents: write - id-token: write - packages: write jobs: release-gates: @@ -356,177 +351,10 @@ jobs: git push origin HEAD echo "pushed wright.rb for ${RELEASE_TAG} to wrightkit/homebrew-tap" - package-npm: - name: Package npm distribution - needs: upload-artifacts - runs-on: ubuntu-latest - env: - RELEASE_TAG: ${{ inputs.tag }} - steps: - - name: Check out repository - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref }} - fetch-depth: 0 - - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version: 22 - registry-url: "https://registry.npmjs.org" - - - name: Download release artifacts - uses: actions/download-artifact@v8 - with: - pattern: wright-* - path: artifacts - - - name: Package npm distribution tarballs - run: | - set -euo pipefail - VERSION="${RELEASE_TAG#v}" - python3 scripts/package-npm.py \ - --version "$VERSION" \ - --artifacts-dir artifacts \ - --out-dir "$GITHUB_WORKSPACE/npm-packages" - ls -lh "$GITHUB_WORKSPACE/npm-packages" - - - name: Smoke test host npm package (Linux x64) - run: | - set -euo pipefail - VERSION="${RELEASE_TAG#v}" - SANDBOX="$(mktemp -d)" - cd "$SANDBOX" - npm init -y - PLATFORM_TB="$(ls "$GITHUB_WORKSPACE"/npm-packages/wrightkit-wright-linux-x64-*.tgz "$GITHUB_WORKSPACE"/npm-packages/*wright-linux-x64-*.tgz 2>/dev/null | head -n 1)" - META_TB="$(ls "$GITHUB_WORKSPACE"/npm-packages/wrightkit-wright-[0-9]*.tgz "$GITHUB_WORKSPACE"/npm-packages/*wright-[0-9]*.tgz 2>/dev/null | head -n 1)" - npm install "$PLATFORM_TB" "$META_TB" - npx wright --version | grep -F "$VERSION" - npx wright-lsp --version | grep -F "$VERSION" - npx wright compile "$GITHUB_WORKSPACE/compatibility/fixtures/synthetic/basic-rule/source.opy" --profile compat >/dev/null - npx wright check "$GITHUB_WORKSPACE/scenarios/loops.opy" --profile compat >/dev/null - node -e ' - const { getBinaryPath, getPlatformPackageName } = require("@wrightkit/wright"); - const p = getBinaryPath("wright"); - console.log("Resolved:", p); - ' - echo "npm packages smoke test passed on $(uname -s) $(uname -m)" - - - name: Attach npm tarballs to the draft Release - env: - GH_TOKEN: ${{ github.token }} - run: >- - gh release upload "${RELEASE_TAG}" npm-packages/*.tgz --clobber - --repo "${GITHUB_REPOSITORY}" - - - name: Upload npm tarballs for registry publishing - uses: actions/upload-artifact@v7 - with: - name: npm-packages - path: npm-packages/*.tgz - if-no-files-found: error - - publish-npm: - name: Publish packages to npm registry - needs: - - package-npm - - publish-release - runs-on: ubuntu-latest - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - steps: - - name: Download npm tarballs - uses: actions/download-artifact@v8 - with: - name: npm-packages - path: npm-packages - - - name: Set up Node.js for npmjs.org - uses: actions/setup-node@v7 - with: - node-version: 22 - registry-url: "https://registry.npmjs.org" - - - name: Publish to npmjs.org - if: env.NPM_TOKEN != '' - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - set -euo pipefail - publish_if_missing() { - local pkg="$1" - local metadata name version published - metadata="$(tar -xOf "${pkg}" package/package.json)" - name="$(node -e 'let s=""; process.stdin.on("data", c => s += c).on("end", () => process.stdout.write(JSON.parse(s).name))' <<<"${metadata}")" - version="$(node -e 'let s=""; process.stdin.on("data", c => s += c).on("end", () => process.stdout.write(JSON.parse(s).version))' <<<"${metadata}")" - published="$(npm view "${name}@${version}" version --registry=https://registry.npmjs.org 2>/dev/null || true)" - if [[ "${published}" == "${version}" ]]; then - echo "${name}@${version} is already published; skipping" - return - fi - echo "Publishing ${pkg} to npmjs.org..." - npm publish "${pkg}" --access public --provenance - } - for pkg in "$GITHUB_WORKSPACE"/npm-packages/*.tgz; do - publish_if_missing "${pkg}" - done - - - name: Skip npmjs.org publication - if: env.NPM_TOKEN == '' - run: echo "NPM_TOKEN is not configured; skipping npmjs.org publication." - - publish-github-packages: - name: Publish packages to GitHub Packages - needs: - - package-npm - - publish-release - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Download npm tarballs - uses: actions/download-artifact@v8 - with: - name: npm-packages - path: npm-packages - - - name: Set up Node.js for GitHub Packages - uses: actions/setup-node@v7 - with: - node-version: 22 - registry-url: "https://npm.pkg.github.com" - - - name: Publish to GitHub Packages - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - publish_if_missing() { - local pkg="$1" - local metadata name version published - metadata="$(tar -xOf "${pkg}" package/package.json)" - name="$(node -e 'let s=""; process.stdin.on("data", c => s += c).on("end", () => process.stdout.write(JSON.parse(s).name))' <<<"${metadata}")" - version="$(node -e 'let s=""; process.stdin.on("data", c => s += c).on("end", () => process.stdout.write(JSON.parse(s).version))' <<<"${metadata}")" - published="$(npm view "${name}@${version}" version --registry=https://npm.pkg.github.com 2>/dev/null || true)" - if [[ "${published}" == "${version}" ]]; then - echo "${name}@${version} is already published; skipping" - return - fi - echo "Publishing ${pkg} to GitHub Packages..." - npm publish "${pkg}" \ - --registry=https://npm.pkg.github.com \ - --access public - } - for pkg in "$GITHUB_WORKSPACE"/npm-packages/*.tgz; do - publish_if_missing "${pkg}" - done - publish-release: name: Publish completed GitHub Release needs: - package-manifests - - package-npm runs-on: ubuntu-latest environment: release steps: diff --git a/dist/README.md b/dist/README.md index 0e2550f..1b5662f 100644 --- a/dist/README.md +++ b/dist/README.md @@ -1,7 +1,7 @@ -# Wright Package-Manager Distribution (#108, #121) +# Wright Package-Manager Distribution (#108) -This directory holds the package-manager, installer, and npm distribution -metadata that make the canonical GitHub Release artifacts installable through +This directory holds the package-manager and installer metadata that make the +canonical GitHub Release artifacts installable through platform-native channels. Nothing in here rebuilds Wright: every manifest and package consumes the published `wright--.` archives and their `.sha256` checksums from @@ -13,7 +13,6 @@ archives and their `.sha256` checksums from | Homebrew | `homebrew/wright.rb` | macOS `.tar.gz` archives (arm64 + x86_64) with per-arch `sha256` | | WinGet | `winget/manifests/w/WrightKit/Wright//` | Windows `.zip` with `InstallerSha256` | | Scoop | `scoop/wright.json` | Windows `.zip` with `hash` | -| npm / npx | `npm/wright/`, `npm/wright-*/` | Native release binaries packaged directly into platform npm packages | Standalone installs (the Unix installer or manual archives) upgrade in place with `wright update`, which consumes the same release archives and checksums @@ -49,14 +48,8 @@ stages complete: - `wright-.winget.zip` (unzip into a winget-pkgs checkout) - `wright-.scoop.json` -2. `package-npm` packages the release binaries into platform-native npm packages - via `scripts/package-npm.py`, runs smoke tests on the packaged artifacts, - attaches the `.tgz` tarballs to the draft Release, and publishes the same - tarballs to npmjs.org and GitHub Packages. - -3. `publish-release` marks the draft Release public only after the Homebrew tap - and registry jobs succeed. Re-running an already completed registry stage - skips package versions that already exist. +2. `publish-release` marks the draft Release public after the native and + package-manager stages succeed. ### Homebrew @@ -101,35 +94,6 @@ stages complete: - User experience: `scoop bucket add wrightkit https://github.com/wrightkit/scoop-bucket && scoop install wright`. -### npm / npx (#121) - -Wright distributes native binaries via npm for seamless integration with Node.js -tooling and CI environments without requiring Rust/Cargo compilation or -postinstall download scripts. - -- **Meta package**: `@wrightkit/wright` exposes `wright` and `wright-lsp` CLI - binaries and exports `getBinaryPath()` for programmatic Node.js consumers. - It selects the matching native platform package via `optionalDependencies`. -- **Platform packages**: - - `@wrightkit/wright-darwin-arm64` (macOS Apple Silicon) - - `@wrightkit/wright-darwin-x64` (macOS Intel) - - `@wrightkit/wright-linux-x64` (Linux x64) - - `@wrightkit/wright-win32-x64` (Windows x64) -- **User experience**: - - Direct execution: `npx wright --version` - - Project dependency: `npm install @wrightkit/wright` -- **Programmatic usage**: - ```javascript - const { getBinaryPath } = require('@wrightkit/wright'); - const wrightBin = getBinaryPath('wright'); - ``` -- **Boundary**: npm is strictly a package/distribution layer for the native Rust - CLI; there is no JavaScript reimplementation of the Wright compiler. -- **Registries**: the release workflow publishes to npmjs.org when `NPM_TOKEN` - is configured, and always publishes to GitHub Packages with the workflow - `GITHUB_TOKEN`. GitHub Packages consumers must configure the `@wrightkit` - scope to use `https://npm.pkg.github.com` and authenticate with GitHub. - ## Shell completions (#186) - Standalone installations (`install.sh`) automatically run `wright completion install` @@ -143,8 +107,6 @@ postinstall download scripts. - CI runs `scripts/verify-dist.py` on every commit: it regenerates the checked-in manifests with the workspace version and fails on any mismatch, so a version bump without regenerated metadata is caught before merge. -- CI also runs `scripts/test-npm.py` to package and execute clean-install smoke - tests across supported platforms. - The release workflow consumes the published `.sha256` files when generating the attached manifests, so the attached metadata cannot drift from the actual artifacts of that release. diff --git a/dist/npm/wright-darwin-arm64/README.md b/dist/npm/wright-darwin-arm64/README.md deleted file mode 100644 index 4686be0..0000000 --- a/dist/npm/wright-darwin-arm64/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# @wrightkit/wright-darwin-arm64 - -Native binary package of [Wright](https://github.com/wrightkit/wright) for macOS Apple Silicon (`darwin-arm64`). - -This is a platform-specific distribution package consumed by [`@wrightkit/wright`](https://www.npmjs.com/package/@wrightkit/wright). Users and downstream tools should generally depend on and install `@wrightkit/wright` directly. - -## License - -AGPL-3.0-or-later diff --git a/dist/npm/wright-darwin-arm64/package.json b/dist/npm/wright-darwin-arm64/package.json deleted file mode 100644 index 3e5e599..0000000 --- a/dist/npm/wright-darwin-arm64/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@wrightkit/wright-darwin-arm64", - "version": "0.2.17", - "description": "macOS ARM64 (Apple Silicon) native binary for Wright", - "license": "AGPL-3.0-or-later", - "repository": { - "type": "git", - "url": "https://github.com/wrightkit/wright.git" - }, - "homepage": "https://github.com/wrightkit/wright", - "bugs": { - "url": "https://github.com/wrightkit/wright/issues" - }, - "os": [ - "darwin" - ], - "cpu": [ - "arm64" - ], - "files": [ - "wright", - "wright-lsp", - "version.json", - "README.md", - "LICENSE" - ] -} diff --git a/dist/npm/wright-darwin-x64/README.md b/dist/npm/wright-darwin-x64/README.md deleted file mode 100644 index 01e453f..0000000 --- a/dist/npm/wright-darwin-x64/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# @wrightkit/wright-darwin-x64 - -Native binary package of [Wright](https://github.com/wrightkit/wright) for macOS Intel (`darwin-x64`). - -This is a platform-specific distribution package consumed by [`@wrightkit/wright`](https://www.npmjs.com/package/@wrightkit/wright). Users and downstream tools should generally depend on and install `@wrightkit/wright` directly. - -## License - -AGPL-3.0-or-later diff --git a/dist/npm/wright-darwin-x64/package.json b/dist/npm/wright-darwin-x64/package.json deleted file mode 100644 index ac74156..0000000 --- a/dist/npm/wright-darwin-x64/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@wrightkit/wright-darwin-x64", - "version": "0.2.17", - "description": "macOS x64 (Intel) native binary for Wright", - "license": "AGPL-3.0-or-later", - "repository": { - "type": "git", - "url": "https://github.com/wrightkit/wright.git" - }, - "homepage": "https://github.com/wrightkit/wright", - "bugs": { - "url": "https://github.com/wrightkit/wright/issues" - }, - "os": [ - "darwin" - ], - "cpu": [ - "x64" - ], - "files": [ - "wright", - "wright-lsp", - "version.json", - "README.md", - "LICENSE" - ] -} diff --git a/dist/npm/wright-linux-x64/README.md b/dist/npm/wright-linux-x64/README.md deleted file mode 100644 index b96551b..0000000 --- a/dist/npm/wright-linux-x64/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# @wrightkit/wright-linux-x64 - -Native binary package of [Wright](https://github.com/wrightkit/wright) for Linux x64 (`linux-x64`). - -This is a platform-specific distribution package consumed by [`@wrightkit/wright`](https://www.npmjs.com/package/@wrightkit/wright). Users and downstream tools should generally depend on and install `@wrightkit/wright` directly. - -## License - -AGPL-3.0-or-later diff --git a/dist/npm/wright-linux-x64/package.json b/dist/npm/wright-linux-x64/package.json deleted file mode 100644 index a409d82..0000000 --- a/dist/npm/wright-linux-x64/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@wrightkit/wright-linux-x64", - "version": "0.2.17", - "description": "Linux x64 native binary for Wright", - "license": "AGPL-3.0-or-later", - "repository": { - "type": "git", - "url": "https://github.com/wrightkit/wright.git" - }, - "homepage": "https://github.com/wrightkit/wright", - "bugs": { - "url": "https://github.com/wrightkit/wright/issues" - }, - "os": [ - "linux" - ], - "cpu": [ - "x64" - ], - "files": [ - "wright", - "wright-lsp", - "version.json", - "README.md", - "LICENSE" - ] -} diff --git a/dist/npm/wright-win32-x64/README.md b/dist/npm/wright-win32-x64/README.md deleted file mode 100644 index e99e578..0000000 --- a/dist/npm/wright-win32-x64/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# @wrightkit/wright-win32-x64 - -Native binary package of [Wright](https://github.com/wrightkit/wright) for Windows x64 (`win32-x64`). - -This is a platform-specific distribution package consumed by [`@wrightkit/wright`](https://www.npmjs.com/package/@wrightkit/wright). Users and downstream tools should generally depend on and install `@wrightkit/wright` directly. - -## License - -AGPL-3.0-or-later diff --git a/dist/npm/wright-win32-x64/package.json b/dist/npm/wright-win32-x64/package.json deleted file mode 100644 index 369c559..0000000 --- a/dist/npm/wright-win32-x64/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@wrightkit/wright-win32-x64", - "version": "0.2.17", - "description": "Windows x64 native binary for Wright", - "license": "AGPL-3.0-or-later", - "repository": { - "type": "git", - "url": "https://github.com/wrightkit/wright.git" - }, - "homepage": "https://github.com/wrightkit/wright", - "bugs": { - "url": "https://github.com/wrightkit/wright/issues" - }, - "os": [ - "win32" - ], - "cpu": [ - "x64" - ], - "files": [ - "wright.exe", - "wright-lsp.exe", - "version.json", - "README.md", - "LICENSE" - ] -} diff --git a/dist/npm/wright/README.md b/dist/npm/wright/README.md deleted file mode 100644 index 93187f0..0000000 --- a/dist/npm/wright/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# @wrightkit/wright - -Official npm distribution package for the native [Wright](https://github.com/wrightkit/wright) toolchain. - -Wright is a tooling-first semantic platform and compiler for the Overwatch Workshop and OverPy ecosystem, natively written in Rust. - -**Note**: This package is a distribution wrapper for the native Rust binary, **not** a JavaScript reimplementation. When installed via npm, it downloads the precompiled native binary for your platform without requiring Rust or a local build step. - -## Installation - -```bash -# Local dependency -npm install @wrightkit/wright - -# Run via npx -npx wright --version -npx wright check main.opy -``` - -## Programmatic API - -Downstream Node.js and TypeScript tools can import `@wrightkit/wright` to locate the native binary: - -```javascript -const { getBinaryPath } = require('@wrightkit/wright'); - -// Get path to native 'wright' executable -const wrightBin = getBinaryPath('wright'); - -// Get path to native 'wright-lsp' executable -const lspBin = getBinaryPath('wright-lsp'); -``` - -## Supported Platforms - -- macOS Apple Silicon (arm64): `@wrightkit/wright-darwin-arm64` -- macOS Intel (x64): `@wrightkit/wright-darwin-x64` -- Linux (x64): `@wrightkit/wright-linux-x64` -- Windows (x64): `@wrightkit/wright-win32-x64` - -## License - -AGPL-3.0-or-later diff --git a/dist/npm/wright/bin/wright-lsp.js b/dist/npm/wright/bin/wright-lsp.js deleted file mode 100755 index 3077293..0000000 --- a/dist/npm/wright/bin/wright-lsp.js +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env node -const { getBinaryPath } = require('../index.js'); -const { spawnSync } = require('child_process'); - -let binPath; -try { - binPath = getBinaryPath('wright-lsp'); -} catch (err) { - console.error(err.message); - process.exit(1); -} - -const result = spawnSync(binPath, process.argv.slice(2), { - stdio: 'inherit', - windowsHide: true, -}); - -if (result.error) { - console.error(`Failed to execute wright-lsp at "${binPath}": ${result.error.message}`); - process.exit(1); -} - -if (result.signal) { - process.kill(process.pid, result.signal); -} else { - process.exit(result.status ?? 0); -} diff --git a/dist/npm/wright/bin/wright.js b/dist/npm/wright/bin/wright.js deleted file mode 100755 index b78f1cd..0000000 --- a/dist/npm/wright/bin/wright.js +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env node -const { getBinaryPath } = require('../index.js'); -const { spawnSync } = require('child_process'); - -let binPath; -try { - binPath = getBinaryPath('wright'); -} catch (err) { - console.error(err.message); - process.exit(1); -} - -const result = spawnSync(binPath, process.argv.slice(2), { - stdio: 'inherit', - windowsHide: true, -}); - -if (result.error) { - console.error(`Failed to execute wright at "${binPath}": ${result.error.message}`); - process.exit(1); -} - -if (result.signal) { - process.kill(process.pid, result.signal); -} else { - process.exit(result.status ?? 0); -} diff --git a/dist/npm/wright/index.d.ts b/dist/npm/wright/index.d.ts deleted file mode 100644 index 7ea16b0..0000000 --- a/dist/npm/wright/index.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Resolves the absolute path to the native Wright binary (`wright` or `wright-lsp`) for the current platform. - * - * @param binName - Name of the binary to resolve ('wright' or 'wright-lsp', defaults to 'wright') - * @returns The absolute filesystem path to the native executable - * @throws Error if the current platform is unsupported or the platform package is not installed - */ -export function getBinaryPath(binName?: 'wright' | 'wright-lsp' | string): string; - -/** - * Returns the expected npm platform package name for the current platform/arch (e.g. '@wrightkit/wright-darwin-arm64'), - * or null if unsupported. - */ -export function getPlatformPackageName(): string | null; - -/** - * Returns the current platform key (e.g. 'darwin-arm64', 'linux-x64', 'win32-x64'). - */ -export function getPlatformKey(): string; - -/** - * Mapping of platform keys to package names. - */ -export declare const PLATFORMS: Record; diff --git a/dist/npm/wright/index.js b/dist/npm/wright/index.js deleted file mode 100644 index c2346a8..0000000 --- a/dist/npm/wright/index.js +++ /dev/null @@ -1,81 +0,0 @@ -const path = require('path'); -const fs = require('fs'); - -const PLATFORMS = { - 'darwin-arm64': '@wrightkit/wright-darwin-arm64', - 'darwin-x64': '@wrightkit/wright-darwin-x64', - 'linux-x64': '@wrightkit/wright-linux-x64', - 'win32-x64': '@wrightkit/wright-win32-x64', -}; - -function getPlatformKey() { - return `${process.platform}-${process.arch}`; -} - -function getPlatformPackageName() { - const key = getPlatformKey(); - return PLATFORMS[key] || null; -} - -function findPlatformPackageDir(pkgName) { - // 1. Standard require.resolve from this package and current working directory - try { - const manifestPath = require.resolve(`${pkgName}/package.json`, { - paths: [__dirname, process.cwd()], - }); - return path.dirname(manifestPath); - } catch {} - - // 2. Sibling lookup in node_modules/@wrightkit/ - const shortName = pkgName.replace('@wrightkit/', ''); - const siblingDir = path.resolve(__dirname, '..', shortName); - if (fs.existsSync(path.join(siblingDir, 'package.json'))) { - return siblingDir; - } - - // 3. Nested layout (e.g. node_modules/@wrightkit/wright/node_modules/@wrightkit/...) - const nestedDir = path.resolve(__dirname, 'node_modules', pkgName); - if (fs.existsSync(path.join(nestedDir, 'package.json'))) { - return nestedDir; - } - - return null; -} - -function getBinaryPath(binName = 'wright') { - const key = getPlatformKey(); - const pkgName = PLATFORMS[key]; - if (!pkgName) { - throw new Error( - `Unsupported platform/architecture for @wrightkit/wright: ${process.platform} (${process.arch}).\n` + - `Supported platforms: ${Object.keys(PLATFORMS).join(', ')}` - ); - } - - const pkgDir = findPlatformPackageDir(pkgName); - if (!pkgDir) { - throw new Error( - `Platform package "${pkgName}" for ${process.platform} ${process.arch} is not installed.\n` + - `Ensure optionalDependencies are enabled during installation, or install it explicitly:\n` + - ` npm install ${pkgName}` - ); - } - - const exeName = process.platform === 'win32' ? `${binName}.exe` : binName; - const binPath = path.join(pkgDir, exeName); - - if (!fs.existsSync(binPath)) { - throw new Error( - `Executable "${exeName}" was not found at "${binPath}". The platform package "${pkgName}" appears incomplete or corrupted.` - ); - } - - return binPath; -} - -module.exports = { - getBinaryPath, - getPlatformPackageName, - getPlatformKey, - PLATFORMS, -}; diff --git a/dist/npm/wright/package.json b/dist/npm/wright/package.json deleted file mode 100644 index 96e80f7..0000000 --- a/dist/npm/wright/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@wrightkit/wright", - "version": "0.2.17", - "description": "Wright CLI wrapper - Tooling-first semantic platform for the Overwatch Workshop and OverPy ecosystem", - "license": "AGPL-3.0-or-later", - "repository": { - "type": "git", - "url": "https://github.com/wrightkit/wright.git" - }, - "homepage": "https://github.com/wrightkit/wright", - "bugs": { - "url": "https://github.com/wrightkit/wright/issues" - }, - "main": "index.js", - "types": "index.d.ts", - "bin": { - "wright": "bin/wright.js", - "wright-lsp": "bin/wright-lsp.js" - }, - "files": [ - "bin", - "index.js", - "index.d.ts", - "README.md", - "LICENSE" - ], - "optionalDependencies": { - "@wrightkit/wright-darwin-arm64": "0.2.17", - "@wrightkit/wright-darwin-x64": "0.2.17", - "@wrightkit/wright-linux-x64": "0.2.17", - "@wrightkit/wright-win32-x64": "0.2.17" - }, - "engines": { - "node": ">=18" - } -} diff --git a/docs/release.md b/docs/release.md index 94a5062..858bab4 100644 --- a/docs/release.md +++ b/docs/release.md @@ -63,8 +63,8 @@ workflow is the single product release path: tag and release commit to the reusable `release.yml` workflow. 4. The reusable workflow verifies the tag/revision and version identity, runs `scripts/release.sh` and `scripts/verify-dist.py`, builds and smoke-tests the - native matrix, attaches archives/checksums/manifests/npm tarballs to the - draft, publishes downstream registries and the Homebrew tap, and only then + native matrix, attaches archives/checksums/manifests to the draft, publishes + the Homebrew tap, and only then marks the GitHub Release public. A failure in any gate or required downstream stage leaves the same draft @@ -141,18 +141,15 @@ before attaching them to the draft Release. Enable Actions to create and approve pull requests. The release-please workflow uses the repository's `GH_TOKEN` secret as `GITHUB_TOKEN` so it can create and -update the Release PR. The reusable distribution workflow also needs -`id-token: write` for npm provenance and `packages: write` for GitHub Packages. + update the Release PR. Create a protected `release` environment if publication approval is required; the final `publish-release` job is the only job that uses it. Configure these optional/required environment secrets: -* `NPM_TOKEN` enables npmjs.org publication. If absent, npmjs.org is skipped. * `GH_TOKEN` is a fine-grained token with write access to `wrightkit/homebrew-tap`; it is required for automatic Homebrew tap updates. -* The workflow's built-in `GITHUB_TOKEN` publishes GitHub Packages and updates - the draft GitHub Release. +* The workflow's built-in `GITHUB_TOKEN` updates the draft GitHub Release. ## Supported installation channels @@ -173,7 +170,6 @@ secret and the per-channel publication process and boundaries. | Homebrew (`wrightkit/tap`) | macOS arm64 + Intel | `wright` + `wright-lsp` formula | per-arch `sha256` in the formula | | WinGet (`WrightKit.Wright`) | Windows x86_64 | `wright` + `wright-lsp` portable ZIP | `InstallerSha256` in the manifest | | Scoop (`wrightkit` bucket) | Windows x86_64 | `wright` + `wright-lsp` ZIP | `hash` in the manifest | -| npm (`@wrightkit/wright`) | Linux x86_64, macOS arm64, macOS x86_64, Windows x86_64 | `wright` + `wright-lsp` native binaries via platform npm packages | packaged binary checksums and signatures verified at release packaging | `install.sh` is the supported Unix installer: it detects the platform (with explicit failures for unsupported OS/architecture combinations), resolves the @@ -183,28 +179,6 @@ installs both binaries, and runs a post-install version smoke check. Its functional behavior is covered by `scripts/test-install.sh` against a mock release server on Linux and macOS CI. -### npm distribution channel (#121) - -Wright distributes native release binaries through npm packages for seamless -integration with Node.js tooling, language clients, and CI agents: - -- **Meta package**: `@wrightkit/wright` exposes `wright` and `wright-lsp` in `bin`, - and exports `getBinaryPath()` for programmatic Node.js / TypeScript consumers. - It selects the matching native package via `optionalDependencies`. -- **Platform packages**: - - `@wrightkit/wright-darwin-arm64`: macOS Apple Silicon (`aarch64-apple-darwin`) - - `@wrightkit/wright-darwin-x64`: macOS Intel (`x86_64-apple-darwin`) - - `@wrightkit/wright-linux-x64`: Linux x64 (`x86_64-unknown-linux-gnu`) - - `@wrightkit/wright-win32-x64`: Windows x64 (`x86_64-pc-windows-msvc`) -- **Execution**: - - `npx wright --version` or `npm install @wrightkit/wright` - - Zero source compilation or postinstall download scripts; native binaries are packaged directly in the platform tarballs. - - npm is strictly a package/distribution layer for the native Rust CLI; there is no JavaScript reimplementation. -- **Registries**: the release workflow publishes the same packages to npmjs.org - when `NPM_TOKEN` is available and to GitHub Packages using the workflow - `GITHUB_TOKEN`. GitHub Packages installs require an authenticated npm scope - mapping for `@wrightkit` to `https://npm.pkg.github.com`. - Standalone installations are also updatable in place: `wright update` consumes the same release artifacts and checksums (no `install.sh` re-execution, no second release path), verifies the checksum before @@ -221,9 +195,7 @@ release pipeline does not assume them. Version drift is detectable: CI runs `scripts/verify-dist.py`, which regenerates the checked-in metadata for the current workspace version and fails on any mismatch, and the release workflow generates the attached manifests from the release's own -checksum files. Registry publication checks whether the exact package/version -already exists before publishing, so rerunning the downstream stage reuses the -same release identity. +checksum files. ### Still deferred diff --git a/scripts/package-npm.py b/scripts/package-npm.py deleted file mode 100755 index e3fd232..0000000 --- a/scripts/package-npm.py +++ /dev/null @@ -1,406 +0,0 @@ -#!/usr/bin/env python3 -"""Package Wright for npm distribution (#121). - -Assembles the @wrightkit/wright meta package and platform-specific native binary -packages (@wrightkit/wright-darwin-arm64, etc.) from canonical release archives -or pre-built binaries, verifies their contents and permissions, and creates npm -.tgz tarballs ready for publishing or installation. - -Usage: - # From canonical release archives (release workflow / CI): - python3 scripts/package-npm.py --version 0.1.0 --artifacts-dir artifacts --out-dir dist/npm-packages - - # From local binaries (development / smoke testing): - python3 scripts/package-npm.py --version 0.1.0 --binaries-dir target/release --out-dir dist/npm-packages -""" - -import argparse -import gzip -import hashlib -import importlib.util -import io -import json -import os -import platform -import shutil -import subprocess -import tarfile -import tempfile -import zipfile -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parent.parent - -# npm is a .cmd shim on Windows; subprocess needs the explicit extension -# because CreateProcess does not apply PATHEXT. -NPM = "npm.cmd" if os.name == "nt" else "npm" - - -def load_update_dist_manifests(): - spec = importlib.util.spec_from_file_location( - "wright_dist", REPO_ROOT / "scripts" / "update-dist-manifests.py" - ) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -dist_module = load_update_dist_manifests() -NPM_PLATFORM_PACKAGES = dist_module.NPM_PLATFORM_PACKAGES -TARGETS = dist_module.TARGETS -ARCHIVE_EXT = dist_module.ARCHIVE_EXT -version_from_tag = dist_module.version_from_tag - - -def detect_host_target() -> str: - system = platform.system() - machine = platform.machine().lower() - if system == "Darwin": - if machine in ("arm64", "aarch64"): - return "darwin-arm64" - elif machine in ("x86_64", "amd64"): - return "darwin-x64" - elif system == "Linux": - if machine in ("x86_64", "amd64"): - return "linux-x64" - elif system == "Windows": - if machine in ("x86_64", "amd64"): - return "windows-x64" - raise SystemExit(f"Unsupported host platform/architecture: {system} {machine}") - - -def calculate_sha256(path: Path) -> str: - h = hashlib.sha256() - with open(path, "rb") as f: - while chunk := f.read(65536): - h.update(chunk) - return h.hexdigest() - - -def find_archive(artifacts_dir: Path, version: str, triple: str, ext: str) -> Path | None: - expected_name = f"wright-{version}-{triple}.{ext}" - direct = artifacts_dir / expected_name - if direct.is_file(): - return direct - # Check subdirectories (e.g. artifacts/wright-/...) - for candidate in artifacts_dir.rglob(expected_name): - if candidate.is_file(): - return candidate - return None - - -def extract_archive(archive_path: Path, ext: str, dest_dir: Path) -> None: - if ext == "zip": - with zipfile.ZipFile(archive_path, "r") as z: - z.extractall(dest_dir) - else: - with tarfile.open(archive_path, "r:*") as t: - t.extractall(dest_dir) - - -def stage_platform_package( - platform_key: str, - config: dict, - version: str, - bin_dir: Path, - staging_root: Path, -) -> Path: - pkg_staging = staging_root / config["dir_name"] - pkg_staging.mkdir(parents=True, exist_ok=True) - - # Copy binary files - is_windows = "win32" in config["os"] - exe_suffix = ".exe" if is_windows else "" - - wright_bin = bin_dir / f"wright{exe_suffix}" - wright_lsp_bin = bin_dir / f"wright-lsp{exe_suffix}" - version_json = bin_dir / "version.json" - - if not wright_bin.is_file(): - raise SystemExit(f"Missing wright binary at {wright_bin}") - if not wright_lsp_bin.is_file(): - raise SystemExit(f"Missing wright-lsp binary at {wright_lsp_bin}") - - dest_wright = pkg_staging / f"wright{exe_suffix}" - dest_wright_lsp = pkg_staging / f"wright-lsp{exe_suffix}" - dest_version_json = pkg_staging / "version.json" - - shutil.copy2(wright_bin, dest_wright) - shutil.copy2(wright_lsp_bin, dest_wright_lsp) - - if not is_windows: - os.chmod(dest_wright, 0o755) - os.chmod(dest_wright_lsp, 0o755) - - if version_json.is_file(): - shutil.copy2(version_json, dest_version_json) - # Verify version inside version.json - try: - vdata = json.loads(dest_version_json.read_text()) - if vdata.get("version") != version: - raise SystemExit(f"version.json reports {vdata.get('version')}, expected {version}") - except Exception as e: - raise SystemExit(f"Failed to read/verify {dest_version_json}: {e}") - else: - # Create minimal version.json if not present - dest_version_json.write_text(json.dumps({"version": version, "requires": {"node": False, "overpy": False}}, indent=2) + "\n") - - # Copy package.json, README.md, LICENSE - dist_dir = REPO_ROOT / "dist" / "npm" / config["dir_name"] - shutil.copy2(dist_dir / "package.json", pkg_staging / "package.json") - shutil.copy2(dist_dir / "README.md", pkg_staging / "README.md") - shutil.copy2(REPO_ROOT / "LICENSE", pkg_staging / "LICENSE") - - return pkg_staging - - -def stage_meta_package(version: str, staging_root: Path) -> Path: - pkg_staging = staging_root / "wright" - pkg_staging.mkdir(parents=True, exist_ok=True) - - dist_dir = REPO_ROOT / "dist" / "npm" / "wright" - shutil.copytree(dist_dir / "bin", pkg_staging / "bin", dirs_exist_ok=True) - shutil.copy2(dist_dir / "index.js", pkg_staging / "index.js") - shutil.copy2(dist_dir / "index.d.ts", pkg_staging / "index.d.ts") - shutil.copy2(dist_dir / "package.json", pkg_staging / "package.json") - shutil.copy2(dist_dir / "README.md", pkg_staging / "README.md") - shutil.copy2(REPO_ROOT / "LICENSE", pkg_staging / "LICENSE") - - # Set executable permissions on bin scripts - for script in (pkg_staging / "bin").glob("*.js"): - os.chmod(script, 0o755) - - return pkg_staging - - -def pack_directory(pkg_dir: Path, out_dir: Path) -> Path: - result = subprocess.run( - [NPM, "pack", "--pack-destination", str(out_dir)], - cwd=pkg_dir, - capture_output=True, - text=True, - check=True, - ) - tarball_name = result.stdout.strip().splitlines()[-1] - tarball_path = out_dir / tarball_name - if not tarball_path.is_file(): - raise SystemExit(f"npm pack failed to produce {tarball_path}") - return tarball_path - - -# The bin entries every npm tarball must carry executable (0o755): the meta -# package's JS launchers and each platform package's native binaries (and the -# Windows .exe counterparts). `verify_tarball` enforces the exec bit on these. -BIN_ENTRIES = { - "package/wright", - "package/wright-lsp", - "package/wright.exe", - "package/wright-lsp.exe", - "package/bin/wright.js", - "package/bin/wright-lsp.js", -} - - -def fixed_entry_metadata(version: str) -> tuple[int, int, int]: - """Host-independent uid/gid/mtime for every tar entry. - - `npm pack` embeds the staged files' stat metadata verbatim. Windows cannot - set the executable bit (`os.chmod` is a no-op) and reports source modes - like 0o644/0o666, so a Windows source tree yields a tarball whose bytes - and bin-entry modes differ from a Unix pack. This derives a fixed stamp - deterministically from the version (uid 0, gid 0, a fixed epoch plus a - version-derived sub-day offset), so the same version always packs - identically on any host (#123). - """ - digest = hashlib.sha256(version.encode("utf-8")).hexdigest() - # 0x60000000 is a fixed epoch well past the ustar minimum (1980); the - # version hash keeps the stamp stable per version without tying it to the - # wall clock. - mtime = 0x60000000 + (int(digest[:8], 16) % 86_400) - return 0, 0, mtime - - -def normalize_tarball(tarball_path: Path, version: str) -> Path: - """Rewrite a packed `.tgz` with deterministic, portable entry metadata. - - npm pack's tarball reflects the source files' stat modes, so on Windows - (where the executable bit cannot be set) bin entries are packed at 0o644 - and `verify_tarball` rejects them. This single pure step reads every entry - of the packed tarball and rewrites it with: - - * bin entries forced to mode 0o755 (regular files 0o644, directories - 0o755), and - * fixed, version-derived uid/gid/mtime on every entry (and a fixed gzip - header mtime), - - preserving content, names, and order, so the resulting bytes are identical - regardless of host OS or source-file stat modes. The exec-bit validation in - [`verify_tarball`] is unchanged; this step makes it pass on every host. - """ - uid, gid, mtime = fixed_entry_metadata(version) - - with tarfile.open(tarball_path, "r:gz") as tar: - members = tar.getmembers() - payloads = { - member.name: (tar.extractfile(member).read() if member.isfile() else None) - for member in members - } - - buffer = io.BytesIO() - with tarfile.open(fileobj=buffer, mode="w", format=tarfile.USTAR_FORMAT) as tar: - for member in members: - # ustar needs no pax/global extended headers for our short paths; - # re-emitting them as-is would not survive the format change. - if member.type not in (tarfile.REGTYPE, tarfile.DIRTYPE, tarfile.LNKTYPE, tarfile.SYMTYPE): - continue - rewritten = tarfile.TarInfo(member.name) - rewritten.size = member.size - rewritten.type = member.type - rewritten.linkname = member.linkname - if member.isdir(): - rewritten.mode = 0o755 - elif member.issym() or member.islnk(): - rewritten.mode = 0o777 - else: - rewritten.mode = 0o755 if member.name in BIN_ENTRIES else 0o644 - rewritten.uid = uid - rewritten.gid = gid - rewritten.mtime = mtime - if member.isfile(): - tar.addfile(rewritten, io.BytesIO(payloads[member.name])) - else: - tar.addfile(rewritten) - - out_path = tarball_path.with_name(tarball_path.name + ".tmp") - with open(out_path, "wb") as out: - with gzip.GzipFile(fileobj=out, mode="wb", mtime=0) as gz: - gz.write(buffer.getvalue()) - out_path.replace(tarball_path) - return tarball_path - - -def verify_tarball(tarball_path: Path, is_meta: bool) -> None: - forbidden_suffixes = {".rs", ".o", ".a", ".pdb", ".d", ".rlib"} - found_files = set() - - with tarfile.open(tarball_path, "r:gz") as tar: - for member in tar.getmembers(): - name = member.name - found_files.add(name) - for suffix in forbidden_suffixes: - if name.endswith(suffix): - raise SystemExit(f"Forbidden file in tarball {tarball_path.name}: {name}") - - # Check permissions for binaries and scripts - if name in ("package/wright", "package/wright-lsp", "package/bin/wright.js", "package/bin/wright-lsp.js"): - if not (member.mode & 0o111): - raise SystemExit(f"File {name} in {tarball_path.name} is not executable (mode {oct(member.mode)})") - - if is_meta: - expected = { - "package/package.json", - "package/index.js", - "package/index.d.ts", - "package/bin/wright.js", - "package/bin/wright-lsp.js", - "package/README.md", - "package/LICENSE", - } - for exp in expected: - if exp not in found_files: - raise SystemExit(f"Meta package tarball {tarball_path.name} is missing {exp}") - else: - # Check required base files - for exp in ("package/package.json", "package/version.json", "package/README.md", "package/LICENSE"): - if exp not in found_files: - raise SystemExit(f"Platform package tarball {tarball_path.name} is missing {exp}") - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--version", required=True, help="Wright release version (e.g. 0.1.0)") - parser.add_argument("--artifacts-dir", type=Path, help="Directory containing release archives and checksums") - parser.add_argument("--binaries-dir", type=Path, help="Directory containing local wright/wright-lsp binaries (host platform only)") - parser.add_argument("--host-target", help="Explicit platform key (e.g. darwin-arm64) when packaging local binaries") - parser.add_argument("--out-dir", type=Path, default=REPO_ROOT / "dist" / "npm-packages", help="Output directory for .tgz tarballs") - args = parser.parse_args() - - version = version_from_tag(args.version) - out_dir = args.out_dir.resolve() - out_dir.mkdir(parents=True, exist_ok=True) - - if not args.artifacts_dir and not args.binaries_dir: - raise SystemExit("Must provide either --artifacts-dir or --binaries-dir") - - created_tarballs = [] - - with tempfile.TemporaryDirectory(prefix="wright-npm-") as tmp: - tmp_dir = Path(tmp) - staging_dir = tmp_dir / "staging" - extract_root = tmp_dir / "extracted" - - if args.artifacts_dir: - artifacts_dir = args.artifacts_dir.resolve() - print(f"Packaging npm packages for version {version} from artifacts in {artifacts_dir}") - - for platform_key, config in NPM_PLATFORM_PACKAGES.items(): - triple = TARGETS[platform_key] - ext = ARCHIVE_EXT[triple] - archive_path = find_archive(artifacts_dir, version, triple, ext) - if not archive_path: - raise SystemExit(f"Could not find release archive for {platform_key} ({triple}) in {artifacts_dir}") - - # Check sha256 file if present - sha256_file = archive_path.with_name(f"{archive_path.name}.sha256") - if sha256_file.is_file(): - expected_hash = sha256_file.read_text().split()[0].strip() - actual_hash = calculate_sha256(archive_path) - if expected_hash.lower() != actual_hash.lower(): - raise SystemExit(f"Checksum mismatch for {archive_path.name}: expected {expected_hash}, got {actual_hash}") - print(f"verified checksum for {archive_path.name}") - - extract_dir = extract_root / platform_key - extract_dir.mkdir(parents=True, exist_ok=True) - extract_archive(archive_path, ext, extract_dir) - - payload_name = f"wright-{version}-{triple}" - bin_dir = extract_dir / payload_name - if not bin_dir.is_dir(): - # Fallback to extract_dir if top-level payload directory is flat - bin_dir = extract_dir - - pkg_dir = stage_platform_package(platform_key, config, version, bin_dir, staging_dir) - tarball = normalize_tarball(pack_directory(pkg_dir, out_dir), version) - verify_tarball(tarball, is_meta=False) - created_tarballs.append(tarball) - print(f"packaged {config['name']} -> {tarball.name}") - - elif args.binaries_dir: - binaries_dir = args.binaries_dir.resolve() - platform_key = args.host_target or detect_host_target() - config = NPM_PLATFORM_PACKAGES.get(platform_key) - if not config: - raise SystemExit(f"Unknown platform key {platform_key}") - - print(f"Packaging npm package for host platform {platform_key} from {binaries_dir}") - pkg_dir = stage_platform_package(platform_key, config, version, binaries_dir, staging_dir) - tarball = normalize_tarball(pack_directory(pkg_dir, out_dir), version) - verify_tarball(tarball, is_meta=False) - created_tarballs.append(tarball) - print(f"packaged {config['name']} -> {tarball.name}") - - # Package meta package - meta_dir = stage_meta_package(version, staging_dir) - meta_tarball = normalize_tarball(pack_directory(meta_dir, out_dir), version) - verify_tarball(meta_tarball, is_meta=True) - created_tarballs.append(meta_tarball) - print(f"packaged @wrightkit/wright -> {meta_tarball.name}") - - print("\nSummary of created npm packages:") - for tb in created_tarballs: - print(f" {tb.name} ({tb.stat().st_size} bytes)") - print("done") - - -if __name__ == "__main__": - main() diff --git a/scripts/test-npm.py b/scripts/test-npm.py deleted file mode 100755 index d7067ab..0000000 --- a/scripts/test-npm.py +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env python3 -"""Smoke test Wright platform-native npm packages (#121). - -Tests clean npm installation, npx CLI execution, compilation/check commands, -programmatic Node.js module resolution, and error handling in an isolated -sandbox environment without requiring source checkout or Rust runtime. -Also validates multi-platform archive packaging and checksum validation. - -Usage: python3 scripts/test-npm.py [--binaries-dir target/release] -""" - -import argparse -import hashlib -import json -import os -import shutil -import subprocess -import sys -import tarfile -import tempfile -import zipfile -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parent.parent - -# npm/npx are .cmd shims on Windows; subprocess needs the explicit extension -# because CreateProcess does not apply PATHEXT. -NPM = "npm.cmd" if os.name == "nt" else "npm" -NPX = "npx.cmd" if os.name == "nt" else "npx" - - -def get_workspace_version() -> str: - metadata = subprocess.run( - ["cargo", "metadata", "--no-deps", "--format-version", "1"], - cwd=REPO_ROOT, - check=True, - capture_output=True, - text=True, - ).stdout - for package in json.loads(metadata)["packages"]: - if package["name"] == "wright-cli": - return package["version"] - raise SystemExit("wright-cli not found in workspace metadata") - - -def find_binaries_dir() -> Path: - candidates = [ - REPO_ROOT / "target" / "release", - REPO_ROOT / "target" / "debug", - ] - for c in candidates: - if (c / "wright").is_file() or (c / "wright.exe").is_file(): - return c - raise SystemExit("Could not find wright binary in target/release or target/debug") - - -def test_multiplatform_packaging(version: str) -> None: - print("\n--- Testing multi-platform archive packaging & verification ---") - with tempfile.TemporaryDirectory(prefix="wright-npm-multi-") as tmp: - tmp_path = Path(tmp) - art_dir = tmp_path / "artifacts" - art_dir.mkdir() - out_dir = tmp_path / "out" - out_dir.mkdir() - - triples = [ - ("x86_64-unknown-linux-gnu", "tar.gz", False), - ("aarch64-apple-darwin", "tar.gz", False), - ("x86_64-apple-darwin", "tar.gz", False), - ("x86_64-pc-windows-msvc", "zip", True), - ] - - for triple, ext, is_win in triples: - stage = tmp_path / f"stage-{triple}" - payload = stage / f"wright-{version}-{triple}" - payload.mkdir(parents=True) - exe = ".exe" if is_win else "" - (payload / f"wright{exe}").write_text("#!/bin/sh\nexit 0\n") - (payload / f"wright-lsp{exe}").write_text("#!/bin/sh\nexit 0\n") - if not is_win: - (payload / f"wright{exe}").chmod(0o755) - (payload / f"wright-lsp{exe}").chmod(0o755) - (payload / "version.json").write_text( - json.dumps({"version": version, "requires": {"node": False, "overpy": False}}) + "\n" - ) - - arch_file = art_dir / f"wright-{version}-{triple}.{ext}" - if ext == "zip": - with zipfile.ZipFile(arch_file, "w") as z: - for f in payload.rglob("*"): - if f.is_file(): - z.write(f, f.relative_to(stage)) - else: - with tarfile.open(arch_file, "w:gz") as t: - t.add(payload, arcname=payload.name) - - h = hashlib.sha256(arch_file.read_bytes()).hexdigest() - (art_dir / f"wright-{version}-{triple}.{ext}.sha256").write_text(f"{h} {arch_file.name}\n") - - subprocess.run( - [ - sys.executable, - str(REPO_ROOT / "scripts" / "package-npm.py"), - "--version", - version, - "--artifacts-dir", - str(art_dir), - "--out-dir", - str(out_dir), - ], - check=True, - capture_output=True, - ) - - pkgs = list(out_dir.glob("*.tgz")) - if len(pkgs) != 5: - raise SystemExit(f"Expected 5 npm packages, got {len(pkgs)}") - print(f"ok: packaged all 5 npm packages from multi-platform release archives") - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--binaries-dir", type=Path, help="Directory with built wright and wright-lsp") - parser.add_argument("--version", help="Wright version (default: workspace version)") - args = parser.parse_args() - - version = args.version or get_workspace_version() - bin_dir = args.binaries_dir or find_binaries_dir() - - print(f"Running npm distribution smoke tests for Wright v{version}") - print(f"Using binaries from {bin_dir}") - - with tempfile.TemporaryDirectory(prefix="wright-npm-test-") as tmp: - tmp_dir = Path(tmp) - packages_dir = tmp_dir / "packages" - sandbox_dir = tmp_dir / "sandbox" - packages_dir.mkdir(parents=True, exist_ok=True) - sandbox_dir.mkdir(parents=True, exist_ok=True) - - # 1. Package npm tarballs - print("\n--- Step 1: Packaging npm tarballs ---") - subprocess.run( - [ - sys.executable, - str(REPO_ROOT / "scripts" / "package-npm.py"), - "--version", - version, - "--binaries-dir", - str(bin_dir), - "--out-dir", - str(packages_dir), - ], - check=True, - ) - - meta_tarballs = list(packages_dir.glob("wrightkit-wright-*.tgz")) - if not meta_tarballs: - meta_tarballs = list(packages_dir.glob("*wright*.tgz")) - meta_tarball = [tb for tb in meta_tarballs if "darwin" not in tb.name and "linux" not in tb.name and "win32" not in tb.name][0] - platform_tarball = [tb for tb in packages_dir.glob("*.tgz") if tb != meta_tarball][0] - - print(f"Meta tarball: {meta_tarball.name}") - print(f"Platform tarball: {platform_tarball.name}") - - # 2. Setup clean sandbox - print("\n--- Step 2: Clean install in sandbox ---") - package_json = { - "name": "test-consumer", - "version": "1.0.0", - "private": True, - } - (sandbox_dir / "package.json").write_text(json.dumps(package_json, indent=2)) - - # Install platform tarball and meta tarball - subprocess.run( - [NPM, "install", str(platform_tarball), str(meta_tarball)], - cwd=sandbox_dir, - check=True, - capture_output=True, - ) - print("npm install completed successfully in isolated sandbox") - - # 3. Test npx / node_modules/.bin execution - print("\n--- Step 3: Testing CLI entry points ---") - - # Test wright --version - res = subprocess.run( - [NPX, "wright", "--version"], - cwd=sandbox_dir, - capture_output=True, - text=True, - check=True, - ) - if version not in res.stdout: - raise SystemExit(f"npx wright --version did not report {version}: {res.stdout}") - print(f"ok: npx wright --version -> {res.stdout.strip()}") - - # Test wright-lsp --version - res = subprocess.run( - [NPX, "wright-lsp", "--version"], - cwd=sandbox_dir, - capture_output=True, - text=True, - check=True, - ) - if version not in res.stdout: - raise SystemExit(f"npx wright-lsp --version did not report {version}: {res.stdout}") - print(f"ok: npx wright-lsp --version -> {res.stdout.strip()}") - - # 4. Test compiler functionality via npx - print("\n--- Step 4: Testing compilation and check commands via npx ---") - fixture_file = REPO_ROOT / "compatibility" / "fixtures" / "synthetic" / "basic-rule" / "source.opy" - scenario_file = REPO_ROOT / "scenarios" / "loops.opy" - - res = subprocess.run( - [NPX, "wright", "compile", str(fixture_file), "--profile", "compat"], - cwd=sandbox_dir, - capture_output=True, - text=True, - check=True, - ) - if "rule" not in res.stdout: - raise SystemExit(f"npx wright compile output unexpected: {res.stdout}") - print("ok: npx wright compile produced Workshop output") - - res = subprocess.run( - [NPX, "wright", "check", str(scenario_file), "--profile", "compat"], - cwd=sandbox_dir, - capture_output=True, - text=True, - check=True, - ) - print("ok: npx wright check succeeded on scenario fixture") - - # 5. Test programmatic Node.js API - print("\n--- Step 5: Testing programmatic Node.js API ---") - test_script = """ -const assert = require('assert'); -const path = require('path'); -const fs = require('fs'); -const { getBinaryPath, getPlatformPackageName, getPlatformKey, PLATFORMS } = require('@wrightkit/wright'); - -const key = getPlatformKey(); -const pkg = getPlatformPackageName(); -console.log('Platform key:', key); -console.log('Platform pkg:', pkg); -assert.ok(pkg, 'Expected valid platform package'); - -const wrightPath = getBinaryPath('wright'); -console.log('Resolved wright path:', wrightPath); -assert.ok(fs.existsSync(wrightPath), 'wright binary must exist'); - -const lspPath = getBinaryPath('wright-lsp'); -console.log('Resolved wright-lsp path:', lspPath); -assert.ok(fs.existsSync(lspPath), 'wright-lsp binary must exist'); - -console.log('Programmatic API assertions passed'); -""" - (sandbox_dir / "test-api.cjs").write_text(test_script) - subprocess.run( - ["node", "test-api.cjs"], - cwd=sandbox_dir, - check=True, - ) - print("ok: programmatic Node.js API works as expected") - - # 6. Test error handling - print("\n--- Step 6: Testing error handling ---") - error_test_script = """ -const assert = require('assert'); -const { getBinaryPath, PLATFORMS } = require('@wrightkit/wright'); - -// Test asking for non-existent binary -try { - getBinaryPath('non-existent-binary-12345'); - assert.fail('Expected error for non-existent binary'); -} catch (err) { - assert.ok(err.message.includes('not found') || err.message.includes('incomplete'), 'Expected not found error message'); - console.log('ok: handled missing binary error properly'); -} -""" - (sandbox_dir / "test-error.cjs").write_text(error_test_script) - subprocess.run( - ["node", "test-error.cjs"], - cwd=sandbox_dir, - check=True, - ) - print("ok: error handling verified") - - # 7. Test multi-platform archive packaging - test_multiplatform_packaging(version) - - print("\nAll npm distribution smoke tests passed successfully!") - - -if __name__ == "__main__": - main() diff --git a/scripts/tests/test_npm_packaging.py b/scripts/tests/test_npm_packaging.py deleted file mode 100644 index 8dd2117..0000000 --- a/scripts/tests/test_npm_packaging.py +++ /dev/null @@ -1,230 +0,0 @@ -"""npm tarball bin-metadata determinism/portability regressions (#123). - -Drives the REAL shipped packaging functions (`scripts/package-npm.py` loaded -via the repo's importlib convention) on staged inputs whose bin files are -deliberately created WITHOUT the executable bit, simulating a Windows source -tree where `os.chmod` cannot set exec bits. Asserts: - -* after the shipped post-pack normalization every bin entry in the produced - `.tgz` carries mode `0o755` (and all entry metadata is fixed); -* the normalized bytes are identical to a pack of the same inputs staged WITH - exec bits, and identical across repeated packs; -* the shipped `verify_tarball` still rejects a non-executable bin entry - (the Unix executability validation is retained, not loosened); -* an end-to-end npm pack of an exec-less staged package (when npm is - available) passes the real validation after normalization. -""" - -import gzip -import hashlib -import importlib.util -import io -import json -import shutil -import tarfile -import tempfile -import unittest -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[2] -PACKAGE_NPM = REPO_ROOT / "scripts" / "package-npm.py" - - -def load_package_npm(): - spec = importlib.util.spec_from_file_location("wright_package_npm", PACKAGE_NPM) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -package_npm = load_package_npm() - -META_LAYOUT = { - "package/": b"", - "package/bin/": b"", - "package/bin/wright.js": b"#!/usr/bin/env node\nconst { getBinaryPath } = require('../index.js');\n", - "package/bin/wright-lsp.js": b"#!/usr/bin/env node\nconst { getBinaryPath } = require('../index.js');\n", - "package/index.js": b"module.exports = { getBinaryPath: () => null };\n", - "package/index.d.ts": b"declare const x: number;\nexport = x;\n", - "package/package.json": json.dumps( - {"name": "@wrightkit/wright", "version": "0.1.0", "bin": {"wright": "bin/wright.js", "wright-lsp": "bin/wright-lsp.js"}} - ).encode(), - "package/README.md": b"# wright\n", - "package/LICENSE": b"AGPL-3.0-or-later\n", -} - -PLATFORM_LAYOUT = { - "package/": b"", - "package/wright": b"#!/bin/sh\nexit 0\n", - "package/wright-lsp": b"#!/bin/sh\nexit 0\n", - "package/wright.exe": b"MZ\x00\x00fake\n", - "package/wright-lsp.exe": b"MZ\x00\x00fake\n", - "package/version.json": b'{"version": "0.1.0"}\n', - "package/package.json": json.dumps( - {"name": "@wrightkit/wright-win32-x64", "version": "0.1.0"} - ).encode(), - "package/README.md": b"# wright\n", - "package/LICENSE": b"AGPL-3.0-or-later\n", -} - -BIN_PATHS = { - "package/wright", - "package/wright-lsp", - "package/wright.exe", - "package/wright-lsp.exe", - "package/bin/wright.js", - "package/bin/wright-lsp.js", -} - - -def stage_package(root: Path, layout: dict[str, bytes], exec_bits: bool) -> None: - for rel, content in layout.items(): - path = root / rel - if content == b"" and rel.endswith("/"): - path.mkdir(parents=True, exist_ok=True) - continue - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(content) - if rel in BIN_PATHS: - path.chmod(0o755 if exec_bits else 0o644) - else: - path.chmod(0o644) - - -def pack_staged(root: Path, mtime: int) -> bytes: - buffer = io.BytesIO() - with tarfile.open(fileobj=buffer, mode="w", format=tarfile.USTAR_FORMAT) as tar: - for path in sorted(root.rglob("*")): - rel = str(path.relative_to(root)) - info = tar.gettarinfo(str(path), arcname=rel) - info.uid = 501 - info.gid = 20 - info.mtime = mtime - if path.is_dir(): - info.mode = 0o755 - tar.addfile(info) - else: - with open(path, "rb") as handle: - tar.addfile(info, handle) - raw = buffer.getvalue() - out = io.BytesIO() - with gzip.GzipFile(fileobj=out, mode="wb", mtime=987654) as gz: - gz.write(raw) - return out.getvalue() - - -def write_tgz(bytes_: bytes) -> Path: - directory = tempfile.mkdtemp(prefix="wright-npm-test-") - path = Path(directory) / "wrightkit-wright-0.1.0.tgz" - path.write_bytes(bytes_) - return path - - -def entry_modes(tgz: Path) -> dict[str, tuple[int, int, int, int]]: - with tarfile.open(tgz, "r:gz") as tar: - return {m.name: (m.mode, m.uid, m.gid, m.mtime) for m in tar.getmembers()} - - -class NpmPackagingNormalizationTests(unittest.TestCase): - def setUp(self): - self._dirs = [] - - def tearDown(self): - for d in self._dirs: - shutil.rmtree(d, ignore_errors=True) - - def _pack(self, layout, exec_bits, mtime): - root = Path(tempfile.mkdtemp(prefix="wright-npm-stage-")) - self._dirs.append(str(root)) - stage_package(root, layout, exec_bits) - return pack_staged(root, mtime) - - def test_bin_entries_are_0755_after_normalization_from_exec_less_sources(self): - _, _, expected_mtime = package_npm.fixed_entry_metadata("0.1.0") - covered = set() - for layout in (META_LAYOUT, PLATFORM_LAYOUT): - raw = self._pack(layout, exec_bits=False, mtime=499162500) - tgz = write_tgz(raw) - package_npm.normalize_tarball(tgz, "0.1.0") - modes = entry_modes(tgz) - for path in BIN_PATHS & set(layout): - covered.add(path) - self.assertIn(path, modes, f"{path} missing after normalization") - self.assertEqual(modes[path][0], 0o755, f"{path} must be 0o755") - for path, (_, uid, gid, mtime) in modes.items(): - self.assertEqual(uid, 0, f"{path} uid must be fixed") - self.assertEqual(gid, 0, f"{path} gid must be fixed") - self.assertEqual(mtime, expected_mtime, f"{path} mtime must be fixed") - self.assertEqual(covered, BIN_PATHS) - - def test_normalized_bytes_match_exec_bit_source_pack(self): - windows = self._pack(META_LAYOUT, exec_bits=False, mtime=499162500) - unix = self._pack(META_LAYOUT, exec_bits=True, mtime=1610613000) - a = write_tgz(windows) - b = write_tgz(unix) - package_npm.normalize_tarball(a, "0.1.0") - package_npm.normalize_tarball(b, "0.1.0") - self.assertEqual(hashlib.sha256(a.read_bytes()).hexdigest(), hashlib.sha256(b.read_bytes()).hexdigest()) - - def test_normalization_is_byte_deterministic(self): - first = self._pack(META_LAYOUT, exec_bits=False, mtime=499162500) - second = self._pack(META_LAYOUT, exec_bits=False, mtime=1700000000) - a = write_tgz(first) - b = write_tgz(second) - package_npm.normalize_tarball(a, "0.1.0") - package_npm.normalize_tarball(b, "0.1.0") - self.assertEqual(hashlib.sha256(a.read_bytes()).hexdigest(), hashlib.sha256(b.read_bytes()).hexdigest()) - - def test_verify_tarball_still_rejects_non_executable_bin(self): - raw = self._pack(META_LAYOUT, exec_bits=False, mtime=499162500) - tgz = write_tgz(raw) - with self.assertRaises(SystemExit) as ctx: - package_npm.verify_tarball(tgz, is_meta=True) - message = str(ctx.exception) - self.assertIn("not executable", message) - # The observed mode is platform-dependent (0o644 on POSIX, - # 0o666 on Windows where exec bits do not exist); the contract is - # that the rejection names the offending file and reports the mode. - self.assertIn("package/bin/", message) - self.assertIn("mode 0o", message) - - def test_normalized_tarballs_pass_real_verify(self): - for layout, is_meta in ((META_LAYOUT, True), (PLATFORM_LAYOUT, False)): - raw = self._pack(layout, exec_bits=False, mtime=499162500) - tgz = write_tgz(raw) - package_npm.normalize_tarball(tgz, "0.1.0") - package_npm.verify_tarball(tgz, is_meta=is_meta) - - def test_end_to_end_npm_pack_of_exec_less_staged_package(self): - npm = shutil.which("npm.cmd") or shutil.which("npm") - if npm is None: - self.skipTest("npm is not available on this host") - root = Path(tempfile.mkdtemp(prefix="wright-npm-e2e-")) - self._dirs.append(str(root)) - out = root.parent / "wright-npm-e2e-out" - out.mkdir(exist_ok=True) - self._dirs.append(str(out)) - for rel, content in META_LAYOUT.items(): - if rel.endswith("/"): - continue - path = root / rel.removeprefix("package/") - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(content) - path.chmod(0o755 if rel in BIN_PATHS else 0o644) - tarball = package_npm.pack_directory(root, out) - package_npm.normalize_tarball(tarball, "0.1.0") - package_npm.verify_tarball(tarball, is_meta=True) - modes = entry_modes(tarball) - for path in ("package/bin/wright.js", "package/bin/wright-lsp.js"): - self.assertEqual(modes[path][0], 0o755, f"{path} must be 0o755") - - def test_exec_bit_validation_source_is_preserved(self): - source = PACKAGE_NPM.read_text() - self.assertIn("mode & 0o111", source) - self.assertIn("not executable", source) - for path in sorted(BIN_PATHS): - self.assertIn(path, source) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/update-dist-manifests.py b/scripts/update-dist-manifests.py index 4b2a021..219f1f2 100644 --- a/scripts/update-dist-manifests.py +++ b/scripts/update-dist-manifests.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -"""Generate Wright package-manager distribution metadata (#108, #121). +"""Generate Wright package-manager distribution metadata (#108). -Rewrites dist/ with the Homebrew formula, WinGet manifests, Scoop manifest, -and npm package manifests for a Wright version. All generated manifests +Rewrites dist/ with the Homebrew formula, WinGet manifests, and Scoop manifest +for a Wright version. All generated manifests consume the canonical GitHub Release archives (`wright--.` + `.sha256`) or the canonical release binaries; nothing here rebuilds Wright. @@ -20,7 +20,6 @@ """ import argparse -import json import re import shutil from pathlib import Path @@ -43,43 +42,6 @@ "x86_64-pc-windows-msvc": "zip", } -# Platform packages configuration for npm distribution (#121) -NPM_PLATFORM_PACKAGES = { - "darwin-arm64": { - "dir_name": "wright-darwin-arm64", - "name": "@wrightkit/wright-darwin-arm64", - "description": "macOS ARM64 (Apple Silicon) native binary for Wright", - "os": ["darwin"], - "cpu": ["arm64"], - "files": ["wright", "wright-lsp", "version.json", "README.md", "LICENSE"], - }, - "darwin-x64": { - "dir_name": "wright-darwin-x64", - "name": "@wrightkit/wright-darwin-x64", - "description": "macOS x64 (Intel) native binary for Wright", - "os": ["darwin"], - "cpu": ["x64"], - "files": ["wright", "wright-lsp", "version.json", "README.md", "LICENSE"], - }, - "linux-x64": { - "dir_name": "wright-linux-x64", - "name": "@wrightkit/wright-linux-x64", - "description": "Linux x64 native binary for Wright", - "os": ["linux"], - "cpu": ["x64"], - "files": ["wright", "wright-lsp", "version.json", "README.md", "LICENSE"], - }, - "windows-x64": { - "dir_name": "wright-win32-x64", - "name": "@wrightkit/wright-win32-x64", - "description": "Windows x64 native binary for Wright", - "os": ["win32"], - "cpu": ["x64"], - "files": ["wright.exe", "wright-lsp.exe", "version.json", "README.md", "LICENSE"], - }, -} - - def valid_hash(value: str) -> bool: return bool(re.fullmatch(r"[0-9a-fA-F]{64}", value)) @@ -227,66 +189,6 @@ def scoop_manifest(version: str, windows_hash: str) -> str: """ -def npm_meta_package_json(version: str) -> str: - optional_deps = { - pkg["name"]: version - for pkg in NPM_PLATFORM_PACKAGES.values() - } - manifest = { - "name": "@wrightkit/wright", - "version": version, - "description": "Wright CLI wrapper - Tooling-first semantic platform for the Overwatch Workshop and OverPy ecosystem", - "license": "AGPL-3.0-or-later", - "repository": { - "type": "git", - "url": "https://github.com/wrightkit/wright.git", - }, - "homepage": "https://github.com/wrightkit/wright", - "bugs": { - "url": "https://github.com/wrightkit/wright/issues", - }, - "main": "index.js", - "types": "index.d.ts", - "bin": { - "wright": "bin/wright.js", - "wright-lsp": "bin/wright-lsp.js", - }, - "files": [ - "bin", - "index.js", - "index.d.ts", - "README.md", - "LICENSE", - ], - "optionalDependencies": optional_deps, - "engines": { - "node": ">=18", - }, - } - return json.dumps(manifest, indent=2) + "\n" - - -def npm_platform_package_json(version: str, config: dict) -> str: - manifest = { - "name": config["name"], - "version": version, - "description": config["description"], - "license": "AGPL-3.0-or-later", - "repository": { - "type": "git", - "url": "https://github.com/wrightkit/wright.git", - }, - "homepage": "https://github.com/wrightkit/wright", - "bugs": { - "url": "https://github.com/wrightkit/wright/issues", - }, - "os": config["os"], - "cpu": config["cpu"], - "files": config["files"], - } - return json.dumps(manifest, indent=2) + "\n" - - def generate(version: str, hashes: dict, out_dir: Path) -> list: """Write every distribution manifest for `version` into `out_dir`. @@ -325,19 +227,6 @@ def generate(version: str, hashes: dict, out_dir: Path) -> list: scoop.write_text(scoop_manifest(version, hashes["windows-x64"])) written.append(scoop.relative_to(out_dir)) - # npm meta-package - npm_meta = out_dir / "dist" / "npm" / "wright" / "package.json" - npm_meta.parent.mkdir(parents=True, exist_ok=True) - npm_meta.write_text(npm_meta_package_json(version)) - written.append(npm_meta.relative_to(out_dir)) - - # npm platform packages - for platform_key, config in NPM_PLATFORM_PACKAGES.items(): - pkg_file = out_dir / "dist" / "npm" / config["dir_name"] / "package.json" - pkg_file.parent.mkdir(parents=True, exist_ok=True) - pkg_file.write_text(npm_platform_package_json(version, config)) - written.append(pkg_file.relative_to(out_dir)) - return written diff --git a/scripts/verify-dist.py b/scripts/verify-dist.py index d3765f4..27b37b6 100644 --- a/scripts/verify-dist.py +++ b/scripts/verify-dist.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 -"""Validate Wright distribution metadata and the install script (#108, #121). +"""Validate Wright distribution metadata and the install script (#108). Runs in CI and locally. It detects version drift and hand-edited metadata by regenerating every checked-in dist/ manifest with the workspace version and placeholder hashes and comparing it byte-for-byte to what is committed. It also validates manifest structure (hash format, artifact URLs), checks that -install.sh covers the declared release target matrix, verifies the shell -syntax of install.sh, and verifies the integrity of npm packages and scripts. +install.sh covers the declared release target matrix, and verifies the shell +syntax of install.sh. Usage: python3 scripts/verify-dist.py """ @@ -16,7 +16,6 @@ import os import re import subprocess -import sys import tempfile from pathlib import Path @@ -129,76 +128,6 @@ def main() -> None: # `bash` on PATH is the WSL launcher, which fails without a distro. print("skip: install.sh shell syntax check (Unix-only, no real bash on Windows)") - # npm distribution validation (#121) - meta_json_path = REPO_ROOT / "dist" / "npm" / "wright" / "package.json" - if not meta_json_path.is_file(): - fail("dist/npm/wright/package.json is missing") - meta_json = json.loads(meta_json_path.read_text()) - - if meta_json.get("name") != "@wrightkit/wright": - fail("dist/npm/wright/package.json name must be @wrightkit/wright") - if meta_json.get("version") != version: - fail(f"dist/npm/wright/package.json version {meta_json.get('version')} does not match {version}") - - meta_opt_deps = meta_json.get("optionalDependencies", {}) - for platform_key, config in gen.NPM_PLATFORM_PACKAGES.items(): - pkg_name = config["name"] - if pkg_name not in meta_opt_deps: - fail(f"missing optional dependency {pkg_name} in @wrightkit/wright package.json") - if meta_opt_deps[pkg_name] != version: - fail(f"optional dependency {pkg_name} version {meta_opt_deps[pkg_name]} does not match {version}") - - platform_dir = REPO_ROOT / "dist" / "npm" / config["dir_name"] - pkg_json_file = platform_dir / "package.json" - if not pkg_json_file.is_file(): - fail(f"{pkg_json_file} is missing") - pkg_json = json.loads(pkg_json_file.read_text()) - if pkg_json.get("name") != pkg_name: - fail(f"{pkg_json_file} name mismatch") - if pkg_json.get("version") != version: - fail(f"{pkg_json_file} version mismatch") - if pkg_json.get("os") != config["os"]: - fail(f"{pkg_json_file} os constraint mismatch") - if pkg_json.get("cpu") != config["cpu"]: - fail(f"{pkg_json_file} cpu constraint mismatch") - - readme_file = platform_dir / "README.md" - if not readme_file.is_file(): - fail(f"{readme_file} is missing") - - for script_file in [ - REPO_ROOT / "dist" / "npm" / "wright" / "bin" / "wright.js", - REPO_ROOT / "dist" / "npm" / "wright" / "bin" / "wright-lsp.js", - REPO_ROOT / "dist" / "npm" / "wright" / "index.js", - ]: - if not script_file.is_file(): - fail(f"{script_file} is missing") - subprocess.run(["node", "-c", str(script_file)], check=True) - - dts_file = REPO_ROOT / "dist" / "npm" / "wright" / "index.d.ts" - if not dts_file.is_file() or "getBinaryPath" not in dts_file.read_text(): - fail("dist/npm/wright/index.d.ts is missing or does not export getBinaryPath") - - print("ok: npm packages and wrapper scripts valid") - - # Detailed tarball-mode/determinism tests are distribution regressions, - # not language compatibility tests. Run them once in the Linux matrix; - # the normal dist smoke below still runs on every supported CI platform. - if sys.platform.startswith("linux"): - subprocess.run( - [ - sys.executable, - "-m", - "unittest", - "discover", - "-s", - str(REPO_ROOT / "scripts" / "tests"), - ], - cwd=REPO_ROOT, - check=True, - ) - print("ok: distribution helper regression tests passed") - print("dist validation passed")