Agent skill

Promotion Branches

by hardisgroupcom in hardisgroupcom/sfdx-hardis

How the promotion branches feature works (enablePromotionBranches, sf hardis:project:promotion:create) across sfdx-hardis and vscode-sfdx-hardis, and every file to touch when changing it.

AGPL-3.0Auto-check: notesDevelopment

Install Promotion Branches

skills CLI
$ npx skills add hardisgroupcom/sfdx-hardis --skill promotion-branches -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install hardisgroupcom/sfdx-hardis promotion-branches --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/hardisgroupcom/sfdx-hardis.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/promotion-branches .claude/skills/promotion-branches && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
promotion-branches
GitHub stars
402
Token cost
~8.9k tokens
SKILL.md length
3,918 words
Files
1
Skills in repo
21
Repo updated
First seen
Licence
AGPL-3.0

At a glance

How the promotion branches feature works (enablePromotionBranches, sf hardis:project:promotion:create) across sfdx-hardis and vscode-sfdx-hardis, and every file to touch when changing it.

  • Works in 12 steps: With enablePromotionBranches: false,… → A promotion never runs actions against… → A Pull Request number appears in a… → …
  • Working on promotion branches
  • SKILL.md covers Mental model, Invariants, sfdx-hardis (CLI) and vscode-sfdx-hardis (extension), plus 3 more sections
  • Calls git, sf and node

What it does

Promotion Branches is an agent skill from hardisgroupcom/sfdx-hardis. How the promotion branches feature works (enablePromotionBranches, sf hardis:project:promotion:create) across sfdx-hardis and vscode-sfdx-hardis, and every file to touch when changing it. Use when working on promotion branches, on the Pull Request scope of deploy:smart, on release notes filtering, or on the DevOps Pipeline windows, counters and toggles.

Its SKILL.md is about 8.9k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Development, covering Pull requests and Changelog and release notes. It works with Salesforce and Visual Studio Code. The repository describes itself as: French-army-knife Toolbox for Salesforce. Orchestrates base commands and assist users with interactive wizards to make much more than native Salesforce CLI + Allows you to define…. The licence is AGPL-3.0.

When your agent uses it

  • Working on promotion branches
  • On the Pull Request scope of deploy:smart
  • On release notes filtering
  • On the DevOps Pipeline windows

Example prompts

  • “/promotion-branches”

Workflow steps

12 steps, taken from the first numbered list in SKILL.md.

  1. With enablePromotionBranches: false, deployment behaviour is byte for byte what it was. A
  2. A promotion never runs actions against the wrong org. A promotion/uat/preprod/... branch
  3. A Pull Request number appears in a single place in the pipeline diagram: the branch the
  4. User facing lists show User Stories only. Vehicles are hidden unless the **Show merge and
  5. Nothing is silently dropped. A declared Pull Request that cannot be read is reported, not
  6. Expansion is multi level. A preprod -> main promotion can carry a uat -> preprod
  7. Conflict markers never reach an org, and the reviewer is told why.
  8. A promotion is never closed before its replacement exists. promotion:create closes the
  9. Not knowing is not a reason to act. When the git provider cannot list the open Pull
  10. A vehicle is never carried. The candidate list of promotion:create associates a commit
  11. A branch merged twice is listed once. shouldAddVirtualPullRequest refuses the number-less
  12. Superseding gives the stories back. A promotion this run is about to close is not evidence

What it can do on your machine

Read from SKILL.md and the folder at commit 33d4de0. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • git
    • sf
    • node

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Links to these hosts (documentation or services it may open):

    • sfdx-hardis.cloudity.com

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Promotion Branches loads about 8.9k tokens when it runs. Until then it costs about 94 tokens; SKILL.md has 3,918 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~94
When it runs · the whole SKILL.md, loaded when a task matches
~8.9k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check: notes

The automated check noted patterns worth knowing about, such as sudo or a known installer.

  • NoteMentions a .env fileSKILL.md:173
    from the environment or a `.env` file against the project path of `remote.origin.url`, and

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from hardisgroupcom/sfdx-hardis at commit 33d4de0, republished under its AGPL-3.0 licence (© hardisgroupcom). 3,918 words, ~8,852 tokens.

Download SKILL.mdSave it as .claude/skills/promotion-branches/SKILL.md (or your agent's skills folder).
name
promotion-branches
description
How the promotion branches feature works (enablePromotionBranches, sf hardis:project:promotion:create) across sfdx-hardis and vscode-sfdx-hardis, and every file to touch when changing it. Use when working on promotion branches, on the Pull Request scope of deploy:smart, on release notes filtering, or on the DevOps Pipeline windows, counters and toggles.
user-invocable
false

Promotion branches (Beta)

A promotion branch carries a chosen subset of the User Stories waiting in a major branch to the next major branch, ahead of the rest of the promotion window. It is a selective merge: instead of merging uat into preprod and shipping everything, you assemble a branch holding only the approved stories.

Feature switch: enablePromotionBranches in config/.sfdx-hardis.yml, default false. Required with it: allowedPromotionSteps, the source and target branches a release manager may assemble a promotion between (- source: uat / target: preprod). promotion:create refuses to run while the list is missing. Everything below is inert while it is off, with one exception noted in Filtering. The feature is in Beta and must be labelled as such in docs and UIs, the way backpromote is.

User documentation: docs/salesforce-devops-promotion-branches.md (https://sfdx-hardis.cloudity.com/salesforce-devops-promotion-branches/). End to end test runbook: the promotion-branches-e2e skill.

Mental model

  • Promotion branch = promotion/<source>/<target>/<YYYY-MM-DD>-<HHMM>, for example promotion/uat/preprod/2026-09-06-1430 (UTC minute, -2, -3... only when that name is taken). The shape is fixed, not configurable. Only sf hardis:project:promotion:create builds one, with git cherry-pick -x (-m 1 on merge commits).

  • Declaration = the promotion Pull Request lists what it carries in a ```yaml block of its description:

    yaml
    promotionPullRequests: [482, 487]

    This is the only link back to the stories: a cherry-pick rewrites the commit SHAs, so SHA matching cannot find them. A branch named promotion/... without the block, or a branch with the block but not named promotion/..., is an ordinary branch and gets a warning.

  • Carried story = a Pull Request named in that block. sfdx-hardis pulls it into the scope of the job so its deployment actions run, its Apex test classes are selected and its custom behaviors are inherited, exactly as if it had been merged directly.

  • Vehicle = a Pull Request that moves other Pull Requests rather than carrying work of its own: a merge between two major branches (always), and a promotion Pull Request (only when the feature is on). Everything else, whatever it is named (feature/, fix/, retrofit/, hotfix/...), carries its own change and is a User Story.

  • Single place = a Pull Request number appears in exactly one branch of the DevOps Pipeline, the branch it actually reached. A story a promotion carried away is listed in the target branch, not in the one it was merged into.

  • One promotion in flight per step = at most one open promotion Pull Request between two given major branches. promotion:create closes the ones it supersedes (after asking a human, silently in agent mode), and the DevOps Pipeline draws the open one on the arrow between the two branch nodes instead of giving it a branch node of its own.

Invariants

Break one of these and the feature is wrong, whatever the tests say.

  1. With enablePromotionBranches: false, deployment behaviour is byte for byte what it was. A promotion/... branch is an ordinary feature branch. The only allowed difference is one informational log line. This is proven by the A/B regression check in the promotion-branches-e2e skill.
  2. A promotion never runs actions against the wrong org. A promotion/uat/preprod/... branch whose Pull Request targets main is retargeted by hand: its declaration is ignored (isPromotionPullRequestForItsTarget), scope is the Pull Request alone, with a warning.
  3. A Pull Request number appears in a single place in the pipeline diagram: the branch the promotion carried it to (promotedAway hides it from the branch it left).
  4. User facing lists show User Stories only. Vehicles are hidden unless the Show merge and promotion Pull Requests toggle is on (diagram and modal), or --include-promotions is passed (release notes).
  5. Nothing is silently dropped. A declared Pull Request that cannot be read is reported, not skipped in silence; a promotion whose stories could not be resolved stays in the release notes.
  6. Expansion is multi level. A preprod -> main promotion can carry a uat -> preprod promotion, which carries stories. Both levels must resolve.
  7. Conflict markers never reach an org, and the reviewer is told why. assertNoPromotionConflictMarkers greps every tracked file, not only the package directories, except the git glob patterns of promotionConflictMarkersIgnoredFiles (files that hold markers on purpose, like the merge conflict lab of the training). It reads the checked out files only, never the history, so a shallow CI clone is enough. It stops the job before anything is deployed, so no other code would ever post a Pull Request comment: it sets deployErrorsMarkdownBody / status: 'invalid' on the Pull Request data and calls GitProvider.managePostPullRequestComment(checkOnly) before throwing. A red job with no comment on the very Pull Request that has to be fixed is not an acceptable outcome.
  8. A promotion is never closed before its replacement exists. promotion:create closes the superseded Pull Requests only after the new one has been created, so a failure while cherry-picking cannot leave a pipeline step with no promotion open.
  9. Not knowing is not a reason to act. When the git provider cannot list the open Pull Requests, nothing is closed and the command says so.
  10. A vehicle is never carried. The candidate list of promotion:create associates a commit with the Pull Requests of the commits it brought in, and a sync merge from a major branch matches that major branch's own Pull Request. dropVehiclePullRequests removes them (and their deployment actions) before anything is offered, declared or cherry-picked.
  11. A branch merged twice is listed once. shouldAddVirtualPullRequest refuses the number-less entry when a real Pull Request of the same branch is already in the group.
  12. Superseding gives the stories back. A promotion this run is about to close is not evidence that its stories are already promoted (countsAsAlreadyPromoted): otherwise agreeing to supersede it would leave nothing to assemble.
  13. A vehicle is expanded before it is dropped. A promotion merged into the source branch arrives in the next branch as one cherry-picked merge commit that names the promotion, not the stories under it: the -x trailers only survive one level. expandPromotionsInGroups replaces it with what its promotionPullRequests block declares, recursively, before invariant 10 removes it. Without that, a candidate two levels down carries no number, cannot be selected, and its stories are stranded one branch short of production.
  14. Which commits a merge brought in is a question about the graph, never about the dates. A cherry-picked commit keeps the author date it had on the branch it came from, so it is older than the merge before it. attributeCommitsToFirstParents walks git rev-list --parents, stopping at the other first-parent commits, oldest merge first. Attributing by date puts the stories of a promotion under the wrong merge, and a promotion assembled from that grouping declares stories whose metadata it does not carry.
  15. Every yaml block of a description counts, and a list adds up. mergePrDescriptionYamlBlocks concatenates list values across blocks (without duplicates) and keeps the last value for anything else, so appending a block to name one more Apex test class does not drop the ones declared above it.
  16. Nobody promotes on a step the project never declared. allowedPromotionSteps is required as soon as enablePromotionBranches is on: promotion:create stops with promotionCreateAllowedStepsRequired while the list is missing, and with promotionCreateAllowedStepsInvalid when it is there but unreadable. Guessing "every major branch to every merge target" would be a decision the command has no business making. The pure helpers still treat an empty list as no restriction, which is what keeps the deployment jobs and the pipeline diagram working on a project that is mid-configuration. The rule gates creation, never deployment: a promotion assembled outside the list is deployed with a warning, since refusing it would block a branch that is already merged.
  17. A vehicle is opened up, never offered whole. A first-parent commit of the source branch that only moves other merges (integration -> uat, a promotion merged into its target) is replaced by the first-parent commits it brought in (splitVehicleMerges, called from listMergedPrsWithCommits only when promotion:create / list-candidates asks for it). On a pipeline where stories are merged into integration, every first-parent commit of uat is one of those syncs: without this, the whole promotion window is a single selectable row and carrying one story carries them all. Two guards keep it honest: only a merge with exactly two parents is opened up (an octopus merge would lose every side but the second), and only when everything it brought in is inside the window being listed, which keeps a back-merge from the target branch whole instead of turning it into a page of rows already delivered. The vehicle stays a boundary of attributeCommitsToFirstParents (extraBoundaries) so the merge after it does not swallow it.
  18. A configuration file can stop being readable mid-command. promotion:create commits git conflict markers on purpose, and config/.sfdx-hardis.yml is a file like any other: loadFromConfigFile keeps the last configuration it read from each set of files and falls back to it with a warning (configFileUnreadableUsingPrevious, configFileConflictMarkers) rather than crashing halfway, with a branch assembled and no Pull Request. It only falls back to something it actually read: a project whose configuration is broken from the start still stops.
  19. A conflict answer can be given once for the whole promotion. The prompt of cherryPickCandidates offers commit-with-markers-all next to the three PromotionConflictChoice values: it commits the markers like commit-with-markers and fills rememberedChoice, so the rest of the window is handled without asking again (promotionCreateConflictRemembered says so in the log). It is a prompt answer, not a flag value: PROMOTION_CONFLICT_CHOICES stays skip / commit-with-markers / abort, because --on-conflict already applies to every conflict. A promotion window usually conflicts on the same files story after story, so answering ten times in a row is answering once.
  20. A resolution nobody can read is not a resolution. buildConflictResolutionPrompt asks the coding agent for a commit message whose body carries one line per conflicting file, naming the story, what the target side had, what the story added and what was kept, and forbids "solved conflicts" / "merged both versions". The reviewer of the promotion Pull Request reads the resolutions from git log, without opening the diff.
  21. A branch that was just pushed may not be visible to the provider yet. GitProvider.createPullRequest takes { retries, retryDelayMs } and pushAndCreatePromotionPullRequest passes 3 retries: GitLab answers {"source_branch":["does not exist"]} on a ref its API has not indexed, a fraction of a second after the push that created it. Each attempt starts with findOpenPullRequest, so a create that succeeded while reporting an error is picked up instead of being created twice, and GitProvider.lastPullRequestCreationError carries the reason into promotionCreatePullRequestManual: the fallback message names what the provider said instead of guessing at a missing token. When it still fails, GitProvider.getPullRequestCreateUrl hands the user the provider's own "new Pull Request" form with the source branch, the target branch, the title and the description already filled in. Each provider class builds its own URL (GithubProvider.getPullRequestCreateUrl and the three others, static, from the git remote, so a project with no token gets one too); buildPrCreateUrl drops the description and keeps the link when the URL would go past MAX_PR_CREATE_URL_LENGTH, and the caller then says where to paste it from. Retyping a promotion description is not an option: the promotionPullRequests block is what the deployment jobs read.
  22. The git remote is the authority on which project the API talks to. Outside a GitLab CI job (!process.env.GITLAB_CI), GitlabProvider.autoDetectSettings checks a CI_PROJECT_ID coming from the environment or a .env file against the project path of remote.origin.url, and replaces it with a warning (gitlabProjectIdMismatch) when they disagree. A leftover CI_PROJECT_ID from another repository makes every call answer about that other project, which looks exactly like a branch that does not exist.
  23. A promotion branch is validated, never deployed. It is the source branch of a Pull Request, like a feature branch: the only job it may run is that Pull Request's validation. A deployment job triggered by the push that created it would send the promotion to the target org before anyone reviewed or merged it. assertPromotionBranchIsNotDeployed stops deploy:smart at the top of run() and names the CI setting to fix, because the answer is to fix the trigger, not to let the job continue. Silent when the feature is off, on a validation job, and with SFDX_HARDIS_DEPLOY_BEFORE_MERGE, where a deployment legitimately runs from the source branch. It raises an error and posts no Pull Request comment: the job that trips it is the branch pipeline of the push, not the validation, and failing the Pull Request over it would say the promotion is broken when it is the pipeline configuration that is.
  24. A promotion branch always runs two pipelines, and only one of them is its Pull Request's. It is pushed to the server, so it gets a branch pipeline of its own next to the Pull Request validation pipeline, on the same commit. In vscode-sfdx-hardis, GitProviderGitlab.pickMergeRequestPipeline reads the newest merge_request_event pipeline (the newest of all of them when the project runs none), which is how GitLab picks the head_pipeline its own merge request page shows; reporting every pipeline of the commit drew a green merge request red on the DevOps Pipeline diagram. GitLab is the only provider concerned: GitHub already asks for event: "pull_request" runs, Azure DevOps and Bitbucket read builds and statuses attached to the Pull Request. Deployment status is a different question, answered by getJobsForBranchLatestCommit, which leaves the merge request pipelines out. Related project-side trap: an unanchored DEPLOY_BRANCHES regex (/(integration|uat|preprod|main)/ instead of /^(...)$/) matches promotion/integration/uat/..., so every promotion branch push starts the deployment job and fails it. The sfdx-hardis default template is anchored.
  25. A User Story is offered once, whatever brought it in. A story a promotion cherry-picked into a branch and an ordinary sync merge delivered again afterwards sits in the window twice, as two commits: dropOfferedTwice keeps the first candidate row, which is the one the command already cherry-picks, so the table a release manager reads never lists the same number twice. Only an exact repeat of the same set of numbers is dropped: a row grouping several Pull Requests (a back-merge, an octopus merge) never hides the finer rows of the stories it holds, and a row with no number is a commit of its own and always stays.
  26. Every provider writes its own merge sentence. extractPrNumbersFromMessage and mergedSourceBranches read the Pull Request number and the merged branch out of a merge commit message, and the shapes differ: Merge pull request #N from owner/branch (GitHub), See merge request group/repo!N (GitLab), Merged PR N: (Azure DevOps squash) and Merge pull request N from source into target (Azure DevOps completing without fast-forward, which is what keeps the -x trailers). Miss one and every commit a promotion cherry-picked comes back as a row with no number, so the story it carried can no longer be selected.
  27. A description a provider refuses is a promotion nobody can review. Azure DevOps caps a description at 4000 characters and a promotion whose cherry-picks conflicted embeds a coding agent prompt that goes past it: buildPromotionPullRequestBody takes the provider's getMaxPullRequestDescriptionLength() and drops the embedded prompt (saved in hardis-report/ either way) rather than letting the creation fail with the branch already pushed. The yaml declaration, the carried table and the conflicting file list always survive.
  28. A promotion branch name is never handed out twice. buildPromotionBranchName stamps the UTC date and minute (<YYYY-MM-DD>-<HHMM>, UTC so two machines in different time zones compute the same name), and computePromotionBranchName adds -2, -3... only when that name is taken, one past the highest counter found, never back into a gap. "Taken" is read from everything that can still carry a name after the branch is gone: listExistingPromotionBranchNames collects local branches, git ls-remote, stale remote-tracking refs, the merge commits of the target branch (extractPromotionBranchNames, the merge sentence of all four providers) and the promotion Pull Requests merged into it in the last two days (rebase, fast-forward and Azure squash merges leave no merge commit naming the branch). createPromotionBranch passes refuseExisting, so a name that still exists stops the command instead of resuming a branch already merged. parsePromotionBranchName (and its mirror in the extension, plus the fallback regex of pipeline.js) still accepts the <YYYY-MM-DD>-<counter> names of the first releases: a promotion assembled before the upgrade can still be open, or waiting in a branch for the next step.
  29. A backpromote never touches a promotion branch, and a promotion never sees a backpromote branch. hardis:work:backpromote deploys what was merged in a parent major branch into a developer sandbox from its own technical branch backpromote/<parent branch>/<sandbox name> (a child of the parent branch holding the manual merges only, see the backpromote skill). Whatever branch is checked out when it starts, it commits nothing there: it switches to the backpromote branch. backpromote/* branches join promotion/* and retrofit/* in classifyBackpromoteCurrentBranch and in the extension mirror: the DevOps Pipeline, the release notes and the promotion candidates ignore them, and no CI job runs on them. The backpromote history lives in the "Backpromotes" Pull Request comment, which is not the CI/CD "Deployment Actions" comment: neither side reads the other's.
  30. What a story already reached is read from the merged promotions of the step, not from the windows. The window of a branch runs since its last direct merge into the next branch, and a project using promotions may never merge uat into preprod directly again, so a promotion merged into preprod leaves the preprod window at the next go-live while the stories it carried stay in the uat window for months (issue #2260: 292 counted, 129 pending). In the extension, listMergedPromotionsOfSteps (orgConfigUtils.ts) asks the provider (listMergedPullRequestsIntoBranch) for the merged promotions of every branch -> merge target step, bounded by the oldest Pull Request of the source window, and feeds them into the index next to the ones the windows hold; enforceSinglePlacePerPullRequest stays as the fallback for a provider that answers nothing. In the CLI, the branch mode of hardis:doc:release-notes subtracts the same stories (dropAlreadyPromotedPullRequests), while deploy:smart keeps them in scope on purpose (no-op redeploy, actions skipped).
Show full SKILL.md (945 more words)Show less

sfdx-hardis (CLI)

FileRole
src/common/utils/promotionBranchUtils.tsPure logic: naming, parsing the declaration, classification, expansion, inherited behaviors, promotion index. No I/O.
src/common/utils/promotionCreateUtils.tsEverything the two promotion commands need: the configuration gate, branch resolution, candidate listing, already-promoted detection, candidate table and summaries, cherry-picking, conflict handling, Pull Request body.
src/commands/hardis/project/promotion/create.tsThe command. Flags: --source-branch, --target-branch, --pull-requests, --skip-pull-request, --include-already-promoted, --on-conflict, --agent.
src/commands/hardis/project/promotion/list-candidates.tsRead-only listing of the candidates, for agents. Flags: --source-branch, --target-branch, --include-already-promoted, --agent. Creates, pushes and closes nothing.
src/common/utils/pullRequestUtils.tsResolves the declared Pull Requests from the git provider, walks the downstream promotions, merges the yaml blocks of a description.
src/common/utils/backpromoteUtils.tslistMergedPrsWithCommits, which the candidate list is built from: attributeCommitsToFirstParents decides which commits a merge brought in, splitVehicleMerges opens up the merges that only move other merges.
src/common/gitProvider/gitProviderRoot.ts + the four providersclosePullRequest() (close on GitHub/GitLab, abandon on Azure DevOps, decline on Bitbucket), used to supersede the promotion already open, and getPullRequestCreateUrl() on each provider class (buildPrCreateUrl / PullRequestCreateUrlResult live in the root).
src/commands/hardis/project/deploy/smart.tsApplies the inherited custom behaviors and the conflict-marker gate.
src/common/utils/prePostCommandUtils.tsDeployment actions of the carried stories, promotion scope wording.
src/common/utils/releaseNotesUtils.tsLeaves the vehicles out, --include-promotions brings them back.
src/common/gitProvider/index.tsinheritedCustomBehaviors + the inheritedCustomBehaviorsPrId guard, createPullRequest retries + lastPullRequestCreationError.
config/sfdx-hardis.jsonschema.jsonenablePromotionBranches and allowedPromotionSteps properties (required for any new config key).
test/common/utils/promotionBranchUtils.test.ts, promotionCreateUtils.test.ts, releaseNotesPromotion.test.ts, backpromoteUtils.test.ts, prDescriptionYaml.test.tsUnit tests.

Reading the flag: getConfig('branch') (project config merged with the running branch's config), via getPromotionBranchConfig(config), which also parses allowedPromotionSteps into config.allowedSteps. Both are project level settings: the extension exposes them at project scope only, because promotion:create runs from any branch and would not see a branch file.

Allowed steps

parsePromotionSteps reads the list (objects, or a "uat > preprod" string for a hand-edited config; no target means any target of that source). isPromotionStepAllowed, allowedPromotionSourceBranches, allowedPromotionTargetBranches and formatPromotionSteps are what the callers use. resolvePromotionSourceAndTarget filters both prompts and refuses a flag naming a step outside the list; warnAboutPromotionPullRequestMisuse warns in the deployment job; the extension mirrors the same functions in pipeline.js (it cannot import them: an LWC module of the webview only resolves other s/ modules). _isPromotionSourceAllowed asks for a step naming the branch and a target it can reach, a merge target read from pipelineData.links, so a step pointing somewhere the pipeline does not go opens nothing. showCreatePromotionButton ends with it and modalHideCheckboxColumn is its negation, so the button and the per-story checkboxes appear and disappear together. An empty list keeps them, on purpose: the command then answers with promotionCreateAllowedStepsRequired, which names the setting, where a missing button would say nothing.

Pull Request scope kinds

single-pr, batch, go-live, check, promotion, promotion-check. A promotion job resolves to promotion / promotion-check and its scope is the declared stories plus the promotion Pull Request itself.

Custom behavior keywords

NO_DELTA, PURGE_FLOW_VERSIONS and DESTRUCTIVE_CHANGES_AFTER_DEPLOYMENT are recognized anywhere in a description. FLOW_DELETE_INTERVIEWS is not: interview deletion is irreversible, so it only counts on a line of its own, as a bullet, or as a ticked checkbox. A promotion inherits the keywords of the stories it carries, and only those: a keyword of a story left behind must not leak in.

vscode-sfdx-hardis (extension)

FileRole
src/utils/pipeline/promotionBranchUtils.tsMirror of the CLI pure logic, plus the pipeline rules: isVehiclePullRequest, userStoryPullRequests, visiblePullRequests, annotateAlreadyPromoted, enforceSinglePlacePerPullRequest.
src/commands/showPipeline.tsFetches the declared Pull Requests (in parallel), builds the windows.
src/utils/orgConfigUtils.tscompleteMajorOrgsWithPromotionBranches: expands the windows, reads the merged promotions of each step from the provider (listMergedPromotionsOfSteps), annotates, enforces the invariant.
src/utils/gitProviders/*.tslistMergedPullRequestsIntoBranch(target, updatedAfter) on the four providers and the mock, sharing the per-branch fetch of the window listings.
src/pipeline-data-provider.tsFeeds the mermaid builder.
src/utils/pipeline/branchStrategyMermaidBuilder.tsNode counters (data-count, User Stories not promoted away), and the open promotion drawn on the major-to-major edge (isPromotionOfStep).
src/webviews/lwc-ui/modules/s/pipeline/pipeline.jsBranch window modal: filtering, the vehicles toggle, the per-Pull-Request checkboxes and the Create promotion button.
src/utils/pipeline/sfdxHardisConfigHelper.tsenablePromotionBranches and allowedPromotionSteps sit in the Danger Zone of Pipeline Settings, scope ["global"].
src/hardis-commands-provider.tsCommand palette entry for hardis:project:promotion:create.

Filtering: what moves the Pull Requests

The rule the lists and counters follow, in the diagram, the modal and the release notes:

Pull Requestpromotions OFFpromotions ON
feature/, fix/, retrofit/, hotfix/, anything elselistedlisted
uat -> preprod (major to major)hiddenhidden
promotion/uat/preprod/...listedhidden

Major-to-major merges are filtered for every project: such a merge is plumbing in any pipeline. A promotion/ branch is only a vehicle when the feature is enabled, because otherwise it really is an ordinary branch, which is exactly how the deployment jobs treat it.

What brings the vehicles back: the Merges and promotions chip of the Pull Requests list (s/pullRequestList, next to its text filter), and --include-promotions on hardis:doc:release-notes.

Performance

A big project has hundreds of Pull Requests, so:

  • buildPromotionIndex parses each promotion description once and indexes by story number; never re-parse per story.
  • Provider queries for already-promoted detection are bounded by oldestCandidateDate.
  • The extension fetches the declared Pull Requests in parallel and caches the merged ones.

When you change something

  • New config property: add it to config/sfdx-hardis.jsonschema.json, and to resources/sfdx-hardis.jsonschema.json in the extension if the settings UI must know it before sfdx-hardis publishes the schema.
  • New user-visible string: t() plus the key in all 9 locales (node scripts/i18n-upsert.mjs in the CLI, the 9 src/i18n/*.json in the extension).
  • Any behaviour change: re-run the A/B regression check with the feature off before saying it is safe. A green unit suite has already missed a flag-off regression once.
  • The monitoring AGENTS.md explains promotion branches and the Pull Request scope of deploy:smart to coding agents: when their behaviour changes, load the monitoring-agents-md skill and update its "How the CI/CD pipeline works" section.
  • The feature is in Beta: keep the "(Beta)" mention in the docs page, the JSON schema description and the settings UI. It moved from "(experimental)" in September 2026, so a new mention is written "(Beta)", capital B, like backpromote.

© hardisgroupcom, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/promotion-branches of hardisgroupcom/sfdx-hardis.

Open the folder on GitHubat commit 33d4de0

Compare with similar skills

Promotion Branches next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Promotion Branches compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Promotion Branches this skillhardisgroupcom/sfdx-hardis402—~8.9kAutomated safety check: NotesAGPL-3.0
Changelog Entrycertinia/debug-log-analyzer114—~1.9kAutomated safety check: PassCustom licence
StarRocks Release NotesStarRocks/starrocks12k—~1.9kAutomated safety check: NotesApache-2.0
Git Workflow and Versioningaddyosmani/agent-skills103k2 repos~3.5kAutomated safety check: NotesMIT
Changesetbiomejs/biome26k—~839Automated safety check: PassApache-2.0
Verdaccio Pull Request Workflowverdaccio/verdaccio18k—~1.9kAutomated safety check: PassMIT

Similar skills

  • Changelog Entry

    certinia/debug-log-analyzer

    Write, review or trim CHANGELOG.md entries. An agent skill from certinia/debug-log-analyzer.

    114 GitHub stars~1.9k tokensUpdated today
    DevelopmentAuto-check passed
  • StarRocks Release Notes

    StarRocks/starrocks

    Drafts English release notes for a StarRocks patch release from the PRs merged into its release branch, then opens a documentation PR and hands translation to /translate.

    12k GitHub stars~1.9k tokensUpdated today
    DevelopmentAuto-check: notes
  • Git Workflow and Versioning

    addyosmani/agent-skills

    Sets git habits for every change: short-lived branches, atomic commits with descriptive messages, clean pull requests, plus versioning, tagging and changelogs for releases.

    103k GitHub starsUsed in 2 repos~3.5k tokens
    DevelopmentAuto-check: notes
  • Changeset

    biomejs/biome

    Official

    A skill your agent uses when a Biome change may affect users and you must decide whether it needs a changeset, choose the release level, or create and edit .changeset/.md release-note text.

    26k GitHub stars~839 tokensUpdated today
    DevelopmentAuto-check passed
  • Takes a change through a verdaccio pull request: branch, local checks, changeset, title and body, labels, CI and review rounds, and ports to other release lines.

    18k GitHub stars~1.9k tokensUpdated today
    DevelopmentAuto-check passed
  • Opens the pybind11 release-preparation pull request: picking the release base, bumping the version in common.h and integrating the changelog, following docs/release.rst.

    18k GitHub stars~1.7k tokensUpdated today
    DevelopmentAuto-check passed

More from hardisgroupcom/sfdx-hardis

All 21 skills in this repo
  • sfdx-hardis Training End-to-End Test

    hardisgroupcom/sfdx-hardis

    Walks the sfdx-hardis training course end to end as a learner would, against a real Developer Edition org and fork, fixing broken steps and screenshots that no longer match.

    402 GitHub stars~2.6k tokensUpdated today
    Auto-check: notes
  • Promotion Branches E2E Test

    hardisgroupcom/sfdx-hardis

    Runs a full end-to-end test of sfdx-hardis promotion branches and backpromote against real Salesforce orgs and a throwaway repository, then writes a report.

    402 GitHub stars~11k tokensUpdated today
    Auto-check: notes
  • sfdx-hardis Architecture Guide

    hardisgroupcom/sfdx-hardis

    Explains how the sfdx-hardis Salesforce CLI plugin is built: its TypeScript and Oclif stack, command layout, agent-mode flag and provider classes for git, notifications and AI.

    402 GitHub stars~2.1k tokensUpdated today
    Auto-check passed
  • Changelog Style Rules

    hardisgroupcom/sfdx-hardis

    Style rules for adding CHANGELOG.md entries: short, user-facing bullets grouped by command under the beta section, each linking the command's docs page.

    402 GitHub stars~1.4k tokensUpdated today
    Auto-check passed
  • Documentation

    hardisgroupcom/sfdx-hardis

    Documentation standards for sfdx-hardis commands (description format with Command Behavior and Technical explanations sections, MkDocs site, build:doc).

    402 GitHub stars~1k tokensUpdated today
    Auto-check passed
  • Fix Jscpd

    hardisgroupcom/sfdx-hardis

    Decision framework for fixing jscpd (copy-paste detector) errors.

    402 GitHub stars~613 tokensUpdated today
    Auto-check passed

Categories

Questions about Promotion Branches

What does Promotion Branches do?

How the promotion branches feature works (enablePromotionBranches, sf hardis:project:promotion:create) across sfdx-hardis and vscode-sfdx-hardis, and every file to touch when changing it. Promotion Branches is an agent skill from hardisgroupcom/sfdx-hardis. How the promotion branches feature works (enablePromotionBranches, sf hardis:project:promotion:create) across sfdx-hardis and vscode-sfdx-hardis, and every file to touch when changing it.

When should I use Promotion Branches?

Promotion Branches fits situations like: working on promotion branches; on the Pull Request scope of deploy:smart; on release notes filtering; on the DevOps Pipeline windows.

How do I install Promotion Branches in Claude Code?

Run `npx skills add hardisgroupcom/sfdx-hardis --skill promotion-branches -a claude-code`. Or copy the skill folder (.claude/skills/promotion-branches in hardisgroupcom/sfdx-hardis) into .claude/skills/promotion-branches in your project. Claude Code loads it when a task matches its description.

How do I install Promotion Branches in Codex?

Run `npx skills add hardisgroupcom/sfdx-hardis --skill promotion-branches -a codex`. Or copy the skill folder (.claude/skills/promotion-branches in hardisgroupcom/sfdx-hardis) into .agents/skills/promotion-branches in your project. Codex loads it when a task matches its description.

Can I use Promotion Branches in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add hardisgroupcom/sfdx-hardis --skill promotion-branches -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/promotion-branches, .gemini/skills/promotion-branches, .github/skills/promotion-branches and .opencode/skills/promotion-branches in your project.

What does Promotion Branches need to run?

Going by SKILL.md and its folder, Promotion Branches needs the command-line tools its instructions call (git, sf and node).

Does Promotion Branches access the network?

SKILL.md names 1 domain. As links in the text: sfdx-hardis.cloudity.com. This is read from the text; nothing was executed.

Is Promotion Branches safe to install?

Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.

What licence does Promotion Branches use?

Promotion Branches is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Promotion Branches use?

About 8.9k tokens (SKILL.md is roughly 35k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Promotion Branches?

Skills that share tags, products or a category with Promotion Branches: Changelog Entry (certinia/debug-log-analyzer, 114 stars), StarRocks Release Notes (StarRocks/starrocks, 12k stars), Git Workflow and Versioning (addyosmani/agent-skills, 103k stars) and Changeset (biomejs/biome, 26k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Promotion Branches?

hardisgroupcom (a GitHub organization) maintains it in hardisgroupcom/sfdx-hardis, which has 402 GitHub stars. The repository holds 21 skills in this directory. The repository was last updated on October 9, 2026.

Source: hardisgroupcom/sfdx-hardis on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.