Skip to content

Commit

Permalink
feat: 🎸 get jira issue from PR
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavotr committed Oct 17, 2023
1 parent 78132ce commit be28b1f
Show file tree
Hide file tree
Showing 9 changed files with 2,572 additions and 2,364 deletions.
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"semi": true
}
24 changes: 13 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,24 @@
"node": ">=4.0.0"
},
"devDependencies": {
"ts-jest": "^20.0.0",
"@types/jest": "^19.2.4",
"tslint": "^5.4.3",
"@types/jest": "^29.5.5",
"axios": "^1.5.1",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"danger": "*",
"commitizen": "^2.9.6",
"cz-conventional-changelog": "^2.0.0",
"husky": "^0.13.3",
"jest": "^20.0.1",
"lint-staged": "^3.4.1",
"prettier": "^1.3.1",
"husky": "^8.0.3",
"jest": "^29.7.0",
"lint-staged": "^14.0.1",
"nock": "^13.3.4",
"prettier": "^3.0.3",
"semantic-release": "^22.0.5",
"typescript": "^2.3.2",
"ts-jest": "^29.1.1",
"tslint": "^6.1.3",
"typescript": "^5.2.2",
"validate-commit-msg": "^2.12.1"
},
"optionalDependencies": {
"esdoc": "^0.5.2"
"esdoc": "^1.1.0"
},
"config": {
"commitizen": {
Expand Down
114 changes: 114 additions & 0 deletions src/clients/jira-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { JiraClient, JiraKey } from "./jira-client";
import nock from "nock";

describe("basics", () => {
it("constructs", () => {
const client = new JiraClient("base-url", "username", "token", "PRJ");
expect(client).toBeDefined();
});
});

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

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

it("gets the issue type of a jira issue", async () => {
const response = {
fields: {
issuetype: {
name: "Story",
},
summary: "My Issue",
},
};

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");
});
});

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;

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

it("extracts the jira key if present", () => {
const jiraKey = client.extractJiraKey(
"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",
);
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",
);
expect(jiraKey?.toString()).toBe("PRJ-3721");
});

it("extracts the jira key case insensitive", () => {
const jiraKey = client.extractJiraKey(
"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",
);
expect(jiraKey).toBeUndefined();
});
});
98 changes: 98 additions & 0 deletions src/clients/jira-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import axios, { AxiosInstance } from "axios";

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

toString(): string {
return `${this.project}-${this.keyNumber}`;
}
}

export class JiraIssue {
constructor(
public key: JiraKey,
public link: string,
public title: string | undefined,
public type: string | undefined,
public fixVersions?: string[],
) {}

toString(): string {
return `${this.key} | ${this.type} | ${this.title}`;
}
}

export class JiraClient {
client: AxiosInstance;

constructor(
private baseUrl: string,
private username: string,
private token: string,
private projectKey: string,
) {
this.client = axios.create({
baseURL: this.baseUrl,
auth: {
username: this.username,
password: this.token,
},
timeout: 2000,
});
}

extractJiraKey(input: string): JiraKey | undefined {
const regex = new RegExp(`${this.projectKey}-(?<number>\\d+)`, "i");
const match = input.match(regex);

if (!match?.groups?.number) {
return undefined;
}

return new JiraKey(this.projectKey, match?.groups?.number);
}

async getIssue(key: JiraKey): Promise<JiraIssue | undefined> {
try {
const res = await this.client.get(
this.getRestApiUrl(`issue/${key}?fields=issuetype,summary,fixVersions`),
);
const obj = res.data;

let issuetype: string | undefined;
let title: string | undefined;
let fixVersions: string[] | undefined;
for (const 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,
fixVersions,
);
} catch (error) {
if (error.response) {
throw new Error(JSON.stringify(error.response, null, 4));
}
throw error;
}
}

private getRestApiUrl(endpoint: string): string {
return `${this.baseUrl}/rest/api/3/${endpoint}`;
}
}
39 changes: 19 additions & 20 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
import jiraPrValidation from "./index"
import jiraPrValidation from "./index";

declare const global: any
declare const global: any;

describe("jiraPrValidation()", () => {
beforeEach(() => {
global.warn = jest.fn()
global.message = jest.fn()
global.fail = jest.fn()
global.markdown = jest.fn()
})
global.warn = jest.fn();
global.message = jest.fn();
global.fail = jest.fn();
global.markdown = jest.fn();
});

afterEach(() => {
global.warn = undefined
global.message = undefined
global.fail = undefined
global.markdown = undefined
})
global.warn = undefined;
global.message = undefined;
global.fail = undefined;
global.markdown = undefined;
});

it("Checks for a that message has been called", () => {
global.danger = {
github: { pr: { title: "My Test Title" } },
}
github: { pr: { title: "My Test Title", base: "pr-base" } },
};

jiraPrValidation()
jiraPrValidation();

expect(global.message).toHaveBeenCalledWith(
"PR Title: My Test Title",
)
})
})
expect(global.message).toHaveBeenCalledWith("PR Title: My Test Title");
expect(global.message).toHaveBeenCalledWith("PR Base: pr-base");
});
});
61 changes: 52 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,59 @@
// Provides dev-time type structures for `danger` - doesn't affect runtime.
import {DangerDSLType} from "../node_modules/danger/distribution/dsl/DangerDSL"
declare var danger: DangerDSLType
export declare function message(message: string): void
export declare function warn(message: string): void
export declare function fail(message: string): void
export declare function markdown(message: string): void
import { DangerDSLType } from "../node_modules/danger/distribution/dsl/DangerDSL";
import { JiraClient } from "./clients/jira-client";
declare const danger: DangerDSLType;
export declare function message(message: string): void;
export declare function warn(message: string): void;
export declare function fail(message: string): void;
export declare function markdown(message: string): void;

/**
* Import metadata from the issue on Jira and perform validations
*/
export default function jiraPrValidation() {
export default async function jiraPrValidation(
baseUrl: string,
username: string,
token: string,
projectKey: string,
) {
// Replace this with the code from your Dangerfile
const title = danger.github.pr.title
message(`PR Title: ${title}`)
const title = danger.github.pr.title;
const base = danger.github.pr.base.ref;
const head = danger.github.pr.head.ref;
message(`PR Title: ${title}`);
message(`PR Base: ${base}`);
message(`PR Head: ${head}`);

const jiraClient = new JiraClient(baseUrl, username, token, projectKey);

const jiraKey = jiraClient.extractJiraKey(head);

message("jiraKey " + jiraKey);
if (!jiraKey) {
warn("⚠️ No Jira key found in branch name, exiting");
return;
}

const jiraIssue = await jiraClient.getIssue(jiraKey);
message("jiraIssue " + jiraIssue);
if (!jiraIssue) {
warn("⚠️ Could not get issue, exiting");
return;
}

if (fixVersionsMatchesBranch(base, jiraIssue.fixVersions)) {
fail("🚨 Base branch doesn't match Jira fixVersion");
}
}

function fixVersionsMatchesBranch(branch: string, fixVersions?: string[]) {
if (!fixVersions?.length) {
return true;
}

if (fixVersions.some((version) => branch.includes(version))) {
return true;
}

return false;
}
Loading

0 comments on commit be28b1f

Please sign in to comment.