hardis:project:promotion:create
Description
Command Behavior (Beta)
This feature is in Beta. Promotion branches are switched off by default; their behavior may still change from feedback. Please report any issue or feedback on the sfdx-hardis GitHub repository.
Assembles a promotion branch: a branch carrying only the approved User Stories of a major branch (ex: uat), so they reach the next major branch (ex: preprod) before the rest of the promotion window.
This is the only supported way to create a promotion branch (Beta). The command:
- checks that
enablePromotionBranches: trueis set in the sfdx-hardis configuration; - checks that
allowedPromotionStepsdeclares the steps promotions may run on (ex:- source: uat/target: preprod), and keeps to them: only those source and target branches are offered, and naming another one fails; - requires the git provider connection (GitHub, GitLab, Bitbucket or Azure DevOps) and refuses to run without it: the carried Pull Requests are read from the provider, so the command behaves the same on every platform and every carried Pull Request gets its real title and author. From VS Code, the extension passes its own connection; from a terminal, an agent or CI, set the provider token as an environment variable or in a
.envfile at the repository root; - lists the Pull Requests merged into the source branch and not yet promoted to the target branch, and lets you select the ones to carry (or takes them from
--pull-requests). A Pull Request another promotion branch already carries to the same target is left out, unless--include-already-promotedis passed. WithdetectManualCherryPicks: true, so is a Pull Request that reached the target branch through a cherry-pick made by hand (seehardis:project:promotion:list-candidates); - creates the branch from the target branch, named
promotion/<source>/<target>/<YYYY-MM-DD>-<HHMM>(UTC, ex:promotion/uat/preprod/2026-09-06-1430), with-2,-3... added only when that name is already taken; - cherry-picks the merge commit of each selected Pull Request, oldest first, with
-xso each commit keeps a pointer to its origin; - pushes the branch and creates the Pull Request to the target branch, with a description declaring the carried Pull Requests (
promotionPullRequests), their titles, authors, source branches and tickets.
The deployment jobs then treat the declared Pull Requests as the scope of the promotion Pull Request: their deployment actions run, their Apex test classes are collected, their custom behaviors are inherited.
On a cherry-pick conflict, you choose (or --on-conflict decides) to:
- commit-with-markers, and all the following conflicts (recommended, the first answer offered): same as below, and the command stops asking for the rest of the promotion. A promotion window conflicts on the same files story after story, so the promotion is assembled in one go and every conflict is then solved in a single pass by a coding agent: copy the prompt from the Pull Request description (also saved in
hardis-report/) and paste it to Claude Code, Codex, Copilot... This is a prompt answer only,--on-conflict commit-with-markersalready applies to every conflict; - commit-with-markers: commit the story anyway with its git conflict markers, so the conflicts can be solved later on the branch, by hand or with a coding agent. The Pull Request description lists the files to fix and embeds a ready-to-paste prompt for a coding agent (Claude Code, Codex, Copilot...), also saved as a markdown report in
hardis-report/. The validation job fails until the markers are gone. The git hooks of the project are skipped for that commit only (a pre-commit hook would fail on the markers); the commit that solves them runs the hooks as usual; - solve now: the conflicting files are listed (and opened, when VS Code runs the command), you solve them while the command waits, then confirm. The command checks that no marker is left in those files and commits the story with its resolution (the git hooks of the project run, as for any clean commit). Until then you can still commit with the markers, skip the story or abort. Asked again at the next conflict, and a prompt answer only: nothing can solve a conflict unattended;
- skip: leave the story out, it is listed as such in the Pull Request description;
- take-source (risky, for this conflict or all the following ones): keep the source branch version of the conflicting lines, the rest of each file still merges both sides. Nobody reviews that choice: a change made in the target branch on the same lines (a hotfix, a retrofit) can be lost, and lines of User Stories that are not promoted can come along, and no validation job notices. The Pull Request description lists those files under a warning, and the commit message names them. A file deleted on one side and changed on the other gets the source side too: kept as the story has it, or deleted when the story deleted it, so this answer never asks again;
- abort: stop, the branch is deleted and nothing is pushed.
Technical explanations
- Candidates are the first-parent commits of
origin/<source>since its merge base withorigin/<target>, grouped with their Pull Requests likehardis:work:backpromotedoes (Pull Request numbers read from the merge commit messages and completed by the git provider API, whose connection this command requires). - A first-parent commit that only moves other merges (a major-to-major sync like
integration -> uat, a promotion branch merged into its target) is opened up into the first-parent commits it brought in, so each User Story is a candidate of its own instead of the whole sync window being a single row. - The branch is created with
git checkout -b <name> origin/<target>, commits are applied withgit cherry-pick -x(-m 1for merge commits). - take-source cherry-picks the story again with
git cherry-pick -x -X theirs: "theirs" is the commit being picked (source branch), "ours" the promotion branch (target branch). - A commit holding conflict markers is made with
-c core.hooksPath=<empty folder>, a private folder created for that commit and removed after it:git cherry-pickhas no--no-verify, and this skips the hooks of any hook manager (Husky, pre-commit, lefthook,.git/hooks). - The Pull Request is created through the git provider API (GitHub, GitLab, Azure DevOps, Bitbucket token), or with the
ghCLI on GitHub. The creation is retried a few times: the branch is pushed a fraction of a second before, and a provider that has not indexed the new ref yet answers that the source branch does not exist. Without either, the branch is pushed and the description is saved underhardis-report/to create the Pull Request by hand, and the message names the reason the provider gave. - The date and the time are in UTC. The name is checked against every promotion branch of the step that exists or existed: local and remote branches, remote-tracking refs, the merge commits of the target branch and its Pull Requests merged in the last two days. A name already taken gets
-2,-3..., one more than the highest one found, and the branch creation stops on a name that still exists instead of resuming that branch.
Agent Mode
Supports non-interactive execution with --agent:
sf hardis:project:promotion:create --agent --source-branch uat --pull-requests 482,487,491
In agent mode:
--source-branchand--pull-requestsare required;--target-branchdefaults to the first merge target of the source branch. Outside agent mode,--pull-requestsonly preselects the stories in the prompt (this is how the VS Code extension passes the stories ticked in the DevOps Pipeline), and the user confirms the selection.- Every number of
--pull-requestsmust match a Pull Request merged into the source branch and not yet promoted, otherwise the command fails before touching git. - A cherry-pick conflict undoes the whole promotion (branch deleted, nothing pushed) and fails the command naming the conflicting Pull Request, unless
--on-conflict skip,--on-conflict commit-with-markersor--on-conflict take-source(risky, see above) is passed. - To choose those numbers first, list what can be promoted with
sf hardis:project:promotion:list-candidates --agent --source-branch uat --json: same candidates, nothing created. - The git provider token must be available (ex:
GITHUB_TOKEN), from the environment or from a.envfile at the repository root: agent mode never prompts for the connection, it stops without it.
Learn by doing
The free Salesforce DevOps with sfdx-hardis course runs this command, click by click, on an org of your own:
Parameters
| Name | Type | Description | Default | Required | Options |
|---|---|---|---|---|---|
| agent | boolean | Run in non-interactive mode for agents and automation | |||
| debug -d |
boolean | Activate debug mode (more logs) | |||
| flags-dir | option | Import flag values from a directory. | |||
| include-already-promoted | boolean | Also offer the Pull Requests another promotion branch already carries to the same target branch (left out by default). | |||
| json | boolean | Format output as json. | |||
| on-conflict | option | What to do when a cherry-pick conflicts: skip (leave the story out), commit-with-markers (commit it with its conflict markers, to solve later), take-source (risky: keep the source branch version of the conflicting lines, without review), abort (undo the whole promotion). Prompted if not provided, abort in agent mode. | skip commit-with-markers take-source abort |
||
| pull-requests -p |
option | Comma-separated numbers of the Pull Requests to carry (ex: 482,487). Preselected in the prompt when provided, taken as is in agent mode where the flag is required. | |||
| skip-pull-request | boolean | Push the promotion branch without creating its Pull Request (the description is saved in hardis-report/). | |||
| skipauth | boolean | Skip authentication check when a default username is required | |||
| source-branch -s |
option | Major branch the approved User Stories are merged into (ex: uat). Prompted if not provided, required in agent mode. | |||
| target-branch -t |
option | Major branch the promotion goes to (ex: preprod). Defaults to the first merge target of the source branch. | |||
| websocket | option | Websocket host:port for VsCode SFDX Hardis UI integration |
Examples
$ sf hardis:project:promotion:create
$ sf hardis:project:promotion:create --source-branch uat
$ sf hardis:project:promotion:create --source-branch uat --target-branch preprod --pull-requests 482,487
$ sf hardis:project:promotion:create --agent --source-branch uat --pull-requests 482,487,491
$ sf hardis:project:promotion:create --agent --source-branch uat --pull-requests 482 --skip-pull-request
$ sf hardis:project:promotion:create --agent --source-branch uat --pull-requests 482,487 --on-conflict commit-with-markers