Skip to content

Commit 7cfbdc5

Browse files
authored
[MNG-5668] Execute after:* phases when build fails (#2195)
JIRA issue: [MNG-5668](https://issues.apache.org/jira/browse/MNG-5668) When a build step fails, Maven should still execute its corresponding after:* phases to ensure proper cleanup. This fix modifies the BuildPlanExecutor to: - Execute after:* phases when their corresponding before:* phase has been executed - Maintain proper phase ordering during failure handling - Handle cleanup phase failures gracefully without affecting the original error - Preserve concurrent build capabilities This ensures cleanup tasks (like resource cleanup or test environment teardown) are properly executed even when the build fails.
1 parent f040da3 commit 7cfbdc5

File tree

4 files changed

+170
-0
lines changed

4 files changed

+170
-0
lines changed

impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,10 @@ private void executePlan() {
343343
} catch (Exception e) {
344344
step.status.compareAndSet(SCHEDULED, FAILED);
345345
global.stop();
346+
347+
// Find and execute all pending after:* phases for this project
348+
executeAfterPhases(step);
349+
346350
handleBuildError(reactorContext, session, step.project, e, global);
347351
}
348352
});
@@ -352,6 +356,43 @@ private void executePlan() {
352356
}
353357
}
354358

359+
private void executeAfterPhases(BuildStep failedStep) {
360+
if (failedStep == null || failedStep.project == null) {
361+
return;
362+
}
363+
364+
lock.readLock().lock();
365+
try {
366+
// Find all after:* phases that should be executed
367+
plan.steps(failedStep.project)
368+
.filter(step -> step.name != null && step.name.startsWith(AFTER))
369+
.filter(step -> step.status.get() == CREATED)
370+
.filter(step -> {
371+
// Only execute after:xxx if before:xxx has been executed or failed
372+
String phaseName = step.name.substring(AFTER.length());
373+
return plan.step(failedStep.project, BEFORE + phaseName)
374+
.map(s -> {
375+
int status = s.status.get();
376+
return status == EXECUTED || status == FAILED;
377+
})
378+
.orElse(false);
379+
})
380+
.filter(step -> step.status.compareAndSet(CREATED, SCHEDULED))
381+
.forEach(afterStep -> {
382+
try {
383+
executeStep(afterStep);
384+
afterStep.status.compareAndSet(SCHEDULED, EXECUTED);
385+
} catch (Exception e) {
386+
// Log but don't fail - we're already in error handling
387+
logger.error("Error executing cleanup phase " + afterStep.name, e);
388+
afterStep.status.compareAndSet(SCHEDULED, FAILED);
389+
}
390+
});
391+
} finally {
392+
lock.readLock().unlock();
393+
}
394+
}
395+
355396
private void executeStep(BuildStep step) throws IOException, LifecycleExecutionException {
356397
Clock clock = getClock(step.project);
357398
switch (step.name) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.it;
20+
21+
import java.io.File;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
import static org.junit.jupiter.api.Assertions.assertTrue;
26+
27+
/**
28+
* This is a test for MNG-5668:
29+
* Verifies that after:xxx phases are executed even when the build fails
30+
*/
31+
class MavenITmng5668AfterPhaseExecutionTest extends AbstractMavenIntegrationTestCase {
32+
33+
MavenITmng5668AfterPhaseExecutionTest() {
34+
super("[4.0.0-rc-4,)"); // test is only relevant for Maven 4.0+
35+
}
36+
37+
@Test
38+
void testAfterPhaseExecutionOnFailure() throws Exception {
39+
File testDir = extractResources("/mng-5668-after-phase-execution");
40+
41+
Verifier verifier = newVerifier(testDir.getAbsolutePath());
42+
verifier.setAutoclean(false);
43+
verifier.deleteDirectory("target");
44+
45+
try {
46+
verifier.addCliArgument("-b");
47+
verifier.addCliArgument("concurrent");
48+
verifier.addCliArgument("verify");
49+
verifier.execute();
50+
fail("Build should have failed");
51+
} catch (VerificationException e) {
52+
// expected
53+
}
54+
55+
// Verify that marker files were created in the expected order
56+
verifier.verifyFilePresent("target/before-verify.txt");
57+
verifier.verifyFilePresent("target/verify-failed.txt");
58+
verifier.verifyFilePresent("target/after-verify.txt");
59+
60+
// Verify the execution order through timestamps
61+
long beforeTime = new File(testDir, "target/before-verify.txt").lastModified();
62+
long failTime = new File(testDir, "target/verify-failed.txt").lastModified();
63+
long afterTime = new File(testDir, "target/after-verify.txt").lastModified();
64+
65+
assertTrue(beforeTime <= failTime);
66+
assertTrue(failTime <= afterTime);
67+
}
68+
}

its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public TestSuiteOrdering() {
101101
* the tests are to finishing. Newer tests are also more likely to fail, so this is
102102
* a fail fast technique as well.
103103
*/
104+
suite.addTestSuite(MavenITmng5668AfterPhaseExecutionTest.class);
104105
suite.addTestSuite(MavenITmng8648ProjectStartedEventsTest.class);
105106
suite.addTestSuite(MavenITmng8645ConsumerPomDependencyManagementTest.class);
106107
suite.addTestSuite(MavenITmng8594AtFileTest.class);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<groupId>org.apache.maven.its.mng5668</groupId>
6+
<artifactId>test</artifactId>
7+
<version>1.0-SNAPSHOT</version>
8+
9+
<name>Test</name>
10+
<description>Test that verifies after:xxx phase execution when build fails</description>
11+
12+
<build>
13+
<plugins>
14+
<plugin>
15+
<groupId>org.apache.maven.plugins</groupId>
16+
<artifactId>maven-antrun-plugin</artifactId>
17+
<version>3.1.0</version>
18+
<executions>
19+
<execution>
20+
<id>before-verify</id>
21+
<goals>
22+
<goal>run</goal>
23+
</goals>
24+
<phase>before:verify</phase>
25+
<configuration>
26+
<target>
27+
<touch file="${project.build.directory}/before-verify.txt" />
28+
</target>
29+
</configuration>
30+
</execution>
31+
<execution>
32+
<id>verify</id>
33+
<goals>
34+
<goal>run</goal>
35+
</goals>
36+
<phase>verify</phase>
37+
<configuration>
38+
<target>
39+
<touch file="${project.build.directory}/verify-failed.txt" />
40+
<fail message="Intentionally failing the verify phase" />
41+
</target>
42+
</configuration>
43+
</execution>
44+
<execution>
45+
<id>after-verify</id>
46+
<goals>
47+
<goal>run</goal>
48+
</goals>
49+
<phase>after:verify</phase>
50+
<configuration>
51+
<target>
52+
<touch file="${project.build.directory}/after-verify.txt" />
53+
</target>
54+
</configuration>
55+
</execution>
56+
</executions>
57+
</plugin>
58+
</plugins>
59+
</build>
60+
</project>

0 commit comments

Comments
 (0)