Skip to content

Commit

Permalink
Merge pull request #405 from smallrye/new-release-script
Browse files Browse the repository at this point in the history
New Release Script
  • Loading branch information
jponge authored Jan 4, 2021
2 parents 0df4b37 + a92c3c3 commit 0ab0835
Show file tree
Hide file tree
Showing 23 changed files with 891 additions and 430 deletions.
69 changes: 69 additions & 0 deletions .build/Helper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package helpers;

import org.kohsuke.github.GHRelease;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GHTag;

import java.io.IOException;
import java.util.Collection;
import java.util.function.Supplier;


public class Helper {

public static void info(String msg, Object... params) {
System.out.println("[INFO] " + String.format(msg, params));
}

public static void completed(String msg, Object... params) {
System.out.println("[DONE] " + String.format(msg, params));
}

public static void success(String msg, Object... params) {
System.out.println("[WARN] " + String.format(msg, params));
}

public static void warn(String msg, Object... params) {
System.out.println("[WARN] " + String.format(msg, params));
}

public static void fail(String msg, Object... params) {
fail(String.format(msg, params), 2);
}

public static void fail(String msg, int code) {
System.out.println("[FAIL] " + msg);
System.exit(code);
}

public static void failIfTrue(Supplier<Boolean> predicate, String message, Object... params) {
if (predicate.get()) {
fail(message, params);
}
}

public static <T> T first(Collection<T> collection) {
if (collection.isEmpty()) {
fail("Cannot retrieve the first item from an empty collection");
}
return collection.iterator().next();
}

public static String computeNextVersion(GHRepository repository, GHTag tag, boolean micro) throws IOException {
info("Retrieving release associated with tag %s", tag.getName());
GHRelease name = repository.getReleaseByTagName(tag.getName());
if (name == null) {
warn("No release associated with tag %s", tag.getName());
}
String[] segments = tag.getName().split("\\.");
if (segments.length < 3) {
fail("Invalid version %s, number of segments must be at least 3, found %d", tag.getName(), segments.length);
}

if (micro) {
return String.format("%s.%s.%d", segments[0], segments[1], Integer.parseInt(segments[2]) + 1);
} else {
return String.format("%s.%d.0", segments[0], Integer.parseInt(segments[1]) + 1);
}
}
}
164 changes: 164 additions & 0 deletions .build/PostRelease.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.kohsuke:github-api:1.117
//DEPS info.picocli:picocli:4.5.0
//SOURCES Helper.java

import org.kohsuke.github.*;
import picocli.CommandLine;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

import static helpers.Helper.*;

/**
* Script run after the release.
* The script does the following action in this order:
* - checks that the previous milestone is closed, close it if not
* - checks that the next milestone exists, or create it if not
* - creates the Github release and compute the release notes
* <p>
* Run with `./PostRelease.java --token=GITHUB_TOKEN --release-version=version
* <p>
* 1. The github token is mandatory.
* <p>
* The version is taken from the last tag if not set.
*/
@CommandLine.Command(name = "post-release", mixinStandardHelpOptions = true, version = "0.1",
description = "Post-Release Check")
public class PostRelease implements Callable<Integer> {

@CommandLine.Option(names = "--token", description = "The Github Token", required = true)
private String token;

@CommandLine.Option(names = "--release-version", description = "Set the released version", required = true)
private String releaseVersion;

private static final String REPO = "smallrye/smallrye-mutiny";

public static void main(String... args) {
int exitCode = new CommandLine(new PostRelease()).execute(args);
System.exit(exitCode);
}

@Override
public Integer call() throws Exception {
GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
GHRepository repository = gitHub.getRepository(REPO);

List<GHTag> tags = repository.listTags().toList();
List<GHMilestone> milestones = repository.listMilestones(GHIssueState.ALL).toList();

info("Running post-release checks for release %s", releaseVersion);

// Check that the tag associated with the release version exists
GHTag tag = getTag(releaseVersion, tags);

assert tag != null;

// Check that the milestone exists (this check has already been done during the pre-release checks, just to be double sure)
GHMilestone milestone = milestones.stream().filter(m -> m.getTitle().equals(releaseVersion)).findFirst()
.orElse(null);
failIfTrue(() -> milestone == null, "Unable to find the milestone %s", releaseVersion);
assert milestone != null;
success("Milestone %s found (%s)", milestone.getTitle(), milestone.getHtmlUrl());

success("Post-release check successful");
info("Starting post-release tasks");

// Close milestone
if (milestone.getState() != GHMilestoneState.CLOSED) {
milestone.close();
success("Milestone %s closed (%s)", milestone.getTitle(), milestone.getHtmlUrl());
} else {
success("Milestone %s already closed (%s)", milestone.getTitle(), milestone.getHtmlUrl());
}

// Compute next version
String nextVersion = getNextVersion(releaseVersion);
success("Next version will be %s", nextVersion);

// Create new milestone if it does not already exist
GHMilestone nextMilestone = milestones.stream().filter(m -> m.getTitle().equals(nextVersion)).findFirst()
.orElse(null);
if (nextMilestone != null) {
success("Next milestone (%s) already exists: %s", nextMilestone.getTitle(), nextMilestone.getHtmlUrl());
} else {
nextMilestone = repository.createMilestone(nextVersion, null);
success("Next milestone (%s) created: %s", nextMilestone.getTitle(), nextMilestone.getHtmlUrl());
}

// Compute the release notes and create releases
List<GHIssue> issues = repository.getIssues(GHIssueState.CLOSED, milestone);
String description = createReleaseDescription(issues);
// Check if release already exists
GHRelease existingRelease = repository.getReleaseByTagName(tag.getName());
if (existingRelease != null) {
info("Release %s already exists (%s) - skip release note generation", existingRelease.getName(), existingRelease.getHtmlUrl());
info("Generated release notes:\n%s", description);

if (existingRelease.isDraft()) {
existingRelease.update().draft(false);
success("Marked release %s as non-draft", existingRelease.getName());
}
} else {
GHRelease release = repository.createRelease(releaseVersion)
.name(releaseVersion)
.body(description)
.create();
success("Release %s created: %s", releaseVersion, release.getHtmlUrl());
}

completed("Post-Release done!");

return 0;
}

private GHTag getTag(String releaseVersion, List<GHTag> tags) {
failIfTrue(tags::isEmpty, "No tags found in repository");
Optional<GHTag> first = tags.stream().filter(tag -> tag.getName().equals(releaseVersion)).findFirst();
if (first.isPresent()) {
success("Tag %s found", releaseVersion);
return first.get();
}
fail("Unable to find the tag %s in the repository", releaseVersion);
return null;
}

private String getNextVersion(String v) {
String[] segments = v.split("\\.");
if (segments.length < 3) {
fail("Invalid version %s, number of segments must be at least 3, found %d", v, segments.length);
}

return String.format("%s.%d.0", segments[0], Integer.parseInt(segments[1]) + 1);
}

private String createReleaseDescription(List<GHIssue> issues) throws IOException {
File file = new File("target/differences.md");
String compatibility = "";
if (file.isFile()) {
compatibility += new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
}

StringBuilder desc = new StringBuilder();
desc.append("### Changelog\n\n");
List<String> list = issues.stream().map(this::line).collect(Collectors.toList());
desc.append(String.join("\n", list));
desc.append("\n");

desc.append(compatibility).append("\n");
return desc.toString();
}

private String line(GHIssue issue) {
return String.format(" * [#%d](%s) - %s", issue.getNumber(), issue.getHtmlUrl(), issue.getTitle());
}

}
116 changes: 116 additions & 0 deletions .build/PreRelease.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.kohsuke:github-api:1.117
//DEPS info.picocli:picocli:4.5.0
//SOURCES Helper.java

import org.kohsuke.github.*;
import picocli.CommandLine;

import java.io.File;
import java.nio.file.Files;
import java.util.List;
import java.util.concurrent.Callable;

import static helpers.Helper.*;

/**
* Script checking that the repository can be released.
* It checks that:
* - the new version target an existing milestone
* - the milestone has no opened issues
* - the tag does not exist
*
* Run with `./PreRelease.java --token=GITHUB_TOKEN [--micro] [--release-version=version]
*/
@CommandLine.Command(name = "pre-release", mixinStandardHelpOptions = true, version = "0.1",
description = "Pre-Release Check")
public class PreRelease implements Callable<Integer> {

@CommandLine.Option(names = "--token", description = "The Github Token", required = true)
private String token;

@CommandLine.Option(names = "--micro", description = "To set the release to be a micro release", defaultValue = "false")
private boolean micro;

@CommandLine.Option(names = "--release-version", description = "Set the released version - if not set, the version is computed")
private String target;

private static final String REPO = "smallrye/smallrye-mutiny";

public static void main(String... args) {
int exitCode = new CommandLine(new PreRelease()).execute(args);
System.exit(exitCode);
}

@Override
public Integer call() throws Exception {
if (micro) {
info("Preparing micro release");
} else {
info("Preparing release");
}

GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
GHRepository repository = gitHub.getRepository(REPO);

List<GHTag> tags = repository.listTags().toList();
List<GHMilestone> milestones = repository.listMilestones(GHIssueState.ALL).toList();

String newVersion;
if (target != null && ! target.trim().isEmpty()) {
newVersion = target;
} else {
info("No version passed, computing new version from the last tag");
GHTag tag = getLastTag(tags);
newVersion = computeNextVersion(repository, tag, micro);
}
info("Released version would be %s", newVersion);

info("Running pre-checks...");
failIfTrue(milestones::isEmpty, "No milestones in repository %s", repository.getName());

// Check that the is no tag with the name newVersion
if (tags.stream().anyMatch(t -> t.getName().equals(newVersion))) {
fail("Cannot cut release %s - there is already a tag with this version", newVersion);
}
success("No existing tag named %s", newVersion);

// Check that there is a milestone named newVersion
GHMilestone milestone = milestones.stream().filter(m -> m.getTitle().equals(newVersion)).findFirst().orElse(null);
if (milestone == null) {
fail("No milestone named %s in repository %s - you need to create one", newVersion, repository.getName());
} else {
success("Milestone %s found with %d closed issues",
milestone.getTitle(),
milestone.getClosedIssues());
}

assert milestone != null;
// Check that all the issues associated with the milestone are closed
failIfTrue(() -> milestone.getOpenIssues() != 0, "The milestone %s has %d opened issues, all the issues "
+ "must be closed or moved to another milestone before being able to cut the release. Visit %s for details",
milestone.getTitle(), milestone.getOpenIssues(), milestone.getHtmlUrl().toExternalForm());
success("No remaining (open) issue associated with the milestone %s", milestone.getTitle());

success("Release pre-checks successful!");
info("Writing release version in the /tmp/release-version file");
Files.write(new File("/tmp/release-version").toPath(), newVersion.getBytes());
if (micro) {
//noinspection ResultOfMethodCallIgnored
new File("/tmp/micro").createNewFile();
}

completed("Pre-release checks completed!");

return 0;
}

private GHTag getLastTag(List<GHTag> tags) {
failIfTrue(tags::isEmpty, "No tags found in repository");
GHTag tag = first(tags);
assert tag != null;
success("Last tag retrieved: %s", tag.getName());
return tag;
}

}
Loading

0 comments on commit 0ab0835

Please sign in to comment.