Packaging & Publishing¶
Node Wire ships as multiple independent PyPI packages (the runtime plus one package per connector) built from a single monorepo. All wheels are binary-only (Cython-compiled .so/.pyd files) — no .py source is included in any published wheel.
Package inventory¶
| PyPI name | Source path | Entry-point key |
|---|---|---|
node-wire-runtime |
src/node_wire_runtime/ |
— (no entry point; this is the runtime) |
node-wire-bindings (not published) |
src/bindings/ (MCP surface) |
— (factory / invoke / mcp_server for MCP images) |
node-wire-toolhive (not published) |
src/node_wire_toolhive/ |
— (credential relay, tenant header, config tools for nw gen-stacklok servers) |
node-wire-fhir-cerner |
src/node_wire_fhir_cerner/ |
fhir_cerner |
node-wire-fhir-epic |
src/node_wire_fhir_epic/ |
fhir_epic |
node-wire-google-drive |
src/node_wire_google_drive/ |
google_drive |
node-wire-http |
src/node_wire_http_generic/ |
http_generic |
node-wire-salesforce |
src/node_wire_salesforce/ |
salesforce |
node-wire-slack |
src/node_wire_slack/ |
slack |
node-wire-smtp |
src/node_wire_smtp/ |
smtp |
node-wire-stripe |
src/node_wire_stripe/ |
stripe |
Each connector's pyproject.toml lives at packages/connectors/<name>/pyproject.toml; the runtime's is at packages/runtime/pyproject.toml; MCP bindings at packages/bindings/pyproject.toml.
Only the runtime, the connectors, and node-wire-connectors ship to PyPI. node-wire-connectors is metadata only (packages/meta); it is not in the table above or in ALL_PACKAGES. node-wire-bindings is built as a
local wheel (scripts/build-packages.sh, nw gen-whl --bindings) for MCP Docker
images and is deliberately absent from the package lists in publish.yml,
github-release.yml, and security-pr.yml. Do not add it to them. The same applies to
node-wire-toolhive, built only for nw gen-stacklok images (packages/toolhive/).
Source of truth: Keep this table in sync with ALL_PACKAGES in scripts/build-packages.sh (which also builds the unpublished bindings wheel); tests/test_docs_lifecycle.py fails unless the names here match ALL_PACKAGES exactly (no missing rows, no extra rows). MCP Docker images are a separate subset — see Docker demo images. http_generic is publishable on PyPI but does not have a standalone MCP container image.
Adding a new publishable connector¶
This is the one checklist for shipping a connector. Write it first (Build a connector), or generate it (nw gen-all, which already does the Tier 1 wiring and the ALL_PACKAGES entry unless you pass --no-wire). Then update these files to ship it on PyPI and, optionally, as a standalone MCP server.
Tier 1 — Runtime (dev, always required)¶
| File / area | Purpose |
|---|---|
src/node_wire_<name>/ |
__init__.py (required, may be empty), schema.py, logic.py (with optional error_map), optional action_spec.py, README.md |
Root pyproject.toml |
[project.entry-points."node_wire.connectors"] for editable dev install. uv run rebuilds the editable install on its next call once pyproject.toml changes. If you run .venv/bin/python or an activated venv directly, run uv sync first or the new entry point is not visible. |
config/connectors.yaml |
enabled, exposed_via, auth: |
sample.env |
Commented placeholders for connector secrets, and the connector id added to the NW_ALLOWED_CONNECTORS line |
| Connector catalog | Add an entry with the same fields as the others |
| Tests | tests/test_<name>.py. At minimum, assert three things: the connector instantiates with its connector_id, nw_action_metas() lists the expected actions, and auto_register() (with NW_ALLOWED_CONNECTORS set) imports your logic module. Example, and see tests/test_connectors_basic.py. Run the suite as in Code quality — Tests. |
auto_register() discovers the connector via the entry point — no factory branch required.
Tier 2 — Publishable PyPI package¶
| File | Purpose |
|---|---|
packages/connectors/<name>/pyproject.toml |
Publishable package metadata, version, entry point — see Tier 2 templates below |
packages/connectors/<name>/setup.py |
Cython/build glue — see Tier 2 templates below |
packages/connectors/<name>/README.md |
The PyPI project page — see README.md template below |
packages/connectors/<name>/LICENSE |
Copy of the repository LICENSE; license-files ships it in the wheel |
scripts/build-packages.sh |
Add path to ALL_PACKAGES |
.github/workflows/publish.yml |
Add to the package dropdown and the allowed set — see CI allowlist updates below |
.github/workflows/github-release.yml |
Add to package_paths list — see CI allowlist updates below |
.github/workflows/security-pr.yml |
Add to matrix package_path — see CI allowlist updates below |
| This doc — Package inventory | Add row |
Root + all package pyproject.toml |
Version bump on release via ./scripts/bump-version.py (nw-mcp-builder excluded) |
CHANGELOG.md |
Release section (scaffolded by the bump script; fill in notes before tagging) |
Tier 2 templates¶
Every connector package uses the same pyproject.toml and README.md layout, so the PyPI pages read
alike. Only the marked fields change from one connector to the next.
pyproject.toml template¶
# SPDX-FileCopyrightText: 2026 AOT Technologies
#
# SPDX-License-Identifier: Apache-2.0
[project]
name = "node-wire-<dist-name>"
version = "1.0.0"
# "Node Wire connector for <Service>: <the actions, in plain words>"
description = "Node Wire connector for <Service>: <what it does>"
readme = "README.md"
requires-python = ">=3.11"
license = "Apache-2.0"
license-files = ["LICENSE"]
authors = [{ name = "AOT Technologies", email = "opensource@aot-technologies.com" }]
maintainers = [{ name = "AOT Technologies", email = "opensource@aot-technologies.com" }]
# Always node-wire, connector, mcp; then the service and its domain.
keywords = ["node-wire", "connector", "mcp", "<service>", "<domain>"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: Cython",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Software Development :: Libraries :: Application Frameworks",
# Plus one or two Topic classifiers for the service's domain, e.g.
# "Topic :: Communications :: Email".
]
dependencies = [
"node-wire-runtime>=1.0.0",
# Add vendor SDK or HTTP client here, e.g.:
# "httpx[http2]>=0.27.0,<0.28.0",
]
[project.entry-points."node_wire.connectors"]
<connector_id> = "node_wire_<name>.logic"
[project.urls]
Homepage = "https://github.com/AOT-Technologies/node-wire"
# The connector's setup guide if it has one, else its connector-reference entry.
Documentation = "https://aot-technologies.github.io/node-wire/connector-reference/#<connector_id>"
Repository = "https://github.com/AOT-Technologies/node-wire"
Issues = "https://github.com/AOT-Technologies/node-wire/issues"
Changelog = "https://github.com/AOT-Technologies/node-wire/blob/main/CHANGELOG.md"
[build-system]
requires = ["setuptools>=69.0.0", "cython>=3.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["../../../src"]
include = ["node_wire_<name>*"]
setup.py template¶
#
# SPDX-FileCopyrightText: 2026 AOT Technologies
# SPDX-License-Identifier: Apache-2.0
#
# packages/connectors/<name>/setup.py
import glob
import os
from Cython.Build import cythonize
from setuptools import setup
from setuptools.command.build_ext import build_ext as _BuildExt
from setuptools.command.build_py import build_py as _BuildPy
class NoPyBuild(_BuildPy):
def find_package_modules(self, package, package_dir):
return []
class ParallelBuildExt(_BuildExt):
"""Compile extension modules with one compiler job per core."""
def finalize_options(self):
super().finalize_options()
if self.parallel is None:
self.parallel = os.cpu_count() or 1
src_root = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../../src/node_wire_<name>")
)
py_files = glob.glob(os.path.join(src_root, "**", "*.py"), recursive=True)
# Guarded so Cython's process pool can re-import this file on macOS and Windows.
if __name__ == "__main__":
setup(
cmdclass={"build_py": NoPyBuild, "build_ext": ParallelBuildExt},
ext_modules=cythonize(
py_files,
nthreads=os.cpu_count() or 1,
compiler_directives={"language_level": "3"},
build_dir="build",
annotate=False,
),
)
README.md template¶
The README is the PyPI project page, so every link is an absolute URL — relative links break on PyPI. Keep the sections in this order and drop an optional one rather than leave it empty.
<!--
SPDX-FileCopyrightText: 2026 AOT Technologies
SPDX-License-Identifier: Apache-2.0
-->
# node-wire-<dist-name>
<One or two sentences: which service, and what an agent or app can do with it.>
Part of [Node Wire](https://github.com/AOT-Technologies/node-wire). Every call runs through the
Node Wire runtime, so it gets input validation, retries with a circuit breaker, scope policy,
a shared error taxonomy, and OpenTelemetry tracing. A Node Wire host exposes each action over REST,
gRPC and MCP.
## Install
```bash
pip install node-wire-<dist-name>
```
This pulls in [`node-wire-runtime`](https://pypi.org/project/node-wire-runtime/). Pre-releases
are published as `X.Y.ZbN`; install one with `pip install --pre node-wire-<dist-name>`. To install
every connector at once, use [`node-wire-connectors`](https://pypi.org/project/node-wire-connectors/).
Wheels are compiled with Cython and contain no Python source. They are built for CPython 3.11,
3.12 and 3.13 on Linux x86_64 (manylinux and musllinux), macOS arm64 and Windows x64.
## Actions
The connector registers as `<connector_id>`. MCP tool names are `<connector_id>_<action>`, with
dots replaced by underscores.
| Action | MCP tool | What it does |
|---|---|---|
| `<action>` | `<connector_id>_<action>` | <one line> |
## Configuration
Add the connector to `config/connectors.yaml`:
```yaml
connectors:
<connector_id>:
enabled: true
exposed_via: ["rest", "grpc", "mcp"]
auth:
provider: <provider>
# ...
```
Secrets are read through the configured secret provider (environment variables by default):
| Variable | Purpose |
|---|---|
| `<VAR>` | <what it holds> |
## Usage
Set `NW_ALLOWED_CONNECTORS=<connector_id>` (comma-separate several). Node Wire loads only the
connectors listed there, and discovers them through the `node_wire.connectors` entry point.
A Node Wire host (the REST, gRPC or MCP server, or `ConnectorFactory`) then builds the connector
from the configuration above. See [Use a connector](https://aot-technologies.github.io/node-wire/connectors/).
## Security notes
<Optional. Egress restrictions, sandboxing, privacy of logged data.>
## Documentation
- [Node Wire on GitHub](https://github.com/AOT-Technologies/node-wire): source, issues and full documentation
- [<Service> setup guide](<url>) <!-- optional -->
- [Connector reference](https://aot-technologies.github.io/node-wire/connector-reference/#<connector_id>)
- [Configuration](https://aot-technologies.github.io/node-wire/configuration/)
- [Error codes](https://aot-technologies.github.io/node-wire/errors/)
- [Changelog](https://github.com/AOT-Technologies/node-wire/blob/main/CHANGELOG.md)
## License
Apache-2.0. See [LICENSE](https://github.com/AOT-Technologies/node-wire/blob/main/LICENSE).
Replace <name> with the connector's snake_case name (e.g. my_service), <dist-name> with the
same name in kebab case (e.g. my-service), and <connector_id> with the entry-point key (same
string used in config/connectors.yaml and NW_ALLOWED_CONNECTORS).
CI allowlist updates¶
Three workflows each hold a hard-coded list of publishable packages. Each workflow file is the
source of truth for its list. Add packages/connectors/<name> to each:
| Workflow | Where the list is | Rule |
|---|---|---|
publish.yml |
on.workflow_dispatch.inputs.package.options and the allowed set in the validate step |
Both must agree. A path missing from allowed fails at validate; a path missing from the dropdown is unreachable. |
github-release.yml |
package_paths in the release-manifest step |
Lists what the release manifest covers. |
security-pr.yml |
strategy.matrix.package_path |
Each entry gets a pip-audit job. |
Never add packages/bindings or packages/toolhive to them. Those are local-only wheels.
Tier 3 — Standalone MCP server (optional)¶
Prerequisite: Tier 2 (the PyPI wheel) must be completed first. The Dockerfile copies pre-built
.whlfiles frompackages/connectors/<name>/dist/; that directory does not exist until you runbash scripts/build-packages.sh packages/connectors/<name>.
Use when you need a dedicated Docker/ToolHive image for a single connector (not required for the combined agents.mcp_entrypoint server). For the wheel → image mapping see local-packages-to-images.md; for an existing connector's entrypoint/Dockerfile, use src/agents/<name>_mcp.py and docker/<name>/Dockerfile as templates.
| File | Purpose |
|---|---|
src/agents/<name>_mcp.py |
Per-connector MCP agent entrypoint |
Root pyproject.toml |
[project.scripts] e.g. nw-<kebab-name> |
docker/<name>/Dockerfile |
Demo MCP image |
scripts/build-mcp-images.sh |
docker build block |
docker-compose.mcp.yml |
Service + NW_ALLOWED_CONNECTORS |
| local-packages-to-images.md | Add a row to the wheel → image table |
| nw-mcp-builder | Add to "Supported connectors" if it also gets a generated host |
Python package build lifecycle¶
Prerequisites: pip install build cython wheel (and a usable python on the host). Run bash scripts/build-packages.sh --help for usage.
Build all packages (default)¶
For a single connector (or runtime) from the orchestrator CLI, see nw gen-whl — it wraps this script with Linux-only as the default.
Default mode builds each package path listed in ALL_PACKAGES in the script (see the Package inventory for the current set): python -m build --wheel on the host, then again inside Docker so you get Linux-tagged wheels suitable for containers. Docker must be installed and the daemon running (unless you use --host-only). After each package, the script scans every produced wheel and fails if any .py file appears inside the archive.
Linux builds use a local-only image nw-wheel-builder:local from docker/wheel-builder/Dockerfile (build-essential + Cython toolchain). The script runs docker build once per invocation; Docker layer cache makes later runs fast when the Dockerfile is unchanged. This image is not published to Docker Hub or any registry.
Host-only or Linux-only¶
# Fast local iteration — host OS wheels only (no Docker)
bash scripts/build-packages.sh --host-only
bash scripts/build-packages.sh --host-only packages/connectors/stripe
# or: uv run nw gen-whl --connector-id stripe --host
# Container / ToolHive wheels only
bash scripts/build-packages.sh --linux-only
bash scripts/build-packages.sh --linux-only packages/runtime
# or: uv run nw gen-whl --connector-id stripe
# uv run nw gen-whl --runtime
--host-only and --linux-only cannot be combined with each other or with --all.
Artifact layout and safe command usage¶
scripts/build-packages.sh writes wheels per package under packages/**/dist/ (there is no single repo-root dist/ output).
Before using wildcard wheel commands, clear old wheel artifacts so commands do not accidentally match stale versions:
Build a single package¶
Optional: broader wheels with cibuildwheel (--all)¶
For additional platform wheels from your current machine (whatever cibuildwheel can target there), install it and use the same script:
uv sync --all-extras --dev # cibuildwheel 4.2.1 is a dev dependency
bash scripts/build-packages.sh --all
bash scripts/build-packages.sh --all packages/runtime
Local --all builds CPython 3.13 (CIBW_BUILD=cp313-*) and skips win32, 32-bit manylinux, and PyPy (CIBW_SKIP=*-win32 *-manylinux_i686 pp*) unless you override those variables. Publish CI (.github/workflows/publish.yml) builds cp311, cp312, and cp313 with cibuildwheel==4.2.1, one job per platform and CPython version, so manylinux and musllinux each get their own skip list. A full Linux, macOS, and Windows set comes from that workflow.
Wheels for a specific image (--cibw-linux, --musllinux)¶
nw gen-stacklok servers run stacklok's DHI Alpine image, which needs musl wheels. The default
--linux-only builder is Debian/glibc, and the DHI image has no Python headers to compile in.
--cibw-linux builds Linux wheels with cibuildwheel in Docker for the CIBW_BUILD selector. It
bind-mounts src/ exactly like publish.yml, and replaces only the matching wheels in dist/.
nw gen-stacklok sets the selector from the generated server's base image, and skips packages
whose sources are unchanged (dist/.nw-source-<target>.sha256). --musllinux is shorthand for
CIBW_BUILD='cp313-musllinux_*'.
These builds compile at -O1 -g0, and keep -fno-strict-overflow -DNDEBUG from CPython's defaults.
Python's own default is -O3 -g. Generated connector modules contain one very large module-init
function, and gcc's higher optimisation levels take minutes on it: slack_web's schema.c took
173s at -O3 and 37s at -O1. The compiled code is almost entirely Python C-API calls, so the
lower level costs little at run time, and dropping debug info roughly halves wheel size. Override
the flags with NW_WHEEL_CFLAGS.
uv sync --all-extras --dev
bash scripts/build-packages.sh --musllinux packages/runtime packages/bindings packages/toolhive
CIBW_BUILD='cp313-manylinux_*' bash scripts/build-packages.sh --cibw-linux packages/runtime
NW_WHEEL_ARCHS="x86_64 aarch64" bash scripts/build-packages.sh --musllinux packages/runtime
The arch defaults to the host (native); other arches run under emulation and are slow.
Inspect wheel contents¶
After building, confirm no source leaks:
unzip -l packages/connectors/stripe/dist/node_wire_stripe-*.whl
# Must show .so/.pyd files only — no .py files
Install from wheels and verify entry points¶
# Install into an active (clean) virtual env
pip install \
packages/runtime/dist/node_wire_runtime-*.whl \
packages/connectors/stripe/dist/node_wire_stripe-*.whl
# Confirm entry points registered
python -c "
from importlib.metadata import entry_points
print(list(entry_points(group='node_wire.connectors')))
"
Verify connector loading¶
python -c "
from node_wire_runtime.connector_registry import auto_register
loaded = auto_register()
print('Loaded:', loaded)
"
Client consumption model¶
pip install node-wire-connectors installs the runtime and every public connector. The meta package has no extras. A subset is the connector packages themselves:
A downstream client can also name the runtime and individual connectors:
At startup, auto_register() discovers all installed connectors via the node_wire.connectors entry-point group — no explicit import list required.
Runtime loading knobs¶
Fail-closed connector loading and related env vars are documented in
configuration.md. Packaging clients must
still set NW_ALLOWED_CONNECTORS and may set NW_CONNECTOR_MODULE_PREFIX
(default node_wire_) when embedding wheels.
connectors.yaml and secrets¶
An installed connector runs only if it is enabled in connectors.yaml and named in
NW_ALLOWED_CONNECTORS. Keys: Connector reference.
Per-connector secrets: catalog. Secret backends:
Configuration — Secrets Management.
src/node_wire_runtime/connectors.yaml.sample is a commented template with every supported field.
Release process (tag-first)¶
Stable releases are tag-driven: create and push the tag first, then publish each package from it. Betas need no tag — publish derives the pre-release version itself.
- Stable (
v1.2.0) — Create Release Tag → GitHub Release → publish withchannel: release - Beta — publish with
channel: betafrom any branch or tag (no tag, no GitHub Release)
Both channels use the same .github/workflows/publish.yml; its channel input
selects the behaviour, and the version always comes from the selected package's
pyproject.toml rather than from a tag name. PyPI treats PEP 440 pre-releases as
pre-releases: pip install node-wire-runtime stays on stable;
pip install --pre node-wire-runtime or an exact pin (for example
node-wire-runtime==1.2.0b1) installs a beta.
Create Release Tag (.github/workflows/create-tag.yml) accepts both
MAJOR.MINOR.PATCH and PEP 440 pre-releases (aN / bN / rcN). It checks
that the tag is free, package versions match, and CHANGELOG.md has the matching
entry, then pushes v…. Stable releases use it with the version from the steps
below; Beta PyPI publish does not use it at all.
Stable release¶
Step 1 — Prepare the release¶
- Bump lockstep package versions with
./scripts/bump-version.py X.Y.Z(rootpyproject.toml, everypackages/**/pyproject.toml, and connectornode-wire-runtime>=…floors). This does not touchnw-mcp-builder/, which is versioned independently. - Fill in the scaffolded
CHANGELOG.mdnotes. Create Release Tag and GitHub Release both require a dated## [X.Y.Z] - YYYY-MM-DDsection and a footer link[X.Y.Z]: …/releases/tag/vX.Y.Z(the bump script scaffolds these when missing). - Merge to
mainand confirm required CI checks are green.
Step 2 — Create the tag¶
Dispatch Create Release Tag in Actions with version set to 1.0.0 (no
leading v). For stable releases, run this before dispatching "GitHub Release"
below.
Manual fallback, if you must create the tag by hand (bypasses the validation above):
Step 3 — Create the GitHub Release¶
Dispatch GitHub Release in Actions with version set to 1.0.0 (no leading v).
Stable versions only — the workflow rejects PEP 440 pre-releases.
Workflow: .github/workflows/github-release.yml — manual workflow_dispatch
after the tag has been pushed.
The workflow:
- Validates all package versions match the tag.
- Verifies
CHANGELOG.mdhas the matching section and release link. - Generates
sbom.json(release-level SBOM). - Creates
release-manifest.txtlisting all publishable package paths (one per entry ingithub-release.yml'spackage_pathslist). - Creates the GitHub Release with changelog notes, SBOM, and manifest attached.
Step 4 — Publish packages to PyPI¶
After the GitHub Release exists, dispatch .github/workflows/publish.yml once per
package (once per entry in the allowed set in that workflow). There is no version
input — the version is read from the selected package's pyproject.toml, so dispatch
from the release tag via the Use workflow from dropdown.
Required inputs:
| Input | Example | Notes |
|---|---|---|
channel |
release |
release publishes the declared version as-is; beta derives the next pre-release |
package |
packages/connectors/stripe |
Dropdown backed by the workflow allowlist |
Prerequisites checked before build (channel: release):
packageis allowlisted.- Package
pyproject.tomldeclares a stableX.Y.Zversion. - That version is not already published on PyPI.
CHANGELOG.mdcontains the matching release section/link.- A GitHub Release exists for
v<version>.
Pipeline steps:
- Matrix-build wheels on Ubuntu, macOS, and Windows via
cibuildwheel(CPython 3.11, 3.12, 3.13) - Post-build gate: verify zero
.pyfiles per wheel; record SHA256 checksums - Merge artifacts;
pip-audit --fail-on HIGHCVE gate - Publish to PyPI via OIDC Trusted Publisher with Sigstore attestations
Note: The release-level SBOM is attached to the GitHub Release (step 3). Package publish produces PyPI Sigstore attestations per wheel; it does not generate a separate SBOM.
PyPI Trusted Publisher: The workflow file is kept as
publish.ymland the workflow name asPublish Node Wire packageso existing PyPI publisher configuration continues to work. Stable and beta both use this same publisher.
If a published release must be withdrawn or replaced, follow release-rollback.md (PyPI yank, corrective patch release, and GitHub tag/release handling).
Beta PyPI publish¶
Beta builds ship to the same PyPI projects as PEP 440 pre-releases. They
never create a GitHub Release (github-release.yml rejects pre-release
versions).
Leave pyproject.toml on the plain X.Y.Z version you are working towards —
publish derives the pre-release suffix itself.
- Confirm the packages declare the target stable version (e.g.
1.2.0) and CI is green. - Dispatch
.github/workflows/publish.ymlfrom that branch or tag, once per allowlisted package, withchannel: beta. The workflow reads what is already on PyPI and publishes the next unused suffix —1.2.0b1, then1.2.0b2, and so on — stamping it into the wheel at build time. No changelog entry, git tag, or GitHub Release is required; pipeline steps are otherwise identical to stable.
Note:
./scripts/bump-version.pystill acceptsaN/bN/rcN, but publish rejects a pre-releaseproject.version. Keep the source tree onX.Y.Zand letchannel: betaderive the suffix.
Install a beta with:
CI publish flow (Trusted Publisher)¶
See Release process (tag-first) above for the full
end-to-end flow (stable and beta). The package publish workflow is
.github/workflows/publish.yml.
Docker demo images¶
The docker/*/Dockerfile images are demonstration templates for running one connector as a
standalone MCP server. They are not production orchestration artefacts. Building them, the wheels
each one needs, and docker-compose.mcp.yml: Local wheels → images.
Pre-PyPI local validation checklist¶
Run these gates before triggering the CI publish workflow (default build-packages.sh is enough; --all is optional for broader local wheels):
-
bash scripts/build-packages.shexits 0 -
unzip -l packages/<pkg>/dist/*.whlshows no.pyfiles - Install wheels into a clean venv; confirm entry points resolve
-
auto_register()loads expected connectors -
pytest tests/test_connector_registry.py tests/test_connectors_basic.pypasses - Wheel SHA256 checksums recorded and match expected values
-
packageandchannelinputs are correct before dispatching, and forchannel: releasethe tag and GitHub Release already exist