Changelog Entry
certinia/debug-log-analyzer
Write, review or trim CHANGELOG.md entries. An agent skill from certinia/debug-log-analyzer.
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.
$ npx skills add hardisgroupcom/sfdx-hardis --skill promotion-branches -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install hardisgroupcom/sfdx-hardis promotion-branches --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ 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-srcUse ~/.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/
Install the "promotion-branches" agent skill from https://github.com/hardisgroupcom/sfdx-hardis/tree/main/.claude/skills/promotion-branches into .claude/skills/promotion-branches/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "promotion-branches", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/hardisgroupcom/sfdx-hardis/tree/main/.claude/skills/promotion-branchesType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add hardisgroupcom/sfdx-hardis --skill promotion-branches -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install hardisgroupcom/sfdx-hardis promotion-branches --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/hardisgroupcom/sfdx-hardis.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/promotion-branches .agents/skills/promotion-branches && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "promotion-branches" agent skill from https://github.com/hardisgroupcom/sfdx-hardis/tree/main/.claude/skills/promotion-branches into .agents/skills/promotion-branches/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "promotion-branches", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add hardisgroupcom/sfdx-hardis --skill promotion-branches -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install hardisgroupcom/sfdx-hardis promotion-branches --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/hardisgroupcom/sfdx-hardis.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/promotion-branches .cursor/skills/promotion-branches && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "promotion-branches" agent skill from https://github.com/hardisgroupcom/sfdx-hardis/tree/main/.claude/skills/promotion-branches into .cursor/skills/promotion-branches/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "promotion-branches", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/hardisgroupcom/sfdx-hardis.git --path .claude/skills/promotion-branches--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add hardisgroupcom/sfdx-hardis --skill promotion-branches -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install hardisgroupcom/sfdx-hardis promotion-branches --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/hardisgroupcom/sfdx-hardis.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/promotion-branches .gemini/skills/promotion-branches && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "promotion-branches" agent skill from https://github.com/hardisgroupcom/sfdx-hardis/tree/main/.claude/skills/promotion-branches into .gemini/skills/promotion-branches/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "promotion-branches", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install hardisgroupcom/sfdx-hardis promotion-branchesInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add hardisgroupcom/sfdx-hardis --skill promotion-branches -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/hardisgroupcom/sfdx-hardis.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/promotion-branches .github/skills/promotion-branches && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "promotion-branches" agent skill from https://github.com/hardisgroupcom/sfdx-hardis/tree/main/.claude/skills/promotion-branches into .github/skills/promotion-branches/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "promotion-branches", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add hardisgroupcom/sfdx-hardis --skill promotion-branches -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install hardisgroupcom/sfdx-hardis promotion-branches --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/hardisgroupcom/sfdx-hardis.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/promotion-branches .opencode/skills/promotion-branches && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "promotion-branches" agent skill from https://github.com/hardisgroupcom/sfdx-hardis/tree/main/.claude/skills/promotion-branches into .opencode/skills/promotion-branches/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "promotion-branches", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
promotion-branchesHow 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. 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.
12 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 33d4de0. It shows what the files ask for, not the result of running them.
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.
Shell commands in SKILL.md call:
gitsfnodeFrom the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
sfdx-hardis.cloudity.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
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.
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.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
from the environment or a `.env` file against the project path of `remote.origin.url`, andAutomated 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.
The full file from hardisgroupcom/sfdx-hardis at commit 33d4de0, republished under its AGPL-3.0 licence (© hardisgroupcom). 3,918 words, ~8,852 tokens.
.claude/skills/promotion-branches/SKILL.md (or your agent's skills folder).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.
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:
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.
Break one of these and the feature is wrong, whatever the tests say.
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.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.promotedAway hides it from the branch it left).--include-promotions is passed
(release notes).preprod -> main promotion can carry a uat -> preprod
promotion, which carries stories. Both levels must resolve.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.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.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.shouldAddVirtualPullRequest refuses the number-less
entry when a real Pull Request of the same branch is already in the group.countsAsAlreadyPromoted): otherwise agreeing to
supersede it would leave nothing to assemble.-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.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.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.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.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.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.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.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.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.!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.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.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.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.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.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.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.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.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).| File | Role |
|---|---|
src/common/utils/promotionBranchUtils.ts | Pure logic: naming, parsing the declaration, classification, expansion, inherited behaviors, promotion index. No I/O. |
src/common/utils/promotionCreateUtils.ts | Everything 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.ts | The command. Flags: --source-branch, --target-branch, --pull-requests, --skip-pull-request, --include-already-promoted, --on-conflict, --agent. |
src/commands/hardis/project/promotion/list-candidates.ts | Read-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.ts | Resolves the declared Pull Requests from the git provider, walks the downstream promotions, merges the yaml blocks of a description. |
src/common/utils/backpromoteUtils.ts | listMergedPrsWithCommits, 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 providers | closePullRequest() (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.ts | Applies the inherited custom behaviors and the conflict-marker gate. |
src/common/utils/prePostCommandUtils.ts | Deployment actions of the carried stories, promotion scope wording. |
src/common/utils/releaseNotesUtils.ts | Leaves the vehicles out, --include-promotions brings them back. |
src/common/gitProvider/index.ts | inheritedCustomBehaviors + the inheritedCustomBehaviorsPrId guard, createPullRequest retries + lastPullRequestCreationError. |
config/sfdx-hardis.jsonschema.json | enablePromotionBranches 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.ts | Unit 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.
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.
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.
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.
| File | Role |
|---|---|
src/utils/pipeline/promotionBranchUtils.ts | Mirror of the CLI pure logic, plus the pipeline rules: isVehiclePullRequest, userStoryPullRequests, visiblePullRequests, annotateAlreadyPromoted, enforceSinglePlacePerPullRequest. |
src/commands/showPipeline.ts | Fetches the declared Pull Requests (in parallel), builds the windows. |
src/utils/orgConfigUtils.ts | completeMajorOrgsWithPromotionBranches: expands the windows, reads the merged promotions of each step from the provider (listMergedPromotionsOfSteps), annotates, enforces the invariant. |
src/utils/gitProviders/*.ts | listMergedPullRequestsIntoBranch(target, updatedAfter) on the four providers and the mock, sharing the per-branch fetch of the window listings. |
src/pipeline-data-provider.ts | Feeds the mermaid builder. |
src/utils/pipeline/branchStrategyMermaidBuilder.ts | Node 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.js | Branch window modal: filtering, the vehicles toggle, the per-Pull-Request checkboxes and the Create promotion button. |
src/utils/pipeline/sfdxHardisConfigHelper.ts | enablePromotionBranches and allowedPromotionSteps sit in the Danger Zone of Pipeline Settings, scope ["global"]. |
src/hardis-commands-provider.ts | Command palette entry for hardis:project:promotion:create. |
The rule the lists and counters follow, in the diagram, the modal and the release notes:
| Pull Request | promotions OFF | promotions ON |
|---|---|---|
feature/, fix/, retrofit/, hotfix/, anything else | listed | listed |
uat -> preprod (major to major) | hidden | hidden |
promotion/uat/preprod/... | listed | hidden |
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.
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.oldestCandidateDate.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.t() plus the key in all 9 locales (node scripts/i18n-upsert.mjs
in the CLI, the 9 src/i18n/*.json in the extension).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.© 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
Just SKILL.md in .claude/skills/promotion-branches of hardisgroupcom/sfdx-hardis.
Open the folder on GitHubat commit 33d4de0
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.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Promotion Branches this skillhardisgroupcom/sfdx-hardis | 402 | — | ~8.9k | Automated safety check: Notes | AGPL-3.0 | |
| Changelog Entrycertinia/debug-log-analyzer | 114 | — | ~1.9k | Automated safety check: Pass | Custom licence | |
| StarRocks Release NotesStarRocks/starrocks | 12k | — | ~1.9k | Automated safety check: Notes | Apache-2.0 | |
| Git Workflow and Versioningaddyosmani/agent-skills | 103k | 2 repos | ~3.5k | Automated safety check: Notes | MIT | |
| Changesetbiomejs/biome | 26k | — | ~839 | Automated safety check: Pass | Apache-2.0 | |
| Verdaccio Pull Request Workflowverdaccio/verdaccio | 18k | — | ~1.9k | Automated safety check: Pass | MIT |
certinia/debug-log-analyzer
Write, review or trim CHANGELOG.md entries. An agent skill from certinia/debug-log-analyzer.
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.
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.
biomejs/biome
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.
verdaccio/verdaccio
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.
pybind/pybind11
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.
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.
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.
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.
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.
hardisgroupcom/sfdx-hardis
Documentation standards for sfdx-hardis commands (description format with Command Behavior and Technical explanations sections, MkDocs site, build:doc).
hardisgroupcom/sfdx-hardis
Decision framework for fixing jscpd (copy-paste detector) errors.
Works with
Categories
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.
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.
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.
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.
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.
Going by SKILL.md and its folder, Promotion Branches needs the command-line tools its instructions call (git, sf and node).
SKILL.md names 1 domain. As links in the text: sfdx-hardis.cloudity.com. This is read from the text; nothing was executed.
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.
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.
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.
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.
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.