Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions .github/workflows/node-spanner-lib-wrapper-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ name: Node Wrapper Unit Tests
on:
push:
branches: [ "main" ]
paths:
- 'spannerlib/wrappers/spannerlib-node/**'
pull_request:
branches: [ "main" ]
paths:
- 'spannerlib/wrappers/spannerlib-node/**'
workflow_dispatch:

permissions:
Expand All @@ -21,25 +17,33 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
# Use ubuntu-22.04 (glibc 2.35) for Linux to ensure broad binary compatibility.
# Pin to windows-2022 (Visual Studio 2022) to avoid node-gyp auto-detection failure on experimental VS 18 preview on windows-latest.
os: [ubuntu-22.04, macos-latest, windows-2022]
node-version: [22, 24, 26]

env:
npm_config_enable_thin_lto: "false"
npm_config_enable_lto: "false"
npm_config_use_lld: "false"
GYP_DEFINES: "enable_thin_lto=false enable_lto=false use_lld=false lto_jobs="

defaults:
run:
shell: bash
working-directory: ./spannerlib/wrappers/spannerlib-node

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: '1.26.x'
cache-dependency-path: spannerlib/go.sum

- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
Expand All @@ -49,7 +53,9 @@ jobs:
run: npm install

- name: Build Addon and TS
run: npm run build
run: |
unset CFLAGS CXXFLAGS LDFLAGS 2>/dev/null || true
npm run build

- name: Run Unit Tests
run: npm test
204 changes: 204 additions & 0 deletions .github/workflows/release-node-wrapper.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
name: Build and Release Node Wrapper

on:
workflow_dispatch:
inputs:
platform:
description: 'Target platform package to release'
required: true
type: choice
options:
- 'all'
- 'darwin-arm64'
- 'linux-x64'
- 'linux-arm64'
- 'win32-x64'
default: 'all'
npm_tag:
Comment thread
olavloite marked this conversation as resolved.
description: 'NPM distribution tag (e.g. alpha(default), beta, latest, next)'
required: false
type: string
default: 'alpha'
npm_token:
description: 'NPM Token for Wombat (leave empty for dry run)'
required: false
type: string
default: ''

permissions:
contents: read

jobs:
release:
name: Release ${{ matrix.pkg_name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: darwin-arm64
os_name: darwin
cpu_name: arm64
goarch: arm64
pkg_name: "@google-cloud/spannerlib-node-darwin-arm64"
- os: ubuntu-22.04
target: linux-x64
os_name: linux
cpu_name: x64
goarch: amd64
pkg_name: "@google-cloud/spannerlib-node-linux-x64"
- os: ubuntu-22.04
target: linux-arm64
os_name: linux
cpu_name: arm64
goarch: arm64
pkg_name: "@google-cloud/spannerlib-node-linux-arm64"
cc: aarch64-linux-gnu-gcc
cxx: aarch64-linux-gnu-g++
ar: aarch64-linux-gnu-ar
- os: windows-2022
target: win32-x64
os_name: win32
cpu_name: x64
goarch: amd64
pkg_name: "@google-cloud/spannerlib-node-win32-x64"

defaults:
run:
shell: bash
working-directory: ./spannerlib/wrappers/spannerlib-node

steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Check Platform Target
id: check
env:
PLATFORM_INPUT: ${{ inputs.platform || 'all' }}
MATRIX_TARGET: ${{ matrix.target }}
run: |
if [ "$PLATFORM_INPUT" = "all" ] || [ "$PLATFORM_INPUT" = "$MATRIX_TARGET" ]; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
echo "Skipping matrix target $MATRIX_TARGET (requested: $PLATFORM_INPUT)"
fi

- name: Install ARM64 Cross-Compiler (Linux ARM64)
if: steps.check.outputs.run == 'true' && matrix.target == 'linux-arm64'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binutils-aarch64-linux-gnu

- name: Set up Go
if: steps.check.outputs.run == 'true'
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: '1.26.x'
cache-dependency-path: spannerlib/go.sum

- name: Set up Node.js
if: steps.check.outputs.run == 'true'
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: spannerlib/wrappers/spannerlib-node/package.json

- name: Mask NPM Token
if: steps.check.outputs.run == 'true' && inputs.npm_token != ''
env:
NPM_TOKEN: ${{ inputs.npm_token }}
run: |
echo "::add-mask::${NPM_TOKEN}"

- name: Configure NPM registry auth
if: steps.check.outputs.run == 'true' && inputs.npm_token != ''
env:
NPM_TOKEN: ${{ inputs.npm_token }}
run: |
echo "//wombat-dressing-room.appspot.com/:_authToken=${NPM_TOKEN}" > ~/.npmrc

- name: Install dependencies
if: steps.check.outputs.run == 'true'
run: npm install

- name: Build Go library and C++ Addon
if: steps.check.outputs.run == 'true'
env:
npm_config_enable_thin_lto: "false"
npm_config_enable_lto: "false"
npm_config_use_lld: "false"
GYP_DEFINES: "enable_thin_lto=false enable_lto=false use_lld=false lto_jobs="
npm_config_arch: ${{ matrix.cpu_name }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: "1"
run: |
unset CFLAGS CXXFLAGS LDFLAGS 2>/dev/null || true
[ -n "${{ matrix.cc }}" ] && export CC="${{ matrix.cc }}"
[ -n "${{ matrix.cxx }}" ] && export CXX="${{ matrix.cxx }}"
[ -n "${{ matrix.ar }}" ] && export AR="${{ matrix.ar }}"
npm run build

- name: Prepare Platform Package Manifest
if: steps.check.outputs.run == 'true'
env:
PKG_NAME: ${{ matrix.pkg_name }}
OS_NAME: ${{ matrix.os_name }}
CPU_NAME: ${{ matrix.cpu_name }}
run: |
node -e '
const fs = require("fs");
const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
pkg.name = process.env.PKG_NAME;
pkg.os = [process.env.OS_NAME];
pkg.cpu = [process.env.CPU_NAME];
fs.writeFileSync("./package.json", JSON.stringify(pkg, null, 2) + "\n");
console.log("Updated package.json for " + pkg.name + " (" + pkg.os + "/" + pkg.cpu + ")");
'

- name: Package and Publish
if: steps.check.outputs.run == 'true'
env:
NPM_TOKEN: ${{ inputs.npm_token }}
NPM_TAG: ${{ inputs.npm_tag || 'alpha' }}
run: |
set -eo pipefail
export NPM_CONFIG_PREFIX="${HOME}/.npm-global"

# Optional releasetool publisher script reporter if available
if python3 -c "import releasetool" 2>/dev/null; then
python3 -m releasetool publish-reporter-script > /tmp/publisher-script 2>/dev/null || true
if [ -f /tmp/publisher-script ]; then
source /tmp/publisher-script || true
fi
fi

npm pack .
TARBALL=$(ls -1 -t *.tgz | head -1)
echo "Generated tarball: $TARBALL"

# Publish only when a non-empty npm_token is provided
if [ -n "$NPM_TOKEN" ]; then
echo "Publishing $TARBALL with tag '$NPM_TAG' to Wombat registry..."
npm publish --access=public --tag "$NPM_TAG" --registry=https://wombat-dressing-room.appspot.com "$TARBALL"

Check notice on line 186 in .github/workflows/release-node-wrapper.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

use-trusted-publishing

release-node-wrapper.yml:186: prefer trusted publishing for authentication: this command
else
echo "Dry run mode active: No NPM token provided. Skipped publishing $TARBALL."
fi

find node_modules -name package-lock.json -o -name "*.tgz" | xargs rm -f 2>/dev/null || true

- name: Upload Release Tarball Artifact
if: steps.check.outputs.run == 'true'
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: spannerlib-node-${{ matrix.target }}-tarball
path: spannerlib/wrappers/spannerlib-node/*.tgz

- name: Cleanup credentials
if: always()
run: |
rm -f ~/.npmrc
rm -f /tmp/publisher-script 2>/dev/null || true
56 changes: 48 additions & 8 deletions spannerlib/wrappers/spannerlib-node/BUILD_AND_RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,27 @@ Bridging JavaScript to the native Go SDK involves a sequential compilation pipel
|
(cgo linker bridge)
v
[Go Shared Library]
[Go Static Archive / DLL]
```

## Compilation Phases

### Phase 1: Compiling the Go Shared Library (CGO Link)
### Phase 1: Compiling the Go Library (CGO Link)

Before building the Node.js Addon, the underlying Go codebase must be compiled into an object format that C/C++ can link against.
* **Trigger:** Executed via `npm run build:go` which runs `bash scripts/build-shared-lib.sh`.
* **Action:** The build script invokes the Go compiler with the `-buildmode=c-shared` flag, targeting the primary C-shared entry point located at [shared_lib.go](../../shared/shared_lib.go).
* **Outputs:** Generates a platform-specific native shared library (e.g., `libspannerlib.dylib` on macOS, `.so` on Linux, `.dll` on Windows) along with the corresponding C header file (`libspannerlib.h`). Both files are placed into the `spannerlib/shared/` directory.
* **Action & Outputs:**
* **Linux & macOS (`-buildmode=c-archive`):** Invokes Go with `-buildmode=c-archive` targeting [shared_lib.go](../../shared/shared_lib.go) to generate a static archive (`libspanner.a`) and C header (`libspanner.h`). The static archive embeds all Go runtime and driver symbols directly into the final `spanner_napi.node` binary, eliminating dynamic shared library dependencies, `@loader_path` rpaths, and `.so`/`.dylib` file distribution.
* **Windows (`-buildmode=c-shared`):** Invokes Go with `-buildmode=c-shared` to generate a dynamic shared library (`libspanner.dll`) along with a companion MSVC-compatible import library (`libspanner.lib`) required by MSVC's linker (`link.exe`).

### Phase 2: Compiling the Native Bridge (node-gyp)

Once the Go shared library is generated, the Node.js C++ wrapper is compiled using `node-gyp` to map V8 engine objects into Go pointers.
Once the Go library is generated, the Node.js C++ wrapper is compiled using `node-gyp` to map V8 engine objects into Go pointers.
* **Trigger:** Executed as part of `npm run build` which invokes `node-gyp rebuild`.
* **Action:** Reads the `gyp` build instructions in [binding.gyp](./binding.gyp) to locate the Go header files, and dynamically links the bridge against the generated Go shared object. It compiles the bridge source file [addon.cc](./src/cpp/addon.cc) using the local OS C++ compiler toolchain (e.g., Clang on macOS, GCC on Linux, MSVC on Windows).
* **Action:** Reads the build instructions in [binding.gyp](./binding.gyp) to locate the Go header files, and links the bridge against the generated Go library. It compiles the bridge source file [addon.cc](./src/cpp/addon.cc) using the local OS C++ compiler toolchain (Clang on macOS, GCC on Linux, MSVC on Windows).
* On **Linux and macOS**, `binding.gyp` statically links `libspanner.a` directly into the `.node` binary.
* On **Windows**, `binding.gyp` links against `libspanner.lib` and copies `libspanner.dll` adjacent to the addon in `build/Release/`.
* **Output:** Generates the native Node.js binary file at `build/Release/spanner_napi.node`.
* **Post-build Link Patch (macOS Only):** To ensure portability on macOS without requiring root or global library installs, `npm run postbuild` invokes the `install_name_tool`. This command alters the dynamic linker search path in the `.node` file to use `@loader_path/libspannerlib.dylib`, ensuring Node.js locates the Go dynamic library relatively from the compiled C++ bridge binary path.

### Phase 3: TypeScript Compilation & Dual-Publishing (ESM / CJS)

Expand All @@ -51,4 +53,42 @@ To run the entire pipeline end-to-end and generate a fully runnable local build,
```bash
npm run build
```
This builds the underlying Go shared library, links the C++ bridge layer via `node-gyp`, patches dynamic linker paths, and outputs the final dual ESM/CJS JavaScript distributions.
This builds the underlying Go library, links the C++ bridge layer via `node-gyp`, and outputs the final dual ESM/CJS JavaScript distributions.

## Platform-Specific Release Pipelines (GitHub Actions)

Releasing the prebuilt native platform packages is managed via a unified manual GitHub Actions workflow:
* **Workflow:** [release-node-wrapper.yml](../../../.github/workflows/release-node-wrapper.yml)

### Target Platform Packages

| Package Name | Target Platform | Runner OS | glibc / Toolchain |
| :--- | :--- | :--- | :--- |
| **`@google-cloud/spannerlib-node-darwin-arm64`** | macOS (Apple Silicon `arm64`) | `macos-latest` | Apple Clang (`c-archive` static) |
| **`@google-cloud/spannerlib-node-linux-x64`** | Linux (`x64`) | `ubuntu-22.04` | **glibc 2.35** / GCC (`c-archive` static) |
| **`@google-cloud/spannerlib-node-linux-arm64`** | Linux (`arm64`) | `ubuntu-22.04` | **glibc 2.35** / `gcc-aarch64-linux-gnu` (cross-compile) |
| **`@google-cloud/spannerlib-node-win32-x64`** | Windows (`x64`) | `windows-2022` | MSVC 2022 (`c-shared` DLL + `.lib`) |

> **Note on Linux Compatibility:** Compiling on `ubuntu-22.04` dynamically links against **glibc 2.35**, ensuring wide binary compatibility with older and enterprise Linux distributions (such as Debian 12, Ubuntu 22.04+, and RHEL 9). The Linux ARM64 build is cross-compiled on `ubuntu-22.04` using `gcc-aarch64-linux-gnu` rather than running on Ubuntu 24.04 ARM runners to prevent glibc 2.39 lock-in.

### Triggering a Release
The workflow uses `workflow_dispatch` and publishes to the Google Wombat registry (`https://wombat-dressing-room.appspot.com`):

1. Go to the **Actions** tab in GitHub.
2. Select **Build and Release Node Wrapper**.
3. Click **Run workflow**.
4. Provide the inputs:
* **`platform`** *(Required)*: Select `all` (default) to build and release all 4 platforms concurrently, or select a specific target (`darwin-arm64`, `linux-x64`, `linux-arm64`, `win32-x64`).
* **`npm_tag`** *(Optional, default: `alpha`)*: NPM distribution tag (e.g. `alpha`, `beta`, `latest`).
* **`npm_token`** *(Optional)*: The authentication token for `//wombat-dressing-room.appspot.com/:_authToken`. If left empty, the workflow automatically runs in **dry-run mode** (builds, packages, and uploads `.tgz` artifacts without publishing).

### Release Execution Steps
For each target platform in the matrix, the workflow:
1. Sets up Go (`1.26.x`) and Node.js (`22`) environments.
2. Masks the supplied `npm_token` and configures `~/.npmrc` for Wombat registry auth.
3. Installs dependencies (`npm install`).
4. Compiles the Go library (`libspanner.a` / `libspanner.dll`), links the C++ N-API addon, and compiles TypeScript dual outputs (`npm run build`).
5. Sets the target package name (e.g. `@google-cloud/spannerlib-node-linux-x64`) and `os`/`cpu` metadata in `package.json`.
6. Packages the distribution tarball (`npm pack .`) containing both the JavaScript bundles and the compiled native binary (`spanner_napi.node` plus `libspanner.dll` on Windows).
7. Publishes the generated tarball to Wombat with the specified dist-tag (`npm publish --access=public --tag $NPM_TAG --registry=https://wombat-dressing-room.appspot.com`).
8. Archives and uploads the release `.tgz` as a workflow artifact for auditing and SBOM generation.
2 changes: 1 addition & 1 deletion spannerlib/wrappers/spannerlib-node/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Node-API Wrapper for Spanner Shared Library

This package provides a high-performance Node-API (N-API) bridge to the Go-based Spanner shared library. It offers superior stability and performance compared to traditional FFI approaches.
> **NOTICE:** This is an internal library intended for use by Google Cloud Spanner driver packages (such as the high-level Node.js Spanner driver). It is not intended for direct use by end customers and can introduce breaking changes without prior notice.

## Prerequisites

Expand Down
Loading
Loading