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
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Only the staged binaries are needed in the build context - without this the
# context drags in bin/ and dist/ (hundreds of MB of zips and bundles).
*
!.oci-stage
124 changes: 124 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,127 @@ jobs:

- name: Smoke test (deterministic MCP handshake)
run: python scripts/smoke-test.py dist/stackql-mcp-windows-x64.mcpb

# OCI image: built from the release zips (which exist as soon as the
# upstream release does, so this runs at PR time), smoke-tested in-container.
# Pushing happens in publish.yml, gated on Docker Hub secrets.
oci:
name: oci (build + smoke)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Build image
run: make oci VERSION=${{ inputs.version }}

- name: Smoke test (deterministic MCP handshake)
run: python3 scripts/smoke-test.py --docker stackql/stackql-mcp:${{ inputs.version }}

# npm wrapper: the real platforms.json pins the sha256 of the PUBLISHED
# .mcpb assets, which do not exist yet at PR time for a new version (this
# repo publishes them at tag time). CI therefore tests the wrapper's
# extract/cache/spawn logic against a locally built bundle via the
# STACKQL_MCP_BUNDLE override, with placeholder pins. The download+verify
# path is exercised by 'make npm-manifest && npm pack' after publish.
npm-wrapper:
name: npm wrapper (smoke)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Build local bundle for the wrapper to consume
run: make one TARGET=linux-x64 VERSION=${{ inputs.version }}

- name: Render placeholder manifest
run: |
zeros="0000000000000000000000000000000000000000000000000000000000000000"
cat > npm/platforms.json <<EOF
{
"version": "${{ inputs.version }}",
"baseUrl": "https://invalid.example/not-used-in-ci",
"platforms": {
"linux-x64": { "bundle": "stackql-mcp-linux-x64.mcpb", "sha256": "$zeros" },
"linux-arm64": { "bundle": "stackql-mcp-linux-arm64.mcpb", "sha256": "$zeros" },
"windows-x64": { "bundle": "stackql-mcp-windows-x64.mcpb", "sha256": "$zeros" },
"darwin-universal": { "bundle": "stackql-mcp-darwin-universal.mcpb", "sha256": "$zeros" }
}
}
EOF

- name: Install wrapper dependencies
run: cd npm && npm install --no-audit --no-fund

- name: Smoke test via wrapper
env:
STACKQL_MCP_BUNDLE: dist/stackql-mcp-linux-x64.mcpb
run: python3 scripts/smoke-test.py --cmd "node npm/bin/stackql-mcp.js"

# PyPI wrapper: same constraints and approach as the npm wrapper - the real
# platforms.json pins published-asset hashes, so CI tests the wrapper logic
# against a locally built bundle via STACKQL_MCP_BUNDLE with placeholder pins.
pypi-wrapper:
name: pypi wrapper (smoke)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Build local bundle for the wrapper to consume
run: make one TARGET=linux-x64 VERSION=${{ inputs.version }}

- name: Render placeholder manifest
run: |
zeros="0000000000000000000000000000000000000000000000000000000000000000"
cat > pypi/src/stackql_mcp_server/platforms.json <<EOF
{
"version": "${{ inputs.version }}",
"baseUrl": "https://invalid.example/not-used-in-ci",
"platforms": {
"linux-x64": { "bundle": "stackql-mcp-linux-x64.mcpb", "sha256": "$zeros" },
"linux-arm64": { "bundle": "stackql-mcp-linux-arm64.mcpb", "sha256": "$zeros" },
"windows-x64": { "bundle": "stackql-mcp-windows-x64.mcpb", "sha256": "$zeros" },
"darwin-universal": { "bundle": "stackql-mcp-darwin-universal.mcpb", "sha256": "$zeros" }
}
}
EOF

- name: Install wrapper into a venv
run: |
python3 -m venv .venv
.venv/bin/pip install --quiet ./pypi

- name: Smoke test via wrapper
env:
STACKQL_MCP_BUNDLE: dist/stackql-mcp-linux-x64.mcpb
run: python3 scripts/smoke-test.py --cmd "$PWD/.venv/bin/stackql-mcp"

# GitHub Action: installs from a locally built bundle (published assets may
# not exist yet for a new version at PR time), then smokes the server using
# the action's own emitted mcp-config.
action-test:
name: setup action (smoke)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Build local bundle for the action to install
run: make one TARGET=linux-x64 VERSION=${{ inputs.version }}

- id: setup
uses: ./action
with:
bundle-path: dist/stackql-mcp-linux-x64.mcpb

- name: Smoke test via emitted config
env:
MCP_CONFIG: ${{ steps.setup.outputs.mcp-config }}
run: |
echo "$MCP_CONFIG" | python3 -m json.tool > /dev/null && echo "mcp-config is valid JSON"
python3 - <<'EOF'
import importlib.util, json, os
cfg = json.loads(os.environ["MCP_CONFIG"])
server = cfg["mcpServers"]["stackql"]
spec = importlib.util.spec_from_file_location("smoke", "scripts/smoke-test.py")
smoke = importlib.util.module_from_spec(spec)
spec.loader.exec_module(smoke)
smoke.run_command([server["command"], *server["args"]])
EOF
67 changes: 67 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,70 @@ jobs:
env:
GH_TOKEN: ${{ secrets.STACKQL_RELEASE_TOKEN }}
run: make publish VERSION=${{ needs.verify-tag.outputs.version }}

# Multi-arch OCI image push. Soft-skips unless DOCKERHUB_USERNAME and
# DOCKERHUB_TOKEN secrets are set (token needs push rights on
# docker.io/stackql/stackql-mcp).
oci-publish:
name: push OCI image (optional)
needs: [verify-tag, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Build and push multi-arch image
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
if [ -z "$DOCKERHUB_USERNAME" ] || [ -z "$DOCKERHUB_TOKEN" ]; then
echo "OCI push skipped (set DOCKERHUB_USERNAME + DOCKERHUB_TOKEN secrets)"
exit 0
fi
echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
docker buildx create --use
make oci-push VERSION=${{ needs.verify-tag.outputs.version }}

# Render the npm manifest from the freshly published .sha256 files and build
# the tarball as a run artifact. Publishing to npmjs stays MANUAL (2FA):
# download the artifact (or run 'make npm-pack' locally), then
# 'cd npm && npm publish --access public'.
npm-tarball:
name: npm tarball (manual publish artifact)
needs: [verify-tag, publish]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Render manifest from published assets and pack
run: make npm-pack VERSION=${{ needs.verify-tag.outputs.version }}

- name: Upload tarball artifact
uses: actions/upload-artifact@v7
with:
name: npm-package
path: npm/*.tgz
if-no-files-found: error

# Render the pypi manifest from the freshly published .sha256 files and
# build sdist + wheel as a run artifact. Publishing to PyPI stays MANUAL
# (2FA / token): download the artifact (or run 'make pypi-build' locally),
# then 'python -m twine upload pypi/dist/*'.
pypi-dist:
name: pypi dist (manual publish artifact)
needs: [verify-tag, publish]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Render manifest from published assets and build
run: |
python3 -m pip install --quiet build
make pypi-build VERSION=${{ needs.verify-tag.outputs.version }}

- name: Upload dist artifact
uses: actions/upload-artifact@v7
with:
name: pypi-package
path: pypi/dist/*
if-no-files-found: error
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@ node_modules/
tmp/
/.stackql
/.tools
/.oci-stage
npm/platforms.json
npm/package-lock.json
npm/*.tgz
pypi/dist/
pypi/src/stackql_mcp_server/platforms.json
pypi/src/*.egg-info/
/.venv*
Loading