screwdriver-coverage-sonar 3.3.4 → 3.4.1

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.
Files changed (3) hide show
  1. package/commands.txt +2 -2
  2. package/index.js +289 -293
  3. package/package.json +7 -7
package/commands.txt CHANGED
@@ -2,12 +2,12 @@ if [ ! -f sonar-project.properties ] && [ -z "$SD_SONAR_OPTS" ]; then echo "ERRO
2
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&monorepo=false`;
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,284 @@ class CoverageSonar extends CoverageBase {
323
54
  'Invalid config for sonar coverage plugin'
324
55
  );
325
56
 
326
- sdCoverageAuthUrl = `${this.config.sdApiUrl}/v4/coverage/token`;
327
- adminToken = this.config.adminToken;
328
- sonarHost = this.config.sonarHost;
329
- sonarEnterprise = this.config.sonarEnterprise;
330
- sonarGitAppName = this.config.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', this.config.sdUiUrl)
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
+ }) {
286
+ let jobId = buildJobId;
287
+ let jobName = buildJobName;
288
+
289
+ // Determine scope based on projectKey
290
+ if (projectKey) {
291
+ const [projectScope, id] = projectKey.split(':');
292
+ const projectName = projectScope === 'pipeline' ? pipelineName : `${pipelineName}:${jobName}`;
293
+ const username = `user-${projectScope}-${id}`;
294
+
295
+ return {
296
+ projectKey,
297
+ username,
298
+ projectName,
299
+ projectScope
300
+ };
301
+ }
302
+
303
+ // Use user-configured scope; otherwise figure out default scope: pipeline scope for enterprise edition, job scope for everything else
304
+ const userScope = scope && scope !== 'undefined' ? scope : undefined;
305
+ const coverageScope = userScope || (enterpriseEnabled ? 'pipeline' : 'job');
306
+
307
+ if (coverageScope === 'pipeline') {
308
+ return {
309
+ projectKey: `pipeline:${pipelineId}`,
310
+ projectName: pipelineName,
311
+ username: `user-pipeline-${pipelineId}`,
312
+ projectScope: coverageScope
313
+ };
314
+ }
315
+
316
+ // Use prParentJobId as ID for PRs
317
+ if (coverageScope === 'job' && enterpriseEnabled) {
318
+ const prRegex = /^PR-(\d+)(?::([\w-]+))?$/;
319
+ const prNameMatch = jobName.match(prRegex);
320
+
321
+ if (prNameMatch && prNameMatch.length > 1) {
322
+ jobId = prParentJobId;
323
+ jobName = prNameMatch[2];
324
+ }
325
+ }
326
+
327
+ return {
328
+ projectKey: `job:${jobId}`,
329
+ projectName: `${pipelineName}:${jobName}`,
330
+ username: `user-job-${jobId}`,
331
+ projectScope: coverageScope
332
+ };
333
+ }
334
+
340
335
  /**
341
336
  * Return an access token that build can use to talk to coverage server
342
337
  * @method getAccessToken
@@ -356,8 +351,8 @@ class CoverageSonar extends CoverageBase {
356
351
  let projectData = { username, projectKey, projectName };
357
352
 
358
353
  if (!username || !projectKey || !projectName || projectName.includes('undefined')) {
359
- projectData = getProjectData({
360
- enterpriseEnabled: sonarEnterprise,
354
+ projectData = this.getProjectData({
355
+ enterpriseEnabled: this.sonarEnterprise,
361
356
  jobId,
362
357
  jobName,
363
358
  prParentJobId,
@@ -370,11 +365,11 @@ class CoverageSonar extends CoverageBase {
370
365
 
371
366
  const password = uuidv4();
372
367
 
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))
368
+ return this.createProject(projectData.projectKey)
369
+ .then(() => this.configureGitApp(projectData.projectKey, projectData.projectName))
370
+ .then(() => this.createUser(projectData.username, password))
371
+ .then(() => this.grantUserPermission(projectData.username, projectData.projectKey))
372
+ .then(() => this.generateToken(projectData.username))
378
373
  .then(res => res.body.token);
379
374
  }
380
375
 
@@ -410,23 +405,22 @@ class CoverageSonar extends CoverageBase {
410
405
  projectKey: coverageProjectKey,
411
406
  prParentJobId
412
407
  }) {
413
- const { projectScope, projectKey, projectName, username } = getProjectData({
414
- enterpriseEnabled: sonarEnterprise,
408
+ const { projectScope, projectKey, projectName, username } = this.getProjectData({
409
+ enterpriseEnabled: this.sonarEnterprise,
415
410
  jobId,
416
411
  pipelineId,
417
412
  scope,
418
413
  pipelineName,
419
414
  jobName,
420
415
  projectKey: coverageProjectKey,
421
- prParentJobId,
422
- prNum
416
+ prParentJobId
423
417
  });
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
- return getMetrics({ projectKey, startTime, endTime, prNum, sonarEnterprise }).then(
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.config.sonarHost}/dashboard?id=${componentId}`;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "screwdriver-coverage-sonar",
3
- "version": "3.3.4",
3
+ "version": "3.4.1",
4
4
  "description": "SonarQube implementation of the coverage-base class",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -29,10 +29,10 @@
29
29
  "Tiffany Kyi <tiffanykyi@gmail.com>"
30
30
  ],
31
31
  "devDependencies": {
32
- "chai": "^4.2.0",
33
- "eslint": "^7.31.0",
32
+ "chai": "^4.3.6",
33
+ "eslint": "^7.32.0",
34
34
  "eslint-config-screwdriver": "^5.0.1",
35
- "mocha": "^9.1.3",
35
+ "mocha": "^9.2.2",
36
36
  "mocha-multi-reporters": "^1.5.1",
37
37
  "mocha-sonarqube-reporter": "^1.0.2",
38
38
  "mockery": "^2.1.0",
@@ -40,11 +40,11 @@
40
40
  "sinon": "^9.0.0"
41
41
  },
42
42
  "dependencies": {
43
- "@hapi/hoek": "^9.2.0",
44
- "joi": "^17.4.1",
43
+ "@hapi/hoek": "^9.3.0",
44
+ "joi": "^17.6.0",
45
45
  "screwdriver-coverage-base": "^2.1.0",
46
46
  "screwdriver-logger": "^1.1.0",
47
- "screwdriver-request": "^1.0.1",
47
+ "screwdriver-request": "^1.0.3",
48
48
  "uuid": "^3.4.0"
49
49
  },
50
50
  "release": {