Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions src/main/java/org/apache/maven/plugins/deploy/DeployFileMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.maven.api.Artifact;
import org.apache.maven.api.ProducedArtifact;
Expand Down Expand Up @@ -209,6 +210,12 @@ public class DeployFileMojo extends AbstractDeployMojo {
@Parameter(property = "maven.deploy.file.containedIn")
private Path containedIn;

/**
* Whether {@link #pomFile} points at a temporary POM extracted from the artifact's jar (as
* opposed to an operator-supplied file): extracted POMs must be deleted after the deployment.
*/
private boolean pomFromJar;

void initProperties() throws MojoException {
Path deployedPom;
if (pomFile != null) {
Expand All @@ -218,6 +225,7 @@ void initProperties() throws MojoException {
deployedPom = readingPomFromJarFile();
if (deployedPom != null) {
pomFile = deployedPom;
pomFromJar = true;
}
}

Expand All @@ -230,10 +238,19 @@ private Path readingPomFromJarFile() {
Pattern pomEntry = Pattern.compile("META-INF/maven/.*/pom\\.xml");
try {
try (JarFile jarFile = new JarFile(file.toFile())) {
JarEntry entry = jarFile.stream()
List<JarEntry> entries = jarFile.stream()
.filter(e -> pomEntry.matcher(e.getName()).matches())
.findFirst()
.orElse(null);
.collect(Collectors.toList());
if (entries.size() > 1) {
// a shaded/multi-POM jar's author would otherwise choose which embedded POM
// fills in the missing coordinates (first match wins): require explicitness
getLog().warn("Found " + entries.size() + " POMs in " + file.getFileName() + " ("
+ entries.stream().map(JarEntry::getName).collect(Collectors.joining(", "))
+ "); none will be used to derive coordinates. Specify pomFile or explicit"
+ " groupId/artifactId/version/packaging.");
return null;
}
JarEntry entry = entries.isEmpty() ? null : entries.get(0);
if (entry != null) {
getLog().debug("Using " + entry.getName() + " as pomFile");

Expand All @@ -242,6 +259,10 @@ private Path readingPomFromJarFile() {
if (base.indexOf('.') > 0) {
base = base.substring(0, base.lastIndexOf('.'));
}
while (base.length() < 3) {
// File.createTempFile rejects prefixes shorter than 3 characters
base = base + "_";
}
Path pomFile = File.createTempFile(base, ".pom").toPath();

Files.copy(pomInputStream, pomFile, StandardCopyOption.REPLACE_EXISTING);
Expand All @@ -255,7 +276,9 @@ private Path readingPomFromJarFile() {
}
}
} catch (IOException e) {
// ignore, artifact not packaged by Maven
// a corrupt (or hostile) jar must not silently degrade coordinate derivation
getLog().warn("Could not read a POM from " + file.getFileName() + ": " + e.getMessage()
+ "; coordinates will not be derived from the artifact");
}
return null;
}
Expand Down Expand Up @@ -334,7 +357,7 @@ public void execute() throws MojoException {
ProducedArtifact artifact = session.createProducedArtifact(
groupId, artifactId, version, classifier, isFilePom ? "pom" : getExtension(file), packaging);

if (file.equals(getLocalRepositoryFile(artifact))) {
if (isSameLocation(file, getLocalRepositoryFile(artifact))) {
throw new MojoException("Cannot deploy artifact from the local repository: " + file);
}

Expand Down Expand Up @@ -469,7 +492,7 @@ public void execute() throws MojoException {
} catch (ArtifactDeployerException e) {
throw new MojoException(e.getMessage(), e);
} finally {
if (pomFile == null && deployedPom != null) {
if ((pomFile == null || pomFromJar) && deployedPom != null) {
try {
Files.deleteIfExists(deployedPom);
} catch (IOException e) {
Expand Down Expand Up @@ -532,6 +555,16 @@ private Path getLocalRepositoryFile(Artifact artifact) {
return session.getPathForLocalArtifact(artifact);
}

/**
* Compares two paths as locations rather than spellings: a textual {@code Path.equals} lets a
* relative path, a symlink, or any non-canonical spelling of the same file slip past the
* local-repository self-deploy guard (an anti-footgun against local-repo metadata corruption,
* not a security boundary - but it should at least hold against trivial re-spellings).
*/
static boolean isSameLocation(Path a, Path b) {
return realOrNormalized(a).equals(realOrNormalized(b));
}

/**
* Process the supplied pomFile to get groupId, artifactId, version, and packaging
*
Expand Down
72 changes: 58 additions & 14 deletions src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,11 @@ public void execute() {
warnIfAffectedPackagingAndMaven(project.getPackaging().id());

if (!deployAtEnd) {
getLog().info("Deploying deploy for " + project.getGroupId() + ":" + project.getArtifactId() + ":"
+ project.getVersion() + " at end");
// this is the immediate-deploy branch: it must not claim the deploy happens "at
// end" - the deploy log is the operator's audit trail for what was deferred vs
// published immediately (the correct deferring message is in the else branch)
getLog().info("Deploying " + project.getGroupId() + ":" + project.getArtifactId() + ":"
+ project.getVersion());
deploy(createDeployerRequest());
synchronized (DEPLOY_AT_END_LOCK) {
putState(State.DEPLOYED);
Expand Down Expand Up @@ -336,32 +339,73 @@ private void deployAllAtOnce() {
if (!requests.isEmpty()) {
// Requests are deployed sequentially and there is no rollback: if one fails, make the
// partial-publication state explicit instead of only surfacing the failing module.
List<String> deployedRepositoryIds = new ArrayList<>();
List<Project> deployedProjects = new ArrayList<>();
for (ArtifactDeployerRequest request : requests) {
try {
deploy(request);
} catch (RuntimeException e) {
if (!deployedRepositoryIds.isEmpty()) {
getLog().error("Deploy-at-end batch failed after " + deployedRepositoryIds.size() + " of "
+ requests.size() + " deploy request(s) had already completed. Artifacts already"
+ " published to repository id(s) " + String.join(", ", deployedRepositoryIds)
+ " remain published: there is no rollback.");
}
logPartialDeployInventory(batchedProjects, deployedProjects, request);
throw e;
}
deployedRepositoryIds.add(request.getRepository().getId());
// exactly-once: mark each project DEPLOYED as soon as its contributing request
// completes, so a concurrent or repeated trigger skips completed work — and the
// partial-deploy inventory can distinguish published from pending projects
for (Project reactorProject : batchedProjects) {
if (getState(reactorProject) == State.TO_BE_DEPLOYED) {
ArtifactDeployerRequest projRequest = (ArtifactDeployerRequest)
session.getPluginContext(reactorProject).get(ArtifactDeployerRequest.class.getName());
if (request.getRepository().equals(projRequest.getRepository())
&& request.getRetryFailedDeploymentCount()
== projRequest.getRetryFailedDeploymentCount()) {
putState(reactorProject, State.DEPLOYED);
deployedProjects.add(reactorProject);
}
}
}
}
} else {
getLog().info("No actual deploy requests");
}
// Mark every batched project DEPLOYED so a re-triggered batch (second bound deploy
// execution, or a direct deploy:deploy invocation walking the reactor) cannot publish
// the same artifacts a second time. Only reached when all requests deployed successfully.
// Any remaining TO_BE_DEPLOYED projects (should not happen here, but for completeness)
for (Project reactorProject : batchedProjects) {
if (getState(reactorProject) == State.TO_BE_DEPLOYED) {
putState(reactorProject, State.DEPLOYED);
}
}
}

/**
* The contract documented on {@link #deployAtEnd} is all-or-nothing; when a deploy-at-end
* batch fails mid-loop that contract can no longer be met, so leave an explicit inventory
* of which projects already reached the remote repository and which were skipped, instead
* of failing silently into a mixed state. Mirrors the install plugin's partial-install
* inventory for consistent operator experience across both plugins.
*/
private void logPartialDeployInventory(
List<Project> batchedProjects, List<Project> deployedProjects, ArtifactDeployerRequest failedRequest) {
getLog().error("Deploy-at-end batch failed; the remote repository "
+ failedRequest.getRepository().getId() + " ("
+ redactUrlUserInfo(failedRequest.getRepository().getUrl())
+ ") is in a partially deployed state:");
for (Project reactorProject : batchedProjects) {
putState(reactorProject, State.DEPLOYED);
if (deployedProjects.contains(reactorProject)) {
getLog().error(" deployed: " + gav(reactorProject));
} else {
getLog().error(" not deployed: " + gav(reactorProject));
}
}
List<Project> skipped = getProjectsWithDeployExecution().stream()
.filter(p -> getState(p) == State.SKIPPED)
.collect(Collectors.toList());
for (Project p : skipped) {
getLog().error(" skipped: " + gav(p));
}
}

private static String gav(Project project) {
return project.getGroupId() + ":" + project.getArtifactId() + ":" + project.getVersion();
}

private void deploy(ArtifactDeployerRequest request) {
try {
getLog().info("Deploying artifacts " + request.getArtifacts().toString() + " to repository "
Expand Down
14 changes: 11 additions & 3 deletions src/site/markdown/examples/deploy-ftp.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ under the License.

# Deployment of artifacts with FTP

**FTP is a cleartext protocol: credentials and artifacts cross the network unencrypted, and
anyone on the path can capture the deployment credential. Prefer deploying over HTTPS to a
repository manager.** The plugin refuses `ftp://` (and `http://`) deployment URLs to
non-loopback hosts by default; deploying over FTP requires an explicit
`-Dmaven.deploy.allowInsecureUrl=true` opt-out. Note also that the default Maven Resolver
transport does not support FTP at all: you must switch to the wagon transport
(`-Dmaven.resolver.transport=wagon`) in addition to declaring the extension below.

In order to deploy artifacts using FTP you must first specify the use of an FTP server in the **distributionManagement** element of your POM as well as specifying an `extension` in your `build` element which will pull in the FTP artifacts required to deploy with FTP:

```unknown
Expand All @@ -52,16 +60,16 @@ In order to deploy artifacts using FTP you must first specify the use of an FTP
</project>
```

Your `settings.xml` would contain a `server` element where the `id` of that element matches `id` of the FTP repository specified in the POM above:
Your `settings.xml` would contain a `server` element where the `id` of that element matches `id` of the FTP repository specified in the POM above. Store the password in [encrypted form](https://maven.apache.org/guides/mini/guide-encryption.html) rather than as clear text:

```unknown
<settings>
...
<servers>
<server>
<id>ftp-repository</id>
<username>user</username>
<password>pass</password>
<username>my-user</username>
<password>{encrypted-password}</password>
</server>
</servers>
...
Expand Down
4 changes: 4 additions & 0 deletions src/site/markdown/examples/deploy-ssh-external.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ under the License.

# Deployment of artifacts in an external SSH command

**Note:** the default Maven Resolver transport supports HTTP(S) and file URLs only. Deploying
over `scpexe://` requires switching to the wagon transport
(`-Dmaven.resolver.transport=wagon`) in addition to declaring the extension below.

In order to deploy artifacts using SSH you must first specify the use of an SSH server in the **distributionManagement** element of your POM as well as specifying an `extension` in your `build` element which will pull in the SSH artifacts required to deploy with SSH:

```unknown
Expand Down
16 changes: 6 additions & 10 deletions src/site/markdown/examples/deploying-in-legacy-layout.md.vm
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,11 @@ under the License.
|---metadata
```

In able to deploy an artifact in a legacy layout of repository, set the **repositoryLayout** parameter to `legacy` value.
**Legacy layout support was removed in version 3.0.0 of this plugin.** There is no
`repositoryLayout` parameter in the 3.x/4.x lines: Maven 3 and later only support the
default (Maven 2) repository layout, and a `-DrepositoryLayout=legacy` flag on the command
line is silently ignored by Maven (it does not select a legacy layout).

```unknown
mvn ${project.groupId}:${project.artifactId}:${project.version}:deploy-file -Durl=file:///C:/m2-repo \
-DrepositoryId=some.id \
-Dfile=your-artifact-1.0.jar \
-DpomFile=your-pom.xml \
-DrepositoryLayout=legacy
```

**Note**: By using the fully qualified path of a goal, you&apos;re ensured to be using the preferred version of the maven-deploy-plugin. When using `mvn deploy:deploy-file` its version depends on its specification in the pom or the version of Apache Maven.
To deploy into a Maven 1 (legacy) layout repository, use maven-deploy-plugin 2.x with Maven 2,
or convert the repository to the default layout with a repository manager.

29 changes: 9 additions & 20 deletions src/site/markdown/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,15 @@ under the License.

### I get an Unsupported Protocol Error when deploying a 3rd party jar. What should I do?

If you are using the `deploy:deploy-file` goal and encounter this error:

*&quot;Error deploying artifact: Unsupported Protocol: &apos;ftp&apos;: Cannot find
wagon which supports the requested protocol: ftp&quot;*

Then you need to place the appropriate wagon provider in your `%M2_HOME%/lib`. In
this case the provider needed is ftp, so we have to place the wagon-ftp jar in the
lib directory of your Maven 2 installation.

As an alternative to placing the wagon provider into the Maven distribution, you can
also create a dummy POM that declares the required wagon as an `<extension>` inside
the current directory.

If the error description is something like this:

*&quot;Error deploying artifact: Unsupported Protocol: &apos;ftp&apos;: Cannot find
wagon which supports the requested protocol: ftp
org/apache/commons/net/ftp/FTP&quot;*

Then you need to place the commons-net jar in `%M2_HOME%/lib`.
Deployment uses the [Maven Resolver transport](https://maven.apache.org/guides/mini/guide-resolver-transport.html),
which supports `https://` (and `http://`) plus `file://` URLs out of the box. Other protocols
such as FTP, SCP or SFTP are not available by default: they require switching to the wagon
transport (`-Dmaven.resolver.transport=wagon`) and declaring the corresponding wagon provider
as a build `<extension>` in your POM.

Where possible, prefer deploying over HTTPS to a repository manager instead: it needs no extra
extensions and does not send credentials in the clear (see the
[HTTP(S) deployment example](./examples/deploy-http.html)).

<a id="skip"></a>

Expand Down
3 changes: 1 addition & 2 deletions src/site/markdown/index.md.vm
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ As a repository contains more than JAR files (POMs, the metadata, MD5 and SHA1 h

To work, the deployment will require:

- information about the repository: its location, the transport method used to access it (FTP, SCP, SFTP\.\.\.) and the optional user specific required account information
- information about the repository: its location and the optional user specific required account information; prefer `https://` URLs - the default Maven Resolver transport supports HTTP(S) and file URLs, while other protocols (FTP, SCP, SFTP\.\.\.) need the wagon transport plus a matching build extension
- information about the artifact(s): the group, artifact, version, packaging, classifier\.\.\.
- a deployer: a method to actually perform the deployment. This can be implemented as a wagon transport (making it cross-platform), or use a system specific method.

The information will be taken from the implied (or specified) pom and from the command line. The settings.xml file may also be parsed to retrieve user credentials.

Expand Down
5 changes: 2 additions & 3 deletions src/site/markdown/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ mvn deploy

## The `deploy:deploy-file` Mojo

The `deploy:deploy-file` mojo is used primarily for deploying artifacts, which were not built by Maven. The project&apos;s development team may or may not provide a POM for the artifact, and in some cases you may want to deploy the artifact to an internal remote repository. The deploy-file mojo provides functionality covering all of these use cases, and offers a wide range of configurability for generating a POM on-the-fly. Additionally, you can specify what layout your repository uses. The full usage statement of the deploy-file mojo can be described as:
The `deploy:deploy-file` mojo is used primarily for deploying artifacts, which were not built by Maven. The project&apos;s development team may or may not provide a POM for the artifact, and in some cases you may want to deploy the artifact to an internal remote repository. The deploy-file mojo provides functionality covering all of these use cases, and offers a wide range of configurability for generating a POM on-the-fly. The full usage statement of the deploy-file mojo can be described as:

```unknown
mvn deploy:deploy-file -Durl=file://C:\m2-repo \
Expand All @@ -88,8 +88,7 @@ mvn deploy:deploy-file -Durl=file://C:\m2-repo \
[-Dpackaging=jar] \
[-Dclassifier=test] \
[-DgeneratePom=true] \
[-DgeneratePom.description="My Project Description"] \
[-DrepositoryLayout=legacy]
[-DgeneratePom.description="My Project Description"]
```

If the following required information is not specified in some way, the goal will fail:
Expand Down
Loading
Loading