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>.zip automatically. If the path looks like a file, the same date+hash suffix is inserted before the extension.

What it does

  1. Reads requirements.txt from the integration directory (if present). If neither uv nor pip is available on the host, this step errors out with a non-zero exit status.

  2. Installs each dependency directly into modules/ — via uv pip install --no-deps --target ./modules -r requirements.txt if uv is on PATH, otherwise pip install --no-deps --target ./modules -r requirements.txt.

  3. Scans modules/ recursively for .so, .pyd, and .dylib files.

    • If any are found, it prints a clear error naming each offending file and exits with a non-zero status.

  4. If neither --zip nor --output is given, prints a “ready to zip” message and exits without producing an archive.

  5. On --zip and/or --output:

    • Builds a ZIP of the integration directory, excluding __pycache__/, *.pyc, *.dist-info, and .DS_Store files.

    • --zip prints the base64-encoded ZIP contents to stdout.

    • --output copies 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_dir points to a path that isn’t there. Check for typos or a missing cd.

  • ERROR: no package installer found — neither uv nor pip is on PATH. Install one of them, or remove requirements.txt if the integration has no third-party dependencies.

  • ERROR: pip install failed — dependency install failed; the installer’s stderr is printed directly below this line. Check requirements.txt for 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 --output must already exist; oaa bundle does not create it for you.

  • Nothing happens after “ready to zip” — this is expected: without --zip or --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.