Skip to content

Commit a85d19d

Browse files
authored
Programming exercises: Fix missing build plan during import on Jenkins setups (#7058)
1 parent 91d27ba commit a85d19d

17 files changed

+250
-38
lines changed

src/main/java/de/tum/in/www1/artemis/repository/BuildPlanRepository.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,17 @@ default BuildPlan setBuildPlanForExercise(final String buildPlan, final Programm
4444
buildPlanWrapper.addProgrammingExercise(exercise);
4545
return save(buildPlanWrapper);
4646
}
47+
48+
/**
49+
* Copies the build plan from the source exercise to the target exercise.
50+
*
51+
* @param sourceExercise The exercise containing the build plan to be copied.
52+
* @param targetExercise The exercise into which the build plan is copied.
53+
*/
54+
default void copyBetweenExercises(ProgrammingExercise sourceExercise, ProgrammingExercise targetExercise) {
55+
findByProgrammingExercises_IdWithProgrammingExercises(sourceExercise.getId()).ifPresent(buildPlan -> {
56+
buildPlan.addProgrammingExercise(targetExercise);
57+
save(buildPlan);
58+
});
59+
}
4760
}

src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,14 +446,14 @@ private ProgrammingExerciseStudentParticipation configureRepository(ProgrammingE
446446
private ProgrammingExerciseStudentParticipation copyBuildPlan(ProgrammingExerciseStudentParticipation participation) {
447447
// only execute this step if it has not yet been completed yet or if the build plan id is missing for some reason
448448
if (!participation.getInitializationState().hasCompletedState(InitializationState.BUILD_PLAN_COPIED) || participation.getBuildPlanId() == null) {
449-
final var projectKey = participation.getProgrammingExercise().getProjectKey();
449+
final var exercise = participation.getProgrammingExercise();
450450
final var planName = BuildPlanType.TEMPLATE.getName();
451451
final var username = participation.getParticipantIdentifier();
452452
final var buildProjectName = participation.getExercise().getCourseViaExerciseGroupOrCourseMember().getShortName().toUpperCase() + " "
453453
+ participation.getExercise().getTitle();
454454
final var targetPlanName = participation.addPracticePrefixIfTestRun(username.toUpperCase());
455455
// 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
456-
final var buildPlanId = continuousIntegrationService.orElseThrow().copyBuildPlan(projectKey, planName, projectKey, buildProjectName, targetPlanName, true);
456+
final var buildPlanId = continuousIntegrationService.orElseThrow().copyBuildPlan(exercise, planName, exercise, buildProjectName, targetPlanName, true);
457457
participation.setBuildPlanId(buildPlanId);
458458
participation.setInitializationState(InitializationState.BUILD_PLAN_COPIED);
459459
return programmingExerciseStudentParticipationRepository.saveAndFlush(participation);

src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,11 @@ private BambooBuildPlanDTO getBuildPlan(String planKey, boolean expand, boolean
310310
}
311311

312312
@Override
313-
public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
313+
public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
314314
boolean targetProjectExists) {
315+
String sourceProjectKey = sourceExercise.getProjectKey();
316+
String targetProjectKey = targetExercise.getProjectKey();
317+
315318
final var cleanPlanName = getCleanPlanName(targetPlanName);
316319
final var sourcePlanKey = sourceProjectKey + "-" + sourcePlanName;
317320
final var targetPlanKey = targetProjectKey + "-" + cleanPlanName;

src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,16 @@ void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, Vc
5353
/**
5454
* Clones an existing build plan. Illegal characters in the plan key, or name will be replaced.
5555
*
56-
* @param sourceProjectKey The key of the source project, normally the key of the exercise -> courseShortName + exerciseShortName.
56+
* @param sourceExercise The exercise from which the build plan should be copied
5757
* @param sourcePlanName The name of the source plan
58-
* @param targetProjectKey The key of the project the plan should get copied to
58+
* @param targetExercise The exercise to which the build plan is copied to
5959
* @param targetProjectName The wanted name of the new project
6060
* @param targetPlanName The wanted name of the new plan after copying it
6161
* @param targetProjectExists whether the target project already exists or not
6262
* @return The key of the new build plan
6363
*/
64-
String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName, boolean targetProjectExists);
64+
String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
65+
boolean targetProjectExists);
6566

6667
/**
6768
* 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 -

src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,14 @@ public void recreateBuildPlansForExercise(ProgrammingExercise exercise) {
193193
}
194194

195195
@Override
196-
public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
196+
public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
197197
boolean targetProjectExists) {
198198
// In GitLab CI we don't have to copy the build plan.
199199
// Instead, we configure a CI config path leading to the API when enabling the CI.
200200

201201
// When sending the build results back, the build plan key is used to identify the participation.
202202
// Therefore, we return the key here even though GitLab CI does not need it.
203-
return targetProjectKey + "-" + targetPlanName.toUpperCase().replaceAll("[^A-Z0-9]", "");
203+
return targetExercise.getProjectKey() + "-" + targetPlanName.toUpperCase().replaceAll("[^A-Z0-9]", "");
204204
}
205205

206206
@Override

src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ public void recreateBuildPlansForExercise(ProgrammingExercise exercise) {
9393
}
9494

9595
@Override
96-
public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
96+
public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
9797
boolean targetProjectExists) {
98-
return jenkinsBuildPlanService.copyBuildPlan(sourceProjectKey, sourcePlanName, targetProjectKey, targetPlanName);
98+
return jenkinsBuildPlanService.copyBuildPlan(sourceExercise, sourcePlanName, targetExercise, targetPlanName);
9999
}
100100

101101
@Override

src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import de.tum.in.www1.artemis.domain.enumeration.RepositoryType;
3838
import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation;
3939
import de.tum.in.www1.artemis.exception.JenkinsException;
40+
import de.tum.in.www1.artemis.repository.BuildPlanRepository;
4041
import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository;
4142
import de.tum.in.www1.artemis.repository.UserRepository;
4243
import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService;
@@ -78,9 +79,12 @@ public class JenkinsBuildPlanService {
7879

7980
private final ProgrammingExerciseRepository programmingExerciseRepository;
8081

82+
private final BuildPlanRepository buildPlanRepository;
83+
8184
public JenkinsBuildPlanService(@Qualifier("jenkinsRestTemplate") RestTemplate restTemplate, JenkinsServer jenkinsServer, JenkinsBuildPlanCreator jenkinsBuildPlanCreator,
8285
JenkinsJobService jenkinsJobService, JenkinsJobPermissionsService jenkinsJobPermissionsService, JenkinsInternalUrlService jenkinsInternalUrlService,
83-
UserRepository userRepository, ProgrammingExerciseRepository programmingExerciseRepository, JenkinsPipelineScriptCreator jenkinsPipelineScriptCreator) {
86+
UserRepository userRepository, ProgrammingExerciseRepository programmingExerciseRepository, JenkinsPipelineScriptCreator jenkinsPipelineScriptCreator,
87+
BuildPlanRepository buildPlanRepository) {
8488
this.restTemplate = restTemplate;
8589
this.jenkinsServer = jenkinsServer;
8690
this.jenkinsBuildPlanCreator = jenkinsBuildPlanCreator;
@@ -90,6 +94,7 @@ public JenkinsBuildPlanService(@Qualifier("jenkinsRestTemplate") RestTemplate re
9094
this.programmingExerciseRepository = programmingExerciseRepository;
9195
this.jenkinsInternalUrlService = jenkinsInternalUrlService;
9296
this.jenkinsPipelineScriptCreator = jenkinsPipelineScriptCreator;
97+
this.buildPlanRepository = buildPlanRepository;
9398
}
9499

95100
/**
@@ -191,6 +196,34 @@ public void updateBuildPlanRepositories(String buildProjectKey, String buildPlan
191196
log.error("Pipeline Script not found", e);
192197
}
193198

199+
postBuildPlanConfigChange(buildPlanKey, buildProjectKey, jobConfig);
200+
}
201+
202+
/**
203+
* Replaces the old build plan URL with a new one containing an updated exercise and access token.
204+
*
205+
* @param templateExercise The exercise containing the old build plan URL.
206+
* @param newExercise The exercise of which the build plan URL is updated.
207+
* @param jobConfig The job config in Jenkins for the new exercise.
208+
*/
209+
private void updateBuildPlanURLs(ProgrammingExercise templateExercise, ProgrammingExercise newExercise, Document jobConfig) {
210+
final Long previousExerciseId = templateExercise.getId();
211+
final String previousBuildPlanAccessSecret = templateExercise.getBuildPlanAccessSecret();
212+
final Long newExerciseId = newExercise.getId();
213+
final String newBuildPlanAccessSecret = newExercise.getBuildPlanAccessSecret();
214+
215+
String toBeReplaced = String.format("/%d/build-plan?secret=%s", previousExerciseId, previousBuildPlanAccessSecret);
216+
String replacement = String.format("/%d/build-plan?secret=%s", newExerciseId, newBuildPlanAccessSecret);
217+
218+
try {
219+
JenkinsBuildPlanUtils.replaceScriptParameters(jobConfig, toBeReplaced, replacement);
220+
}
221+
catch (IllegalArgumentException e) {
222+
log.error("Pipeline Script not found", e);
223+
}
224+
}
225+
226+
private void postBuildPlanConfigChange(String buildPlanKey, String buildProjectKey, Document jobConfig) {
194227
final var errorMessage = "Error trying to configure build plan in Jenkins " + buildPlanKey;
195228
try {
196229
URI uri = JenkinsEndpoints.PLAN_CONFIG.buildEndpoint(serverUrl.toString(), buildProjectKey, buildPlanKey).build(true).toUri();
@@ -233,17 +266,25 @@ public String getBuildPlanKeyFromTestResults(TestResultsDTO testResultsDTO) thro
233266
/**
234267
* Copies a build plan to another and replaces the old reference to the master and main branch with a reference to the default branch
235268
*
236-
* @param sourceProjectKey the source project key
237-
* @param sourcePlanName the source plan name
238-
* @param targetProjectKey the target project key
239-
* @param targetPlanName the target plan name
269+
* @param sourceExercise the source exercise
270+
* @param sourcePlanName the source plan name
271+
* @param targetExercise the target exercise
272+
* @param targetPlanName the target plan name
240273
* @return the key of the created build plan
241274
*/
242-
public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetPlanName) {
275+
public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetPlanName) {
276+
buildPlanRepository.copyBetweenExercises(sourceExercise, targetExercise);
277+
278+
String sourceProjectKey = sourceExercise.getProjectKey();
279+
String targetProjectKey = targetExercise.getProjectKey();
280+
243281
final var cleanTargetName = getCleanPlanName(targetPlanName);
244282
final var sourcePlanKey = sourceProjectKey + "-" + sourcePlanName;
245283
final var targetPlanKey = targetProjectKey + "-" + cleanTargetName;
246284
final var jobXml = jenkinsJobService.getJobConfigForJobInFolder(sourceProjectKey, sourcePlanKey);
285+
286+
updateBuildPlanURLs(sourceExercise, targetExercise, jobXml);
287+
247288
jenkinsJobService.createJobInFolder(jobXml, targetProjectKey, targetPlanKey);
248289

249290
return targetPlanKey;
@@ -362,7 +403,7 @@ public boolean buildPlanExists(String projectKey, String buildPlanId) {
362403
/**
363404
* Assigns access permissions to instructors and TAs for the specified build plan.
364405
* This is done by getting all users that belong to the instructor and TA groups of
365-
* the exercise' course and adding permissions to the Jenkins job.
406+
* the exercises' course and adding permissions to the Jenkins job.
366407
*
367408
* @param programmingExercise the programming exercise
368409
* @param planName the name of the build plan

src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanUtils.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ public class JenkinsBuildPlanUtils {
88
private static final String PIPELINE_SCRIPT_DETECTION_COMMENT = "// ARTEMIS: JenkinsPipeline";
99

1010
/**
11-
* Replaces the base repository url written within the Jenkins pipeline script with the value specified by repoUrl
11+
* Replaces either one of the previous repository urls or the build plan url written within the Jenkins pipeline
12+
* script with the value specified by newUrl.
1213
*
1314
* @param jobXmlDocument the Jenkins pipeline
14-
* @param repoUrl the new repository url
15-
* @param baseRepoUrl the base repository url that will be replaced
15+
* @param previousUrl the previous url that will be replaced
16+
* @param newUrl the new repository or build plan url
1617
* @throws IllegalArgumentException if the xml document isn't a Jenkins pipeline script
1718
*/
18-
public static void replaceScriptParameters(Document jobXmlDocument, String repoUrl, String baseRepoUrl) throws IllegalArgumentException {
19+
public static void replaceScriptParameters(Document jobXmlDocument, String previousUrl, String newUrl) throws IllegalArgumentException {
1920
final var scriptNode = findScriptNode(jobXmlDocument);
2021
if (scriptNode == null || scriptNode.getFirstChild() == null) {
2122
throw new IllegalArgumentException("Pipeline Script not found");
@@ -27,9 +28,9 @@ public static void replaceScriptParameters(Document jobXmlDocument, String repoU
2728
if (!pipeLineScript.startsWith("pipeline") && !pipeLineScript.startsWith(PIPELINE_SCRIPT_DETECTION_COMMENT)) {
2829
throw new IllegalArgumentException("Pipeline Script not found");
2930
}
30-
// Replace repo URL
31-
// TODO: properly replace the baseRepoUrl with repoUrl by looking up the ciRepoName in the pipelineScript
32-
pipeLineScript = pipeLineScript.replace(baseRepoUrl, repoUrl);
31+
// Replace URL
32+
// TODO: properly replace the previousUrl with newUrl by looking up the ciRepoName in the pipelineScript
33+
pipeLineScript = pipeLineScript.replace(previousUrl, newUrl);
3334

3435
scriptNode.getFirstChild().setTextContent(pipeLineScript);
3536
}

src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,11 @@ public BuildStatus getBuildStatus(ProgrammingExerciseParticipation participation
100100
}
101101

102102
@Override
103-
public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
103+
public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
104104
boolean targetProjectExists) {
105105
// No build plans exist for local CI. Only return a plan name.
106106
final String cleanPlanName = getCleanPlanName(targetPlanName);
107-
return targetProjectKey + "-" + cleanPlanName;
107+
return targetExercise.getProjectKey() + "-" + cleanPlanName;
108108
}
109109

110110
@Override

src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportBasicService.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,7 @@ public ProgrammingExerciseImportBasicService(ExerciseHintService exerciseHintSer
102102
@Transactional // TODO: apply the transaction on a smaller scope
103103
// IMPORTANT: the transactional context only works if you invoke this method from another class
104104
public ProgrammingExercise importProgrammingExerciseBasis(final ProgrammingExercise templateExercise, final ProgrammingExercise newExercise) {
105-
// Set values we don't want to copy to null
106-
setupExerciseForImport(newExercise);
107-
newExercise.setBranch(versionControlService.orElseThrow().getDefaultBranchOfArtemis());
105+
prepareBasicExerciseInformation(templateExercise, newExercise);
108106

109107
// Note: same order as when creating an exercise
110108
programmingExerciseParticipationService.setupInitialTemplateParticipation(newExercise);
@@ -161,6 +159,25 @@ else if (Boolean.TRUE.equals(importedExercise.isStaticCodeAnalysisEnabled()) &&
161159
return savedImportedExercise;
162160
}
163161

162+
/**
163+
* Prepares information directly stored in the exercise for the copy process.
164+
* <p>
165+
* Replaces attributes in the new exercise that should not be copied from the previous one.
166+
*
167+
* @param templateExercise Some exercise the information is copied from.
168+
* @param newExercise The exercise that is prepared.
169+
*/
170+
private void prepareBasicExerciseInformation(final ProgrammingExercise templateExercise, final ProgrammingExercise newExercise) {
171+
// Set values we don't want to copy to null
172+
setupExerciseForImport(newExercise);
173+
174+
if (templateExercise.hasBuildPlanAccessSecretSet()) {
175+
newExercise.generateAndSetBuildPlanAccessSecret();
176+
}
177+
178+
newExercise.setBranch(versionControlService.orElseThrow().getDefaultBranchOfArtemis());
179+
}
180+
164181
/**
165182
* Sets up the test repository for a new exercise by setting the repository URL. This does not create the actual
166183
* repository on the version control server!

0 commit comments

Comments
 (0)