Add workflow step that updates the GitLab commit status depending on the build status (fixes #262)

This commit is contained in:
Robin Müller 2016-04-28 19:39:28 +02:00
parent 099fe76dd3
commit eb1abb2e2c
5 changed files with 230 additions and 86 deletions

View File

@ -123,6 +123,12 @@ If you plan to use forked repositories, you will need to enable the GitLab CI in
**Note:** You do not need to select any "Trigger Events" as the Web Hook for Merge Request Events will alert Jenkins.
* GitLab 8.1 has implemented a commit status api. To enable add the post-build step ``Publish build status to GitLab commit (GitLab 8.1+ required)`` to the job.
For pipeline jobs surround your build step with the gitlabCommitStatus step like this:
```
gitlabCommitStatus {
<script that builds your project>
}
```
* Configure access to GitLab as described above in "Configure access to GitLab" (the account needs at least developer permissions to post commit statuses)
## Forked repositories

View File

@ -7,7 +7,7 @@
</parent>
<properties>
<jenkins.version>1.609.1</jenkins.version>
<jenkins.version>1.609.3</jenkins.version>
<hpi-plugin.version>1.115</hpi-plugin.version>
<jenkins-test-harness.version>${jenkins.version}</jenkins-test-harness.version>
<findbugs.failOnError>false</findbugs.failOnError>
@ -161,6 +161,11 @@
<artifactId>git-client</artifactId>
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>

View File

@ -1,40 +1,26 @@
package com.dabsquared.gitlabjenkins.publisher;
import com.dabsquared.gitlabjenkins.cause.GitLabWebHookCause;
import com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty;
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabApi;
import com.dabsquared.gitlabjenkins.gitlab.api.model.BuildState;
import com.dabsquared.gitlabjenkins.util.ProjectIdUtil;
import hudson.EnvVars;
import com.dabsquared.gitlabjenkins.util.CommitStatusUpdater;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.plugins.git.util.BuildData;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.DataBoundConstructor;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.WebApplicationException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author Robin Müller
*/
public class GitLabCommitStatusPublisher extends Notifier {
private final static Logger LOGGER = Logger.getLogger(GitLabCommitStatusPublisher.class.getName());
@DataBoundConstructor
public GitLabCommitStatusPublisher() { }
@ -44,89 +30,23 @@ public class GitLabCommitStatusPublisher extends Notifier {
@Override
public boolean prebuild(AbstractBuild<?, ?> build, BuildListener listener) {
String commitHash = getBuildRevision(build);
updateCommitStatus(build, listener, BuildState.running, commitHash, getBuildUrl(build));
CommitStatusUpdater.updateCommitStatus(build, listener, BuildState.running);
return true;
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
String commitHash = getBuildRevision(build);
String buildUrl = getBuildUrl(build);
Result buildResult = build.getResult();
if (buildResult == Result.SUCCESS) {
updateCommitStatus(build, listener, BuildState.success, commitHash, buildUrl);
CommitStatusUpdater.updateCommitStatus(build, listener, BuildState.success);
} else if (buildResult == Result.ABORTED) {
updateCommitStatus(build, listener, BuildState.canceled, commitHash, buildUrl);
CommitStatusUpdater.updateCommitStatus(build, listener, BuildState.canceled);
} else {
updateCommitStatus(build, listener, BuildState.failed, commitHash, buildUrl);
CommitStatusUpdater.updateCommitStatus(build, listener, BuildState.failed);
}
return true;
}
private String getBuildRevision(AbstractBuild<?, ?> build) {
return build.getAction(BuildData.class).getLastBuiltRevision().getSha1String();
}
private void updateCommitStatus(AbstractBuild<?, ?> build, BuildListener listener, BuildState state, String commitHash, String buildUrl) {
try {
for (String gitlabProjectId : retrieveGitlabProjectIds(build, build.getEnvironment(listener))) {
try {
GitLabApi client = getClient(build);
if (client == null) {
listener.getLogger().println("No GitLab connection configured");
} else if (existsCommit(client, gitlabProjectId, commitHash)) {
client.changeBuildStatus(gitlabProjectId, commitHash, state, getBuildBranch(build), "jenkins", buildUrl, null);
}
} catch (WebApplicationException e) {
listener.getLogger().printf("Failed to update Gitlab commit status for project '%s': %s%n", gitlabProjectId, e.getMessage());
LOGGER.log(Level.SEVERE, String.format("Failed to update Gitlab commit status for project '%s'", gitlabProjectId), e);
}
}
} catch (IOException | InterruptedException e) {
listener.getLogger().printf("Failed to update Gitlab commit status: %s%n", e.getMessage());
}
}
private boolean existsCommit(GitLabApi client, String gitlabProjectId, String commitHash) {
try {
client.headCommit(gitlabProjectId, commitHash);
return true;
} catch (NotFoundException e) {
LOGGER.log(Level.FINE, String.format("Project (%s) and commit (%s) combination not found", gitlabProjectId, commitHash));
return false;
}
}
private String getBuildBranch(AbstractBuild<?, ?> build) {
GitLabWebHookCause cause = build.getCause(GitLabWebHookCause.class);
return cause == null ? null : cause.getData().getSourceBranch();
}
private String getBuildUrl(AbstractBuild<?, ?> build) {
return Jenkins.getInstance().getRootUrl() + build.getUrl();
}
private GitLabApi getClient(AbstractBuild<?, ?> build) {
GitLabConnectionProperty connectionProperty = build.getProject().getProperty(GitLabConnectionProperty.class);
if (connectionProperty != null) {
return connectionProperty.getClient();
}
return null;
}
private List<String> retrieveGitlabProjectIds(AbstractBuild<?, ?> build, EnvVars environment) {
List<String> result = new ArrayList<>();
for (String remoteUrl : build.getAction(BuildData.class).getRemoteUrls()) {
try {
result.add(ProjectIdUtil.retrieveProjectId(environment.expand(remoteUrl)));
} catch (ProjectIdUtil.ProjectIdResolutionException e) {
// nothing to do
}
}
return result;
}
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {

View File

@ -0,0 +1,109 @@
package com.dabsquared.gitlabjenkins.util;
import com.dabsquared.gitlabjenkins.cause.GitLabWebHookCause;
import com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty;
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabApi;
import com.dabsquared.gitlabjenkins.gitlab.api.model.BuildState;
import hudson.EnvVars;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.plugins.git.util.BuildData;
import jenkins.model.Jenkins;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.WebApplicationException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author Robin Müller
*/
public class CommitStatusUpdater {
private final static Logger LOGGER = Logger.getLogger(CommitStatusUpdater.class.getName());
public static void updateCommitStatus(Run<?, ?> build, TaskListener listener, BuildState state) {
String commitHash = getBuildRevision(build);
String buildUrl = getBuildUrl(build);
try {
for (String gitlabProjectId : retrieveGitlabProjectIds(build, build.getEnvironment(listener))) {
try {
GitLabApi client = getClient(build);
if (client == null) {
println(listener, "No GitLab connection configured");
} else if (existsCommit(client, gitlabProjectId, commitHash)) {
client.changeBuildStatus(gitlabProjectId, commitHash, state, getBuildBranch(build), "jenkins", buildUrl, null);
}
} catch (WebApplicationException e) {
printf(listener, "Failed to update Gitlab commit status for project '%s': %s%n", gitlabProjectId, e.getMessage());
LOGGER.log(Level.SEVERE, String.format("Failed to update Gitlab commit status for project '%s'", gitlabProjectId), e);
}
}
} catch (IOException | InterruptedException e) {
printf(listener, "Failed to update Gitlab commit status: %s%n", e.getMessage());
}
}
private static void println(TaskListener listener, String message) {
if (listener == null) {
LOGGER.log(Level.FINE, "failed to print message {0} due to null TaskListener", message);
} else {
listener.getLogger().println(message);
}
}
private static void printf(TaskListener listener, String message, Object... args) {
if (listener == null) {
LOGGER.log(Level.FINE, "failed to print message {0} due to null TaskListener", String.format(message, args));
} else {
listener.getLogger().printf(message, args);
}
}
private static String getBuildRevision(Run<?, ?> build) {
return build.getAction(BuildData.class).getLastBuiltRevision().getSha1String();
}
private static boolean existsCommit(GitLabApi client, String gitlabProjectId, String commitHash) {
try {
client.headCommit(gitlabProjectId, commitHash);
return true;
} catch (NotFoundException e) {
LOGGER.log(Level.FINE, String.format("Project (%s) and commit (%s) combination not found", gitlabProjectId, commitHash));
return false;
}
}
private static String getBuildBranch(Run<?, ?> build) {
GitLabWebHookCause cause = build.getCause(GitLabWebHookCause.class);
return cause == null ? null : cause.getData().getSourceBranch();
}
private static String getBuildUrl(Run<?, ?> build) {
return Jenkins.getInstance().getRootUrl() + build.getUrl();
}
private static GitLabApi getClient(Run<?, ?> build) {
GitLabConnectionProperty connectionProperty = build.getParent().getProperty(GitLabConnectionProperty.class);
if (connectionProperty != null) {
return connectionProperty.getClient();
}
return null;
}
private static List<String> retrieveGitlabProjectIds(Run<?, ?> build, EnvVars environment) {
List<String> result = new ArrayList<>();
for (String remoteUrl : build.getAction(BuildData.class).getRemoteUrls()) {
try {
result.add(ProjectIdUtil.retrieveProjectId(environment.expand(remoteUrl)));
} catch (ProjectIdUtil.ProjectIdResolutionException e) {
// nothing to do
}
}
return result;
}
}

View File

@ -0,0 +1,104 @@
package com.dabsquared.gitlabjenkins.workflow;
import com.dabsquared.gitlabjenkins.gitlab.api.model.BuildState;
import com.dabsquared.gitlabjenkins.util.CommitStatusUpdater;
import hudson.Extension;
import hudson.model.Run;
import hudson.model.TaskListener;
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.BodyExecution;
import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.export.ExportedBean;
import javax.annotation.Nonnull;
/**
* @author <a href="mailto:robin.mueller@1und1.de">Robin Müller</a>
*/
@ExportedBean
public class GitLabCommitStatusStep extends AbstractStepImpl {
@DataBoundConstructor
public GitLabCommitStatusStep() { }
public static class Execution extends AbstractStepExecutionImpl {
private static final long serialVersionUID = 1;
@StepContextParameter
private transient Run<?, ?> run;
private BodyExecution body;
@Override
public boolean start() throws Exception {
body = getContext().newBodyInvoker()
.withCallback(new BodyExecutionCallback() {
@Override
public void onStart(StepContext context) {
CommitStatusUpdater.updateCommitStatus(run, getTaskListener(context), BuildState.running);
}
@Override
public void onSuccess(StepContext context, Object result) {
CommitStatusUpdater.updateCommitStatus(run, getTaskListener(context), BuildState.success);
context.onSuccess(result);
}
@Override
public void onFailure(StepContext context, Throwable t) {
CommitStatusUpdater.updateCommitStatus(run, getTaskListener(context), BuildState.failed);
context.onFailure(t);
}
})
.start();
return false;
}
@Override
public void stop(@Nonnull Throwable cause) throws Exception {
// should be no need to do anything special (but verify in JENKINS-26148)
if (body != null) {
CommitStatusUpdater.updateCommitStatus(run, null, BuildState.canceled);
body.cancel(cause);
}
}
private TaskListener getTaskListener(StepContext context) {
if (!context.isReady()) {
return null;
}
try {
return context.get(TaskListener.class);
} catch (Exception x) {
return null;
}
}
}
@Extension
public static final class DescriptorImpl extends AbstractStepDescriptorImpl {
public DescriptorImpl() {
super(Execution.class);
}
@Override
public String getDisplayName() {
return "Update the commit status in GitLab depending on the build status";
}
@Override
public String getFunctionName() {
return "gitlabCommitStatus";
}
@Override
public boolean takesImplicitBlockArgument() {
return true;
}
}
}