Blog
How Do I Version-Control and CI/CD My Customer Integrations?
Dev Tips

How Do I Version-Control and CI/CD My Customer Integrations?

Bring engineering best practices to your integrations. Version control, test, and auto-deploy TypeScript integrations through staging and prod.
Aug 04, 2026
Taylor Reece
Taylor ReeceDeveloper Experience Engineer
How Do I Version-Control and CI/CD My Customer Integrations?

Treating integrations as side projects isn’t efficient or workable long-term. Using code-native TypeScript SDKs and CLI tools, teams can move integration logic out of browser tabs and into Git. This enables standard software engineering best practices: code reviews, automated CI testing, feature branching, and environment promotion. Bringing integrations into your existing pipeline eliminates manual deployment risks and ensures predictable deployment and maintenance for every instance.

Your application code lives in Git. Changes go through a pull request. CI runs your tests. Deploys move through staging before they touch production. And if you ship a bad release, you roll it back like it's no big deal.

So here's a fun question: "Why doesn't your integration code get any of that?"

At many B2B SaaS companies, it doesn't. Integrations are built in a browser tab, saved with a click, and shipped with (yes) another click. There's no branch, no diff, and no CI run. You've got a Publish button and some hope.

That's fine when you have one integration, and you're the only one touching it. It stops being fine somewhere when three people are editing things in parallel and no one's sure what's live in production right now.

The thing is, integrations aren't some lightweight, disposable side project. They talk to third-party APIs, move truckloads of customer data, and need to keep working when the vendor on the other end changes something without telling you. That's production software, full stop. It deserves the same treatment as the rest of your codebase. The good news is, it can get it.

Let's walk through what that looks like in practice, using Prismatic's code-native integrations (built on our @prismatic-io/spectral SDK) and the prism CLI.

The "click Publish and hope" problem

Picture shipping a change to your main product by opening a browser, editing production code right there in the UI, and hitting Publish.

Nobody would sign off on that for application code. But that's business as usual for integrations at many companies, and it causes the problems you'd expect:

  • You've got no history of what changed or why – just "someone edited it at some point."
  • Two people touch the same integration, and one of them loses their changes.
  • "Code review" means eyeballing a screenshot in Slack.
  • Deploys are manual, and every person does it slightly differently.
  • Staging and production end up out of sync, and a customer is the one who notices.
  • Rolling back means trying to remember what it used to look like, not reverting a commit.

None of this is an "integrations problem." It's what happens to any code that lives outside version control. The fix is the same one you already use for everything else: put the source of truth in Git, and run it through a proper pipeline.

First, make it code

For any of this to work, an integration has to exist as plain, readable code – not as JSON tucked away inside a visual builder. With Prismatic's TypeScript SDK, an integration is a TypeScript project. Triggers, inputs, flow steps, custom connector logic – it's all code, sitting in your repo.

Here's a typical layout:

12345678910111213
my-saas-integrations/
├── .github/
│ └── workflows/
│ └── deploy-integration.yml # CI/CD workflow
├── components # Custom connector projects
│ ├── acme
│ └── todoist
├── integrations # Code-native integrations
│ ├── slack
│ └── todoist
└── shared-libs
├── acme # Reusable code for auth, API clients, etc
└── todoist

Once you've got more than a couple of integrations, you'll be glad you set up shared-libs/ – it's where auth helpers and common data-mapping logic go so you're not copy-pasting the same function into fifteen different projects. And if some of your catalog still lives in Prismatic's low-code designer, that's fine too. You can export it as YAML, commit it right next to your TypeScript, and the same pipeline can publish both.'

It doesn't matter if this sits in its own repo or right inside your main app's monorepo. What matters is that Git is the source of truth, not a UI somewhere. And since it's TypeScript, your devs get autocomplete, type checking, and linting in whatever IDEs they already use.

What does the workflow look like?

Let's say you need to add an additional field that LLMs can use when invoking an agentic flow via MCP. Nothing fancy – just a normal change. And it should feel like a normal change:

1234567891011121314151617
main
Create a feature branch
Write and test the change locally
Open a pull request
Someone reviews a diff
CI runs: lint, type-check, tests
Merge → auto-publish to staging
Poke at it in staging
Promote the same version to prod

Branch first, like always

1
git checkout -b feature/add-folder-filtering-to-mcp-tool

Now your change is off in its own corner. Production is untouched, and there's a clear trail starting from your very first commit. You can use prism to push your work-in-progress into a dev tenant and use some real (or mocked) API calls before you even open a PR.

Then open a PR that shows a diff

This is where things get noticeably better than "here's a screenshot of my change." Here's what it looks like when you add an additional parameter an LLM can use when calling an agentic flow:

12345678910111213141516171819202122232425
diff --git a/integrations/dropbox/src/flows/searchAndFetchFiles.ts b/integrations/dropbox/src/flows/searchAndFetchFiles.ts
index 7ccf320..30b20f6 100644
--- a/integrations/dropbox/src/flows/searchAndFetchFiles.ts
+++ b/integrations/dropbox/src/flows/searchAndFetchFiles.ts
@@ -36,6 +36,11 @@ export const searchAndFetchFiles = flow({
description:
"The name of the file to search for in Dropbox. This can be a partial or full file name.",
},
+ folder: {
+ type: "string",
+ description:
+ "The folder path in Dropbox to search for files. If not provided, the root folder will be searched.",
+ },
},
required: ["filename"],
},
@@ -57,6 +62,9 @@ export const searchAndFetchFiles = flow({
"/files/search_v2",
{
query: filename,
+ options: {
+ path: folder || "",
+ },
},
);

A reviewer can see what changed here – and can ask the obvious question ("What happens when folder is not specified, or someone omits a leading /?") before it ships instead of after. It's the same kind of review your product code gets, because, well, it's the same kind of diff.

And, Prismatic tags every published version with source-control metadata. So later, both the Prismatic UI and your IDE can tell you exactly which commit, which PR, and which author produced a given version – which is a great thing to have on hand at 4:35 on a Friday afternoon when something's acting weird.

Let CI do the boring work

Before anything merges, CI should run the same checks any other package in your repo would get – type-checking, linting, and unit tests against mocked payloads:

1234567891011121314151617
import { transformOpportunityToInvoice } from "./transforms";
describe("Salesforce data transformation", () => {
it("applies the discount rate when present in the payload", () => {
const mockPayload = {
AccountId: "acc_98765",
Amount: 12000,
Discount_Applied__c: 0.15,
Id: "opp_12345",
};
const result = transformOpportunityToInvoice(mockPayload);
expect(result.customerId).toBe("acc_98765");
expect(result.discountRate).toBe(0.15);
});
});

GitHub Actions, GitLab CI, CircleCI, Jenkins – it doesn't matter which one you're using. The point is the same: nothing gets deployed until it's proven itself.

Merge it, ship it to staging, then promote it

Prismatic has official GitHub Actions built on top of the prism CLI, so publishing becomes just another job in the same workflow file that's already running your tests. Here's a trimmed-down version:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546
name: Integration CI/CD Pipeline
on:
push:
branches: [main]
tags: ["v*.*.*"]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: npm run lint && npx tsc --noEmit
- run: npm test
publish-staging:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Publish to staging tenant
uses: prismatic-io/integration-publisher@v1
with:
PATH_TO_CNI: ./src/integrations/salesforce-sync
PRISMATIC_URL: ${{ secrets.PRISMATIC_STAGING_URL }}
PRISM_REFRESH_TOKEN: ${{ secrets.PRISMATIC_REFRESH_TOKEN }}
COMMENT: "Automated build from commit ${{ github.sha }}"
promote-production:
needs: publish-staging
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Promote to production tenant(s)
uses: prismatic-io/integration-publisher@v1
with:
PATH_TO_CNI: ./src/integrations/salesforce-sync
PRISMATIC_URL: ${{ secrets.PRISMATIC_PROD_URL }}
PRISM_REFRESH_TOKEN: ${{ secrets.PRISMATIC_PROD_REFRESH_TOKEN }}

Double-check the exact action inputs and CLI flags against current Prismatic docs before you copy-paste this. The pattern is what matters: test, publish to staging, then promote that same artifact to prod on a tag or approval.

Because it's the same commit riding through every stage, the code your reviewer approved is the code in staging, and the version you promote to prod is the version you already validated. Nobody has to squint at two environments and hope they match – Git already knows.

My integration runs for a hundred different customers

Good. This is the one place integrations do differ from a typical app: the same integration definition runs across a bunch of separate customer instances, each with its own credentials and config.

A version-aware pipeline handles that gracefully. When you publish a new, immutable version – it doesn't yank the rug out from under everyone already running the old one. You can roll a new version out to one friendly customer first, see how it goes, and then push it to everyone else.

You can also diff versions across tenants or across time, which is handy when you're trying to figure out why staging behaves differently from prod (or whether your EU and US regions are running the same thing). And if something breaks, rolling back means pinning affected instances to the last known-good version while you sort out the fix – not trying to rebuild a mental snapshot of "how it used to work."

A few things you'll bump into

Once you're past the proof-of-concept stage, a handful of practical wrinkles show up:

  • Customer-specific stuff (credentials, per-customer field mappings, and endpoint URLs) stays out of your repo entirely. That lives in Prismatic's instance configuration. What's in Git is the shared logic; what's tenant-specific stays tenant-specific, which is exactly how you want it.
  • Secrets, obviously, never go in code. CI authenticates to Prismatic with a refresh token stored as a CI secret, and your integrations authenticate to third-party systems using credentials stored in Prismatic's connections – never hardcoded, never committed.
  • Breaking changes deserve the same care you'd give a public API. Favor additive changes when you can, and when something truly has to break, lean on versioning and instance pinning so existing customers aren't forced onto the new behavior before they're ready.

And as your catalog grows, shared libraries start doing a lot of heavy lifting. Most teams that start with a pile of near-duplicate integrations eventually consolidate them to a smaller set of well-parameterized ones, backed by a shared-libs package that handles the common stuff.

Give it a try

  1. Spin up dev, staging, and production tenants (or regions) in Prismatic.
  2. Wire up prism CLI auth and the relevant secrets in your CI system.
  3. Take an existing integration and turn it code-native – or export a low-code one as YAML and commit it.
  4. Write a workflow that tests every PR and publishes to staging on merge to main.
  5. Open a PR, review a diff, merge it, and watch it roll from staging into production.

Integrations, but treated like software

Once your integrations live in Git and move through the same review and promotion process as everything else you ship, they stop being that weird, slightly scary corner of your stack that only one person understands. They become software, the kind that's easier to maintain over time, instead of the kind that you'd rather not ever have to look at again.

And that's about it. Not a new process to learn, just the one you already trust, finally applied to the part of your codebase that's been sitting outside it.

Want to see this running against your own repo? Check out the developer docs or grab time with an integration architect and we'll walk through it together.

Get a Demo

Ready to make your product extensible?

Join teams from Fortune 500s to high-growth startups that turned integrations into a growth driver and made their products the foundation that customers build on.