Skip to content

v0.6.0

v0.6.0 #31

name: npm-build-publish
# Builds hyperdb-mcp and hyperdb-api-node npm packages with bundled
# hyperd binaries. Triggered after a successful release workflow creates
# a GitHub release, or manually via workflow_dispatch.
on:
workflow_dispatch:
inputs:
tag:
description: "Tag or branch to build (e.g. v0.1.0). Leave empty for current default branch."
required: false
default: ""
release:
types: [published]
concurrency:
group: npm-build-publish
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
HYPERD_VERSION: "0.0.25080"
HYPERD_BUILD_ID: "r2bfd835b"
jobs:
verify-ci:
name: verify CI passed
if: github.repository == 'tableau/hyper-api-rust' && github.event_name == 'release'
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.release.tag_name }}
- name: Wait for CI to pass
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
SHA=$(git rev-parse HEAD)
echo "Waiting for required CI check-runs on $SHA"
# GitHub Actions report results via check-runs, not the legacy
# commit-status API. Poll check-runs by name; ignore the
# verify-ci job from this workflow (it would self-reference).
MAX_ATTEMPTS=30
SLEEP_SECONDS=60
# Exact-name membership against the canonical check-run list.
# We previously used a regex via `test()` here, but the names
# contain `(` and `+` which both have regex meaning — escaping
# them through bash → jq → Oniguruma was fragile and broke
# silently on the v0.2.2 release. Exact-string match avoids
# the whole class of regex-escaping bugs.
REQUIRED_JSON='[
"rustfmt","clippy","cargo-audit","cargo-deny",
"version consistency","publish dry-run",
"test (ubuntu-latest)","test (macos-14)","test (windows-latest)",
"hyperdb-api-node (build + smoke)"
]'
for i in $(seq 1 "$MAX_ATTEMPTS"); do
RUNS=$(gh api "repos/$REPO/commits/$SHA/check-runs?per_page=100" \
--jq "[.check_runs[] | select(.name as \$n | $REQUIRED_JSON | index(\$n))]")
TOTAL=$(echo "$RUNS" | jq 'length')
COMPLETED=$(echo "$RUNS" | jq '[.[] | select(.status=="completed")] | length')
FAILED=$(echo "$RUNS" | jq '[.[] | select(.status=="completed" and (.conclusion!="success" and .conclusion!="skipped" and .conclusion!="neutral"))] | length')
echo "Attempt $i/$MAX_ATTEMPTS: total=$TOTAL completed=$COMPLETED failed=$FAILED"
if [[ "$FAILED" -gt 0 ]]; then
echo "::error::CI failed for $SHA. Failing check-runs:"
echo "$RUNS" | jq -r '.[] | select(.status=="completed" and (.conclusion!="success" and .conclusion!="skipped" and .conclusion!="neutral")) | " - \(.name): \(.conclusion)"'
exit 1
fi
if [[ "$TOTAL" -gt 0 && "$COMPLETED" == "$TOTAL" ]]; then
echo "CI passed ($TOTAL required check-runs completed successfully)."
exit 0
fi
echo "CI still pending, waiting ${SLEEP_SECONDS}s..."
sleep "$SLEEP_SECONDS"
done
echo "::error::Timed out waiting for CI to pass ($SHA)."
exit 1
build-npm:
name: build-npm (${{ matrix.platform }})
needs: verify-ci
if: always() && (needs.verify-ci.result == 'success' || needs.verify-ci.result == 'skipped')
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- platform: darwin-arm64
os: macos-14
target: aarch64-apple-darwin
hyperd-slug: macos-arm64
hyperd-sha256: "2b0fa3fefcf4eba60f052e1cb51abfc32d8c84354274513763760f9549b45991"
# TODO: re-enable when macos-13 runners are more available
# - platform: darwin-x64
# os: macos-13
# target: x86_64-apple-darwin
# hyperd-slug: macos-x86_64
# hyperd-sha256: "d5d3dae60ce071aed45e534fb6a4fc6c7edce47e31d664d131af9f76d9b3a2aa"
- platform: linux-x64-gnu
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
hyperd-slug: linux-x86_64
hyperd-sha256: "3d3fd2104f55f7fad832470592394dc78f350a03d52e89d36c5288b202dd0bc0"
- platform: win32-x64-msvc
os: windows-latest
target: x86_64-pc-windows-msvc
hyperd-slug: windows-x86_64
hyperd-sha256: "9dc4851d416e0e6e00f0367ee6b45fcd676e7ba3a110d4644e3bec871b9aa1de"
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.tag || github.event.release.tag_name || github.ref }}
- name: Install system libraries (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update -q && sudo apt-get install -y libfontconfig1-dev mold protobuf-compiler
- name: Install protobuf (macOS)
if: runner.os == 'macOS'
run: brew install protobuf
- name: Install protobuf (Windows)
if: runner.os == 'Windows'
run: choco install protoc -y
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target }}
cache-key: npm-${{ matrix.target }}
rustflags: ""
- name: Build hyperdb-mcp
env:
TARGET: ${{ matrix.target }}
run: cargo build --release --target "$TARGET" -p hyperdb-mcp
- name: Build hyperdb-api-node
env:
TARGET: ${{ matrix.target }}
run: cargo build --release --target "$TARGET" -p hyperdb-api-node
- name: Download and verify hyperd
env:
SLUG: ${{ matrix.hyperd-slug }}
EXPECTED_SHA256: ${{ matrix.hyperd-sha256 }}
run: |
set -euo pipefail
URL="https://downloads.tableau.com/tssoftware/tableauhyperapi-java-${SLUG}-release-main.${HYPERD_VERSION}.${HYPERD_BUILD_ID}.zip"
echo "Downloading: $URL"
curl --fail --silent --show-error --location --output hyperd-archive.zip "$URL"
if command -v sha256sum &>/dev/null; then
ACTUAL_SHA256=$(sha256sum hyperd-archive.zip | awk '{print $1}')
else
ACTUAL_SHA256=$(shasum -a 256 hyperd-archive.zip | awk '{print $1}')
fi
if [[ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]]; then
echo "::error::SHA256 mismatch: expected $EXPECTED_SHA256, got $ACTUAL_SHA256"
exit 1
fi
echo "SHA256 verified: $ACTUAL_SHA256"
- name: Extract hyperd from archive
run: |
set -euo pipefail
mkdir -p hyperd-extracted
unzip -q hyperd-archive.zip -d hyperd-raw
# Find the lib/hyper or bin/hyper directory (matches extract.rs logic)
HYPER_DIR=$(find hyperd-raw -type d -name "hyper" -path "*/lib/hyper" -o -type d -name "hyper" -path "*/bin/hyper" | head -1)
if [[ -z "$HYPER_DIR" ]]; then
echo "::error::Could not find lib/hyper or bin/hyper in archive"
exit 1
fi
cp -R "$HYPER_DIR"/* hyperd-extracted/
ls -la hyperd-extracted/
# Verify hyperd binary exists
if [[ -f hyperd-extracted/hyperd ]] || [[ -f hyperd-extracted/hyperd.exe ]]; then
echo "hyperd binary found"
else
echo "::error::hyperd binary not found in extracted archive"
exit 1
fi
- name: Extract LICENSE from hyperd archive
run: |
set -euo pipefail
LICENSE_FILE=$(find hyperd-raw -iname "LICENSE*" -o -iname "NOTICE*" | head -1)
if [[ -n "$LICENSE_FILE" ]]; then
cp "$LICENSE_FILE" LICENSE-HYPERD
else
cat > LICENSE-HYPERD << 'LICEOF'
The hyperd binary is part of the Tableau Hyper API, Copyright Salesforce, Inc.
Licensed under the Apache License, Version 2.0.
See https://www.apache.org/licenses/LICENSE-2.0
LICEOF
fi
- name: Assemble hyperdb-mcp npm platform package
env:
PLATFORM: ${{ matrix.platform }}
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
DEST="hyperdb-mcp/npm/${PLATFORM}"
EXE=""
HYPERD_BIN="hyperd"
if [[ "$PLATFORM" == "win32-x64-msvc" ]]; then
EXE=".exe"
HYPERD_BIN="hyperd.exe"
fi
cp "target/${TARGET}/release/hyperdb-mcp${EXE}" "$DEST/"
cp "hyperd-extracted/${HYPERD_BIN}" "$DEST/"
# Copy shared libraries that hyperd needs
find hyperd-extracted -name "*.so*" -o -name "*.dylib" -o -name "*.dll" | while read f; do
cp "$f" "$DEST/"
done
cp LICENSE-HYPERD "$DEST/"
chmod +x "$DEST/hyperdb-mcp${EXE}" "$DEST/${HYPERD_BIN}" 2>/dev/null || true
echo "Contents of $DEST:"
ls -la "$DEST/"
- name: Assemble hyperdb-api-node npm platform package
env:
PLATFORM: ${{ matrix.platform }}
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
DEST="hyperdb-api-node/npm/${PLATFORM}"
EXE=""
HYPERD_BIN="hyperd"
if [[ "$PLATFORM" == "win32-x64-msvc" ]]; then
EXE=".exe"
HYPERD_BIN="hyperd.exe"
fi
# Copy the .node native addon
node hyperdb-api-node/scripts/ci-copy-artifact.js "$TARGET"
# Copy hyperd + shared libraries
cp "hyperd-extracted/${HYPERD_BIN}" "$DEST/"
find hyperd-extracted -name "*.so*" -o -name "*.dylib" -o -name "*.dll" | while read f; do
cp "$f" "$DEST/"
done
cp LICENSE-HYPERD "$DEST/"
chmod +x "$DEST/${HYPERD_BIN}" 2>/dev/null || true
echo "Contents of $DEST:"
ls -la "$DEST/"
- uses: actions/upload-artifact@v7
with:
name: npm-${{ matrix.platform }}
path: |
hyperdb-mcp/npm/${{ matrix.platform }}/
hyperdb-api-node/npm/${{ matrix.platform }}/
if-no-files-found: error
retention-days: 7
publish-npm:
name: publish to npm
needs: build-npm
if: always() && needs.build-npm.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.tag || github.event.release.tag_name || github.ref }}
- uses: actions/setup-node@v6
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Download all platform artifacts
uses: actions/download-artifact@v8
with:
pattern: npm-*
merge-multiple: true
- name: Restore executable bits on platform binaries
# actions/upload-artifact preserves perms via tarball but
# actions/download-artifact extracts without them. Re-apply
# +x so the binaries are runnable after `npm install`.
run: |
set -euo pipefail
for d in hyperdb-mcp/npm/* hyperdb-api-node/npm/*; do
[[ -d "$d" ]] || continue
for f in "$d"/hyperdb-mcp "$d"/hyperdb-mcp.exe "$d"/hyperd "$d"/hyperd.exe; do
[[ -f "$f" ]] && chmod +x "$f" || true
done
done
- name: Copy README into hyperdb-mcp npm publish directory
run: cp hyperdb-mcp/README.md hyperdb-mcp/npm/README.md
- name: Resolve version and dist-tag from event
id: version
# Source-of-truth: the git tag that triggered the workflow.
# release events carry it in github.event.release.tag_name;
# workflow_dispatch carries it in inputs.tag. Neither path
# reads from package.json, so the platform/umbrella package
# files don't need to track the version in source control.
env:
RAW_VERSION: ${{ github.event.inputs.tag || github.event.release.tag_name }}
run: |
set -euo pipefail
if [[ -z "${RAW_VERSION:-}" ]]; then
echo "::error::No tag/version available; refusing to publish."
exit 1
fi
VERSION="${RAW_VERSION#v}"
# Defense in depth: tags from workflow_dispatch are user-supplied.
# Enforce strict vX.Y.Z / vX.Y.Z-rc.N shape before the value
# flows into npm pkg set / npm publish commands.
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(rc|alpha|beta)\.[0-9]+)?$ ]]; then
echo "::error::Invalid version: $VERSION (expected X.Y.Z or X.Y.Z-rc.N)"
exit 1
fi
case "$VERSION" in
*-rc.*) TAG=rc ;;
*-alpha.*) TAG=alpha ;;
*-beta.*) TAG=beta ;;
*) TAG=latest ;;
esac
{
echo "version=$VERSION"
echo "tag=$TAG"
} >> "$GITHUB_OUTPUT"
echo "Resolved version=$VERSION dist-tag=$TAG"
- name: Verify tag matches workspace Cargo.toml version
env:
VERSION: ${{ steps.version.outputs.version }}
# Defense against a tag that doesn't match what `cargo publish`
# will publish in release.yml. If they diverge, npm and crates.io
# would ship at different versions. Match the same field
# release-please patches: [workspace.package].version.
run: |
set -euo pipefail
CARGO_VERSION=$(awk '
/^\[workspace\.package\]/ { in_section = 1; next }
/^\[/ { in_section = 0 }
in_section && /^version[[:space:]]*=/ {
gsub(/[[:space:]"]/, "", $0)
sub(/^version=/, "", $0)
print
exit
}
' Cargo.toml)
if [[ "$VERSION" != "$CARGO_VERSION" ]]; then
echo "::error::Tag version ($VERSION) does not match Cargo.toml workspace version ($CARGO_VERSION). Refusing to publish to avoid registry skew between npm and crates.io."
exit 1
fi
- name: Inject version into all npm package.json files
env:
VERSION: ${{ steps.version.outputs.version }}
# The platform and umbrella package.json files are stripped of
# `version` and `optionalDependencies` in source. We materialize
# them here from the tag-derived $VERSION just before publish.
#
# PLATFORMS is the SINGLE source of truth for which platforms
# ship. It must match (a) the build-npm matrix above, (b) the
# publish loops below, and (c) the `napi.targets` array in
# hyperdb-api-node/package.json. hyperdb-mcp and hyperdb-api-node
# currently ship the same 3 platforms; if that ever diverges,
# split into MCP_PLATFORMS / NODE_PLATFORMS arrays here AND
# below in the publish steps.
run: |
set -euo pipefail
PLATFORMS=(darwin-arm64 linux-x64-gnu win32-x64-msvc)
for p in "${PLATFORMS[@]}"; do
(cd "hyperdb-mcp/npm/$p" && npm pkg set "version=$VERSION")
(cd "hyperdb-api-node/npm/$p" && npm pkg set "version=$VERSION")
done
# Umbrellas: own version + every optionalDependency exact-pinned
# to ==VERSION so installs resolve to the matching platform tarball.
(
cd hyperdb-mcp/npm
npm pkg set "version=$VERSION"
for p in "${PLATFORMS[@]}"; do
npm pkg set "optionalDependencies.hyperdb-mcp-$p=$VERSION"
done
)
(
cd hyperdb-api-node
npm pkg set "version=$VERSION"
for p in "${PLATFORMS[@]}"; do
npm pkg set "optionalDependencies.hyperdb-api-node-$p=$VERSION"
done
)
- name: Publish hyperdb-mcp platform packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
for dir in hyperdb-mcp/npm/darwin-arm64 hyperdb-mcp/npm/linux-x64-gnu hyperdb-mcp/npm/win32-x64-msvc; do
echo "::group::Publishing $(basename $dir)"
cd "$dir"
npm publish --access public --tag "$NPM_TAG"
cd -
echo "::endgroup::"
done
- name: Publish hyperdb-mcp main package
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TAG: ${{ steps.version.outputs.tag }}
run: |
cd hyperdb-mcp/npm
npm publish --access public --tag "$NPM_TAG"
- name: Publish hyperdb-api-node platform packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
for dir in hyperdb-api-node/npm/darwin-arm64 hyperdb-api-node/npm/linux-x64-gnu hyperdb-api-node/npm/win32-x64-msvc; do
echo "::group::Publishing $(basename $dir)"
cd "$dir"
npm publish --access public --tag "$NPM_TAG"
cd -
echo "::endgroup::"
done
- name: Publish hyperdb-api-node main package
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TAG: ${{ steps.version.outputs.tag }}
# --ignore-scripts skips the prepublishOnly hook (`napi prepublish`),
# which would try to re-orchestrate the platform packages our CI
# has already built and published independently.
run: |
cd hyperdb-api-node
npm publish --access public --tag "$NPM_TAG" --ignore-scripts