-
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.
- Loading branch information
1 parent
256bca9
commit 1d2acbe
Showing
4 changed files
with
346 additions
and
4 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,40 @@ | ||
name: Issue Milestoned | ||
description: Generate a work item for milestoned issue | ||
inputs: | ||
token: | ||
description: GitHub token with issue, comment, and label read/write permissions | ||
required: true | ||
milestone-prefix: | ||
description: 'the specific milestones prefix to create work item' | ||
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', | ||
sprintPath, | ||
); | ||
} | ||
|
||
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,101 @@ | ||
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 milestonePrefix = getRequiredInput('milestone-prefix'); | ||
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 featureLabel = getInput('feature-label'); | ||
const featureArea = getInput('feature-area-path'); | ||
const featureIteration = getInput('feature-iteration-path'); | ||
|
||
class Milestoned extends Action { | ||
id = 'Milestoned'; | ||
|
||
async onMilestoned(issue: OctoKitIssue) { | ||
const content = await issue.getIssue(); | ||
const milestoneTitle = content.milestone?.title ?? ""; | ||
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> milestoned 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 + `[${milestoneTitle}]` + content.title; | ||
let workItem: WorkItemTrackingInterfaces.WorkItem; | ||
if (featureLabel && content.labels.includes(featureLabel)) { | ||
safeLog(`issue labeled with ${featureLabel}. Feature work item will be created.`); | ||
workItem = await client.createFeatureItem(title, asignee, undefined, url, sprintPath); | ||
} else if (content.labels.includes(bugLabel)) { | ||
safeLog(`issue labeled with ${bugLabel}. Bug work item will be created.`); | ||
workItem = await client.createBugItem(title, asignee, undefined, url, sprintPath); | ||
} else { | ||
safeLog( | ||
`issue labeled without feature label(${featureLabel}) and bug label(${bugLabel}). Default bug work item will be created.`, | ||
); | ||
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 milestoned with sprint milestone ${milestoneTitle} 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.onMilestoned(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 Milestoned().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