Skip to content

fix: Version from modrinth command error handling and logging - #854

Closed
harrisonablack wants to merge 31 commits into
itzg:mainfrom
harrisonablack:fix/versionFromModrinthProject
Closed

fix: Version from modrinth command error handling and logging #854
harrisonablack wants to merge 31 commits into
itzg:mainfrom
harrisonablack:fix/versionFromModrinthProject

Conversation

@harrisonablack

Copy link
Copy Markdown
Contributor

Reworked the internal logic of VersionFromModrinthProjectsCommand to support #630 and #676.

Version Blame #676

When a specific Modrinth project is blocking a release, a debug log is printed specifying the project that is blocking that version, e.g.

./gradlew run --args="--debug version-from-modrinth-projects --loader=paper --projects=paper:viaversion,paper:viabackwards,paper:griefprevention,paper:discordsrv,paper:pl3xmap:beta"
[mc-image-helper] 23:19:15.426 DEBUG : 26.2: 4/5 projects; missing: griefprevention
[mc-image-helper] 23:19:15.426 DEBUG : 26.1.2: 5/5 projects

Furthermore, if no compatible version is found, the same DEBUG logs are printed per version, as well as a final error;

[mc-image-helper] 23:20:27.039 ERROR : Failed to find a compatible Minecraft version across all projects

Empty Projects and No Projects #630

When passing --projects= with no data, an error is thrown

./gradlew run --args="--debug version-from-modrinth-projects --projects="
[mc-image-helper] 22:37:41.380 ERROR : Invalid parameter provided for 'version-from-modrinth-projects' command: No Modrinth projects parsed successfully, please ensure projects follow "<loader>:<project ID>|<slug>" and are delimited by commas

If no projects provided, another error is thrown.

./gradlew run --args="--debug version-from-modrinth-projects"
[mc-image-helper] 22:38:46.338 ERROR : Invalid parameter provided for 'version-from-modrinth-projects' command: No projects provided, please provide at least one Modrinth project

Returns a list of all minecraft releases, excluding snapshots, betas /
alphas
…esting

Exposes url to override during testing
Produces duplicate logs as the ExceptionHandler will handle errors again
after log
…instead of Generic Exception

Generic exception handled by ExceptionHandler prints trace log,
producing unneeded extra logs
- Class no longer exposes internal process gameVersionArgs
- Added test to ensure logs contain information
- Append existing tests with Psuedo MCAIPI json
@itzg

itzg commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Thanks for addressing these. Glancing at this I'm a bit worried about line count on the diff. Anything you can do to minimize that is good.

@harrisonablack

Copy link
Copy Markdown
Contributor Author

Thanks for addressing these. Glancing at this I'm a bit worried about line count on the diff. Anything you can do to minimize that is good.

Yeah i had a similar instinctual thought, around ~150 lines are from tests, and probably another ~100 from javadoc.

I did update/touch pretty much all the core logic due to the way of logging, however if it is still too much i'm happy to revert the core structural changes and just focus on the retrofiting the fixes

@itzg

itzg commented Aug 29, 2026

Copy link
Copy Markdown
Owner

I did update/touch pretty much all the core logic due to the way of logging, however if it is still too much i'm happy to revert the core structural changes and just focus on the retrofiting the fixes

Thanks for confirming. I'll review and let you know. Is it about ready for a full review?

@harrisonablack harrisonablack left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh should be good, it's getting quite late for me now, but if you have a look over, i can come back tomorrow morning to fix anything up if needed!

@harrisonablack
harrisonablack marked this pull request as ready for review August 29, 2026 15:42

@itzg itzg left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tightening up the algorithm. Just stylist changes needed.

Comment on lines +172 to +176
.withBody(String.format(
"[{\"game_versions\":[\"%s\"],\"id\":\"version-%s\",\"date_published\":\"2026-01-01T00:00:00Z\",\"version_type\":\"release\"}]",
gameVersion,
project
))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to not build JSON string with placeholders. Just a little brittle and easy to miss quotes, etc. Since WireMock supports it, I prefer building dynamic JSON from node objects such as

final ObjectMapper mapper = new ObjectMapper();
JsonNode responseProject = mapper.valueToTree(
new Project()
.setSlug(projectName)
.setId(projectId)
.setTitle("Test"));
stubFor(get("/v2/project/" + projectName)
.willReturn(ok()
.withHeader("Content-Type", "application/json")
.withJsonBody(responseProject)));

Comment on lines -39 to -106
@SuppressWarnings("unused") // will be fixed https://youtrack.jetbrains.com/issue/IDEA-358214/Support-JUnit-5-FieldSource-annotation
static List<Arguments> processGameVersionsArgs = asList(
argumentSet("matches", asList(
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7", "1.21.8")
), "1.21.8"
),
argumentSet("justOneOff", asList(
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7"),
asList("1.21.6", "1.21.7", "1.21.8")
), "1.21.7"
),
argumentSet("mismatch", asList(
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.4", "1.21.5"),
asList("1.21.6", "1.21.7", "1.21.8")
), null
),
argumentSet("fabric-api + nucledoom", asList(
// part of fabric-api
asList("24w46a",
"1.21.4-pre1",
"1.21.4-pre2",
"1.21.4-pre3",
"1.21.4-rc3",
"1.21.4",
"25w02a",
"25w03a",
"25w04a",
"25w05a",
"25w06a",
"25w07a",
"25w08a",
"25w09a",
"25w09b",
"25w10a",
"1.21.5-pre1",
"1.21.5-pre2",
"1.21.5-pre3",
"1.21.5-rc1",
"1.21.5-rc2",
"1.21.5",
"25w14craftmine",
"25w15a",
"25w16a",
"25w17a",
"25w18a",
"25w19a",
"25w20a",
"25w21a",
"1.21.6-pre1",
"1.21.6-pre3",
"1.21.6",
"1.21.7-rc1",
"1.21.7",
"1.21.8",
"25w31a",
"25w32a"),
// part of nucledoom
singletonList("1.21.4")
), "1.21.4"
)
);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did all this go?

@harrisonablack harrisonablack Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the changes of the command, VersionFromModrinthProjectsCommand#processGameVersions was updated to process project refs alongside the List<List<String>>, this allowed logging and errors based on what projects are missing.

I will add back a set of test to cover the test regression.

@Test
void rejectsEmptyProjects() {
final VersionFromModrinthProjectsCommand command = new VersionFromModrinthProjectsCommand();
command.projects = List.of();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even with tests I don't like fields to be directly set. Even more so I prefer the CLI aspect to be re-tested in these various cases.

I know it's a whole lot more code, but do it like this

final LatchingExecutionExceptionHandler executionExceptionHandler = new LatchingExecutionExceptionHandler();
final int exitCode = new CommandLine(
new ModrinthCommand()
)
.setExecutionExceptionHandler(executionExceptionHandler)
.execute(
"--api-base-url", wm.getRuntimeInfo().getHttpBaseUrl(),
"--output-directory", tempDir.toString(),
"--game-version", "1.21.1",
"--loader", "paper",
"--projects", projectId,
"--download-dependencies", DownloadDependencies.REQUIRED.name()
);
assertThat(exitCode).isNotEqualTo(ExitCode.OK);
assertThat(executionExceptionHandler.getExecutionException())
.isInstanceOf(InvalidParameterException.class)
.hasCauseInstanceOf(NoFilesAvailableException.class);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've replaced the test cases with test calling new CommandLine(new McImageHelper)).execute(args...)

Comment on lines +32 to +45
@BeforeEach
void startCapturingLogging() {
prevLevel = logger.getLevel();
logger.setLevel(Level.DEBUG);
logAppender.start();
logger.addAppender(logAppender);
}

@SuppressWarnings("unused") // will be fixed https://youtrack.jetbrains.com/issue/IDEA-358214/Support-JUnit-5-FieldSource-annotation
static List<Arguments> processGameVersionsArgs = asList(
argumentSet("matches", asList(
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7", "1.21.8")
), "1.21.8"
),
argumentSet("justOneOff", asList(
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7"),
asList("1.21.6", "1.21.7", "1.21.8")
), "1.21.7"
),
argumentSet("mismatch", asList(
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.6", "1.21.7", "1.21.8"),
asList("1.21.4", "1.21.5"),
asList("1.21.6", "1.21.7", "1.21.8")
), null
),
argumentSet("fabric-api + nucledoom", asList(
// part of fabric-api
asList("24w46a",
"1.21.4-pre1",
"1.21.4-pre2",
"1.21.4-pre3",
"1.21.4-rc3",
"1.21.4",
"25w02a",
"25w03a",
"25w04a",
"25w05a",
"25w06a",
"25w07a",
"25w08a",
"25w09a",
"25w09b",
"25w10a",
"1.21.5-pre1",
"1.21.5-pre2",
"1.21.5-pre3",
"1.21.5-rc1",
"1.21.5-rc2",
"1.21.5",
"25w14craftmine",
"25w15a",
"25w16a",
"25w17a",
"25w18a",
"25w19a",
"25w20a",
"25w21a",
"1.21.6-pre1",
"1.21.6-pre3",
"1.21.6",
"1.21.7-rc1",
"1.21.7",
"1.21.8",
"25w31a",
"25w32a"),
// part of nucledoom
singletonList("1.21.4")
), "1.21.4"
)
);
@AfterEach
void stopCapturingLogging() {
logger.detachAppender(logAppender);
logAppender.stop();
logger.setLevel(prevLevel);
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use this for capturing output

https://github.com/stefanbirkner/system-lambda

and you can pass --debug via the normal command setup

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh, much nicer.

I've kept the @BeforeEach and @AfterEach to reset LogLevel. When running without it, debug log level is set and escapes the VersionFromModrinthProjectsCommandTest, it then breaks SyncAndInterpolateTest as the test assert stderr empty, however due to debug level, debug logs are sent to stderr

final Map<String, Set<Integer>> supportedByVersion = new HashMap<>();

for (int projectIndex = 0; projectIndex < versionMatrix.size(); projectIndex++) {
for (String gameVersion : new HashSet<>(versionMatrix.get(projectIndex))) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new HashMap threw me off. Using https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Set.html#copyOf(java.util.Collection) would be more readable.

Also, why is deduping needed here and not earlier, if at all?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realised after coming back after a few days, i was was overcomplicating this.

Initially when i was envisioning my approach, i planned a Map of each Minecraft version, against a set of supported projects; it doesn't make sense to allow duplicated of either. However deduping at this point in the process is silly, as we've already made api calls and performed logic.

I've added to the initial projects ref list pass a .distinct() to filter out duplicate entries.

final List<ProjectRef> refs = projects.stream()
    .filter(Objects::nonNull)
    .map(String::trim)
    .filter(ref -> !ref.isEmpty())
    .map(ProjectRef::parse)
    .distinct()
    .collect(Collectors.toList());

I also realised the whole buildVersionMatrix approach which this comment is inside of was was overcomplicating everything. All the function did was invert a map of Projects -> Versions, to a map of Versions -> Projects. Removed that intermediary function.

@itzg

itzg commented Sep 2, 2026

Copy link
Copy Markdown
Owner

I don't see new commits. Why did you mark the review comments as resolved?

@itzg

itzg commented Sep 2, 2026

Copy link
Copy Markdown
Owner

...speaking of which, it is standard practice for the reviewer to be the one that marks their review comments resolved and not the code author. It serves as a way to methodically close the review feedback loop.

Unnecessarily built Map<String,Set<Integer>> Effectively inverting the
data without providing any extra value, removed function which build
matrix, and helper functions, updated wiring to take List<List<String>>
input from versionFromProject
@harrisonablack

Copy link
Copy Markdown
Contributor Author

I don't see new commits. Why did you mark the review comments as resolved?

My apologies, i resolved the comments as an acknowledgement without requiring contest or clarification. I hadn't committed yet as i was still reviewing my local changes, as i felt the initial PR was quite brash.

...speaking of which, it is standard practice for the reviewer to be the one that marks their review comments resolved and not the code author.

Ahh, i see, my mistake. Thank you for the note, i wasn't aware of the convention.

It serves as a way to methodically close the review feedback loop.

I do agree this makes much more sense than what i initially thought.

@harrisonablack
harrisonablack marked this pull request as draft September 3, 2026 03:48
@harrisonablack
harrisonablack marked this pull request as ready for review September 3, 2026 06:20
@harrisonablack
harrisonablack requested a review from itzg September 3, 2026 06:20
@itzg

itzg commented Sep 3, 2026

Copy link
Copy Markdown
Owner

My apologies, i resolved the comments as an acknowledgement without requiring contest or clarification.

And I forgot to mention the other half of my advice... as a code author I do like to keep track of which review comments I have looked at and agree without reply. Sometimes I'll use the thumbs up reaction that GitHub provides at the bottom of each comment.

@itzg

itzg commented Sep 3, 2026

Copy link
Copy Markdown
Owner

I haven't looked at the changes yet, but based on some of your follow up where you mention reducing complexity it got me wondering. When I first worked on this feature I found myself over complicating the algorithm and then found it was fairly concise in the end. The downside of that conciseness is that it seemed to limit to traceability back to the mismatching projects.

I feel bad asking this, however I'm also a bit proud of my original implementation. Now that you know what you know, I'd be curious if you could look at my original code and see if there was a more direct way to get to the debugging output needed?

@harrisonablack

Copy link
Copy Markdown
Contributor Author

The downside of that conciseness is that it seemed to limit to traceability back to the mismatching projects.

This was the initial problem that prompted me to look at widening the breadth of what i was touching, it felt too awkward to retrofit the debugging into the existing algorithm

I feel bad asking this

Please don't, half of my motivation for working on these issues bring out some humility in myself and learn a thing or two

however I'm also a bit proud of my original implementation. Now that you know what you know, I'd be curious if you could look at my original code and see if there was a more direct way to get to the debugging output needed?

I think the diff of this PR is way out of proportion of what it needs to be, and i did have a similar thought as well, that i was lulled by a sunken cost fallacy.

Looking at it in retrospect, yes, there is a much more direct more direct way i've got which i will note shortly

@harrisonablack

Copy link
Copy Markdown
Contributor Author

@itzg

I created a seperate fresh branch for the change

harrisonablack@73b4cbc

Within the original processGameVersions function, i've added a Map<String, int[]>> gameVersionPositions. This maps a Minecraft version, to the index of a Modrinth project.

For example;

Given the following List<List<String>>, which represents the supported Minecraft versions for each Modrinth project.

[
[26.2, 26.1,],          // Project A
[26.2, 26.1, 26],       // Project B
[26.1, 26,   1.21.11],  // Project C
]

We can create an Map<String, int[]> which maps a Minecraft version, to the list of all indexs which it exists for in each project, or -1 if it doesn't exist

During the first pass of the algorithm, we'd create the map;

26.2, [0,0,-1]

Which reads as; for minecraft 26.2, Project A, exists at index [0], of the [0] List, Project B exists at index [0] of the [1] list, and Project C doesn't support 26.2

Then for the second iteration we create another map.

"26.1, [1,1,0]"

For minecraft 26.1, project A exists at index [1] of list [0], Project B exists at index [1] of list [1], Project C exists at index [0] of list [2].

This way, we maintain a way to identify each project, before in the original algorithm, we were simply storing an integer score for each version, "how many projects does this version support", whereas now it maintains a seperate check per project, "Does the 0th project support this minecraft version?"

For the following command,

./gradlew run --args="--debug version-from-modrinth-projects --loader=paper --projects=paper:viaversion,paper:viabackwards,paper:griefprevention,paper:discordsrv,paper:pl3xmap"

It produces logs of which look like;

[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 26.2 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 26.1.2 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 26.1.1 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 26.1 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 1.21.11 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 1.21.10 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 1.21.9 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 1.21.8 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 1.21.7 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 1.21.6 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 1.21.5 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.216 DEBUG : Minecraft version 1.21.4 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.21.3 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.21.2 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.21.1 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.21 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.20.6 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.20.5 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.20.4 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.20.3 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.20.2 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.20.1 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.20 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.19.4 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.19.3 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.19.2 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.19.1 is blocked by project indexes [4]
[mc-image-helper] 00:20:06.217 DEBUG : Minecraft version 1.19 is blocked by project indexes [4]

I do want to add named logs, however will need to give the processGameVerseions function knowledge of the List<Project> effectiveRefs list.

@itzg

itzg commented Sep 3, 2026

Copy link
Copy Markdown
Owner

i did have a similar thought as well, that i was lulled by a sunken cost fallacy.

FWIW that is more self reflection I have heard than most of the companies / projects I have worked on. Executives never admit sunk cost fallacy 🤦‍♂️.

@itzg

itzg commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Cool. The new approach looks and sounds really good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add debug logs to see which contribute to "version from Modrinth" Version from Modrinth projects needs better error reporting

2 participants