Add support for Maven Publisher API.
This commit is contained in:
@@ -11,11 +11,13 @@ export PATH="$MAVEN_HOME/bin:$JAVA_HOME/bin:$PATH"
|
||||
|
||||
export JENKINS_HOME=/tmp/jenkins-home
|
||||
export RELEASE_TOOLS_MAVEN_REPOSITORY=$(pwd)/maven-repository
|
||||
export WORK_DIR=$(pwd)/work
|
||||
export LOGS_DIR=$(pwd)/logs
|
||||
export SETTINGS_XML=$(pwd)/ci/settings.xml
|
||||
|
||||
mkdir -p ${RELEASE_TOOLS_MAVEN_REPOSITORY}
|
||||
mkdir -p ${LOGS_DIR}
|
||||
mkdir -p ${WORK_DIR}
|
||||
|
||||
export GNUPGHOME=~/.gnupg/
|
||||
mkdir -p ${GNUPGHOME}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<properties>
|
||||
<spring-data-bom.version>2023.0.0-M1</spring-data-bom.version>
|
||||
<stagingRepository>orgspringframework-2453</stagingRepository>
|
||||
<stagingRepository>deploymentId</stagingRepository>
|
||||
<mongodb.version>5.3.1</mongodb.version>
|
||||
</properties>
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>s01.oss.sonatype.org</id>
|
||||
<id>central-publisher-api</id>
|
||||
<url>
|
||||
https://s01.oss.sonatype.org/service/local/repositories/${stagingRepository}/content/
|
||||
https://central.sonatype.com/api/v1/publisher/deployment/${deploymentId}/download/
|
||||
</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
@@ -29,6 +29,18 @@
|
||||
<username>${env.COMMERCIAL_USR}</username>
|
||||
<password>${env.COMMERCIAL_PSW}</password>
|
||||
</server>
|
||||
|
||||
<server>
|
||||
<id>central-publisher-api</id>
|
||||
<configuration>
|
||||
<httpHeaders>
|
||||
<property>
|
||||
<name>Authorization</name>
|
||||
<value>Bearer ${CENTRAL_BEARER}</value>
|
||||
</property>
|
||||
</httpHeaders>
|
||||
</configuration>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
</settings>
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.release.model.SupportedProject;
|
||||
import org.springframework.plugin.core.PluginRegistry;
|
||||
|
||||
import org.xmlbeam.XBProjector;
|
||||
import org.xmlbeam.XBProjector.Flags;
|
||||
import org.xmlbeam.config.DefaultXMLFactoriesConfig;
|
||||
|
||||
@@ -19,6 +19,7 @@ import static org.springframework.data.release.model.Projects.*;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
@@ -27,9 +28,12 @@ import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.assertj.core.util.VisibleForTesting;
|
||||
|
||||
import org.springframework.data.release.deployment.DeploymentInformation;
|
||||
import org.springframework.data.release.deployment.MavenPublisher;
|
||||
import org.springframework.data.release.deployment.StagingRepository;
|
||||
import org.springframework.data.release.git.BranchMapping;
|
||||
import org.springframework.data.release.io.Workspace;
|
||||
import org.springframework.data.release.model.ModuleIteration;
|
||||
import org.springframework.data.release.model.Phase;
|
||||
import org.springframework.data.release.model.ProjectAware;
|
||||
@@ -56,6 +60,8 @@ public class BuildOperations {
|
||||
private final @NonNull Logger logger;
|
||||
private final @NonNull MavenProperties properties;
|
||||
private final @NonNull BuildExecutor executor;
|
||||
private final @NonNull MavenPublisher publisher;
|
||||
private final Workspace workspace;
|
||||
|
||||
/**
|
||||
* Updates all inter-project dependencies based on the given {@link TrainIteration} and release {@link Phase}.
|
||||
@@ -120,35 +126,6 @@ public class BuildOperations {
|
||||
return doWithBuildSystem(iteration, (system, module) -> system.prepareVersion(module, phase));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a repository to stage artifacts for this {@link ModuleIteration}.
|
||||
*
|
||||
* @param iteration must not be {@literal null}.
|
||||
*/
|
||||
public void open(ModuleIteration iteration) {
|
||||
|
||||
doWithBuildSystem(iteration, (buildSystem, moduleIteration) -> buildSystem.open(iteration.getTrain()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a repository to stage artifacts for this {@link ModuleIteration}.
|
||||
*
|
||||
* @param iteration must not be {@literal null}.
|
||||
* @param stagingRepository must not be {@literal null}.
|
||||
*/
|
||||
public void close(ModuleIteration iteration, StagingRepository stagingRepository) {
|
||||
|
||||
Assert.notNull(stagingRepository, "StagingRepository must not be null");
|
||||
Assert.isTrue(stagingRepository.isPresent(), "StagingRepository must be present");
|
||||
|
||||
Train train = iteration.getTrain();
|
||||
|
||||
doWithBuildSystem(iteration, (buildSystem, moduleIteration) -> {
|
||||
buildSystem.close(train, stagingRepository);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a local build for all modules in the given {@link TrainIteration}.
|
||||
*
|
||||
@@ -163,55 +140,14 @@ public class BuildOperations {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a staging repository.
|
||||
* Promote the staging repository by publishing the deployment.
|
||||
*
|
||||
* @param iteration
|
||||
* @return
|
||||
*/
|
||||
public StagingRepository openStagingRepository(TrainIteration iteration) {
|
||||
|
||||
BuildSystem orchestrator = getRepositoryOrchestrator(iteration.getTrain());
|
||||
|
||||
return iteration.isPublic() ? orchestrator.open(iteration.getTrain()) : StagingRepository.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a staging repository.
|
||||
*
|
||||
* @param stagingRepository
|
||||
* @return
|
||||
*/
|
||||
public void closeStagingRepository(Train train, StagingRepository stagingRepository) {
|
||||
|
||||
BuildSystem orchestrator = getRepositoryOrchestrator(train);
|
||||
|
||||
if (stagingRepository.isPresent()) {
|
||||
orchestrator.close(train, stagingRepository);
|
||||
}
|
||||
}
|
||||
|
||||
private BuildSystem getRepositoryOrchestrator(Train train) {
|
||||
|
||||
SupportedProject project = train.getSupportedProject(BUILD);
|
||||
BuildSystem orchestrator = buildSystems.getRequiredPluginFor(project);
|
||||
|
||||
return orchestrator.withJavaVersion(executor.detectJavaVersion(project));
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote the staging repository.
|
||||
*
|
||||
* @param train must not be {@literal null}.
|
||||
* @param stagingRepository must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public void releaseStagingRepository(Train train, StagingRepository stagingRepository) {
|
||||
|
||||
BuildSystem orchestrator = getRepositoryOrchestrator(train);
|
||||
|
||||
if (stagingRepository.isPresent()) {
|
||||
orchestrator.release(train, stagingRepository);
|
||||
}
|
||||
public void publishDeployment(TrainIteration iteration, StagingRepository stagingRepository) {
|
||||
publisher.publish(iteration, stagingRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,22 +164,6 @@ public class BuildOperations {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases a repository of staged artifacts for this {@link ModuleIteration}.
|
||||
*
|
||||
* @param iteration
|
||||
* @param stagingRepository
|
||||
*/
|
||||
public void release(ModuleIteration iteration, StagingRepository stagingRepository) {
|
||||
|
||||
Train train = iteration.getTrain();
|
||||
|
||||
doWithBuildSystem(iteration, (buildSystem, moduleIteration) -> {
|
||||
buildSystem.release(train, stagingRepository);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public void buildDocumentation(TrainIteration iteration) {
|
||||
|
||||
Streamable<ModuleIteration> of = Streamable.of(iteration.getModulesExcept(BOM, COMMONS, BUILD));
|
||||
@@ -268,17 +188,14 @@ public class BuildOperations {
|
||||
*/
|
||||
public List<DeploymentInformation> performRelease(TrainIteration iteration) {
|
||||
|
||||
StagingRepository stagingRepository = iteration.isPublic() //
|
||||
? openStagingRepository(iteration) //
|
||||
: StagingRepository.EMPTY;
|
||||
StagingRepository localStaging = iteration.isPublic() ? initializeStagingRepository() : StagingRepository.EMPTY;
|
||||
StagingRepository stagingRepository = StagingRepository.EMPTY;
|
||||
|
||||
BuildExecutor.Summary<DeploymentInformation> summary = executor.doWithBuildSystemOrdered(iteration,
|
||||
(buildSystem, moduleIteration) -> buildSystem.deploy(moduleIteration, stagingRepository));
|
||||
(buildSystem, moduleIteration) -> buildSystem.deploy(moduleIteration, localStaging));
|
||||
|
||||
Train train = iteration.getTrain();
|
||||
|
||||
if (stagingRepository.isPresent()) {
|
||||
closeStagingRepository(train, stagingRepository);
|
||||
if (iteration.isPublic()) {
|
||||
stagingRepository = uploadDeployment(iteration.getModule(BOM), localStaging);
|
||||
}
|
||||
|
||||
smokeTests(iteration, stagingRepository);
|
||||
@@ -286,12 +203,81 @@ public class BuildOperations {
|
||||
logger.log(iteration, "Release: %s", summary);
|
||||
|
||||
if (stagingRepository.isPresent()) {
|
||||
releaseStagingRepository(train, stagingRepository);
|
||||
publishDeployment(iteration, stagingRepository);
|
||||
}
|
||||
|
||||
return summary.getExecutions().stream()
|
||||
.map(BuildExecutor.ExecutionResult::getResult)
|
||||
.collect(Collectors.toList());
|
||||
return summary.getExecutions().stream().map(BuildExecutor.ExecutionResult::getResult).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the staging build for all modules in the given {@link TrainIteration} without deploying artifacts
|
||||
* remotely.
|
||||
*
|
||||
* @param iteration must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public List<DeploymentInformation> stageRelease(TrainIteration iteration) {
|
||||
|
||||
StagingRepository localStaging = initializeStagingRepository();
|
||||
|
||||
BuildExecutor.Summary<DeploymentInformation> summary = executor.doWithBuildSystemOrdered(iteration,
|
||||
(buildSystem, moduleIteration) -> buildSystem.deploy(moduleIteration, localStaging));
|
||||
|
||||
logger.log(iteration, "Release: %s", summary);
|
||||
|
||||
return summary.getExecutions().stream().map(BuildExecutor.ExecutionResult::getResult).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the staging build for {@link ModuleIteration} without deploying artifacts remotely.
|
||||
*
|
||||
* @param module must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public DeploymentInformation stageRelease(ModuleIteration module) {
|
||||
|
||||
StagingRepository localStaging = initializeStagingRepository();
|
||||
|
||||
return doWithBuildSystem(module,
|
||||
(buildSystem, moduleIteration) -> buildSystem.deploy(moduleIteration, localStaging));
|
||||
}
|
||||
|
||||
public void uploadDeployment(TrainIteration iteration) {
|
||||
|
||||
StagingRepository localStaging = publisher.getStagingRepository();
|
||||
StagingRepository stagingRepository = uploadDeployment(iteration.getModule(BOM), localStaging);
|
||||
|
||||
logger.log(iteration, "Created deployment: %s", stagingRepository);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void validateDeployment(TrainIteration iteration, StagingRepository deploymentId) {
|
||||
|
||||
MavenPublisher.DeploymentStatus status = publisher.validate(iteration, deploymentId);
|
||||
logger.log(iteration, "Deployment validated: %s, \n%s", status.getDeploymentState(),
|
||||
status.getPurls().stream().map(it -> " * " + it).collect(Collectors.joining(System.lineSeparator())));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private StagingRepository uploadDeployment(ModuleIteration iteration, StagingRepository localStaging) {
|
||||
|
||||
String deploymentName;
|
||||
|
||||
if (iteration.getProject() == BOM) {
|
||||
deploymentName = String.format("Spring Data %s", iteration.getTrainIteration().getName());
|
||||
} else {
|
||||
deploymentName = String.format("Spring Data %s", iteration);
|
||||
}
|
||||
|
||||
StagingRepository deploymentId = publisher.upload(iteration, deploymentName, localStaging);
|
||||
publisher.validate(iteration.getTrainIteration(), deploymentId);
|
||||
|
||||
return deploymentId;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private StagingRepository initializeStagingRepository() {
|
||||
return publisher.initializeStagingRepository();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -420,8 +406,7 @@ public class BuildOperations {
|
||||
* @param function must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private <T, S extends ProjectAware> T doWithBuildSystem(S module,
|
||||
BiFunction<BuildSystem, S, T> function) {
|
||||
private <T, S extends ProjectAware> T doWithBuildSystem(S module, BiFunction<BuildSystem, S, T> function) {
|
||||
|
||||
Assert.notNull(module, "ModuleIteration must not be null!");
|
||||
|
||||
|
||||
@@ -69,27 +69,6 @@ interface BuildSystem extends Plugin<SupportedProject> {
|
||||
*/
|
||||
<M extends ProjectAware> M triggerPreReleaseCheck(M module);
|
||||
|
||||
/**
|
||||
* Open a remote repository for staging artifacts.
|
||||
*
|
||||
* @param train must not be {@literal null}.
|
||||
*/
|
||||
StagingRepository open(Train train);
|
||||
|
||||
/**
|
||||
* Close a remote repository for staging artifacts.
|
||||
*
|
||||
* @param train must not be {@literal null}.
|
||||
*/
|
||||
void close(Train train, StagingRepository stagingRepository);
|
||||
|
||||
/**
|
||||
* Release a remote repository of staged artifacts.
|
||||
*
|
||||
* @param train must not be {@literal null}.
|
||||
*/
|
||||
void release(Train train, StagingRepository stagingRepository);
|
||||
|
||||
<M extends ProjectAware> M triggerBuild(M module);
|
||||
|
||||
<M extends ProjectAware> M triggerDocumentationBuild(M module);
|
||||
|
||||
@@ -225,55 +225,6 @@ class MavenBuildSystem implements BuildSystem {
|
||||
return module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a {@literal nexus-staging:rc-open} and extract the {@code stagingProfileId} from the results.
|
||||
*/
|
||||
@Override
|
||||
public StagingRepository open(Train train) {
|
||||
|
||||
Assert.notNull(properties.getMavenCentral(), "Maven Central properties must not be null");
|
||||
Assert.hasText(properties.getMavenCentral().getStagingProfileId(), "Staging Profile Identifier must not be empty");
|
||||
|
||||
CommandLine arguments = CommandLine.of(goal("nexus-staging:rc-open"), //
|
||||
profile("central"), //
|
||||
arg("stagingProfileId").withValue(properties.getMavenCentral().getStagingProfileId()), //
|
||||
arg("openedRepositoryMessageFormat").withValue("'" + REPO_OPENING_TAG + "%s" + REPO_CLOSING_TAG + "'"))
|
||||
.andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), () -> settingsXml(properties.getSettingsXml()));
|
||||
|
||||
MavenRuntime.MavenInvocationResult invocationResult = mvn.execute(train.getSupportedProject(BUILD), arguments);
|
||||
|
||||
List<String> rcOpenLogContents = invocationResult.getLog();
|
||||
|
||||
String stagingRepositoryId = rcOpenLogContents.stream() //
|
||||
.filter(line -> line.contains(REPO_OPENING_TAG) && !line.contains("%s")) //
|
||||
.reduce((first, second) -> second) // find the last entry, a.k.a. the most recent log line
|
||||
.map(s -> s.substring( //
|
||||
s.indexOf(REPO_OPENING_TAG) + REPO_OPENING_TAG.length(), //
|
||||
s.indexOf(REPO_CLOSING_TAG))) //
|
||||
.orElse("");
|
||||
|
||||
logger.log(BUILD, "Opened staging repository with Id: " + stagingRepositoryId);
|
||||
|
||||
return StagingRepository.of(stagingRepositoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a {@literal nexus-staging:rc-close}.
|
||||
*/
|
||||
@Override
|
||||
public void close(Train train, StagingRepository stagingRepository) {
|
||||
|
||||
Assert.notNull(stagingRepository, "StagingRepository must not be null");
|
||||
Assert.isTrue(stagingRepository.isPresent(), "StagingRepository must be present");
|
||||
|
||||
CommandLine arguments = CommandLine.of(goal("nexus-staging:rc-close"), //
|
||||
profile("central"), //
|
||||
arg("stagingRepositoryId").withValue(stagingRepository.getId()))
|
||||
.andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), () -> settingsXml(properties.getSettingsXml()));
|
||||
|
||||
mvn.execute(train.getSupportedProject(BUILD), arguments);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.release.build.BuildSystem#triggerBuild(org.springframework.data.release.model.ModuleIteration)
|
||||
@@ -331,7 +282,7 @@ class MavenBuildSystem implements BuildSystem {
|
||||
|
||||
deployToArtifactory(module, information);
|
||||
|
||||
deployToMavenCentral(module, information);
|
||||
doDeploy(module, information);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,7 +333,7 @@ class MavenBuildSystem implements BuildSystem {
|
||||
* @param module must not be {@literal null}.
|
||||
* @param deploymentInformation must not be {@literal null}.
|
||||
*/
|
||||
private void deployToMavenCentral(ModuleIteration module, DeploymentInformation deploymentInformation) {
|
||||
private void doDeploy(ModuleIteration module, DeploymentInformation deploymentInformation) {
|
||||
|
||||
Assert.notNull(module, "Module iteration must not be null!");
|
||||
Assert.notNull(deploymentInformation, "DeploymentInformation iteration must not be null!");
|
||||
@@ -392,19 +343,24 @@ class MavenBuildSystem implements BuildSystem {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(module, "Deploying artifacts to Sonatype OSS Nexus…");
|
||||
StagingRepository stagingRepository = deploymentInformation.getStagingRepositoryId();
|
||||
|
||||
if (stagingRepository.isPresent()) {
|
||||
logger.log(module, "Staging artifacts to %s…", stagingRepository);
|
||||
} else {
|
||||
logger.log(module, "Deploying artifacts Maven Central …");
|
||||
}
|
||||
|
||||
Gpg gpg = getGpg();
|
||||
|
||||
CommandLine arguments = CommandLine.of(Goal.CLEAN, Goal.DEPLOY, //
|
||||
profile("ci,release,central"), //
|
||||
profile("ci,release"), //
|
||||
SKIP_TESTS, //
|
||||
arg("gpg.executable").withValue(gpg.getExecutable()), //
|
||||
arg("gpg.keyname").withValue(gpg.getKeyname()), //
|
||||
arg("gpg.passphrase").withValue(gpg.getPassphrase())) //
|
||||
.andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), settingsXml(properties.getSettingsXml()))
|
||||
.andIf(deploymentInformation.getStagingRepositoryId().isPresent(),
|
||||
() -> arg("stagingRepositoryId").withValue(deploymentInformation.getStagingRepositoryId()))
|
||||
.andIf(stagingRepository.isPresent(), () -> arg("altDeploymentRepository").withValue(stagingRepository))
|
||||
.andIf(gpg.hasSecretKeyring(), () -> arg("gpg.secretKeyring").withValue(gpg.getSecretKeyring()));
|
||||
|
||||
mvn.execute(module.getSupportedProject(), arguments);
|
||||
@@ -437,30 +393,14 @@ class MavenBuildSystem implements BuildSystem {
|
||||
profile(profile), //
|
||||
arg("s").withValue("settings.xml"), //
|
||||
arg("spring-data-bom.version").withValue(iteration.getReleaseTrainNameAndVersion())) //
|
||||
.andIf(mavenCentral, arg("stagingRepository").withValue(stagingRepository.getId()));
|
||||
.andIf(mavenCentral, arg("deploymentId").withValue(stagingRepository.getId())) //
|
||||
.andIf(mavenCentral, arg("CENTRAL_BEARER").withValue(properties.getMavenCentral().getBearer()));
|
||||
|
||||
mvn.execute(smokeTests, arguments);
|
||||
|
||||
logger.log(iteration, "✅ Smoke tests passed. Do not smoke 🚭. It's unhealthy.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a {@literal nexus-staging:rc-release}.
|
||||
*/
|
||||
@Override
|
||||
public void release(Train train, StagingRepository stagingRepository) {
|
||||
|
||||
Assert.notNull(stagingRepository, "StagingRepository must not be null");
|
||||
Assert.isTrue(stagingRepository.isPresent(), "StagingRepository must be present");
|
||||
|
||||
CommandLine arguments = CommandLine.of(goal("nexus-staging:rc-release"), //
|
||||
profile("central"), //
|
||||
arg("stagingRepositoryId").withValue(stagingRepository.getId()))
|
||||
.andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), () -> settingsXml(properties.getSettingsXml()));
|
||||
|
||||
mvn.execute(train.getSupportedProject(BUILD), arguments);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <M extends ProjectAware> M triggerDocumentationBuild(M module) {
|
||||
|
||||
@@ -629,14 +569,6 @@ class MavenBuildSystem implements BuildSystem {
|
||||
|
||||
logger.log(BUILD, "Verifying Maven Staging Authentication…");
|
||||
|
||||
mvn.execute(train.getSupportedProject(BUILD), //
|
||||
CommandLine.of(goal("nexus-staging:rc-list-profiles"), //
|
||||
profile("central")));
|
||||
|
||||
Assert.notNull(properties.getMavenCentral(),
|
||||
"Maven Central properties are not set (deployment.maven-central.staging-profile-id=…)");
|
||||
Assert.hasText(properties.getMavenCentral().getStagingProfileId(),
|
||||
"Staging Profile Id is not set (deployment.maven-central.staging-profile-id=…)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class ReleaseCommands extends TimedCommand {
|
||||
@NonNull BuildOperations build;
|
||||
@NonNull IssueTrackerCommands tracker;
|
||||
@NonNull GitHubCommands gitHub;
|
||||
private final Logger logger;
|
||||
@NonNull Logger logger;
|
||||
|
||||
/**
|
||||
* Composite command to prepare a release.
|
||||
@@ -130,32 +130,6 @@ class ReleaseCommands extends TimedCommand {
|
||||
git.commit(iteration, "Release version %s.");
|
||||
}
|
||||
|
||||
@CliCommand(value = "repository open")
|
||||
public void repositoryOpen(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
|
||||
|
||||
if (iteration.isPublic()) {
|
||||
build.open(iteration.getModule(Projects.BUILD));
|
||||
}
|
||||
}
|
||||
|
||||
@CliCommand(value = "repository close")
|
||||
public void repositoryClose(@CliOption(key = "", mandatory = true) TrainIteration iteration,
|
||||
@CliOption(key = "stagingRepositoryId", mandatory = true) String stagingRepositoryId) {
|
||||
|
||||
if (iteration.isPublic()) {
|
||||
build.close(iteration.getModule(Projects.BUILD), StagingRepository.of(stagingRepositoryId));
|
||||
}
|
||||
}
|
||||
|
||||
@CliCommand(value = "release repository")
|
||||
public void repositoryRelease(@CliOption(key = "", mandatory = true) TrainIteration iteration,
|
||||
@CliOption(key = "stagingRepositoryId", mandatory = true) String stagingRepositoryId) {
|
||||
|
||||
if (iteration.isPublic()) {
|
||||
build.release(iteration.getModule(Projects.BUILD), StagingRepository.of(stagingRepositoryId));
|
||||
}
|
||||
}
|
||||
|
||||
@CliCommand(value = "release build")
|
||||
public void buildRelease(@CliOption(key = "", mandatory = true) TrainIteration iteration, //
|
||||
@CliOption(key = "project", mandatory = false) String projectName) {
|
||||
@@ -179,6 +153,38 @@ class ReleaseCommands extends TimedCommand {
|
||||
}
|
||||
}
|
||||
|
||||
@CliCommand(value = "release local-stage")
|
||||
public void stageRelease(@CliOption(key = "", mandatory = true) TrainIteration iteration, //
|
||||
@CliOption(key = "project", mandatory = false) String projectName) {
|
||||
|
||||
git.checkout(iteration);
|
||||
|
||||
if (!iteration.isPublic()) {
|
||||
deployment.verifyAuthentication(iteration);
|
||||
}
|
||||
|
||||
if (projectName != null) {
|
||||
|
||||
Project project = Projects.requiredByName(projectName);
|
||||
ModuleIteration module = iteration.getModule(project);
|
||||
|
||||
build.stageRelease(module);
|
||||
} else {
|
||||
build.stageRelease(iteration);
|
||||
}
|
||||
}
|
||||
|
||||
@CliCommand(value = "release central upload")
|
||||
public void uploadDeployment(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
|
||||
build.uploadDeployment(iteration);
|
||||
}
|
||||
|
||||
@CliCommand(value = "release central validate")
|
||||
public void validateDeployment(@CliOption(key = "", mandatory = true) TrainIteration iteration,
|
||||
@CliOption(key = "deploymentId", mandatory = true) String deploymentId) {
|
||||
build.validateDeployment(iteration, StagingRepository.of(deploymentId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Concludes the release of the given {@link TrainIteration}.
|
||||
*
|
||||
|
||||
@@ -18,12 +18,14 @@ package org.springframework.data.release.deployment;
|
||||
import lombok.Data;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.data.release.model.Gpg;
|
||||
import org.springframework.data.release.model.Password;
|
||||
import org.springframework.data.release.model.SupportStatusAware;
|
||||
import org.springframework.data.release.utils.HttpBasicCredentials;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -99,7 +101,10 @@ public class DeploymentProperties implements InitializingBean {
|
||||
@Data
|
||||
public static class MavenCentral {
|
||||
|
||||
private String stagingProfileId;
|
||||
private String username;
|
||||
private Password password;
|
||||
private String centralApiBaseUrl = "https://central.sonatype.com";
|
||||
private Duration validationTimeout = Duration.ofMinutes(5);
|
||||
|
||||
private Gpg gpg;
|
||||
|
||||
@@ -109,10 +114,18 @@ public class DeploymentProperties implements InitializingBean {
|
||||
|
||||
public void validate() {
|
||||
|
||||
if (!StringUtils.hasText(stagingProfileId)) {
|
||||
throw new IllegalArgumentException("No staging profile Id for Maven Central");
|
||||
if (password == null) {
|
||||
throw new IllegalArgumentException("No Maven Publisher API authentication configured");
|
||||
}
|
||||
}
|
||||
|
||||
public HttpBasicCredentials getHttpCredentials() {
|
||||
return new HttpBasicCredentials(username, password);
|
||||
}
|
||||
|
||||
public Password getBearer() {
|
||||
return Password.of(getHttpCredentials().encode());
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* Copyright 2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.release.deployment;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.FileVisitor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.data.release.io.Workspace;
|
||||
import org.springframework.data.release.model.ModuleIteration;
|
||||
import org.springframework.data.release.model.TrainIteration;
|
||||
import org.springframework.data.release.utils.Logger;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
/**
|
||||
* Component to publish artifacts to Maven Central using the Maven Publisher API.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@Component
|
||||
public class MavenPublisher {
|
||||
|
||||
private static final String UPLOAD_URI = "/api/v1/publisher/upload?name={deploymentName}&publishingType=USER_MANAGED";
|
||||
private static final String DEPLOYMENT_STATUS = "/api/v1/publisher/status?id={deploymentId}";
|
||||
private static final String PUBLISH_DEPLOYMENT = "/api/v1/publisher/deployment/{deploymentId}";
|
||||
|
||||
private final Logger logger;
|
||||
private final Workspace workspace;
|
||||
private final DeploymentProperties properties;
|
||||
private final RestOperations restTemplate;
|
||||
|
||||
public MavenPublisher(Logger logger, Workspace workspace, DeploymentProperties properties,
|
||||
RestTemplateBuilder builder) {
|
||||
|
||||
this.logger = logger;
|
||||
this.workspace = workspace;
|
||||
this.properties = properties;
|
||||
this.restTemplate = createOperations(
|
||||
builder.additionalMessageConverters(new FormHttpMessageConverter(), new StringHttpMessageConverter()),
|
||||
properties.getMavenCentral());
|
||||
}
|
||||
|
||||
private static RestOperations createOperations(RestTemplateBuilder templateBuilder,
|
||||
DeploymentProperties.MavenCentral properties) {
|
||||
|
||||
String authentication = "Bearer " + properties.getBearer().toString();
|
||||
return templateBuilder.uriTemplateHandler(new DefaultUriBuilderFactory(properties.getCentralApiBaseUrl()))
|
||||
.defaultHeader(HttpHeaders.AUTHORIZATION, authentication).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and clear the staging repository.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@SneakyThrows
|
||||
public StagingRepository initializeStagingRepository() {
|
||||
|
||||
File stagingDirectory = getStagingDirectory();
|
||||
File zip = getStagingFile();
|
||||
workspace.delete(stagingDirectory.toPath(), "central-staging");
|
||||
|
||||
if (zip.exists()) {
|
||||
if (!zip.delete()) {
|
||||
throw new IllegalStateException(String.format("Unable to delete '%s'", zip));
|
||||
}
|
||||
}
|
||||
|
||||
if (!stagingDirectory.exists() && !stagingDirectory.mkdirs()) {
|
||||
throw new IllegalStateException(String.format("Unable to create '%s'", stagingDirectory));
|
||||
}
|
||||
|
||||
String[] list = stagingDirectory.list();
|
||||
|
||||
if (list == null) {
|
||||
throw new IllegalStateException(String.format("Staging directory cannot be listed '%s'", stagingDirectory));
|
||||
}
|
||||
|
||||
if (list.length != 0) {
|
||||
throw new IllegalStateException(String.format("Staging directory is not empty: %s", Arrays.asList(list)));
|
||||
}
|
||||
|
||||
return getStagingRepository(stagingDirectory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the staging repository.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public StagingRepository getStagingRepository() {
|
||||
return getStagingRepository(getStagingDirectory());
|
||||
}
|
||||
|
||||
private StagingRepository getStagingRepository(File stagingDirectory) {
|
||||
return LocalStagingRepository.of(stagingDirectory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a compressed version of the staged artifacts to Maven Publisher.
|
||||
*
|
||||
* @param iteration
|
||||
* @param deploymentName
|
||||
* @param localStaging
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public StagingRepository upload(ModuleIteration iteration, String deploymentName, StagingRepository localStaging)
|
||||
throws IOException {
|
||||
|
||||
Assert.notNull(localStaging, "Local StagingRepository must not be null");
|
||||
Assert.isTrue(localStaging.isPresent(), "Local StagingRepository must be present");
|
||||
Assert.isInstanceOf(LocalStagingRepository.class, localStaging);
|
||||
|
||||
File zipFile = compressStagedArtifacts(iteration.getTrainIteration(), (LocalStagingRepository) localStaging);
|
||||
|
||||
return uploadStagingFile(iteration, deploymentName, zipFile);
|
||||
}
|
||||
|
||||
private StagingRepository uploadStagingFile(ModuleIteration iteration, String deploymentName, File zipFile) {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
headers.setAccept(Collections.singletonList(MediaType.TEXT_PLAIN));
|
||||
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("bundle", new FileSystemResource(zipFile));
|
||||
|
||||
logger.log(iteration, "🚛 Uploading staging file …");
|
||||
ResponseEntity<String> upload = restTemplate.postForEntity(UPLOAD_URI, new HttpEntity<>(body, headers),
|
||||
String.class, Collections.singletonMap("deploymentName", deploymentName));
|
||||
|
||||
if (upload.getStatusCode().is2xxSuccessful()) {
|
||||
|
||||
String deploymentId = upload.getBody();
|
||||
logger.log(iteration, "📦 Staging file uploaded successfully. Created deploymentId '%s'", deploymentId);
|
||||
return StagingRepository.of(deploymentId);
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
String.format("😵💫 Staging upload of '%s' failed: %s %s", zipFile, upload.getStatusCode(), upload.getBody()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the deployment status.
|
||||
*
|
||||
* @param stagingRepository
|
||||
* @return
|
||||
*/
|
||||
public DeploymentStatus getStatus(StagingRepository stagingRepository) {
|
||||
|
||||
Assert.notNull(stagingRepository, "StagingRepository must not be null");
|
||||
Assert.isTrue(stagingRepository.isPresent(), "StagingRepository must be present");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
|
||||
ResponseEntity<DeploymentStatus> status = restTemplate.exchange(DEPLOYMENT_STATUS, HttpMethod.POST,
|
||||
new HttpEntity<>(headers), DeploymentStatus.class,
|
||||
Collections.singletonMap("deploymentId", stagingRepository.getId()));
|
||||
|
||||
if (status.getStatusCode().is2xxSuccessful()) {
|
||||
return status.getBody();
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
String.format("😵💫 Obtaining deployment status for deploymentId '%s' failed: %s %s", stagingRepository,
|
||||
status.getStatusCode(), status.getBody()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the deployment to Maven Central.
|
||||
*
|
||||
* @param iteration
|
||||
* @param stagingRepository
|
||||
*/
|
||||
public void publish(TrainIteration iteration, StagingRepository stagingRepository) {
|
||||
|
||||
Assert.notNull(stagingRepository, "StagingRepository must not be null");
|
||||
Assert.isTrue(stagingRepository.isPresent(), "StagingRepository must be present");
|
||||
|
||||
logger.log(iteration, "🛳️ Publishing deployment '%s'…", stagingRepository.getId());
|
||||
|
||||
ResponseEntity<String> publish = restTemplate.postForEntity(PUBLISH_DEPLOYMENT, null, String.class,
|
||||
Collections.singletonMap("deploymentId", stagingRepository.getId()));
|
||||
|
||||
if (publish.getStatusCodeValue() == HttpStatus.NO_CONTENT.value()) {
|
||||
logger.log(iteration, "🚀 Au revoir. Bye bye. See you later at Maven Central.");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new IllegalStateException(String.format("😵💫 Publishing deployment for deploymentId '%s' failed: %s %s",
|
||||
stagingRepository, publish.getStatusCode(), publish.getBody()));
|
||||
}
|
||||
|
||||
File compressStagedArtifacts(TrainIteration iteration, LocalStagingRepository stagingDirectory) throws IOException {
|
||||
|
||||
File zipFile = getStagingFile();
|
||||
|
||||
logger.log(iteration, "🗜️ Creating staging file '%s'…", zipFile);
|
||||
FileOutputStream zip = new FileOutputStream(zipFile);
|
||||
ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(zip));
|
||||
|
||||
AtomicLong counter = new AtomicLong();
|
||||
Path root = stagingDirectory.getFile().toPath();
|
||||
Files.walkFileTree(root, new FileVisitor<Path>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
|
||||
if (file.getFileName().toString().contains("maven-metadata.")
|
||||
|| file.getFileName().toString().startsWith(".")) {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
String entry = root.relativize(file).toString();
|
||||
zos.putNextEntry(new ZipEntry(entry));
|
||||
Files.copy(file, zos);
|
||||
zos.closeEntry();
|
||||
|
||||
counter.incrementAndGet();
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
|
||||
throw exc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
|
||||
if (exc != null) {
|
||||
throw exc;
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
|
||||
zos.close();
|
||||
|
||||
if (counter.get() == 0) {
|
||||
throw new IllegalStateException(String.format("Staging directory '%s' empty", stagingDirectory));
|
||||
}
|
||||
|
||||
logger.log(iteration, "🗜️ Created staging file '%s' with %d files", zipFile, counter.get());
|
||||
|
||||
return zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the deployment.
|
||||
*
|
||||
* @param iteration
|
||||
* @param deploymentId
|
||||
* @return
|
||||
* @throws TimeoutException
|
||||
*/
|
||||
public DeploymentStatus validate(TrainIteration iteration, StagingRepository deploymentId)
|
||||
throws TimeoutException, InterruptedException {
|
||||
|
||||
long maxWaitSeconds = TimeUnit.MILLISECONDS
|
||||
.toSeconds(properties.getMavenCentral().getValidationTimeout().toMillis());
|
||||
|
||||
Instant start = Instant.now();
|
||||
boolean validating = false;
|
||||
while (Duration.between(start, Instant.now()).get(ChronoUnit.SECONDS) < maxWaitSeconds) {
|
||||
|
||||
DeploymentStatus status = getStatus(deploymentId);
|
||||
|
||||
switch (status.getDeploymentState()) {
|
||||
|
||||
case PENDING:
|
||||
case VALIDATING:
|
||||
if (!validating) {
|
||||
logger.log(iteration, "⏳ Validation. Waiting for completion…");
|
||||
validating = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case VALIDATED:
|
||||
case PUBLISHED:
|
||||
logger.log(iteration, "✅ Validation successful.");
|
||||
return status;
|
||||
|
||||
case FAILED:
|
||||
logger.log(iteration, "⚠️ Validation failed: %s", status.getErrorDetail());
|
||||
throw new IllegalStateException("Deployment Validation failed");
|
||||
}
|
||||
|
||||
TimeUnit.SECONDS.sleep(5);
|
||||
}
|
||||
|
||||
throw new TimeoutException(String.format("Validation timeout '%d seconds' exceeded: %d seconds", maxWaitSeconds,
|
||||
Duration.between(start, Instant.now()).get(ChronoUnit.SECONDS)));
|
||||
}
|
||||
|
||||
File getStagingDirectory() {
|
||||
return new File(workspace.getStagingDirectory(), "central-staging");
|
||||
}
|
||||
|
||||
File getStagingFile() {
|
||||
return new File(workspace.getStagingDirectory(), "central-staging.zip");
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Data
|
||||
public static class DeploymentStatus {
|
||||
|
||||
private String deploymentId;
|
||||
private DeploymentState deploymentState;
|
||||
private Map<String, List<String>> errors = new LinkedHashMap<>();
|
||||
private List<String> purls = new ArrayList<>();
|
||||
|
||||
public String getErrorDetail() {
|
||||
|
||||
StringBuilder errorMessage = new StringBuilder();
|
||||
|
||||
errorMessage.append("\n\n").append("Deployment ").append(getDeploymentId()).append(" failed\n");
|
||||
|
||||
for (Map.Entry<String, List<String>> errorCategory : getErrors().entrySet()) {
|
||||
|
||||
errorMessage.append(errorCategory.getKey()).append(":\n");
|
||||
|
||||
for (String error : errorCategory.getValue()) {
|
||||
errorMessage.append(" - ").append(error).append("\n");
|
||||
}
|
||||
errorMessage.append("\n");
|
||||
}
|
||||
|
||||
return errorMessage.toString();
|
||||
}
|
||||
|
||||
enum DeploymentState {
|
||||
PENDING, VALIDATING, VALIDATED, PUBLISHING, PUBLISHED, FAILED
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class LocalStagingRepository extends StagingRepository {
|
||||
|
||||
private final File file;
|
||||
|
||||
LocalStagingRepository(File file) {
|
||||
super(String.format("central-staging::default::file://%s", file.getAbsolutePath()));
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public static StagingRepository of(File file) {
|
||||
|
||||
Assert.notNull(file, "File must not be null!");
|
||||
Assert.isTrue(file.exists(), () -> String.format("File '%s' must exist!", file));
|
||||
|
||||
return new LocalStagingRepository(file);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.release.deployment;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
@@ -24,12 +22,19 @@ import org.springframework.util.ObjectUtils;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@Value(staticConstructor = "of")
|
||||
public class StagingRepository {
|
||||
|
||||
public static final StagingRepository EMPTY = StagingRepository.of("");
|
||||
|
||||
String id;
|
||||
private final String id;
|
||||
|
||||
protected StagingRepository(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public static StagingRepository of(String id) {
|
||||
return new StagingRepository(id);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return ObjectUtils.isEmpty(id);
|
||||
@@ -47,4 +52,23 @@ public class StagingRepository {
|
||||
|
||||
return "(empty)";
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof StagingRepository)) {
|
||||
return false;
|
||||
}
|
||||
StagingRepository that = (StagingRepository) o;
|
||||
return ObjectUtils.nullSafeEquals(id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return ObjectUtils.nullSafeHashCode(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@ import org.springframework.stereotype.Component;
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "io")
|
||||
class IoProperties {
|
||||
public class IoProperties {
|
||||
|
||||
private File workDir, logs;
|
||||
private File workDir, stagingDir, logs;
|
||||
|
||||
public void setWorkDir(String workDir) {
|
||||
|
||||
@@ -43,4 +43,10 @@ class IoProperties {
|
||||
this.workDir = new File(workDir.replace("~", FileUtils.getUserDirectoryPath()));
|
||||
}
|
||||
|
||||
public void setStagingDir(String stagingDir) {
|
||||
|
||||
log.info(String.format("🔧 Using %s as staging directory!", stagingDir));
|
||||
this.stagingDir = new File(stagingDir.replace("~", FileUtils.getUserDirectoryPath()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.release.io;
|
||||
|
||||
import static org.springframework.data.release.utils.StreamUtils.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -31,17 +29,14 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.Scanner;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
import org.springframework.data.release.model.Project;
|
||||
import org.springframework.data.release.model.Projects;
|
||||
import org.springframework.data.release.model.SupportedProject;
|
||||
import org.springframework.data.release.utils.Logger;
|
||||
@@ -52,6 +47,7 @@ import org.springframework.util.Assert;
|
||||
* Abstraction of the workspace that is used to work with the {@link Project}'s repositories, execute builds, etc.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@@ -61,7 +57,6 @@ public class Workspace {
|
||||
private static final Charset UTF_8 = StandardCharsets.UTF_8;
|
||||
|
||||
@NonNull IoProperties ioProperties;
|
||||
@NonNull ResourcePatternResolver resolver;
|
||||
@NonNull Logger logger;
|
||||
|
||||
/**
|
||||
@@ -73,6 +68,15 @@ public class Workspace {
|
||||
return ioProperties.getWorkDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current staging directory.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public File getStagingDirectory() {
|
||||
return ioProperties.getStagingDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current logs directory.
|
||||
*
|
||||
@@ -90,35 +94,23 @@ public class Workspace {
|
||||
public void cleanup() throws IOException {
|
||||
|
||||
delete(getWorkingDirectory().toPath(), "workspace");
|
||||
delete(getStagingDirectory().toPath(), "staging");
|
||||
delete(getLogsDirectory().toPath(), "logs");
|
||||
}
|
||||
|
||||
private void delete(Path path, String type) throws IOException {
|
||||
public void delete(Path path, String type) throws IOException {
|
||||
|
||||
logger.log("Workspace", "Cleaning up %s directory at %s.", type, path.toAbsolutePath());
|
||||
|
||||
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
|
||||
if (!path.equals(dir)) {
|
||||
Files.delete(dir);
|
||||
}
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
purge(path, it -> !path.equals(it));
|
||||
}
|
||||
|
||||
public void purge(Path path, Predicate<Path> filter) throws IOException {
|
||||
|
||||
if (!path.toFile().exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
|
||||
|
||||
@Override
|
||||
@@ -187,18 +179,6 @@ public class Workspace {
|
||||
return new File(getProjectDirectory(project), name);
|
||||
}
|
||||
|
||||
public Stream<File> getFiles(String pattern, SupportedProject project) {
|
||||
|
||||
File projectDirectory = getProjectDirectory(project);
|
||||
String patternToLookup = String.format("file:%s/%s", projectDirectory.getAbsolutePath(), pattern);
|
||||
|
||||
try {
|
||||
return Arrays.stream(resolver.getResources(patternToLookup)).map(wrap(Resource::getFile));
|
||||
} catch (IOException o_O) {
|
||||
throw new RuntimeException(o_O);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean processFile(String filename, SupportedProject project, LineCallback callback) {
|
||||
|
||||
File file = getFile(filename, project);
|
||||
|
||||
@@ -38,9 +38,13 @@ public class HttpBasicCredentials {
|
||||
*/
|
||||
public String toString() {
|
||||
|
||||
return "Basic ".concat(encode());
|
||||
}
|
||||
|
||||
public String encode() {
|
||||
|
||||
String header = username.concat(":").concat(password.toString());
|
||||
byte[] encodedAuth = Base64.getEncoder().encode(header.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
return "Basic ".concat(new String(encodedAuth));
|
||||
return new String(encodedAuth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,28 +8,25 @@ git.gpg.passphrase=${GIT_SIGNING_KEY_PASSWORD}
|
||||
|
||||
# Spring OpenSource Artifactory
|
||||
deployment.opensource.username=${REPO_SPRING_IO_USR}
|
||||
deployment.opensource.password=${REPO_SPRING_IO_PSW}
|
||||
deployment.opensource.api-key=${REPO_SPRING_IO_PSW}
|
||||
deployment.settings-xml=${SETTINGS_XML}
|
||||
|
||||
# Spring Commercial Artifactory
|
||||
deployment.commercial.username=${COMMERCIAL_USR}
|
||||
deployment.commercial.password=${COMMERCIAL_PSW}
|
||||
deployment.commercial.api-key=${COMMERCIAL_PSW}
|
||||
|
||||
deployment.maven-central.stagingProfileId=${STAGING_PROFILE_ID}
|
||||
|
||||
deployment.maven-central.gpg.keyname=003C0425
|
||||
deployment.maven-central.gpg.passphrase=${MAVEN_SIGNING_KEY_PASSWORD}
|
||||
deployment.maven-central.gpg.executable=/usr/bin/gpg
|
||||
deployment.maven-central.username=${CENTRAL_TOKEN_USERNAME}
|
||||
deployment.maven-central.password=${CENTRAL_TOKEN_PASSWORD}
|
||||
|
||||
sagan.key=n/a
|
||||
project-service.key=n/a
|
||||
|
||||
maven.maven-home=${MAVEN_HOME}
|
||||
maven.console-logger=false
|
||||
maven.local-repository=${RELEASE_TOOLS_MAVEN_REPOSITORY}
|
||||
maven.parallelize=true
|
||||
|
||||
io.workDir=dist
|
||||
io.workDir=${WORK_DIR}/dist
|
||||
io.staging-dir=${WORK_DIR}/staging
|
||||
io.logs=${LOGS_DIR}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
spring.main.banner-mode=off
|
||||
io.work-dir=~/temp/spring-data-shell/workspace
|
||||
io.staging-dir=~/temp/spring-data-shell/staging
|
||||
io.logs=logs
|
||||
|
||||
# Maven setup
|
||||
@@ -35,7 +36,6 @@ deployment.commercial.staging-repository=spring-enterprise-maven-stage-local
|
||||
deployment.commercial.target-repository=spring-enterprise-maven-prod-local
|
||||
deployment.commercial.distribution-repository=spring-enterprise-maven-prod-local
|
||||
deployment.commercial.project=spring
|
||||
deployment.maven-central.staging-profile-id=2a29ff48cbb4b
|
||||
|
||||
# deployment.commercial.distribution-repository=
|
||||
# deployment.commercial.username <- local, for build
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.release.deployment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.data.release.io.IoProperties;
|
||||
import org.springframework.data.release.io.Workspace;
|
||||
import org.springframework.data.release.model.Iteration;
|
||||
import org.springframework.data.release.model.Password;
|
||||
import org.springframework.data.release.model.ReleaseTrains;
|
||||
import org.springframework.data.release.utils.Logger;
|
||||
|
||||
/**
|
||||
* Tests for {@link MavenPublisher}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class MavenPublisherTests {
|
||||
|
||||
@TempDir File stagingDir;
|
||||
private MavenPublisher publisher;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
|
||||
IoProperties io = new IoProperties();
|
||||
io.setStagingDir(stagingDir.getAbsolutePath());
|
||||
|
||||
Workspace workspace = new Workspace(io, new Logger());
|
||||
DeploymentProperties deploymentProperties = new DeploymentProperties();
|
||||
|
||||
DeploymentProperties.MavenCentral central = new DeploymentProperties.MavenCentral();
|
||||
central.setUsername("foo");
|
||||
central.setPassword(Password.of("bar"));
|
||||
deploymentProperties.setMavenCentral(central);
|
||||
|
||||
publisher = new MavenPublisher(new Logger(), workspace, deploymentProperties, new RestTemplateBuilder());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInitializeEmptyStagingRepository() {
|
||||
|
||||
MavenPublisher.LocalStagingRepository stagingRepository = (MavenPublisher.LocalStagingRepository) publisher
|
||||
.initializeStagingRepository();
|
||||
|
||||
assertThat(stagingRepository.getFile()).isEqualTo(new File(stagingDir, "central-staging"));
|
||||
assertThat(stagingRepository.getFile()).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPurgeStagingRepository() throws IOException {
|
||||
|
||||
File inner = new File(stagingDir, "central-staging/foo");
|
||||
inner.mkdirs();
|
||||
|
||||
Files.write(new File(stagingDir, "central-staging.zip").toPath(), "foo".getBytes());
|
||||
|
||||
publisher.initializeStagingRepository();
|
||||
|
||||
assertThat(stagingDir.list()).hasSize(1).contains("central-staging");
|
||||
assertThat(new File(stagingDir, "central-staging")).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCompressStagingContent() throws IOException {
|
||||
|
||||
MavenPublisher.LocalStagingRepository stagingRepository = (MavenPublisher.LocalStagingRepository) publisher
|
||||
.initializeStagingRepository();
|
||||
|
||||
File directory = new File(publisher.getStagingDirectory(), "foo/bar");
|
||||
directory.mkdirs();
|
||||
|
||||
Files.write(new File(directory, "baz.txt").toPath(), "foo".getBytes());
|
||||
Files.write(new File(directory, ".DS_Store").toPath(), "foo".getBytes());
|
||||
Files.write(new File(directory, "maven-metadata.xml").toPath(), "foo".getBytes());
|
||||
Files.write(new File(directory, "maven-metadata.xml.md5").toPath(), "foo".getBytes());
|
||||
|
||||
publisher.compressStagedArtifacts(ReleaseTrains.Z.getIteration(Iteration.M1), stagingRepository);
|
||||
|
||||
ZipFile zf = new ZipFile(publisher.getStagingFile());
|
||||
List<String> entries = Collections.list(zf.entries()).stream().map(ZipEntry::getName).collect(Collectors.toList());
|
||||
zf.close();
|
||||
|
||||
assertThat(entries).hasSize(1).contains("foo/bar/baz.txt");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user