--- name: backpromote description: Drive a backpromote (sf hardis:work:backpromote, Beta) from a coding agent, and solve the merges it asks for. Use when asked to bring what was merged in a parent major branch (integration, uat...) into a developer sandbox or a scratch org, to read or fix the "Backpromotes" Pull Request comments, or when a backpromote stopped with status waitingForMerges or conflictsRemaining. user-invocable: true --- # Backpromote from a coding agent `sf hardis:work:backpromote` deploys into a **developer sandbox** what the team merged in a **parent major branch** since the last backpromote of that sandbox: the metadata (computed by sfdx-git-delta on the window of merged Pull Requests) and the deployment actions declared by those Pull Requests. The command page is `docs/hardis/work/backpromote.md`, and the user guide is `docs/salesforce-devops-backpromote.md`. Vocabulary: parent branch (`developmentBranch` or one of `availableTargetBranches`), target sandbox (never a major org), sandbox name (from the instance URL, `--sandbox-name` overrides), backpromote branch `backpromote//` (holds the manual merges only), start Pull Request, window, "Backpromotes" comment (the only source of the history). ## Procedure Everything runs with `--json`; every decision is a flag; nothing is asked in `--agent` mode. 1. **Plan.** `sf hardis:work:backpromote --plan --json --target-org --parent-branch `. Read `status` (`ok`, `blocked`, `nothingToDo`), `checks` (a failed `gitProvider` check means no token: set `GITHUB_TOKEN`, `CI_SFDX_HARDIS_GITLAB_TOKEN`, `SYSTEM_ACCESSTOKEN` or `CI_SFDX_HARDIS_BITBUCKET_TOKEN`), `pullRequests[]` (newest first, `backpromote` non null when the sandbox already received it, `selected` for the default start), `scan.found`, `window`, `items[]`, `deletions[]`, `actions[]` (`alreadyRunOn` set when it already ran in this sandbox), `comparison[]` and `runId`. When `scan.found` is false nothing is selected: pick the start with `--from-pull-request ` and run the plan again with `--run-id `. 2. **Decide.** For every `comparison[]` entry with status `different` or `pendingInOrg`, read the two (or three) versions at `versions.sandbox`, `versions.parentHead` and `versions.base` (absolute paths in the cache), then choose: `--on-diff "=git"` (overwrite with the parent branch version, the default), `=org` (keep the org version, the item is listed as kept and offered again next time) or `=merge`. `--on-diff-default git|org|merge` covers the rest. An item with `noOverwrite: true` (listed in `manifest/package-no-overwrite.xml`) is deployed when every `comparison[]` entry of it is `missingInOrg`; when the sandbox already has it, it is left alone unless `--include-no-overwrite Type:Name` names it (then it takes decisions like any item). Untick with `--exclude-metadata Type:Name`, `--skip-destructive`, `--actions a,b`, `--skip-actions`. 3. **Run.** `sf hardis:work:backpromote --agent --json --run-id --target-org --parent-branch --from-pull-request [decisions]`. The checkout is switched to the backpromote branch (uncommitted changes are stashed, the JSON says so in `checkout`), the pre-deployment actions run, the metadata is deployed (`NoTestRun`), the deletions, the post-deployment actions, then the "Backpromotes" comments are written and the branch is pushed when it holds merges. Exit code 0 with `status` `ok`, `nothingToDo` or `waitingForMerges`; exit code 1 with the plan in `data` for `refused`, `conflictsRemaining`, `deployFailed`, `pushRejected`. 4. **Merge when asked.** `status: waitingForMerges` means the files marked `merge` were written with conflict markers in the checkout (`comparison[].file` is relative to `gitRoot`, the prompt is in `promptFile`, the exact command in `runCommand`). Edit each file, leave no `<<<<<<<`, `|||||||`, `=======`, `>>>>>>>` line, commit the solved files on the backpromote branch (the body of the message says what was kept from each side), then run `runCommand`. It checks the files, commits any merged file still uncommitted, deploys and pushes the branch. A file left with markers is not deployed and is listed as "conflict pending": the next plan offers it first. 5. **Deployment errors.** `status: deployFailed` lists the refused components in `result.deployErrors` (file, line, the Salesforce error, the sfdx-hardis hint in `tip` and the AI suggestion in `aiTip` when there is one), and `result.deployErrorsPromptFile` holds a prompt with all of it. Fix the files on the backpromote branch and nowhere else (no other branch, no Pull Request), commit them there, then run the same command again. A component that cannot be fixed now is left out with `--exclude-metadata Type:Name`. 6. **Manual actions.** `result.actions.pending` lists the manual actions (and the ones whose custom username could not be authenticated). Once done by hand in the sandbox: `sf hardis:work:backpromote --confirm-action --json --run-id --target-org --parent-branch `. 7. **Back to the developer's branch.** The checkout stays on the backpromote branch. When the user wants their branch back: `git checkout `, `git stash pop` when `checkout.stashed` is true, then propose `git merge origin/` so that the next `hardis:work:save` does not commit the backpromoted metadata as the story's own work. Every call is idempotent: running the same command twice deploys nothing twice and runs no action twice (the comment rows are the state). `--reset --auto` deletes the backpromote branch and abandons the pending merges; the history in the comments is never touched. ## Merge rules - Keep both intents when they touch different parts of the file. - For shared configuration (layouts, profiles, permission sets, settings, flexipages) prefer the parent branch version; never remove an org-only element the sandbox work still needs. - Salesforce metadata is XML: well-formed result, one entry per API name, the existing order and indentation, the XML declaration and namespace untouched. Only change the conflicting lines. - Write, for each file, one sentence on what was kept from each side: it is the body of the commit of the solved files on the backpromote branch (the prompt the command writes asks for it). ## Where things live | What | Where | |----------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| | Command | `src/commands/hardis/work/backpromote.ts` | | Pure rules (branch and sandbox names, history walk, two-way merge, flags, prompt) | `src/common/utils/backpromoteRules.ts` | | "Backpromotes" comment (parse, render, upsert, cached reads) | `src/common/utils/backpromoteCommentUtils.ts` | | Git (fetch, checkout switch, branch rebuild, merge-file, push with lease, delta cache) | `src/common/utils/backpromoteGitUtils.ts` | | Org (target org, retrieve to the cache, comparison, deployments) | `src/common/utils/backpromoteOrgUtils.ts` | | Plan JSON version 3, run state, terminal prompts | `src/common/utils/backpromotePlanUtils.ts` | | Pull Request listing and action execution shared with promotion branches | `src/common/utils/backpromoteUtils.ts` | | VS Code panel | `../vscode-sfdx-hardis/src/commands/showBackpromote.ts`, `src/utils/backpromote/`, `src/webviews/lwc-ui/modules/s/backpromote/` | | End to end runbook | `.claude/skills/promotion-branches-e2e/reference/runbook.md`, section 6bis |