screwdriver-coverage-sonar 3.3.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands.txt +3 -3
- package/index.js +287 -291
- package/package.json +6 -10
package/commands.txt
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
if [ ! -f sonar-project.properties ] && [ -z "$SD_SONAR_OPTS" ]; then echo "ERROR - No sonar project properties defined." && exit 0; fi
|
|
2
|
-
if [ ! -z "$SD_PULL_REQUEST" ] && [ "$SD_SONAR_ENTERPRISE" = true ]; then export SD_SONAR_OPTS="${SD_SONAR_OPTS} -Dsonar.pullrequest.key=$SD_PULL_REQUEST -Dsonar.pullrequest.branch
|
|
2
|
+
if [ ! -z "$SD_PULL_REQUEST" ] && [ "$SD_SONAR_ENTERPRISE" = true ]; then export SD_SONAR_OPTS="${SD_SONAR_OPTS} -Dsonar.pullrequest.key=$SD_PULL_REQUEST -Dsonar.pullrequest.branch='${PR_BRANCH_NAME#*/}' -Dsonar.pullrequest.base='${PR_BASE_BRANCH_NAME}'"; fi
|
|
3
3
|
if curl --version > /dev/null 2>&1; then SD_CURL_CMD_WRAPPER="eval"; else SD_CURL_CMD_WRAPPER="sd-step exec --pkg-version 7.54.1 core/curl"; fi
|
|
4
4
|
export SD_CURL_CMD_WRAPPER
|
|
5
|
-
SONAR_TOKEN=`$SD_CURL_CMD_WRAPPER "curl -H \"Authorization: Bearer $SD_TOKEN\" $SD_SONAR_AUTH_URL"`
|
|
5
|
+
SONAR_TOKEN=`$SD_CURL_CMD_WRAPPER "curl -H \"Authorization: Bearer ${SD_TOKEN}\" \"${SD_SONAR_AUTH_URL}&selfSonarHost=${SD_SELF_SONAR_HOST}&selfSonarAdminToken=${SD_SELF_SONAR_ADMIN_TOKEN}\""`
|
|
6
6
|
VERSION=`cat sonar-project.properties | awk -F= '/sonar.projectVersion=/{print $2}'`
|
|
7
7
|
PROJECT_VERSION=${VERSION:='not provided'}
|
|
8
8
|
SONAR_OPTIONS=`eval echo "$SD_SONAR_OPTS"`
|
|
9
9
|
if echo $SCM_URL | grep git@ > /dev/null 2>&1; then export SONAR_SCM_URL=`echo $SCM_URL | sed -e 's/:/\//g' | sed -e 's/git@/https:\/\//g'`; else export SONAR_SCM_URL=${SCM_URL}; fi
|
|
10
|
-
SCANNER_OPTION='"-Dsonar.host.url=$SD_SONAR_HOST" "-Dsonar.login=$SONAR_TOKEN" "-Dsonar.projectKey=$SD_SONAR_PROJECT_KEY" "-Dsonar.projectName=$SD_SONAR_PROJECT_NAME" "-Dsonar.projectVersion=$PROJECT_VERSION" "-Dsonar.links.scm=$SONAR_SCM_URL" "-Dsonar.links.ci=$SD_UI_URL/pipelines/$SD_PIPELINE_ID" "-Dsonar.scm.revision=$SD_BUILD_SHA" $SONAR_OPTIONS'
|
|
10
|
+
if [ ! -z "$SD_SELF_SONAR_HOST" ]; then SCANNER_OPTION='"-Dsonar.host.url=$SD_SELF_SONAR_HOST" "-Dsonar.login=$SONAR_TOKEN" "-Dsonar.projectKey=$SD_SONAR_PROJECT_KEY" "-Dsonar.projectName=$SD_SONAR_PROJECT_NAME" "-Dsonar.projectVersion=$PROJECT_VERSION" "-Dsonar.links.scm=$SONAR_SCM_URL" "-Dsonar.links.ci=$SD_UI_URL/pipelines/$SD_PIPELINE_ID" "-Dsonar.scm.revision=$SD_BUILD_SHA" $SONAR_OPTIONS'; else SCANNER_OPTION='"-Dsonar.host.url=$SD_SONAR_HOST" "-Dsonar.login=$SONAR_TOKEN" "-Dsonar.projectKey=$SD_SONAR_PROJECT_KEY" "-Dsonar.projectName=$SD_SONAR_PROJECT_NAME" "-Dsonar.projectVersion=$PROJECT_VERSION" "-Dsonar.links.scm=$SONAR_SCM_URL" "-Dsonar.links.ci=$SD_UI_URL/pipelines/$SD_PIPELINE_ID" "-Dsonar.scm.revision=$SD_BUILD_SHA" $SONAR_OPTIONS'; fi
|
|
11
11
|
SCANNER_PATH="/opt/sd/sonar-scanner/bin/sonar-scanner"
|
|
12
12
|
if [ -s ${SCANNER_PATH} ] && [ -x ${SCANNER_PATH} ]; then eval $SCANNER_PATH $SCANNER_OPTION || true; exit 0; fi
|
|
13
13
|
if [ `uname` = 'Darwin' ]; then TARGET_OS='macosx'; else TARGET_OS='linux'; fi
|
package/index.js
CHANGED
|
@@ -14,275 +14,6 @@ const CoverageBase = require('screwdriver-coverage-base');
|
|
|
14
14
|
const COMMANDS = fs.readFileSync(path.join(__dirname, 'commands.txt'), 'utf8').trim();
|
|
15
15
|
const DEFAULT_GIT_APP_NAME = 'Screwdriver Sonar PR Checks';
|
|
16
16
|
|
|
17
|
-
let adminToken;
|
|
18
|
-
let sonarHost;
|
|
19
|
-
let sdCoverageAuthUrl;
|
|
20
|
-
let sonarEnterprise;
|
|
21
|
-
let sonarGitAppName;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Create a project in sonar
|
|
25
|
-
* @method createProject
|
|
26
|
-
* @param {String} projectKey Unique identifier for the project
|
|
27
|
-
* @return {Promise} Project object if it gets created or empty object if already exists
|
|
28
|
-
*/
|
|
29
|
-
function createProject(projectKey) {
|
|
30
|
-
return request({
|
|
31
|
-
method: 'POST',
|
|
32
|
-
url: `${sonarHost}/api/projects/create?project=${projectKey}&name=${projectKey}`,
|
|
33
|
-
username: adminToken
|
|
34
|
-
}).catch(err => {
|
|
35
|
-
if (err.statusCode === 400 && err.message.includes('already exists')) {
|
|
36
|
-
return {};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
throw new Error(`Failed to create project ${projectKey}: ${err.message}`);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Create a user in sonar
|
|
45
|
-
* @method createUser
|
|
46
|
-
* @param {String} username Username of the user
|
|
47
|
-
* @param {String} password Password of the user
|
|
48
|
-
* @return {Promise} User object if it gets created or empty object if already exists
|
|
49
|
-
*/
|
|
50
|
-
function createUser(username, password) {
|
|
51
|
-
return request({
|
|
52
|
-
method: 'POST',
|
|
53
|
-
url: `${sonarHost}/api/users/create?login=${username}&name=${username}&password=${password}`,
|
|
54
|
-
username: adminToken
|
|
55
|
-
}).catch(err => {
|
|
56
|
-
if (err.statusCode === 400 && err.message.includes('already exists')) {
|
|
57
|
-
return {};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
throw new Error(`Failed to create user ${username}: ${err.message}`);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Configure Git App in SonarQube
|
|
66
|
-
* @param {String} projectKey Sonar project key
|
|
67
|
-
* @param {String} projectName Sonar project name
|
|
68
|
-
* @return {Promise} Nothing if Git App configured successfully
|
|
69
|
-
*/
|
|
70
|
-
function configureGitApp(projectKey, projectName) {
|
|
71
|
-
const gitApp = sonarGitAppName;
|
|
72
|
-
const gitAppEncoded = encodeURIComponent(gitApp);
|
|
73
|
-
const componentId = encodeURIComponent(projectKey);
|
|
74
|
-
|
|
75
|
-
// Check if binding exists
|
|
76
|
-
return request({
|
|
77
|
-
method: 'GET',
|
|
78
|
-
url: `${sonarHost}/api/alm_settings/get_binding?project=${componentId}`,
|
|
79
|
-
username: adminToken
|
|
80
|
-
}).catch(() => {
|
|
81
|
-
// if binding does not exist, add it
|
|
82
|
-
logger.info(`Binding does not exist for Sonar project ${projectKey}, adding`);
|
|
83
|
-
|
|
84
|
-
if (!sonarEnterprise || !projectName) {
|
|
85
|
-
return Promise.resolve();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const parameters = `almSetting=${gitAppEncoded}&project=${componentId}&repository=${projectName}&summaryCommentEnabled=true`;
|
|
89
|
-
|
|
90
|
-
return request({
|
|
91
|
-
method: 'POST',
|
|
92
|
-
url: `${sonarHost}/api/alm_settings/set_github_binding?${parameters}`,
|
|
93
|
-
username: adminToken
|
|
94
|
-
}).catch(error => {
|
|
95
|
-
// if cannot configure app, do not throw err
|
|
96
|
-
logger.error(`Failed to configure Git App ${gitApp} for Sonar project ${projectKey}: ${error.message}`);
|
|
97
|
-
|
|
98
|
-
return Promise.resolve();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Give specific user push access to the project
|
|
105
|
-
* @method grantUserPermission
|
|
106
|
-
* @param {String} username Username of the user
|
|
107
|
-
* @param {String} projectKey Unique identifier for the project
|
|
108
|
-
* @return {Promise} Nothing if permission granted
|
|
109
|
-
*/
|
|
110
|
-
function grantUserPermission(username, projectKey) {
|
|
111
|
-
// Always return 204 even with duplicate calls
|
|
112
|
-
return request({
|
|
113
|
-
method: 'POST',
|
|
114
|
-
url: `${sonarHost}/api/permissions/add_user?login=${username}&permission=scan&projectKey=${projectKey}`,
|
|
115
|
-
username: adminToken
|
|
116
|
-
}).catch(err => {
|
|
117
|
-
throw new Error(`Failed to grant user ${username} permission: ${err.message}`);
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Generate an access token for the given user
|
|
123
|
-
* @method generateToken
|
|
124
|
-
* @param {String} username Username of the user
|
|
125
|
-
* @return {Promise} Object with a token field
|
|
126
|
-
*/
|
|
127
|
-
function generateToken(username) {
|
|
128
|
-
const tokenName = uuidv4();
|
|
129
|
-
|
|
130
|
-
return request({
|
|
131
|
-
method: 'POST',
|
|
132
|
-
url: `${sonarHost}/api/user_tokens/generate?login=${username}&name=${tokenName}`,
|
|
133
|
-
username: adminToken
|
|
134
|
-
}).catch(err => {
|
|
135
|
-
throw new Error(`Failed to generate user ${username} token: ${err.message}`);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Get metrics for a project
|
|
141
|
-
* @method getMetrics
|
|
142
|
-
* @param {Object} config
|
|
143
|
-
* @param {String} config.projectKey Sonar project key (job:jobId or pipeline:pipelineId)
|
|
144
|
-
* @param {String} config.startTime Job start time
|
|
145
|
-
* @param {String} config.endTime Job end time
|
|
146
|
-
* @param {String} [config.prNum] Pull request number
|
|
147
|
-
* @return {Promise} Object with coverage percentage and tests success percentage
|
|
148
|
-
*/
|
|
149
|
-
function getMetrics({ projectKey, startTime, endTime, prNum, sonarEnterprise: enterpriseEnabled }) {
|
|
150
|
-
const componentId = encodeURIComponent(projectKey);
|
|
151
|
-
// get timezone offset (e.g. -0700) from 'Fri May 11 2018 15:25:37 GMT-0700 (PDT)'
|
|
152
|
-
const timezoneOffset = new Date().toString().match(/GMT(.*?) /)[1];
|
|
153
|
-
// Convert the time format from 2018-05-10T19:05:53.123Z to 2018-05-10T19:05:53-0700 as required by sonar
|
|
154
|
-
const parsedStartTime = startTime.replace(/\.(.*)/, timezoneOffset);
|
|
155
|
-
const parsedEndTime = endTime.replace(/\.(.*)/, timezoneOffset);
|
|
156
|
-
const from = encodeURIComponent(parsedStartTime);
|
|
157
|
-
const to = encodeURIComponent(parsedEndTime);
|
|
158
|
-
let coverageUrl = `${sonarHost}/api/measures/search_history?component=${componentId}&metrics=tests,test_errors,test_failures,coverage&from=${from}&to=${to}&ps=1`;
|
|
159
|
-
|
|
160
|
-
if (enterpriseEnabled && prNum) {
|
|
161
|
-
coverageUrl = coverageUrl.concat(`&pullRequest=${prNum}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return request({
|
|
165
|
-
method: 'GET',
|
|
166
|
-
url: coverageUrl,
|
|
167
|
-
username: adminToken
|
|
168
|
-
})
|
|
169
|
-
.then(result => {
|
|
170
|
-
const measures = {};
|
|
171
|
-
|
|
172
|
-
// measures in result is an array, covert it to an object with metric name as key
|
|
173
|
-
(hoek.reach(result, 'body.measures') || []).forEach(measure => {
|
|
174
|
-
measures[measure.metric] = measure;
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const metrics = {
|
|
178
|
-
coverage: hoek.reach(measures, 'coverage.history.0.value') || 'N/A',
|
|
179
|
-
tests: 'N/A'
|
|
180
|
-
};
|
|
181
|
-
const zero = { default: 0 };
|
|
182
|
-
const total = hoek.reach(measures, 'tests.history.0.value', { default: 'N/A' });
|
|
183
|
-
const testErrors = hoek.reach(measures, 'test_errors.history.0.value', zero);
|
|
184
|
-
const testFailures = hoek.reach(measures, 'test_failures.history.0.value', zero);
|
|
185
|
-
|
|
186
|
-
if (!Number.isNaN(Number(total))) {
|
|
187
|
-
const totalInt = parseInt(total, 10);
|
|
188
|
-
const pass = totalInt - parseInt(testErrors, 10) - parseInt(testFailures, 10);
|
|
189
|
-
|
|
190
|
-
metrics.tests = `${pass}/${totalInt}`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return metrics;
|
|
194
|
-
})
|
|
195
|
-
.catch(err => {
|
|
196
|
-
// if there is no coverage measurement target, 404 and 'Component key not found' are returned and this is not an error
|
|
197
|
-
if (err.statusCode !== 404 || !/Component key '.*' not found/.test(err.message)) {
|
|
198
|
-
// if cannot get coverage, do not throw err
|
|
199
|
-
logger.error(
|
|
200
|
-
`Failed to get coverage and tests percentage for Sonar project ${projectKey}: ${err.message}`
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
tests: 'N/A',
|
|
206
|
-
coverage: 'N/A'
|
|
207
|
-
};
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Determine Sonar project key, project name, and username based on:
|
|
213
|
-
* - SonarQube edition
|
|
214
|
-
* - job annotation
|
|
215
|
-
* @method getProjectData
|
|
216
|
-
* @param {Object} config
|
|
217
|
-
* @param {Object} [config.scope] Coverage scope (pipeline or job)
|
|
218
|
-
* @param {Boolean} config.enterpriseEnabled If enterprise is enabled
|
|
219
|
-
* @param {String} config.jobId Screwdriver job ID
|
|
220
|
-
* @param {String} config.jobName Screwdriver job name
|
|
221
|
-
* @param {String} config.pipelineId Screwdriver pipeline ID
|
|
222
|
-
* @param {String} config.pipelineName Screwdriver pipeline name
|
|
223
|
-
* @param {String} [config.projectKey] Sonar project key
|
|
224
|
-
* @param {String} [config.prNum] PR number
|
|
225
|
-
* @param {String} [config.prParentJobId] Screwdriver PR parent job ID
|
|
226
|
-
* @return {Object} Sonar project key, project name, and username
|
|
227
|
-
*/
|
|
228
|
-
function getProjectData({
|
|
229
|
-
scope,
|
|
230
|
-
enterpriseEnabled,
|
|
231
|
-
jobId: buildJobId,
|
|
232
|
-
jobName: buildJobName,
|
|
233
|
-
pipelineId,
|
|
234
|
-
pipelineName,
|
|
235
|
-
projectKey,
|
|
236
|
-
prParentJobId,
|
|
237
|
-
prNum
|
|
238
|
-
}) {
|
|
239
|
-
let jobId = buildJobId;
|
|
240
|
-
let jobName = buildJobName;
|
|
241
|
-
|
|
242
|
-
// Determine scope based on projectKey
|
|
243
|
-
if (projectKey) {
|
|
244
|
-
const [projectScope, id] = projectKey.split(':');
|
|
245
|
-
const projectName = projectScope === 'pipeline' ? pipelineName : `${pipelineName}:${jobName}`;
|
|
246
|
-
const username = `user-${projectScope}-${id}`;
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
projectKey,
|
|
250
|
-
username,
|
|
251
|
-
projectName,
|
|
252
|
-
projectScope
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Use user-configured scope; otherwise figure out default scope: pipeline scope for enterprise edition, job scope for everything else
|
|
257
|
-
const userScope = scope && scope !== 'undefined' ? scope : undefined;
|
|
258
|
-
const coverageScope = userScope || (enterpriseEnabled ? 'pipeline' : 'job');
|
|
259
|
-
|
|
260
|
-
if (coverageScope === 'pipeline') {
|
|
261
|
-
return {
|
|
262
|
-
projectKey: `pipeline:${pipelineId}`,
|
|
263
|
-
projectName: pipelineName,
|
|
264
|
-
username: `user-pipeline-${pipelineId}`,
|
|
265
|
-
projectScope: coverageScope
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Use prParentJobId as ID for PRs
|
|
270
|
-
if (prNum && coverageScope === 'job' && enterpriseEnabled) {
|
|
271
|
-
const prRegex = /^PR-(\d+)(?::([\w-]+))?$/;
|
|
272
|
-
const prNameMatch = jobName.match(prRegex);
|
|
273
|
-
|
|
274
|
-
jobId = prParentJobId;
|
|
275
|
-
jobName = prNameMatch && prNameMatch.length > 1 ? prNameMatch[2] : jobName;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
projectKey: `job:${jobId}`,
|
|
280
|
-
projectName: `${pipelineName}:${jobName}`,
|
|
281
|
-
username: `user-job-${jobId}`,
|
|
282
|
-
projectScope: coverageScope
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
17
|
class CoverageSonar extends CoverageBase {
|
|
287
18
|
/**
|
|
288
19
|
* Constructor
|
|
@@ -323,20 +54,283 @@ class CoverageSonar extends CoverageBase {
|
|
|
323
54
|
'Invalid config for sonar coverage plugin'
|
|
324
55
|
);
|
|
325
56
|
|
|
326
|
-
sdCoverageAuthUrl = `${
|
|
327
|
-
adminToken =
|
|
328
|
-
sonarHost =
|
|
329
|
-
sonarEnterprise =
|
|
330
|
-
sonarGitAppName =
|
|
57
|
+
this.sdCoverageAuthUrl = `${config.sdApiUrl}/v4/coverage/token`;
|
|
58
|
+
this.adminToken = config.adminToken;
|
|
59
|
+
this.sonarHost = config.sonarHost;
|
|
60
|
+
this.sonarEnterprise = config.sonarEnterprise;
|
|
61
|
+
this.sonarGitAppName = config.sonarGitAppName;
|
|
331
62
|
|
|
332
|
-
this.uploadCommands = COMMANDS.replace('$SD_SONAR_HOST', sonarHost)
|
|
333
|
-
.replace('$SD_UI_URL',
|
|
334
|
-
.replace('$SD_SONAR_ENTERPRISE', sonarEnterprise)
|
|
63
|
+
this.uploadCommands = COMMANDS.replace('$SD_SONAR_HOST', this.sonarHost)
|
|
64
|
+
.replace('$SD_UI_URL', config.sdUiUrl)
|
|
65
|
+
.replace('$SD_SONAR_ENTERPRISE', this.sonarEnterprise)
|
|
335
66
|
.split('\n');
|
|
336
67
|
|
|
337
68
|
this.uploadCommands[this.uploadCommands.length - 1] += ' || true';
|
|
338
69
|
}
|
|
339
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Create a project in sonar
|
|
73
|
+
* @method createProject
|
|
74
|
+
* @param {String} projectKey Unique identifier for the project
|
|
75
|
+
* @return {Promise} Project object if it gets created or empty object if already exists
|
|
76
|
+
*/
|
|
77
|
+
createProject(projectKey) {
|
|
78
|
+
return request({
|
|
79
|
+
method: 'POST',
|
|
80
|
+
url: `${this.sonarHost}/api/projects/create?project=${projectKey}&name=${projectKey}`,
|
|
81
|
+
username: this.adminToken
|
|
82
|
+
}).catch(err => {
|
|
83
|
+
if (err.statusCode === 400 && err.message.includes('already exists')) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error(`Failed to create project ${projectKey}: ${err.message}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a user in sonar
|
|
93
|
+
* @method createUser
|
|
94
|
+
* @param {String} username Username of the user
|
|
95
|
+
* @param {String} password Password of the user
|
|
96
|
+
* @return {Promise} User object if it gets created or empty object if already exists
|
|
97
|
+
*/
|
|
98
|
+
createUser(username, password) {
|
|
99
|
+
return request({
|
|
100
|
+
method: 'POST',
|
|
101
|
+
url: `${this.sonarHost}/api/users/create?login=${username}&name=${username}&password=${password}`,
|
|
102
|
+
username: this.adminToken
|
|
103
|
+
}).catch(err => {
|
|
104
|
+
if (err.statusCode === 400 && err.message.includes('already exists')) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error(`Failed to create user ${username}: ${err.message}`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Configure Git App in SonarQube
|
|
114
|
+
* @param {String} projectKey Sonar project key
|
|
115
|
+
* @param {String} projectName Sonar project name
|
|
116
|
+
* @return {Promise} Nothing if Git App configured successfully
|
|
117
|
+
*/
|
|
118
|
+
configureGitApp(projectKey, projectName) {
|
|
119
|
+
const gitApp = this.sonarGitAppName;
|
|
120
|
+
const gitAppEncoded = encodeURIComponent(gitApp);
|
|
121
|
+
const componentId = encodeURIComponent(projectKey);
|
|
122
|
+
|
|
123
|
+
// Check if binding exists
|
|
124
|
+
return request({
|
|
125
|
+
method: 'GET',
|
|
126
|
+
url: `${this.sonarHost}/api/alm_settings/get_binding?project=${componentId}`,
|
|
127
|
+
username: this.adminToken
|
|
128
|
+
}).catch(() => {
|
|
129
|
+
// if binding does not exist, add it
|
|
130
|
+
logger.info(`Binding does not exist for Sonar project ${projectKey}, adding`);
|
|
131
|
+
|
|
132
|
+
if (!this.sonarEnterprise || !projectName) {
|
|
133
|
+
return Promise.resolve();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const parameters = `almSetting=${gitAppEncoded}&project=${componentId}&repository=${projectName}&summaryCommentEnabled=true&monorepo=false`;
|
|
137
|
+
|
|
138
|
+
return request({
|
|
139
|
+
method: 'POST',
|
|
140
|
+
url: `${this.sonarHost}/api/alm_settings/set_github_binding?${parameters}`,
|
|
141
|
+
username: this.adminToken
|
|
142
|
+
}).catch(error => {
|
|
143
|
+
// if cannot configure app, do not throw err
|
|
144
|
+
logger.error(`Failed to configure Git App ${gitApp} for Sonar project ${projectKey}: ${error.message}`);
|
|
145
|
+
|
|
146
|
+
return Promise.resolve();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Give specific user push access to the project
|
|
153
|
+
* @method grantUserPermission
|
|
154
|
+
* @param {String} username Username of the user
|
|
155
|
+
* @param {String} projectKey Unique identifier for the project
|
|
156
|
+
* @return {Promise} Nothing if permission granted
|
|
157
|
+
*/
|
|
158
|
+
grantUserPermission(username, projectKey) {
|
|
159
|
+
// Always return 204 even with duplicate calls
|
|
160
|
+
return request({
|
|
161
|
+
method: 'POST',
|
|
162
|
+
url: `${this.sonarHost}/api/permissions/add_user?login=${username}&permission=scan&projectKey=${projectKey}`,
|
|
163
|
+
username: this.adminToken
|
|
164
|
+
}).catch(err => {
|
|
165
|
+
throw new Error(`Failed to grant user ${username} permission: ${err.message}`);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate an access token for the given user
|
|
171
|
+
* @method generateToken
|
|
172
|
+
* @param {String} username Username of the user
|
|
173
|
+
* @return {Promise} Object with a token field
|
|
174
|
+
*/
|
|
175
|
+
generateToken(username) {
|
|
176
|
+
const tokenName = uuidv4();
|
|
177
|
+
|
|
178
|
+
return request({
|
|
179
|
+
method: 'POST',
|
|
180
|
+
url: `${this.sonarHost}/api/user_tokens/generate?login=${username}&name=${tokenName}`,
|
|
181
|
+
username: this.adminToken
|
|
182
|
+
}).catch(err => {
|
|
183
|
+
throw new Error(`Failed to generate user ${username} token: ${err.message}`);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get metrics for a project
|
|
189
|
+
* @method getMetrics
|
|
190
|
+
* @param {Object} config
|
|
191
|
+
* @param {String} config.projectKey Sonar project key (job:jobId or pipeline:pipelineId)
|
|
192
|
+
* @param {String} config.startTime Job start time
|
|
193
|
+
* @param {String} config.endTime Job end time
|
|
194
|
+
* @param {String} [config.prNum] Pull request number
|
|
195
|
+
* @return {Promise} Object with coverage percentage and tests success percentage
|
|
196
|
+
*/
|
|
197
|
+
getMetrics({ projectKey, startTime, endTime, prNum, sonarEnterprise: enterpriseEnabled }) {
|
|
198
|
+
const componentId = encodeURIComponent(projectKey);
|
|
199
|
+
// get timezone offset (e.g. -0700) from 'Fri May 11 2018 15:25:37 GMT-0700 (PDT)'
|
|
200
|
+
const timezoneOffset = new Date().toString().match(/GMT(.*?) /)[1];
|
|
201
|
+
// Convert the time format from 2018-05-10T19:05:53.123Z to 2018-05-10T19:05:53-0700 as required by sonar
|
|
202
|
+
const parsedStartTime = startTime.replace(/\.(.*)/, timezoneOffset);
|
|
203
|
+
const parsedEndTime = endTime.replace(/\.(.*)/, timezoneOffset);
|
|
204
|
+
const from = encodeURIComponent(parsedStartTime);
|
|
205
|
+
const to = encodeURIComponent(parsedEndTime);
|
|
206
|
+
let coverageUrl = `${this.sonarHost}/api/measures/search_history?component=${componentId}&metrics=tests,test_errors,test_failures,coverage&from=${from}&to=${to}&ps=1`;
|
|
207
|
+
|
|
208
|
+
if (enterpriseEnabled && prNum) {
|
|
209
|
+
coverageUrl = coverageUrl.concat(`&pullRequest=${prNum}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return request({
|
|
213
|
+
method: 'GET',
|
|
214
|
+
url: coverageUrl,
|
|
215
|
+
username: this.adminToken
|
|
216
|
+
})
|
|
217
|
+
.then(result => {
|
|
218
|
+
const measures = {};
|
|
219
|
+
|
|
220
|
+
// measures in result is an array, covert it to an object with metric name as key
|
|
221
|
+
(hoek.reach(result, 'body.measures') || []).forEach(measure => {
|
|
222
|
+
measures[measure.metric] = measure;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const metrics = {
|
|
226
|
+
coverage: hoek.reach(measures, 'coverage.history.0.value') || 'N/A',
|
|
227
|
+
tests: 'N/A'
|
|
228
|
+
};
|
|
229
|
+
const zero = { default: 0 };
|
|
230
|
+
const total = hoek.reach(measures, 'tests.history.0.value', { default: 'N/A' });
|
|
231
|
+
const testErrors = hoek.reach(measures, 'test_errors.history.0.value', zero);
|
|
232
|
+
const testFailures = hoek.reach(measures, 'test_failures.history.0.value', zero);
|
|
233
|
+
|
|
234
|
+
if (!Number.isNaN(Number(total))) {
|
|
235
|
+
const totalInt = parseInt(total, 10);
|
|
236
|
+
const pass = totalInt - parseInt(testErrors, 10) - parseInt(testFailures, 10);
|
|
237
|
+
|
|
238
|
+
metrics.tests = `${pass}/${totalInt}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return metrics;
|
|
242
|
+
})
|
|
243
|
+
.catch(err => {
|
|
244
|
+
// if there is no coverage measurement target, 404 and 'Component key not found' are returned and this is not an error
|
|
245
|
+
if (err.statusCode !== 404 || !/Component key '.*' not found/.test(err.message)) {
|
|
246
|
+
// if cannot get coverage, do not throw err
|
|
247
|
+
logger.error(
|
|
248
|
+
`Failed to get coverage and tests percentage for Sonar project ${projectKey}: ${err.message}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
tests: 'N/A',
|
|
254
|
+
coverage: 'N/A'
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Determine Sonar project key, project name, and username based on:
|
|
261
|
+
* - SonarQube edition
|
|
262
|
+
* - job annotation
|
|
263
|
+
* @method getProjectData
|
|
264
|
+
* @param {Object} config
|
|
265
|
+
* @param {Object} [config.scope] Coverage scope (pipeline or job)
|
|
266
|
+
* @param {Boolean} config.enterpriseEnabled If enterprise is enabled
|
|
267
|
+
* @param {String} config.jobId Screwdriver job ID
|
|
268
|
+
* @param {String} config.jobName Screwdriver job name
|
|
269
|
+
* @param {String} config.pipelineId Screwdriver pipeline ID
|
|
270
|
+
* @param {String} config.pipelineName Screwdriver pipeline name
|
|
271
|
+
* @param {String} [config.projectKey] Sonar project key
|
|
272
|
+
* @param {String} [config.prNum] PR number
|
|
273
|
+
* @param {String} [config.prParentJobId] Screwdriver PR parent job ID
|
|
274
|
+
* @return {Object} Sonar project key, project name, and username
|
|
275
|
+
*/
|
|
276
|
+
getProjectData({
|
|
277
|
+
scope,
|
|
278
|
+
enterpriseEnabled,
|
|
279
|
+
jobId: buildJobId,
|
|
280
|
+
jobName: buildJobName,
|
|
281
|
+
pipelineId,
|
|
282
|
+
pipelineName,
|
|
283
|
+
projectKey,
|
|
284
|
+
prParentJobId,
|
|
285
|
+
prNum
|
|
286
|
+
}) {
|
|
287
|
+
let jobId = buildJobId;
|
|
288
|
+
let jobName = buildJobName;
|
|
289
|
+
|
|
290
|
+
// Determine scope based on projectKey
|
|
291
|
+
if (projectKey) {
|
|
292
|
+
const [projectScope, id] = projectKey.split(':');
|
|
293
|
+
const projectName = projectScope === 'pipeline' ? pipelineName : `${pipelineName}:${jobName}`;
|
|
294
|
+
const username = `user-${projectScope}-${id}`;
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
projectKey,
|
|
298
|
+
username,
|
|
299
|
+
projectName,
|
|
300
|
+
projectScope
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Use user-configured scope; otherwise figure out default scope: pipeline scope for enterprise edition, job scope for everything else
|
|
305
|
+
const userScope = scope && scope !== 'undefined' ? scope : undefined;
|
|
306
|
+
const coverageScope = userScope || (enterpriseEnabled ? 'pipeline' : 'job');
|
|
307
|
+
|
|
308
|
+
if (coverageScope === 'pipeline') {
|
|
309
|
+
return {
|
|
310
|
+
projectKey: `pipeline:${pipelineId}`,
|
|
311
|
+
projectName: pipelineName,
|
|
312
|
+
username: `user-pipeline-${pipelineId}`,
|
|
313
|
+
projectScope: coverageScope
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Use prParentJobId as ID for PRs
|
|
318
|
+
if (prNum && coverageScope === 'job' && enterpriseEnabled) {
|
|
319
|
+
const prRegex = /^PR-(\d+)(?::([\w-]+))?$/;
|
|
320
|
+
const prNameMatch = jobName.match(prRegex);
|
|
321
|
+
|
|
322
|
+
jobId = prParentJobId;
|
|
323
|
+
jobName = prNameMatch && prNameMatch.length > 1 ? prNameMatch[2] : jobName;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
projectKey: `job:${jobId}`,
|
|
328
|
+
projectName: `${pipelineName}:${jobName}`,
|
|
329
|
+
username: `user-job-${jobId}`,
|
|
330
|
+
projectScope: coverageScope
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
340
334
|
/**
|
|
341
335
|
* Return an access token that build can use to talk to coverage server
|
|
342
336
|
* @method getAccessToken
|
|
@@ -356,8 +350,8 @@ class CoverageSonar extends CoverageBase {
|
|
|
356
350
|
let projectData = { username, projectKey, projectName };
|
|
357
351
|
|
|
358
352
|
if (!username || !projectKey || !projectName || projectName.includes('undefined')) {
|
|
359
|
-
projectData = getProjectData({
|
|
360
|
-
enterpriseEnabled: sonarEnterprise,
|
|
353
|
+
projectData = this.getProjectData({
|
|
354
|
+
enterpriseEnabled: this.sonarEnterprise,
|
|
361
355
|
jobId,
|
|
362
356
|
jobName,
|
|
363
357
|
prParentJobId,
|
|
@@ -370,11 +364,11 @@ class CoverageSonar extends CoverageBase {
|
|
|
370
364
|
|
|
371
365
|
const password = uuidv4();
|
|
372
366
|
|
|
373
|
-
return createProject(projectData.projectKey)
|
|
374
|
-
.then(() => configureGitApp(projectData.projectKey, projectData.projectName))
|
|
375
|
-
.then(() => createUser(projectData.username, password))
|
|
376
|
-
.then(() => grantUserPermission(projectData.username, projectData.projectKey))
|
|
377
|
-
.then(() => generateToken(projectData.username))
|
|
367
|
+
return this.createProject(projectData.projectKey)
|
|
368
|
+
.then(() => this.configureGitApp(projectData.projectKey, projectData.projectName))
|
|
369
|
+
.then(() => this.createUser(projectData.username, password))
|
|
370
|
+
.then(() => this.grantUserPermission(projectData.username, projectData.projectKey))
|
|
371
|
+
.then(() => this.generateToken(projectData.username))
|
|
378
372
|
.then(res => res.body.token);
|
|
379
373
|
}
|
|
380
374
|
|
|
@@ -410,8 +404,8 @@ class CoverageSonar extends CoverageBase {
|
|
|
410
404
|
projectKey: coverageProjectKey,
|
|
411
405
|
prParentJobId
|
|
412
406
|
}) {
|
|
413
|
-
const { projectScope, projectKey, projectName, username } = getProjectData({
|
|
414
|
-
enterpriseEnabled: sonarEnterprise,
|
|
407
|
+
const { projectScope, projectKey, projectName, username } = this.getProjectData({
|
|
408
|
+
enterpriseEnabled: this.sonarEnterprise,
|
|
415
409
|
jobId,
|
|
416
410
|
pipelineId,
|
|
417
411
|
scope,
|
|
@@ -424,9 +418,9 @@ class CoverageSonar extends CoverageBase {
|
|
|
424
418
|
|
|
425
419
|
const infoObject = {
|
|
426
420
|
envVars: {
|
|
427
|
-
SD_SONAR_AUTH_URL: `${sdCoverageAuthUrl}?projectKey=${projectKey}&projectName=${projectName}&username=${username}&scope=${projectScope}`,
|
|
428
|
-
SD_SONAR_HOST: sonarHost,
|
|
429
|
-
SD_SONAR_ENTERPRISE: sonarEnterprise,
|
|
421
|
+
SD_SONAR_AUTH_URL: `${this.sdCoverageAuthUrl}?projectKey=${projectKey}&projectName=${projectName}&username=${username}&scope=${projectScope}`,
|
|
422
|
+
SD_SONAR_HOST: this.sonarHost,
|
|
423
|
+
SD_SONAR_ENTERPRISE: this.sonarEnterprise,
|
|
430
424
|
SD_SONAR_PROJECT_KEY: projectKey,
|
|
431
425
|
SD_SONAR_PROJECT_NAME: projectName
|
|
432
426
|
}
|
|
@@ -434,10 +428,12 @@ class CoverageSonar extends CoverageBase {
|
|
|
434
428
|
|
|
435
429
|
// Only get coverage percentage if the steps are finished
|
|
436
430
|
if (projectKey && startTime && endTime) {
|
|
437
|
-
|
|
431
|
+
const { sonarEnterprise } = this;
|
|
432
|
+
|
|
433
|
+
return this.getMetrics({ projectKey, startTime, endTime, prNum, sonarEnterprise }).then(
|
|
438
434
|
({ coverage, tests }) => {
|
|
439
435
|
const componentId = encodeURIComponent(projectKey);
|
|
440
|
-
let projectUrl = `${this.
|
|
436
|
+
let projectUrl = `${this.sonarHost}/dashboard?id=${componentId}`;
|
|
441
437
|
|
|
442
438
|
if (sonarEnterprise && prNum) {
|
|
443
439
|
projectUrl = projectUrl.concat(`&pullRequest=${prNum}`);
|
package/package.json
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "screwdriver-coverage-sonar",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "SonarQube implementation of the coverage-base class",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"pretest": "eslint .",
|
|
8
|
-
"test": "nyc --report-dir ./artifacts/coverage --reporter=lcov mocha --reporter mocha-multi-reporters --reporter-options configFile=./mocha.config.json --recursive --timeout 4000 --retries 1 --exit --allow-uncaught true --color true"
|
|
9
|
-
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
|
|
8
|
+
"test": "nyc --report-dir ./artifacts/coverage --reporter=lcov mocha --reporter mocha-multi-reporters --reporter-options configFile=./mocha.config.json --recursive --timeout 4000 --retries 1 --exit --allow-uncaught true --color true"
|
|
10
9
|
},
|
|
11
10
|
"repository": {
|
|
12
11
|
"type": "git",
|
|
13
|
-
"url": "git
|
|
12
|
+
"url": "git+https://github.com/screwdriver-cd/coverage-sonar.git"
|
|
14
13
|
},
|
|
15
14
|
"homepage": "https://github.com/screwdriver-cd/coverage-sonar",
|
|
16
15
|
"bugs": "https://github.com/screwdriver-cd/screwdriver/issues",
|
|
@@ -33,7 +32,7 @@
|
|
|
33
32
|
"chai": "^4.2.0",
|
|
34
33
|
"eslint": "^7.31.0",
|
|
35
34
|
"eslint-config-screwdriver": "^5.0.1",
|
|
36
|
-
"mocha": "^
|
|
35
|
+
"mocha": "^9.1.3",
|
|
37
36
|
"mocha-multi-reporters": "^1.5.1",
|
|
38
37
|
"mocha-sonarqube-reporter": "^1.0.2",
|
|
39
38
|
"mockery": "^2.1.0",
|
|
@@ -49,9 +48,6 @@
|
|
|
49
48
|
"uuid": "^3.4.0"
|
|
50
49
|
},
|
|
51
50
|
"release": {
|
|
52
|
-
"debug": false
|
|
53
|
-
"verifyConditions": {
|
|
54
|
-
"path": "./node_modules/semantic-release/src/lib/plugin-noop.js"
|
|
55
|
-
}
|
|
51
|
+
"debug": false
|
|
56
52
|
}
|
|
57
|
-
}
|
|
53
|
+
}
|