Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for updating releases #124

Merged
merged 1 commit into from May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Expand Up @@ -19,7 +19,8 @@ Encourage and help software developers set up their releases to be fully automat
# Shipkit Changelog Gradle plugin

Our plugin generates changelog based on commit history and Github pull requests/issues.
Optionally, the changelog content can be posted to Github Releases.
Optionally, the changelog content can be posted to Github Releases
(as a new release or updating an existing release for a given tag).
This plugin is very small (<1kloc) and has a single dependency "com.eclipsesource.minimal-json:minimal-json:0.9.5".
The dependency is very small (30kb), stable (no changes since 2017), and brings zero transitive dependencies.

Expand Down Expand Up @@ -182,7 +183,10 @@ Pick the best tool that work for you and start automating releases and changelog

### Posting Github releases

Uses Github REST API to post releases.
Uses Github REST API to post releases.
First, the code checks if the release _already exists_ for the given tag.
If it exists, the release notes are updated ([REST doc](https://docs.github.com/en/rest/releases/releases#update-a-release)).
If not, the new release is created ([REST doc](https://docs.github.com/en/rest/releases/releases#create-a-release)).

## Usage

Expand Down
23 changes: 21 additions & 2 deletions src/main/java/org/shipkit/changelog/GithubApi.java
Expand Up @@ -31,6 +31,10 @@ public String post(String url, String body) throws IOException {
return doRequest(url, "POST", Optional.of(body)).content;
}

public String patch(String url, String body) throws IOException {
return doRequest(url, "PATCH", Optional.of(body)).content;
}

public Response get(String url) throws IOException {
return doRequest(url, "GET", Optional.empty());
}
Expand All @@ -39,9 +43,15 @@ private Response doRequest(String urlString, String method, Optional<String> bod
URL url = new URL(urlString);

HttpsURLConnection c = (HttpsURLConnection) url.openConnection();
c.setRequestMethod(method);
//workaround for Java limitation (https://bugs.openjdk.java.net/browse/JDK-7016595), works with GitHub REST API
if (method.equals("PATCH")) {
c.setRequestMethod("POST");
}
c.setDoOutput(true);
c.setRequestProperty("Content-Type", "application/json");
if (method.equals("PATCH")) {
c.setRequestProperty("X-HTTP-Method-Override", "PATCH");
}
if (authToken != null) {
c.setRequestProperty("Authorization", "token " + authToken);
}
Expand Down Expand Up @@ -83,7 +93,16 @@ private String call(String method, HttpsURLConnection conn) throws IOException {
String errorMessage =
String.format("%s %s failed, response code = %s, response body:%n%s",
method, conn.getURL(), conn.getResponseCode(), IOUtil.readFully(conn.getErrorStream()));
throw new IOException(errorMessage);
throw new ResponseException(conn.getResponseCode(), errorMessage);
}
}

public static class ResponseException extends IOException {
public final int responseCode;

public ResponseException(int responseCode, String errorMessage) {
super(errorMessage);
this.responseCode = responseCode;
}
}

Expand Down
52 changes: 50 additions & 2 deletions src/main/java/org/shipkit/github/release/GithubReleaseTask.java
Expand Up @@ -2,6 +2,7 @@

import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.logging.Logger;
Expand All @@ -14,6 +15,7 @@

import java.io.File;
import java.io.IOException;
import java.util.Optional;

public class GithubReleaseTask extends DefaultTask {

Expand Down Expand Up @@ -142,9 +144,11 @@ public void setNewTagRevision(String newTagRevision) {
body.add("body", releaseNotesTxt);

GithubApi githubApi = new GithubApi(githubToken);

try {
String response = githubApi.post(url, body.toString());
String htmlUrl = Json.parse(response).asObject().getString("html_url", "");
LOG.lifecycle("Checking if release exists for tag {}...", releaseTag);
Optional<Integer> existingRelease = existingRelease(githubApi, url, releaseTag);
final String htmlUrl = performRelease(existingRelease, githubApi, url, body.toString());
LOG.lifecycle("Posted release to Github: " + htmlUrl);
} catch (IOException e) {
throw new GradleException("Unable to post release to Github.\n" +
Expand All @@ -159,4 +163,48 @@ public void setNewTagRevision(String newTagRevision) {
, e);
}
}

/**
* Updates an existing release or creates a new release.
* @param existingReleaseId if empty, new release will created.
* If it contains release ID (internal GH identifier) it will update that release
* @param githubApi the GH api object
* @param url the url to use
* @param body payload
* @return String with JSON contents
* @throws IOException when something goes wrong with REST call / HTTP connectivity
*/
String performRelease(Optional<Integer> existingReleaseId, GithubApi githubApi, String url, String body) throws IOException {
final String htmlUrl;
if (existingReleaseId.isPresent()) {
LOG.lifecycle("Release already exists for tag {}! Updating the release notes...", releaseTag);

String response = githubApi.patch(url + "/" + existingReleaseId.get(), body);
htmlUrl = Json.parse(response).asObject().getString("html_url", "");
} else {
String response = githubApi.post(url, body);
htmlUrl = Json.parse(response).asObject().getString("html_url", "");
}
return htmlUrl;
}

/**
* Finds out if the release for given tag already exists
*
* @param githubApi api object
* @param url main REST url
* @param releaseTag the tag name, will be appended to the url
* @return existing release ID or empty optional if there is no release for the given tag
* @throws IOException when something goes wrong with REST call / HTTP connectivity
*/
Optional<Integer> existingRelease(GithubApi githubApi, String url, String releaseTag) throws IOException {
try {
GithubApi.Response r = githubApi.get(url + "/tags/" + releaseTag);
JsonValue result = Json.parse(r.getContent());
int releaseId = result.asObject().getInt("id", -1);
return Optional.of(releaseId);
} catch (GithubApi.ResponseException e) {
return Optional.empty();
}
}
}
84 changes: 84 additions & 0 deletions src/test/groovy/org/shipkit/changelog/GithubReleaseTaskTest.groovy
@@ -0,0 +1,84 @@
package org.shipkit.changelog

import org.gradle.api.GradleException
import org.gradle.testfixtures.ProjectBuilder
import org.shipkit.github.release.GithubReleasePlugin
import org.shipkit.github.release.GithubReleaseTask
import spock.lang.Specification

class GithubReleaseTaskTest extends Specification {

def apiMock = Mock(GithubApi)
def project = ProjectBuilder.builder().build()

def setup() {
project.plugins.apply(GithubReleasePlugin)
}

def "knows if release already exists"() {
GithubReleaseTask task = project.tasks.githubRelease
apiMock.get("dummy/releases/tags/v1.2.3") >> new GithubApi.Response('{"id": 10}', '')

when:
def result = task.existingRelease(apiMock, "dummy/releases", "v1.2.3")

then:
result.get() == 10
}

def "knows if release does not yet exist"() {
GithubReleaseTask task = project.tasks.githubRelease
apiMock.get("dummy/releases/tags/v1.2.3") >> { throw new GithubApi.ResponseException(404, "") }

when:
def result = task.existingRelease(apiMock, "dummy/releases", "v1.2.3")

then:
!result.present
}

def "creates new release"() {
GithubReleaseTask task = project.tasks.githubRelease
apiMock.post("dummy/url", "dummy body") >> '{"html_url": "dummy html url"}'

when:
def result = task.performRelease(Optional.empty(), apiMock, "dummy/url", "dummy body")

then:
result == "dummy html url"
}

def "updates existing release"() {
GithubReleaseTask task = project.tasks.githubRelease
apiMock.patch("api/releases/123", "dummy body") >> '{"html_url": "dummy html url"}'

when:
def result = task.performRelease(Optional.of(123), apiMock, "api/releases", "dummy body")

then:
result == "dummy html url"
}

/**
* Update githubToken and repo name for manual integration testing
*/
def "manual integration test"() {
project.version = "1.2.4"
project.file("changelog.md") << "Spanking new release! " + System.currentTimeSeconds()
project.tasks.named("githubRelease") { GithubReleaseTask it ->
it.changelog = project.file("changelog.md")
it.repository = "mockitoguy/shipkit-demo" //feel free to change to your private repo
it.newTagRevision = "aa51a6fe99d710c0e7ca30fc1d0411a8e9cdb7a8" //use sha of the repo above
it.githubToken = "secret" //update, use your token, DON'T CHECK IN
}

when:
project.tasks.githubRelease.postRelease()

then:
//when doing manual integration testing you won't get an exception here
//remove below / change the assertion when integ testing
thrown(GradleException)
// true
}
}
2 changes: 1 addition & 1 deletion version.properties
@@ -1 +1 @@
version=1.1.*
version=1.2.*