Deploy pipeline stages
Both deploy project and deploy happy-soup expose the same shape of pipeline, one command per stage. Read Project vs. Happy Soup first if you haven’t — this page assumes you already know which topic you’re using.
The stages, in order
Section titled “The stages, in order”validate— validates the deploy config file and deploy rules file against their JSON schemas before anything else runs. A missing file is a warning, not a failure; a malformed one, or one that violates adeploy-rules.jsonrequirement, fails the command. Run this early in the pipeline (e.g. on every merge request) so a misconfigured deployment fails fast instead of mid-deployment.pre-destructive— runsbin/preDestructive.shif present.install-packaged— installs the packaged dependencies declared insfdx-project.json(and, forproject, the project’s own package). This one is unconditional: it always runs regardless of what’s in the deploy config’sdeploymentsarray.deploy-unpackaged— deploys unpackaged source viabin/unpackagedDeploy.sh. Exists on both topics — aprojectpipeline isn’t limited to installing its package as-is; anything that has to ship as source alongside it runs here too. This is where--test-level/--test-suite/--testsapply for happy-soup.post-destructive— runsbin/postDestructive.shif present.post-deploy— runsbin/postDeploy.shif present.
Project deployments also have a standalone run-apex-tests command, run separately from deploy-unpackaged, since a project’s Apex tests live inside the installed package rather than being triggered inline.
Every bin/*.sh script is optional — a stage with no matching script for a given repo is effectively a no-op for that repo. This is what makes the same simply-cicd pipeline reusable across projects with very different deployment needs: the CLI provides the orchestration (auth, config resolution, per-app selection, progress tracking, resuming); your repo provides the actual deployment logic in shell scripts it owns. See The bin/*.sh stage script contract for exactly how those scripts are invoked — arguments, working directory, and environment.
State lives in files, not in command chaining
Section titled “State lives in files, not in command chaining”There’s no simply-cicd “pipeline runner” command that calls these stages in sequence for you — your CI config (e.g. .gitlab-ci.yml) does that, as separate jobs, typically wired with needs:/artifact-passing so each stage only starts once the previous one succeeds. What ties them together is two files, both read and written by the stage commands themselves:
DEPLOY_PROGRESS.json(--deploy-progress-file, defaultDEPLOY_PROGRESS.json) — one key per stage, holding either thenameof the last app that finished that stage, or"COMPLETE"once every app configured for that stage has run. If a pipeline fails partway through a stage and is retried, that stage automatically resumes one app after the recorded name — it doesn’t repeat the app that already succeeded, and it doesn’t blindly restart from the top. Pass--start-from <name>to override this and force a stage to (re-)start at a specific named app instead of after it — useful for manually re-running just one app that failed without re-running everyone before it.- The deploy config file itself (
--deploy-config-file, project defaultconfig/deploy.json; happy-soup derives it from--source-branch-namewhen not given explicitly) — itsdeploymentsarray is whatdeploy-unpackaged/pre-destructive/post-destructive/post-deployiterate over (see Project vs. Happy Soup for the exact schema).deploy-rules-file(defaultconfig/deploy-rules.json) layers minimum-stage requirements on top, enforced atvalidatetime.
Because state is file-based rather than encoded in command arguments passed between jobs, each stage’s CI job can be a clean, independent step — it just needs read/write access to the repo checkout (for the progress and config files, usually passed as CI artifacts between jobs) and the target org credentials.
A minimal stage job
Section titled “A minimal stage job”simply-cicd never authenticates orgs itself — every stage command just takes --alias, and the org it names must already be authenticated by the pipeline (e.g. sf org login jwt/sf org login web/sf org login sfdx-url, run as its own CI step before the stage job) before the stage runs. Each stage command also needs --ci-job-token to authenticate the read-only git operations it performs (cloning each configured app’s repo, for happy-soup). See the GitLab CI pipeline guide for a full working example wiring these into CI jobs.