From 9ef357bb3551cb69cd4506640cf79829aa4fae8a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 2 Sep 2024 15:11:56 +0200 Subject: [PATCH] Add command to distribute GitHub workflows. Closes #96 --- readme.adoc | 9 +++++ .../data/release/git/GitOperations.java | 32 +++++++++++++++- .../release/infra/InfrastructureCommands.java | 18 +++++++++ .../infra/InfrastructureOperations.java | 38 +++++++++++++------ .../data/release/model/Project.java | 11 ++++-- .../data/release/model/ProjectMaintainer.java | 33 ++++++++++++++++ .../data/release/model/Projects.java | 9 +++-- 7 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/springframework/data/release/model/ProjectMaintainer.java diff --git a/readme.adoc b/readme.adoc index ac26229..66f9fbf 100644 --- a/readme.adoc +++ b/readme.adoc @@ -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 diff --git a/src/main/java/org/springframework/data/release/git/GitOperations.java b/src/main/java/org/springframework/data/release/git/GitOperations.java index 56ab6e9..f9d3710 100644 --- a/src/main/java/org/springframework/data/release/git/GitOperations.java +++ b/src/main/java/org/springframework/data/release/git/GitOperations.java @@ -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 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 -> { diff --git a/src/main/java/org/springframework/data/release/infra/InfrastructureCommands.java b/src/main/java/org/springframework/data/release/infra/InfrastructureCommands.java index 5ac32a5..8f7efcc 100644 --- a/src/main/java/org/springframework/data/release/infra/InfrastructureCommands.java +++ b/src/main/java/org/springframework/data/release/infra/InfrastructureCommands.java @@ -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); + } + } diff --git a/src/main/java/org/springframework/data/release/infra/InfrastructureOperations.java b/src/main/java/org/springframework/data/release/infra/InfrastructureOperations.java index 2e32249..682ac0a 100644 --- a/src/main/java/org/springframework/data/release/infra/InfrastructureOperations.java +++ b/src/main/java/org/springframework/data/release/infra/InfrastructureOperations.java @@ -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 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 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 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())); } } } diff --git a/src/main/java/org/springframework/data/release/model/Project.java b/src/main/java/org/springframework/data/release/model/Project.java index 908186e..ad2ff75 100644 --- a/src/main/java/org/springframework/data/release/model/Project.java +++ b/src/main/java/org/springframework/data/release/model/Project.java @@ -45,6 +45,7 @@ public class Project implements Comparable, 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, 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 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, 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, Named { public Project withDependencies(Project... project) { return new Project(key, name, fullName, Arrays.asList(project), tracker, additionalArtifacts, skipTests, - useShortVersionMilestones); + useShortVersionMilestones, maintainer); } /** diff --git a/src/main/java/org/springframework/data/release/model/ProjectMaintainer.java b/src/main/java/org/springframework/data/release/model/ProjectMaintainer.java new file mode 100644 index 0000000..acc9dd7 --- /dev/null +++ b/src/main/java/org/springframework/data/release/model/ProjectMaintainer.java @@ -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; + } + +} diff --git a/src/main/java/org/springframework/data/release/model/Projects.java b/src/main/java/org/springframework/data/release/model/Projects.java index bb4f070..d0e33b5 100644 --- a/src/main/java/org/springframework/data/release/model/Projects.java +++ b/src/main/java/org/springframework/data/release/model/Projects.java @@ -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);