From d139a4c9c1a77af0c932f62be6b48089fc7a43aa Mon Sep 17 00:00:00 2001 From: Matt Kuritz Date: Sat, 27 Jun 2020 17:42:17 -0400 Subject: [PATCH 1/5] add pull request functionality --- README.md | 69 +++++++++++++++++++++++++++++++++++------- index.js | 31 +++++++++++++------ src/clubhouse.js | 33 ++++++++++++++++++-- test/clubhouse.test.js | 13 ++++++-- 4 files changed, 120 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index cc1955c..dc509c6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ A github action to update and transition the workflow state of clubhouse stories ## Usage -Currently the action supports updating stories listed in a github release. +Currently the action supports updating stories listed in a github release, or +pull request title. ### Releases @@ -26,16 +27,16 @@ jobs: update-clubhouse: runs-on: ubuntu-latest steps: - - uses: farmersdog/clubhouse-workflow-action@v1 - with: - # Required. Auth token to use the clubhouse api for your workspace. - clubhouseToken: ${{ secrets.CLUBHOUSE_TOKEN }} - # Optional. The clubhouse workflow state released stories should be in. - # default: 'Completed'. - endStateName: Completed - # Optional. Whether to update story descriptions with a link to the release. - # default: false. - addReleaseInfo: false + - uses: farmersdog/clubhouse-workflow-action@v1 + with: + # Required. Auth token to use the clubhouse api for your workspace. + clubhouseToken: ${{ secrets.CLUBHOUSE_TOKEN }} + # Optional. The clubhouse workflow state released stories should be in. + # default: 'Completed'. + endStateName: Completed + # Optional. Whether to update story descriptions with a link to the release. + # default: false. + addReleaseInfo: false ``` The only requirement for identifying stories in a release is that the id is prepended with `ch`. Any surrounding brackets, or no brackets, will all work. @@ -60,3 +61,49 @@ If `addReleaseInfo: true` is set the action will update the story with a link ba https://github.com/org/repo/releases/tag/v1.0.0 ``` The action will only add `### Release Info` to a story if it's not already present. In some case a story is released more than once it will have a link to the first release. + +### Pull Requests + +Clubhouse [natively supports](https://help.clubhouse.io/hc/en-us/articles/208139833-Configuring-The-Clubhouse-GitHub-Event-Handlers) transitioning stories when a PR is opened or merged. This action can help with the in-between cases, like if a specific label is added to the PR. + +```yaml +name: Move Stories to Testing +on: + pull_request: + types: ['labeled'] + +jobs: + update-clubhouse: + runs-on: ubuntu-latest + if: github.event.label.name == 'Ready for QA' + steps: + - uses: farmersdog/clubhouse-workflow-action@v1 + with: + # Required. Auth token to use the clubhouse api for your workspace. + clubhouseToken: ${{ secrets.CLUBHOUSE_TOKEN }} + # Optional. The clubhouse workflow state released stories should be in. + # default: 'Completed'. + endStateName: In Testing +``` + +Or alternatively, you can run the action in response to a review request. + +```yaml +name: Move Stories to Testing +on: + pull_request: + types: ['review_requested'] + +jobs: + update-clubhouse: + runs-on: ubuntu-latest + if: contains(github.event.pull_request.requested_teams.*.name, 'QA') + steps: + - uses: farmersdog/clubhouse-workflow-action@v1 + with: + # Required. Auth token to use the clubhouse api for your workspace. + clubhouseToken: ${{ secrets.CLUBHOUSE_TOKEN }} + # Optional. The clubhouse workflow state released stories should be in. + # default: 'Completed'. + endStateName: In Testing +``` diff --git a/index.js b/index.js index 2a6e3bc..6a7b43a 100644 --- a/index.js +++ b/index.js @@ -5,16 +5,27 @@ const ch = require('./src/clubhouse'); async function run() { try { - const { body, html_url } = github.context.payload.release; - const addReleaseInfo = (core.getInput('addReleaseInfo') === 'true'); - const releasedStories = await ch.releaseStories( - body, - core.getInput('endStateName'), - html_url, - addReleaseInfo - ); - core.setOutput(releasedStories); - console.log(`Updated Stories: \n \n${releasedStories.join(' \n')}`); + let updatedStories; + if (github.context.eventName === "release") { + const { body, html_url } = github.context.payload.release; + const addReleaseInfo = (core.getInput('addReleaseInfo') === 'true'); + updatedStories = await ch.releaseStories( + body, + core.getInput('endStateName'), + html_url, + addReleaseInfo + ); + } else if (github.context.eventName === "pull_request") { + const { title } = github.context.payload.pull_request; + updatedStories = await ch.transitionStories( + title, + core.getInput('endStateName') + ); + } else { + throw new Error("Invalid event type"); + } + core.setOutput(updatedStories); + console.log(`Updated Stories: \n \n${updatedStories.join(' \n')}`); } catch (error) { core.setFailed(error.message); diff --git a/src/clubhouse.js b/src/clubhouse.js index e8d190c..4aa65a3 100644 --- a/src/clubhouse.js +++ b/src/clubhouse.js @@ -203,7 +203,7 @@ async function releaseStories( console.warn('No clubhouse stories were found in the release.'); return []; } - const stories = await addDetailstoStories(storyIds, releaseUrl); + const stories = await addDetailstoStories(storyIds); const storiesWithUpdatedDescriptions = updateDescriptionsMaybe( stories, releaseUrl, @@ -219,6 +219,34 @@ async function releaseStories( return updatedStoryNames; } +/** + * Updates all clubhouse stories found in given content. + * + * @param {string} content - a string that might have clubhouse story IDs. + * @param {string} endStateName - Desired workflow state for stories. + * @return {Promise} - Names of the stories that were updated + */ + +async function transitionStories( + content, + endStateName +) { + const storyIds = extractStoryIds(content); + if (storyIds === null) { + console.warn('No clubhouse stories were found.'); + return []; + } + const stories = await addDetailstoStories(storyIds); + const workflows = await client.listWorkflows(); + const storiesWithEndStateIds = addEndStateIds( + stories, + workflows, + endStateName + ); + const updatedStoryNames = await updateStories(storiesWithEndStateIds); + return updatedStoryNames; +} + module.exports = { client, extractStoryIds, @@ -230,5 +258,6 @@ module.exports = { addEndStateIds, updateStory, updateStories, - releaseStories + releaseStories, + transitionStories }; diff --git a/test/clubhouse.test.js b/test/clubhouse.test.js index 55f5eaa..4599a35 100644 --- a/test/clubhouse.test.js +++ b/test/clubhouse.test.js @@ -30,6 +30,7 @@ other bugch015 `; const release2 = '7895 [94536] (98453) #89'; const release3 = 'tchotchke ch-thing chi789'; + const prTitle = 'Re-writing the app in another language [ch1919]'; const releaseUrl = 'https://github.com/org/repo/releases/14'; const stories = [ { @@ -112,15 +113,16 @@ other bugch015 const expectedIds1 = ['4287', '890', '8576', '3', '015']; const expectedIds2 = null; const expectedIds3 = null; + const expectedIdsPR = ['1919']; it('should find all story ids in well formatted release', function () { const storyIds = ch.extractStoryIds(release0); - assert.deepEqual(storyIds, expectedIds0); + assert.deepStrictEqual(storyIds, expectedIds0); }); it('should find all story ids in poorly formatted release', function () { const storyIds = ch.extractStoryIds(release1); - assert.deepEqual(storyIds, expectedIds1); + assert.deepStrictEqual(storyIds, expectedIds1); }); it('should not match plain number strings', function () { @@ -132,6 +134,11 @@ other bugch015 const storyIds = ch.extractStoryIds(release3); assert.strictEqual(storyIds, expectedIds3); }); + + it('should find 1 story id in PR Title', function () { + const storyIds = ch.extractStoryIds(prTitle); + assert.deepStrictEqual(storyIds, expectedIdsPR); + }); }); describe('adding details to stories', function () { @@ -220,7 +227,7 @@ https://github.com/org/repo/releases/14 releaseUrl, false ); - assert.deepEqual(stories, newStories); + assert.deepStrictEqual(stories, newStories); }); }); From 9f19564741230fc39c9839470b45d4dd57aef7b7 Mon Sep 17 00:00:00 2001 From: Matt Kuritz Date: Sat, 27 Jun 2020 19:22:56 -0400 Subject: [PATCH 2/5] cleanup vars --- index.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 6a7b43a..9ca88ee 100644 --- a/index.js +++ b/index.js @@ -5,9 +5,10 @@ const ch = require('./src/clubhouse'); async function run() { try { + const { payload, eventName } = github.context; let updatedStories; - if (github.context.eventName === "release") { - const { body, html_url } = github.context.payload.release; + if (eventName === "release") { + const { body, html_url } = payload.release; const addReleaseInfo = (core.getInput('addReleaseInfo') === 'true'); updatedStories = await ch.releaseStories( body, @@ -15,8 +16,8 @@ async function run() { html_url, addReleaseInfo ); - } else if (github.context.eventName === "pull_request") { - const { title } = github.context.payload.pull_request; + } else if (eventName === "pull_request") { + const { title } = payload.pull_request; updatedStories = await ch.transitionStories( title, core.getInput('endStateName') From 1d5c8884d3b3da06e4652dcc748ebfc8c6403446 Mon Sep 17 00:00:00 2001 From: Matt Kuritz Date: Sat, 27 Jun 2020 19:23:23 -0400 Subject: [PATCH 3/5] package --- dist/index.js | 65 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/dist/index.js b/dist/index.js index ea96bd4..cea5837 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1943,16 +1943,28 @@ const ch = __webpack_require__(519); async function run() { try { - const { body, html_url } = github.context.payload.release; - const addReleaseInfo = (core.getInput('addReleaseInfo') === 'true'); - const releasedStories = await ch.releaseStories( - body, - core.getInput('endStateName'), - html_url, - addReleaseInfo - ); - core.setOutput(releasedStories); - console.log(`Updated Stories: \n \n${releasedStories.join(' \n')}`); + const { payload, eventName } = github.context; + let updatedStories; + if (eventName === "release") { + const { body, html_url } = payload.release; + const addReleaseInfo = (core.getInput('addReleaseInfo') === 'true'); + updatedStories = await ch.releaseStories( + body, + core.getInput('endStateName'), + html_url, + addReleaseInfo + ); + } else if (eventName === "pull_request") { + const { title } = payload.pull_request; + updatedStories = await ch.transitionStories( + title, + core.getInput('endStateName') + ); + } else { + throw new Error("Invalid event type"); + } + core.setOutput(updatedStories); + console.log(`Updated Stories: \n \n${updatedStories.join(' \n')}`); } catch (error) { core.setFailed(error.message); @@ -12536,7 +12548,7 @@ async function releaseStories( console.warn('No clubhouse stories were found in the release.'); return []; } - const stories = await addDetailstoStories(storyIds, releaseUrl); + const stories = await addDetailstoStories(storyIds); const storiesWithUpdatedDescriptions = updateDescriptionsMaybe( stories, releaseUrl, @@ -12552,6 +12564,34 @@ async function releaseStories( return updatedStoryNames; } +/** + * Updates all clubhouse stories found in given content. + * + * @param {string} content - a string that might have clubhouse story IDs. + * @param {string} endStateName - Desired workflow state for stories. + * @return {Promise} - Names of the stories that were updated + */ + +async function transitionStories( + content, + endStateName +) { + const storyIds = extractStoryIds(content); + if (storyIds === null) { + console.warn('No clubhouse stories were found.'); + return []; + } + const stories = await addDetailstoStories(storyIds); + const workflows = await client.listWorkflows(); + const storiesWithEndStateIds = addEndStateIds( + stories, + workflows, + endStateName + ); + const updatedStoryNames = await updateStories(storiesWithEndStateIds); + return updatedStoryNames; +} + module.exports = { client, extractStoryIds, @@ -12563,7 +12603,8 @@ module.exports = { addEndStateIds, updateStory, updateStories, - releaseStories + releaseStories, + transitionStories }; From e5c16de07ffaeb50bbb4b4d8fbc6672c664c2816 Mon Sep 17 00:00:00 2001 From: Matt Kuritz Date: Mon, 29 Jun 2020 11:27:25 -0400 Subject: [PATCH 4/5] get stories from branch, title, or body for PR --- index.js | 6 ++++-- src/clubhouse.js | 14 ++++++++------ test/clubhouse.test.js | 22 ++++++++++++++++++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 9ca88ee..35cc25e 100644 --- a/index.js +++ b/index.js @@ -17,9 +17,11 @@ async function run() { addReleaseInfo ); } else if (eventName === "pull_request") { - const { title } = payload.pull_request; + const { title, body } = payload.pull_request; + const { ref } = payload.pull_request.head; + const content = `${title} ${body} ${ref}`; updatedStories = await ch.transitionStories( - title, + content, core.getInput('endStateName') ); } else { diff --git a/src/clubhouse.js b/src/clubhouse.js index 4aa65a3..0b5e304 100644 --- a/src/clubhouse.js +++ b/src/clubhouse.js @@ -4,15 +4,17 @@ const clubhouseToken = process.env.INPUT_CLUBHOUSETOKEN; const client = Clubhouse.create(clubhouseToken); /** - * Finds all clubhouse story IDs in body of release object. + * Finds all clubhouse story IDs in some string content. * - * @param {string} releaseBody - The body field of a github release object. + * @param {string} content - content that may contain story IDs. * @return {Array} - Clubhouse story IDs 1-7 digit strings. */ -function extractStoryIds(releaseBody) { +function extractStoryIds(content) { const regex = /(?<=ch)\d{1,7}/g; - return releaseBody.match(regex); + const all = content.match(regex); + const unique = [...new Set(all)]; + return unique; } /** @@ -232,9 +234,9 @@ async function transitionStories( endStateName ) { const storyIds = extractStoryIds(content); - if (storyIds === null) { + if (storyIds.length === 0) { console.warn('No clubhouse stories were found.'); - return []; + return storyIds; } const stories = await addDetailstoStories(storyIds); const workflows = await client.listWorkflows(); diff --git a/test/clubhouse.test.js b/test/clubhouse.test.js index 4599a35..cf7880d 100644 --- a/test/clubhouse.test.js +++ b/test/clubhouse.test.js @@ -31,6 +31,8 @@ other bugch015 const release2 = '7895 [94536] (98453) #89'; const release3 = 'tchotchke ch-thing chi789'; const prTitle = 'Re-writing the app in another language [ch1919]'; + const branch = 'user/ch2189/something-important-maybe'; + const duplicates = 'Only one change [ch6754] ch6754 [ch6754]'; const releaseUrl = 'https://github.com/org/repo/releases/14'; const stories = [ { @@ -111,9 +113,11 @@ other bugch015 describe('story id extraction from release body', function () { const expectedIds0 = ['0002', '1', '12345', '987', '56789']; const expectedIds1 = ['4287', '890', '8576', '3', '015']; - const expectedIds2 = null; - const expectedIds3 = null; + const expectedIds2 = []; + const expectedIds3 = []; const expectedIdsPR = ['1919']; + const expectedIdsBranch = ['2189']; + const expectedIdsDups = ['6754']; it('should find all story ids in well formatted release', function () { const storyIds = ch.extractStoryIds(release0); @@ -127,18 +131,28 @@ other bugch015 it('should not match plain number strings', function () { const storyIds = ch.extractStoryIds(release2); - assert.strictEqual(storyIds, expectedIds2); + assert.deepStrictEqual(storyIds, expectedIds2); }); it('should not match other strings beginning in "ch"', function () { const storyIds = ch.extractStoryIds(release3); - assert.strictEqual(storyIds, expectedIds3); + assert.deepStrictEqual(storyIds, expectedIds3); }); it('should find 1 story id in PR Title', function () { const storyIds = ch.extractStoryIds(prTitle); assert.deepStrictEqual(storyIds, expectedIdsPR); }); + + it('should find 1 story id in branch name', function () { + const storyIds = ch.extractStoryIds(branch); + assert.deepStrictEqual(storyIds, expectedIdsBranch); + }); + + it('should find 1 story id from duplicates', function () { + const storyIds = ch.extractStoryIds(duplicates); + assert.deepStrictEqual(storyIds, expectedIdsDups); + }); }); describe('adding details to stories', function () { From 25e3228556f6c30074971d0ac31e581b395a0cbc Mon Sep 17 00:00:00 2001 From: Matt Kuritz Date: Mon, 29 Jun 2020 11:27:44 -0400 Subject: [PATCH 5/5] package --- dist/index.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/dist/index.js b/dist/index.js index cea5837..8f41c1a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1955,9 +1955,11 @@ async function run() { addReleaseInfo ); } else if (eventName === "pull_request") { - const { title } = payload.pull_request; + const { title, body } = payload.pull_request; + const { ref } = payload.pull_request.head; + const content = `${title} ${body} ${ref}`; updatedStories = await ch.transitionStories( - title, + content, core.getInput('endStateName') ); } else { @@ -12349,15 +12351,17 @@ const clubhouseToken = process.env.INPUT_CLUBHOUSETOKEN; const client = Clubhouse.create(clubhouseToken); /** - * Finds all clubhouse story IDs in body of release object. + * Finds all clubhouse story IDs in some string content. * - * @param {string} releaseBody - The body field of a github release object. + * @param {string} content - content that may contain story IDs. * @return {Array} - Clubhouse story IDs 1-7 digit strings. */ -function extractStoryIds(releaseBody) { +function extractStoryIds(content) { const regex = /(?<=ch)\d{1,7}/g; - return releaseBody.match(regex); + const all = content.match(regex); + const unique = [...new Set(all)]; + return unique; } /** @@ -12577,9 +12581,9 @@ async function transitionStories( endStateName ) { const storyIds = extractStoryIds(content); - if (storyIds === null) { + if (storyIds.length === 0) { console.warn('No clubhouse stories were found.'); - return []; + return storyIds; } const stories = await addDetailstoStories(storyIds); const workflows = await client.listWorkflows();