Repository: ls1intum/artemis
Issue: `Programming exercises`: Missing build plan when importing programming exercises

### Describe the bug

After importing a programming exercise, the build plan is missing. No inputs can be made, and the wheel in the top bar keeps on spinning.
The re-create build plan option also doesn't work.

### To Reproduce

1. Import a programming exercise from instance (meaning from another course, for example)
2. Click on 'Edit Build Plan'
3. The editor is empty and no inputs can be made


### Expected behavior

I expect the import to also include the build plan.

### Screenshots

![bpe](https://github.com/ls1intum/Artemis/assets/53149143/d08e9873-6148-4636-89fd-3f087c5973cd)


### Which version of Artemis are you seeing the problem on?

6.3.8

### What browsers are you seeing the problem on?

Chrome

### Additional context

_No response_

### Relevant log output

_No response_

Changed files:
src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java
src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java
src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java
src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java
src/test/java/de/tum/in/www1/artemis/service/GitlabCIServiceTest.java
src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java
src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java
src/test/java/de/tum/in/www1/artemis/service/JenkinsServiceTest.java
src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java
src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooService.java
src/main/java/de/tum/in/www1/artemis/repository/BuildPlanRepository.java

diff --git a/src/main/java/de/tum/in/www1/artemis/repository/BuildPlanRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/BuildPlanRepository.java
index 9d6113fb08..ad98dcf17f 100644
--- a/src/main/java/de/tum/in/www1/artemis/repository/BuildPlanRepository.java
+++ b/src/main/java/de/tum/in/www1/artemis/repository/BuildPlanRepository.java
@@ -44,4 +44,9 @@ public interface BuildPlanRepository extends JpaRepository<BuildPlan, Long> {
         buildPlanWrapper.addProgrammingExercise(exercise);
         return save(buildPlanWrapper);
     }
+
+    default void copyBetweenExercises(ProgrammingExercise sourceExercise, ProgrammingExercise targetExercise) {
+        BuildPlan buildPlan = findByProgrammingExercises_IdWithProgrammingExercisesElseThrow(sourceExercise.getId());
+        setBuildPlanForExercise(buildPlan.getBuildPlan(), targetExercise);
+    }
 }
diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java
index 90cb01efe3..4e60f3f109 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java
@@ -449,14 +449,14 @@ public class ParticipationService {
     private ProgrammingExerciseStudentParticipation copyBuildPlan(ProgrammingExerciseStudentParticipation participation) {
         // only execute this step if it has not yet been completed yet or if the build plan id is missing for some reason
         if (!participation.getInitializationState().hasCompletedState(InitializationState.BUILD_PLAN_COPIED) || participation.getBuildPlanId() == null) {
-            final var projectKey = participation.getProgrammingExercise().getProjectKey();
+            final var exercise = participation.getProgrammingExercise();
             final var planName = BuildPlanType.TEMPLATE.getName();
             final var username = participation.getParticipantIdentifier();
             final var buildProjectName = participation.getExercise().getCourseViaExerciseGroupOrCourseMember().getShortName().toUpperCase() + " "
                     + participation.getExercise().getTitle();
             final var targetPlanName = participation.addPracticePrefixIfTestRun(username.toUpperCase());
             // the next action includes recovery, which means if the build plan has already been copied, we simply retrieve the build plan id and do not copy it again
-            final var buildPlanId = continuousIntegrationService.orElseThrow().copyBuildPlan(projectKey, planName, projectKey, buildProjectName, targetPlanName, true);
+            final var buildPlanId = continuousIntegrationService.orElseThrow().copyBuildPlan(exercise, planName, exercise, buildProjectName, targetPlanName, true);
             participation.setBuildPlanId(buildPlanId);
             participation.setInitializationState(InitializationState.BUILD_PLAN_COPIED);
             return programmingExerciseStudentParticipationRepository.saveAndFlush(participation);
diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooService.java
index f5c5628d28..24386d0334 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooService.java
@@ -352,8 +352,11 @@ public class BambooService extends AbstractContinuousIntegrationService {
     }
 
     @Override
-    public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
+    public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
             boolean targetProjectExists) {
+        String sourceProjectKey = sourceExercise.getProjectKey();
+        String targetProjectKey = targetExercise.getProjectKey();
+
         final var cleanPlanName = getCleanPlanName(targetPlanName);
         final var sourcePlanKey = sourceProjectKey + "-" + sourcePlanName;
         final var targetPlanKey = targetProjectKey + "-" + cleanPlanName;
diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java
index d87948fb51..21d0b82ed6 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java
@@ -53,15 +53,16 @@ public interface ContinuousIntegrationService {
     /**
      * Clones an existing build plan. Illegal characters in the plan key, or name will be replaced.
      *
-     * @param sourceProjectKey    The key of the source project, normally the key of the exercise -> courseShortName + exerciseShortName.
+     * @param sourceExercise      The exercise from which the build plan should be copied
      * @param sourcePlanName      The name of the source plan
-     * @param targetProjectKey    The key of the project the plan should get copied to
+     * @param targetExercise      The exercise to which the build plan is copied to
      * @param targetProjectName   The wanted name of the new project
      * @param targetPlanName      The wanted name of the new plan after copying it
      * @param targetProjectExists whether the target project already exists or not
      * @return The key of the new build plan
      */
-    String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName, boolean targetProjectExists);
+    String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
+            boolean targetProjectExists);
 
     /**
      * Configure the build plan with the given participation on the CI system. Common configurations: - update the repository in the build plan - set appropriate user permissions -
diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java
index 13170b8628..536f49092b 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java
@@ -193,14 +193,14 @@ public class GitLabCIService extends AbstractContinuousIntegrationService {
     }
 
     @Override
-    public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
+    public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
             boolean targetProjectExists) {
         // In GitLab CI we don't have to copy the build plan.
         // Instead, we configure a CI config path leading to the API when enabling the CI.
 
         // When sending the build results back, the build plan key is used to identify the participation.
         // Therefore, we return the key here even though GitLab CI does not need it.
-        return targetProjectKey + "-" + targetPlanName.toUpperCase().replaceAll("[^A-Z0-9]", "");
+        return targetExercise.getProjectKey() + "-" + targetPlanName.toUpperCase().replaceAll("[^A-Z0-9]", "");
     }
 
     @Override
diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java
index 7a23093332..243e19c584 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java
@@ -98,9 +98,9 @@ public class JenkinsService extends AbstractContinuousIntegrationService {
     }
 
     @Override
-    public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
+    public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
             boolean targetProjectExists) {
-        return jenkinsBuildPlanService.copyBuildPlan(sourceProjectKey, sourcePlanName, targetProjectKey, targetPlanName);
+        return jenkinsBuildPlanService.copyBuildPlan(sourceExercise, sourcePlanName, targetExercise, targetPlanName);
     }
 
     @Override
diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java
index 0a94f3a2ab..f756160991 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java
@@ -37,6 +37,7 @@ import de.tum.in.www1.artemis.domain.enumeration.ProjectType;
 import de.tum.in.www1.artemis.domain.enumeration.RepositoryType;
 import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation;
 import de.tum.in.www1.artemis.exception.JenkinsException;
+import de.tum.in.www1.artemis.repository.BuildPlanRepository;
 import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository;
 import de.tum.in.www1.artemis.repository.UserRepository;
 import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService;
@@ -78,9 +79,12 @@ public class JenkinsBuildPlanService {
 
     private final ProgrammingExerciseRepository programmingExerciseRepository;
 
+    private final BuildPlanRepository buildPlanRepository;
+
     public JenkinsBuildPlanService(@Qualifier("jenkinsRestTemplate") RestTemplate restTemplate, JenkinsServer jenkinsServer, JenkinsBuildPlanCreator jenkinsBuildPlanCreator,
             JenkinsJobService jenkinsJobService, JenkinsJobPermissionsService jenkinsJobPermissionsService, JenkinsInternalUrlService jenkinsInternalUrlService,
-            UserRepository userRepository, ProgrammingExerciseRepository programmingExerciseRepository, JenkinsPipelineScriptCreator jenkinsPipelineScriptCreator) {
+            UserRepository userRepository, ProgrammingExerciseRepository programmingExerciseRepository, JenkinsPipelineScriptCreator jenkinsPipelineScriptCreator,
+            BuildPlanRepository buildPlanRepository) {
         this.restTemplate = restTemplate;
         this.jenkinsServer = jenkinsServer;
         this.jenkinsBuildPlanCreator = jenkinsBuildPlanCreator;
@@ -90,6 +94,7 @@ public class JenkinsBuildPlanService {
         this.programmingExerciseRepository = programmingExerciseRepository;
         this.jenkinsInternalUrlService = jenkinsInternalUrlService;
         this.jenkinsPipelineScriptCreator = jenkinsPipelineScriptCreator;
+        this.buildPlanRepository = buildPlanRepository;
     }
 
     /**
@@ -233,13 +238,20 @@ public class JenkinsBuildPlanService {
     /**
      * Copies a build plan to another and replaces the old reference to the master and main branch with a reference to the default branch
      *
-     * @param sourceProjectKey the source project key
-     * @param sourcePlanName   the source plan name
-     * @param targetProjectKey the target project key
-     * @param targetPlanName   the target plan name
+     * @param sourceExercise the source exercise
+     * @param sourcePlanName the source plan name
+     * @param targetExercise the target exercise
+     * @param targetPlanName the target plan name
      * @return the key of the created build plan
      */
-    public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetPlanName) {
+    public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetPlanName) {
+        buildPlanRepository.copyBetweenExercises(sourceExercise, targetExercise);
+        targetExercise.generateAndSetBuildPlanAccessSecret();
+        targetExercise = programmingExerciseRepository.save(targetExercise);
+
+        String sourceProjectKey = sourceExercise.getProjectKey();
+        String targetProjectKey = targetExercise.getProjectKey();
+
         final var cleanTargetName = getCleanPlanName(targetPlanName);
         final var sourcePlanKey = sourceProjectKey + "-" + sourcePlanName;
         final var targetPlanKey = targetProjectKey + "-" + cleanTargetName;
@@ -362,7 +374,7 @@ public class JenkinsBuildPlanService {
     /**
      * Assigns access permissions to instructors and TAs for the specified build plan.
      * This is done by getting all users that belong to the instructor and TA groups of
-     * the exercise' course and adding permissions to the Jenkins job.
+     * the exercises' course and adding permissions to the Jenkins job.
      *
      * @param programmingExercise the programming exercise
      * @param planName            the name of the build plan
diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java
index ed969ec8f0..23579184d3 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java
@@ -110,11 +110,11 @@ public class LocalCIService extends AbstractContinuousIntegrationService {
     }
 
     @Override
-    public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
+    public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
             boolean targetProjectExists) {
         // No build plans exist for local CI. Only return a plan name.
         final String cleanPlanName = getCleanPlanName(targetPlanName);
-        return targetProjectKey + "-" + cleanPlanName;
+        return targetExercise.getProjectKey() + "-" + cleanPlanName;
     }
 
     @Override
diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java
index 317e591956..c9c0eafc4d 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java
@@ -200,8 +200,8 @@ public class ProgrammingExerciseImportService {
         final var targetKey = newExercise.getProjectKey();
         final var targetName = newExercise.getCourseViaExerciseGroupOrCourseMember().getShortName().toUpperCase() + " " + newExercise.getTitle();
         continuousIntegrationService.orElseThrow().createProjectForExercise(newExercise);
-        continuousIntegrationService.get().copyBuildPlan(templateKey, templatePlanName, targetKey, targetName, templatePlanName, false);
-        continuousIntegrationService.get().copyBuildPlan(templateKey, solutionPlanName, targetKey, targetName, solutionPlanName, true);
+        continuousIntegrationService.get().copyBuildPlan(templateExercise, templatePlanName, newExercise, targetName, templatePlanName, false);
+        continuousIntegrationService.get().copyBuildPlan(templateExercise, solutionPlanName, newExercise, targetName, solutionPlanName, true);
         continuousIntegrationService.get().givePlanPermissions(newExercise, templatePlanName);
         continuousIntegrationService.get().givePlanPermissions(newExercise, solutionPlanName);
         programmingExerciseService.giveCIProjectPermissions(newExercise);
diff --git a/src/test/java/de/tum/in/www1/artemis/service/GitlabCIServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/GitlabCIServiceTest.java
index e9360f48a2..228ae25390 100644
--- a/src/test/java/de/tum/in/www1/artemis/service/GitlabCIServiceTest.java
+++ b/src/test/java/de/tum/in/www1/artemis/service/GitlabCIServiceTest.java
@@ -22,6 +22,7 @@ import org.springframework.security.test.context.support.WithMockUser;
 
 import de.tum.in.www1.artemis.AbstractSpringIntegrationGitlabCIGitlabSamlTest;
 import de.tum.in.www1.artemis.domain.BuildLogEntry;
+import de.tum.in.www1.artemis.domain.Course;
 import de.tum.in.www1.artemis.domain.ProgrammingExercise;
 import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage;
 import de.tum.in.www1.artemis.domain.enumeration.ProjectType;
@@ -241,15 +242,20 @@ class GitlabCIServiceTest extends AbstractSpringIntegrationGitlabCIGitlabSamlTes
     @Test
     @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
     void testCopyBuildPlan() {
-        final String targetProjectKey = "TARGETPROJECTKEY";
+        final Course course = new Course();
+        final ProgrammingExercise targetExercise = new ProgrammingExercise();
+        course.addExercises(targetExercise);
+        targetExercise.generateAndSetProjectKey();
+
+        final String targetProjectKey = targetExercise.getProjectKey();
         final String targetPlanName1 = "TARGETPLANNAME1";
         final String targetPlanName2 = "target-plan-name-#2";
 
-        final String expectedBuildPlanKey1 = "TARGETPROJECTKEY-TARGETPLANNAME1";
-        final String expectedBuildPlanKey2 = "TARGETPROJECTKEY-TARGETPLANNAME2";
+        final String expectedBuildPlanKey1 = targetProjectKey + "-TARGETPLANNAME1";
+        final String expectedBuildPlanKey2 = targetProjectKey + "-TARGETPLANNAME2";
 
-        assertThat(continuousIntegrationService.copyBuildPlan(null, null, targetProjectKey, null, targetPlanName1, false)).isEqualTo(expectedBuildPlanKey1);
-        assertThat(continuousIntegrationService.copyBuildPlan(null, null, targetProjectKey, null, targetPlanName2, false)).isEqualTo(expectedBuildPlanKey2);
+        assertThat(continuousIntegrationService.copyBuildPlan(null, null, targetExercise, null, targetPlanName1, false)).isEqualTo(expectedBuildPlanKey1);
+        assertThat(continuousIntegrationService.copyBuildPlan(null, null, targetExercise, null, targetPlanName2, false)).isEqualTo(expectedBuildPlanKey2);
     }
 
     @Test
diff --git a/src/test/java/de/tum/in/www1/artemis/service/JenkinsServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/JenkinsServiceTest.java
index 7501e70093..ba35d12081 100644
--- a/src/test/java/de/tum/in/www1/artemis/service/JenkinsServiceTest.java
+++ b/src/test/java/de/tum/in/www1/artemis/service/JenkinsServiceTest.java
@@ -25,11 +25,15 @@ import org.springframework.util.StreamUtils;
 import com.offbytwo.jenkins.model.JobWithDetails;
 
 import de.tum.in.www1.artemis.AbstractSpringIntegrationJenkinsGitlabTest;
+import de.tum.in.www1.artemis.course.CourseUtilService;
+import de.tum.in.www1.artemis.domain.BuildPlan;
+import de.tum.in.www1.artemis.domain.ProgrammingExercise;
 import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage;
 import de.tum.in.www1.artemis.exception.JenkinsException;
 import de.tum.in.www1.artemis.exercise.programmingexercise.ContinuousIntegrationTestService;
 import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService;
 import de.tum.in.www1.artemis.participation.ParticipationUtilService;
+import de.tum.in.www1.artemis.repository.BuildPlanRepository;
 import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository;
 import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseImportService;
 
@@ -52,6 +56,12 @@ class JenkinsServiceTest extends AbstractSpringIntegrationJenkinsGitlabTest {
     @Autowired
     private ParticipationUtilService participationUtilService;
 
+    @Autowired
+    private CourseUtilService courseUtilService;
+
+    @Autowired
+    private BuildPlanRepository buildPlanRepository;
+
     /**
      * This method initializes the test case by setting up a local repo
      */
@@ -256,4 +266,29 @@ class JenkinsServiceTest extends AbstractSpringIntegrationJenkinsGitlabTest {
                     List.of());
         }).withMessageStartingWith("Error trying to configure build plan in Jenkins");
     }
+
+    @Test
+    @WithMockUser(roles = "INSTRUCTOR", username = TEST_PREFIX + "instructor1")
+    void testCopyBuildPlan() throws IOException {
+        var course = courseUtilService.addEmptyCourse();
+
+        ProgrammingExercise sourceExercise = new ProgrammingExercise();
+        course.addExercises(sourceExercise);
+        sourceExercise.generateAndSetProjectKey();
+        sourceExercise = programmingExerciseRepository.save(sourceExercise);
+        String buildPlanContent = "this is just to test if the build plan is copied correctly";
+        buildPlanRepository.setBuildPlanForExercise(buildPlanContent, sourceExercise);
+
+        ProgrammingExercise targetExercise = new ProgrammingExercise();
+        course.addExercises(targetExercise);
+        targetExercise.generateAndSetProjectKey();
+        targetExercise = programmingExerciseRepository.save(targetExercise);
+
+        jenkinsRequestMockProvider.mockCopyBuildPlan(sourceExercise.getProjectKey(), targetExercise.getProjectKey());
+
+        continuousIntegrationService.copyBuildPlan(sourceExercise, "", targetExercise, "", "", true);
+        BuildPlan sourceBuildPlan = buildPlanRepository.findByProgrammingExercises_IdWithProgrammingExercisesElseThrow(sourceExercise.getId());
+        BuildPlan targetBuildPlan = buildPlanRepository.findByProgrammingExercises_IdWithProgrammingExercisesElseThrow(targetExercise.getId());
+        assertThat(sourceBuildPlan).isEqualTo(targetBuildPlan);
+    }
 }