Foreman›Docs›Salesforce pack›Salesforce ship steps
Salesforce ship steps
Salesforce pack · page 6 of 9
Org guard, delta and deletes, validate, deploy, verify, Gearset promotion, rollback and the baseline.
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 describes every ship step the Salesforce pack adds, one by one: what it does, which config it reads, and what happens when it fails. Read it when a release stops and you want to know why, or before you write your own ship.steps. Which steps a release path uses, and in what order, is on Stages and release paths; the core steps (merge-main, push, pr, ci, merge) are on How a story ships. The Salesforce pack is a preview: these steps pass their own tests, but no story has yet shipped through them on a live org, and the Gearset steps have not run against a live Gearset pipeline.
Every sf call a step makes runs through Foreman’s isolated sf home, never through your own logins, and none of them ever passes --ignore-conflicts, --ignore-errors, deploy quick or NoTestRun. A failure message names aliases, org IDs, stages, branches, commits, job IDs and metadata members, plus sf’s own error text; never a username or a raw result.
| Step | What it does | Fails with | Halts |
|---|---|---|---|
sf-org-guard |
Checks every org is an allow-listed sandbox and no pull request targets the production branch | 8 | yes |
sf-delta |
Works out the story’s delta and asks you about metadata deletes | 1, 2 or 10 | no |
sf-validate |
Dry-runs the delta on the first stage’s org before the merge | 2 | no |
sf-deploy-<stage> |
Deploys the delta to the stage after the merge | 5 | yes |
sf-verify-<stage> |
Checks the org’s newest deploy is Foreman’s and runs the Agentforce check | 5 | yes |
ci-deploy-watch-<stage> |
Watches your GitHub Actions deploy and confirms it in the org | 1 or 5 | yes |
gearset-pr |
Opens the pull request and follows Gearset’s promotion pull request | 1 or 2 | no |
gearset-promote-<stage> |
Merges the promotion pull request and confirms Gearset deployed it, or parks for your Promote click | 1, 2 or 5 | yes |
A halt freezes releases until you clear it; the exit codes are the ones every release uses, listed on How a story ships.
sf-org-guard
The first step of every Salesforce release: production is never a target. In order, it refuses:
- a pull request into
salesforce.productionBranch, throughship.pr.into,baseBranchor the first stage’s branch. Nosfcall runs before this check. - an isolated
sfhome (FOREMAN_SF_HOME) that is not an absolute path - a
salesforceblock with no orgs - any org in
salesforce.orgsthat cannot be read, has no org ID, or is another org than its configured org ID; a build org or stage org that is not a sandbox; and an org marked"production": truewhose server says it is a sandbox
Each org is queried once, in key order, on the server, so the answer is the org’s own IsSandbox and Id, never a local cache. A marked production org that is not a deploy target may be production: the allow-listed commands may target it, and it may be the scratch Dev Hub. A deploy target marked production is still refused by its server’s answer.
Any refusal is code 8 and a halt: a release line that could reach production stays frozen until you fix the config or the sf home. The reason names only aliases, org IDs and branches, plus sf’s own error message. A project with its own ship.steps must list this step before sf-validate or any sf-deploy-<stage>; the config check refuses a list that does not.
sf-delta and metadata deletes
sf-delta works out what the story changes in Salesforce terms, for every step after it. It runs after merge-main, so the base branch is already merged in.
- It checks the sfdx-git-delta plugin is installed on the 6.45 line (the Foreman runner image pins the same). Another version, or none, is code 1 with the install command:
sf plugins install sfdx-git-delta@6.45.1. - The range is from where the branch left the base (
git merge-base HEAD <remote>/<baseBranch>) toHEAD, both as commits. It never passes--merge-base. sf sgd source delta --generate-deltawrites the delta into.foreman/salesforce/delta/<story id>/, emptied first so nothing from an earlier run is read. The run has 10 minutes.- An
AiEvaluationDefinitionin the package manifest stops with code 2 and one failure per member, so nothing is pushed and the builder gets a fix round: agent test definitions never go in the package folders; the eval spec lives intests/<Api>-eval.json. Unlesssalesforce.agentforceisoff, everyAiAuthoringBundlein the manifest needs that spec atHEAD, in the shape The eval spec describes; a missing or malformed one is code 2 too. - sfdx-git-delta always writes
destructiveChanges/destructiveChanges.xml, empty when nothing is deleted. Flow and RecordType deletes are never deployed: each becomes a manual step for you (“Delete Flow Acme_Intake by hand in each org the story reaches; Foreman never deletes Flows”). The rest are written topostDestructiveChanges.xml, sorted by type then member. - The decision. No deletes, or
salesforce.destructiveset toallow, passes. Withask(the default), the deletes pass only when you allowed them for this exactdestructiveChanges.xml: the approval is bound to its SHA-256, so a change that deletes anything different asks again. Otherwise the step records the question and stops with code 10: nothing pushed, nothing halted.
On code 10 the story session blocks the story and puts a decision on Waiting on you, “Allow deletes for <ID>?”, listing each component as <type> <member>, with two options:
| Option | What it does |
|---|---|
| Allow the deletes | Runs foreman sf deletes allow <ID> and puts the story back to todo. On its next release the deletes go ahead only if the change still deletes exactly the same components; if they differ, you are asked again. |
| Keep them out | Runs foreman sf deletes keep-out <ID> and puts the story back to todo with “Note for the architect: the owner wants nothing deleted in this story”. It is built again to keep those components. |
The record is .foreman/salesforce/deletes/<story id>.json: story, sha256, deletes, askedAt, answer (null, allow or keep-out) and answeredAt. A fresh question replaces it, answer and all. When you asked to keep deletes out and the rebuilt change still deletes something, the step stops with code 10 again, marked keepOut, and the story gets a fix round to keep those components; a second such stop fails the story. The command itself is on foreman sf commands.
The facts the step leaves for later steps are the delta folder, the two commits, the manifest, the post-destructive file, the deletes and the hash, plus manualSteps, which the story note lists.
sf-validate
A dry run of the story’s delta on the first stage’s org, before the point of no return. It runs there even when that org is the build org. It deploys nothing and never halts.
A delta with no components in package.xml and no deletes is skipped with no sf call. Otherwise it runs sf project deploy start --dry-run --manifest <delta> [--post-destructive-changes <file>] --test-level <salesforce.testLevel.validate> --target-org <stage 1 org> --async, then reads the job every 10 seconds for up to 30 minutes. The test level is RunLocalTests by default; RunAllTestsInOrg and RunRelevantTests are the other choices, and NoTestRun is never one. The start itself has 10 minutes.
A failed validation is code 2, the code for a CI failure with nothing merged, so the story gets a fix round; the result lists each problem as a component, test or coverage failure, up to 20, each message cut to 300 characters. A validation that never started or did not finish in 30 minutes is code 2 too, and the reason says so.
A failure only in what the org already failed before this story, as the doctor’s baseline for that org recorded it (tests it lists, plus org-wide coverage it already lacked), is still code 2 but marked inherited, and the reason says it is not the story’s change and suggests RunRelevantTests for salesforce.testLevel.validate, or fixing the org. The story is then held for you rather than sent back to the builder; see The baseline.
This step counts as the one that tests the merged code, so on a release with no ci step merge-main does not run the project’s checks a second time.
sf-deploy and sf-verify
sf-deploy-<stage>
Deploys the story’s delta to one stage’s org, after the merge. On a later stage with its own branch, the story branch is first merged into that branch (see Stages); a merge that fails is code 5 and nothing is deployed. The step marks the stage reached on the story’s release record before anything else, skipped or not.
An empty delta is skipped with no sf call. Otherwise the step runs sf project deploy start --manifest <delta> [--post-destructive-changes <file>] --test-level <salesforce.testLevel.deploy> --target-org <org> --async and reads the job every 10 seconds for up to 30 minutes. The job is recorded before it is first read, in .foreman/salesforce/deploys/<story id>/<stage>.json, so a release that dies and runs again resumes the same job instead of deploying twice: the record must name the same org alias and org ID, the same commit, a real job ID and a valid start time, and the job sf reports must be a real deploy (not a validation) created no more than 15 minutes before the record says it started. Otherwise a new deploy starts.
On success the stage’s last deploy becomes this job, with the stage’s commit; on stage 1 the deploy is also recorded as the story’s first deploy, with the branch the pull request merged into and the merge method, which is what a rollback reverts. Every failure is code 5 and halts. An inherited failure (see The baseline) is code 5 too and still halts, with a reason that says it is not the story’s change. A job still running at the 30-minute deadline keeps its record, but the pull request is merged by then and nothing re-runs the release, so the reason tells you to check it with sf project deploy report --job-id <id> --target-org <org>, finish the deploy by hand if it failed, run foreman sf fixed <stage>, then clear the halt with “I’ve fixed it”.
sf-verify-<stage>
Checks the stage’s org after its deploy. A skipped deploy passes with no sf call. Otherwise the step reads the org’s newest real deploy through the Tooling API (succeeded, not check-only, so dry runs other slots validate there do not count) and fails with code 5 when it is not Foreman’s job: someone else deployed after Foreman, or the deploy is not there.
Then it runs the post-deploy checks that apply to the config. Today there is one, the Agentforce check, which validates, publishes, activates and tests each agent the story changed; the OmniStudio check joins it later. See Agentforce and OmniStudio. A publish is itself a deploy, so once a check changed the org the step reads the newest deploy again and, when that deploy finished no earlier than the check started, moves the stage record to it, so foreman health does not take the publish for a foreign deploy. Notes a check leaves, such as an agent reply the judge scored low, are added to the story’s manual steps.
Every failure is code 5 and halts. The step also gives foreman health its check: it fetches the stage branch and asks whether it is still at the commit Foreman last deployed, and whether the org still has that deploy. On a stage with its own org that is the newest real deploy; on a stage that shares the build org, where the next story’s builder deploys, it is that job’s own status. A failure a fix by hand can clear ends with the command for it, foreman sf fixed <stage>.
ci-deploy-watch
On the ci release path this one step replaces sf-deploy-<stage> and sf-verify-<stage>. Your repository’s GitHub Actions workflow deploys the merge to the stage’s org, and Foreman watches it, after the merge. On a later stage with its own branch, the story branch is first merged into that branch (a failure is code 5) and that merge’s run is watched. Any other stage can be watched only when its branch is the one the merge commit landed on; otherwise the step fails with code 1 before anything runs.
- The commit is the merge into the stage’s own branch, else the merge step’s commit, and its committer date is the merge time.
- The runs:
gh api repos/{owner}/{repo}/actions/runs?head_sha=<sha>&branch=<branch>&event=push, every 20 seconds for up to 30 minutes. Only runs whose commit and branch match count. Every one must end as success, skipped or neutral, and the finished set must look the same on two polls in a row, so a workflow queued after the first finished is watched too. A run that ends any other way fails the step with its name and page. - The org: a Tooling API query of the deploy requests created after the merge, every 20 seconds until the same deadline. A succeeded deploy that is not check-only, finished after the merge, and was created while the runs ran (between the first run’s start and the last run’s update, with one minute of slack for clock drift) confirms the release. A validation never counts, nor does a deploy made outside that window, such as a manual one.
The step only reads: no sf call of its deploys anything. Every failure but the unwatchable stage is code 5 and halts: no run started within 30 minutes, a run did not finish, the runs passed but the org shows no matching deploy, or the deploy did not finish. The ci path has no rollback, so you fix by hand and run foreman sf fixed <stage>.
gearset-pr and gearset-promote
These two steps make up the Gearset release path, which is built but unproven. They are described in full on Release through Gearset; in short:
gearset-prstands in forpr,ci,mergeable-recheckandmerge. It finds the stage whose own branch is the pull request target and that org’s Gearset environment through the Reporting API (GEARSET_API_TOKEN, optionally onesalesforce.gearset.pipelineId), opens the story’s pull request unless one is open, finds the promotion pull request Gearset opens fromgs-pipeline/<story branch>_-_<stage branch>, and polls itsGearset <environment>commit status every 20 seconds forship.ci.timeoutMinutes(30 by default). A missing token, a refused team, no environment for the org, or Gearset’s Production environment is code 1 with no pull request opened. A failed or late validation is code 2. It never merges.gearset-promote-<stage>runs for each stage up tostopAt. Withsalesforce.gearset.promotemerge(the default) it merges the validated promotion pull request with a merge commit and confirms the deploy in the org or through the Reporting API; a failure after the merge is code 5 and halts, since a merged promotion cannot be undone. Withownerit parks the release for your Promote clicks, andforeman release watchfollows each stage. A stage with no org ID, no branch of its own, or on the production branch is code 1 before any merge.
Neither step sets a rollback.
Rollback on stage 1
foreman sf rollback <story id> puts stage 1’s org and branch back the way they were before the story’s merge. On the sf path it is ship.rollback.command, so Foreman runs it when a step fails after sf-deploy-<stage 1> succeeded; the halt stays either way, and the halt reason says whether the rollback worked. You can also run it yourself.
It reverts metadata only, never data, and only on stage 1: the stages after it are shared with people, so a story that reached one is refused and releases stay halted, with the reason “roll <stage> back by hand”. Before any git or sf call it also refuses a release path other than sf, a config with no stage, a story with no first deploy recorded, a story already rolled back, a stage 1 org other than the one the deploy went to (the config changed since), an unknown merge commit, a rebase merge (nothing single to revert), a branch that is the production branch, and an isolated sf home that is not absolute. Then it checks the org on the server: a sandbox, with the org ID salesforce.orgs gives it.
Then, in a temporary worktree of the branch fetched from the remote, never your checkout:
- the merge must still be on the branch;
git revertmakes the revert commit (-m 1for a merge commit) - sfdx-git-delta builds the reverse delta from the merge to its revert, so what the story changed or deleted comes back and what it added goes into the post-destructive changes, except Flows and RecordTypes, which become manual steps
- that delta is deployed to stage 1’s org with
salesforce.testLevel.deployand watched for up to 30 minutes - the agents the story published on stage 1 are put back: each previous version reactivated, or an agent the story created deactivated
- only then is the revert pushed to the branch, the stage’s last deploy set to the revert, and the story marked rolled back
A failure up to and including the deploy leaves the branch and the org as they were. When the org was changed but the revert is not on the branch (the push failed, the deploy outlived the wait, or an agent could not be restored), the stage’s last deploy is still the revert and foreman health fails until you do by hand what the message says: for an agent, its sf command, then git push <remote> <revert>:<branch>. The full result, with the components restored, the deletes and the manual steps, is on foreman sf commands.
The baseline
The baseline is what an org already fails before any story touches it: its local Apex tests and its org-wide coverage. Without it, a story would be blamed for a test the org has failed for months.
foreman doctor records it. Its salesforce baseline <alias> check, for each org with a role (the build orgs and each stage’s org), starts sf apex run test --test-level RunLocalTests --code-coverage without waiting the first time, saves the test run ID, reads the run on later doctor runs until it completes, and then keeps the result until you refresh it. The file is .foreman/salesforce/baseline/<alias>.json: alias, org ID, test run ID, start and finish times, state (running, done or aborted), the failing tests as Class.method (sorted, each once, at most 200) and the coverage as a whole percent. Nothing else is copied from sf. A record for another org ID is ignored, since a refreshed sandbox gets a new one.
Two steps read it. sf-validate and sf-deploy-<stage> call a failure inherited when every failure is a test the baseline lists, or an org-wide coverage warning while the baseline coverage was already under 75%. The doctor’s salesforce test level check warns when salesforce.testLevel.deploy or .validate is RunLocalTests or RunAllTestsInOrg on an org whose baseline cannot pass it, and a stage org with failing tests or coverage under 75% gets the same warning on its baseline check. A build-only org warns only on failing tests, since it takes no deploy with tests; the tester reports those as inherited.
foreman sf baseline prints the saved baselines, --story prints only the ones that count for the current story, and --refresh drops them and starts the tests again; see foreman sf commands.
Was this page useful? Something missing or wrong?Email us and say which section.