Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
lib
yarn-error.log
coverage
coverage
.env*
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"@types/node": "^14.0.12",
"clubhouse-lib": "^0.10.0",
"commander": "^5.1.0",
"got": "^11.3.0",
"got": "^11.8.0",
"md-to-adf": "^0.6.4",
"mdast-builder": "^1.1.1",
"mdast-util-to-string": "^1.1.0",
"node-emoji": "^1.10.0",
Expand Down
41 changes: 41 additions & 0 deletions src/cli/jira-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Client } from "../jira-client/Client"
// import { Issue } from "../jira-client/Issue"
// import { IssueType } from "../jira-client/IssueType";
// import { Board } from "../jira-client/Project";
// import {inspect} from "util"
import { Client as ImportClient } from "../import-adapters/jira/Client"

const client = new Client({
host: process.env.JIRA_HOST || "",
projectKey: "CURR",
authentication: {
basic: {
email: process.env.JIRA_EMAIL || "",
password: process.env.JIRA_API_TOKEN || ""
}
}
});


(async () => {
// const board = new Board(client);
// console.log(await board.getList())

// const issueType = new IssueType(client)
// console.log(await issueType.getList())


// const issue = new Issue(client)
// console.log(await issue.getList("type = Epic"))

// const issue = new Issue(client)
// console.log(inspect(await issue.getCreateMetadata(), {depth: 100}))
// await issue.create({
// title: "Test Epic",
// description: "* do you make\n* appropriate bullets\n* for formatting",
// issueTypeId: "10032",
// projectId: "10015"
// })
const importClient = new ImportClient(client)
console.log(await importClient.listEpics())
})()
89 changes: 49 additions & 40 deletions src/cli/push.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,58 @@
import { Command } from "commander";
import fs from "fs";
// import fs from "fs";
import chalk from "chalk";
import emoji from "node-emoji";
import StoryParser from "../markdown/StoryParser";
import Client from "../import-adapters/clubhouse/Client";
import { Client as ClubhouseClient } from "../import-adapters/clubhouse/Client";
import { Client as JiraClient } from "../import-adapters/jira/Client"

import configuration from "../configuration";
import normalizeStorySet from "../markdown/normalizeStorySet";
import StorySetImport from "../import-adapters/clubhouse/StorySetImport";
import { StorySetImport as ClubhouseStorySetImport } from "../import-adapters/clubhouse/StorySetImport";
import { StorySetImport as JiraStorySetImport } from "../import-adapters/jira/StorySetImport";

type PrecheckError = {
message: string;
};
// type PrecheckError = {
// message: string;
// };

const fileExists = (markdownFile): PrecheckError | undefined => {
if (!fs.existsSync(markdownFile)) {
return {
message: "File not found.",
};
}
return undefined;
};
// const fileExists = (markdownFile): PrecheckError | undefined => {
// if (!fs.existsSync(markdownFile)) {
// return {
// message: "File not found.",
// };
// }
// return undefined;
// };

const clubhouseTokenSet = (): PrecheckError | undefined => {
if (configuration.clubhouse.apiToken?.trim() === "") {
return {
message: "Clubhouse Token not set.",
};
}
return undefined;
};
// const clubhouseTokenSet = (): PrecheckError | undefined => {
// if (configuration.clubhouse.apiToken?.trim() === "") {
// return {
// message: "Clubhouse Token not set.",
// };
// }
// return undefined;
// };

const clubhouseProjectSet = (): PrecheckError | undefined => {
if (!configuration.clubhouse.projectId) {
return {
message: "Clubhouse Project ID not set.",
};
}
return undefined;
};
// const clubhouseProjectSet = (): PrecheckError | undefined => {
// if (!configuration.clubhouse.projectId) {
// return {
// message: "Clubhouse Project ID not set.",
// };
// }
// return undefined;
// };

const pushPrechecks = [fileExists, clubhouseTokenSet, clubhouseProjectSet];
// const pushPrechecks = [fileExists, clubhouseTokenSet, clubhouseProjectSet];

const processPush = async (markdownFile: string) => {
let errors: string[] = [];
errors = pushPrechecks.reduce((list: string[], func) => {
const { message } = func(markdownFile) || {};
if (message) {
list = [...list, message];
}
return list;
}, errors);
// errors = pushPrechecks.reduce((list: string[], func) => {
// const { message } = func(markdownFile) || {};
// if (message) {
// list = [...list, message];
// }
// return list;
// }, errors);

if (errors.length > 0) {
errors.forEach((error) => {
Expand All @@ -65,8 +67,15 @@ const processPush = async (markdownFile: string) => {
if (storySet.epicMap.size > 0) {
console.log(chalk.yellow(`${storySet.epicMap.size} epic(s) found`));
}
const setImport = new StorySetImport(storySet, Client.factory(), configuration.clubhouse.projectId);
await setImport.create();
if(configuration.jira.projectKey === "") {
const setImport = new ClubhouseStorySetImport(storySet, ClubhouseClient.factory(), configuration.clubhouse.projectId);
await setImport.create();
}
else {
const setImport = new JiraStorySetImport(storySet, JiraClient.factory(), configuration.jira.projectKey || "" )
await setImport.create();
}

} else {
chalk.red("No stories found.");
}
Expand Down
6 changes: 6 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ const config = {
apiToken: process.env.CLUBHOUSE_TOKEN || "",
projectId: parseInt(process.env.CLUBHOUSE_PROJECT_ID || "", 10),
},
jira: {
projectKey: process.env.JIRA_PROJECT_KEY,
host: process.env.JIRA_HOST || "",
email: process.env.JIRA_EMAIL || "",
apiToken: process.env.JIRA_API_TOKEN || ""
}
};

export default config;
2 changes: 1 addition & 1 deletion src/import-adapters/clubhouse/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type ClubhouseConfiguration = {
projectId?: number;
};

class Client {
export class Client {
private client: ClubhouseClient<any, any>;

constructor(config: ClubhouseConfiguration) {
Expand Down
2 changes: 1 addition & 1 deletion src/import-adapters/clubhouse/StorySetImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import StorySet from "../../markdown/StorySet";
import Story from "../../markdown/Story";
import { StoryChange } from "clubhouse-lib";

class StorySetImport {
export class StorySetImport {
storySet: StorySet;
projectId: number;
private client: Client;
Expand Down
49 changes: 49 additions & 0 deletions src/import-adapters/jira/Client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Client as JiraClient } from "../../jira-client/Client";
import { Issue } from "../../jira-client/Issue"
import { IssueCreationPayload } from "../../jira-client/IssueCreationPayload"
import { IssueResponse } from "../../jira-client/IssueResponse";
import configuration from "../../configuration";

export class Client {
private client: Issue

constructor(client: JiraClient) {
this.client = new Issue(client)
}

createEpic(payload: IssueCreationPayload) : Promise<IssueResponse> {
return this.createStory({
...payload,
issueTypeName: "Epic"
})
}

async listEpics() {
return this.client.getEpics();
}

createStory(payload: IssueCreationPayload) : Promise<IssueResponse> {
return this.client.create(payload).then(data => data as IssueResponse).catch(err => {
console.log(err.response.body)
throw err
});
}

static factory(): Client {
return new Client(new JiraClient(this.makeConfiguration()));
}

private static makeConfiguration() {
const {host, email, apiToken, projectKey} = configuration.jira
return {
host,
projectKey: projectKey || "",
authentication: {
basic: {
email,
password: apiToken
}
}
}
}
}
65 changes: 65 additions & 0 deletions src/import-adapters/jira/StorySetImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Client } from "./Client";
import StorySet from "../../markdown/StorySet";
import { IssueCreationPayload } from "../../jira-client/IssueCreationPayload";
import Story from "../../markdown/Story"
// import Epic from "../../markdown/Epic";

export class StorySetImport {
storySet: StorySet;
projectKey: string;
private client: Client;

constructor(storySet: StorySet, client: Client, projectKey: string) {
this.storySet = storySet;
this.client = client;
this.projectKey = projectKey;
}

async create() {
const jiraList = await this.client.listEpics();

// build a list of persisted epics
const epicNameIdMap = new Map<string, string>();
jiraList.issues.forEach((jiraEpic) => {
epicNameIdMap.set(jiraEpic.fields.title, jiraEpic.key);
});


const { epicMap, stories } = this.storySet;

// create non-existent epics where necessary
for (const [_, epic] of epicMap) {
if (epic.name) {
const persistedEpicId = epicNameIdMap.get(epic.name);
if (!persistedEpicId) {
const persistedEpic = await this.client.createEpic({
title: epic.name,
description: "",
projectKey: this.projectKey
});
epicNameIdMap.set(epic.name, persistedEpic.key);
}
}
}

// create stories
for (const story of stories) {
const storyChange = this.makeStoryChange(story);
if (story.epicName) {
storyChange.parentId = epicNameIdMap.get(story.epicName);
}
await this.client.createStory(storyChange);
}
}

private makeStoryChange(story: Story): IssueCreationPayload {
return {
title: story.name || "Imported Story",
description: story.description,
issueTypeName: "Story",
projectKey: this.projectKey,
};
}
}

export default StorySetImport;
40 changes: 40 additions & 0 deletions src/jira-client/Client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Configuration } from "./Configuration"
import Got from "got"


export class Client {
private configuration: Configuration
private httpClient: typeof Got

constructor(config: Configuration) {
this.configuration = config
this.httpClient = this.makeHttpClient()
}

get(path, params = {}) {
return this.httpClient.get(path, { searchParams: params })
}

post(path, data) {
return this.httpClient.post(path, {json: { ...data, Authorization: this.buildAuthString()}}).json()
}

private makeHttpClient() : typeof Got {

return Got.extend({
prefixUrl: `${this.configuration.host}/rest/api/3/`,
headers: {
"Content-Type": "application/json",
"Authorization": this.buildAuthString(),
"accept": "application/json"
},
responseType: 'json'
});
}

private buildAuthString() {
const {email, password} = this.configuration.authentication.basic
const authString = Buffer.from(`${email}:${password}`).toString('base64')
return `Basic ${authString}`
}
}
9 changes: 9 additions & 0 deletions src/jira-client/ClientObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Client } from "./Client";

export abstract class ClientObject {
protected client: Client

constructor(client: Client) {
this.client = client
}
}
10 changes: 10 additions & 0 deletions src/jira-client/Configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type Configuration = {
host: string,
projectKey: string,
authentication: {
basic: {
email: string,
password: string
}
}
}
Loading