Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5d70db3
Make TaskModrinthUpload untracked
ScoreUnder Jul 30, 2026
67987f5
Keep our canonical ModrinthExtension around
ScoreUnder Jul 30, 2026
9e3a279
Make AdditionalFileDSL configuration-cache compatible
ScoreUnder Jul 30, 2026
199a79a
WIP: Remove Project from ModrinthExtension
ScoreUnder Jul 30, 2026
9bbec5e
Add legacyFile back in & complete Project-free migration
ScoreUnder Jul 30, 2026
954b54e
Make untyped additional files configuration-cache compatible
ScoreUnder Jul 30, 2026
a48c1c9
Make changelog property CC compatible
ScoreUnder Jul 30, 2026
9c01a3e
replaceAll -> replace
ScoreUnder Jul 30, 2026
fbfb306
Make file property CC compatible
ScoreUnder Jul 30, 2026
7fa294f
Deprecate and remove auto-add depends
ScoreUnder Jul 30, 2026
34cdb81
Deprecate getUploadFile()
ScoreUnder Jul 30, 2026
b169dd0
Wire getFailSilently()
ScoreUnder Jul 30, 2026
e296591
Wire versionNumber/versionName
ScoreUnder Jul 30, 2026
787b248
Partially remove Util.api(Project)
ScoreUnder Jul 30, 2026
3d12ea6
Make TaskModrinthSyncBody untracked too
ScoreUnder Jul 30, 2026
c5418dc
Split up modrinth API settings into its own interface
ScoreUnder Jul 30, 2026
11016f5
Wire debug mode and project ID properties
ScoreUnder Jul 30, 2026
789194c
Get API URL in log statement from API settings
ScoreUnder Jul 30, 2026
ed7e408
Perform loader autodetection at configure time
ScoreUnder Jul 30, 2026
4ee4173
Perform loader validation early
ScoreUnder Jul 30, 2026
2efcacc
Move game version autodetection to configuration time
ScoreUnder Jul 30, 2026
15a878f
Use InvalidUserDataException for misconfiguration
ScoreUnder Jul 30, 2026
afadb85
Wire dependencies properties
ScoreUnder Jul 30, 2026
eac42bc
Make Dependency serializable just in case
ScoreUnder Jul 30, 2026
d2b6f0f
Wire versionType through and move validity check earlier
ScoreUnder Jul 30, 2026
c2bea05
Wire a few misc props up to TaskModrinthSyncBody
ScoreUnder Jul 30, 2026
b9b8090
Remove Util.{ext,resolveVersionNumber}
ScoreUnder Jul 30, 2026
3ffe715
replaceAll -> replace again, this time in SyncBody
ScoreUnder Jul 30, 2026
37fba60
Tidy up exception handling behaviour a little
ScoreUnder Jul 30, 2026
2849172
Mark as compatible with configuration cache
ScoreUnder Jul 30, 2026
7747ae5
Remove redundant null check
ScoreUnder Jul 30, 2026
2d75ab9
Move log statement back up to top
ScoreUnder Jul 30, 2026
4dcd92c
Factor some functions out; move file existence checks before API usage
ScoreUnder Jul 30, 2026
1a2c66c
Pull out two more functions
ScoreUnder Jul 30, 2026
089a017
Use fake token as fallback in debug mode
ScoreUnder Jul 31, 2026
f63c0d0
Detect loaders/versions as late as we can
ScoreUnder Jul 31, 2026
bc3b027
Log which files are due to be uploaded in debug mode
ScoreUnder Jul 31, 2026
156262a
Move game version detection and loader detection out to dedicated cla…
ScoreUnder Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 67 additions & 33 deletions src/main/java/com/modrinth/minotaur/Minotaur.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package com.modrinth.minotaur;

import com.modrinth.minotaur.dependencies.container.NamedDependency;
import com.modrinth.minotaur.request.ModrinthApiSettings;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;

import static com.modrinth.minotaur.Util.ext;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.modrinth.minotaur.gameversion.GameVersionDetection.detectGameVersions;
import static com.modrinth.minotaur.loader.LoaderDetection.detectLoaders;

/**
* The main class for Minotaur.
Expand All @@ -20,55 +28,81 @@ public class Minotaur implements Plugin<Project> {
*/
@Override
public void apply(final Project project) {
project.getExtensions().create("modrinth", ModrinthExtension.class, project);
ModrinthExtension ext = project.getExtensions().create("modrinth", ModrinthExtension.class);
project.getLogger().debug("Created the `modrinth` extension.");

ListProperty<String> defaultLoaders = project.getObjects().listProperty(String.class).empty();
ListProperty<String> defaultGameVersions = project.getObjects().listProperty(String.class).empty();

// Some of the plugins we inspect register their extensions *super* late
// and this is the only thing that's late enough, as far as I can tell.
project.getGradle().projectsEvaluated(g -> {
defaultLoaders.set(detectLoaders(project));
defaultGameVersions.set(detectGameVersions(project));
});

TaskContainer tasks = project.getTasks();
tasks.register("modrinth", TaskModrinthUpload.class, task -> {
task.setGroup("publishing");
task.setDescription("Upload project to Modrinth");
task.dependsOn(tasks.named("assemble"));
task.mustRunAfter(tasks.named("build"));
task.notCompatibleWithConfigurationCache("Fundamentally incompatible with configuration cache");

task.getFile().set(ext.getFile());
task.getAdditionalFiles().set(ext.getAdditionalFileDsl().getAdditionalFiles());
task.getUntypedAdditionalFiles().from(ext.getAdditionalFiles());
task.getChangelog().set(ext.getChangelog());
task.getFailSilently().set(ext.getFailSilently());
Provider<String> resolvedVersion = makeResolvedVersion(project, ext);
task.getProjectId().set(ext.getProjectId());
task.getVersionNumber().set(resolvedVersion);
task.getVersionName().set(ext.getVersionName().orElse(task.getVersionNumber()));
wireUpApiSettings(task.getApiSettings(), ext, resolvedVersion);
task.getIsDryRun().set(ext.getDebugMode());
task.getLoaders().set(getOrDefaultLoaders(ext, defaultLoaders));
task.getGameVersions().set(getOrDefaultGameVersions(ext, defaultGameVersions));
task.getDependencies().set(ext.getDependencies().zip(ext.getNamedDependencies(), (deps, named) ->
Stream.concat(
named.stream().map(NamedDependency::getDependency),
deps.stream()
).collect(Collectors.toList())
));
task.getVersionType().set(ext.getVersionType());
});
project.getLogger().debug("Registered the `modrinth` task.");

tasks.register("modrinthSyncBody", TaskModrinthSyncBody.class, task -> {
task.setGroup("publishing");
task.setDescription("Sync project description to Modrinth");
task.notCompatibleWithConfigurationCache("Fundamentally incompatible with configuration cache");

wireUpApiSettings(task.getApiSettings(), ext, makeResolvedVersion(project, ext));
task.getProjectId().set(ext.getProjectId());
task.getSyncBodyFrom().set(ext.getSyncBodyFrom());
task.getIsDryRun().set(ext.getDebugMode());
task.getFailSilently().set(ext.getFailSilently());
});
project.getLogger().debug("Registered the `modrinthSyncBody` task.");
project.getLogger().debug("Successfully applied the Modrinth plugin!");
}

project.afterEvaluate(evaluatedProject -> {
ModrinthExtension ext = ext(evaluatedProject);

if (!ext.getAutoAddDependsOn().getOrElse(true)) {
return;
}

evaluatedProject.getTasks().named("modrinth", TaskModrinthUpload.class).configure(task -> {
task.getWiredInputFiles().from(ext.getFile());
task.getInputs().property("changelog", ext.getChangelog()).optional(true);

ext.getAdditionalFiles().get().forEach(file -> {
if (file == null) {
return;
}
private static Provider<String> makeResolvedVersion(Project project, ModrinthExtension ext) {
return ext.getVersionNumber().orElse(project.getVersion().toString());
}

// Try to get an AbstractArchiveTask from the input file by whatever means possible.
if (file instanceof AbstractArchiveTask) {
task.dependsOn(file);
} else if (file instanceof TaskProvider<?> &&
((TaskProvider<?>) file).get() instanceof AbstractArchiveTask) {
task.dependsOn(((TaskProvider<?>) file).get());
}
});
private static void wireUpApiSettings(ModrinthApiSettings settings, ModrinthExtension ext, Provider<String> resolvedVersion) {
settings.getApiUrl().set(ext.getApiUrl());
settings.getToken().set(ext.getToken().orElse(ext.getDebugMode().map(d -> d ? "mrp-meow" : null)));
settings.getProjectId().set(ext.getProjectId());
settings.getVersionNumber().set(resolvedVersion);
}

evaluatedProject.getLogger().debug("Made the `modrinth` task depend on the upload file and additional files.");
});
});
private static Provider<List<String>> getOrDefaultLoaders(ModrinthExtension ext, Provider<List<String>> defaultLoaders) {
Provider<List<String>> fallback = ext.getDetectLoaders()
.zip(defaultLoaders, (detect, loaders) -> detect ? loaders : Collections.emptyList());
return ext.getLoaders().map(l -> l.isEmpty() ? null : l).orElse(fallback);
}

project.getLogger().debug("Successfully applied the Modrinth plugin!");
private static Provider<List<String>> getOrDefaultGameVersions(ModrinthExtension ext, Provider<List<String>> detectedVersions) {
return ext.getGameVersions().zip(detectedVersions, (v, def) -> v.isEmpty() ? def : v);
}
}
73 changes: 46 additions & 27 deletions src/main/java/com/modrinth/minotaur/ModrinthExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@
import com.modrinth.minotaur.dependencies.container.DependencyDSL;
import masecla.modrinth4j.model.version.ProjectVersion.VersionType;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.file.RegularFile;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.ProviderFactory;

import javax.inject.Inject;
import java.io.File;
import java.util.concurrent.Callable;

/**
* Class defining the extension used for configuring {@link TaskModrinthUpload}. This is done via the {@code modrinth
Expand Down Expand Up @@ -46,30 +55,28 @@ public class ModrinthExtension extends DependencyDSL {
*/
public static final String DEFAULT_VERSION_TYPE = "release";

/**
* @param project The Gradle project that the extension is applied to
*/
public ModrinthExtension(Project project) {
super(project.getObjects());
additionalFileDsl = project.getObjects().newInstance(AdditionalFileDSL.class);
apiUrl = project.getObjects().property(String.class).convention(DEFAULT_API_URL);
token = project.getObjects().property(String.class).convention(project.getProviders().environmentVariable("MODRINTH_TOKEN"));
projectId = project.getObjects().property(String.class);
versionNumber = project.getObjects().property(String.class);
versionName = project.getObjects().property(String.class);
changelog = project.getObjects().property(String.class).convention(DEFAULT_CHANGELOG);
legacyUploadFile = project.getObjects().property(Object.class);
file = project.getObjects().fileProperty().convention(legacyUploadFile.flatMap(o -> Util.resolveFileProperty(project, o)));
additionalFiles = project.getObjects().listProperty(Object.class).empty();
versionType = project.getObjects().property(String.class).convention(DEFAULT_VERSION_TYPE);
gameVersions = project.getObjects().listProperty(String.class).empty();
loaders = project.getObjects().listProperty(String.class).empty();
dependencies = project.getObjects().listProperty(Dependency.class).empty();
failSilently = project.getObjects().property(Boolean.class).convention(false);
detectLoaders = project.getObjects().property(Boolean.class).convention(true);
debugMode = project.getObjects().property(Boolean.class).convention(false);
syncBodyFrom = project.getObjects().property(String.class);
autoAddDependsOn = project.getObjects().property(Boolean.class).convention(true);
@Inject
public ModrinthExtension(ProviderFactory providers, ObjectFactory objects, ProjectLayout layout) {
super(objects);
additionalFileDsl = objects.newInstance(AdditionalFileDSL.class);
apiUrl = objects.property(String.class).convention(DEFAULT_API_URL);
token = objects.property(String.class).convention(providers.environmentVariable("MODRINTH_TOKEN"));
projectId = objects.property(String.class);
versionNumber = objects.property(String.class);
versionName = objects.property(String.class);
changelog = objects.property(String.class).convention(DEFAULT_CHANGELOG);
legacyUploadFile = objects.property(Object.class);
file = objects.fileProperty().convention(resolveLegacyFile(objects, layout, legacyUploadFile));
additionalFiles = objects.listProperty(Object.class).empty();
versionType = objects.property(String.class).convention(DEFAULT_VERSION_TYPE);
gameVersions = objects.listProperty(String.class).empty();
loaders = objects.listProperty(String.class).empty();
dependencies = objects.listProperty(Dependency.class).empty();
failSilently = objects.property(Boolean.class).convention(false);
detectLoaders = objects.property(Boolean.class).convention(true);
debugMode = objects.property(Boolean.class).convention(false);
syncBodyFrom = objects.property(String.class);
autoAddDependsOn = objects.property(Boolean.class).convention(true);
}

public void additionalFiles(Action<? super AdditionalFileDSL> action) {
Expand Down Expand Up @@ -128,9 +135,10 @@ public Property<String> getChangelog() {
}

/**
* @return The upload artifact file. This can be any object type that is resolvable by
* {@link Util#resolveFile(Project, Object)}.
* @return The upload artifact file
* @deprecated Use {@link #getFile()} instead.
*/
@Deprecated
public Property<Object> getUploadFile() {
return this.legacyUploadFile;
}
Expand Down Expand Up @@ -209,8 +217,19 @@ public Property<String> getSyncBodyFrom() {

/**
* @return Whether to automatically add the `dependsOn` information for upload files
* @deprecated No longer does anything; Gradle does this for us.
*/
@Deprecated
public Property<Boolean> getAutoAddDependsOn() {
return autoAddDependsOn;
}

private static Provider<RegularFile> resolveLegacyFile(ObjectFactory objects, ProjectLayout layout, Property<Object> legacyFile) {
ConfigurableFileCollection legacyFiles = objects.fileCollection();
legacyFiles.from((Callable<Object>) legacyFile::getOrNull);

Provider<File> singleLegacyFile = legacyFiles.getElements()
.map(files -> files.isEmpty() ? null : files.iterator().next().getAsFile());
return layout.file(singleLegacyFile);
}
}
61 changes: 46 additions & 15 deletions src/main/java/com/modrinth/minotaur/TaskModrinthSyncBody.java
Original file line number Diff line number Diff line change
@@ -1,47 +1,76 @@
package com.modrinth.minotaur;

import com.google.gson.JsonObject;
import com.modrinth.minotaur.request.ModrinthApiSettings;
import masecla.modrinth4j.endpoints.project.ModifyProject.ProjectModifications;
import masecla.modrinth4j.main.ModrinthAPI;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.UntrackedTask;

import java.util.Objects;
import java.util.regex.Pattern;

import static com.modrinth.minotaur.Util.*;

/**
* A task used to communicate with Modrinth for the purpose of syncing project body with, for example, a README.
*/
public class TaskModrinthSyncBody extends DefaultTask {
@UntrackedTask(because = "edits data remotely on Modrinth")
public abstract class TaskModrinthSyncBody extends DefaultTask {
/**
* @return the Modrinth API settings
*/
@Nested
public abstract ModrinthApiSettings getApiSettings();

/**
* @return The ID of the project to upload the file to.
*/
@Input
public abstract Property<String> getProjectId();

/**
* @return the file to sync the project's description from
*/
@Input
public abstract Property<String> getSyncBodyFrom();

/**
* @return whether the task should only simulate the changes without actually performing them
*/
@Input
public abstract Property<Boolean> getIsDryRun();

/**
* @return whether the build should continue even if the operation failed
*/
@Input
public abstract Property<Boolean> getFailSilently();

/**
* Uploads a body to a project, both of which are specified in {@link ModrinthExtension}.
*/
@TaskAction
public void apply() {
getLogger().lifecycle("Minotaur: {}", getClass().getPackage().getImplementationVersion());
ModrinthExtension ext = ext(getProject());
try {
if (ext.getSyncBodyFrom() == null) {
throw new GradleException("Sync project body task was called, but `syncBodyFrom` was null!");
}

ModrinthAPI api = api(getProject());
ModrinthAPI api = Util.api(getLogger(), getApiSettings());

// This isn't used until later, but resolve it early anyway to throw invalid IDs early
String id = Objects.requireNonNull(
api.projects().getProjectIdBySlug(ext.getProjectId().get()).join(),
"Failed to resolve project ID: " + ext.getProjectId().get()
api.projects().getProjectIdBySlug(getProjectId().get()).join(),
"Failed to resolve project ID: " + getProjectId().get()
);
getLogger().debug("Syncing body to project {}", id);

Pattern excludeRegex = Pattern.compile("<!-- modrinth_exclude\\.start -->.*?<!-- modrinth_exclude\\.end -->", Pattern.DOTALL);
String body = ext.getSyncBodyFrom().get().replaceAll("\r\n", "\n");
String body = getSyncBodyFrom().get().replace("\r\n", "\n");
body = excludeRegex.matcher(body).replaceAll("");

if (ext.getDebugMode().get()) {
if (getIsDryRun().get()) {
JsonObject data = new JsonObject();
data.addProperty("body", body);
getLogger().lifecycle("Full data to be sent for upload: {}", data);
Expand All @@ -50,11 +79,13 @@ public void apply() {
}

api.projects().modify(id, ProjectModifications.builder().body(body).build()).join();
getLogger().lifecycle("Successfully synced body to project {}.", ext.getProjectId().get());
getLogger().lifecycle("Successfully synced body to project {}.", getProjectId().get());
} catch (Exception e) {
if (ext.getFailSilently().get()) {
if (getFailSilently().get()) {
getLogger().info("Failed to sync body to Modrinth. Check logs for more info.");
getLogger().error("Modrinth body sync failed silently.", e);
} else if (e instanceof GradleException) {
throw (GradleException) e;
} else {
throw new GradleException("Failed to sync project body! " + e.getMessage(), e);
}
Expand Down
Loading