Skip to content

Commit

Permalink
Added github subcommand to download latest release asset (#397)
Browse files Browse the repository at this point in the history
  • Loading branch information
itzg authored Feb 11, 2024
1 parent 1b6da2b commit 846a3db
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 0 deletions.
5 changes: 5 additions & 0 deletions dev/github.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
###
@org = kettingpowered
@repo = kettinglauncher
GET https://api.github.com/repos/{{org}}/{{repo}}/releases/latest
Accept: Accept: application/vnd.github+json
2 changes: 2 additions & 0 deletions src/main/java/me/itzg/helpers/McImageHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import me.itzg.helpers.forge.InstallForgeCommand;
import me.itzg.helpers.forge.InstallNeoForgeCommand;
import me.itzg.helpers.get.GetCommand;
import me.itzg.helpers.github.GithubCommands;
import me.itzg.helpers.modrinth.InstallModrinthModpackCommand;
import me.itzg.helpers.modrinth.ModrinthCommand;
import me.itzg.helpers.mvn.MavenDownloadCommand;
Expand Down Expand Up @@ -59,6 +60,7 @@
CurseForgeFilesCommand.class,
FindCommand.class,
GetCommand.class,
GithubCommands.class,
HashCommand.class,
InstallCurseForgeCommand.class,
InstallFabricLoaderCommand.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package me.itzg.helpers.github;

import java.nio.file.Path;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import me.itzg.helpers.errors.InvalidParameterException;
import me.itzg.helpers.http.Fetch;
import me.itzg.helpers.http.SharedFetch;
import me.itzg.helpers.http.SharedFetchArgs;
import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

@Command(name = "download-latest-asset",
description = "From the latest release, downloads the first matching asset, and outputs the downloaded filename"
)
@Slf4j
public class DownloadLatestAssetCommand implements Callable<Integer> {

String organization;
String repo;

@Option(names = "--name-pattern")
Pattern namePattern;

@Option(names = "--output-directory", defaultValue = ".")
Path outputDirectory;

@Option(names = "--api-base-url", defaultValue = GithubClient.DEFAULT_API_BASE_URL)
String apiBaseUrl;

@Parameters(arity = "1", paramLabel = "org/repo")
public void setOrgRepo(String input) {
final String[] parts = input.split("/", 2);
if (parts.length != 2) {
throw new InvalidParameterException("org/repo needs to be slash delimited");
}
this.organization = parts[0];
this.repo = parts[1];
}

@ArgGroup(exclusive = false)
SharedFetchArgs sharedFetchArgs = new SharedFetchArgs();

@Override
public Integer call() throws Exception {

try (SharedFetch sharedFetch = Fetch.sharedFetch("github download-latest-asset", sharedFetchArgs.options())) {

final GithubClient client = new GithubClient(sharedFetch, apiBaseUrl);
final Path result = client.downloadLatestAsset(organization, repo, namePattern, outputDirectory)
.block();

if (result == null) {
log.error("Unable to locate latest, matching asset from {}/{}", organization, repo);
return CommandLine.ExitCode.USAGE;
}
else {
System.out.println(result);
}
}

return CommandLine.ExitCode.OK;
}
}
63 changes: 63 additions & 0 deletions src/main/java/me/itzg/helpers/github/GithubClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package me.itzg.helpers.github;

import java.net.URI;
import java.nio.file.Path;
import java.util.Collections;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import me.itzg.helpers.github.model.Asset;
import me.itzg.helpers.github.model.Release;
import me.itzg.helpers.http.FailedRequestException;
import me.itzg.helpers.http.SharedFetch;
import me.itzg.helpers.http.UriBuilder;
import org.jetbrains.annotations.Nullable;
import reactor.core.publisher.Mono;

@Slf4j
public class GithubClient {

public static final String DEFAULT_API_BASE_URL = "https://api.github.com";

private final SharedFetch sharedFetch;
private final UriBuilder uriBuilder;

public GithubClient(SharedFetch sharedFetch, String apiBaseUrl) {
this.sharedFetch = sharedFetch;
this.uriBuilder = UriBuilder.withBaseUrl(apiBaseUrl);
}

public Mono<Path> downloadLatestAsset(String org, String repo, @Nullable Pattern namePattern, Path outputDirectory) {
return sharedFetch.fetch(
uriBuilder.resolve("/repos/{org}/{repo}/releases/latest", org, repo)
)
.acceptContentTypes(Collections.singletonList("application/vnd.github+json"))
.toObject(Release.class)
.assemble()
.onErrorResume(throwable -> throwable instanceof FailedRequestException && ((FailedRequestException) throwable).getStatusCode() == 404,
throwable -> Mono.empty()
)
.flatMap(release -> {
if (log.isDebugEnabled()) {
log.debug("Assets in latest release '{}': {}",
release.getName(),
release.getAssets().stream()
.map(Asset::getName)
.collect(Collectors.toList())
);
}

return Mono.justOrEmpty(
release.getAssets().stream()
.filter(asset -> namePattern == null || namePattern.matcher(asset.getName()).matches())
.findFirst()
)
.flatMap(asset ->
sharedFetch.fetch(URI.create(asset.getBrowserDownloadUrl()))
.toDirectory(outputDirectory)
.skipExisting(true)
.assemble()
);
});
}
}
12 changes: 12 additions & 0 deletions src/main/java/me/itzg/helpers/github/GithubCommands.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package me.itzg.helpers.github;

import picocli.CommandLine.Command;

@Command(name = "github",
subcommands = {
DownloadLatestAssetCommand.class
}
)
public class GithubCommands {

}
12 changes: 12 additions & 0 deletions src/main/java/me/itzg/helpers/github/model/Asset.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package me.itzg.helpers.github.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;

@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Asset {
String name;
String browserDownloadUrl;
}
13 changes: 13 additions & 0 deletions src/main/java/me/itzg/helpers/github/model/Release.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package me.itzg.helpers.github.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import java.util.List;
import lombok.Data;

@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Release {
String name;
List<Asset> assets;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package me.itzg.helpers.github;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.assertj.core.api.Assertions.assertThat;

import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import java.nio.file.Path;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
import picocli.CommandLine;

class DownloadLatestAssetCommandTest {
@RegisterExtension
static WireMockExtension wm = WireMockExtension.newInstance()
.options(wireMockConfig()
.dynamicPort()
.extensions(new ResponseTemplateTransformer(false))
)
.configureStaticDsl(true)
.build();

@Test
void usingNamePattern(@TempDir Path tempDir, WireMockRuntimeInfo wmInfo) {
final String filename = RandomStringUtils.randomAlphabetic(10)+".jar";
final String fileContent = RandomStringUtils.randomAlphabetic(20);

stubFor(get("/repos/org/repo/releases/latest")
.willReturn(ok()
.withHeader("Content-Type", "application/json")
.withBodyFile("github/release-with-sources-jar.json")
.withTransformers("response-template")
.withTransformerParameter("filename", filename)
)
);
stubFor(head(urlPathEqualTo("/download/"+filename))
.willReturn(
ok()
.withHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", filename))
)
);
stubFor(get("/download/"+filename)
.willReturn(
ok()
.withHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", filename))
.withBody(fileContent)
)
);

final int exitCode = new CommandLine(new DownloadLatestAssetCommand())
.execute(
"--api-base-url", wmInfo.getHttpBaseUrl(),
"--name-pattern", "app-.+?(?<!-sources)\\.jar",
"--output-directory", tempDir.toString(),
"org/repo"
);

assertThat(exitCode).isEqualTo(0);

assertThat(tempDir.resolve(filename))
.exists()
.content().isEqualTo(fileContent);
}
}
13 changes: 13 additions & 0 deletions src/test/resources/__files/github/release-with-sources-jar.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "app v1.3.0",
"assets": [
{
"name": "app-1.3.0-sources.jar",
"browser_download_url": "{{request.baseUrl}}/invalid.jar"
},
{
"name": "app-1.3.0.jar",
"browser_download_url": "{{request.baseUrl}}/download/{{parameters.filename}}"
}
]
}

0 comments on commit 846a3db

Please sign in to comment.