Add command to distribute GitHub workflows.

Closes #96
This commit is contained in:
Mark Paluch
2024-09-02 15:11:56 +02:00
parent b2a57fc45d
commit 9ef357bb35
7 changed files with 129 additions and 21 deletions

View File

@@ -223,6 +223,15 @@ To distribute `ci/pipeline.properties` from Spring Data Build across all modules
$ infra distribute ci-properties $trainIteration
----
===== GitHub Workflow Distribution
To distribute `.github/workflows/project.yml` from Spring Data Build across all modules:
----
$ infra distribute gh-workflow $trainIteration
----
Note that your GitHub token to authenticate against GitHub must have the `workflow` permission.
===== Broken Link Report

View File

@@ -58,7 +58,9 @@ import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialItem.CharArrayType;
import org.eclipse.jgit.transport.CredentialItem.InformationalMessage;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.TagOpt;
import org.eclipse.jgit.transport.URIish;
@@ -76,6 +78,7 @@ import org.springframework.lang.Nullable;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Component to execute Git related operations.
@@ -270,10 +273,35 @@ public class GitOperations {
call(git.push() //
.setRemote("origin") //
.setRefSpecs(new RefSpec(ref.getName())));
.setRefSpecs(new RefSpec(ref.getName()))).forEach(pushResult -> {
handlePushResult(module, pushResult);
});
});
}
private void handlePushResult(ModuleIteration module, PushResult pushResult) {
Set<RemoteRefUpdate.Status> success = new HashSet<>(Arrays.asList(RemoteRefUpdate.Status.AWAITING_REPORT,
RemoteRefUpdate.Status.NOT_ATTEMPTED, RemoteRefUpdate.Status.OK, RemoteRefUpdate.Status.UP_TO_DATE));
if (StringUtils.hasText(pushResult.getMessages())) {
logger.log(module, pushResult.getMessages());
}
for (RemoteRefUpdate remoteUpdate : pushResult.getRemoteUpdates()) {
if (success.contains(remoteUpdate.getStatus())) {
logger.log(module, String.format("✅️ Push done: %s %s", remoteUpdate.getStatus(),
StringUtils.hasText(remoteUpdate.getMessage()) ? remoteUpdate.getMessage() : ""));
continue;
}
logger.warn(module, String.format("⚠️ Push failed: %s %s", remoteUpdate.getStatus(), remoteUpdate.getMessage()));
}
}
public void pushTags(Train train) {
ExecutionUtils.run(executor, train.getModules(), module -> {
@@ -793,7 +821,7 @@ public class GitOperations {
Assert.notNull(project, "Project must not be null!");
logger.log(project, "git add \"filepattern\"");
logger.log(project, "git add \"%s\"", filepattern);
doWithGit(project, git -> {

View File

@@ -107,4 +107,22 @@ public class InfrastructureCommands extends TimedCommand {
infra.distributeCiProperties(iteration);
}
/**
* Distribute GH workflows across all modules using Build as template.
*
* @param iteration
* @throws IOException
* @throws InterruptedException
*/
@CliCommand(value = "infra distribute gh-workflow")
public void distributeGhWorkflow(@CliOption(key = "", mandatory = true) TrainIteration iteration)
throws IOException, InterruptedException {
logger.log(iteration, "Distributing GH Workflow for Spring Data…");
git.prepare(iteration);
infra.distributeGhWorkflow(iteration);
}
}

View File

@@ -28,8 +28,10 @@ import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.function.Predicate;
import org.apache.commons.io.FileUtils;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.git.Branch;
import org.springframework.data.release.git.GitOperations;
@@ -39,10 +41,10 @@ import org.springframework.data.release.model.ModuleIteration;
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.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.data.release.utils.Logger;
import org.springframework.data.util.Predicates;
import org.springframework.data.util.Streamable;
import org.springframework.stereotype.Component;
@@ -70,11 +72,19 @@ public class InfrastructureOperations extends TimedCommand {
* @param iteration
*/
void distributeCiProperties(TrainIteration iteration) {
distributeFile(iteration, "ci/pipeline.properties", "CI Properties", Predicates.isTrue());
}
File master = workspace.getFile(CI_PROPERTIES, iteration.getSupportedProject(Projects.BUILD));
void distributeGhWorkflow(TrainIteration iteration) {
distributeFile(iteration, ".github/workflows/project.yml", "GitHub Actions", project -> project != Projects.BOM);
}
private void distributeFile(TrainIteration iteration, String file, String description,
Predicate<Project> projectFilter) {
File master = workspace.getFile(file, iteration.getSupportedProject(Projects.BUILD));
if (!master.exists()) {
throw new IllegalStateException(String.format("CI Properties file %s does not exist", master));
throw new IllegalStateException(String.format("%s file %s does not exist", description, master));
}
ExecutionUtils.run(executor, iteration, module -> {
@@ -86,29 +96,33 @@ public class InfrastructureOperations extends TimedCommand {
git.checkout(project, branch);
});
verifyExistingPropertyFiles(iteration.getTrain(), master);
Streamable<ModuleIteration> projects = Streamable.of(iteration.getModulesExcept(Projects.BUILD))
.filter(it -> projectFilter.test(it.getProject())).filter(it -> it.getProject().getMaintainer().isCore());
ExecutionUtils.run(executor, Streamable.of(iteration.getModulesExcept(Projects.BUILD)), module -> {
verifyExistingFiles(projects, file, description);
File target = workspace.getFile(CI_PROPERTIES, module.getSupportedProject());
ExecutionUtils.run(executor, projects, module -> {
File target = workspace.getFile(file, module.getSupportedProject());
target.delete();
FileUtils.copyFile(master, target);
git.add(module.getSupportedProject(), CI_PROPERTIES);
git.commit(module, "Update CI properties.", Optional.empty(), false);
git.add(module.getSupportedProject(), file);
git.commit(module, String.format("Update %s.", description), Optional.empty(), false);
git.push(module);
});
}
private void verifyExistingPropertyFiles(Train train, File master) {
private void verifyExistingFiles(Streamable<ModuleIteration> train, String file, String description) {
for (SupportedProject project : train) {
for (ModuleIteration moduleIteration : train) {
File target = workspace.getFile(CI_PROPERTIES, project);
File target = workspace.getFile(file, moduleIteration.getSupportedProject());
if (!target.exists()) {
throw new IllegalStateException(String.format("CI Properties file %s does not exist", master));
throw new IllegalStateException(
String.format("%s file %s does not exist in %s", description, file, moduleIteration.getSupportedProject()));
}
}
}

View File

@@ -45,6 +45,7 @@ public class Project implements Comparable<Project>, Named {
private final @With ArtifactCoordinates additionalArtifacts;
private final @With boolean skipTests;
private final @Getter @With boolean useShortVersionMilestones; // use a short version 2.3.0-RC1 instead of 2.3 RC1 if
private final @Getter @With ProjectMaintainer maintainer;
// true
Project(String key, String name, Tracker tracker) {
@@ -53,13 +54,14 @@ public class Project implements Comparable<Project>, Named {
private Project(String key, String name, String fullName, Tracker tracker) {
this(new ProjectKey(key), name, fullName, Collections.emptySet(), tracker, ArtifactCoordinates.SPRING_DATA, true,
false);
false, ProjectMaintainer.CORE);
}
@java.beans.ConstructorProperties({ "key", "name", "fullName", "dependencies", "tracker", "additionalArtifacts",
"skipTests", "plainVersionMilestones" })
"skipTests", "plainVersionMilestones", "owner" })
private Project(ProjectKey key, String name, String fullName, Collection<Project> dependencies, Tracker tracker,
ArtifactCoordinates additionalArtifacts, boolean skipTests, boolean useShortVersionMilestones) {
ArtifactCoordinates additionalArtifacts, boolean skipTests, boolean useShortVersionMilestones,
ProjectMaintainer maintainer) {
this.key = key;
this.name = name;
@@ -69,6 +71,7 @@ public class Project implements Comparable<Project>, Named {
this.additionalArtifacts = additionalArtifacts;
this.skipTests = skipTests;
this.useShortVersionMilestones = useShortVersionMilestones;
this.maintainer = maintainer;
}
public boolean uses(Tracker tracker) {
@@ -110,7 +113,7 @@ public class Project implements Comparable<Project>, Named {
public Project withDependencies(Project... project) {
return new Project(key, name, fullName, Arrays.asList(project), tracker, additionalArtifacts, skipTests,
useShortVersionMilestones);
useShortVersionMilestones, maintainer);
}
/**

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2024 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
*
* https://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.model;
/**
* @author Mark Paluch
*/
public enum ProjectMaintainer {
CORE, COMMUNITY;
public boolean isCore() {
return this == CORE;
}
public boolean isCommunity() {
return this == COMMUNITY;
}
}

View File

@@ -67,20 +67,23 @@ public class Projects {
.withAdditionalArtifacts(
ArtifactCoordinates.SPRING_DATA.artifacts("spring-data-mongodb-cross-store", "spring-data-mongodb-log4j"));
NEO4J = new Project("DATAGRAPH", "Neo4j", Tracker.GITHUB).withDependencies(COMMONS);
NEO4J = new Project("DATAGRAPH", "Neo4j", Tracker.GITHUB).withDependencies(COMMONS)
.withMaintainer(ProjectMaintainer.COMMUNITY);
SOLR = new Project("DATASOLR", "Solr", Tracker.GITHUB) //
.withDependencies(COMMONS) //
.withFullName("Spring Data for Apache Solr");
COUCHBASE = new Project("DATACOUCH", "Couchbase", Tracker.GITHUB).withDependencies(COMMONS);
COUCHBASE = new Project("DATACOUCH", "Couchbase", Tracker.GITHUB).withDependencies(COMMONS)
.withMaintainer(ProjectMaintainer.COMMUNITY);
CASSANDRA = new Project("DATACASS", "Cassandra", Tracker.GITHUB) //
.withDependencies(COMMONS) //
.withAdditionalArtifacts(ArtifactCoordinates.SPRING_DATA.artifacts("spring-cql"))
.withFullName("Spring Data for Apache Cassandra");
ELASTICSEARCH = new Project("DATAES", "Elasticsearch", Tracker.GITHUB).withDependencies(COMMONS);
ELASTICSEARCH = new Project("DATAES", "Elasticsearch", Tracker.GITHUB).withDependencies(COMMONS)
.withMaintainer(ProjectMaintainer.COMMUNITY);
KEY_VALUE = new Project("DATAKV", "KeyValue", Tracker.GITHUB).withDependencies(COMMONS);