Skip to main content

Automate with CI/CD

Once you have verified your app by hand in your Staging sub-account, the next step is to make releases repeatable: build and submit from a pipeline instead of a developer's laptop, and promote the exact code you tested to production.

This page describes a setup that keeps the Staging and production apps completely separate while guaranteeing they contain the same code — and that lets your developers release without ever holding admin rights in your production account.

Put your code in a library, not in the app

Step 4 of the Staging environment flow asks you to create a QA version of your app. Do this by sharing one extension between two thin apps rather than copying the source, so there is only ever one implementation to test and maintain.

my-workspace/
├── libs/app-ext/ ← all the code: components, logic, unit tests
└── apps/
├── myapp-staging/ ← manifest only
└── myapp/ ← manifest only (production)

Each app is a wrapper whose manifest imports the same extension. The package name always differs — it is the app's identity on the platform — and the installation and marketplace blocks differ according to who each app is for.

Your Staging wrapper is always a private app targeting only your Staging account, so you can install it and test without it being visible to anyone else:

apps/myapp-staging/iris-app-manifest.ts
import appExt from '@yourorg/app-ext';

installation: {
accountIds: ['<staging-account-id>'],
pricingPlanPolicy: { ALL_PLANS: 'PREINSTALLED' },
},
marketplace: { showInMarketplace: false, /* ... */ },
extensions: [appExt],

Your production wrapper depends on how the app is distributed. Do not copy the Staging shape onto a Marketplace appshowInMarketplace: false would hide it from the store.

If your app is private to your own accounts, the production wrapper mirrors the Staging one, pointing at your production account instead:

apps/myapp/iris-app-manifest.ts — private app
import appExt from '@yourorg/app-ext';

installation: {
accountIds: ['<production-account-id>'],
pricingPlanPolicy: { ALL_PLANS: 'PREINSTALLED' },
},
marketplace: { showInMarketplace: false, /* ... */ },
extensions: [appExt],

If your app is customer-facing on the Marketplace, keep the production marketplace configuration and install policy you intend to ship — step 9 of the Staging environment flow — and only the package name and the shared extensions import are carried over from the Staging wrapper:

apps/myapp/iris-app-manifest.ts — Marketplace app
import appExt from '@yourorg/app-ext';

// installation and marketplace: your intended production configuration.
// See ../publish-app/marketplace-config.md
marketplace: { showInMarketplace: true, /* ... */ },
extensions: [appExt],

A Marketplace version always goes to manual review — see Review & approval — so plan for that step in your release, rather than expecting the production publish to land immediately.

Because your tests run against libs/app-ext, testing it once covers both published apps.

The package name is the app's identity

The name in each app's package.json becomes the app's identifier on the platform, so the two apps must have different package names. That name also namespaces the app at runtime: extension routes are /fleet/app/<orgId>/<appId>/<extensionId>, so two apps built from the same extension never collide — even when both are installed in the same account.

Give each account its own API key

An app is published to an account, by a key belonging to that account. Create one API key per account, so the credential that can reach production is never the one your developers use day to day.

AccountKey created byKey held byPublishes
StagingStaging account adminYour CI pipeline, and developers@yourorg/myapp-staging
ProductionProduction account adminA protected CI context only@yourorg/myapp

Each key is created under AdministrationAPI Keys with iris.app.submit as its only allowed scope, and is used from a pipeline exactly as described in CI/CD publishing. The App SDK exchanges the Client ID and Secret for a short-lived token itself, so no browser login is involved.

This is the pipeline equivalent of step 3 of the Staging environment flow — "log in as the Staging account before creating the QA app". Publishing with the Staging account's key makes the Staging account the owner of the QA app, exactly as an interactive login would.

Treat the production key as write-only

The Client Secret is shown once, when the key is created. A production admin can paste it straight into your CI system's secret store without anyone else ever reading it. Never commit it — it is easy to leave a .env file next to your app while getting started and forget it is tracked in git. If a secret is ever committed, revoke the key in Manager and issue a new one; rewriting history is not enough.

Promote the commit, not the artifact

There is no "move this build to production" action, and you do not need one. Because the production app is a separate package, it is built separately — so anchor both builds to the same commit and let the source guarantee they match.

A tag-driven pipeline does this well:

TriggerKey usedPublishes to
Merge to mainDev key, if you keep a separate dev accountDev account
Tag staging-v1.4.0Staging keyStaging account
Tag prod-v1.4.0 on the same commitProduction keyProduction account

Two things make this reliable:

  • Tag the commit you tested. Point prod-v1.4.0 at the exact commit that staging-v1.4.0 named, rather than rebuilding from a branch that has moved on. Afterwards git diff staging-v1.4.0 prod-v1.4.0 is empty — a direct answer to "is production running what we tested?"
  • Bump the version every time. A version can never be submitted twice; the submit is rejected if that version already exists. Deriving the app's version from the tag keeps every publish unique and traceable.

Gate the production job on the prod-* tag and on the CI context that holds the production key, then restrict who can push that tag. That, rather than account permissions, becomes your release control.

Review both releases side by side

If your production app is private and your Staging account sits under the account you publish production from, you can target both apps at Staging — add the Staging account to the production app's installation.accountIds alongside your production account. Both releases then appear in the Staging account's menu, so a tester can switch between the current production version and the release candidate without changing accounts.

The two apps stay completely independent: they have different URLs, and each is pinned or unpinned on its own.

For this to publish without waiting for review, all of the following must hold:

  • Staging is a descendant of the publishing account. A Staging account created alongside production under a shared parent is a sibling, not a descendant, and is outside the hierarchy the platform checks.
  • You publish production with the production account's key, since the hierarchy is resolved from the account that publishes.
  • Both manifests keep showInMarketplace: false and use a specific accountIds list rather than "ALL_ACCOUNTS".

If any of those does not hold — most commonly because the production app is a Marketplace app — the publish still succeeds, but that version waits for manual review. See Review & approval.

Tell the two menu entries apart

Both apps are built from the same extension, so they share the extension's menuItem.name and appear under the same label. Leave menuItem.description unset in the shared extension and each entry falls back to its own app's marketplace.description in the Applications menu — a good place to say "release candidate" or "production". Alternatively, set a distinct menuItem.name per app.

Next step

Learn what triggers automatic approval versus manual review in Review & approval.