How to Bundle an Integration for the OAA Runner Connector¶
This guide explains how to package an OAA Runner integration as a ZIP archive
that the OAA Runner Go connector can extract and execute. It covers how
Python dependencies are resolved at runtime, the pure-Python constraint, and
how to use oaa bundle to automate the process safely.
How dependencies work inside the connector¶
When the OAA Runner connector extracts a ZIP archive and runs an integration,
it adds the modules/ directory inside the archive to sys.path before
importing any logic modules. This means any pure-Python package you place
inside modules/ is importable at runtime — no pip, no network access, no
installer is needed on the connector host.
my-integration/
├── config.yaml
├── modules/
│ ├── my_hooks.py ← your hook code
│ └── humanize/ ← bundled third-party dependency
│ ├── __init__.py
│ └── ...
└── requirements.txt ← used by `oaa bundle` at packaging time
Python’s import system finds humanize/ inside modules/ because that
directory is on sys.path.
The pure-Python constraint¶
Only pure-Python packages can be bundled.
Compiled extensions (.so on Linux/macOS, .pyd on Windows) are built for a
specific OS and CPU architecture. A .so compiled on macOS arm64 will fail to
load — silently or noisily — on Linux x86_64. Because the OAA Runner connector
may run on any platform, compiled extensions cannot be bundled reliably.
Common packages that pull in compiled extensions transitively include
greenlet, cryptography, lxml, and psycopg2. If you need functionality
from one of these, look for a pure-Python alternative (e.g. requests instead
of httpx[h2], PyMySQL instead of mysqlclient).
Using oaa bundle¶
oaa bundle automates the install-and-validate workflow:
# From the integration directory:
oaa bundle
# Or with an explicit path:
oaa bundle --integration_dir ./my-integration
# Produce a ready-to-use base64 ZIP for the connector config:
oaa bundle --zip | pbcopy
# Save a ZIP archive directly to a directory or file path:
oaa bundle --integration_dir ./my-integration --output ./dist
Parameters¶
--integration_dir(default:.) — path to the integration directory.--zip(default:False) — print a base64-encoded ZIP to stdout.--output(default:None) — save a ZIP archive to this path. If the path is an existing directory (or ends in/), the file is named<integration-name>-<YYYYMMDD>-<md5>.zipautomatically. If the path looks like a file, the same date+hash suffix is inserted before the extension.
What it does¶
Reads
requirements.txtfrom the integration directory (if present). If neitheruvnorpipis available on the host, this step errors out with a non-zero exit status.Installs each dependency directly into
modules/— viauv pip install --no-deps --target ./modules -r requirements.txtifuvis onPATH, otherwisepip install --no-deps --target ./modules -r requirements.txt.Scans
modules/recursively for.so,.pyd, and.dylibfiles.If any are found, it prints a clear error naming each offending file and exits with a non-zero status.
If neither
--zipnor--outputis given, prints a “ready to zip” message and exits without producing an archive.On
--zipand/or--output:Builds a ZIP of the integration directory, excluding
__pycache__/,*.pyc,*.dist-info, and.DS_Storefiles.--zipprints the base64-encoded ZIP contents to stdout.--outputcopies the ZIP to the given path (see naming rule above) and prints the saved path.
Example output (validation only, no --zip/--output)¶
No requirements.txt found — skipping dependency install.
Bundle validation passed — no compiled extensions found.
Integration at /path/to/my-integration is ready to zip.
Example output (--output with dependencies installed)¶
Installing dependencies from requirements.txt into modules/ ...
Successfully installed humanize-4.9.0
Bundle validation passed — no compiled extensions found.
ZIP saved to ./my-integration-20260720-9e3f53cc.zip
Example output (compiled extension detected)¶
ERROR: Compiled extension(s) found in modules/:
modules/greenlet/_greenlet.cpython-312-darwin.so
Compiled extensions are platform-specific and cannot be bundled for the
OAA Runner connector.
Only pure-Python packages are supported.
Troubleshooting¶
ERROR: integration directory does not exist—--integration_dirpoints to a path that isn’t there. Check for typos or a missingcd.ERROR: no package installer found— neitheruvnorpipis onPATH. Install one of them, or removerequirements.txtif the integration has no third-party dependencies.ERROR: pip install failed— dependency install failed; the installer’s stderr is printed directly below this line. Checkrequirements.txtfor invalid pins or a package that needs network access you don’t have.ERROR: Compiled extension(s) found in modules/— one or more dependencies pulled in a compiled.so/.pyd/.dylib. See The pure-Python constraint above for how to work around this.ERROR: output directory does not exist— the parent directory of--outputmust already exist;oaa bundledoes not create it for you.Nothing happens after “ready to zip” — this is expected: without
--zipor--output, the command only validates. Pass one of those flags to actually produce an archive.
Manual bundling (without oaa bundle)¶
If you prefer to bundle manually:
# Install a pure-Python dependency into modules/
pip install --no-deps --target ./modules humanize
# Check for compiled extensions (must return nothing)
find ./modules -name '*.so' -o -name '*.pyd' -o -name '*.dylib'
# Create the ZIP
zip -r my-integration.zip my-integration/ \
--exclude "*/__pycache__/*" "*.pyc" "*/.DS_Store"
# Base64-encode for use as integration_zip in the connector config
base64 -i my-integration.zip | tr -d '\n'
End-to-end example: bundling humanize¶
Suppose your integration uses humanize to format dates in a hook:
modules/my_hooks.py
import humanize
from datetime import date
from oaa.hooks.decorators import hook, OAAHookEvent
@hook(event=OAAHookEvent.PRE_PUSH_OAA)
def format_hire_dates(provider=None, **kwargs):
for uid, user in provider.local_users.items():
if user.hire_date:
user.hire_date = humanize.naturaldate(user.hire_date)
return True
requirements.txt
humanize
Bundle it:
cd my-integration
oaa bundle --zip | pbcopy
Paste the clipboard value into your connector config as integration_zip.
ZIP structure expected by the connector¶
The connector expects a ZIP whose top-level directory is the integration name:
my-integration.zip
└── my-integration/
├── config.yaml
├── modules/
│ ├── my_hooks.py
│ └── humanize/
│ └── ...
└── requirements.txt
oaa bundle --zip produces this structure automatically.