Skip to content

Commit

Permalink
Merge pull request #76 from gustavotr/feature/get-issue-fix-versions
Browse files Browse the repository at this point in the history
  • Loading branch information
pieterclaerhout authored Sep 21, 2023
2 parents ae7706f + d735270 commit b9a8f31
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 58 deletions.
57 changes: 31 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,23 @@ jobs:
automation:
runs-on: ubuntu-latest
steps:
- name: Add Jira info
uses: contractify/add-jira-info@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
jira-base-url: ${{ secrets.JIRA_BASE_URL }}
jira-username: ${{ secrets.JIRA_USERNAME }}
jira-token: ${{ secrets.JIRA_TOKEN }}
jira-project-key: PRJ
add-label-with-issue-type: true
issue-type-label-color: FBCA04
issue-type-label-description: 'Jira Issue Type'
add-jira-key-to-title: true
add-jira-key-to-body: true
- name: Add Jira info
uses: contractify/add-jira-info@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
jira-base-url: ${{ secrets.JIRA_BASE_URL }}
jira-username: ${{ secrets.JIRA_USERNAME }}
jira-token: ${{ secrets.JIRA_TOKEN }}
jira-project-key: PRJ
add-label-with-issue-type: true
issue-type-label-color: FBCA04
issue-type-label-description: "Jira Issue Type"
add-jira-key-to-title: true
add-jira-key-to-body: true
add-jira-fix-versions-to-body: true
```
The `on:` section defines when the workflow needs to run. We ussually run them
The `on:` section defines when the workflow needs to run. We usually run them
on everything that has to do with a pull request. We also use
`workflow_dispatch` to allow us to manually trigger the workflow.

Expand All @@ -76,18 +77,19 @@ We strongly suggest to store the sensitive configuration parameters as

Various inputs are defined in [`action.yml`](action.yml) to let you configure the action:

| Name | Description | Required | Default |
| - | - | - | - |
| `github-token` | Token to use to authorize label changes. Typically the GITHUB_TOKEN secret, with `contents:read` and `pull-requests:write` access | N/A |
| `jira-base-url` | The subdomain of JIRA cloud that you use to access it. Ex: "https://your-domain.atlassian.net". | `true` | `null` |
| `jira-username` | Username used to fetch Jira Issue information. Check [below](#how-to-get-the-jira-token-and-jira-username) for more details on how to generate the token. | `true` | `null` |
| `jira-token` | Token used to fetch Jira Issue information. Check [below](#how-to-get-the-jira-token-and-jira-username) for more details on how to generate the token. | `true` | `null` |
| `jira-project-key` | Key of project in jira. First part of issue key | `true` | `null` |
| `add-label-with-issue-type` | If set to `true`, a label with the issue type from Jira will be added to the pull request | `false` | `true` |
| `issue-type-label-color` | The hex color to use for the issue type label | `false` | `FBCA04` |
| `issue-type-label-description` | The description to use for the issue type label | `false` | `Jira Issue Type` |
| `add-jira-key-to-title` | If set to `true`, the title of the pull request will be prefixed with the Jira issue key | `false` | `true` |
| `add-jira-key-to-body` | If set to `true`, the body of the pull request will be suffix with a link to the Jira issue | `false` | `true` |
| Name | Description | Required | Default |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------- |
| `github-token` | Token to use to authorize label changes. Typically the GITHUB_TOKEN secret, with `contents:read` and `pull-requests:write` access | N/A |
| `jira-base-url` | The subdomain of JIRA cloud that you use to access it. Ex: "https://your-domain.atlassian.net". | `true` | `null` |
| `jira-username` | Username used to fetch Jira Issue information. Check [below](#how-to-get-the-jira-token-and-jira-username) for more details on how to generate the token. | `true` | `null` |
| `jira-token` | Token used to fetch Jira Issue information. Check [below](#how-to-get-the-jira-token-and-jira-username) for more details on how to generate the token. | `true` | `null` |
| `jira-project-key` | Key of project in jira. First part of issue key | `true` | `null` |
| `add-label-with-issue-type` | If set to `true`, a label with the issue type from Jira will be added to the pull request | `false` | `true` |
| `issue-type-label-color` | The hex color to use for the issue type label | `false` | `FBCA04` |
| `issue-type-label-description` | The description to use for the issue type label | `false` | `Jira Issue Type` |
| `add-jira-key-to-title` | If set to `true`, the title of the pull request will be prefixed with the Jira issue key | `false` | `true` |
| `add-jira-key-to-body` | If set to `true`, the body of the pull request will be suffix with a link to the Jira issue | `false` | `true` |
| `add-jira-fix-versions-to-body` | If set to `true`, the body of the pull request will be suffix with the `fixVersions` from to the Jira issue | `false` | `true` |

Tokens are private, so it's suggested adding them as [GitHub secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets).

Expand Down Expand Up @@ -155,6 +157,7 @@ It will automatically be assigned to the pull request.
## How to get the `jira-token` and `jira-username`

The Jira token is used to fetch issue information via the Jira REST API. To get the token:

1. Generate an [API token via JIRA](https://confluence.atlassian.com/cloud/api-tokens-938839638.html)
2. Add the Jira username to the `JIRA_USERNAME` secret in your project
3. Add the Jira API token to the `JIRA_TOKEN` secret in your project
Expand All @@ -166,13 +169,15 @@ Note: The user should have the [required permissions (mentioned under GET Issue)
Contractify is a blooming Belgian SaaS scale-up offering contract management software and services.

We help business leaders, legal & finance teams to

- 🗄️ centralize contracts & responsibilities, even in a decentralized organization.
- 📝 keep track of all contracts & related mails or documents in 1 tool
- 🔔 automate & collaborate on contract follow-up tasks
- ✒️ approve & sign documents safely & fast
- 📊 report on custom contract data

The cloud platform is easily supplemented with full contract management support, including:

- ✔️ registration and follow up of your existing & new contracts
- ✔️ expert advice on contract management
- ✔️ periodic reporting & status updates
Expand Down
48 changes: 42 additions & 6 deletions __tests__/jira_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,50 @@ describe("get jira issue type", () => {
};

nock("https://base-url")
.get("/rest/api/3/issue/PRJ-123?fields=issuetype,summary")
.get("/rest/api/3/issue/PRJ-123?fields=issuetype,summary,fixVersions")
.reply(200, () => response);

const issue = await client.getIssue(new JiraKey("PRJ", "123"));
expect(issue?.type).toBe("story");
});
});

describe("get jira issue fixVersions", () => {
let client: JiraClient;

beforeEach(() => {
client = new JiraClient("https://base-url", "username", "token", "PRJ");
});

it("gets the fixVersions property of a jira issue", async () => {
const response = {
fields: {
issuetype: {
name: "Story",
},
summary: "My Issue",
fixVersions: [
{
description: "",
name: "v1.0.0",
archived: false,
released: false,
releaseDate: "2023-10-31",
},
],
},
};

nock("https://base-url")
.get("/rest/api/3/issue/PRJ-123?fields=issuetype,summary,fixVersions")
.reply(200, () => response);

const issue = await client.getIssue(new JiraKey("PRJ", "123"));
expect(issue?.type).toBe("story");
expect(issue?.fixVersions![0]).toBe("v1.0.0");
});
});

describe("extract jira key", () => {
let client: JiraClient;

Expand All @@ -43,35 +79,35 @@ describe("extract jira key", () => {

it("extracts the jira key if present", () => {
const jiraKey = client.extractJiraKey(
"PRJ-3721_actions-workflow-improvements"
"PRJ-3721_actions-workflow-improvements",
);
expect(jiraKey?.toString()).toBe("PRJ-3721");
});

it("extracts the jira key if present without underscore", () => {
const jiraKey = client.extractJiraKey(
"PRJ-3721-actions-workflow-improvements"
"PRJ-3721-actions-workflow-improvements",
);
expect(jiraKey?.toString()).toBe("PRJ-3721");
});

it("extracts the jira key from a feature branch if present", () => {
const jiraKey = client.extractJiraKey(
"feature/PRJ-3721_actions-workflow-improvements"
"feature/PRJ-3721_actions-workflow-improvements",
);
expect(jiraKey?.toString()).toBe("PRJ-3721");
});

it("extracts the jira key case insensitive", () => {
const jiraKey = client.extractJiraKey(
"PRJ-3721_actions-workflow-improvements"
"PRJ-3721_actions-workflow-improvements",
);
expect(jiraKey?.toString()).toBe("PRJ-3721");
});

it("returns undefined if not present", () => {
const jiraKey = client.extractJiraKey(
"prj3721_actions-workflow-improvements"
"prj3721_actions-workflow-improvements",
);
expect(jiraKey).toBeUndefined();
});
Expand Down
41 changes: 40 additions & 1 deletion __tests__/updater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ describe("body", () => {

beforeEach(() => {
const jiraKey = new JiraKey("PRJ", "1234");
const jiraIssue = new JiraIssue(jiraKey, "http://jira", "title", "story");
const jiraIssue = new JiraIssue(jiraKey, "http://jira", "title", "story", [
"v1.0.0",
]);
updater = new Updater(jiraIssue);
});

Expand Down Expand Up @@ -124,4 +126,41 @@ describe("body", () => {
const actual = updater.body(body);
expect(actual).toBe("PRJ-1234\n\ntest");
});

it("adds the fixVersions to an undefined body", () => {
const body = undefined;

const actual = updater.addFixVersionsToBody(body);
expect(actual).toBe("**Fix versions**: v1.0.0");
});

it("adds the fixVersions to an empty body", () => {
const body = "";

const actual = updater.addFixVersionsToBody(body);
expect(actual).toBe("**Fix versions**: v1.0.0");
});

it("adds the fixVersions to an existing body", () => {
const body = "test";

const actual = updater.addFixVersionsToBody(body);
expect(actual).toBe("test\n\n**Fix versions**: v1.0.0");
});

it("adds the fixVersions to an existing body with reference to ticket", () => {
const body = "test\n\nReferences PRJ-1234";

const actual = updater.addFixVersionsToBody(body);
expect(actual).toBe(
"test\n\nReferences PRJ-1234\n\n**Fix versions**: v1.0.0",
);
});

it("update the fixVersions if the body contains the fixVersions already", () => {
const body = "**Fix versions**: v0.9.9";

const actual = updater.addFixVersionsToBody(body);
expect(actual).toBe("**Fix versions**: v1.0.0");
});
});
38 changes: 21 additions & 17 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
name: 'Add Jira info to pull request'
description: 'Automatically add Jira info to a pull request'
author: 'Contractify'
name: "Add Jira info to pull request"
description: "Automatically add Jira info to a pull request"
author: "Contractify"
inputs:
github-token:
description: 'The GITHUB_TOKEN secret'
description: "The GITHUB_TOKEN secret"
jira-username:
description: 'Username used to access the Jira REST API. Must have read access to your Jira Projects & Issues.'
description: "Username used to access the Jira REST API. Must have read access to your Jira Projects & Issues."
jira-token:
description: 'API Token used to access the Jira REST API. Must have read access to your Jira Projects & Issues.'
description: "API Token used to access the Jira REST API. Must have read access to your Jira Projects & Issues."
required: true
jira-base-url:
description: 'The subdomain of JIRA cloud that you use to access it. Ex: "https://your-domain.atlassian.net"'
required: true
jira-project-key:
description: 'Key of project in jira. First part of issue key'
description: "Key of project in jira. First part of issue key"
required: true
default: ''
default: ""
add-label-with-issue-type:
description: 'If set to true, a label with the issue type from Jira will be added to the pull request'
description: "If set to true, a label with the issue type from Jira will be added to the pull request"
default: "true"
required: false
issue-type-label-color:
description: 'The hex color of the label to use for the issue type'
default: 'FBCA04'
description: "The hex color of the label to use for the issue type"
default: "FBCA04"
required: false
issue-type-label-description:
description: 'The description of the label to use for the issue type'
default: 'Jira Issue Type'
description: "The description of the label to use for the issue type"
default: "Jira Issue Type"
required: false
add-jira-key-to-title:
description: 'If set to true, the title of the pull request will be prefixed with the Jira issue key'
description: "If set to true, the title of the pull request will be prefixed with the Jira issue key"
default: "true"
required: false
add-jira-key-to-body:
description: 'If set to true, the body of the pull request will be suffix with a link to the Jira issue'
description: "If set to true, the body of the pull request will be suffix with a link to the Jira issue"
default: "true"
required: false
add-jira-fix-versions-to-body:
description: "If set to `true`, the body of the pull request will be suffix with the `fixVersions` from to the Jira issue"
default: "true"
required: false

Expand All @@ -42,5 +46,5 @@ branding:
color: green

runs:
using: 'node16'
main: 'dist/index.js'
using: "node16"
main: "dist/index.js"
20 changes: 15 additions & 5 deletions src/common/jira_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { HttpClient } from "@actions/http-client";
import { BasicCredentialHandler } from "@actions/http-client/lib/auth";

export class JiraKey {
constructor(public project: string, public number: string) {}
constructor(
public project: string,
public number: string,
) {}

toString(): string {
return `${this.project}-${this.number}`;
Expand All @@ -14,7 +17,8 @@ export class JiraIssue {
public key: JiraKey,
public link: string,
public title: string | undefined,
public type: string | undefined
public type: string | undefined,
public fixVersions?: string[],
) {}

toString(): string {
Expand All @@ -29,7 +33,7 @@ export class JiraClient {
private baseUrl: string,
private username: string,
private token: string,
private projectKey: string
private projectKey: string,
) {
const credentials = new BasicCredentialHandler(this.username, this.token);

Expand All @@ -52,26 +56,32 @@ export class JiraClient {
async getIssue(key: JiraKey): Promise<JiraIssue | undefined> {
try {
const res = await this.client.get(
this.getRestApiUrl(`issue/${key}?fields=issuetype,summary`)
this.getRestApiUrl(`issue/${key}?fields=issuetype,summary,fixVersions`),
);
const body: string = await res.readBody();
const obj = JSON.parse(body);

var issuetype: string | undefined = undefined;
var title: string | undefined = undefined;
var fixVersions: string[] | undefined = undefined;
for (let field in obj.fields) {
if (field === "issuetype") {
issuetype = obj.fields[field].name?.toLowerCase();
} else if (field === "summary") {
title = obj.fields[field];
} else if (field === "fixVersions") {
fixVersions = obj.fields[field]
.map(({ name }) => name)
.filter(Boolean);
}
}

return new JiraIssue(
key,
`${this.baseUrl}/browse/${key}`,
title,
issuetype
issuetype,
fixVersions,
);
} catch (error: any) {
if (error.response) {
Expand Down
Loading

0 comments on commit b9a8f31

Please sign in to comment.