diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 5f2cefa9..5cd86996 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -33,23 +33,66 @@ jobs: with: node-version: ${{ matrix.node }} - - name: Install dependencies (npm install) - run: npm install + - name: Build npm packages from the current release + shell: bash + run: | + TEMP_DIR="${RUNNER_TEMP//\\//}" + VERSION=$(node -p "require('./package.json').version") + gh release download "v$VERSION" -p '*.zip' -D "$TEMP_DIR/zips" + ../scripts/build-npm-platform-packages.sh "v$VERSION" "$TEMP_DIR/zips" "$TEMP_DIR/platform" + git checkout package.json + npm pack --pack-destination "$TEMP_DIR" + (cd "$TEMP_DIR/platform/sqlx-ts-$(node -p 'process.platform + "-" + process.arch')" && npm pack --pack-destination "$TEMP_DIR") + env: + GH_TOKEN: ${{ github.token }} + + - name: Install sqlx-ts with its platform package + shell: bash + run: | + TEMP_DIR="${RUNNER_TEMP//\\//}" + mkdir "$TEMP_DIR/project" + cd "$TEMP_DIR/project" + npm init -y + npm install "$TEMP_DIR"/sqlx-ts-*.tgz - name: Verify sqlx-ts binary from npm install + working-directory: ${{ runner.temp }}/project + run: | + npx sqlx-ts --version + npx sqlx-ts --help + + - uses: oven-sh/setup-bun@v2 + if: matrix.node == 22 + + - name: Verify sqlx-ts binary from bun install + if: matrix.node == 22 + shell: bash run: | - chmod +x ./sqlx-ts || true - ./sqlx-ts --version - ./sqlx-ts --help + TEMP_DIR="${RUNNER_TEMP//\\//}" + mkdir "$TEMP_DIR/bun-project" + cd "$TEMP_DIR/bun-project" + echo '{ "name": "bun-project" }' > package.json + bun add "$TEMP_DIR"/sqlx-ts-*.tgz + bunx sqlx-ts --version + + - uses: denoland/setup-deno@v2 + if: matrix.node == 22 + with: + deno-version: v2.x - - name: Install using local install.sh - run: node postinstall.js + - name: Verify sqlx-ts launcher with deno + if: matrix.node == 22 + working-directory: ${{ runner.temp }}/project + run: deno run -A node_modules/sqlx-ts/bin/sqlx-ts.js --version - - name: Verify sqlx-ts binary from local install + - name: Install using install.sh + shell: bash run: | - chmod +x ./sqlx-ts || true - ./sqlx-ts --version - ./sqlx-ts --help + OS=$(node -p 'process.platform') + CPU=$(node -p '["arm64", "x64"].includes(process.arch) ? process.arch : "x32"') + TEMP_DIR="${RUNNER_TEMP//\\//}" + sh ../scripts/install.sh --os "$OS" --cpu "$CPU" --to "$TEMP_DIR/bin" + "$TEMP_DIR/bin/sqlx-ts" --version linux-distro-static-binary-test: name: linux distro ${{ matrix.distro }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3fc28be7..d65b426f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -124,13 +124,31 @@ jobs: node-version: '24' registry-url: 'https://registry.npmjs.org' - run: npm ci + - run: npm run compile + - run: cp ../README.md . - run: npm version "${TAG#v}" --no-git-tag-version env: TAG: ${{ github.event.release.tag_name }} - - run: npm publish --access public + - name: Publish platform packages and sqlx-ts + run: | + [[ "$TAG" == *-* ]] && NPM_TAG=next || NPM_TAG=latest + gh release download "$TAG" -p '*.zip' -D "$RUNNER_TEMP/zips" + ../scripts/build-npm-platform-packages.sh "$TAG" "$RUNNER_TEMP/zips" "$RUNNER_TEMP/platform" + for dir in "$RUNNER_TEMP"/platform/*; do + if [ -n "$(npm view "$(basename "$dir")@${TAG#v}" version 2> /dev/null)" ]; then + echo "$(basename "$dir")@${TAG#v} is already published" + else + npm publish "$dir" --access public --tag "$NPM_TAG" + fi + done + npm publish --access public --tag "$NPM_TAG" --ignore-scripts + env: + TAG: ${{ github.event.release.tag_name }} + GH_TOKEN: ${{ github.token }} bump-version: needs: node + if: ${{ !contains(github.event.release.tag_name, '-') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -148,3 +166,45 @@ jobs: git push origin main env: TAG: ${{ github.event.release.tag_name }} + + smoke-test: + needs: node + name: smoke test ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + steps: + - uses: actions/setup-node@v6 + with: + node-version: '24' + - uses: oven-sh/setup-bun@v2 + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + - name: Wait for npm to make the packages available + shell: bash + run: | + VERSION="${TAG#v}" + PLATFORM_PACKAGE="sqlx-ts-$(node -p 'process.platform + "-" + process.arch')" + for i in $(seq 1 30); do + if [ -n "$(npm view "sqlx-ts@$VERSION" version 2> /dev/null)" ] && [ -n "$(npm view "$PLATFORM_PACKAGE@$VERSION" version 2> /dev/null)" ]; then + exit 0 + fi + sleep 20 + done + echo "sqlx-ts@$VERSION or $PLATFORM_PACKAGE@$VERSION is not available on npm after 10 minutes" + exit 1 + env: + TAG: ${{ github.event.release.tag_name }} + - name: Run sqlx-ts with npx, bunx and deno + shell: bash + run: | + VERSION="${TAG#v}" + cd "$(mktemp -d)" + npx -y "sqlx-ts@$VERSION" --version | grep -F "sqlx-ts $VERSION" + bunx "sqlx-ts@$VERSION" --version | grep -F "sqlx-ts $VERSION" + deno run -A "npm:sqlx-ts@$VERSION" --version | grep -F "sqlx-ts $VERSION" + env: + TAG: ${{ github.event.release.tag_name }} diff --git a/README.md b/README.md index be5ebd2e..c0d36538 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,32 @@ Verify the installation by running $ npx sqlx-ts --version ``` +##### Bun and Deno + +From v0.50.0, the `sqlx-ts` binary comes as an optional dependency for your platform (e.g. `sqlx-ts-darwin-arm64`), so +Bun and Deno install it the same way as npm + +```bash +# Bun +$ bun add sqlx-ts +$ bunx sqlx-ts --version +# Deno +$ deno add npm:sqlx-ts +$ deno run -A npm:sqlx-ts --version +``` + +The binary is not installed with `--omit=optional` or `--no-optional`. In that case, or on a platform without a prebuilt +binary, [install the binary separately](#installing-binary-separately) to a directory on your PATH and `sqlx-ts` will use it. + +v0.49.0 and older download the binary in a `postinstall` step, which Bun and Deno do not run. For these versions, install +the binary separately, or with Bun add sqlx-ts to `trustedDependencies` in package.json + +```json +{ + "trustedDependencies": ["sqlx-ts"] +} +``` + And to use sqlx-ts in your code In TypeScript based projects: @@ -119,7 +145,7 @@ If you're a **Windows** user, then you can install sqlx-ts from via install.sh: # windows & x32 $ curl -LSfs https://jasonshin.github.io/sqlx-ts/install.sh | sh -s -- --os win32 --cpu x32 # windows & x64 -$ curl -LSfs https://jasonshin.github.io/sqlx-ts/install.sh | sh -s -- --os win32 --cpu x32 +$ curl -LSfs https://jasonshin.github.io/sqlx-ts/install.sh | sh -s -- --os win32 --cpu x64 ``` If you're a **Linux** user, then you can install sqlx-ts from via install.sh: diff --git a/book/docs/installation.md b/book/docs/installation.md index fb64c20b..508b5fc8 100644 --- a/book/docs/installation.md +++ b/book/docs/installation.md @@ -22,4 +22,35 @@ Verify the installation of `sqlx-ts` binary by running $ npx sqlx-ts --version ``` +## Bun and Deno + +From v0.50.0, the `sqlx-ts` binary comes as an optional dependency for your platform (e.g. `sqlx-ts-darwin-arm64`), so +Bun and Deno install it the same way as npm + +```bash +# Bun +$ bun add --dev sqlx-ts +$ bunx sqlx-ts --version +# Deno +$ deno add --dev npm:sqlx-ts +$ deno run -A npm:sqlx-ts --version +``` + +The binary is not installed with `--omit=optional` or `--no-optional`. In that case, or on a platform without a prebuilt +binary, install the binary to a directory on your PATH with install.sh and `sqlx-ts` will use it + +```bash +# --os: darwin | linux | win32, --cpu: arm64 | x64 | x32 +$ curl -LSfs https://jasonshin.github.io/sqlx-ts/install.sh | sh -s -- --os darwin --cpu arm64 --to ~/.local/bin +``` + +v0.49.0 and older download the binary in a `postinstall` step, which Bun and Deno do not run. For these versions, install +the binary with install.sh as above, or with Bun add sqlx-ts to `trustedDependencies` in package.json + +```json +{ + "trustedDependencies": ["sqlx-ts"] +} +``` + 🚀 you now have `sqlx-ts` installed 🚀 \ No newline at end of file diff --git a/node/bin/sqlx-ts.js b/node/bin/sqlx-ts.js new file mode 100755 index 00000000..34c305ca --- /dev/null +++ b/node/bin/sqlx-ts.js @@ -0,0 +1,44 @@ +#!/usr/bin/env node +const { spawnSync } = require('child_process') +const fs = require('fs') +const path = require('path') + +const platformPackage = `sqlx-ts-${process.platform}-${process.arch}` +const binaryName = process.platform === 'win32' ? 'sqlx-ts.exe' : 'sqlx-ts' + +function findInPlatformPackage() { + try { + return require.resolve(`${platformPackage}/${binaryName}`) + } catch { + return undefined + } +} + +function findOnPath() { + for (const dir of (process.env.PATH || '').split(path.delimiter)) { + const candidate = path.join(dir, binaryName) + if (fs.existsSync(candidate) && !fs.realpathSync(candidate).endsWith('.js')) { + return candidate + } + } +} + +const binary = findInPlatformPackage() || findOnPath() + +if (!binary) { + const cpu = process.arch === 'arm64' || process.arch === 'x64' ? process.arch : 'x32' + console.error(`sqlx-ts binary is not installed. It comes from the optional dependency ${platformPackage}. +If you installed with --omit=optional or --no-optional, or copied node_modules from another OS, reinstall without them. +Otherwise install the binary separately to a directory on your PATH: + + curl -LSfs https://jasonshin.github.io/sqlx-ts/install.sh | sh -s -- --os ${process.platform} --cpu ${cpu} --to ~/.local/bin + +See https://jasonshin.github.io/sqlx-ts for other installation options.`) + process.exit(1) +} + +const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' }) +if (result.error) { + throw result.error +} +process.exit(result.status ?? 1) diff --git a/node/package-lock.json b/node/package-lock.json index fd0c7e21..0d1839f2 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -7,13 +7,9 @@ "": { "name": "sqlx-ts", "version": "0.49.0", - "hasInstallScript": true, "license": "MIT", - "dependencies": { - "adm-zip": "^0.5.16" - }, "bin": { - "sqlx-ts": "sqlx-ts" + "sqlx-ts": "bin/sqlx-ts.js" }, "devDependencies": { "@types/jest": "^27.4.1", @@ -1436,15 +1432,6 @@ "node": ">=0.4.0" } }, - "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - "license": "MIT", - "engines": { - "node": ">=12.0" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", diff --git a/node/package.json b/node/package.json index c77cb5a7..a17b118e 100644 --- a/node/package.json +++ b/node/package.json @@ -25,7 +25,7 @@ "homepage": "https://jasonshin.github.io/sqlx-ts", "repository": { "type": "git", - "url": "https://github.com/JasonShin/sqlx-ts" + "url": "git+https://github.com/JasonShin/sqlx-ts.git" }, "bugs": { "url": "https://github.com/JasonShin/sqlx-ts/issues" @@ -36,19 +36,15 @@ "author": "Jason Shin ", "license": "MIT", "bin": { - "sqlx-ts": "sqlx-ts" + "sqlx-ts": "bin/sqlx-ts.js" }, "scripts": { - "postinstall": "node postinstall.js", "compile": "npx tsc -p tsconfig.json", "lint": "npx eslint --ext .ts src", "lint:fix": "npx eslint --ext .ts src --fix", "test": "npx jest", "prepublishOnly": "cp ../README.md . && npm i && npm run compile" }, - "dependencies": { - "adm-zip": "^0.5.16" - }, "devDependencies": { "@types/jest": "^27.4.1", "@types/node": "^20.14.8", @@ -61,5 +57,9 @@ }, "engines": { "node": ">=16" - } + }, + "files": [ + "dist", + "bin" + ] } diff --git a/node/postinstall.js b/node/postinstall.js deleted file mode 100644 index ff837b4b..00000000 --- a/node/postinstall.js +++ /dev/null @@ -1,194 +0,0 @@ -const { createHash } = require('crypto') -const fs = require('fs') -const https = require('https') -const path = require('path') -const os = require('os') -const tag = require('./package.json').version -const AdmZip = require('adm-zip') - -const platform = process.platform -const cpu = process.arch - -const colors = { - reset: "\x1b[0m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - cyan: "\x1b[36m", -} - -const info = (msg) => console.log(`${colors.cyan}INFO: ${msg} ${colors.reset}`) -const success = (msg) => console.log(`${colors.green}SUCCESS: ${msg} ${colors.reset}`) -const warn = (msg) => console.warn(`${colors.yellow}WARNING: ${msg} ${colors.reset}`) -const error = (msg) => console.error(`${colors.red}ERROR: ${msg} ${colors.reset}`) - -function getBinaryInfo() { - let build = '' - if (platform === 'darwin') { - if (cpu === 'arm64') { - build = 'macos-arm' - } else { - build = 'macos-64-bit' - } - } else if (platform === 'win32') { - if (cpu === 'x64') { - build = 'windows-64-bit' - } else { - build = 'windows-32-bit' - } - } else if (platform === 'linux') { - if (cpu === 'x64') { - build = 'linux-64-bit' - } else if (cpu === 'arm64') { - build = 'linux-arm' - } else { - build = 'linux-32-bit' - } - } else { - throw new Error(`Unsupported platform: ${platform}-${cpu}`) - } - - return { - build, - filename: `sqlx-ts-v${tag}-${build}.zip`, - binaryName: platform === 'win32' ? 'sqlx-ts.exe' : 'sqlx-ts' - } -} - -function safeUnlink(filePath) { - try { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath) - } - } catch (err) { - warn(`Failed to delete file: filePath: ${filePath}, err: ${err.message}`) - } -} - -// Download file from URL -function downloadFile(url, destination, redirectCount = 0) { - return new Promise((resolve, reject) => { - if (redirectCount > 5) { - return reject(new Error("Too many redirects while downloading file")) - } - - const file = fs.createWriteStream(destination) - https.get(url, (response) => { - if (response.statusCode === 302 || response.statusCode === 301) { - // Handle redirects - // Close the current file and delete it - // Then download from the new location - file.close() - safeUnlink(destination) - return downloadFile(response.headers.location, destination, redirectCount + 1) - .then(resolve) - .catch(reject) - } - - if (response.statusCode !== 200) { - file.close() - safeUnlink(destination) - return reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`)) - } - - response.pipe(file) - file.on('finish', () => { - file.close(resolve) - }) - }).on('error', (err) => { - safeUnlink(destination) - reject(err) - }) - }) -} - -// Calculate SHA-256 hash of a file -function calculateSHA256(filePath) { - return new Promise((resolve, reject) => { - const hash = createHash('sha256') - const stream = fs.createReadStream(filePath) - - stream.on('data', (data) => hash.update(data)) - stream.on('end', () => resolve(hash.digest('hex'))) - stream.on('error', reject) - }) -} - -async function verifyHash(filePath, expectedHash) { - const actualHash = await calculateSHA256(filePath) - - if (actualHash !== expectedHash) { - throw new Error( - `Hash mismatch!\n` + - `Expected: ${expectedHash}\n` + - `Got: ${actualHash}\n` + - `This could indicate a corrupted download or a security issue.` - ) - } - - return true -} - -function extractBinary(zipPath, binaryName, targetPath) { - const zip = new AdmZip(zipPath) - const zipEntries = zip.getEntries() - - for (const entry of zipEntries) { - if (entry.entryName.endsWith(binaryName)) { - // Extract the entry's content directly - const data = entry.getData() - fs.writeFileSync(targetPath, data) - fs.chmodSync(targetPath, 0o755) - return - } - } - - throw new Error(`Binary ${binaryName} not found in archive`) -} - -async function install() { - try { - const { filename, binaryName } = getBinaryInfo() - const baseUrl = `https://github.com/JasonShin/sqlx-ts/releases/download/v${tag}` - const zipUrl = `${baseUrl}/${filename}` - const checksumUrl = `${zipUrl}.sha256` - - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sqlx-ts-')) - const zipPath = path.join(tmpDir, filename) - const checksumPath = path.join(tmpDir, `${filename}.sha256`) - const targetPath = path.join(__dirname, 'sqlx-ts' + (platform === 'win32' ? '.exe' : '')) - - info(`Downloading sqlx-ts v${tag} for ${platform}-${cpu}...`) - info(`URL: ${zipUrl}`) - - // Download the zip file - await downloadFile(zipUrl, zipPath) - success('Download complete') - - // Download and verify the checksum - info('Downloading checksum...') - await downloadFile(checksumUrl, checksumPath) - const expectedHash = fs.readFileSync(checksumPath, 'utf8').trim() - info(`Expected SHA-256: ${expectedHash}`) - - // Verify the hash - info('Verifying checksum...') - await verifyHash(zipPath, expectedHash) - success('Checksum verified successfully') - - // Extract the binary - info('Extracting binary...') - extractBinary(zipPath, binaryName, targetPath) - - // Cleanup - fs.rmSync(tmpDir, { recursive: true, force: true }) - - info('sqlx-ts installation successful') - process.exit(0) - } catch (err) { - error(`Installation failed: ${err.message}`) - process.exit(1) - } -} - -install() diff --git a/scripts/build-npm-platform-packages.sh b/scripts/build-npm-platform-packages.sh new file mode 100755 index 00000000..e9ee4545 --- /dev/null +++ b/scripts/build-npm-platform-packages.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Usage: build-npm-platform-packages.sh +# Builds one npm package per release zip into and adds them to node/package.json as optionalDependencies +set -euo pipefail + +tag=$1 +zip_dir=$2 +out_dir=$3 +version="${tag#v}" +node_dir="$(cd "$(dirname "$0")/../node" && pwd)" + +for entry in macos-arm:darwin:arm64 macos-64-bit:darwin:x64 linux-64-bit:linux:x64 linux-arm:linux:arm64 \ + linux-32-bit:linux:ia32 windows-64-bit:win32:x64 windows-32-bit:win32:ia32; do + IFS=: read -r build os cpu <<< "$entry" + name="sqlx-ts-$os-$cpu" + dir="$out_dir/$name" + mkdir -p "$dir" + unzip -oqj "$zip_dir/sqlx-ts-$tag-$build.zip" -d "$dir" + chmod 755 "$dir"/sqlx-ts* + cat > "$dir/package.json" < /dev/null 2>&1; then + actual=$(sha256sum $td/sqlx-ts.zip | cut -d' ' -f1) else - cp $td/sqlx-ts . + actual=$(shasum -a 256 $td/sqlx-ts.zip | cut -d' ' -f1) +fi +if [ "$actual" != "$expected" ]; then + err "checksum mismatch for $url: expected $expected, got $actual" fi +rm $td/sqlx-ts.zip.sha256 +unzip -j $td/sqlx-ts.zip -d $td # shellcheck disable=SC2045 for f in $(ls $td); do