diff --git a/.github/workflows/protected-release.yml b/.github/workflows/protected-release.yml new file mode 100644 index 000000000..6a953ec84 --- /dev/null +++ b/.github/workflows/protected-release.yml @@ -0,0 +1,315 @@ +name: Protected npm Core Release + +on: + workflow_dispatch: + inputs: + expected_sha: + description: Full immutable main-branch SHA to publish + required: true + type: string + platform_version: + description: Platform package version + required: true + type: string + core_version: + description: '@ruvector/core version' + required: true + type: string + +permissions: + contents: read + id-token: write + +concurrency: + group: protected-npm-core-release + cancel-in-progress: false + +env: + PLATFORM_VERSION: ${{ inputs.platform_version }} + CORE_VERSION: ${{ inputs.core_version }} + EXPECTED_SHA: ${{ inputs.expected_sha }} + +jobs: + seal: + name: Seal exact npm artifacts + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ inputs.expected_sha }} + fetch-depth: 0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '22' + registry-url: https://registry.npmjs.org + + - name: Bind candidate identity + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + test "$(git rev-parse origin/main)" = "$EXPECTED_SHA" + test "${#EXPECTED_SHA}" = 40 + test "$(git status --porcelain)" = "" + test "$(node -p "require('./npm/packages/core/package.json').version")" = "$CORE_VERSION" + for manifest in npm/core/platforms/*/package.json; do + test "$(node -p "require('./$manifest').version")" = "$PLATFORM_VERSION" + done + + - name: Pack immutable artifacts + run: | + set -euo pipefail + mkdir -p release-evidence/sealed + for directory in \ + npm/core/platforms/linux-x64-gnu \ + npm/core/platforms/linux-arm64-gnu \ + npm/core/platforms/darwin-x64 \ + npm/core/platforms/darwin-arm64 \ + npm/core/platforms/win32-x64-msvc \ + npm/packages/core; do + npm pack "./$directory" --pack-destination release-evidence/sealed --json >/dev/null + done + test "$(find release-evidence/sealed -name '*.tgz' -type f | wc -l)" = 6 + (cd release-evidence/sealed && sha256sum ./*.tgz | sort > SHA256SUMS) + cp npm/packages/core/test.js release-evidence/core.test.js + node -e ' + const fs = require("fs"); + const receipt = { + schemaVersion: 1, + sha: process.env.EXPECTED_SHA, + dirty: false, + platformVersion: process.env.PLATFORM_VERSION, + coreVersion: process.env.CORE_VERSION, + artifacts: fs.readFileSync("release-evidence/sealed/SHA256SUMS", "utf8").trim().split("\n") + }; + fs.writeFileSync("release-evidence/candidate-receipt.json", JSON.stringify(receipt, null, 2) + "\n"); + ' + + - name: Refuse conflicting registry bytes + run: | + set -euo pipefail + packages=( + "ruvector-core-linux-x64-gnu@$PLATFORM_VERSION" + "ruvector-core-linux-arm64-gnu@$PLATFORM_VERSION" + "ruvector-core-darwin-x64@$PLATFORM_VERSION" + "ruvector-core-darwin-arm64@$PLATFORM_VERSION" + "ruvector-core-win32-x64-msvc@$PLATFORM_VERSION" + "@ruvector/core@$CORE_VERSION" + ) + files=( + "ruvector-core-linux-x64-gnu-$PLATFORM_VERSION.tgz" + "ruvector-core-linux-arm64-gnu-$PLATFORM_VERSION.tgz" + "ruvector-core-darwin-x64-$PLATFORM_VERSION.tgz" + "ruvector-core-darwin-arm64-$PLATFORM_VERSION.tgz" + "ruvector-core-win32-x64-msvc-$PLATFORM_VERSION.tgz" + "ruvector-core-$CORE_VERSION.tgz" + ) + existing_dir="$(mktemp -d)" + for index in "${!packages[@]}"; do + package="${packages[$index]}" + if url="$(npm view "$package" dist.tarball 2>/dev/null)"; then + curl --fail --location "$url" --output "$existing_dir/${files[$index]}" + cmp "$existing_dir/${files[$index]}" "release-evidence/sealed/${files[$index]}" || { + echo "::error::$package exists with bytes that do not match this candidate" + exit 1 + } + echo "$package already has the exact sealed bytes; a retry may safely skip it" + fi + done + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: sealed-core-npm-${{ inputs.expected_sha }} + path: release-evidence + if-no-files-found: error + retention-days: 90 + + runtime-grader: + name: Runtime grader (100 required) + needs: seal + runs-on: ubuntu-22.04 + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: sealed-core-npm-${{ inputs.expected_sha }} + path: release-evidence + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '22' + - name: Install sealed tarballs into a virgin project + run: | + set -euo pipefail + (cd release-evidence/sealed && sha256sum -c SHA256SUMS) + install_dir="$(mktemp -d)" + cd "$install_dir" + npm init -y >/dev/null + npm install \ + "$GITHUB_WORKSPACE/release-evidence/sealed/ruvector-core-linux-x64-gnu-$PLATFORM_VERSION.tgz" \ + "$GITHUB_WORKSPACE/release-evidence/sealed/ruvector-core-$CORE_VERSION.tgz" \ + --ignore-scripts --no-audit --no-fund + node -e ' + const { VectorDB, VectorDb } = require("@ruvector/core"); + if (VectorDB !== VectorDb) throw new Error("VectorDB alias mismatch"); + (async () => { + const db = VectorDB.withDimensions(128); + const id = await db.insert({vector: new Float32Array(128).fill(0.5)}); + const result = await db.search({vector: new Float32Array(128).fill(0.5), k: 1}); + if (await db.len() !== 1 || result.length !== 1 || result[0].id !== id) process.exit(1); + })().catch(error => { console.error(error); process.exit(1); }); + ' + npm install --global agentic-qe@3.13.10 --no-audit --no-fund + cp "$GITHUB_WORKSPACE/release-evidence/core.test.js" node_modules/@ruvector/core/core.test.js + AQE_LLM_PROVIDER=claude-code aqe test execute \ + node_modules/@ruvector/core/core.test.js \ + --framework node-test \ + --type integration \ + --format json \ + --output "$GITHUB_WORKSPACE/agentic-qe-result.json" + node -e ' + const result = require(process.env.GITHUB_WORKSPACE + "/agentic-qe-result.json"); + if (result.status !== "passed" || result.total < 1 || result.failed !== 0) process.exit(1); + ' + node -e ' + const fs = require("fs"); + fs.writeFileSync(process.env.GITHUB_WORKSPACE + "/runtime-grader.json", JSON.stringify({ + grader: "runtime", + score: 100, + agenticQe: "3.13.10", + testsPassed: 1, + testsFailed: 0 + }, null, 2) + "\n"); + ' + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: runtime-grader-${{ inputs.expected_sha }} + path: | + runtime-grader.json + agentic-qe-result.json + + contract-grader: + name: Package-contract grader (100 required) + needs: seal + runs-on: ubuntu-22.04 + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: sealed-core-npm-${{ inputs.expected_sha }} + path: release-evidence + - name: Verify package identities and contents + run: | + set -euo pipefail + (cd release-evidence/sealed && sha256sum -c SHA256SUMS) + test "$(find release-evidence/sealed -name '*.tgz' -type f | wc -l)" = 6 + for archive in "release-evidence/sealed"/ruvector-core-*-"$PLATFORM_VERSION".tgz; do + tar -tzf "$archive" | grep -qx 'package/ruvector.node' + tar -tzf "$archive" | grep -qx 'package/package.json' + done + tar -tzf "release-evidence/sealed/ruvector-core-$CORE_VERSION.tgz" | grep -qx 'package/index.js' + echo '{"grader":"package-contract","score":100}' > contract-grader.json + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: contract-grader-${{ inputs.expected_sha }} + path: contract-grader.json + + publish: + name: Publish sealed npm artifacts + needs: [seal, runtime-grader, contract-grader] + runs-on: ubuntu-22.04 + environment: npm-production + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: sealed-core-npm-${{ inputs.expected_sha }} + path: release-evidence + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '22' + registry-url: https://registry.npmjs.org + - name: Publish in dependency order + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -euo pipefail + test -n "$NODE_AUTH_TOKEN" + (cd release-evidence/sealed && sha256sum -c SHA256SUMS) + packages=( + "ruvector-core-linux-x64-gnu@$PLATFORM_VERSION" + "ruvector-core-linux-arm64-gnu@$PLATFORM_VERSION" + "ruvector-core-darwin-x64@$PLATFORM_VERSION" + "ruvector-core-darwin-arm64@$PLATFORM_VERSION" + "ruvector-core-win32-x64-msvc@$PLATFORM_VERSION" + "@ruvector/core@$CORE_VERSION" + ) + files=( + "ruvector-core-linux-x64-gnu-$PLATFORM_VERSION.tgz" + "ruvector-core-linux-arm64-gnu-$PLATFORM_VERSION.tgz" + "ruvector-core-darwin-x64-$PLATFORM_VERSION.tgz" + "ruvector-core-darwin-arm64-$PLATFORM_VERSION.tgz" + "ruvector-core-win32-x64-msvc-$PLATFORM_VERSION.tgz" + "ruvector-core-$CORE_VERSION.tgz" + ) + for index in "${!packages[@]}"; do + if npm view "${packages[$index]}" version >/dev/null 2>&1; then + echo "${packages[$index]} already has the exact sealed bytes; skipping" + else + npm publish "release-evidence/sealed/${files[$index]}" --access public --provenance + fi + done + + - name: Verify public bytes and runtime + run: | + set -euo pipefail + mkdir public + packages=( + "ruvector-core-linux-x64-gnu@$PLATFORM_VERSION" + "ruvector-core-linux-arm64-gnu@$PLATFORM_VERSION" + "ruvector-core-darwin-x64@$PLATFORM_VERSION" + "ruvector-core-darwin-arm64@$PLATFORM_VERSION" + "ruvector-core-win32-x64-msvc@$PLATFORM_VERSION" + "@ruvector/core@$CORE_VERSION" + ) + files=( + "ruvector-core-linux-x64-gnu-$PLATFORM_VERSION.tgz" + "ruvector-core-linux-arm64-gnu-$PLATFORM_VERSION.tgz" + "ruvector-core-darwin-x64-$PLATFORM_VERSION.tgz" + "ruvector-core-darwin-arm64-$PLATFORM_VERSION.tgz" + "ruvector-core-win32-x64-msvc-$PLATFORM_VERSION.tgz" + "ruvector-core-$CORE_VERSION.tgz" + ) + for index in "${!packages[@]}"; do + package="${packages[$index]}" + for _attempt in {1..20}; do + npm view "$package" dist.tarball --json > /tmp/tarball.json 2>/dev/null && break + sleep 6 + done + url="$(node -p "JSON.parse(require('fs').readFileSync('/tmp/tarball.json')).toString()")" + test -n "$url" + curl --fail --location "$url" --output "public/${files[$index]}" + done + (cd release-evidence/sealed && sha256sum -c SHA256SUMS) + while read -r digest file; do + test "$(sha256sum "public/$file" | cut -d' ' -f1)" = "$digest" + done < release-evidence/sealed/SHA256SUMS + install_dir="$(mktemp -d)" + cd "$install_dir" + npm init -y >/dev/null + npm install "@ruvector/core@$CORE_VERSION" --ignore-scripts --no-audit --no-fund + node -e 'const m=require("@ruvector/core"); if(m.VectorDB !== m.VectorDb) process.exit(1); console.log(m.version())' + cd "$GITHUB_WORKSPACE" + node -e ' + const fs = require("fs"); + fs.writeFileSync("publication-receipt.json", JSON.stringify({ + schemaVersion: 1, + sha: process.env.EXPECTED_SHA, + platformVersion: process.env.PLATFORM_VERSION, + coreVersion: process.env.CORE_VERSION, + publicBytesMatch: true, + publicRuntimePassed: true + }, null, 2) + "\n"); + ' + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: publication-receipt-${{ inputs.expected_sha }} + path: publication-receipt.json + if-no-files-found: error + retention-days: 365 diff --git a/npm/core/platforms/darwin-arm64/package.json b/npm/core/platforms/darwin-arm64/package.json index 1fa2a8377..2a631a1dd 100644 --- a/npm/core/platforms/darwin-arm64/package.json +++ b/npm/core/platforms/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "ruvector-core-darwin-arm64", - "version": "0.1.25", + "version": "0.1.30", "description": "macOS ARM64 (Apple Silicon M1/M2/M3) native binding for ruvector-core - High-performance vector database with HNSW indexing built in Rust", "main": "index.js", "type": "commonjs", diff --git a/npm/core/platforms/darwin-x64/package.json b/npm/core/platforms/darwin-x64/package.json index 1a8aa31e5..0eaf37a8b 100644 --- a/npm/core/platforms/darwin-x64/package.json +++ b/npm/core/platforms/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "ruvector-core-darwin-x64", - "version": "0.1.25", + "version": "0.1.30", "description": "macOS x64 (Intel) native binding for ruvector-core - High-performance vector database with HNSW indexing built in Rust", "main": "index.js", "type": "commonjs", diff --git a/npm/core/platforms/linux-arm64-gnu/package.json b/npm/core/platforms/linux-arm64-gnu/package.json index c8e5c33b1..fa3d1476b 100644 --- a/npm/core/platforms/linux-arm64-gnu/package.json +++ b/npm/core/platforms/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "ruvector-core-linux-arm64-gnu", - "version": "0.1.25", + "version": "0.1.30", "description": "Linux ARM64 GNU native binding for ruvector-core - High-performance vector database with HNSW indexing built in Rust", "main": "index.js", "type": "commonjs", diff --git a/npm/core/platforms/linux-x64-gnu/package.json b/npm/core/platforms/linux-x64-gnu/package.json index 4d8ea9727..e598ba3fd 100644 --- a/npm/core/platforms/linux-x64-gnu/package.json +++ b/npm/core/platforms/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "ruvector-core-linux-x64-gnu", - "version": "0.1.26", + "version": "0.1.30", "description": "Linux x64 GNU native binding for ruvector-core - High-performance vector database with HNSW indexing built in Rust", "main": "index.js", "type": "commonjs", diff --git a/npm/core/platforms/win32-x64-msvc/package.json b/npm/core/platforms/win32-x64-msvc/package.json index 3e778a799..9b8457099 100644 --- a/npm/core/platforms/win32-x64-msvc/package.json +++ b/npm/core/platforms/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "ruvector-core-win32-x64-msvc", - "version": "0.1.25", + "version": "0.1.30", "description": "Windows x64 MSVC native binding for ruvector-core - High-performance vector database with HNSW indexing built in Rust", "main": "index.js", "type": "commonjs", diff --git a/npm/package-lock.json b/npm/package-lock.json index d68483973..0e3fe286b 100644 --- a/npm/package-lock.json +++ b/npm/package-lock.json @@ -17684,7 +17684,7 @@ }, "packages/core": { "name": "@ruvector/core", - "version": "0.1.31", + "version": "0.1.32", "license": "MIT", "devDependencies": { "@napi-rs/cli": "^2.18.0" @@ -17693,11 +17693,11 @@ "node": ">=18.0.0" }, "optionalDependencies": { - "ruvector-core-darwin-arm64": "0.1.29", - "ruvector-core-darwin-x64": "0.1.29", - "ruvector-core-linux-arm64-gnu": "0.1.29", - "ruvector-core-linux-x64-gnu": "0.1.29", - "ruvector-core-win32-x64-msvc": "0.1.29" + "ruvector-core-darwin-arm64": "0.1.30", + "ruvector-core-darwin-x64": "0.1.30", + "ruvector-core-linux-arm64-gnu": "0.1.30", + "ruvector-core-linux-x64-gnu": "0.1.30", + "ruvector-core-win32-x64-msvc": "0.1.30" } }, "packages/diskann": { diff --git a/npm/packages/core/index.js b/npm/packages/core/index.js index d1e8f40dc..eeabf5ac2 100644 --- a/npm/packages/core/index.js +++ b/npm/packages/core/index.js @@ -42,4 +42,12 @@ function loadNativeModule() { } } -module.exports = loadNativeModule(); +const nativeModule = loadNativeModule(); + +// Match index.d.ts and preserve the historical `VectorDB` spelling while +// retaining the native NAPI-RS `VectorDb` export. +if (nativeModule.VectorDb && !nativeModule.VectorDB) { + nativeModule.VectorDB = nativeModule.VectorDb; +} + +module.exports = nativeModule; diff --git a/npm/packages/core/package.json b/npm/packages/core/package.json index dbabf89a0..f2534ca9e 100644 --- a/npm/packages/core/package.json +++ b/npm/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@ruvector/core", - "version": "0.1.31", + "version": "0.1.32", "description": "High-performance vector database with HNSW indexing - 50k+ inserts/sec, built in Rust for AI/ML similarity search and semantic search applications", "main": "index.js", "types": "index.d.ts", @@ -32,11 +32,11 @@ "@napi-rs/cli": "^2.18.0" }, "optionalDependencies": { - "ruvector-core-linux-x64-gnu": "0.1.29", - "ruvector-core-linux-arm64-gnu": "0.1.29", - "ruvector-core-darwin-x64": "0.1.29", - "ruvector-core-darwin-arm64": "0.1.29", - "ruvector-core-win32-x64-msvc": "0.1.29" + "ruvector-core-linux-x64-gnu": "0.1.30", + "ruvector-core-linux-arm64-gnu": "0.1.30", + "ruvector-core-darwin-x64": "0.1.30", + "ruvector-core-darwin-arm64": "0.1.30", + "ruvector-core-win32-x64-msvc": "0.1.30" }, "publishConfig": { "access": "public" @@ -65,4 +65,4 @@ "ruv", "ruvector" ] -} \ No newline at end of file +} diff --git a/npm/packages/core/test.js b/npm/packages/core/test.js index d305df63b..6080c0320 100644 --- a/npm/packages/core/test.js +++ b/npm/packages/core/test.js @@ -1,4 +1,8 @@ -const { VectorDB } = require('./index.js'); +const { VectorDB, VectorDb } = require('./index.js'); + +if (VectorDB !== VectorDb) { + throw new Error('VectorDB compatibility alias does not match VectorDb'); +} async function test() { console.log('Testing native module...');