Introducing CairnCI: A Modern, Open-Source Salesforce Deployment Pipeline

CairnCI is an open-source CI/CD pipeline for Salesforce DX, designed to automate the validation and deployment of changes. Built on GitHub Actions, it bridges the gap between Salesforce’s native tooling and code-first workflows, providing a community-maintained reference implementation that simplifies CI/CD for teams.

So. Change sets. We need to talk about change sets.

We’ve all been there. You’ve got a sandbox full of beautiful, carefully crafted metadata, a team that’s ready to ship, and then… you’re staring at the Change Set screen clicking individual components like you’re defusing a bomb. Except the bomb isn’t optional, and it definitely doesn’t care about your timeline.

If you’ve spent any time in the Salesforce ecosystem, you know the pain. Salesforce DevOps Center was a real step forward — and I’ve written about it before — but it’s still a Salesforce-native tool with Salesforce-native constraints. For teams that have grown into a more code-first, source-driven workflow, or who have specific governance requirements their org doesn’t bend to meet, there’s a real gap in the tooling available. Especially when it comes to CI/CD.

That gap is what CairnCI is trying to fill.


What is CairnCI?

CairnCI is an open-source CI/CD pipeline for Salesforce DX, built on GitHub Actions. It’s designed to be plugged into a Salesforce DX repository and handle two core things automatically:

  • Validate on every pull request — runs a delta-only, check-only deployment against your org using RunLocalTests, so your team knows before anything merges whether the changes will actually deploy.
  • Deploy on merge — maps branches to environments, reuses the validation job-id from the PR for a quick deploy (because running all your tests again immediately after you just ran them is a special kind of tedious), with an automatic fallback to a full deploy if the validation has expired or isn’t available.

The name comes from the idea of a cairn — those stacked stone trail markers that hikers build to mark the way forward in terrain where the path isn’t obvious. That felt right. Salesforce DevOps can be a lot of terrain.


Why build this?

Honestly? Because I’ve set up enough Salesforce CI/CD pipelines from scratch to know that everyone is solving the same problems in slightly different ways, and most of the solutions are locked inside someone’s private company repo.

The Salesforce ecosystem has a lot of great individual tools — the sf CLI, sfdx-git-delta, GitHub Actions — but there isn’t really a good, community-maintained reference implementation that ties them together in a way that’s ready to adopt. So I built one, and I’m putting it out in the open so other teams can use it, adapt it, and (hopefully!) contribute back to it.

The goal isn’t to be a black box. CairnCI is designed to be extensible and transparent — you can see exactly what it’s doing, pin it to a specific version, and override its behavior through inputs or a committed config file.


Who is it for?

If you’re running a Salesforce DX project with your metadata in source format and you’re using GitHub, CairnCI is probably useful to you. In particular:

Developers and admins who are tired of manual deployments. If you’re still clicking through Change Sets or running sf project deploy by hand every time someone wants to push code, CairnCI automates that entire loop from PR to production.

Teams that want to adopt modern DevOps practices without building everything themselves. You don’t need to write and maintain your own GitHub Actions workflow from scratch. CairnCI gives you a starting point that already handles the tricky bits.

Organizations with compliance or security requirements. There’s a “vendor” adoption path specifically for air-gapped or strict-security environments where every line of executed CI code needs to live in your own repository, reviewed and pinned — no external action references at runtime. This one comes up a lot in government and regulated industries.


The two ways to adopt it

CairnCI supports two adoption paths, and the choice really comes down to how much control you want over the pipeline code:

Path A (Recommended for most teams): Reference a pinned version. Your repo adds a few small caller workflows, and the heavy lifting stays in CairnCI. When CairnCI releases an update, you bump a version tag. Your .cairnci/config.json file controls which features run and how. This is the “lean” option — minimal code to maintain, easy updates.

Path B: Vendor a pinned version into your repo. You copy the pipeline code directly into your own repository. From that point on, CairnCI isn’t an external dependency — everything runs locally from your repo. This is the right choice if your organization’s policy requires that every line of executed CI code is reviewed and committed in-repo. Re-running an update means copying a new tagged version and reviewing the diff in a PR.

Both paths support the same feature set.


The field governance gate (my personal favorite)

One of the things I’m most proud of in CairnCI is the optional field permission-set governance gate. This one takes a little explaining, but stay with me.

When you add a new field to a Salesforce object, it doesn’t automatically become accessible to anyone. Someone has to wire up permission set access, or the field just… sits there, invisible to your users. And in a team environment, it’s surprisingly easy for that to slip through code review. You’re looking at the field definition, you’re checking the Apex tests, and nobody notices that there’s no permission set entry for it.

The field governance gate catches that at the pipeline level. Before a PR can merge, it checks that any new fields in the changeset have the appropriate permission set access configured — and optionally, that they include required governance metadata. If they don’t, the gate blocks the deploy and tells you exactly what’s missing.

It’s the kind of thing that saves a lot of “wait, why can’t users see the new field?” tickets.


What it’s built on

CairnCI is built on tools that are well-maintained and widely used in the Salesforce developer community:

  • The modern sf CLI (not the legacy sfdx) for all Salesforce operations
  • sfdx-git-delta for generating delta packages — only deploying what actually changed, not your entire source directory every time
  • GitHub Actions for the automation layer
  • GitHub Environments for secure org credential management and deployment protection rules

All of the toolchain versions (Node, the SF CLI, sfdx-git-delta) are pinnable, so you’re not at the mercy of whatever latest decides to be on the day your pipeline runs.


Rollback support

One thing that trips teams up: a validation can pass, a merge can happen, and then the real deploy to production can still fail — timeouts, org-state drift, row locking, you name it. Salesforce will automatically roll back the org when a deploy fails, but your git branch still contains the merged change that never actually landed.

CairnCI has optional rollback support to handle this. You can configure it to automatically open a PR that reverts the merged change, or to push the revert directly (if your branch allows it). The deploy job still reports failure so the problem stays visible — no silent pretending everything is fine.


Getting started

The repo is at github.com/Fossiltalk/CairnCI and the docs/consumer-setup.md file walks you through the whole setup. The short version for Path A:

  1. Get an SFDX auth URL for each of your orgs
  2. Store it as a secret in a GitHub Environment
  3. Copy the example caller workflows into your .github/workflows/ directory
  4. (Optional) Add a .cairnci/config.json to control behavior in-repo

A minimal validate workflow is literally six lines. That’s it.


This is version 1 — contributions welcome

CairnCI is community-maintained and open to contributions. There’s a lot of room to grow — more governance gates, expanded rollback options, additional runner support, whatever the community finds most useful. If you run into a bug, have a feature idea, or want to contribute, the repo is open and I’m paying attention to it.

The goal is for this to be the kind of tool that teams actually want to use — not a pipeline they cobble together under deadline pressure and then never touch again because nobody understands how it works. Something maintainable. Something that a human has to deal with.

Because at the end of the day, there’s always a human who has to maintain it. 👀


CairnCI is not affiliated with or endorsed by Salesforce, Inc. “Salesforce,” “Salesforce DX,” and “SFDX” are trademarks of Salesforce, Inc.

Creating Dynamic Clock Emojis to Tell Time

Sometimes, you just want the system to feel a little more alive.

We spend so much time staring at record pages, grids, and compact layouts that they can start to feel static. I wanted to add a small UI touch that would reflect the real world—specifically, a clock emoji that actually updates to match the current time.

It started as a fun experiment to see if I could get a Formula Field to output the correct clock face (🕐 vs 🕠) rounded to the nearest 30 minutes. It turns out, you can, but it requires a little bit of math to handle the rounding and the timezone offsets.

Here is how I built it.

The Logic

Instead of writing a massive IF/ELSE statement for every single time slot, I used a math-based approach.

  1. Convert time to an index: I treat the 12-hour clock as 24 distinct “slots” (12 hours x 2 half-hour increments).
  2. Round the minutes: We add a “buffer” of 15 minutes before dividing by 30. This ensures that 1:14 rounds down to 1:00, but 1:15 rounds up to 1:30.
  3. Map the index: A simple CASE statement swaps the calculated number (0-23) for the correct emoji.

The Formula (Eastern Time)

The tricky part is that Salesforce formulas calculate in UTC (GMT). If you are in Eastern Time (like me), TIMENOW() will be 4 or 5 hours ahead of you.

To fix this, we have to subtract the milliseconds equivalent of 5 hours (18000000) from the current time.

Here is the code you can paste into a Text Formula field:

A Note on Daylight Savings

Because Salesforce formulas don’t automatically handle Daylight Saving Time (DST) inside the TIMENOW() function, this formula relies on a hardcoded offset.

  • Standard Time (Winter): Use 18000000 (5 hours)
  • Daylight Time (Summer): Use 14400000 (4 hours)

Twice a year, when you change your microwave clock, just pop into the Object Manager and swap that number. It’s a small price to pay for a system that smiles back at you!

Why do this?

Functionally, does this change how we close deals? Probably not. But user experience is about more than just clicking buttons. Small pieces of Micro UX like this make the org feel personalized and polished.

Give it a try and let me know what you think!

Generating Random Numbers in Flows and Formulas

Generating Random Numbers Using an Invocable Method

I’ve come across a handful of use-cases where I needed to generate random numbers in flows for things like random cohorting. I’m taking this as an opportunity to continue working on some “Admin Tools” scripts which would have saved me some time over the years.

The Class and Test Class above create an invocable method which can be used inside a flow to generate a random number within a range, and a specified number of decimal places.

Within the flow builder, the invocable looks like this:

You can enter a Lower Bound, which is the smallest number that the invocable will generate.

The Upper Bound is the largest number that the invocable will generate.

Scale is the number of decimal places.

For some examples of how this works:

Example 1:

  • Lower Bound = 1
  • Upper Bound = 10
  • Scale = 0
  • Result = 1, 2, 3, …, 8, 9, 10

Example 2:

  • Lower Bound = 1
  • Upper Bound = 10
  • Scale = 1
  • Result = 1.0, 1.1, 1.2, 1.3, … , 9.8, 9.9, 10.0

Example 3:

  • Lower Bound = 1
  • Upper Bound = 100
  • Scale = -1
  • Result = 10, 20, 30, …, 80, 90, 100

Generating Random Numbers using Formulas

Sometimes using an invocable method is overkill and you just need something that appears random and doesn’t need to be particularly performant. For that, you can use a formula to generate a random number.

Taking inspiration from old random number generators which used nth digits of pi, I’ve written this formula which uses the same principle, and uses the record id and created dates as seeds to ensure uniqueness. Because these fields will exist on ALL records this solution is more portable than other random number generator formulas you might find on google.

If you need to change the number of returned values, you can simply change the number within the outer-most RIGHT() formula.

You’ll notice some ASCII(RIGHT(Id, #)) formulas being added together. This is because we expect these IDs to increment when the record is created, resulting in uniqueness across records, even those that are created in bulk. The ASCII formula converts the first text value in the string to a number. We don’t so much care what the result is, so much as that it translates letter values to numbers (which we can do math on!). The fact that it is case-sensitive is a nice bonus that enhances the “randomness”.

Note: The ASCII formula only processes the first character in the string, we’re using 3 of these in a row, grabbing 1 additional character each time. Since the formula only operates on the first character, it simply acts as though we’re grabbing the last 3 characters and performing some math on their ASCII value.

Deleting Work Items From Salesforce DevOps Center

Deleting Work Items From Salesforce DevOps Center

This past week we needed to delete a stuck work item from Salesforce DevOps Center. I found an answer online in some Salesforce comments, but the solution was either incomplete or dated. I’ve updated the script and added it here to help anyone else in the same situation!

Thank you to Shreyas Pawar for his comment on the Trailblazer Community Forums for his response here, which got me the information I needed to get started:

https://trailhead.salesforce.com/trailblazer-community/feed/0D54V00007T4YZpSAN