Skip to content

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
captainbrosset committed Jul 17, 2023
1 parent 1fb64a2 commit 774d440
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 8 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Sync issue to Azure DevOps work item

on:
issues:
types:
[labeled]

jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: MicrosoftEdge/action-issue-to-workitem
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}"
with:
label: 'tracked'
ado_organization: 'microsoft'
ado_project: 'Edge'
ado_tags: 'githubSync;patrickTest'
parent_work_item: 37589346
ado_area_path: 'Edge\Dev Experience\Developer Tools\F12 Tools'
ado_work_item_type: 'Deliverable'
ado_dont_check_if_exist: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
88 changes: 80 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,86 @@
# Project
# Sync GitHub issues and PRs to Azure DevOps work items

> This repo has been populated by an initial template to help get you started. Please
> make sure to update the content to build a great experience for community-building.
Create a work item in Azure DevOps when a GitHub issue or PR is interacted with.

As the maintainer of this project, please make a few updates:
Use the `issues` or `pull_requests` trigger in your workflow to call this action.

- Improving this README.MD file to provide a great experience
- Updating SUPPORT.MD with content about this project's support experience
- Understanding the security reporting process in SECURITY.MD
- Remove this section from the README
## Inputs

### `label`

**Optional**. If specified, only issues or PRs with this label will create ADO items.

### `ado_organization`

The name of the ADO organization where work items are to be created.

### `ado_project`

The name of the ADO project within the organization.

### `ado_tags`

**Optional** tags to be added to the work item (separated by semi-colons).

### `parent_work_item`

**Optional** work item number to parent the newly created work item.

### `ado_area_path`

An area path to put the work item under.

### `ado_work_item_type`

**Optional**. The type of work item to create. Defaults to Bug.

Common values: Task, Bug, Deliverable, Scenario.

### `ado_dont_check_if_exist`

**Optional**. By default, the action tries to find an ADO work item that was already created for this issue or PR. If one is found, no new work item is created.

Set this to true to avoid checking and just always create a work item instead.

## Outputs

### `id`

The id of the Work Item created or updated

## Environment variables

The following environment variables need to be provided to the action:

* `ado_token`: an [Azure Personal Access Token](https://docs.microsoft.com/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate) with "read & write" permission for Work Item.
* `github_token`: a GitHub Personal Access Token with "repo" permissions.

## Example usage

```yaml
name: Sync issue to Azure DevOps work item

on:
issues:
types:
[labeled]

jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: MicrosoftEdge/action-issue-to-workitem
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}"
with:
label: 'tracked'
ado_organization: 'ado_organization_name'
ado_project: 'your_project_name'
ado_tags: 'githubSync'
parent_work_item: 123456789
ado_area_path: 'optional_area_path'
```

## Contributing

Expand Down
37 changes: 37 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: "GitHub Issues to Azure DevOps"
description: "This action creates an ADO work item for a GitHub issue or pull request"
author: "MicrosoftEdge"
branding:
icon: "refresh-cw"
color: "yellow"
inputs:
label:
description: 'Only run the action is this label is present on the issue or pull request'
required: false
ado_organization:
description: 'The name of the ADO organization where the work item should be created'
required: true
ado_project:
description: 'The name of the project within the ADO organization'
required: true
ado_tags:
description: 'A list of tags to add to the newly created work item, separated by semi-colon (;)'
required: false
parent_work_item:
description: 'The number of a work item to use as a parent for the newly created work item'
required: false
ado_area_path:
description: 'The area path under which the work item should be created'
required: true
ado_work_item_type:
description: 'The type of work item to create. Defaults to Bug'
required: false
ado_dont_check_if_exist:
description: 'Do not check if a work item that contains the same issue or PR number already exists to avoid re-creating it. Defaults to false, which means the action checks to avoid re-creating.'
required: false
outputs:
id:
description: "id of work item created"
runs:
using: "node12"
main: "index.js"
228 changes: 228 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
const core = require(`@actions/core`);
const github = require(`@actions/github`);
const azdev = require(`azure-devops-node-api`);

async function main() {
const payload = github.context.payload;
const issueOrPr = payload.issue || payload.pull_request;
const isIssue = payload.issue != null;
const isPR = payload.pull_request != null;

if (core.getInput('label') && !issueOrPr.labels.some(label => label.name === core.getInput('label'))) {
console.log(`Action was configured to only run when issue or PR has label ${core.getInput('label')}, but we couldn't find it.`);
return;
}

let adoClient = null;

try {
const orgUrl = "https://dev.azure.com/" + core.getInput('ado_organization');
const adoAuthHandler = azdev.getPersonalAccessTokenHandler(process.env.ado_token);
const adoConnection = new azdev.WebApi(orgUrl, adoAuthHandler);
adoClient = await adoConnection.getWorkItemTrackingApi();
} catch (e) {
console.error(e);
core.setFailed('Could not connect to ADO');
return;
}

try {
if (!core.getInput('ado_dont_check_if_exist')) {
// go check to see if work item already exists in azure devops or not
// based on the title and tags.
console.log("Check to see if work item already exists");
const existingID = await find(issueOrPr);
if (!existingID) {
console.log("Could not find existing ADO workitem, creating one now");
} else {
console.log("Found existing ADO workitem: " + existingID + ". No need to create a new one");
return;
}
}

const workItem = await create(payload, adoClient);

// Add the work item number at the end of the github issue body.
const currentBody = issueOrPr.body || "";
issueOrPr.body = currentBody + "\n\nAB#" + workItem.id;
const octokit = new github.GitHub(process.env.github_token);

if (isIssue) {
await octokit.issues.update({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: issueOrPr.number,
body: issueOrPr.body
});
} else if (isPR) {
await octokit.pulls.update({
owner: payload.repository.owner.login,
repo: payload.repository.name,
pull_number: issueOrPr.number,
body: issueOrPr.body
});
}

// set output message
if (workItem != null || workItem != undefined) {
console.log(`Work item successfully created or found: ${workItem.id}`);
core.setOutput(`id`, `${workItem.id}`);
}
} catch (error) {
console.log("Error: " + error);
core.setFailed();
}
}

function formatTitle(payload) {
const issueOrPr = payload.issue || payload.pull_request;
const isIssue = payload.issue != null;

return `[GitHub ${isIssue ? "issue" : "PR"} #${issueOrPr.number}] ${issueOrPr.title}`;
}

async function formatDescription(payload) {
console.log('Creating a description based on the github issue');

const issueOrPr = payload.issue || payload.pull_request;
const isIssue = payload.issue != null;

const octokit = new github.GitHub(process.env.github_token);
const bodyWithMarkdown = await octokit.markdown.render({
text: issueOrPr.body || "",
mode: "gfm",
context: payload.repository.full_name
});

return `
<hr>
<em>This work item is a mirror of the GitHub
<a href="${issueOrPr.html_url}" target="_new">${isIssue ? "issue" : "PR"} #${issueOrPr.number}</a>.
It will not auto-update when the GitHub ${isIssue ? "issue" : "PR"} changes, please check the original ${isIssue ? "issue" : "PR"} on GitHub for updates.
</em>
<hr>
<br>
${bodyWithMarkdown.data}
`;
}

async function create(payload, adoClient) {
const issueOrPr = payload.issue || payload.pull_request;
const botMessage = await formatDescription(payload);
const shortRepoName = payload.repository.full_name.split("/")[1];
const tags = core.getInput("ado_tags") ? core.getInput("ado_tags") + ";" + shortRepoName : shortRepoName;
const itemType = core.getInput("ado_work_item_type") ? core.getInput("ado_work_item_type") : "Bug";

console.log(`Starting to create a ${itemType} work item for GitHub issue or PR #${issueOrPr.number}`);

const patchDocument = [
{
op: "add",
path: "/fields/System.Title",
value: formatTitle(payload),
},
{
op: "add",
path: "/fields/System.Description",
value: botMessage,
},
{
op: "add",
path: "/fields/Microsoft.VSTS.TCM.ReproSteps",
value: botMessage,
},
{
op: "add",
path: "/fields/System.Tags",
value: tags,
},
{
op: "add",
path: "/relations/-",
value: {
rel: "Hyperlink",
url: issueOrPr.html_url,
},
}
];

if (core.getInput('parent_work_item')) {
let parentUrl = "https://dev.azure.com/" + core.getInput('ado_organization');
parentUrl += '/_workitems/edit/' + core.getInput('parent_work_item');

patchDocument.push({
op: "add",
path: "/relations/-",
value: {
rel: "System.LinkTypes.Hierarchy-Reverse",
url: parentUrl,
attributes: {
comment: ""
}
}
});
}

patchDocument.push({
op: "add",
path: "/fields/System.AreaPath",
value: core.getInput('ado_area_path'),
});

let workItemSaveResult = null;

try {
console.log('Creating work item');
workItemSaveResult = await adoClient.createWorkItem(
(customHeaders = []),
(document = patchDocument),
(project = core.getInput('ado_project')),
(type = itemType),
(validateOnly = false),
(bypassRules = false)
);

// if result is null, save did not complete correctly
if (workItemSaveResult == null) {
workItemSaveResult = -1;

console.log("Error: createWorkItem failed");
console.log(`WIT may not be correct: ${wit}`);
core.setFailed();
} else {
console.log("Work item successfully created");
}
} catch (error) {
workItemSaveResult = -1;

console.log("Error: createWorkItem failed");
console.log(patchDocument);
console.log(error);
core.setFailed(error);
}

if (workItemSaveResult != -1) {
console.log(workItemSaveResult);
}

return workItemSaveResult;
}

async function find(issueOrPr) {
console.log('Checking if a work item already exists for #' + issueOrPr.number);

// Isues or PRs that got mirrored have the AB#123456 tag in the body.
// So we can simply look for this and extract the number.

if (issueOrPr.body != null && issueOrPr.body.includes("AB#")) {
const regex = /AB#(\d+)/g;
const matches = regex.exec(issueOrPr.body);
if (matches != null) {
return matches[1];
}
}

return null;
}

main();
Loading

0 comments on commit 774d440

Please sign in to comment.