All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jreleaser.sdk.github.Github Maven / Gradle / Ivy

The newest version!
/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2020-2024 The JReleaser authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jreleaser.sdk.github;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.SerializationFeature;
import feign.form.FormData;
import feign.form.FormEncoder;
import feign.httpclient.ApacheHttpClient;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import org.jreleaser.bundle.RB;
import org.jreleaser.model.api.JReleaserContext;
import org.jreleaser.model.spi.release.Asset;
import org.jreleaser.model.spi.release.Release;
import org.jreleaser.model.spi.release.User;
import org.jreleaser.model.spi.upload.UploadException;
import org.jreleaser.sdk.commons.ClientUtils;
import org.jreleaser.sdk.commons.RestAPIException;
import org.jreleaser.sdk.github.api.GhAsset;
import org.jreleaser.sdk.github.api.GhAttachment;
import org.jreleaser.sdk.github.api.GhBranch;
import org.jreleaser.sdk.github.api.GhDiscussion;
import org.jreleaser.sdk.github.api.GhIssue;
import org.jreleaser.sdk.github.api.GhLabel;
import org.jreleaser.sdk.github.api.GhMilestone;
import org.jreleaser.sdk.github.api.GhOrganization;
import org.jreleaser.sdk.github.api.GhPackageVersion;
import org.jreleaser.sdk.github.api.GhRelease;
import org.jreleaser.sdk.github.api.GhReleaseNotes;
import org.jreleaser.sdk.github.api.GhReleaseNotesParams;
import org.jreleaser.sdk.github.api.GhRepository;
import org.jreleaser.sdk.github.api.GhSearchUser;
import org.jreleaser.sdk.github.api.GhTag;
import org.jreleaser.sdk.github.api.GhUser;
import org.jreleaser.sdk.github.api.GithubAPI;
import org.jreleaser.sdk.github.internal.Page;
import org.jreleaser.sdk.github.internal.PaginatingDecoder;
import org.jreleaser.util.CollectionUtils;

import java.io.IOException;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static org.jreleaser.sdk.commons.ClientUtils.toFormData;
import static org.jreleaser.util.StringUtils.requireNonBlank;

/**
 * @author Andres Almiray
 * @since 0.1.0
 */
class Github {
    private static final String USERS_NOREPLY_GITHUB_COM = "@users.noreply.github.com";
    private static final String ENDPOINT = "https://api.github.com";
    private static final String GITHUB_API_VERSION = "2022-11-28";
    private static final String GITHUB_MIME_TYPE = "application/vnd.github+json";

    private final JReleaserContext context;
    private final ObjectMapper objectMapper;
    private final GithubAPI api;
    private final String token;
    private final int connectTimeout;
    private final int readTimeout;

    Github(JReleaserContext context,
           String token,
           int connectTimeout,
           int readTimeout) {
        this(context, ENDPOINT, token, connectTimeout, readTimeout);
    }

    Github(JReleaserContext context,
           String endpoint,
           String token,
           int connectTimeout,
           int readTimeout) {
        this.context = requireNonNull(context, "'context' must not be null");
        this.token = requireNonBlank(token, "'token' must not be blank");
        this.connectTimeout = connectTimeout;
        this.readTimeout = readTimeout;
        requireNonBlank(endpoint, "'endpoint' must not be blank");

        if (endpoint.endsWith("/")) {
            endpoint = endpoint.substring(0, endpoint.length() - 1);
        }

        this.objectMapper = new ObjectMapper()
            .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .configure(SerializationFeature.INDENT_OUTPUT, true);

        this.api = ClientUtils.builder(context, connectTimeout, readTimeout)
            .client(new ApacheHttpClient())
            .encoder(new FormEncoder(new JacksonEncoder(objectMapper)))
            .decoder(new PaginatingDecoder(new JacksonDecoder(objectMapper)))
            .requestInterceptor(template -> {
                template.header("Accept", GITHUB_MIME_TYPE);
                template.header("X-GitHub-Api-Version", GITHUB_API_VERSION);
                template.header("Authorization", String.format("Bearer %s", token));
            })
            .target(GithubAPI.class, endpoint);
    }

    GhRepository findRepository(String owner, String repo) {
        context.getLogger().debug(RB.$("git.repository.lookup"), owner, repo);
        try {
            return api.getRepository(owner, repo);
        } catch (RestAPIException e) {
            if (e.isNotFound()) {
                // ok
                return null;
            }
            throw e;
        }
    }

    List listReleases(String owner, String repoName) {
        context.getLogger().debug(RB.$("git.list.releases"), owner, repoName);

        List releases = new ArrayList<>();

        int pageCount = 0;
        Map params = new LinkedHashMap<>();
        params.put("draft", false);
        params.put("prerelease", false);
        params.put("per_page", 20);

        boolean consume = true;
        do {
            params.put("page", ++pageCount);
            Page> page = api.listReleases(owner, repoName, params);
            page.getContent().stream()
                .map(r -> new Release(
                    r.getName(),
                    r.getTagName(),
                    r.getHtmlUrl(),
                    r.getPublishedAt()
                ))
                .forEach(releases::add);

            if (!page.hasLinks() || !page.getLinks().hasNext()) {
                consume = false;
            }
        }
        while (consume);

        return releases;
    }

    List listBranches(String owner, String repoName) {
        context.getLogger().debug(RB.$("git.list.branches"), owner, repoName);

        List branches = new ArrayList<>();

        int pageCount = 0;
        Map params = new LinkedHashMap<>();
        params.put("per_page", 20);

        boolean consume = true;
        do {
            params.put("page", ++pageCount);
            Page> page = api.listBranches(owner, repoName, params);
            page.getContent().stream()
                .map(GhBranch::getName)
                .forEach(branches::add);

            if (!page.hasLinks() || !page.getLinks().hasNext()) {
                consume = false;
            }
        }
        while (consume);

        return branches;
    }

    Map listAssets(String owner, String repo, GhRelease release) {
        context.getLogger().debug(RB.$("git.list.assets.github"), owner, repo, release.getId());

        Map assets = new LinkedHashMap<>();
        for (GhAsset asset : api.listAssets(owner, repo, release.getId())) {
            assets.put(asset.getName(), asset);
        }

        return assets;
    }

    List listMilestones(String owner, String repoName, String state) {
        context.getLogger().debug(RB.$("git.list.milestones"), owner, repoName, state);

        List milestones = new ArrayList<>();

        int pageCount = 0;
        Map params = new LinkedHashMap<>();
        params.put("state", state);
        params.put("per_page", 20);

        boolean consume = true;
        do {
            params.put("page", ++pageCount);
            Page> page = api.listMilestones(owner, repoName, params);
            milestones.addAll(page.getContent());

            if (!page.hasLinks() || !page.getLinks().hasNext()) {
                consume = false;
            }
        }
        while (consume);

        return milestones;
    }

    Optional findMilestoneByName(String owner, String repo, String milestoneName) {
        context.getLogger().debug(RB.$("git.milestone.lookup"), milestoneName, owner, repo);

        return findMilestone(owner, repo, milestoneName, "open");
    }

    Optional findClosedMilestoneByName(String owner, String repo, String milestoneName) {
        context.getLogger().debug(RB.$("git.milestone.lookup.closed"), milestoneName, owner, repo);

        return findMilestone(owner, repo, milestoneName, "closed");
    }

    private Optional findMilestone(String owner, String repo, String milestoneName, String state) {
        return listMilestones(owner, repo, state).stream()
            .filter(m -> milestoneName.equals(m.getTitle()))
            .findFirst();
    }

    void closeMilestone(String owner, String repo, GhMilestone milestone) {
        context.getLogger().debug(RB.$("git.milestone.close"), milestone.getTitle(), owner, repo);

        api.updateMilestone(CollectionUtils.map()
            .e("state", "closed"), owner, repo, milestone.getNumber());
    }

    GhRepository createRepository(String owner, String repo) {
        context.getLogger().debug(RB.$("git.repository.create"), owner, repo);

        Map params = new LinkedHashMap<>();
        params.put("name", repo);
        params.put("private", false);

        GhOrganization organization = resolveOrganization(owner);
        if (null != organization) {
            return api.createRepository(params, owner);
        }

        return api.createRepository(params);
    }

    List listTags(String owner, String repoName) {
        context.getLogger().debug(RB.$("git.list.milestones"), owner, repoName);

        List tags = new ArrayList<>();

        int pageCount = 0;
        Map params = new LinkedHashMap<>();
        params.put("per_page", 20);

        boolean consume = true;
        do {
            params.put("page", ++pageCount);
            Page> page = api.listTags(owner, repoName, params);
            tags.addAll(page.getContent());

            if (!page.hasLinks() || !page.getLinks().hasNext()) {
                consume = false;
            }
        }
        while (consume);

        return tags;
    }

    GhRelease findReleaseByTag(String owner, String repo, String tagName) {
        context.getLogger().debug(RB.$("git.fetch.release.by.tag"), owner, repo, tagName);

        try {
            return api.getReleaseByTagName(owner, repo, tagName);
        } catch (RestAPIException e) {
            if (e.isNotFound()) {
                // ok
                return null;
            }
            throw e;
        }
    }

    GhRelease findReleaseById(String owner, String repo, Long id) {
        context.getLogger().debug(RB.$("git.fetch.release.by.id"), owner, repo, id);

        return api.getRelease(owner, repo, id);
    }

    void deleteTag(String owner, String repo, String tagName) throws RestAPIException {
        context.getLogger().debug(RB.$("git.delete.tag.from"), tagName, owner, repo);

        api.deleteTag(owner, repo, tagName);
    }

    GhRelease createRelease(String owner, String repo, GhRelease release) throws RestAPIException {
        context.getLogger().debug(RB.$("git.create.release"), owner, repo, release.getTagName());

        return api.createRelease(release, owner, repo);
    }

    void updateRelease(String owner, String repo, Long id, GhRelease release) throws RestAPIException {
        context.getLogger().debug(RB.$("git.update.release"), owner, repo, release.getTagName());

        api.updateRelease(release, owner, repo, id);
    }

    void deleteRelease(String owner, String repo, String tagName, Long id) throws RestAPIException {
        context.getLogger().debug(RB.$("git.delete.release.from.id"), tagName, owner, repo, id);

        try {
            api.deleteRelease(owner, repo, id);
        } catch (RestAPIException e) {
            if (e.isNotFound()) {
                // OK. Release might have been deleted but
                // tag still exists.
                return;
            }
            throw e;
        }
    }

    void uploadAssets(GhRelease release, Set assets) throws IOException {
        for (Asset asset : assets) {
            if (0 == Files.size(asset.getPath()) || !Files.exists(asset.getPath())) {
                // do not upload empty or non existent files
                continue;
            }

            uploadOrUpdateAsset(asset, release, "git.upload.asset", "git.upload.asset.failure");
        }
    }

    void updateAssets(String owner, String repo, GhRelease release, Set assets, Map existingAssets) throws IOException {
        for (Asset asset : assets) {
            if (0 == Files.size(asset.getPath()) || !Files.exists(asset.getPath())) {
                // do not upload empty or non existent files
                continue;
            }

            context.getLogger().debug(" " + RB.$("git.delete.asset"), asset.getFilename());
            try {
                api.deleteAsset(owner, repo, existingAssets.get(asset.getFilename()).getId());
            } catch (RestAPIException e) {
                context.getLogger().error(" " + RB.$("git.delete.asset.failure"), asset.getFilename());
                throw e;
            }

            uploadOrUpdateAsset(asset, release, "git.update.asset", "git.update.asset.failure");
        }
    }

    private void uploadOrUpdateAsset(Asset asset, GhRelease release, String operationMessageKey, String operationErrorMessageKey) throws IOException {
        context.getLogger().info(" " + RB.$(operationMessageKey), asset.getFilename());

        try {
            String uploadUrl = release.getUploadUrl();
            if (uploadUrl.endsWith("{?name,label}")) {
                uploadUrl = uploadUrl.substring(0, uploadUrl.length() - 13);
            }

            URI uri = new URI(uploadUrl + "?name=" + asset.getFilename());
            GhAttachment attachment = uploadAsset(uri, toFormData(asset.getPath()));
            if (!"uploaded".equalsIgnoreCase(attachment.getState())) {
                context.getLogger().warn(" " + RB.$(operationErrorMessageKey), asset.getFilename());
            }
        } catch (URISyntaxException shouldNeverHappen) {
            context.getLogger().error(" " + RB.$(operationErrorMessageKey), asset.getFilename());
            throw new IllegalStateException(RB.$("ERROR_unexpected_error"), shouldNeverHappen);
        } catch (RestAPIException e) {
            context.getLogger().error(" " + RB.$(operationErrorMessageKey), asset.getFilename());
            throw e;
        } catch (UploadException e) {
            context.getLogger().error(" " + RB.$(operationErrorMessageKey), asset.getFilename());
            if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
            throw new IOException(e);
        }
    }

    private GhAttachment uploadAsset(URI uri, FormData data) throws UploadException, IOException {
        Map headers = new LinkedHashMap<>();
        headers.put("Accept", GITHUB_MIME_TYPE);
        headers.put("X-GitHub-Api-Version", GITHUB_API_VERSION);
        headers.put("Authorization", String.format("Bearer %s", token));

        Reader reader = ClientUtils.postFile(context.getLogger(),
            uri,
            connectTimeout,
            readTimeout,
            data,
            headers);

        return objectMapper.readValue(reader, GhAttachment.class);
    }

    Optional findDiscussion(String organization, String team, String title) {
        return listDiscussions(organization, team).stream()
            .filter(d -> title.equals(d.getTitle()))
            .findFirst();
    }

    List listDiscussions(String organization, String team) {
        context.getLogger().debug(RB.$("git.list.discussions"), organization, team);

        List discussions = new ArrayList<>();

        int pageCount = 0;
        Map params = new LinkedHashMap<>();
        params.put("per_page", 20);

        boolean consume = true;
        do {
            params.put("page", ++pageCount);
            Page> page = api.listDiscussions(organization, team, params);
            discussions.addAll(page.getContent());

            if (!page.hasLinks() || !page.getLinks().hasNext()) {
                consume = false;
            }
        }
        while (consume);

        return discussions;
    }

    void createDiscussion(String organization, String team, String title, String message) {
        context.getLogger().debug(RB.$("git.releaser.discussion.create"), title);

        GhDiscussion discussion = new GhDiscussion();
        discussion.setTitle(title);
        discussion.setBody(message);

        api.createDiscussion(discussion, organization, team);
    }

    GhLabel getOrCreateLabel(String owner, String name, String labelName, String labelColor, String description) {
        context.getLogger().debug(RB.$("git.label.fetch", labelName));

        List labels = listLabels(owner, name);
        Optional label = labels.stream()
            .filter(l -> l.getName().equals(labelName))
            .findFirst();

        if (label.isPresent()) {
            return label.get();
        }

        context.getLogger().debug(RB.$("git.label.create", labelName));
        return api.createLabel(owner, name, labelName, labelColor, description);
    }

    public Optional findIssue(String owner, String name, int issueNumber) {
        context.getLogger().debug(RB.$("git.issue.fetch", issueNumber));
        try {
            return Optional.of(api.findIssue(owner, name, issueNumber));
        } catch (RestAPIException e) {
            if (e.isNotFound()) {
                return Optional.empty();
            }
            throw e;
        }
    }

    void addLabelToIssue(String owner, String name, GhIssue issue, GhLabel label) {
        context.getLogger().debug(RB.$("git.issue.label", label.getName(), issue.getNumber()));

        Map> labels = new LinkedHashMap<>();
        List list = labels.computeIfAbsent("labels", k -> new ArrayList<>());
        list.addAll(issue.getLabels().stream().map(GhLabel::getName).collect(toList()));
        list.add(label.getName());

        api.labelIssue(labels, owner, name, issue.getNumber());
    }

    void commentOnIssue(String owner, String name, GhIssue issue, String comment) {
        context.getLogger().debug(RB.$("git.issue.comment", issue.getNumber()));

        Map params = new LinkedHashMap<>();
        params.put("body", comment);

        api.commentIssue(params, owner, name, issue.getNumber());
    }

    void setMilestoneOnIssue(String owner, String name, GhIssue issue, GhMilestone milestone) {
        Map params = new LinkedHashMap<>();
        params.put("milestone", milestone.getNumber());

        api.updateIssue(params, owner, name, issue.getNumber());
    }

    private List listLabels(String owner, String repoName) {
        context.getLogger().debug(RB.$("git.list.labels"), owner, repoName);

        List labels = new ArrayList<>();

        int pageCount = 0;
        Map params = new LinkedHashMap<>();
        params.put("limit", 20);

        boolean consume = true;
        do {
            params.put("page", ++pageCount);
            Page> page = api.listLabels(owner, repoName, params);
            labels.addAll(page.getContent());

            if (!page.hasLinks() || !page.getLinks().hasNext()) {
                consume = false;
            }
        }
        while (consume);

        return labels;
    }

    private GhOrganization resolveOrganization(String name) {
        try {
            return api.getOrganization(name);
        } catch (RestAPIException e) {
            if (e.isNotFound()) {
                // ok
                return null;
            }
            throw e;
        }
    }

    void updateRelease(String owner, String repo, String tag, Long id, GhRelease release) throws RestAPIException {
        context.getLogger().debug(RB.$("git.update.release"), owner, repo, tag);

        api.updateRelease(release, owner, repo, id);
    }

    private String getPrivateEmailUserId(String email) {
        if (!email.endsWith(USERS_NOREPLY_GITHUB_COM)) return null;
        String username = email.substring(0, email.indexOf("@"));
        if (username.contains("+")) {
            username = username.substring(username.indexOf("+") + 1);
        }
        return username;
    }

    Optional findUser(String email, String name) throws RestAPIException {
        context.getLogger().debug(RB.$("git.user.lookup"), name, email);

        String username = getPrivateEmailUserId(email);
        if (null != username) {
            GhUser user = api.getUser(username);
            if (null != user) {
                return Optional.of(new User(user.getLogin(), email, user.getHtmlUrl()));
            }
        }

        GhSearchUser search = api.searchUser(CollectionUtils.mapOf("q", email));
        if (search.getTotalCount() > 0) {
            GhUser user = search.getItems().get(0);
            return Optional.of(new User(user.getLogin(), email, user.getHtmlUrl()));
        }

        return Optional.empty();
    }

    GhReleaseNotes generateReleaseNotes(String owner, String repo, GhReleaseNotesParams params) throws RestAPIException {
        context.getLogger().info(RB.$("github.generate.release.notes"), owner, repo, params.getPreviousTagName(), params.getTagName());

        return api.generateReleaseNotes(params, owner, repo);
    }

    List listPackageVersions(String packageType, String packageName) throws IOException {
        context.getLogger().debug(RB.$("github.list.versions"), packageType, packageName);

        List issues = new ArrayList<>();
        Page> page = api.listPackageVersions0(packageType, packageName);
        issues.addAll(page.getContent());

        if (page.hasLinks() && page.getLinks().hasNext()) {
            try {
                collectPackageVersions(page, issues);
            } catch (URISyntaxException e) {
                throw new IOException(e);
            }
        }

        return issues;
    }

    private void collectPackageVersions(Page> page, List issues) throws URISyntaxException {
        URI next = new URI(page.getLinks().next());
        context.getLogger().debug(next.toString());

        page = api.listPackageVersions1(next);
        issues.addAll(page.getContent());

        if (page.hasLinks() && page.getLinks().hasNext()) {
            collectPackageVersions(page, issues);
        }
    }

    void deletePackageVersion(String packageType, String packageName, String packageVersion) throws RestAPIException {
        context.getLogger().debug(RB.$("github.delete.package.version"), packageVersion, packageName);

        api.deletePackageVersion(packageType, packageName, packageVersion);
    }

    void deletePackage(String packageType, String packageName) throws RestAPIException {
        context.getLogger().debug(RB.$("github.delete.package"), packageType, packageName);

        api.deletePackage(packageType, packageName);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy