Skip to content

Commit

Permalink
user story #1514237:Update CI job root when necessary from Azure devo…
Browse files Browse the repository at this point in the history
…ps extension,

user story #1515241:Add plugin version for Azure devops CI server and add experiment check service for Azure extension
  • Loading branch information
nissimshitrit committed Mar 1, 2022
1 parent 51c3d77 commit 8480f3b
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 26 deletions.
153 changes: 140 additions & 13 deletions src/BaseTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {AuthenticationService} from "./services/security/AuthenticationService";
import {NodeUtils} from "./NodeUtils";
import {SharedSpaceUtils} from "./SharedSpaceUtils";
import {UrlUtils} from "./UrlUtils";
import {WebApi} from "azure-devops-node-api";
import {ConnectionUtils} from "./ConnectionUtils";

const Query = require('@microfocus/alm-octane-js-rest-sdk/lib/query');

Expand Down Expand Up @@ -44,12 +46,16 @@ export class BaseTask {
protected sourceBranchName: string;
protected customWebContext: string;
protected authenticationService: AuthenticationService;
protected definitionId: number;
protected sourceBranch: string;

private octaneServiceConnectionData: any;
private url: URL;
private sharedSpaceId: string;
private workspaces: any;
private analyticsCiInternalApiUrlPart: string;
private ciInternalAzureApiUrlPart: string;
private isRunPipelineFromOctaneEnable: boolean;

protected constructor(tl: any) {
this.tl = tl;
Expand Down Expand Up @@ -126,6 +132,7 @@ export class BaseTask {

private buildAnalyticsCiInternalApiUrlPart() {
this.analyticsCiInternalApiUrlPart = '/internal-api/shared_spaces/' + this.sharedSpaceId + '/analytics/ci';
this.ciInternalAzureApiUrlPart = '/internal-api/shared_spaces/' + this.sharedSpaceId + '/workspaces/{workspace_id}/analytics/ci';
}

private setAgentJobName(agentJobName: string) {
Expand Down Expand Up @@ -168,12 +175,16 @@ export class BaseTask {
this.buildId = this.tl.getVariable('Build.BuildId');
this.sourceBranchName = this.tl.getVariable('Build.SourceBranchName');
this.jobStatus = this.convertJobStatus(this.tl.getVariable('AGENT_JOBSTATUS'));
this.definitionId = this.tl.getVariable("System.DefinitionId");
this.sourceBranch = this.tl.getVariable("Build.SourceBranch");

this.logger.info('collectionUri = ' + this.collectionUri);
this.logger.info('projectId = ' + this.projectId);
this.logger.info('projectName = ' + this.projectName);
this.logger.info('buildDefinitionName = ' + this.buildDefinitionName);
this.logger.info('sourceBranchName = ' + this.sourceBranchName);
this.logger.info('definitionId = ' + this.definitionId);
this.logger.info('sourceBranch = ' + this.sourceBranch);
}

protected convertJobStatus(nativeStatus: string): Result {
Expand Down Expand Up @@ -221,6 +232,16 @@ export class BaseTask {
this.workspaces = this.workspaces.split(',').map(s => s.trim());
}

private async initializeExperiments(octaneSDKConnection,ws):Promise<void>{
const currentVersion = await this.getOctaneVersion(octaneSDKConnection);
this.logger.info("Octane current version: " + currentVersion);
this.isRunPipelineFromOctaneEnable = this.isVersionGreaterOrEquals(currentVersion,'16.0.316') && await this.isExperimentEnable(octaneSDKConnection,ws);
if(this.isRunPipelineFromOctaneEnable){
await this.updatePluginVersion(octaneSDKConnection);
this.logger.info("Send plugin details to Octane.");
}
}

private async createOctaneConnectionsAndRetrieveCiServersAndPipelines() {
let clientId: string = this.authenticationService.getOctaneClientId();
let clientSecret: string = this.authenticationService.getOctaneClientSecret();
Expand All @@ -235,11 +256,11 @@ export class BaseTask {

await octaneSDKConnection._requestHandler.authenticate();
}

await this.initializeExperiments(octaneSDKConnection,ws);
let ciServer = await this.getCiServer(octaneSDKConnection, this.agentJobName === BaseTask.ALM_OCTANE_PIPELINE_START);

await this.getPipeline(octaneSDKConnection, this.buildDefinitionName, this.pipelineFullName, ciServer,
this.agentJobName === BaseTask.ALM_OCTANE_PIPELINE_START);
this.agentJobName === BaseTask.ALM_OCTANE_PIPELINE_START,ws);

this.octaneSDKConnections[ws] = octaneSDKConnection;
})(ws);
Expand Down Expand Up @@ -273,7 +294,7 @@ export class BaseTask {
} else {
throw new Error('CI Server \'' + this.projectFullName + '(instanceId=\'' + this.instanceId + '\')\' not found.');
}
} else {
} else if(!this.isRunPipelineFromOctaneEnable){
let ciServer = {
'name': this.projectFullName,
'url': serverUrl,
Expand Down Expand Up @@ -311,21 +332,127 @@ export class BaseTask {
return ciServers;
}

protected async getPipeline(octaneSDKConnection, pipelineName, rootJobName, ciServer, createOnAbsence) {
let pipelineQuery = Query.field('name').equal(BaseTask.escapeOctaneQueryValue(pipelineName))
protected async getPipeline(octaneSDKConnection, pipelineName, rootJobName, ciServer, createOnAbsence,workspaceId) {
let pipelines;
if(this.isRunPipelineFromOctaneEnable){
const pipelineQuery = Query.field('ci_id').equal(BaseTask.escapeOctaneQueryValue(this.projectFullName + '.' + this.buildDefinitionName))
.and(Query.field(EntityTypeConstants.CI_SERVER_ENTITY_TYPE).equal(Query.field('id').equal(ciServer.id))).build();
const ciJobs = await octaneSDKConnection.get(EntityTypeRestEndpointConstants.CI_JOB_REST_API_NAME)
.fields('pipeline,definition_id')
.query(pipelineQuery).execute();

if(ciJobs && ciJobs.total_count > 0 && ciJobs.data) {
pipelines = ciJobs.data.filter(ciJob => ciJob.pipeline).map(ciJob => ciJob.pipeline);
}
if (!pipelines || pipelines.length == 0) {
if(createOnAbsence) {
rootJobName = rootJobName + '@@@' + this.definitionId + '@@@' + this.sourceBranch;
let result = await this.createPipeline(octaneSDKConnection, pipelineName, rootJobName, ciServer);
return result[0].data[0];
} else {
throw new Error('Pipeline \'' + pipelineName + '\' not found.')
}
} else {
this.logger.debug('Checking if have CI jobs to update Octane');
const ciJobsToUpdate = ciJobs.data.filter(ciJob => ciJob.pipeline && ciJob?.definition_id !== this.definitionId);
if(ciJobsToUpdate?.length > 0) {
this.logger.info('Updating ' + ciJobsToUpdate.length +' CI jobs of Octane');
await this.updateExistCIJobs(ciJobsToUpdate,ciServer.id,workspaceId,octaneSDKConnection);
}
}
return pipelines[0];
} else {
const pipelineQuery = Query.field('name').equal(BaseTask.escapeOctaneQueryValue(pipelineName))
.and(Query.field(EntityTypeConstants.CI_SERVER_ENTITY_TYPE).equal(Query.field('id').equal(ciServer.id))).build();
pipelines = await octaneSDKConnection.get(EntityTypeRestEndpointConstants.PIPELINES_REST_API_NAME).query(pipelineQuery).execute();
if (!pipelines || pipelines.total_count == 0 || pipelines.data.length == 0) {
if (createOnAbsence) {
let result = await this.createPipeline(octaneSDKConnection, pipelineName, rootJobName, ciServer);
return result[0].data[0];
} else {
throw new Error('Pipeline \'' + pipelineName + '\' not found.')
}
}
return pipelines.data[0];
}

let pipelines = await octaneSDKConnection.get(EntityTypeRestEndpointConstants.PIPELINES_REST_API_NAME).query(pipelineQuery).execute();
if (!pipelines || pipelines.total_count == 0 || pipelines.data.length == 0) {
if (createOnAbsence) {
let result = await this.createPipeline(octaneSDKConnection, pipelineName, rootJobName, ciServer);
return result[0].data[0];
} else {
throw new Error('Pipeline \'' + pipelineName + '\' not found.')

}

private async getOctaneVersion(octaneSDKConnection): Promise<string>{
const urlStatus = this.analyticsCiInternalApiUrlPart +'/servers/connectivity/status'
const response = await octaneSDKConnection._requestHandler._requestor.get(urlStatus);
this.logger.debug("Octane connectivity status response: " + response);
return response.octaneVersion;
}
private async updatePluginVersion(octaneSDKConnection): Promise<void>{
const querystring = require('querystring');
const sdk = "";
const plugin = await this.getPluginVersion();
const client_id = this.authenticationService.getOctaneClientId();
const self_url = querystring.escape(this.collectionUri + this.projectName);
const instance_id = this.instanceId;

const urlConnectivity = this.analyticsCiInternalApiUrlPart +
`/servers/${instance_id}/tasks?self-type=azure_devops&api-version=1&sdk-version=${sdk}&plugin-version=${plugin}&self-url=${self_url}&client-id=${client_id}&client-server-user=`;
await octaneSDKConnection._requestHandler._requestor.get(urlConnectivity);
}


private async getPluginVersion():Promise<string>{
const api: WebApi = ConnectionUtils.getWebApiWithProxy(this.collectionUri, this.authenticationService.getAzureAccessToken());
const extApi = await api.getExtensionManagementApi(this.collectionUri)
const extension = await extApi.getInstalledExtensionByName("almoctane","alm-octane-integration-public");
this.logger.info("extenstion version: " + extension?.version);
return extension?.version;
}

private isVersionGreaterOrEquals(version1: string,version2: string): boolean{
if(!version1 || !version2){
return false;
}
const version1Spl = version1.split('.');
const version2Spl = version2.split('.');
for(let i = 0;i < version1Spl.length || i < version2Spl.length;i++){
const decrement = parseInt(version1Spl[i]) - parseInt(version2Spl[i]);
if(decrement !== 0){
return decrement > 0
}
}
return version1Spl.length >= version2Spl.length;
}

private async updateExistCIJobs(ciJobs,ciServerId,workspaceId,octaneSDKConnection): Promise<void> {
let ciJobsToUpdate = [];
ciJobs.forEach(ciJob => ciJobsToUpdate.push(this.createCiJobBody(ciJob)));
this.logger.debug('CI Jobs update body:' + ciJobs);
const url = this.ciInternalAzureApiUrlPart.replace('{workspace_id}',workspaceId) + '/ci_job_update?ci-server-id=' + ciServerId;
await octaneSDKConnection._requestHandler.update(url,ciJobsToUpdate);
}

private createCiJobBody(ciJob){
return {
'id': ciJob.id,
'definitionId': this.definitionId,
'jobCiId': this.projectFullName + '.' + this.buildDefinitionName,
'parameters': [
{
'name': 'branch',
'type': 'string',
'description': 'Branch to execute pipeline',
'defaultValue': this.sourceBranch,
'choices': [],
}
]

}
}

return pipelines.data[0];
private async isExperimentEnable(octaneSDKConnection,workspaceId): Promise<boolean>{
const experimentUrl = this.ciInternalAzureApiUrlPart.replace('{workspace_id}',workspaceId) + '/experiment_run_pipeline';
const response = await octaneSDKConnection._requestHandler._requestor.get(experimentUrl);
this.logger.info("Octane experiment 'run_azure_pipeline' enabled: " + response);
return response;
}

private async createPipeline(octaneSDKConnection, pipelineName, rootJobName, ciServer) {
Expand Down
9 changes: 6 additions & 3 deletions src/ExtensionConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export enum SystemVariablesConstants {
BUILD_BUILD_ID = 'Build.BuildId',
AGENT_JOB_NAME = 'Agent.JobName',
AGENT_JOB_STATUS = 'AGENT_JOBSTATUS',
BUILD_DEFINITION_ID = 'System.DefinitionId'
BUILD_DEFINITION_ID = 'System.DefinitionId',
BUILD_SOURCE_BRANCH = "Build.SourceBranch"
}

export enum EndpointDataConstants {
Expand All @@ -32,12 +33,14 @@ export enum EndpointDataConstants {

export enum EntityTypeConstants {
CI_SERVER_ENTITY_TYPE = 'ci_server',
PIPELINE_ENTITY_TYPE = 'pipeline'
PIPELINE_ENTITY_TYPE = 'pipeline',
CI_JOB_ENTITY_TYPE = 'ci_job'
}

export enum EntityTypeRestEndpointConstants {
CI_SERVERS_REST_API_NAME = EntityTypeConstants.CI_SERVER_ENTITY_TYPE + 's',
PIPELINES_REST_API_NAME = EntityTypeConstants.PIPELINE_ENTITY_TYPE + 's'
PIPELINES_REST_API_NAME = EntityTypeConstants.PIPELINE_ENTITY_TYPE + 's',
CI_JOB_REST_API_NAME = EntityTypeConstants.CI_JOB_ENTITY_TYPE + 's'
}

export enum OctaneTaskConstants {
Expand Down
2 changes: 2 additions & 0 deletions src/debug/debug-conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface Build {
sourceBranchName: string;
definitionName: string;
buildId: string;
sourceBranch: string;
}

interface Octane {
Expand Down Expand Up @@ -119,6 +120,7 @@ export class DebugConfToDebugMapsConverter {
map.set(SystemVariablesConstants.SYSTEM_TEAM_PROJECT_ID, conf.system.teamProjectId);
map.set(SystemVariablesConstants.BUILD_SOURCE_BRANCH_NAME, conf.build.sourceBranchName);
map.set(SystemVariablesConstants.BUILD_DEFINITION_ID,conf.system.definitionId);
map.set(SystemVariablesConstants.BUILD_SOURCE_BRANCH,conf.build.sourceBranch);

map.set(EndpointDataConstants.ENDPOINT_DATA_OCTANE_AZURE_PERSONAL_ACCESS_TOKEN, conf.endpoint.azurePersonalAccessToken);
map.set(EndpointDataConstants.ENDPOINT_DATA_OCTANE_INSTANCE_ID, conf.endpoint.octaneInstanceId);
Expand Down
4 changes: 2 additions & 2 deletions templates/private/Tasks/EndTask/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"demands": [],
"version": {
"Major": "1",
"Minor": "18",
"Patch": "16"
"Minor": "19",
"Patch": "2"
},
"minimumAgentVersion": "2.170.1",
"instanceNameFormat": "ALM Octane Job End",
Expand Down
4 changes: 2 additions & 2 deletions templates/private/Tasks/StartTask/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"demands": [],
"version": {
"Major": "1",
"Minor": "18",
"Patch": "16"
"Minor": "19",
"Patch": "2"
},
"minimumAgentVersion": "2.170.1",
"instanceNameFormat": "ALM Octane Job Start",
Expand Down
3 changes: 3 additions & 0 deletions templates/private/overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# ALM Octane Integration with Azure DevOps Services
## 0.4.2.2 version Release notes
* Update Octane CI Server plugin version
* Update Octane pipeline to will be able to execute from Octane side
## 0.4.1.16 version Release notes
* Report to Octane duration of the pipeline run
## 0.4.1.13 version Release notes
Expand Down
2 changes: 1 addition & 1 deletion templates/private/vss-extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifestVersion": 1,

"id": "alm-octane-integration",
"version": "0.4.1.16",
"version": "0.4.2.2",
"name": "ALM Octane Integration Private Extension",
"description": "Beta version. An Azure DevOps extension that supplies the required entities for establishing an integration with ALM Octane.",
"publisher": "almoctane",
Expand Down
4 changes: 2 additions & 2 deletions templates/public/Tasks/EndTask/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"demands": [],
"version": {
"Major": "1",
"Minor": "8",
"Patch": "6"
"Minor": "9",
"Patch": "1"
},
"minimumAgentVersion": "2.170.1",
"instanceNameFormat": "ALM Octane Job End",
Expand Down
4 changes: 2 additions & 2 deletions templates/public/Tasks/StartTask/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"demands": [],
"version": {
"Major": "1",
"Minor": "8",
"Patch": "6"
"Minor": "9",
"Patch": "1"
},
"minimumAgentVersion": "2.170.1",
"instanceNameFormat": "ALM Octane Job Start",
Expand Down
3 changes: 3 additions & 0 deletions templates/public/overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# ALM Octane Integration with Azure DevOps Services
## 0.2.6.1 version Release notes
* Update Octane CI Server plugin version
* Update Octane pipeline to will be able to execute from Octane side
## 0.2.5.7 version Release notes
* Report to Octane duration of the pipeline run
## 0.2.5.6 version Release notes
Expand Down
2 changes: 1 addition & 1 deletion templates/public/vss-extension.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifestVersion": 1,
"id": "alm-octane-integration-public",
"version": "0.2.5.7",
"version": "0.2.6.0",
"name": "ALM Octane Integration Extension",
"description": "Tech Preview. An Azure DevOps extension that supplies the required entities for establishing an integration with ALM Octane.",
"publisher": "almoctane",
Expand Down

0 comments on commit 8480f3b

Please sign in to comment.