Skip to content

Commit

Permalink
Merge pull request #124 from shipkit/sf--update-release
Browse files Browse the repository at this point in the history
Added support for updating releases
  • Loading branch information
shestee authored May 26, 2022
2 parents 5538cfa + b356d5e commit 2e3b850
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 7 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=1.1.*
version=1.2.*

0 comments on commit 2e3b850

Please sign in to comment.