Skip to Content
ReferencePluginsPromotion & versioning

Promotion and version pinning: dev -> staging -> live

A Plugin row carries a status from this set ((internal plugsync tooling - see the source repo), PluginStatus): draft, dev, promoting_to_staging, staging, promoting_to_live, live, error.

  • draft: created, no bundle deployed yet (or nothing pushed since creation).
  • dev: plugsync plugin push succeeded and outbox_dispatcher created/updated your per-developer Lambda function, always invoked at $LATEST. See dev-loop.md.
  • staging / live: reached via plugsync plugin promote <id> staging|live (or plugin publish, an alias). Each promotion publishes a new immutable numbered version on the shared, per-org Lambda function — the same function for both targets, just a different version number recorded on the Plugin row (staging_version or live_version).

Nothing in POST /api/plugins/{id}/promote requires the plugin to already be in a particular status before promoting: you can call promote ... live directly from dev without ever promoting to staging first. staging is a real, independently invokable Lambda version, not a mandatory gate.

Promoting

plugsync plugin promote <plugin_id> staging plugsync plugin promote <plugin_id> live # asks for interactive confirmation

POST /api/plugins/{id}/promote {"target": "staging"|"live"} sets the plugin’s status to promoting_to_staging/promoting_to_live and enqueues a deploy_plugin_versioned outbox row. When outbox_dispatcher drains it (_handle_deploy_plugin_versioned in (internal plugsync tooling - see the source repo)):

  1. It ensures the shared org function exists (plugsync-{stage}-plugin-{plugin_id:.8}), pushes your currently-stored bundle as its code, and calls lambda:PublishVersion to cut a new immutable numbered version.
  2. plugin.function_arn is set to that function’s ARN; plugin.live_version or plugin.staging_version is set to the new version number; plugin.status becomes live or staging; plugin.last_deployed_at is stamped.
  3. A propagate_plugin_ref outbox row is enqueued, which rewrites the embedded plugin block in every already-published, active config version that references this plugin by logical_name — so connectors that were already live pick up the new ARN/version without a fresh publish. Connectors where resolution fails (e.g. a live connector referencing a plugin that has never been promoted to live) are skipped with a warning log and retried on the next propagate.

How resolution actually works today

app/services/plugin_resolver.py:resolve_plugin_function has exactly two automatic paths, keyed off Connector.mode (which is only ever "live" or "test", set at connector creation and immutable after):

ConnectorResolves to
mode == "test"The per-developer function at $LATEST (requires connector.created_by_id; raises otherwise).
anything else (i.e. mode == "live")The shared org function at plugin.live_version (raises if the plugin has never been promoted to live).

There is no automatic “staging connector” resolution path. plugin.staging_version is recorded on promotion but nothing in resolve_plugin_function reads it — promoting to staging publishes a real, invokable Lambda version and updates the Plugin row’s bookkeeping, but no connector is routed to it automatically. If you need to exercise a staged build against a specific connector before promoting it to live, use the explicit override below.

Pinning a connector to an explicit build

An explicit per-connector override in connector.settings.plugin_overrides wins over both paths above, unconditionally, regardless of connector mode:

curl -X PATCH "$BASE/api/connectors/$CONNECTOR_ID" -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \ -d '{"settings": {"plugin_overrides": {"my-plugin": {"function_arn": "arn:aws:lambda:eu-central-1:...:function:plugsync-dev-plugin-abcd1234", "version": 3}}}}'

version can be a numbered version (int, e.g. a plugin’s staging_version) or the literal string "$LATEST". This is the only mechanism today for pointing a given connector at a specific plugin build outside the test/$LATEST and live/live_version defaults — for example, running a staging build against one connector before promoting it org-wide to live.

Where the resolved coordinates actually end up

You never author function_arn/version/capabilities on a flow step. You author:

{"id": "generate_doc", "action": "plugin", "plugin": {"logical_name": "compass_doc_generator", "config": {"store_keys": ["$.payload.object.id"]}}}

At publish time (_resolve_and_embed_plugin_refs in (internal plugsync tooling - see the source repo)), the backend looks up the Plugin row by (org_id, logical_name), calls resolve_plugin_function, and rewrites the step’s plugin block into the full embedded form:

{"logical_name": "compass_doc_generator", "function_arn": "arn:aws:lambda:eu-central-1:...:function:plugsync-dev-plugin-abcd1234", "version": 3, "capabilities": {"http": ["generate-doc.example.cloudfunctions.net"], "store": ["read", "write"], "auth": ["doc_generator"]}, "config": {"store_keys": ["$.payload.object.id"]}}

capabilities here always comes from the current Plugin.capabilities row, never from anything authored on the step. This resolved block — not the authored one — is what gets replicated to the DynamoDB manifest the flow_worker reads at runtime: zero PostgreSQL lookups in the hot event path. Publishing fails with 422 if the referenced plugin doesn’t exist in your org, or if resolution itself fails (e.g. a live connector referencing a plugin with no live_version yet).

Last updated on