Skip to content

Commit 6b976c1

Browse files
author
Marco Marche
committed
code refactoring + added optional parameters for notification from pipeline
1 parent de30edd commit 6b976c1

File tree

7 files changed

+449
-564
lines changed

7 files changed

+449
-564
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package org.jenkinsci.plugins.bitbucket;
2+
3+
import com.cloudbees.plugins.credentials.CredentialsProvider;
4+
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
5+
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
6+
import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
7+
import com.google.gson.Gson;
8+
import com.google.gson.GsonBuilder;
9+
import hudson.model.*;
10+
import hudson.plugins.git.GitSCM;
11+
import hudson.plugins.mercurial.MercurialSCM;
12+
import hudson.scm.SCM;
13+
import hudson.tasks.test.AbstractTestResultAction;
14+
import hudson.util.LogTaskListener;
15+
16+
import java.util.ArrayList;
17+
import java.util.Collection;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.logging.Level;
21+
import java.util.logging.Logger;
22+
23+
import org.apache.commons.codec.digest.DigestUtils;
24+
import org.eclipse.jgit.transport.URIish;
25+
26+
import org.jenkinsci.plugins.bitbucket.api.BitbucketApi;
27+
import org.jenkinsci.plugins.bitbucket.api.BitbucketApiService;
28+
import org.jenkinsci.plugins.bitbucket.model.BitbucketBuildStatus;
29+
import org.jenkinsci.plugins.bitbucket.model.BitbucketBuildStatusResource;
30+
import org.jenkinsci.plugins.bitbucket.model.BitbucketBuildStatusSerializer;
31+
import org.jenkinsci.plugins.bitbucket.scm.GitScmAdapter;
32+
import org.jenkinsci.plugins.bitbucket.scm.MercurialScmAdapter;
33+
import org.jenkinsci.plugins.bitbucket.scm.MultiScmAdapter;
34+
import org.jenkinsci.plugins.bitbucket.scm.ScmAdapter;
35+
import org.jenkinsci.plugins.bitbucket.validator.BitbucketHostValidator;
36+
import org.jenkinsci.plugins.multiplescms.MultiSCM;
37+
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
38+
import org.jenkinsci.plugins.workflow.steps.StepContext;
39+
import org.scribe.model.*;
40+
41+
class BitbucketBuildStatusHelper {
42+
private static final Logger logger = Logger.getLogger(BitbucketBuildStatusHelper.class.getName());
43+
private static final BitbucketHostValidator hostValidator = new BitbucketHostValidator();
44+
45+
private static List<BitbucketBuildStatusResource> createBuildStatusResources(final SCM scm,
46+
final Run<?, ?> build) throws Exception {
47+
List<BitbucketBuildStatusResource> buildStatusResources = new ArrayList<BitbucketBuildStatusResource>();
48+
49+
if (scm == null) {
50+
throw new Exception("Bitbucket build notifier only works with SCM");
51+
}
52+
53+
ScmAdapter scmAdapter;
54+
if (scm instanceof GitSCM) {
55+
scmAdapter = new GitScmAdapter((GitSCM) scm, build);
56+
} else if (scm instanceof MercurialSCM) {
57+
scmAdapter = new MercurialScmAdapter((MercurialSCM) scm);
58+
} else if (scm instanceof MultiSCM) {
59+
scmAdapter = new MultiScmAdapter((MultiSCM)scm, build);
60+
} else {
61+
throw new Exception("Bitbucket build notifier requires a git repo or a mercurial repo as SCM");
62+
}
63+
64+
Map<String, URIish> commitRepoMap = scmAdapter.getCommitRepoMap();
65+
for (Map.Entry<String, URIish> commitRepoPair : commitRepoMap.entrySet()) {
66+
67+
// if repo is not hosted in bitbucket.org then log it and remove repo from being notified
68+
URIish repoUri = commitRepoPair.getValue();
69+
if (!hostValidator.isValid(repoUri.getHost())) {
70+
logger.log(Level.INFO, hostValidator.renderError());
71+
continue;
72+
}
73+
74+
// expand parameters on repo url
75+
String repoUrl = build.getEnvironment(new LogTaskListener(logger, Level.INFO)).expand(repoUri.getPath());
76+
77+
// extract bitbucket user name and repository name from repo URI
78+
String repoName = repoUrl.substring(
79+
repoUrl.lastIndexOf("/") + 1,
80+
repoUrl.contains(".git") ? repoUrl.indexOf(".git") : repoUrl.length()
81+
);
82+
83+
if (repoName.isEmpty()) {
84+
logger.log(Level.INFO, "Bitbucket build notifier could not extract the repository name from the repository URL");
85+
continue;
86+
}
87+
88+
String userName = repoUrl.substring(0, repoUrl.indexOf("/" + repoName));
89+
if (userName.contains("/")) {
90+
userName = userName.substring(userName.indexOf("/") + 1, userName.length());
91+
}
92+
if (userName.isEmpty()) {
93+
logger.log(Level.INFO, "Bitbucket build notifier could not extract the user name from the repository URL");
94+
continue;
95+
}
96+
97+
String commitId = commitRepoPair.getKey();
98+
if (commitId == null) {
99+
logger.log(Level.INFO, "Commit ID could not be found!");
100+
continue;
101+
}
102+
103+
buildStatusResources.add(new BitbucketBuildStatusResource(userName, repoName, commitId));
104+
}
105+
106+
return buildStatusResources;
107+
}
108+
109+
public static List<BitbucketBuildStatusResource> createBuildStatusResources(final Run<?, ?> build) throws Exception {
110+
Job<?, ?> project = build.getParent();
111+
List<BitbucketBuildStatusResource> buildStatusResources = new ArrayList<BitbucketBuildStatusResource>();
112+
113+
if(project instanceof WorkflowJob) {
114+
Collection<? extends SCM> scms = ((WorkflowJob)project).getSCMs();
115+
116+
for (SCM scm : scms) {
117+
buildStatusResources.addAll(createBuildStatusResources(scm, build));
118+
}
119+
}
120+
else if(project instanceof AbstractProject) {
121+
SCM scm = ((AbstractProject)project).getScm();
122+
buildStatusResources = createBuildStatusResources(scm, build);
123+
}
124+
125+
return buildStatusResources;
126+
}
127+
128+
public static String defaultBitbucketBuildKeyFromBuild(Run<?, ?> build) {
129+
Job<?, ?> project = build.getParent();
130+
return DigestUtils.md5Hex(project.getFullDisplayName() + "#" + build.getNumber());
131+
}
132+
133+
public static String defaultBitbucketBuildNameFromBuild(Run<?, ?> build) {
134+
Job<?, ?> project = build.getParent();
135+
return project.getFullDisplayName() + " #" + build.getNumber();
136+
}
137+
138+
public static String defaultBitbucketBuildDescriptionFromBuild(Run<?, ?> build) {
139+
AbstractTestResultAction testResult = build.getAction(AbstractTestResultAction.class);
140+
String description = "";
141+
if (testResult != null) {
142+
int passedCount = testResult.getTotalCount() - testResult.getFailCount();
143+
description = passedCount + " of " + testResult.getTotalCount() + " tests passed";
144+
}
145+
return description;
146+
}
147+
148+
public static String builUrlFromBuild(Run<?, ?> build) {
149+
Job<?, ?> project = build.getParent();
150+
return project.getAbsoluteUrl() + build.getNumber() + '/';
151+
}
152+
153+
private static BitbucketBuildStatus createBitbucketBuildStatusFromBuild(Run<?, ?> build) throws Exception {
154+
String buildState = guessBitbucketBuildState(build.getResult());
155+
// bitbucket requires the key to be shorter than 40 chars
156+
String buildKey = defaultBitbucketBuildKeyFromBuild(build);
157+
String buildUrl = builUrlFromBuild(build);
158+
String buildName = defaultBitbucketBuildNameFromBuild(build);
159+
String description = defaultBitbucketBuildDescriptionFromBuild(build);
160+
161+
return new BitbucketBuildStatus(buildState, buildKey, buildUrl, buildName, description);
162+
}
163+
164+
private static String guessBitbucketBuildState(final Result result) {
165+
166+
String state;
167+
168+
// possible statuses SUCCESS, UNSTABLE, FAILURE, NOT_BUILT, ABORTED
169+
if (result == null) {
170+
state = BitbucketBuildStatus.INPROGRESS;
171+
} else if (Result.SUCCESS == result) {
172+
state = BitbucketBuildStatus.SUCCESSFUL;
173+
} else if (Result.UNSTABLE == result || Result.FAILURE == result || Result.ABORTED == result) {
174+
state = BitbucketBuildStatus.FAILED;
175+
} else {
176+
// return empty status for every other result (NOT_BUILT, ABORTED)
177+
state = null;
178+
}
179+
180+
return state;
181+
}
182+
183+
public static void notifyBuildStatus(final String credentialsId, final Run<?, ?> build,
184+
final TaskListener listener) throws Exception {
185+
notifyBuildStatus(credentialsId, build, listener, createBitbucketBuildStatusFromBuild(build));
186+
}
187+
188+
public static void notifyBuildStatus(final String credentialsId, final Run<?, ?> build,
189+
final TaskListener listener, BitbucketBuildStatus buildStatus) throws Exception {
190+
191+
List<BitbucketBuildStatusResource> buildStatusResources = createBuildStatusResources(build);
192+
193+
Run<?, ?> prevBuild = build.getPreviousBuild();
194+
List<BitbucketBuildStatusResource> prevBuildStatusResources = new ArrayList<BitbucketBuildStatusResource>();
195+
if (prevBuild != null && prevBuild.getResult() != null && prevBuild.getResult() == Result.ABORTED) {
196+
prevBuildStatusResources = createBuildStatusResources(prevBuild);
197+
}
198+
199+
for (BitbucketBuildStatusResource buildStatusResource : buildStatusResources) {
200+
201+
// if previous build was manually aborted by the user and revision is the same than the current one
202+
// then update the bitbucket build status resource with current status and current build number
203+
for (BitbucketBuildStatusResource prevBuildStatusResource : prevBuildStatusResources) {
204+
if (prevBuildStatusResource.getCommitId().equals(buildStatusResource.getCommitId())) {
205+
BitbucketBuildStatus prevBuildStatus = createBitbucketBuildStatusFromBuild(prevBuild);
206+
buildStatus.setKey(prevBuildStatus.getKey());
207+
break;
208+
}
209+
}
210+
211+
sendBuildStatusNotification(credentialsId, build, buildStatusResource, buildStatus, listener);
212+
}
213+
}
214+
215+
public static void sendBuildStatusNotification(final String credentialsId,
216+
final Run<?, ?> build,
217+
final BitbucketBuildStatusResource buildStatusResource,
218+
final BitbucketBuildStatus buildStatus,
219+
final TaskListener listener) throws Exception {
220+
UsernamePasswordCredentials credentials = getCredentials(credentialsId, build.getParent());
221+
222+
if (credentials == null) {
223+
throw new Exception("Credentials could not be found!");
224+
}
225+
226+
OAuthConfig config = new OAuthConfig(credentials.getUsername(), credentials.getPassword().getPlainText());
227+
BitbucketApiService apiService = (BitbucketApiService) new BitbucketApi().createService(config);
228+
229+
GsonBuilder gsonBuilder = new GsonBuilder();
230+
gsonBuilder.registerTypeAdapter(BitbucketBuildStatus.class, new BitbucketBuildStatusSerializer());
231+
gsonBuilder.setPrettyPrinting();
232+
Gson gson = gsonBuilder.create();
233+
234+
OAuthRequest request = new OAuthRequest(Verb.POST, buildStatusResource.generateUrl(Verb.POST));
235+
request.addHeader("Content-type", "application/json");
236+
request.addPayload(gson.toJson(buildStatus));
237+
238+
Token token = apiService.getAccessToken(OAuthConstants.EMPTY_TOKEN, null);
239+
apiService.signRequest(token, request);
240+
241+
Response response = request.send();
242+
243+
logger.info("This request was sent: " + request.getBodyContents());
244+
logger.info("This response was received: " + response.getBody());
245+
listener.getLogger().println("Sending build status " + buildStatus.getState() +
246+
" for commit " + buildStatusResource.getCommitId() + " to BitBucket is done!");
247+
}
248+
249+
public static StandardUsernamePasswordCredentials getCredentials(String credentialsId, Job<?,?> owner) {
250+
if (credentialsId != null) {
251+
for (StandardUsernamePasswordCredentials c : CredentialsProvider.lookupCredentials(
252+
StandardUsernamePasswordCredentials.class, owner, null,
253+
URIRequirementBuilder.fromUri(BitbucketApi.OAUTH_ENDPOINT).build())) {
254+
if (c.getId().equals(credentialsId)) {
255+
return c;
256+
}
257+
}
258+
}
259+
260+
return null;
261+
}
262+
}

0 commit comments

Comments
 (0)