-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12882 from OfficeDev/wenyt/cd
ci: update the branch policy
- Loading branch information
Showing
11 changed files
with
506 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
name: Issue ADO Create | ||
description: Generate a work item for labeled issue | ||
inputs: | ||
token: | ||
description: GitHub token with issue, comment, and label read/write permissions | ||
required: true | ||
devops-org: | ||
description: 'the org to create work item' | ||
required: true | ||
devops-projectId: | ||
description: 'the project to create work item' | ||
required: true | ||
title-prefix: | ||
description: 'the title prefix' | ||
default: '[Github]' | ||
bug-label: | ||
description: the label to create bug item. | ||
required: true | ||
bug-area-path: | ||
description: the area path to create bug item. | ||
required: true | ||
bug-iteration-path: | ||
description: the iteration path to create bug item. | ||
required: true | ||
feature-label: | ||
description: the label to create feature item. Input empty string to ignore feature | ||
default: '' | ||
feature-area-path: | ||
description: the area path to create feature item. | ||
default: '' | ||
feature-iteration-path: | ||
description: the iteration path to create feature item. | ||
default: '' | ||
|
||
runs: | ||
using: 'node16' | ||
main: 'index.js' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
import * as vm from 'azure-devops-node-api'; | ||
import * as nodeApi from 'azure-devops-node-api'; | ||
import * as WorkItemTrackingApi from 'azure-devops-node-api/WorkItemTrackingApi'; | ||
import * as WorkItemTrackingInterfaces from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces'; | ||
import { | ||
JsonPatchDocument, | ||
JsonPatchOperation, | ||
Operation, | ||
} from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; | ||
|
||
class ItemInfo { | ||
url: string; | ||
id: number; | ||
public constructor(url: string, id: number) { | ||
this.url = url; | ||
this.id = id; | ||
} | ||
} | ||
|
||
export class DevopsClient { | ||
token: string; | ||
org: string; | ||
projectId: string; | ||
bugArea: string; | ||
bugIteration: string; | ||
featureArea: string; | ||
featureIteration: string; | ||
|
||
witApi?: WorkItemTrackingApi.IWorkItemTrackingApi; | ||
|
||
constructor( | ||
token: string, | ||
org: string, | ||
projectId: string, | ||
bugArea: string, | ||
bugIteration: string, | ||
featureArea: string, | ||
featureIteration: string, | ||
) { | ||
this.token = token; | ||
this.org = org; | ||
this.projectId = projectId; | ||
this.bugArea = bugArea; | ||
this.bugIteration = bugIteration; | ||
this.featureArea = featureArea; | ||
this.featureIteration = featureIteration; | ||
} | ||
|
||
public async init() { | ||
let orgUrl = `https://dev.azure.com/${this.org}`; | ||
const webApi: nodeApi.WebApi = await this.getApi(orgUrl); | ||
this.witApi = await webApi.getWorkItemTrackingApi(); | ||
} | ||
|
||
public async queryPreviousItem(description: string): Promise<ItemInfo | undefined> { | ||
var query = `Select [System.Id] From WorkItems Where [System.Description] Contains Words '${description}' AND [System.HyperLinkCount] > 0 AND [State] <> 'Removed' order by [Microsoft.VSTS.Common.Priority] asc, [System.CreatedDate] desc`; | ||
const items = await this.witApi!.queryByWiql({ query: query }, undefined, undefined, 1); | ||
if (items.workItems?.length && items.workItems[0].id) { | ||
const resp = await this.witApi?.getWorkItem(items.workItems[0].id); | ||
return { | ||
url: resp?._links?.html?.href, | ||
id: items.workItems[0].id, | ||
}; | ||
} else { | ||
return undefined; | ||
} | ||
} | ||
|
||
public async createFeatureItem( | ||
titleValue: string, | ||
asigneeValue: string | undefined, | ||
tagsValue: string | undefined, | ||
url: string, | ||
sprintPath: string, | ||
): Promise<WorkItemTrackingInterfaces.WorkItem> { | ||
return this.createItem( | ||
titleValue, | ||
asigneeValue, | ||
this.featureArea, | ||
this.featureIteration, | ||
tagsValue, | ||
url, | ||
'Feature', | ||
sprintPath, | ||
); | ||
} | ||
|
||
public async createBugItem( | ||
titleValue: string, | ||
asigneeValue: string | undefined, | ||
tagsValue: string | undefined, | ||
url: string, | ||
sprintPath: string, | ||
): Promise<WorkItemTrackingInterfaces.WorkItem> { | ||
return this.createItem( | ||
titleValue, | ||
asigneeValue, | ||
this.bugArea, | ||
this.bugIteration, | ||
tagsValue, | ||
url, | ||
'Bug', | ||
this.bugIteration, | ||
); | ||
} | ||
|
||
public async createItem( | ||
titleValue: string, | ||
asigneeValue: string | undefined, | ||
areaValue: string, | ||
iterationValue: string, | ||
tagsValue: string | undefined, | ||
url: string, | ||
type: string, | ||
sprintPath: string, | ||
): Promise<WorkItemTrackingInterfaces.WorkItem> { | ||
let document: JsonPatchOperation[] = []; | ||
|
||
const title: JsonPatchOperation = { | ||
path: '/fields/System.Title', | ||
op: Operation.Add, | ||
value: titleValue, | ||
}; | ||
document.push(title); | ||
|
||
if (asigneeValue) { | ||
const asignee: JsonPatchOperation = { | ||
path: '/fields/System.AssignedTo', | ||
op: Operation.Add, | ||
value: asigneeValue, | ||
}; | ||
document.push(asignee); | ||
} | ||
|
||
const area: JsonPatchOperation = { | ||
path: '/fields/System.AreaPath', | ||
op: Operation.Add, | ||
value: areaValue, | ||
}; | ||
document.push(area); | ||
|
||
const iteration: JsonPatchOperation = { | ||
path: '/fields/System.IterationPath', | ||
op: Operation.Add, | ||
value: sprintPath ?? iterationValue, | ||
}; | ||
document.push(iteration); | ||
|
||
if (tagsValue) { | ||
const tags: JsonPatchOperation = { | ||
path: '/fields/System.Tags', | ||
op: Operation.Add, | ||
value: tagsValue, | ||
}; | ||
document.push(tags); | ||
} | ||
|
||
const previous = await this.queryPreviousItem(url); | ||
const description: JsonPatchOperation = { | ||
path: '/fields/System.Description', | ||
op: Operation.Add, | ||
value: this.buildDescription(url, previous), | ||
}; | ||
document.push(description); | ||
|
||
const hyperLink: JsonPatchOperation = { | ||
path: '/relations/-', | ||
op: Operation.Add, | ||
value: { | ||
rel: 'Hyperlink', | ||
url: url, | ||
attributes: { comment: 'github issue link' }, | ||
}, | ||
}; | ||
document.push(hyperLink); | ||
|
||
const item = await this.witApi!.createWorkItem( | ||
undefined, | ||
document as JsonPatchDocument, | ||
this.projectId, | ||
type, | ||
); | ||
return item; | ||
} | ||
|
||
private async getApi(serverUrl: string): Promise<vm.WebApi> { | ||
let authHandler = vm.getHandlerFromToken(this.token); | ||
let vsts: vm.WebApi = new vm.WebApi(serverUrl, authHandler); | ||
await vsts.connect(); | ||
return vsts; | ||
} | ||
|
||
private buildDescription(url: string, addition?: ItemInfo): string { | ||
var description = `<a href="${url}">${url}</a>`; | ||
if (addition) { | ||
description += `<br>There is an existing work item related to this issue<br>`; | ||
description += `<div><a href="${addition.url}" data-vss-mention="version:1.0">#${addition.id}</a> </div>`; | ||
} | ||
return description; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { OctoKit, OctoKitIssue } from '../api/octokit'; | ||
import { Action } from '../common/Action'; | ||
import { DevopsClient } from './azdo'; | ||
import { getRequiredInput, safeLog } from '../common/utils'; | ||
import { context } from '@actions/github'; | ||
import { getInput } from '@actions/core'; | ||
import { getEmail, sendAlert } from '../teamsfx-utils/utils'; | ||
import * as WorkItemTrackingInterfaces from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces'; | ||
import { AzureCliCredential } from "@azure/identity"; | ||
|
||
|
||
const githubToken = getRequiredInput('token'); | ||
const org = getRequiredInput('devops-org'); | ||
const projectId = getRequiredInput('devops-projectId'); | ||
const titlePreix = getRequiredInput('title-prefix'); | ||
const bugLabel = getRequiredInput('bug-label'); | ||
const bugArea = getRequiredInput('bug-area-path'); | ||
const bugIteration = getRequiredInput('bug-iteration-path'); | ||
const featureArea = getInput('feature-area-path'); | ||
const featureIteration = getInput('feature-iteration-path'); | ||
|
||
class Labeled extends Action { | ||
id = 'Labeled'; | ||
|
||
async onLabeled(issue: OctoKitIssue) { | ||
const content = await issue.getIssue(); | ||
let sprintPath = ""; | ||
if (content.milestone?.description) { | ||
const match = content.milestone?.description.match(/Sprint path is:(.*)/); | ||
if (match && match.length > 1) { | ||
sprintPath = match[1]; | ||
} | ||
} | ||
safeLog(`the issue ${content.number} is created by label`); | ||
let client = await this.createClient(); | ||
const asignee = getEmail(content.assignee); | ||
if (!asignee) { | ||
safeLog(`the issue ${content.number} assignee:${content.assignee} is not associated with email address, ignore.`); | ||
const subject = '[Github Issue Alert] missing associated email address for assignee'; | ||
const issueLink = `https://github.com/OfficeDev/TeamsFx/issues/${content.number}`; | ||
const fileLink = "https://github.com/OfficeDev/TeamsFx/blob/dev/.github/accounts.json"; | ||
const message = `There is a github issue <a>${issueLink}</a> labeled with account <b>${content.assignee}</b> which is not associated with company email. Please check it and update the account mapping in the file <a>${fileLink}</a>.`; | ||
safeLog(message); | ||
sendAlert(subject, message); | ||
} | ||
const url = this.issueUrl(content.number); | ||
const title = titlePreix + content.title; | ||
let workItem: WorkItemTrackingInterfaces.WorkItem; | ||
safeLog(`issue labeled with ${bugLabel}. Bug work item will be created. ${sprintPath}`); | ||
workItem = await client.createBugItem(title, asignee, undefined, url, sprintPath); | ||
safeLog(`finished to create work item.`); | ||
const workItemUrl = workItem._links?.html?.href; | ||
if (workItemUrl) { | ||
await issue.postComment(`The issue is labeled with sprint and a work item created: ${workItemUrl}`); | ||
} else { | ||
safeLog(`no work item url found, ignore to post comment.`); | ||
} | ||
} | ||
async onTriggered(_: OctoKit) { | ||
const issueNumber = process.env.ISSUE_NUMBER; | ||
safeLog(`start manually create work item for issue ${issueNumber}`); | ||
const issue = new OctoKitIssue(githubToken, context.repo, { number: parseInt(issueNumber || "0") }); | ||
await this.onLabeled(issue); | ||
} | ||
|
||
private async createClient() { | ||
let credential = new AzureCliCredential(); | ||
const devopsToken = await credential.getToken("https://app.vssps.visualstudio.com/.default"); | ||
|
||
let client = new DevopsClient( | ||
devopsToken.token, | ||
org, | ||
projectId, | ||
bugArea, | ||
bugIteration, | ||
featureArea, | ||
featureIteration, | ||
); | ||
await client.init(); | ||
return client; | ||
} | ||
|
||
private issueUrl(id: number) { | ||
return `https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${id}`; | ||
} | ||
} | ||
|
||
new Labeled().run(); // eslint-disable-line |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.