Skip to content

Commit f0e05a3

Browse files
authored
Set up automatic publishing from within Gradle (#93)
* chore(build): CF publishing from Gradle * chore(build): Publish a GitHub release as well
1 parent c92dbbd commit f0e05a3

11 files changed

+510
-13
lines changed

build-logic/build.gradle.kts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
plugins {
2+
`java-gradle-plugin`
3+
}
4+
5+
val targetVersion = 11
6+
java {
7+
sourceCompatibility = JavaVersion.toVersion(targetVersion)
8+
targetCompatibility = sourceCompatibility
9+
if (JavaVersion.current() < JavaVersion.toVersion(targetVersion)) {
10+
toolchain.languageVersion = JavaLanguageVersion.of(targetVersion)
11+
}
12+
}
13+
14+
tasks.withType(JavaCompile::class).configureEach {
15+
options.release = targetVersion
16+
options.encoding = "UTF-8"
17+
options.compilerArgs.addAll(listOf("-Xlint:all", "-Xlint:-processing"))
18+
}
19+
20+
dependencies {
21+
implementation(libs.githubApi)
22+
implementation(libs.indra.git)
23+
}
24+
25+
gradlePlugin {
26+
plugins {
27+
register("publish-gh-release") {
28+
id = "org.enginehub.worldeditcui.ghrelease"
29+
description = "Publish a GitHub release"
30+
implementationClass = "org.enginehub.build.worldeditcui.GitHubReleaserPlugin"
31+
}
32+
}
33+
}

build-logic/settings.gradle.kts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
pluginManagement {
2+
repositories {
3+
// mirrors:
4+
// - https://server.bbkr.space/artifactory/libs-release/
5+
// - https://maven.fabricmc.net/
6+
// - gradlePluginPortal
7+
maven(url = "https://repo.stellardrift.ca/repository/stable/") {
8+
name = "stellardriftReleases"
9+
mavenContent { releasesOnly() }
10+
}
11+
maven(url = "https://repo.stellardrift.ca/repository/snapshots/") {
12+
name = "stellardriftSnapshots"
13+
mavenContent { snapshotsOnly() }
14+
}
15+
// maven("https://maven.fabricmc.net/")
16+
// gradlePluginPortal()
17+
}
18+
}
19+
20+
rootProject.name = "worldeditcui-build-logic"
21+
22+
dependencyResolutionManagement {
23+
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
24+
pluginManagement.repositories.forEach(repositories::add)
25+
versionCatalogs {
26+
register(defaultLibrariesExtensionName.get()) {
27+
from(files("../gradle/libs.versions.toml"))
28+
}
29+
}
30+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package org.enginehub.build.worldeditcui;
2+
3+
import org.gradle.api.file.ConfigurableFileCollection;
4+
import org.gradle.api.model.ObjectFactory;
5+
import org.gradle.api.provider.Property;
6+
import org.gradle.api.provider.ProviderFactory;
7+
8+
import javax.inject.Inject;
9+
10+
class GitHubReleaserExtensionImpl implements GithubReleaserExtension {
11+
private final Property<String> enterpriseUrl;
12+
private final Property<String> apiToken;
13+
private final Property<String> releaseTitle;
14+
private final Property<String> releaseBody;
15+
private final Property<String> repository;
16+
private final Property<String> tagName;
17+
private final Property<String> sourceBranch;
18+
private final Property<Boolean> draft;
19+
private final Property<Boolean> prerelease;
20+
private final Property<String> discussionCategoryName;
21+
private final Property<Boolean> generateReleaseNotes;
22+
private final Property<LatestState> makeLatest;
23+
24+
private final ConfigurableFileCollection sourceArtifacts;
25+
26+
@Inject
27+
public GitHubReleaserExtensionImpl(final ObjectFactory objects, final ProviderFactory providers) {
28+
this.enterpriseUrl = objects.property(String.class);
29+
this.apiToken = objects.property(String.class)
30+
.convention(providers.environmentVariable("GITHUB_TOKEN"));
31+
this.releaseTitle = objects.property(String.class);
32+
this.releaseBody = objects.property(String.class).convention("");
33+
this.repository = objects.property(String.class);
34+
this.tagName = objects.property(String.class);
35+
this.sourceBranch = objects.property(String.class);
36+
this.draft = objects.property(Boolean.class).convention(false);
37+
this.prerelease = objects.property(Boolean.class).convention(false);
38+
this.discussionCategoryName = objects.property(String.class);
39+
this.generateReleaseNotes = objects.property(Boolean.class).convention(false);
40+
this.makeLatest = objects.property(LatestState.class).convention(LatestState.TRUE);
41+
42+
this.sourceArtifacts = objects.fileCollection();
43+
}
44+
45+
@Override
46+
public Property<String> getEnterpriseUrl() {
47+
return this.enterpriseUrl;
48+
}
49+
50+
@Override
51+
public Property<String> getApiToken() {
52+
return this.apiToken;
53+
}
54+
55+
@Override
56+
public Property<String> getReleaseName() {
57+
return this.releaseTitle;
58+
}
59+
60+
@Override
61+
public Property<String> getReleaseBody() {
62+
return this.releaseBody;
63+
}
64+
65+
@Override
66+
public Property<String> getRepository() {
67+
return this.repository;
68+
}
69+
70+
@Override
71+
public Property<String> getTagName() {
72+
return this.tagName;
73+
}
74+
75+
@Override
76+
public Property<String> getSourceBranch() {
77+
return this.sourceBranch;
78+
}
79+
80+
@Override
81+
public Property<Boolean> getDraft() {
82+
return this.draft;
83+
}
84+
85+
@Override
86+
public Property<Boolean> getPrerelease() {
87+
return this.prerelease;
88+
}
89+
90+
@Override
91+
public Property<String> getDiscussionCategoryName() {
92+
return this.discussionCategoryName;
93+
}
94+
95+
@Override
96+
public Property<Boolean> getGenerateReleaseNotes() {
97+
return this.generateReleaseNotes;
98+
}
99+
100+
@Override
101+
public Property<LatestState> getMakeLatest() {
102+
return this.makeLatest;
103+
}
104+
105+
@Override
106+
public ConfigurableFileCollection getArtifacts() {
107+
return this.sourceArtifacts;
108+
}
109+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.enginehub.build.worldeditcui;
2+
3+
import net.kyori.indra.git.IndraGitExtension;
4+
import org.eclipse.jgit.lib.Ref;
5+
import org.eclipse.jgit.lib.Repository;
6+
import org.gradle.api.Plugin;
7+
import org.gradle.api.Project;
8+
import org.gradle.api.tasks.TaskContainer;
9+
10+
public class GitHubReleaserPlugin implements Plugin<Project> {
11+
public static final String GITHUB_RELEASE_EXTENSION_NAME = "githubRelease";
12+
public static final String GITHUB_RELEASE_TASK_NAME = "publishToGitHub";
13+
14+
@Override
15+
public void apply(final Project target) {
16+
target.getPlugins().apply("net.kyori.indra.git"); // for git operations
17+
18+
// extension
19+
final GithubReleaserExtension extension = target.getExtensions().create(
20+
GithubReleaserExtension.class,
21+
GITHUB_RELEASE_EXTENSION_NAME,
22+
GitHubReleaserExtensionImpl.class
23+
);
24+
25+
this.configureTasks(target.getTasks(), extension);
26+
this.registerPublishTask(target.getTasks(), extension);
27+
28+
extension.getTagName().convention(target.provider(() -> {
29+
final Ref headTag = target.getExtensions().getByType(IndraGitExtension.class).headTag();
30+
return headTag == null ? null : Repository.shortenRefName(headTag.getName());
31+
}));
32+
}
33+
34+
private void configureTasks(final TaskContainer tasks, final GithubReleaserExtension extension) {
35+
tasks.withType(PublishGitHubRelease.class).configureEach(task -> {
36+
task.getEnterpriseUrl().set(extension.getEnterpriseUrl());
37+
task.getApiToken().set(extension.getApiToken());
38+
});
39+
}
40+
41+
private void registerPublishTask(final TaskContainer tasks, final ReleaseJobParameters sourceParameters) {
42+
tasks.register(GITHUB_RELEASE_TASK_NAME, PublishGitHubRelease.class, task -> {
43+
task.dependsOn("requireClean"); // via indra-git
44+
45+
task.getReleaseName().set(sourceParameters.getReleaseName());
46+
task.getReleaseBody().set(sourceParameters.getReleaseBody());
47+
task.getRepository().set(sourceParameters.getRepository());
48+
task.getTagName().set(sourceParameters.getTagName());
49+
task.getSourceBranch().set(sourceParameters.getSourceBranch());
50+
task.getDraft().set(sourceParameters.getDraft());
51+
task.getPrerelease().set(sourceParameters.getPrerelease());
52+
task.getDiscussionCategoryName().set(sourceParameters.getDiscussionCategoryName());
53+
task.getGenerateReleaseNotes().set(sourceParameters.getGenerateReleaseNotes());
54+
task.getMakeLatest().set(sourceParameters.getMakeLatest());
55+
task.getArtifacts().from(sourceParameters.getArtifacts());
56+
});
57+
}
58+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.enginehub.build.worldeditcui;
2+
3+
import org.gradle.api.provider.Property;
4+
5+
public interface GithubReleaserExtension extends ReleaseJobParameters {
6+
/**
7+
* Get an endpoint override for GitHub.
8+
*
9+
* <p>Only required if using GitHub enterprise.</p>
10+
*
11+
* @return the base url for the GitHub instance
12+
*/
13+
Property<String> getEnterpriseUrl();
14+
15+
/**
16+
* Get the API token used to authenticate with GitHub.
17+
*
18+
* <p>By default, this is read from the {@code GITHUB_TOKEN} environment variable.</p>
19+
*
20+
* @return the api token property
21+
*/
22+
Property<String> getApiToken();
23+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package org.enginehub.build.worldeditcui;
2+
3+
import org.gradle.api.DefaultTask;
4+
import org.gradle.api.GradleException;
5+
import org.gradle.api.InvalidUserDataException;
6+
import org.gradle.api.provider.Property;
7+
import org.gradle.api.tasks.Input;
8+
import org.gradle.api.tasks.Optional;
9+
import org.gradle.api.tasks.TaskAction;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.kohsuke.github.GHRelease;
12+
import org.kohsuke.github.GHReleaseBuilder;
13+
import org.kohsuke.github.GHRepository;
14+
import org.kohsuke.github.GitHub;
15+
import org.kohsuke.github.GitHubBuilder;
16+
import org.kohsuke.github.GitHubRateLimitHandler;
17+
import org.kohsuke.github.connector.GitHubConnectorResponse;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
22+
public abstract class PublishGitHubRelease extends DefaultTask implements ReleaseJobParameters {
23+
24+
@Input
25+
@Optional
26+
public abstract Property<String> getEnterpriseUrl();
27+
28+
@Input
29+
public abstract Property<String> getApiToken();
30+
31+
private GitHub createGitHub() {
32+
final GitHubBuilder builder = new GitHubBuilder();
33+
if (this.getEnterpriseUrl().isPresent()) {
34+
builder.withEndpoint(this.getEnterpriseUrl().get());
35+
}
36+
builder.withOAuthToken(this.getApiToken().get());
37+
builder.withRateLimitHandler(new GitHubRateLimitHandler() {
38+
@Override
39+
public void onError(@NotNull GitHubConnectorResponse response) throws IOException {
40+
getLogger().error(
41+
"Exceeded rate limit while trying to publish release (code {}): {}",
42+
response.statusCode(),
43+
new String(response.bodyStream().readAllBytes())
44+
);
45+
throw new GradleException("Rate limmit exceeded! See log for details");
46+
}
47+
});
48+
49+
try {
50+
return builder.build();
51+
} catch (IOException e) {
52+
this.getLogger().error("Failed to create GitHub instance: {}", e.getMessage(), e);
53+
throw new GradleException("GitHub authentication failed, see log for details");
54+
}
55+
}
56+
57+
@TaskAction
58+
public void doPublish() {
59+
final GitHub gh = this.createGitHub();
60+
61+
final GHRepository repo = runHandlingException(() -> gh.getRepository(this.getRepository().get()));
62+
final GHReleaseBuilder releaseBuilder = repo.createRelease(this.getTagName().get());
63+
64+
if (this.getReleaseName().isPresent()) {
65+
releaseBuilder.name(this.getReleaseName().get());
66+
}
67+
68+
releaseBuilder
69+
.body(this.getReleaseBody().getOrElse(""))
70+
.draft(true)
71+
.prerelease(this.getPrerelease().get());
72+
73+
if (this.getDiscussionCategoryName().isPresent()) {
74+
releaseBuilder.categoryName(this.getDiscussionCategoryName().get());
75+
}
76+
77+
if (this.getSourceBranch().isPresent()) {
78+
releaseBuilder.commitish(this.getSourceBranch().get());
79+
}
80+
81+
// todo: generateReleaseNotes
82+
// todo: makeLatest
83+
84+
// update release content
85+
final GHRelease release = runHandlingException(releaseBuilder::create);
86+
for (final File file : this.getArtifacts()) {
87+
if (!file.isFile()) {
88+
throw new InvalidUserDataException("Release artifact " + file.getAbsolutePath() + " is not a regular file!");
89+
}
90+
runHandlingException(() -> release.uploadAsset(file, determineMimeType(file)));
91+
}
92+
93+
// Now that all elements have been done successfully, if we are non-draft then mark it as non-draft
94+
if (!this.getDraft().get()) {
95+
runHandlingException(release.update().draft(false)::update);
96+
}
97+
}
98+
99+
private String determineMimeType(final File file) {
100+
final String name = file.getName();
101+
if (name.endsWith("jar")) {
102+
return "application/java-archive";
103+
} else if (name.endsWith("zip")) {
104+
return "application/zip";
105+
} else { // unknown // todo: find a better way to determine this
106+
return "application/octet-stream";
107+
}
108+
}
109+
110+
@FunctionalInterface
111+
interface GHCallable<O> {
112+
O execute() throws IOException;
113+
}
114+
115+
private <T> T runHandlingException(final GHCallable<T> item) throws GradleException {
116+
try {
117+
return item.execute();
118+
} catch (final IOException ex) {
119+
this.getLogger().error("Failed to execute GitHub API operation", ex);
120+
throw new GradleException("GitHub API error occurred, see log for details: " + ex.getMessage());
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)