Skip to content

Commit bc3de2b

Browse files
authored
fix(deploy): collect the right Pull Request scope for deployment actions and Apex test classes on every merge (#2054)
* fix(deploy): scope deployment actions and Apex test classes to the merged Pull Request Merging a feature branch into a major branch reported and executed the deployment actions of every other Pull Request of the batch, because listAllPullRequestsForCurrentScope discards the merged Pull Request in deployment mode and re-derives the scope from the branch strategy. One feature merge could therefore run an action belonging to another Pull Request, and attribute it to the wrong one in the Slack notification and in the Pull Request comment. The scope is now the merged Pull Request alone when its source branch is neither a major branch nor a retrofit branch. A merge between major branches, or from a retrofit branch, keeps collecting the batch, because those carry the Pull Requests merged into the source major branch since its last promotion. The scoping happens at collection time, so deployment actions, the Apex test classes selected from Pull Requests, the deployment notification, the Pull Request comment and the per-PR "Deployment Actions" state comment all follow it without any of them needing a filter of their own. Side effect worth noting: the single Pull Request scope no longer depends on mergeTargets being configured. A feature merge into a major branch whose config declares no merge target used to collect nothing at all, silently dropping the merged Pull Request's own actions. * [MegaLinter] Apply linters fixes * fix(deploy): run deployment actions on go-live merges, retrofitted upstream PRs and GitHub push deploys (#2055) * fix(deploy): run deployment actions on go-live merges, retrofitted upstream PRs and GitHub push deploys Three holes in the Pull Request scope collection prevented deployment actions and selected Apex test classes from running in major orgs where they had not been performed yet. Production merges collected nothing. The batch window is anchored on the target branch's mergeTargets, and a production branch has none, so a preprod -> main merge returned an empty scope: no action ran and no test class was selected on the go-live deployment. The scope is now the batch of Pull Requests carried by the go-live merge itself, through the existing listPullRequestsInGoLive provider method, bounded by the merge commit's first parent. When the merge commit is unknown, the scope falls back to the merged Pull Request alone instead of nothing. Upstream Pull Requests were invisible to retrofits. The window collectors only fetched Pull Requests whose base is the window branch or one of its children, so a hotfix merged into main was never collected when a retrofit branch brought its commits down to integration. Every major branch is now part of the search list: matching stays bounded by the window's commit SHAs, so an upstream Pull Request is only collected when its merge commit actually arrived in the window. GitHub push-triggered deployments never entered the engine at all. Without a Pull Request number, the GraphQL commit lookup selected neither the head and base ref names nor the merge commit, so the resolved Pull Request had empty source and target branches and the scope resolution bailed out immediately. The query now selects them and maps the node to the shape the common Pull Request info builder reads, which also fixes the empty author and web URL. * docs(deploy): correct the search-branches example comment in listAllPullRequestsForCurrentScope --------- Co-authored-by: nvuillam <17500430+nvuillam@users.noreply.github.com>
1 parent 538ec96 commit bc3de2b

19 files changed

Lines changed: 327 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
- A destructive changes manifest configured through `packageXmlToDelete` / `packageXmlToDeletePreDeploy` that does not exist on disk now logs a warning, instead of being skipped silently.
3232
- **Behavior change: post-deployment actions are no longer run when the metadata deployment failed.** They are reported as `not run` in the Pull Request comment, no execution state is stored, and the job now fails on the deployment error instead of on a post-deployment action error. The `skipIfError` property is removed, and is ignored if still present in your configuration.<br/>**Check your configuration before upgrading: an action relying on `skipIfError: false` to run after a failed deployment will no longer run.**
3333
- Deployment actions: a failed action now displays why it failed in the job log and in the Pull Request comment. Actions whose command ends with `--json` used to fail with no output at all.
34+
- Deployment actions and selected Apex test classes: merging a feature branch now processes only the Pull Request just merged, while merges between major branches and from `retrofit/*` branches keep replaying the whole batch of upstream Pull Requests.
35+
- Deployment actions and selected Apex test classes now also run on merges into the production branch, scoped to the Pull Requests carried by the go-live merge.
36+
- Deployment actions of Pull Requests merged upstream (like a hotfix in `main`) are now replayed downstream when a retrofit branch brings their commits to another major branch.
37+
- Fix deployment actions never running on GitHub push-triggered deployment jobs, caused by incomplete Pull Request detection.
3438

3539
### CI/CD
3640

docs/hardis/project/deploy/smart.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ You can define command lines to run before or after a deployment, with parameter
161161

162162
Post-deployment actions are never run when the metadata deployment failed: they are reported as `not run` and are proposed again during the next successful deployment.
163163

164+
Deployment actions and selected Apex test classes are scoped to the Pull Request that has just been merged when it comes from a feature branch. A merge from a major branch (ex: integration -> uat) or from a retrofit branch (ex: retrofit/from-main -> integration) keeps those of every Pull Request merged into the source major branch since its last promotion, and a merge into the production branch keeps those of every Pull Request carried by the go-live merge. Pull Requests merged upstream (ex: a hotfix in main) are included as soon as their commits arrive in the window.
165+
166+
If the deployment job of a feature branch fails, its actions are not picked up by the next merged Pull Request: re-run the failed deployment job, or move the actions to a new Pull Request.
167+
164168
After every action runs, its result (✅ success, ❌ failed, 👋 manual) is recorded in a dedicated **"Deployment Actions"** PR comment - ordered by org (integration → uat → preprod → prod) - regardless of `runOnlyOnceByOrg`.
165169

166170
If the commands are not the same depending on the target org, you can define them into **config/branches/.sfdx-hardis-BRANCHNAME.yml** instead of root **config/.sfdx-hardis.yml**

docs/hardis/project/deploy/sources/dx.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ You can define command lines to run before or after a deployment, with parameter
161161

162162
Post-deployment actions are never run when the metadata deployment failed: they are reported as `not run` and are proposed again during the next successful deployment.
163163

164+
Deployment actions and selected Apex test classes are scoped to the Pull Request that has just been merged when it comes from a feature branch. A merge from a major branch (ex: integration -> uat) or from a retrofit branch (ex: retrofit/from-main -> integration) keeps those of every Pull Request merged into the source major branch since its last promotion, and a merge into the production branch keeps those of every Pull Request carried by the go-live merge. Pull Requests merged upstream (ex: a hotfix in main) are included as soon as their commits arrive in the window.
165+
166+
If the deployment job of a feature branch fails, its actions are not picked up by the next merged Pull Request: re-run the failed deployment job, or move the actions to a new Pull Request.
167+
164168
After every action runs, its result (✅ success, ❌ failed, 👋 manual) is recorded in a dedicated **"Deployment Actions"** PR comment - ordered by org (integration → uat → preprod → prod) - regardless of `runOnlyOnceByOrg`.
165169

166170
If the commands are not the same depending on the target org, you can define them into **config/branches/.sfdx-hardis-BRANCHNAME.yml** instead of root **config/.sfdx-hardis.yml**

docs/salesforce-ci-cd-smart-deployment.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,11 @@ Pull Request descriptions can also override config properties using YAML blocks
373373
```mermaid
374374
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#eaf5fe", "primaryTextColor": "#032d60", "primaryBorderColor": "#0176d3", "lineColor": "#0176d3", "fontFamily": "Salesforce Sans, Arial, sans-serif"}}}%%
375375
flowchart TD
376-
CMD_START[executePrePostCommands] --> SOURCES[Collect commands from:<br/>1. Branch config<br/>2. Extra commands from PR<br/>3. PR description YAML blocks]
376+
CMD_START[executePrePostCommands] --> SCOPE{Merge from a<br/>feature branch?}
377+
SCOPE -->|Yes| ONE_PR[Scope: the merged PR only]
378+
SCOPE -->|No: major or retrofit branch| BATCH[Scope: all PRs since<br/>the previous merge]
379+
ONE_PR --> SOURCES[Collect commands from:<br/>1. Branch config<br/>2. Extra commands from PR<br/>3. PR description YAML blocks]
380+
BATCH --> SOURCES
377381
SOURCES --> DEPLOY_KO{Metadata deployment<br/>failed?}
378382
DEPLOY_KO -->|Yes| SKIP_ERR[Report every action as<br/>not run and stop]
379383
DEPLOY_KO -->|No| LOOP{For each command}
@@ -395,8 +399,8 @@ flowchart TD
395399
classDef sfSuccess fill:#cdefc4,stroke:#2e844a,color:#194e31,stroke-width:1.5px
396400
classDef sfSkip fill:#f3f3f3,stroke:#706e6b,color:#3e3e3c,stroke-width:1px
397401
398-
class CMD_START,SOURCES sfAction
399-
class LOOP,VALID,DEPLOY_KO,CTX_CHECK,ONCE_CHECK sfDecision
402+
class CMD_START,SOURCES,ONE_PR,BATCH sfAction
403+
class LOOP,VALID,SCOPE,DEPLOY_KO,CTX_CHECK,ONCE_CHECK sfDecision
400404
class EXEC sfSuccess
401405
class SKIP_INVALID,SKIP_ERR,SKIP_CTX,SKIP_ONCE sfSkip
402406
```
@@ -416,6 +420,8 @@ All action types support the following common properties: `id`, `label`, `contex
416420

417421
Post-deployment actions are never run when the metadata deployment failed. They are reported as `not run` in the Pull Request comment, no execution state is stored, and they are proposed again during the next successful deployment.
418422

423+
A merge from a feature branch only processes the actions and the Apex test classes of the Pull Request that has just been merged. A merge between major branches, or from a retrofit branch (`retrofit/*`), processes those of every Pull Request merged into the source major branch since its last promotion. A merge into the production branch processes those of every Pull Request carried by the go-live merge. Pull Requests merged upstream (ex: a hotfix in `main`) are included as soon as their commits arrive in the window, for example through a retrofit branch.
424+
419425
Command context options:
420426

421427
- `all` (default), runs in both check and process modes

docs/salesforce-ci-cd-work-on-task-deployment-actions.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,28 @@ Each action is an object with the following required and optional properties.
8989

9090
> Post-deployment actions are never run when the metadata deployment failed. They are reported as `not run` in the Pull Request comment, no execution state is stored for them, and they are proposed again during the next successful deployment.
9191

92+
### Which Pull Requests are in scope
93+
94+
The actions collected for a deployment depend on the branch the merged Pull Request comes from.
95+
96+
| Merge | Scope |
97+
|--------------------------------------------------------------------|----------------------------------------------------|
98+
| From a feature branch (ex: `feature/my-story` -> `integration`) | Only the Pull Request that has just been merged |
99+
| Between major branches (ex: `integration` -> `uat`) | Every Pull Request merged since the previous merge |
100+
| From a retrofit branch (ex: `retrofit/from-main` -> `integration`) | Every Pull Request merged since the previous merge |
101+
102+
A feature branch merge carries a single Pull Request, so its notification and its Pull Request comment list only that Pull Request's actions. A major branch or retrofit branch merge carries a batch of Pull Requests, whose actions must be replayed in the target org.
103+
104+
- Between major branches, the batch is every Pull Request merged into the source major branch since its last promotion.
105+
- Into the production branch (which has no promotion target), the batch is every Pull Request carried by the go-live merge itself.
106+
- Pull Requests merged into upstream branches are part of the batch as soon as their commits arrive in the window: a hotfix merged into `main` is collected when a retrofit branch brings it down to `integration`, so its actions run there too.
107+
108+
In every case, `runOnlyOnceByOrg` state tracking ensures each action runs only in the orgs where it has not been performed yet.
109+
110+
The same scope applies to the Apex test classes selected from Pull Requests when `enableDeploymentApexTestClasses` is active.
111+
112+
> If the deployment job of a feature branch fails, its actions are not picked up by the next merged Pull Request. Re-run the failed deployment job, or open a new Pull Request carrying the actions of the previous one.
113+
92114
### Deployment Actions PR comment
93115

94116
After every action runs, sfdx-hardis creates or updates a dedicated **"Deployment Actions"** comment on the Pull Request. This gives release managers a consolidated view of what has been executed across every org for the lifetime of the PR.

src/commands/hardis/project/deploy/smart.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ You can define command lines to run before or after a deployment, with parameter
206206
207207
Post-deployment actions are never run when the metadata deployment failed: they are reported as \`not run\` and are proposed again during the next successful deployment.
208208
209+
Deployment actions and selected Apex test classes are scoped to the Pull Request that has just been merged when it comes from a feature branch. A merge from a major branch (ex: integration -> uat) or from a retrofit branch (ex: retrofit/from-main -> integration) keeps those of every Pull Request merged into the source major branch since its last promotion, and a merge into the production branch keeps those of every Pull Request carried by the go-live merge. Pull Requests merged upstream (ex: a hotfix in main) are included as soon as their commits arrive in the window.
210+
211+
If the deployment job of a feature branch fails, its actions are not picked up by the next merged Pull Request: re-run the failed deployment job, or move the actions to a new Pull Request.
212+
209213
After every action runs, its result (✅ success, ❌ failed, 👋 manual) is recorded in a dedicated **"Deployment Actions"** PR comment - ordered by org (integration → uat → preprod → prod) - regardless of \`runOnlyOnceByOrg\`.
210214
211215
If the commands are not the same depending on the target org, you can define them into **config/branches/.sfdx-hardis-BRANCHNAME.yml** instead of root **config/.sfdx-hardis.yml**

src/common/gitProvider/github.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,13 @@ export class GithubProvider extends GitProviderRoot {
303303
body
304304
url
305305
merged,
306+
headRefName
307+
baseRefName
308+
mergeCommit {
309+
oid
310+
}
311+
createdAt
312+
mergedAt
306313
baseRef {
307314
id
308315
name
@@ -333,7 +340,21 @@ export class GithubProvider extends GitProviderRoot {
333340
(pr: any) => pr.node.merged === true && pr.node.baseRef.name === currentGitBranch,
334341
);
335342
if (candidatePullRequests.length > 0) {
336-
return this.completePullRequestInfo(candidatePullRequests[0].node);
343+
// The GraphQL node does not have the REST shape completePullRequestInfo reads
344+
// (head.ref / base.ref / merge_commit_sha / user.login / html_url), so map it first.
345+
// Without this, push-triggered deployments got a Pull Request with empty source and
346+
// target branches, and the deployment actions engine could not resolve its scope.
347+
const node = candidatePullRequests[0].node;
348+
return this.completePullRequestInfo({
349+
...node,
350+
head: { ref: node.headRefName || "" },
351+
base: { ref: node.baseRefName || node.baseRef?.name || "" },
352+
merge_commit_sha: node.mergeCommit?.oid || undefined,
353+
created_at: node.createdAt || undefined,
354+
merged_at: node.mergedAt || undefined,
355+
user: { login: node.author?.login || "" },
356+
html_url: node.url || "",
357+
});
337358
}
338359
}
339360
uxLog("log", this, c.grey('[GitHub Integration] ' + t('githubUnableToFindPrInfo')));

src/common/utils/orgConfigUtils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,19 @@ export function isUatRun(branchName) {
272272
return (branchName.toLowerCase().startsWith("uat") || branchName.toLowerCase().startsWith("recette")) && branchName.toLowerCase().includes("run");
273273
}
274274

275+
/**
276+
* A retrofit branch carries the changes of an upstream major branch down to another one
277+
* (ex: main is merged into retrofit/from-main, which is then merged into integration).
278+
* Retrofit branches have no org config file, so they never appear in listMajorOrgs():
279+
* they are identified by their name, like the other branch kinds above.
280+
*/
281+
export function isRetrofit(branchName: string): boolean {
282+
// Matches the retrofit/<name> convention produced by hardis:work:new, so an ordinary branch
283+
// named retrofit-JIRA-123 or retrofitting-legacy is not mistaken for one.
284+
const name = (branchName || "").toLowerCase();
285+
return name === "retrofit" || name.startsWith("retrofit/");
286+
}
287+
275288
/**
276289
* Get the Chrome/Chromium executable path for Puppeteer
277290
* This is used by various commands that need browser automation

0 commit comments

Comments
 (0)