diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/Constants.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/Constants.java index f110785e..3a390cd0 100644 --- a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/Constants.java +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/Constants.java @@ -85,6 +85,10 @@ public class Constants { public static final int STATUSES_TO_LOAD_THRESHOLD_DEFAULT_VAL = 50; public static final String GITLAB_FEATURE_TOGGLE_MERGE_RESULTS = "commitStatusPubliser.gitlab.supportMergeResults"; + public static final String GITEA_PUBLISHER_ID = "giteaStatusPublisher"; + public static final String GITEA_API_URL = "giteaApiUrl"; + public static final String GITEA_TOKEN = "secure:giteaAccessToken"; + @NotNull public String getVcsRootIdParam() { return VCS_ROOT_ID_PARAM; @@ -219,4 +223,19 @@ public String getGitlabServer() { public String getGitlabToken() { return GITLAB_TOKEN; } -} \ No newline at end of file + + @NotNull + public String getGiteaPublisherId() { + return GITEA_PUBLISHER_ID; + } + + @NotNull + public String getGiteaServer() { + return GITEA_API_URL; + } + + @NotNull + public String getGiteaToken() { + return GITEA_TOKEN; + } +} diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/GiteaBuildStatus.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/GiteaBuildStatus.java new file mode 100644 index 00000000..94893e69 --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/GiteaBuildStatus.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2022 JetBrains s.r.o. + * + * 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 + * + * http://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 jetbrains.buildServer.commitPublisher.gitea; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public enum GiteaBuildStatus { + PENDING("pending"), + SUCCESS("success"), + ERROR("error"), + FAILURE("failure"); + + private static final Map INDEX = Arrays.stream(values()).collect(Collectors.toMap(GiteaBuildStatus::getName, Function.identity())); + + private final String myName; + + GiteaBuildStatus(@NotNull String name) { + myName = name; + } + + @NotNull + public String getName() { + return myName; + } + + @Nullable + public static GiteaBuildStatus getByName(@NotNull String name) { + return INDEX.get(name); + } +} diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/GiteaPublisher.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/GiteaPublisher.java new file mode 100644 index 00000000..02f8bc19 --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/GiteaPublisher.java @@ -0,0 +1,351 @@ +/* + * Copyright 2000-2022 JetBrains s.r.o. + * + * 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 + * + * http://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 jetbrains.buildServer.commitPublisher.gitea; + +import com.google.gson.Gson; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import jetbrains.buildServer.BuildType; +import jetbrains.buildServer.commitPublisher.*; +import jetbrains.buildServer.commitPublisher.gitea.data.GiteaCommitStatus; +import jetbrains.buildServer.serverSide.*; +import jetbrains.buildServer.serverSide.impl.LogUtil; +import jetbrains.buildServer.util.StringUtil; +import jetbrains.buildServer.vcs.VcsRoot; +import jetbrains.buildServer.vcs.VcsRootInstance; +import jetbrains.buildServer.vcshostings.http.HttpHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static jetbrains.buildServer.commitPublisher.LoggerUtil.LOG; + +class GiteaPublisher extends HttpBasedCommitStatusPublisher { + + private static final Gson myGson = new Gson(); + private static final GitRepositoryParser VCS_URL_PARSER = new GitRepositoryParser(); + + @NotNull private final CommitStatusesCache myStatusesCache; + + GiteaPublisher(@NotNull CommitStatusPublisherSettings settings, + @NotNull SBuildType buildType, @NotNull String buildFeatureId, + @NotNull Map params, + @NotNull CommitStatusPublisherProblems problems, + @NotNull WebLinks links, + @NotNull CommitStatusesCache statusesCache) { + super(settings, buildType, buildFeatureId, params, problems, links); + myStatusesCache = statusesCache; + } + + + @NotNull + @Override + public String getId() { + return Constants.GITEA_PUBLISHER_ID; + } + + + @NotNull + @Override + public String toString() { + return "Gitea"; + } + + @Override + public boolean buildQueued(@NotNull BuildPromotion buildPromotion, @NotNull BuildRevision revision, @NotNull AdditionalTaskInfo additionalTaskInfo) throws PublisherException { + publish(buildPromotion, revision, GiteaBuildStatus.PENDING, additionalTaskInfo); + return true; + } + + @Override + public boolean buildRemovedFromQueue(@NotNull BuildPromotion buildPromotion, @NotNull BuildRevision revision, @NotNull AdditionalTaskInfo additionalTaskInfo) throws PublisherException { + publish(buildPromotion, revision, GiteaBuildStatus.FAILURE, additionalTaskInfo); + return true; + } + + @Override + public boolean buildStarted(@NotNull SBuild build, @NotNull BuildRevision revision) throws PublisherException { + publish(build, revision, GiteaBuildStatus.PENDING, DefaultStatusMessages.BUILD_STARTED); + return true; + } + + + @Override + public boolean buildFinished(@NotNull SBuild build, @NotNull BuildRevision revision) throws PublisherException { + GiteaBuildStatus status = build.getBuildStatus().isSuccessful() ? GiteaBuildStatus.SUCCESS : GiteaBuildStatus.FAILURE; + publish(build, revision, status, build.getStatusDescriptor().getText()); + return true; + } + + + @Override + public boolean buildFailureDetected(@NotNull SBuild build, @NotNull BuildRevision revision) throws PublisherException { + publish(build, revision, GiteaBuildStatus.FAILURE, build.getStatusDescriptor().getText()); + return true; + } + + @Override + public boolean buildMarkedAsSuccessful(@NotNull SBuild build, @NotNull BuildRevision revision, boolean buildInProgress) throws PublisherException { + publish(build, revision, buildInProgress ? GiteaBuildStatus.PENDING : GiteaBuildStatus.SUCCESS, DefaultStatusMessages.BUILD_MARKED_SUCCESSFULL); + return true; + } + + + @Override + public boolean buildInterrupted(@NotNull SBuild build, @NotNull BuildRevision revision) throws PublisherException { + publish(build, revision, GiteaBuildStatus.FAILURE, build.getStatusDescriptor().getText()); + return true; + } + + @Override + public RevisionStatus getRevisionStatusForRemovedBuild(@NotNull SQueuedBuild removedBuild, @NotNull BuildRevision revision) throws PublisherException { + SBuildType buildType = removedBuild.getBuildType(); + GiteaCommitStatus commitStatus = getLatestCommitStatusForBuild(revision, buildType.getFullName(), removedBuild.getBuildPromotion()); + return getRevisionStatusForRemovedBuild(removedBuild, commitStatus); + } + + RevisionStatus getRevisionStatusForRemovedBuild(@NotNull SQueuedBuild removedBuild, @Nullable GiteaCommitStatus commitStatus) { + if(commitStatus == null) { + return null; + } + Event event = getTriggeredEvent(commitStatus); + boolean isSameBuildType = StringUtil.areEqual(getBuildName(removedBuild.getBuildPromotion()), commitStatus.context); + return new RevisionStatus(event, commitStatus.description, isSameBuildType, getBuildIdFromViewUrl(commitStatus.target_url)); + } + + private String getBuildName(BuildPromotion promotion) { + SBuildType buildType = promotion.getBuildType(); + return buildType != null ? buildType.getFullName() : promotion.getBuildTypeExternalId(); + } + + @Override + public RevisionStatus getRevisionStatus(@NotNull BuildPromotion buildPromotion, @NotNull BuildRevision revision) throws PublisherException { + SBuildType buildType = buildPromotion.getBuildType(); + GiteaCommitStatus commitStatus = getLatestCommitStatusForBuild(revision, buildType == null ? buildPromotion.getBuildTypeExternalId() : buildType.getFullName(), buildPromotion); + return getRevisionStatus(buildPromotion, commitStatus); + } + + private GiteaCommitStatus getLatestCommitStatusForBuild(@NotNull BuildRevision revision, @NotNull String buildName, @NotNull BuildPromotion promotion) throws PublisherException { + AtomicReference exception = new AtomicReference<>(null); + GiteaCommitStatus statusFromCache = myStatusesCache.getStatusFromCache(revision, buildName, () -> { + SBuildType exactBuildTypeToLoadStatuses = promotion.isPartOfBuildChain() ? null : promotion.getBuildType(); + try { + GiteaCommitStatus[] commitStatuses = loadGiteaStatuses(revision, exactBuildTypeToLoadStatuses); + return Arrays.asList(commitStatuses); + } catch (PublisherException e) { + exception.set(e); + return Collections.emptyList(); + } + }, status -> status.context); + + if (exception.get() != null) { + throw exception.get(); + } + + return statusFromCache; +} + + private GiteaCommitStatus[] loadGiteaStatuses(@NotNull BuildRevision revision, @Nullable SBuildType buildType) throws PublisherException { + String url = buildRevisionStatusesUrl(revision, buildType); + url += "?access_token=" + getPrivateToken(); + ResponseEntityProcessor processor = new ResponseEntityProcessor<>(GiteaCommitStatus[].class); + GiteaCommitStatus[] commitStatuses = get(url, null, null, processor); + if (commitStatuses == null || commitStatuses.length == 0) { + return new GiteaCommitStatus[0]; + } + return commitStatuses; + } + + @Nullable + RevisionStatus getRevisionStatus(@NotNull BuildPromotion buildPromotion, @Nullable GiteaCommitStatus commitStatus) { + if(commitStatus == null) { + return null; + } + Event event = getTriggeredEvent(commitStatus); + boolean isSameBuildType = StringUtil.areEqual(getBuildName(buildPromotion), commitStatus.context); + return new RevisionStatus(event, commitStatus.description, isSameBuildType, getBuildIdFromViewUrl(commitStatus.target_url)); + } + + private String buildRevisionStatusesUrl(@NotNull BuildRevision revision, @Nullable BuildType buildType) throws PublisherException { + VcsRootInstance root = revision.getRoot(); + String apiUrl = getApiUrl(); + if (null == apiUrl || apiUrl.isEmpty()) + throw new PublisherException("Missing Gitea API URL parameter"); + String pathPrefix = GiteaSettings.getPathPrefix(apiUrl); + Repository repository = parseRepository(root, pathPrefix); + if (repository == null) + throw new PublisherException("Cannot parse repository URL from VCS root " + root.getName()); + return GiteaSettings.getProjectsUrl(getApiUrl(), repository.owner(), repository.repositoryName()) + "/statuses/" + revision.getRevision(); + } + + private Event getTriggeredEvent(GiteaCommitStatus commitStatus) { + if (commitStatus.status == null) { + LOG.warn("No Gitea build status is provided. Related event can not be calculated"); + return null; + } + GiteaBuildStatus status = GiteaBuildStatus.getByName(commitStatus.status); + if (status == null) { + LOG.warn("Unknown Gitea build status \"" + commitStatus.status + "\". Related event can not be calculated"); + return null; + } + + switch (status) { + case PENDING: + if (commitStatus.description == null || commitStatus.description.contains(DefaultStatusMessages.BUILD_QUEUED)) { + return Event.QUEUED; + } + if (commitStatus.description.contains(DefaultStatusMessages.BUILD_STARTED)) { + return Event.STARTED; + } + if (commitStatus.description.contains(DefaultStatusMessages.BUILD_MARKED_SUCCESSFULL)) { + return null; + } + case SUCCESS: + case ERROR: + return null; + case FAILURE: + return commitStatus.description != null + && (commitStatus.description.contains(DefaultStatusMessages.BUILD_REMOVED_FROM_QUEUE) + || commitStatus.description.contains(DefaultStatusMessages.BUILD_REMOVED_FROM_QUEUE_AS_CANCELED)) + ? Event.REMOVED_FROM_QUEUE : null; + default: + LOG.warn("No event is assosiated with Gitea build status \"" + status + "\". Related event can not be defined"); + } + return null; + } + + + private void publish(@NotNull SBuild build, + @NotNull BuildRevision revision, + @NotNull GiteaBuildStatus status, + @NotNull String description) throws PublisherException { + String buildName = getBuildName(build.getBuildPromotion()); + String message = createMessage(status, buildName, revision, getViewUrl(build), description); + publish(message, revision, LogUtil.describe(build)); + myStatusesCache.removeStatusFromCache(revision, buildName); + } + + private void publish(@NotNull BuildPromotion buildPromotion, + @NotNull BuildRevision revision, + @NotNull GiteaBuildStatus status, + @NotNull AdditionalTaskInfo additionalTaskInfo) throws PublisherException { + String url = getViewUrl(buildPromotion); + if (url == null) { + LOG.debug(String.format("Can not build view URL for the build #%d. Probably build configuration was removed. Status \"%s\" won't be published", + buildPromotion.getId(), status.getName())); + return; + } + String description = additionalTaskInfo.getComment(); + String buildName = getBuildName(buildPromotion); + String message = createMessage(status, buildName, revision, url, description); + publish(message, revision, LogUtil.describe(buildPromotion)); + myStatusesCache.removeStatusFromCache(revision, buildName); + } + + private void publish(@NotNull String message, + @NotNull BuildRevision revision, + @NotNull String buildDescription) throws PublisherException { + VcsRootInstance root = revision.getRoot(); + String apiUrl = getApiUrl(); + if (null == apiUrl || apiUrl.isEmpty()) + throw new PublisherException("Missing Gitea API URL parameter"); + String pathPrefix = GiteaSettings.getPathPrefix(apiUrl); + Repository repository = parseRepository(root, pathPrefix); + if (repository == null) + throw new PublisherException("Cannot parse repository URL from VCS root " + root.getName()); + + try { + publish(revision.getRevision(), message, repository, buildDescription); + } catch (Exception e) { + throw new PublisherException("Cannot publish status to Gitea(" + apiUrl + ") for VCS root " + + revision.getRoot().getName() + ": " + e.toString(), e); + } + } + + private void publish(@NotNull String commit, + @NotNull String data, + @NotNull Repository repository, + @NotNull String buildDescription) throws PublisherException { + String url = GiteaSettings.getProjectsUrl(getApiUrl(), repository.owner(), repository.repositoryName()) + "/statuses/" + commit; + LOG.debug("Request url: " + url + ", message: " + data); + url += "?access_token=" + getPrivateToken(); + postJson(url, null, data, null, buildDescription); + } + + @Override + public void processResponse(HttpHelper.HttpResponse response) throws IOException, HttpPublisherException { + final int statusCode = response.getStatusCode(); + if (statusCode >= 400) { + String responseString = response.getContent(); + if (404 == statusCode) { + throw new HttpPublisherException(statusCode, "Repository not found. Please check if it was renamed or moved to another namespace"); + } else if (!responseString.contains("Cannot transition status via :enqueue from :pending") && + !responseString.contains("Cannot transition status via :enqueue from :running") && + !responseString.contains("Cannot transition status via :run from :running")) { + throw new HttpPublisherException(statusCode, + response.getStatusText(), "HTTP response error: " + responseString); + } + } + } + + @NotNull + private String createMessage(@NotNull GiteaBuildStatus status, + @NotNull String name, + @NotNull BuildRevision revision, + @NotNull String url, + @NotNull String description) { + + final Map data = new LinkedHashMap<>(); + data.put("state", status.getName()); + data.put("context", name); + data.put("description", description); + data.put("target_url", url); + return myGson.toJson(data); + } + + @Nullable + static Repository parseRepository(@NotNull VcsRoot root, @Nullable String pathPrefix) { + if ("jetbrains.git".equals(root.getVcsName())) { + String url = root.getProperty("url"); + return url == null ? null : VCS_URL_PARSER.parseRepositoryUrl(url, pathPrefix); + } else { + return null; + } + } + + + private String getApiUrl() { + return HttpHelper.stripTrailingSlash(myParams.get(Constants.GITEA_API_URL)); + } + + private String getPrivateToken() { + return myParams.get(Constants.GITEA_TOKEN); + } + + /* Currently not needed + * determineStatusCommit + * getMergeRequest + * getParentRevisions + * determineParentInSourceBranch + * isOnlyInSourceBranch + * supportMergeResults + */ +} diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/GiteaSettings.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/GiteaSettings.java new file mode 100644 index 00000000..bf985b48 --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/GiteaSettings.java @@ -0,0 +1,271 @@ +/* + * Copyright 2000-2022 JetBrains s.r.o. + * + * 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 + * + * http://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 jetbrains.buildServer.commitPublisher.gitea; + +import jetbrains.buildServer.commitPublisher.*; +import jetbrains.buildServer.commitPublisher.CommitStatusPublisher.Event; +import jetbrains.buildServer.commitPublisher.gitea.data.GiteaCommitStatus; +import jetbrains.buildServer.commitPublisher.gitea.data.GiteaRepoInfo; +import jetbrains.buildServer.commitPublisher.gitea.data.GiteaUserInfo; +import jetbrains.buildServer.serverSide.*; +import jetbrains.buildServer.util.ssl.SSLTrustStoreProvider; +import jetbrains.buildServer.vcs.VcsRoot; +import jetbrains.buildServer.vcshostings.http.HttpHelper; +import jetbrains.buildServer.web.openapi.PluginDescriptor; +import jetbrains.buildServer.web.util.WebUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; +import java.util.regex.Pattern; + +public class GiteaSettings extends BasePublisherSettings implements CommitStatusPublisherSettings { + + private static final Pattern URL_WITH_API_SUFFIX = Pattern.compile("(.*)/api/v."); + + private static final Set mySupportedEvents = new HashSet() {{ + add(Event.STARTED); + add(Event.FINISHED); + add(Event.MARKED_AS_SUCCESSFUL); + add(Event.INTERRUPTED); + add(Event.FAILURE_DETECTED); + }}; + + private static final Set mySupportedEventsWithQueued = new HashSet() {{ + add(Event.QUEUED); + add(Event.REMOVED_FROM_QUEUE); + addAll(mySupportedEvents); + }}; + + private final CommitStatusesCache myStatusesCache; + + public GiteaSettings(@NotNull PluginDescriptor descriptor, + @NotNull WebLinks links, + @NotNull CommitStatusPublisherProblems problems, + @NotNull SSLTrustStoreProvider trustStoreProvider) { + super(descriptor, links, problems, trustStoreProvider); + myStatusesCache = new CommitStatusesCache<>(); + } + + @NotNull + @Override + public String getId() { + return Constants.GITEA_PUBLISHER_ID; + } + + @NotNull + @Override + public String getName() { + return "Gitea"; + } + + @Nullable + @Override + public String getEditSettingsUrl() { + return "gitea/giteaSettings.jsp"; + } + + @NotNull + @Override + public GiteaPublisher createPublisher(@NotNull SBuildType buildType, @NotNull String buildFeatureId, @NotNull Map params) { + return new GiteaPublisher(this, buildType, buildFeatureId, params, myProblems, myLinks, myStatusesCache); + } + + @Override + public boolean isTestConnectionSupported() { + return true; + } + + @Override + public void testConnection(@NotNull BuildTypeIdentity buildTypeOrTemplate, @NotNull VcsRoot root, @NotNull Map params) throws PublisherException { + String apiUrl = params.get(Constants.GITEA_API_URL); + if (null == apiUrl || apiUrl.length() == 0) + throw new PublisherException("Missing Gitea API URL parameter"); + String pathPrefix = getPathPrefix(apiUrl); + Repository repository = GiteaPublisher.parseRepository(root, pathPrefix); + if (null == repository) + throw new PublisherException("Cannot parse repository URL from VCS root " + root.getName()); + String token = params.get(Constants.GITEA_TOKEN); + if (null == token || token.length() == 0) + throw new PublisherException("Missing Gitea API access token"); + try { + IOGuard.allowNetworkCall(() -> { + ProjectInfoResponseProcessor processorPrj = new ProjectInfoResponseProcessor(); + String url = getProjectsUrl(apiUrl, repository.owner(), repository.repositoryName()); + url += "?access_token=" + token; + HttpHelper.get(url, null, null, + BaseCommitStatusPublisher.DEFAULT_CONNECTION_TIMEOUT, trustStore(), processorPrj); + if (!processorPrj.hasPushAccess()) { + UserInfoResponseProcessor processorUser = new UserInfoResponseProcessor(); + url = getUserUrl(apiUrl); + url += "?access_token=" + token; + HttpHelper.get(url, null, null, + BaseCommitStatusPublisher.DEFAULT_CONNECTION_TIMEOUT, trustStore(), processorUser); + if (!processorUser.isAdmin()) { + throw new HttpPublisherException("Gitea does not grant enough permissions to publish a commit status"); + } + } + }); + } catch (HttpPublisherException pe) { + Integer statusCode = pe.getStatusCode(); + if (Objects.equals(statusCode, 404)) { + throw new PublisherException(String.format("Repository \"%s\" can not be found. Please check if it was renamed or moved to another namespace", repository.repositoryName())); + } + if (Objects.equals(statusCode, 401) || Objects.equals(statusCode, 403)) { + throw new PublisherException(String.format("Cannot access the \"%s\" repository. Ensure you are using a valid access token instead of a password (authentication via password is no longer available)", repository.repositoryName())); + } + throw new PublisherException("Request was failed with error", pe); + } catch (Exception ex) { + throw new PublisherException(String.format("Gitea publisher has failed to connect to \"%s\" repository", repository.url()), ex); + } + } + + @Nullable + public static String getPathPrefix(final String apiUrl) { + if (!URL_WITH_API_SUFFIX.matcher(apiUrl).matches()) return null; + try { + URI uri = new URI(apiUrl); + String path = uri.getPath(); + return path.substring(0, path.length() - "/api/v1".length()); + } catch (URISyntaxException e) { + return null; + } + } + + @NotNull + @Override + public String describeParameters(@NotNull Map params) { + String result = super.describeParameters(params); + String url = params.get(Constants.GITEA_API_URL); + if (url != null) + result += " " + WebUtil.escapeXml(url); + return result; + } + + @Nullable + @Override + public PropertiesProcessor getParametersProcessor(@NotNull BuildTypeIdentity buildTypeOrTemplate) { + return new PropertiesProcessor() { + public Collection process(Map params) { + List errors = new ArrayList(); + if (params.get(Constants.GITEA_API_URL) == null) + errors.add(new InvalidProperty(Constants.GITEA_API_URL, "Gitea API URL must be specified")); + if (params.get(Constants.GITEA_TOKEN) == null) + errors.add(new InvalidProperty(Constants.GITEA_TOKEN, "Access token must be specified")); + return errors; + } + }; + } + + @NotNull + public static String getProjectsUrl(@NotNull String apiUrl, @NotNull String owner, @NotNull String repo) { + return apiUrl + "/repos/" + owner.replace(".", "%2E").replace("/", "%2F") + "/" + repo.replace(".", "%2E"); + } + + @NotNull + public static String getUserUrl(@NotNull String apiUrl) { + return apiUrl + "/user"; + } + + @Override + public boolean isPublishingForVcsRoot(final VcsRoot vcsRoot) { + return "jetbrains.git".equals(vcsRoot.getVcsName()); + } + + @Override + protected Set getSupportedEvents(final SBuildType buildType, final Map params) { + return isBuildQueuedSupported(buildType) ? mySupportedEventsWithQueued : mySupportedEvents; + } + + private abstract class JsonResponseProcessor extends DefaultHttpResponseProcessor { + + private final Class myInfoClass; + private T myInfo; + + JsonResponseProcessor(Class infoClass) { + myInfoClass = infoClass; + } + + T getInfo() { + return myInfo; + } + + @Override + public void processResponse(HttpHelper.HttpResponse response) throws HttpPublisherException, IOException { + + super.processResponse(response); + + final String json = response.getContent(); + if (null == json) { + throw new HttpPublisherException("Gitea publisher has received no response"); + } + myInfo = myGson.fromJson(json, myInfoClass); + } + } + + private class ProjectInfoResponseProcessor extends JsonResponseProcessor { + + private boolean myHasPushAccess; + + ProjectInfoResponseProcessor() { + super(GiteaRepoInfo.class); + } + + boolean hasPushAccess() { + return myHasPushAccess; + } + + @Override + public void processResponse(HttpHelper.HttpResponse response) throws HttpPublisherException, IOException { + myHasPushAccess = false; + super.processResponse(response); + GiteaRepoInfo repoInfo = getInfo(); + if (null == repoInfo || null == repoInfo.id || null == repoInfo.permissions) { + throw new HttpPublisherException("Gitea publisher has received a malformed response"); + } + myHasPushAccess = repoInfo.permissions.push; + } + } + + private class UserInfoResponseProcessor extends JsonResponseProcessor { + + private boolean myIsAdmin; + + UserInfoResponseProcessor() { + super(GiteaUserInfo.class); + } + + boolean isAdmin() { + return myIsAdmin; + } + + @Override + public void processResponse(HttpHelper.HttpResponse response) throws HttpPublisherException, IOException { + myIsAdmin = false; + super.processResponse(response); + GiteaUserInfo userInfo = getInfo(); + if (null == userInfo || null == userInfo.id) { + throw new HttpPublisherException("Gitea publisher has received a malformed response"); + } + myIsAdmin = userInfo.is_admin; + } + } + +} diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaCommitStatus.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaCommitStatus.java new file mode 100644 index 00000000..6ed7f090 --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaCommitStatus.java @@ -0,0 +1,20 @@ +package jetbrains.buildServer.commitPublisher.gitea.data; + +/** + * @author Felix Heim, 21/02/22. + */ +public class GiteaCommitStatus { + public final Long id; + public final String status; + public final String description; + public final String context; + public final String target_url; + + public GiteaCommitStatus(Long id, String status, String description, String context, String target_url) { + this.id = id; + this.status = status; + this.description = description; + this.context = context; + this.target_url = target_url; + } +} diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaPermissions.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaPermissions.java new file mode 100644 index 00000000..be4477aa --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaPermissions.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2022 JetBrains s.r.o. + * + * 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 + * + * http://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 jetbrains.buildServer.commitPublisher.gitea.data; + +/** + * @author Felix Heim, 21/02/22. + */ +public class GiteaPermissions { + public boolean push; +} diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaRepoInfo.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaRepoInfo.java new file mode 100644 index 00000000..0f4b1a44 --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaRepoInfo.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2022 JetBrains s.r.o. + * + * 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 + * + * http://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 jetbrains.buildServer.commitPublisher.gitea.data; + +/** + * This class does not represent full repository information. + */ +public class GiteaRepoInfo { + public String id; + public GiteaPermissions permissions; +} diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaUserInfo.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaUserInfo.java new file mode 100644 index 00000000..73881a47 --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/gitea/data/GiteaUserInfo.java @@ -0,0 +1,27 @@ +/* + * Copyright 2000-2022 JetBrains s.r.o. + * + * 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 + * + * http://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 jetbrains.buildServer.commitPublisher.gitea.data; + +/** + * @author Felix Heim, 21/02/22. + */ +public class GiteaUserInfo { + public String id; + public String full_name; + public String login; + public boolean is_admin; +} diff --git a/commit-status-publisher-server/src/main/resources/META-INF/build-server-plugin-voter.xml b/commit-status-publisher-server/src/main/resources/META-INF/build-server-plugin-voter.xml index a710cbf6..0a24ee86 100644 --- a/commit-status-publisher-server/src/main/resources/META-INF/build-server-plugin-voter.xml +++ b/commit-status-publisher-server/src/main/resources/META-INF/build-server-plugin-voter.xml @@ -38,6 +38,7 @@ + diff --git a/commit-status-publisher-server/src/main/resources/buildServerResources/gitea/giteaSettings.jsp b/commit-status-publisher-server/src/main/resources/buildServerResources/gitea/giteaSettings.jsp new file mode 100644 index 00000000..37668020 --- /dev/null +++ b/commit-status-publisher-server/src/main/resources/buildServerResources/gitea/giteaSettings.jsp @@ -0,0 +1,48 @@ +<%@ include file="/include-internal.jsp" %> +<%@ taglib prefix="props" tagdir="/WEB-INF/tags/props" %> +<%@ taglib prefix="l" tagdir="/WEB-INF/tags/layout" %> +<%-- + ~ Copyright 2000-2022 JetBrains s.r.o. + ~ + ~ 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 + ~ + ~ http://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. + --%> + + + + + + + + Format: http[s]://<hostname>[:<port>]/api/v1 + + + + + + + + + + + Can be found at /user/settings/applications in Gitea + + + + + + + diff --git a/commit-status-publisher-server/src/test/java/jetbrains/buildServer/commitPublisher/gitea/GiteaPublisherTest.java b/commit-status-publisher-server/src/test/java/jetbrains/buildServer/commitPublisher/gitea/GiteaPublisherTest.java new file mode 100644 index 00000000..1f6ac948 --- /dev/null +++ b/commit-status-publisher-server/src/test/java/jetbrains/buildServer/commitPublisher/gitea/GiteaPublisherTest.java @@ -0,0 +1,227 @@ +/* + * Copyright 2000-2022 JetBrains s.r.o. + * + * 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 + * + * http://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 jetbrains.buildServer.commitPublisher.gitea; + +import com.google.gson.Gson; +import jetbrains.buildServer.MockBuildPromotion; +import jetbrains.buildServer.commitPublisher.*; +import jetbrains.buildServer.commitPublisher.gitea.data.GiteaCommitStatus; +import jetbrains.buildServer.commitPublisher.gitea.data.GiteaPermissions; +import jetbrains.buildServer.commitPublisher.gitea.data.GiteaRepoInfo; +import jetbrains.buildServer.messages.Status; +import jetbrains.buildServer.serverSide.*; +import jetbrains.buildServer.vcs.VcsRootInstance; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.entity.StringEntity; +import org.jetbrains.annotations.NotNull; +import org.jmock.Mock; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Felix Heim, 21/02/22. + */ +@Test +public class GiteaPublisherTest extends HttpPublisherTest { + + private static final String GROUP_REPO = "group_repo"; + private final Map> myRevisionToStatuses = new HashMap<>(); + + public GiteaPublisherTest() { + myExpectedRegExps.put(EventToTest.QUEUED, String.format(".*/repos/owner/project/statuses/%s.*ENTITY:.*pending.*%s.*", REVISION, DefaultStatusMessages.BUILD_QUEUED)); + myExpectedRegExps.put(EventToTest.REMOVED, String.format(".*/repos/owner/project/statuses/%s.*ENTITY:.*failure.*%s\".*", REVISION, DefaultStatusMessages.BUILD_REMOVED_FROM_QUEUE)); + myExpectedRegExps.put(EventToTest.STARTED, String.format(".*/repos/owner/project/statuses/%s.*ENTITY:.*pending.*%s.*", REVISION, DefaultStatusMessages.BUILD_STARTED)); + myExpectedRegExps.put(EventToTest.FINISHED, String.format(".*/repos/owner/project/statuses/%s.*ENTITY:.*success.*Success.*", REVISION)); + myExpectedRegExps.put(EventToTest.FAILED, String.format(".*/repos/owner/project/statuses/%s.*ENTITY:.*failure.*Failure.*", REVISION)); + myExpectedRegExps.put(EventToTest.COMMENTED_SUCCESS, null); // not to be tested + myExpectedRegExps.put(EventToTest.COMMENTED_FAILED, null); // not to be tested + myExpectedRegExps.put(EventToTest.COMMENTED_INPROGRESS, null); // not to be tested + myExpectedRegExps.put(EventToTest.COMMENTED_INPROGRESS_FAILED, null); // not to be tested + myExpectedRegExps.put(EventToTest.INTERRUPTED, String.format(".*/repos/owner/project/statuses/%s.*ENTITY:.*failure.*%s.*", REVISION, PROBLEM_DESCR)); + myExpectedRegExps.put(EventToTest.FAILURE_DETECTED, String.format(".*/repos/owner/project/statuses/%s.*ENTITY:.*failure.*%s.*", REVISION, PROBLEM_DESCR)); + myExpectedRegExps.put(EventToTest.MARKED_SUCCESSFUL, String.format(".*/repos/owner/project/statuses/%s.*ENTITY:.*success.*%s.*", REVISION, DefaultStatusMessages.BUILD_MARKED_SUCCESSFULL)); + myExpectedRegExps.put(EventToTest.MARKED_RUNNING_SUCCESSFUL, String.format(".*/repos/owner/project/statuses/%s.*ENTITY:.*pending.*%s.*", REVISION, DefaultStatusMessages.BUILD_MARKED_SUCCESSFULL)); + myExpectedRegExps.put(EventToTest.TEST_CONNECTION, ".*/repos/owner/project.*"); + myExpectedRegExps.put(EventToTest.PAYLOAD_ESCAPED, String.format(".*/repos/owner/project/statuses/%s.*ENTITY:.*failure.*%s.*Failure.*", REVISION, BT_NAME_ESCAPED_REGEXP)); + } + + public void test_buildFinishedSuccessfully_server_url_with_subdir() throws Exception { + Map params = getPublisherParams(); + setExpectedApiPath("/subdir/api/v1"); + params.put(Constants.GITEA_API_URL, getServerUrl() + "/subdir/api/v1"); + myVcsRoot.setProperties(Collections.singletonMap("url", "https://url.com/subdir/owner/project")); + VcsRootInstance vcsRootInstance = myBuildType.getVcsRootInstanceForParent(myVcsRoot); + myRevision = new BuildRevision(vcsRootInstance, REVISION, "", REVISION); + myPublisher = new GiteaPublisher(myPublisherSettings, myBuildType, FEATURE_ID, params, myProblems, myWebLinks, new CommitStatusesCache<>()); + test_buildFinished_Successfully(); + } + + public void test_buildFinishedSuccessfully_server_url_with_slash() throws Exception { + Map params = getPublisherParams(); + setExpectedApiPath("/subdir/api/v1"); + params.put(Constants.GITEA_API_URL, getServerUrl() + "/subdir/api/v1/"); + myVcsRoot.setProperties(Collections.singletonMap("url", "https://url.com/subdir/owner/project")); + VcsRootInstance vcsRootInstance = myBuildType.getVcsRootInstanceForParent(myVcsRoot); + myRevision = new BuildRevision(vcsRootInstance, REVISION, "", REVISION); + myPublisher = new GiteaPublisher(myPublisherSettings, myBuildType, FEATURE_ID, params, myProblems, myWebLinks, new CommitStatusesCache<>()); + test_buildFinished_Successfully(); + } + + public void should_fail_with_error_on_wrong_vcs_url() throws InterruptedException { + myVcsRoot.setProperties(Collections.singletonMap("url", "wrong://url.com")); + VcsRootInstance vcsRootInstance = myBuildType.getVcsRootInstanceForParent(myVcsRoot); + BuildRevision revision = new BuildRevision(vcsRootInstance, REVISION, "", REVISION); + try { + myPublisher.buildFinished(myFixture.createBuild(myBuildType, Status.NORMAL), revision); + fail("PublishError exception expected"); + } catch(PublisherException ex) { + then(ex.getMessage()).matches("Cannot parse.*" + myVcsRoot.getName() + ".*"); + } + } + + public void should_work_with_dots_in_id() throws PublisherException, InterruptedException { + myVcsRoot.setProperties(Collections.singletonMap("url", "https://url.com/own.er/pro.ject")); + VcsRootInstance vcsRootInstance = myBuildType.getVcsRootInstanceForParent(myVcsRoot); + BuildRevision revision = new BuildRevision(vcsRootInstance, REVISION, "", REVISION); + setExpectedEndpointPrefix("/repos/own%2Eer/pro%2Eject"); + myPublisher.buildFinished(myFixture.createBuild(myBuildType, Status.NORMAL), revision); + then(getRequestAsString()).isNotNull().doesNotMatch(".*error.*") + .matches(String.format(".*/repos/own%%2Eer/pro%%2Eject/statuses/%s.*ENTITY:.*success.*Success.*", REVISION)); + } + + public void test_testConnection_group_repo() throws Exception { + if (!myPublisherSettings.isTestConnectionSupported()) return; + Map params = getPublisherParams(); + myVcsRoot.setProperties(Collections.singletonMap("url", getServerUrl() + "/" + OWNER + "/" + GROUP_REPO)); + myPublisherSettings.testConnection(myBuildType, myVcsRoot, params); + then(getRequestAsString()).isNotNull() + .doesNotMatch(".*error.*") + .matches(".*/repos/owner/group_repo.*"); + } + + public void should_calculate_correct_revision_status() { + BuildPromotion promotion = new MockBuildPromotion(); + GiteaPublisher publisher = (GiteaPublisher)myPublisher; + assertNull(publisher.getRevisionStatus(promotion, (GiteaCommitStatus)null)); + assertNull(publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, null, null, null, null)).getTriggeredEvent()); + assertNull(publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, "nonsense", null, null, null)).getTriggeredEvent()); + assertNull(publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.SUCCESS.getName(), null, null, null)).getTriggeredEvent()); + assertNull(publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.FAILURE.getName(), null, null, null)).getTriggeredEvent()); + assertNull(publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.ERROR.getName(), null, null, null)).getTriggeredEvent()); + assertNull(publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.PENDING.getName(), DefaultStatusMessages.BUILD_MARKED_SUCCESSFULL, null, null)).getTriggeredEvent()); + assertEquals(CommitStatusPublisher.Event.QUEUED, publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.PENDING.getName(), null, null, null)).getTriggeredEvent()); + assertEquals(CommitStatusPublisher.Event.STARTED, publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.PENDING.getName(), DefaultStatusMessages.BUILD_STARTED, null, null)).getTriggeredEvent()); + assertEquals(CommitStatusPublisher.Event.REMOVED_FROM_QUEUE, publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.FAILURE.getName(), DefaultStatusMessages.BUILD_REMOVED_FROM_QUEUE, null, null)).getTriggeredEvent()); + assertEquals(CommitStatusPublisher.Event.REMOVED_FROM_QUEUE, publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.FAILURE.getName(), DefaultStatusMessages.BUILD_REMOVED_FROM_QUEUE_AS_CANCELED, null, null)).getTriggeredEvent()); + assertNull(publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.PENDING.getName(), "", null, null)).getTriggeredEvent()); + assertEquals(CommitStatusPublisher.Event.QUEUED, publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.PENDING.getName(), DefaultStatusMessages.BUILD_QUEUED, null, null)).getTriggeredEvent()); + assertNull(publisher.getRevisionStatus(promotion, new GiteaCommitStatus(null, GiteaBuildStatus.FAILURE.getName(), null, null, null)).getTriggeredEvent()); + } + + public void should_allow_queued_depending_on_build_type() { + Mock removedBuildMock = new Mock(SQueuedBuild.class); + removedBuildMock.stubs().method("getBuildTypeId").withNoArguments().will(returnValue("buildType")); + removedBuildMock.stubs().method("getItemId").withNoArguments().will(returnValue("123")); + Mock buildPromotionMock = new Mock(BuildPromotion.class); + Mock buildTypeMock = new Mock(SBuildType.class); + buildTypeMock.stubs().method("getFullName").withNoArguments().will(returnValue("typeFullName")); + buildPromotionMock.stubs().method("getBuildType").withNoArguments().will(returnValue(buildTypeMock.proxy())); + removedBuildMock.stubs().method("getBuildPromotion").withNoArguments().will(returnValue(buildPromotionMock.proxy())); + SQueuedBuild removedBuild = (SQueuedBuild)removedBuildMock.proxy(); + + GiteaPublisher publisher = (GiteaPublisher)myPublisher; + assertTrue(publisher.getRevisionStatusForRemovedBuild(removedBuild, new GiteaCommitStatus(null, GiteaBuildStatus.PENDING.getName(), DefaultStatusMessages.BUILD_QUEUED, "typeFullName", "http://localhost:8111/viewQueued.html?itemId=123")).isEventAllowed(CommitStatusPublisher.Event.REMOVED_FROM_QUEUE, Long.MAX_VALUE)); + assertFalse(publisher.getRevisionStatusForRemovedBuild(removedBuild, new GiteaCommitStatus(null, GiteaBuildStatus.PENDING.getName(), DefaultStatusMessages.BUILD_QUEUED, "anotherTypeFullName", "http://localhost:8111/viewQueued.html?itemId=321")).isEventAllowed(CommitStatusPublisher.Event.REMOVED_FROM_QUEUE, Long.MAX_VALUE)); + } + + @BeforeMethod + @Override + protected void setUp() throws Exception { + setExpectedApiPath("/api/v1"); + setExpectedEndpointPrefix("/repos/" + OWNER + "/" + CORRECT_REPO); + super.setUp(); + myPublisherSettings = new GiteaSettings(new MockPluginDescriptor(), myWebLinks, myProblems, myTrustStoreProvider); + Map params = getPublisherParams(); + myPublisher = new GiteaPublisher(myPublisherSettings, myBuildType, FEATURE_ID, params, myProblems, myWebLinks, new CommitStatusesCache<>()); + myBuildType.getProject().addParameter(new SimpleParameter("teamcity.commitStatusPublisher.publishQueuedBuildStatus", "true")); + } + + @Override + protected Map getPublisherParams() { + return new HashMap() {{ + put(Constants.GITEA_TOKEN, "TOKEN"); + put(Constants.GITEA_API_URL, getServerUrl() + getExpectedApiPath()); + }}; + } + + @Override + protected boolean respondToGet(String url, HttpResponse httpResponse) { + if (url.contains("/repos/" + OWNER + "/" + CORRECT_REPO + "/statuses")) { + String revision = getRevision(url, "/api/v1/repos/" + OWNER + "/" + CORRECT_REPO + "/statuses/"); + respondWithStatuses(httpResponse, revision); + } else if (url.contains("/repos" + "/" + OWNER + "/" + CORRECT_REPO)) { + respondWithRepoInfo(httpResponse, CORRECT_REPO, false, true); + } else if (url.contains("/repos" + "/" + OWNER + "/" + GROUP_REPO)) { + respondWithRepoInfo(httpResponse, GROUP_REPO, true, true); + } else if (url.contains("/repos" + "/" + OWNER + "/" + READ_ONLY_REPO)) { + respondWithRepoInfo(httpResponse, READ_ONLY_REPO, false, false); + } else { + respondWithError(httpResponse, 404, String.format("Unexpected URL: %s", url)); + return false; + } + return true; + } + + private void respondWithStatuses(HttpResponse httpResponse, String revision) { + List statuses = myRevisionToStatuses.getOrDefault(revision, new ArrayList<>()); + String json = gson.toJson(statuses.stream().map(s -> new GiteaCommitStatus(0L, s.status, s.description, s.context, s.target_url)).collect(Collectors.toList())); + httpResponse.setEntity(new StringEntity(json, StandardCharsets.UTF_8)); + } + + @Override + protected boolean respondToPost(String url, String requestData, final HttpRequest httpRequest, HttpResponse httpResponse) { + String revision = getRevision(url, "/api/v1/repos/" + OWNER + "/" + CORRECT_REPO + "/statuses/"); + if (revision != null) { + GiteaCommitStatus status = gson.fromJson(requestData, GiteaCommitStatus.class); + myRevisionToStatuses.computeIfAbsent(revision, k -> new ArrayList<>()).add(status); + } + return isUrlExpected(url, httpResponse); + } + + + private void respondWithRepoInfo(HttpResponse httpResponse, String repoName, boolean isGroupRepo, boolean isPushPermitted) { + Gson gson = new Gson(); + GiteaRepoInfo repoInfo = new GiteaRepoInfo(); + repoInfo.id = "111"; + repoInfo.permissions = new GiteaPermissions(); + repoInfo.permissions.push = isPushPermitted; + String jsonResponse = gson.toJson(repoInfo); + httpResponse.setEntity(new StringEntity(jsonResponse, "UTF-8")); + } + + @Override + protected boolean checkEventFinished(@NotNull String requestString, boolean isSuccessful) { + return requestString.contains(isSuccessful ? "success" : "failure"); + } +} diff --git a/commit-status-publisher-server/src/test/testng-commit-status-publisher.xml b/commit-status-publisher-server/src/test/testng-commit-status-publisher.xml index 7763f7ea..5e56e6b2 100644 --- a/commit-status-publisher-server/src/test/testng-commit-status-publisher.xml +++ b/commit-status-publisher-server/src/test/testng-commit-status-publisher.xml @@ -29,6 +29,7 @@ + diff --git a/kotlin-dsl/CommitStatusPublisher.xml b/kotlin-dsl/CommitStatusPublisher.xml index 204d7226..8cebe4b0 100644 --- a/kotlin-dsl/CommitStatusPublisher.xml +++ b/kotlin-dsl/CommitStatusPublisher.xml @@ -176,6 +176,21 @@ +