Foreman›Docs›Salesforce pack›Stages and release paths

Stages and release paths

Salesforce pack · page 5 of 9

QA, UAT and further stages; releasing through sf, your CI, or Gearset; stopping before production.

Foreman 0.11.05 sections

Preview. The Salesforce pack ships in Foreman 0.11.0 as a preview: its release paths pass their own tests, but no story has yet shipped through them on a live org. Treat the Gearset path as experimental.

This page is for whoever sets up how a Salesforce project releases: where a shipped story goes after its pull request merges, which tool deploys it, and where it stops. A stage is one release step with an org and a branch, and a release path is the tool that deploys to it: sf, your own GitHub Actions workflow, or Gearset. The Salesforce pack is a preview: its release paths pass their own tests, but no story has yet shipped through them on a live org, and the Gearset path has not run against a live Gearset pipeline. The keys this page names are explained one by one on The salesforce block.

Stages

salesforce.stages lists 1 to 4 stages in order, each { "name", "org", "branch"? }. A stage name is a lowercase letter followed by up to 15 lowercase letters or digits, because it becomes part of a ship step name (sf-deploy-qa). prod and production are refused as a stage name and as an org alias: production is never a target.

Each stage has an org of its own, an alias from salesforce.orgs that is a sandbox. Only the first stage may use a build org, and then that org must be the only build org and slots must be 1, because the org builds and also takes the stage 1 releases. No stage may sit on an org marked "production": true or on the scratch Dev Hub.

The branch is optional. A stage without one follows the previous stage’s branch, and stage 1 follows the branch pull requests merge into: ship.pr.into, else baseBranch, else main. When stage 1 names its own branch, the pack sets ship.pr.into to it, so pull requests land there. A stage with its own branch needs salesforce.productionBranch set, and the stage branch must not be the production branch.

A typical two-stage layout for Acme, with QA on the branch stories merge into and UAT on its own branch:

"salesforce": {
  "productionBranch": "main",
  "orgs": {
    "acme-dev1": { "orgId": "00DAA0000000001" },
    "acme-qa": { "orgId": "00DAA0000000002" },
    "acme-uat": { "orgId": "00DAA0000000003" }
  },
  "build": ["acme-dev1"],
  "stages": [
    { "name": "qa", "org": "acme-qa", "branch": "qa" },
    { "name": "uat", "org": "acme-uat", "branch": "uat" }
  ],
  "stopAt": "uat"
}

Later stages

The first stage gets the pull request’s merge commit. A later stage with a branch of its own first gets the story branch merged into that branch, then deploys the merge. The story branch is what is merged, never the previous stage’s branch, so the stage gets this story and nothing else waiting upstream. For that the pack turns on ship.merge.keepBranch whenever a later stage has its own branch, so the story branch survives the pull request merge.

The merge is pinned to the commit that was reviewed: the local story branch head, which the push step pushed and the pull request, CI and review saw. It is refused, with nothing merged, when the local story branch is missing or the remote story branch has moved since (a commit pushed after review). A story already on the stage branch is not merged again, so a resumed run gives the same commit. The merge runs with --no-ff in a temporary worktree of the stage branch, never in the project’s own checkout, and is pushed to the stage branch. A conflict is aborted and the failure names the files; you merge it by hand. A later stage that follows another stage’s branch deploys the commit the stage before it produced.

Later stages halt and never roll back, because people share those orgs. Every step of stage 2 onwards is marked no-rollback: once one starts, any failure from it on freezes releases (a halt) and leaves the org as it is, and the halt reason says it was not rolled back. Only stage 1 rolls back, and only on the sf path; see Rollback on stage 1.

Release records

Foreman keeps what each release reached in its state folder (.foreman/ by default):

File What it holds
salesforce/releases/<story id>.json story, to (the story’s commit for this attempt), reached (each stage a deploy step got to, in order, skipped deploys included), first (the deploy to stage 1: stage, org, org ID, branch, merge commit, merge method, job ID, time), rolledBack (null, or the revert, job ID and time) and agents (each agent the story published: stage, org, API name, the version active before, the new version, time)
salesforce/deployed/<stage>.json The last thing Foreman deployed to that stage: stage, org, org ID, job ID, story, commit and time

A new attempt with a different commit starts a fresh record, so it never inherits an old attempt’s stages. A new first deploy clears rolledBack, so a story shipped again after a rollback can be rolled back again. foreman health reads the stage records to say whether each stage still holds Foreman’s last deploy, and foreman sf rollback reads the story record to know what to revert.

Release through sf

"release": "sf" is the default. The pack sets these ship steps, with the stage steps added for each stage up to stopAt:

sf-org-guard, merge-main, sf-delta, push, pr, ci, sf-validate, mergeable-recheck, merge,
sf-deploy-<stage>, sf-verify-<stage>   (for each stage)

In words: Foreman checks every org is an allow-listed sandbox, merges the base branch into the story, works out the story’s delta with sfdx-git-delta, pushes, opens the pull request and waits for its checks (ship.ci.required is empty by default, so CI settles on its own), dry-runs the delta on the first stage’s org before the merge, merges, then deploys the delta to each stage in turn and checks after each deploy that the org’s newest real deploy is Foreman’s. Every deploy runs tests at salesforce.testLevel.deploy (RunLocalTests by default; NoTestRun is never accepted). Each step is described on Salesforce ship steps.

The sf path is the one with a rollback. The pack sets ship.rollback.command to foreman sf rollback "$FOREMAN_STORY" with a time limit of 3600 seconds, since a rollback deploy runs tests and can take 30 minutes. When a step fails after the stage 1 deploy succeeded, Foreman halts releases and rolls stage 1 back: it reverts the merge on the branch, deploys the reverse delta and puts back any agent the story published. A failure in a later stage halts without a rollback. With the watchRollback switch off, no rollback runs at all; see Settings and switches.

A validation that fails is exit code 2 with nothing merged, so the story gets a fix round. A deploy or verify that fails is code 5 and halts. You clear a halt with “I’ve fixed it” on the dashboard or foreman fixed, which runs foreman health first; when you finished a stage by hand, foreman sf fixed <stage> records that so the health check passes. See Release safety for halts in general.

Release through your CI

"release": "ci" keeps the same steps up to the merge and replaces each stage’s deploy and verify with one step, ci-deploy-watch-<stage>. Your repository’s own GitHub Actions workflow deploys the merge to the stage’s org on push, and Foreman watches it: every 20 seconds for up to 30 minutes it reads the Actions runs for the merge commit on the stage branch (push runs only), and every one must end as success, skipped or neutral. Then it reads the org’s deploy requests through the Tooling API, on the same schedule, until it sees a successful deploy that is not check-only, finished after the merge, and was created while those runs ran. A validation or a manual deploy made outside that window does not count.

A stage is watched only when its branch is the one the merge commit landed on. The first stage follows the pull request target, so it always can be. A later stage with its own branch first gets the story branch merged into it (as above), and that merge’s run is watched. A later stage that follows another branch can be watched only if its branch is the one the previous stage used; otherwise the step fails with code 1 before anything runs, saying which branch pull requests merge into.

The ci path sets no rollback. A failure is code 5 and halts; you fix the org or the branch by hand, run foreman sf fixed <stage>, then clear the halt.

foreman doctor reads your workflows for you. Its salesforce workflows check opens every .github/workflows/*.yml and *.yaml as plain text and looks at each workflow whose push trigger runs on the pull request target or a stage branch (a push with no branch filter counts as every branch). In each job’s run: scripts it finds sf and sfdx deploys (project deploy start|validate|quick, force:source:deploy, force:mdapi:deploy), one level into a local reusable workflow too, and reads each deploy’s target: the last -o, --target-org, -u, --u or --targetusername, or the alias of the job’s one org login when the file sets no default org. The check fails when a deploy targets an org not in salesforce.orgs, an org marked "production": true, or a target it cannot read, such as a secret, a variable or a value glued to a short flag. It names only the file and the branches. It does not see deploys inside scripts your workflow calls, inside uses: actions, or push-style commands such as force:source:push, and it cannot tell where a login secret really points. The check runs on every release path, not only ci.

Release through Gearset (unproven)

"release": "gearset" lets Gearset’s pipeline promote each story. This path is built and tested against Gearset’s documented behaviour, but it has not shipped a story through a live Gearset pipeline, so treat it as experimental until a release says otherwise.

Its steps are:

sf-org-guard, merge-main, sf-delta, push, gearset-pr, sf-validate,
gearset-promote-<stage>   (for each stage)

There is no pr, ci, mergeable-recheck or merge. Gearset closes the pull request Foreman opens within seconds and opens its own promotion pull request from gs-pipeline/<story branch>_-_<stage branch> into the stage branch, so gearset-pr opens the story’s pull request into the first stage’s branch, finds Gearset’s promotion pull request by that head branch, and waits for Gearset’s validation, a commit status named Gearset <environment name>, to pass. It polls every 20 seconds for ship.ci.timeoutMinutes (30 by default). A failed validation or the deadline is code 2 with nothing merged. Then sf-validate dry-runs the delta on the first stage’s org as on the other paths.

gearset-promote-<stage> runs once per stage up to stopAt. salesforce.gearset.promote says who merges the promotion pull request:

  • merge (the default): Foreman merges it with gh pr merge <n> --merge --match-head-commit <sha> once Gearset’s validation passes, since Gearset takes merge commits only and owns the gs-pipeline branches. Then it confirms the deploy, every 20 seconds for another ship.ci.timeoutMinutes: a successful deploy request in the stage’s org that is not check-only and finished after the merge, or, asked at most every 3 minutes, the Gearset Reporting API listing the pull request as deployed to that environment. A merged promotion cannot be undone, so a failure after the merge is code 5 and halts; a deploy the Reporting API lists as failed is one. A deploy that ran no Apex tests is a warning. A promotion pull request already merged (a re-run after a crash) is not merged again. Only a pull request GitHub reports as coming from this repository counts, so a fork’s branch of the same name is never merged.
  • owner: you click Promote in Gearset. The step parks the release (no later step runs and nothing halts) for every stage from this one through stopAt, and foreman release watch, which the run calls every 5 minutes, follows each stage in turn: waiting for the promotion pull request, then for its merge, then for the deploy, confirmed in the org or through the Reporting API. See Parked releases.

The Gearset path needs:

Need Why
GEARSET_API_TOKEN in .env.foreman Foreman reads the stage org’s Gearset environment through the Reporting API (api.gearset.com, Gearset’s Teams or Enterprise tier). The token never appears in a message.
salesforce.gearset.pipelineId (optional) Limits the lookup to one pipeline. Unset, every pipeline the token can see is searched.
salesforce.productionBranch Required on this path.
A branch of its own on every stage Gearset promotes environment branches.
Stage orgs that no slot builds in A build org cannot be a Gearset stage.
ship.merge.keepBranch on and ship.pr.mergeMethod merge The pack turns keepBranch on for you; the doctor checks both.

A stage org that Gearset lists as its Production environment is refused before anything is opened or merged. foreman doctor adds the salesforce gearset token, branch, merge method, environments, ci job and ci tests checks on this path, and the Set up tab gets a Gearset step; see Doctor checks.

stopAt and the production branch

salesforce.stopAt names the last stage a story goes to. It must be one of the stage names; the default is the last stage. The pack adds stage steps only for the stages up to and including stopAt, so with stopAt: "qa" in the layout above the UAT org gets nothing. stopAt can never be production, because production is never a stage.

salesforce.productionBranch is the branch that deploys to production outside Foreman. It is optional unless a stage has its own branch or the release is gearset. When set, Foreman refuses, at config check and again at run time, every path that would merge into it:

  • no pull request may target it, through ship.pr.into, baseBranch or the first stage’s branch; sf-org-guard refuses with code 8 before any sf call and freezes releases
  • no stage branch may be it, and the stage merge refuses to merge into it
  • foreman sf rollback refuses a story whose merge went into it, and the Gearset steps refuse a stage on it
  • foreman doctor’s salesforce production branch check also confirms the branch exists on the remote

A project that writes its own ship.steps must run sf-org-guard before sf-validate or any sf-deploy-<stage>; the config check refuses a list that does not. What the guard checks on each org, and how production stays reachable for reads but never for deploys, is on Production stays yours.