Skip to content

Commit

Permalink
chore: [#177458092] Support Jira ticket key in the PR title (pagopa#2935
Browse files Browse the repository at this point in the history
)

* [#177458092] refactoring

* [#177458092] wip

* [#177458092] clean code

* [#177458092] clean code

* [#177458092] wip

* [#177458092] integrate with existing code

* [#177458092] fix

* [#177458092] fix

* [#177458092] read token from env

* [#177458092] change username password concat

* [#177458092] test remove checkDanger

* [#177458092] test new danger

* [#177458092] test

* [#177458092] test

* [#177458092] test

* [#177458092] test url

* [#177458092] add idprefix

* [#177458092] fix

* [#177458092] cannot use ?? operator

* [#177458092] refactoring

* [#177458092] fix ts

* [#177458092] print failure

* [#177458092] update error message

* [#177458092] print also right if some right

* [#177458092] test subtask

* [#177458092] remove print

* [#177458092] test

* [#177458092] add italic

* [#177458092] fix text

* [#177458092] fix

* [#177458092] feat: [FABT-1] test

* feat: [FABT-1] test title
chore: test
chore: [FABT-1,FABT-3] This is a pr title
bug: [FABT-7] this is a bug
test
.
.
.
.
.
test
test
fix: [FABT-7] This is a fix
feat: [#177458092] This is a pivotal story
change func
update jira regex

* [#177458092] refactoring

* [#177458092] Revert "[#177458092] refactoring"

This reverts commit 1ef2a05.

* [#177458092] refactoring

* [#177458092] refactoring

* [#177458092] refactoring

* [#177458092] add comments

* [#177458092] title uppercase

* [#177458092] add tag

* [#177458092] clean label

* [#177458092] update label

* [#177458092] refactoring

* [#177458092] test

* [#177458092] test uppercase

* [#177458092] add comment

* [#177458092] update comment

* [#177458092] update text

* Update scripts/ts/common/ticket/jira/index.ts

Co-authored-by: Matteo Boschi <[email protected]>

* [#177458092] use keyof instead of union

Co-authored-by: Matteo Boschi <[email protected]>
  • Loading branch information
fabriziofff and Undermaken authored Apr 1, 2021
1 parent 9b6a940 commit ff7601a
Show file tree
Hide file tree
Showing 12 changed files with 589 additions and 151 deletions.
61 changes: 8 additions & 53 deletions Dangerfile.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,16 @@
// Import custom DangerJS rules.
import { warn } from "danger";
// See http://danger.systems/js
// See https://github.com/teamdigitale/danger-plugin-digitalcitizenship/
import checkDangers from "danger-plugin-digitalcitizenship";
import { DangerDSLType } from "danger/distribution/dsl/DangerDSL";
import {fromNullable, none} from "fp-ts/lib/Option";
import {
allStoriesSameType,
getChangelogPrefixByStories,
getChangelogScope,
getPivotalStoriesFromPrTitle
} from "./scripts/changelog/ts/changelog";
import { commentPrWithTicketsInfo } from "./scripts/ts/danger/commentPrWithTicketsInfo";
import { updatePrTitleForChangelog } from "./scripts/ts/danger/updatePrTitleForChangelog";
import { getTicketsFromTitle } from "./scripts/ts/danger/utils/titleParser";

declare const danger: DangerDSLType;

const multipleTypesWarning =
"Multiple stories with different types are associated with this Pull request.\n" +
"Only one tag will be added, following the order: `feature > bug > chore`";

/**
* Append the changelog tag and scope to the pull request title
*/
const updatePrTitleForChangelog = async () => {
const associatedStories = await getPivotalStoriesFromPrTitle(
danger.github.pr.title
);

if (!allStoriesSameType(associatedStories)) {
warn(multipleTypesWarning);
}
const maybePrTag = getChangelogPrefixByStories(associatedStories);
const eitherScope = getChangelogScope(associatedStories);

if (eitherScope.isLeft()) {
eitherScope.value.map(err => warn(err.message));
}
const scope = eitherScope
.getOrElse(none)
.map(s => `(${s})`)
.getOrElse("");

const cleanChangelogRegex = /^(fix(\(.+\))?!?: |feat(\(.+\))?!?: |chore(\(.+\))?!?: )?(.*)$/;
const title = fromNullable(danger.github.pr.title.match(cleanChangelogRegex))
.map(matches => matches.pop() || danger.github.pr.title)
.getOrElse(danger.github.pr.title);

maybePrTag.map(tag =>
danger.github.api.pulls.update({
owner: danger.github.thisPR.owner,
repo: danger.github.thisPR.repo,
pull_number: danger.github.thisPR.number,
title: `${tag}${scope}: ${title}`
})
);
const mainDanger = async () => {
const associatedStories = await getTicketsFromTitle(danger.github.pr.title);
commentPrWithTicketsInfo(associatedStories);
await updatePrTitleForChangelog(associatedStories);
};

checkDangers();
void updatePrTitleForChangelog()
.then()
.catch();
void mainDanger().then().catch();
48 changes: 33 additions & 15 deletions scripts/changelog/add_pivotal_stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const fs = require("fs-extra");
const Pivotal = require("pivotaljs");
const pivotal = new Pivotal();

const jiraTicketBaseUrl = "https://pagopa.atlassian.net/browse/";

/**
* Skip the use of API to find the story url if the url is already retrieved (contains pivotaltracker.com)
* or the id length is < 9 (not a pivotal id)
Expand Down Expand Up @@ -32,21 +34,37 @@ async function replacePivotalUrl(match, storyId, url) {
}
}

async function replacePivotalStories() {
async function addJiraUrl(match, ticketKeys) {
return `[${ticketKeys
.split(",")
.map(x => `[${x}](${new URL(x, jiraTicketBaseUrl).toString()})`)}]`;
}

async function replaceJiraStories(content) {
// capture [JIRAID-123], avoid already linked ticket with pattern [JIRAID-123](http://jiraurl)
const jiraTagRegex = /\[([A-Z0-9]+-\d+(,[a-zA-Z]+-\d+)*)](?!\()/g;
return await replaceAsync(content, jiraTagRegex, addJiraUrl);
}

async function addTasksUrls() {
// read changelog
const content = fs.readFileSync("CHANGELOG.md").toString("utf8");
const rawChangelog = fs.readFileSync("CHANGELOG.md").toString("utf8");

// Add pivotal stories url
const withPivotalStories = await replacePivotalStories(rawChangelog);
// Add jira ticket url
const withJiraStories = await replaceJiraStories(withPivotalStories);

// write the new modified changelog
fs.writeFileSync("CHANGELOG.md", withJiraStories);
}

async function replacePivotalStories(content) {
// identify the pattern [#XXXXX](url) for markdown link
const pivotalTagRegex = /\[(#\d+)\]\(([a-zA-z:\/\.\d-@:%._\+~#=]+)\)/g;

// check for all the matches if is a pivotal story and update the url
const updatedChangelog = await replaceAsync(
content,
pivotalTagRegex,
replacePivotalUrl
);

// write the new modified changelog
fs.writeFileSync("CHANGELOG.md", updatedChangelog);
return await replaceAsync(content, pivotalTagRegex, replacePivotalUrl);
}

/**
Expand All @@ -71,18 +89,18 @@ async function replaceAsync(str, regex, asyncFn) {
* @param storyId
* @return {Promise<Pivotal.Story>}
*/
getStory = storyId => {
return new Promise((resolve, reject) => {
getStory = storyId =>
new Promise((resolve, reject) => {
pivotal.getStory(storyId, (err, story) => {
if (err) {
return reject(err);
}
resolve(story);
});
});
};

// Execute the script to find the pivotal stories id in order to associate the right url in the changelog
replacePivotalStories()
// Execute the script to find the pivotal stories and jira ticket id in order to associate
// the right url in the changelog
addTasksUrls()
.then(() => console.log("done"))
.catch(ex => console.log(ex));
60 changes: 60 additions & 0 deletions scripts/ts/common/ticket/jira/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Either, toError } from "fp-ts/lib/Either";
import { TaskEither, tryCatch } from "fp-ts/lib/TaskEither";
import { Errors } from "io-ts";
import fetch from "node-fetch";
import { RemoteJiraTicket } from "./types";

const jiraOrgBaseUrl = "https://pagopa.atlassian.net/rest/api/3/issue/";
export const jiraTicketBaseUrl = "https://pagopa.atlassian.net/browse/";
const username = process.env.JIRA_USERNAME;
const password = process.env.JIRA_PASSWORD;

/**
* Networking code to retrieve the remote representation for the JiraTicket
* @param id
*/
const retrieveRawJiraTicket = async (id: string): Promise<unknown> => {
const url = new URL(id, jiraOrgBaseUrl);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization:
"Basic " + Buffer.from(`${username}:${password}`).toString("base64")
}
});
if (res.status !== 200) {
throw new Error(
`Response status ${res.status} ${res.statusText} for ${id}`
);
}
return await res.json();
};

const retrieveRawJiraTicketTask = (id: string): TaskEither<Error, unknown> =>
tryCatch(() => retrieveRawJiraTicket(id), toError);

/**
* Ensure that the remote payload has the required fields
* @param payload
*/
const decodeRemoteJiraTicket = (
payload: any
): Either<Errors, RemoteJiraTicket> => RemoteJiraTicket.decode(payload);

const getJiraTicket = async (
jiraId: string
): Promise<Either<Errors | Error, RemoteJiraTicket>> =>
(
await retrieveRawJiraTicketTask(jiraId)
.mapLeft<Errors | Error>(e => e)
.run()
).chain(decodeRemoteJiraTicket);

/**
* Retrieve {@link RemoteJiraTicket} using jiraIds as input
* @param jiraIds
*/
export const getJiraTickets = async (
jiraIds: ReadonlyArray<string>
): Promise<ReadonlyArray<Either<Errors | Error, RemoteJiraTicket>>> =>
await Promise.all(jiraIds.map(getJiraTicket));
53 changes: 53 additions & 0 deletions scripts/ts/common/ticket/jira/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as t from "io-ts";

const JiraIssueType = t.keyof({
Epic: null,
Story: null,
Task: null,
Sottotask: null,
Bug: null
});

export type JiraIssueType = t.TypeOf<typeof JiraIssueType>;

const IssueType = t.interface({
name: JiraIssueType,
subtask: t.boolean
});

const Project = t.interface({
id: t.string,
key: t.string,
name: t.string
});

const FieldsR = t.interface({
issuetype: IssueType,
project: Project,
labels: t.array(t.string),
summary: t.string
});

const FieldsP = t.partial({
parent: t.interface({
key: t.string,
fields: t.interface({
summary: t.string,
issuetype: IssueType
})
})
});

const Fields = t.intersection([FieldsR, FieldsP], "Fields");

/**
* The required fields from the remote response.
* Not all the fields are used, see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-get
* for more information
*/
export const RemoteJiraTicket = t.interface({
key: t.string,
fields: Fields
});

export type RemoteJiraTicket = t.TypeOf<typeof RemoteJiraTicket>;
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// TODO: create types for pivotaljs

export type StoryType = "feature" | "bug" | "chore" | "release";
export type StoryCurrentState =
import { IUnitTag } from "italia-ts-commons/lib/units";

export type PivotalStoryType = "feature" | "bug" | "chore" | "release";
export type PivotalStoryCurrentState =
| "accepted"
| "delivered"
| "finished"
Expand All @@ -11,7 +13,7 @@ export type StoryCurrentState =
| "unstarted"
| "unscheduled";

export type Label = {
export type PivotalLabel = {
id: number;
project_id: number;
kind: string;
Expand All @@ -21,15 +23,17 @@ export type Label = {
updated_at: string;
};

export type Story = {
export type PivotalId = string & IUnitTag<"PivotalId">;

export type PivotalStory = {
id: string;
story_type: StoryType;
story_type: PivotalStoryType;
created_at: string;
updated_at: string;
estimate: number;
name: string;
current_state: StoryCurrentState;
current_state: PivotalStoryCurrentState;
url: string;
project_id: number;
labels: ReadonlyArray<Label>;
labels: ReadonlyArray<PivotalLabel>;
};
Loading

0 comments on commit ff7601a

Please sign in to comment.