Skip to Content
ReferencePluginsPromotion & versioning

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

A plugin carries a status from this set: draft, dev, promoting_to_staging, staging, promoting_to_live, live, error. plugsync plugin status <plugin_id> and GET /api/plugins/{id} are where you read it.

  • draft: created, no bundle deployed yet (or nothing pushed since creation).
  • dev: plugsync plugin push succeeded and the dev deploy created or updated your per-developer Lambda function, always invoked at $LATEST. See dev-loop.md.

status and function_arn describe the last deploy that landed, not the highest stage the plugin has reached. A push made after a promotion moves status back to dev and points function_arn at the pusher’s per-developer function once its dev deploy lands. live_version and staging_version are kept, and live connectors keep running the promoted build: they resolve to the shared org function at live_version, not to function_arn.

  • 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 (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"} answers 202 Accepted, sets the plugin’s status to promoting_to_staging/promoting_to_live, and does the AWS work asynchronously — usually within a few seconds. That work is:

  1. Ensure the shared org function exists, push your currently-stored bundle as its code, and cut a new immutable numbered Lambda version.
  2. Set function_arn to that function’s ARN, set live_version or staging_version to the new version number, move status to live or staging, and stamp last_deployed_at.
  3. Propagate: the embedded plugin block is rewritten 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 and retried on the next promotion.

Poll plugsync plugin status <plugin_id> until the status leaves promoting_to_*.

How resolution actually works today

Resolution has exactly two automatic paths, keyed off the connector’s mode (only ever "live" or "test", set at connector creation and immutable after):

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

There is no automatic “staging connector” resolution path. staging_version is recorded on promotion but nothing reads it when resolving a connector — promoting to staging publishes a real, invokable Lambda version and updates the plugin’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. Take the ARN from plugsync plugin status <plugin_id> right after a promotion has landed (status staging or live) and before anyone pushes again: function_arn always reflects the last deploy, so after a push it names a per-developer function, which has no numbered versions.

PATCH /api/connectors/{id} replaces the whole settings object, it does not merge into it. Read the current settings first and send them back with plugin_overrides added, or every other connector setting is lost:

curl -s "$BASE/api/connectors/$CONNECTOR_ID" -H "Authorization: Bearer $JWT" \ | jq '{settings: (.settings + {plugin_overrides: {"my-plugin": {"function_arn": "arn:aws:lambda:eu-central-1:...:function:<the plugin function>", "version": 3}}})}' \ > patch.json curl -X PATCH "$BASE/api/connectors/$CONNECTOR_ID" -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \ -d @patch.json

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": "acme_doc_generator", "config": {"store_keys": ["$.payload.object.id"]}}}

At publish time, plugsync looks the plugin up by (org, logical_name), resolves it as above, and rewrites the step’s plugin block into the full embedded form:

{"logical_name": "acme_doc_generator", "function_arn": "arn:aws:lambda:eu-central-1:...:function:<the plugin function>", "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 plugin’s current capabilities, never from anything authored on the step. This resolved block — not the authored one — is what the published manifest carries and what runs, so the coordinates never have to be looked up while an event is in flight. 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).

When a re-publish is (and isn’t) needed

Promoting a plugin does not, by itself, require anyone to touch draft/publish at all: the propagation step above already rewrites the embedded function_arn/version in every already-published, active config version that references the plugin, in place. A live connector picks up a promote to live without a fresh publish.

Two cases are NOT covered by that automatic propagation, and are exactly what a forced re-publish (POST /publish {"force": true}, or plugsync push --publish --republish) is for:

  • A plugin_overrides change on a connector’s own settings. Pinning a connector to an explicit build (see above) only takes effect on that connector’s NEXT publish; nothing re-resolves the active version’s embedded coordinates just because you changed the override.
  • A promote that failed to propagate to one connector (for example, a live connector whose plugin had not been promoted to live yet at the time): it is retried on the next promote of the same plugin, but a re-publish of that one connector applies it sooner.

GET /preview’s resolved_changes list (see draft-publish.md) tells you which of these, if any, currently applies to a connector: it compares the active version’s embedded plugin coordinates against what publishing would resolve right now, so you know whether a forced re-publish would actually change anything before running one.

Last updated on