aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--build.gradle4
-rw-r--r--gradle.properties2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java112
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java6
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java10
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java25
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java8
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java113
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java1
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java10
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java194
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java3
-rw-r--r--server/sonar-db-dao/src/schema/schema-sq.ddl4
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java63
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java80
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java68
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java67
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java7
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java50
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java54
21 files changed, 871 insertions, 12 deletions
diff --git a/build.gradle b/build.gradle
index f12d73e573c..bc0f777c2f5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -302,7 +302,7 @@ subprojects {
dependency 'com.sonarsource.security:sonar-security-php-frontend-plugin:11.2.1.37710'
dependency 'com.sonarsource.security:sonar-security-plugin:11.2.1.37710'
dependency 'com.sonarsource.security:sonar-security-python-frontend-plugin:11.2.1.37710'
- dependency 'com.sonarsource.slang:sonar-apex-plugin:1.19.0.447'
+ dependency 'com.sonarsource.slang:sonar-apex-plugin:1.20.0.552'
dependency 'org.sonarsource.slang:sonar-ruby-plugin:1.19.0.471'
dependency 'org.sonarsource.slang:sonar-scala-plugin:1.19.0.484'
dependency 'com.sonarsource.swift:sonar-swift-plugin:4.13.1.8101'
@@ -443,7 +443,7 @@ subprojects {
entry 'log4j-api'
entry 'log4j-to-slf4j'
}
- dependencySet(group: 'org.apache.tomcat.embed', version: '11.0.7') {
+ dependencySet(group: 'org.apache.tomcat.embed', version: '11.0.8') {
entry 'tomcat-embed-core'
entry('tomcat-embed-jasper') {
exclude 'org.eclipse.jdt.core.compiler:ecj'
diff --git a/gradle.properties b/gradle.properties
index 1de4c764d0a..219b7317343 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -15,4 +15,4 @@ elasticSearchServerVersion=8.16.3
projectType=application
artifactoryUrl=https://repox.jfrog.io/repox
jre_release_name=jdk-17.0.13+11
-webappVersion=2025.4.0.20378
+webappVersion=2025.4.0.20784
diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java
index b0aed6b3f10..d0c0c7c8c3d 100644
--- a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java
+++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java
@@ -62,7 +62,6 @@ import static org.apache.commons.lang3.RandomStringUtils.secure;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -198,7 +197,6 @@ public class LoadPeriodsStepIT extends BaseStepTest {
underTest.execute(new TestComputationStepContext());
assertPeriod(NewCodePeriodType.REFERENCE_BRANCH, newCodeReferenceBranch, null);
- verify(ceTaskMessages).add(any(CeTaskMessages.Message.class));
}
@Test
diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java
new file mode 100644
index 00000000000..0ede18e3ccf
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java
@@ -0,0 +1,112 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.step;
+
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
+import org.sonar.ce.task.projectanalysis.analysis.TestBranch;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.ReportComponent;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
+import org.sonar.ce.task.projectanalysis.period.Period;
+import org.sonar.ce.task.projectanalysis.period.PeriodHolderRule;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
+import org.sonar.ce.task.step.ComputationStep.Context;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ProjectData;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.server.project.Project;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
+
+class PersistReferenceBranchPeriodStepIT {
+
+ private static final String BRANCH_NAME = "feature";
+
+ @RegisterExtension
+ private final PeriodHolderRule periodHolder = new PeriodHolderRule();
+
+ @RegisterExtension
+ private final AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
+
+ @RegisterExtension
+ private final TreeRootHolderRule treeRootHolderRule = new TreeRootHolderRule();
+
+ @RegisterExtension
+ private final DbTester db = DbTester.create();
+
+ private final PersistReferenceBranchPeriodStep persistReferenceBranchPeriodStep = new PersistReferenceBranchPeriodStep(
+ periodHolder, analysisMetadataHolder, db.getDbClient(), treeRootHolderRule);
+
+ private ProjectData projectData;
+ private String branchUuid;
+
+ @BeforeEach
+ void setUp() {
+ projectData = db.components().insertPrivateProject();
+ BranchDto branchDto = db.components().insertProjectBranch(projectData.getProjectDto(), branch -> branch.setKey(BRANCH_NAME));
+ branchUuid = branchDto.getUuid();
+
+ analysisMetadataHolder.setProject(new Project(projectData.projectUuid(), projectData.projectKey(), projectData.projectKey(), null, List.of()));
+ analysisMetadataHolder.setBranch(new TestBranch(BRANCH_NAME));
+ periodHolder.setPeriod(new Period(REFERENCE_BRANCH.name(), "main", null));
+ periodHolder.setPeriodOrigin(PeriodOrigin.SCANNER);
+
+ ReportComponent reportComponent = ReportComponent
+ .builder(Component.Type.PROJECT, 1)
+ .setUuid(branchUuid)
+ .setKey(branchDto.getKey())
+ .build();
+ treeRootHolderRule.setRoot(reportComponent);
+ }
+
+ @Test
+ void execute_shouldPersistReferenceBranchPeriod() {
+
+ persistReferenceBranchPeriodStep.execute(mock(Context.class));
+
+ NewCodePeriodDto newCodePeriodDto = db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), projectData.projectUuid(), branchUuid)
+ .orElseGet(() -> fail("No new code period found for branch"));
+ assertThat(newCodePeriodDto.getBranchUuid()).isEqualTo(branchUuid);
+ assertThat(newCodePeriodDto.getType()).isEqualTo(REFERENCE_BRANCH);
+ assertThat(newCodePeriodDto.getValue()).isEqualTo("main");
+ }
+
+ @Test
+ void execute_shouldUpdateReferenceBranchPeriod() {
+ db.newCodePeriods().insert(projectData.projectUuid(), branchUuid, REFERENCE_BRANCH, "old_value");
+
+ persistReferenceBranchPeriodStep.execute(mock(Context.class));
+
+ NewCodePeriodDto newCodePeriodDto = db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), projectData.projectUuid(), branchUuid)
+ .orElseGet(() -> fail("No new code period found for branch"));
+ assertThat(newCodePeriodDto.getBranchUuid()).isEqualTo(branchUuid);
+ assertThat(newCodePeriodDto.getType()).isEqualTo(REFERENCE_BRANCH);
+ assertThat(newCodePeriodDto.getValue()).isEqualTo("main");
+ }
+
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java
index 1705c9fb3bb..9fe7dadb9f5 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java
@@ -51,4 +51,10 @@ public interface PeriodHolder {
*/
Period getPeriod();
+ /**
+ * Retrieve the context from which this period is coming from. For example, it can be coming from a scanner parameter, a global setting, etc.
+ * See {@link PeriodOrigin} for the possible values.
+ */
+ PeriodOrigin getPeriodOrigin();
+
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java
index 86ea07a18e2..2150f3f5539 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java
@@ -29,6 +29,7 @@ public class PeriodHolderImpl implements PeriodHolder {
@CheckForNull
private Period period = null;
private boolean initialized = false;
+ private PeriodOrigin periodOrigin = null;
/**
* Initializes the periods in the holder.
@@ -41,6 +42,10 @@ public class PeriodHolderImpl implements PeriodHolder {
this.initialized = true;
}
+ public void setPeriodOrigin(PeriodOrigin periodOrigin) {
+ this.periodOrigin = periodOrigin;
+ }
+
@Override
public boolean hasPeriod() {
checkHolderIsInitialized();
@@ -60,6 +65,11 @@ public class PeriodHolderImpl implements PeriodHolder {
return period;
}
+ @Override
+ public PeriodOrigin getPeriodOrigin() {
+ return periodOrigin;
+ }
+
private void checkHolderIsInitialized() {
checkState(initialized, "Period have not been initialized yet");
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java
new file mode 100644
index 00000000000..6e2847fe480
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.period;
+
+public enum PeriodOrigin {
+ SCANNER,
+ SETTINGS
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
index 8d9026c42af..45831d3bc6f 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
@@ -22,13 +22,13 @@ package org.sonar.ce.task.projectanalysis.step;
import java.util.Optional;
import org.sonar.api.utils.System2;
import org.sonar.ce.task.log.CeTaskMessages;
-import org.sonar.ce.task.log.CeTaskMessages.Message;
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
import org.sonar.ce.task.projectanalysis.period.NewCodePeriodResolver;
import org.sonar.ce.task.projectanalysis.period.Period;
import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
import org.sonar.ce.task.projectanalysis.period.PeriodHolderImpl;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
@@ -89,6 +89,8 @@ public class LoadPeriodsStep implements ComputationStep {
.map(b -> new NewCodePeriodDto().setType(REFERENCE_BRANCH).setValue(b))
.orElse(null);
+ PeriodOrigin periodOrigin = newCodePeriod == null ? PeriodOrigin.SETTINGS : PeriodOrigin.SCANNER;
+
try (DbSession dbSession = dbClient.openSession(false)) {
Optional<NewCodePeriodDto> branchSpecificSetting = getBranchSetting(dbSession, projectUuid, branchUuid);
@@ -102,13 +104,11 @@ public class LoadPeriodsStep implements ComputationStep {
periodsHolder.setPeriod(null);
return;
}
- } else if (branchSpecificSetting.isPresent()) {
- ceTaskMessages.add(new Message("A scanner parameter is defining a new code reference branch, but this conflicts with the New Code Period"
- + " setting of your branch. Please check your project configuration. You should use either one or the other but not both.", system2.now()));
}
Period period = resolver.resolve(dbSession, branchUuid, newCodePeriod, projectVersion);
periodsHolder.setPeriod(period);
+ periodsHolder.setPeriodOrigin(periodOrigin);
}
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java
new file mode 100644
index 00000000000..96f471946ae
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.step;
+
+import java.util.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+
+public class PersistReferenceBranchPeriodStep implements ComputationStep {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PersistReferenceBranchPeriodStep.class);
+
+ private final PeriodHolder periodHolder;
+ private final AnalysisMetadataHolder analysisMetadataHolder;
+ private final DbClient dbClient;
+ private final TreeRootHolder treeRootHolder;
+
+ public PersistReferenceBranchPeriodStep(PeriodHolder periodHolder, AnalysisMetadataHolder analysisMetadataHolder, DbClient dbClient, TreeRootHolder treeRootHolder) {
+ this.periodHolder = periodHolder;
+ this.analysisMetadataHolder = analysisMetadataHolder;
+ this.dbClient = dbClient;
+ this.treeRootHolder = treeRootHolder;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Persist or update reference branch new code period";
+ }
+
+ @Override
+ public void execute(Context context) {
+ if (shouldExecute()) {
+ executePersistPeriodStep();
+ }
+ }
+
+ private boolean shouldExecute() {
+ return analysisMetadataHolder.isBranch() && periodHolder.hasPeriod()
+ && periodHolder.getPeriodOrigin() == PeriodOrigin.SCANNER;
+ }
+
+ void executePersistPeriodStep() {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ String projectUuid = analysisMetadataHolder.getProject().getUuid();
+ String branchUuid = treeRootHolder.getRoot().getUuid();
+
+ dbClient.newCodePeriodDao()
+ .selectByBranch(dbSession, projectUuid, branchUuid)
+ .ifPresentOrElse(
+ existingNewCodePeriod -> updateNewCodePeriodIfNeeded(dbSession, existingNewCodePeriod),
+ () -> createNewCodePeriod(dbSession, branchUuid)
+ );
+ }
+ }
+
+ private void updateNewCodePeriodIfNeeded(DbSession dbSession, NewCodePeriodDto newCodePeriodDto) {
+ if (shouldUpdateNewCodePeriod(newCodePeriodDto)) {
+ LOGGER.debug("Updating reference branch new code period '{}' for project '{}' and branch '{}'",
+ periodHolder.getPeriod().getModeParameter(), analysisMetadataHolder.getProject().getName(), analysisMetadataHolder.getBranch().getName());
+ newCodePeriodDto.setValue(periodHolder.getPeriod().getModeParameter());
+ newCodePeriodDto.setType(NewCodePeriodType.REFERENCE_BRANCH);
+ dbClient.newCodePeriodDao().update(dbSession, newCodePeriodDto);
+ dbSession.commit();
+ }
+ }
+
+ private boolean shouldUpdateNewCodePeriod(NewCodePeriodDto existingNewCodePeriod) {
+ return existingNewCodePeriod.getType() != NewCodePeriodType.REFERENCE_BRANCH
+ || !Objects.equals(existingNewCodePeriod.getValue(), periodHolder.getPeriod().getModeParameter());
+ }
+
+ private void createNewCodePeriod(DbSession dbSession, String branchUuid) {
+ LOGGER.debug("Persisting reference branch new code period '{}' for project '{}' and branch '{}'",
+ periodHolder.getPeriod().getModeParameter(), analysisMetadataHolder.getProject().getName(), analysisMetadataHolder.getBranch().getName());
+ dbClient.newCodePeriodDao().insert(dbSession, buildNewCodePeriodDto(branchUuid));
+ dbSession.commit();
+ }
+
+ private NewCodePeriodDto buildNewCodePeriodDto(String branchUuid) {
+ return new NewCodePeriodDto()
+ .setProjectUuid(analysisMetadataHolder.getProject().getUuid())
+ .setBranchUuid(branchUuid)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(periodHolder.getPeriod().getModeParameter());
+ }
+
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
index 8cbcbbf421a..b121df6e657 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
@@ -106,6 +106,7 @@ public class ReportComputationSteps extends AbstractComputationSteps {
// Persist data
PersistScannerAnalysisCacheStep.class,
PersistComponentsStep.class,
+ PersistReferenceBranchPeriodStep.class,
PersistAnalysisStep.class,
PersistAnalysisPropertiesStep.class,
PersistProjectMeasuresStep.class,
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java
index f47424267a9..7c9b454e8c2 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java
@@ -73,4 +73,14 @@ public class PeriodHolderRule implements TestRule, PeriodHolder, AfterEachCallba
return delegate.getPeriod();
}
+ @Override
+ public PeriodOrigin getPeriodOrigin() {
+ return delegate.getPeriodOrigin();
+ }
+
+ public PeriodHolderRule setPeriodOrigin(PeriodOrigin periodOrigin) {
+ delegate.setPeriodOrigin(periodOrigin);
+ return this;
+ }
+
}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java
new file mode 100644
index 00000000000..84c3cc6d42d
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java
@@ -0,0 +1,194 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.step;
+
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.ArgumentCaptor;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.analysis.TestBranch;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.period.Period;
+import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.newcodeperiod.NewCodePeriodDao;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+import org.sonar.server.project.Project;
+
+import static java.util.Collections.emptyList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.slf4j.event.Level.DEBUG;
+
+class PersistReferenceBranchPeriodStepTest {
+
+ private static final String MAIN_BRANCH = "main";
+ private static final String FEATURE_BRANCH = "feature";
+ private static final String BRANCH_UUID = "branch-uuid";
+ private static final String PROJECT_NAME = "project-name";
+ private static final String PROJECT_UUID = "project-uuid";
+
+ private final PeriodHolder periodHolder = mock(PeriodHolder.class);
+
+ private final AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class);
+
+ private final DbClient dbClient = mock(DbClient.class);
+
+ private final TreeRootHolder treeRootHolder = mock(TreeRootHolder.class);
+
+ @RegisterExtension
+ private final LogTesterJUnit5 logs = new LogTesterJUnit5().setLevel(Level.DEBUG);
+
+ private final PersistReferenceBranchPeriodStep persistReferenceBranchPeriodStep = new PersistReferenceBranchPeriodStep(
+ periodHolder, analysisMetadataHolder, dbClient, treeRootHolder);
+
+ private final DbSession dbSession = mock(DbSession.class);
+ private final NewCodePeriodDao newCodePeriodeDao = mock(NewCodePeriodDao.class);
+
+ private final ComputationStep.Context context = mock(ComputationStep.Context.class);
+
+ @BeforeEach
+ void setUp() {
+ Project project = new Project(PROJECT_UUID, "project-key", PROJECT_NAME, "project-description", emptyList());
+ when(analysisMetadataHolder.isBranch()).thenReturn(true);
+ when(analysisMetadataHolder.getProject()).thenReturn(project);
+ when(analysisMetadataHolder.getBranch()).thenReturn(new TestBranch(FEATURE_BRANCH));
+
+ when(periodHolder.hasPeriod()).thenReturn(true);
+ Period period = new Period(NewCodePeriodType.REFERENCE_BRANCH.name(), MAIN_BRANCH, null);
+ when(periodHolder.getPeriod()).thenReturn(period);
+ when(periodHolder.getPeriodOrigin()).thenReturn(PeriodOrigin.SCANNER);
+
+ when(dbClient.openSession(false)).thenReturn(dbSession);
+ when(dbClient.newCodePeriodDao()).thenReturn(newCodePeriodeDao);
+
+ Component root = mock(Component.class);
+ when(treeRootHolder.getRoot()).thenReturn(root);
+ when(root.getUuid()).thenReturn(BRANCH_UUID);
+
+ }
+
+ @Test
+ void getDescription() {
+ assertThat(persistReferenceBranchPeriodStep.getDescription()).isEqualTo("Persist or update reference branch new code period");
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenNotABranch() {
+ when(analysisMetadataHolder.isBranch()).thenReturn(false);
+ verifyExecuteNotCalled();
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenNoPeriods() {
+ when(periodHolder.hasPeriod()).thenReturn(false);
+ verifyExecuteNotCalled();
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenNotReferenceBranchPeriod() {
+ Period period = new Period("not-ref-branch", MAIN_BRANCH, null);
+ when(periodHolder.getPeriod()).thenReturn(period);
+ when(periodHolder.getPeriodOrigin()).thenReturn(PeriodOrigin.SETTINGS);
+ verifyExecuteNotCalled();
+ }
+
+ private void verifyExecuteNotCalled() {
+ PersistReferenceBranchPeriodStep spyStep = spy(persistReferenceBranchPeriodStep);
+
+ spyStep.execute(context);
+
+ verify(spyStep, never()).executePersistPeriodStep();
+ }
+
+ @Test
+ void execute_shouldCreateNewCodePeriod_whenItDoesNotExists() {
+ NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(MAIN_BRANCH);
+ when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.empty());
+
+ persistReferenceBranchPeriodStep.execute(context);
+
+ assertThat(logs.logs(DEBUG)).contains(
+ String.format("Persisting reference branch new code period '%s' for project '%s' and branch '%s'",MAIN_BRANCH, PROJECT_NAME, FEATURE_BRANCH));
+ ArgumentCaptor<NewCodePeriodDto> newCodePeriodCaptor = ArgumentCaptor.forClass(NewCodePeriodDto.class);
+ verify(newCodePeriodeDao).insert(eq(dbSession), newCodePeriodCaptor.capture());
+ assertThat(newCodePeriodCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedNewCodePeriod);
+ }
+
+ @Test
+ void execute_shouldUpdateNewCodePeriod_whenItExistsAndItChanged() {
+ NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(MAIN_BRANCH);
+ var newCodePeriodInBase = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue("old-value");
+
+ when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.of(newCodePeriodInBase));
+
+ persistReferenceBranchPeriodStep.execute(context);
+
+ assertThat(logs.logs(DEBUG)).contains(
+ String.format("Updating reference branch new code period '%s' for project '%s' and branch '%s'", MAIN_BRANCH ,PROJECT_NAME, FEATURE_BRANCH));
+ ArgumentCaptor<NewCodePeriodDto> newCodePeriodCaptor = ArgumentCaptor.forClass(NewCodePeriodDto.class);
+ verify(newCodePeriodeDao).update(eq(dbSession), newCodePeriodCaptor.capture());
+ assertThat(newCodePeriodCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedNewCodePeriod);
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenItExistsAndItDidNotChanged() {
+ NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(MAIN_BRANCH);
+
+ when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.of(expectedNewCodePeriod));
+
+ persistReferenceBranchPeriodStep.execute(context);
+
+ verify(newCodePeriodeDao, never()).update(any(), any());
+ verify(newCodePeriodeDao, never()).insert(any(), any());
+ }
+
+}
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java
index b781c2ad344..f468306e8f8 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java
@@ -1994,7 +1994,8 @@ oldCreationDate));
// the issue uuids here don't even exist but doesn't matter, we don't delete issues so not testing that
var issueReleaseBase = Map.of("created_at", 0L, "updated_at", 0L,
- "severity", "INFO", "severity_sort_key", 42, "status", "TO_REVIEW");
+ "severity", "INFO", "original_severity", "INFO", "manual_severity", false,
+ "severity_sort_key", 42, "status", "TO_REVIEW");
db.executeInsert("sca_issues_releases", merge(issueReleaseBase, Map.of("uuid", "issue-release-uuid1",
"sca_issue_uuid", "issue-uuid1", "sca_release_uuid", "release-uuid1")));
db.executeInsert("sca_issues_releases", merge(issueReleaseBase, Map.of("uuid", "issue-release-uuid2",
diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl
index 9001f44d56f..1718ab4dddd 100644
--- a/server/sonar-db-dao/src/schema/schema-sq.ddl
+++ b/server/sonar-db-dao/src/schema/schema-sq.ddl
@@ -1118,7 +1118,9 @@ CREATE TABLE "SCA_ISSUES_RELEASES"(
"UPDATED_AT" BIGINT NOT NULL,
"STATUS" CHARACTER VARYING(40) NOT NULL,
"ASSIGNEE_UUID" CHARACTER VARYING(40),
- "PREVIOUS_MANUAL_STATUS" CHARACTER VARYING(40)
+ "PREVIOUS_MANUAL_STATUS" CHARACTER VARYING(40),
+ "ORIGINAL_SEVERITY" CHARACTER VARYING(15) NOT NULL,
+ "MANUAL_SEVERITY" CHARACTER VARYING(15)
);
ALTER TABLE "SCA_ISSUES_RELEASES" ADD CONSTRAINT "PK_SCA_ISSUES_RELEASES" PRIMARY KEY("UUID");
CREATE INDEX "SCA_ISSUES_RELEASES_SCA_ISSUE" ON "SCA_ISSUES_RELEASES"("SCA_ISSUE_UUID" NULLS FIRST);
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java
new file mode 100644
index 00000000000..0fdbda384a9
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.MANUALLY_SET_COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.ORIGINAL_VALUE_COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.TABLE_NAME;
+
+class AddOriginalAndManualSeverityToScaIssuesTest {
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddOriginalAndManualSeverityToScaIssues.class);
+ private final DdlChange underTest = new AddOriginalAndManualSeverityToScaIssues(db.database());
+
+ @Test
+ void execute_shouldAddCalculatedValueColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME, VARCHAR, 15, true);
+ }
+
+ @Test
+ void execute_shouldAddManuallySetColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, MANUALLY_SET_COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, MANUALLY_SET_COLUMN_NAME, VARCHAR, 15, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME);
+ db.assertColumnDoesNotExist(TABLE_NAME, MANUALLY_SET_COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME, VARCHAR, 15, true);
+ db.assertColumnDefinition(TABLE_NAME, MANUALLY_SET_COLUMN_NAME, VARCHAR, 15, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java
new file mode 100644
index 00000000000..31091c17fc1
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PopulateOriginalSeverityForScaIssuesReleasesTableIT {
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(PopulateOriginalSeverityForScaIssuesReleasesTable.class);
+ private final PopulateOriginalSeverityForScaIssuesReleasesTable underTest = new PopulateOriginalSeverityForScaIssuesReleasesTable(db.database());
+
+ @Test
+ void execute_shouldPopulateOriginaldSeverity() throws SQLException {
+ insertScaIssuesReleases(1, "HIGH", null);
+ insertScaIssuesReleases(2, "INFO", null);
+
+ underTest.execute();
+
+ assertThatOriginalSeverityIs(1, "HIGH");
+ assertThatOriginalSeverityIs(2, "INFO");
+ }
+
+ @Test
+ void execute_whenAlreadyExecuted_shouldBeIdempotent() throws SQLException {
+ insertScaIssuesReleases(1, "HIGH", "INFO");
+
+ underTest.execute();
+ underTest.execute();
+
+ assertThatOriginalSeverityIs(1, "INFO");
+ }
+
+ private void insertScaIssuesReleases(Integer index, String severity, @Nullable String originalSeverity) {
+ db.executeInsert("sca_issues_releases",
+ "uuid", "uuid-" + index,
+ "sca_issue_uuid", "issue_id" + index,
+ "sca_release_uuid", "release_id",
+ "status", "TO_REVIEW",
+ "severity", severity,
+ "original_severity", originalSeverity,
+ "manual_severity", null,
+ "severity_sort_key", 1,
+ "created_at", new Date().getTime(),
+ "updated_at", new Date().getTime()
+ );
+ }
+
+ private void assertThatOriginalSeverityIs(Integer index, String expectedSeverity) {
+ String uuid = "uuid-" + index;
+ List<Map<String, Object>> rows = db.select("select original_severity from sca_issues_releases where uuid = '%s'".formatted(uuid));
+ assertThat(rows).isNotEmpty()
+ .allSatisfy(row -> assertThat(row).containsEntry("original_severity", expectedSeverity));
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java
new file mode 100644
index 00000000000..2e194f52ec6
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.sql.DropColumnsBuilder;
+
+import static java.sql.Types.VARCHAR;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+class UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "original_severity";
+ static final int COLUMN_SIZE = 15;
+
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.class);
+
+ private final UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable underTest = new UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable(db.database());
+
+ @Test
+ void execute_whenColumnExists_shouldMakeColumnNotNull() throws SQLException {
+ // Verify column is nullable before update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, true);
+
+ underTest.execute();
+
+ // Verify column is not nullable after update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, false);
+ }
+
+ @Test
+ void execute_whenColumnDoesNotExist_shouldNotFail() throws SQLException {
+ // Ensure the column does not exist before executing the migration
+ DropColumnsBuilder dropColumnsBuilder = new DropColumnsBuilder(db.database().getDialect(), TABLE_NAME, COLUMN_NAME);
+ dropColumnsBuilder.build().forEach(db::executeDdl);
+
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ }
+
+ @Test
+ void execute_whenExecutedTwice_shouldBeIdempotent() throws SQLException {
+ underTest.execute();
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java
new file mode 100644
index 00000000000..66c4c27254e
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddOriginalAndManualSeverityToScaIssues extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String ORIGINAL_VALUE_COLUMN_NAME = "original_severity";
+ static final String MANUALLY_SET_COLUMN_NAME = "manual_severity";
+
+ public AddOriginalAndManualSeverityToScaIssues(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(ORIGINAL_VALUE_COLUMN_NAME)
+ .setLimit(15)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+
+ if (!tableColumnExists(connection, TABLE_NAME, MANUALLY_SET_COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(MANUALLY_SET_COLUMN_NAME)
+ .setLimit(15)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java
index ac608e306a5..97a956b6ca7 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java
@@ -33,6 +33,11 @@ public class DbVersion202504 implements DbVersion {
.add(2025_04_001, "Add 'organization_uuid' column to 'sca_license_profiles' table", AddOrganizationUuidToScaLicenseProfiles.class)
.add(2025_04_002, "Drop unique index from 'sca_license_profiles'", DropUniqueIndexOnScaLicenseProfiles.class)
.add(2025_04_003, "Create unique index from 'sca_license_profiles'", CreateUniqueIndexOnScaLicenseProfiles.class)
- .add(2025_04_004, "Add index on 'alm_repo' column in 'project_alm_settings' table", AddIndexOnAlmRepoInProjectAlmSettings.class);
+ .add(2025_04_004, "Add index on 'alm_repo' column in 'project_alm_settings' table", AddIndexOnAlmRepoInProjectAlmSettings.class)
+ .add(2025_04_005, "Add 'original_severity' and 'manual_severity' columns to 'sca_issues_releases' table", AddOriginalAndManualSeverityToScaIssues.class)
+ .add(2025_04_006, "Populate 'original_severity' column for 'sca_issues_releases' table", PopulateOriginalSeverityForScaIssuesReleasesTable.class)
+ .add(2025_04_007, "Update 'original_severity' column to be not nullable in 'sca_issues_releases' table", UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.class)
+
+ ;
}
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java
new file mode 100644
index 00000000000..bcee5bb0903
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class PopulateOriginalSeverityForScaIssuesReleasesTable extends DataChange {
+ private static final String SELECT_QUERY = "select severity, uuid from sca_issues_releases where original_severity is null";
+ private static final String UPDATE_QUERY = "update sca_issues_releases set original_severity = ? where uuid = ?";
+
+ public PopulateOriginalSeverityForScaIssuesReleasesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select(SELECT_QUERY);
+ massUpdate.update(UPDATE_QUERY);
+
+ massUpdate.execute((row, update, index) -> {
+ update
+ // Set original_severity from severity
+ .setString(1, row.getString(1))
+ // Set uuid from uuid
+ .setString(2, row.getString(2));
+ return true;
+ });
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java
new file mode 100644
index 00000000000..4c842e81e1f
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "original_severity";
+
+ public UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(false)
+ .setLimit(15)
+ .build();
+
+ context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME)
+ .updateColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}