diff --git a/README.md b/README.md index e692282..3059e3a 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ steps: ### Inputs -Want to use Action logic to determine who to assign the issue to? You can pass an input containing the following options: +Want to use Action logic to determine who to assign the issue to, to assign a milestone or to update an existing issue with the same title? You can pass an input containing the following options: ```yaml steps: @@ -84,8 +84,11 @@ steps: with: assignees: JasonEtco, octocat milestone: 1 + update_existing: true ``` +The `assignees` and `milestone` speak for themselves, the `update_existing` param can be passed and set to `true` when you want an existing open issue with the **exact same title** when it exists. + ### Outputs If you need the number or URL of the issue that was created for another Action, you can use the `number` or `url` outputs, respectively. For example: diff --git a/action.yml b/action.yml index 4a8e93c..22cf066 100644 --- a/action.yml +++ b/action.yml @@ -9,12 +9,17 @@ branding: inputs: assignees: description: GitHub handle of the user(s) to assign the issue (comma-separated) + required: false milestone: description: Number of the milestone to assign the issue to required: false filename: description: The name of the file to use as the issue template default: .github/ISSUE_TEMPLATE.md + required: false + update_existing: + description: Update an open existing issue with the same title if it exists + required: false outputs: number: description: Number of the issue that was created diff --git a/index.js b/index.js index 9dc4217..1c5235a 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ function listToArray (list) { Toolkit.run(async tools => { const template = tools.inputs.filename || '.github/ISSUE_TEMPLATE.md' const assignees = tools.inputs.assignees + const updateExisting = Boolean(tools.inputs.update_existing) const env = nunjucks.configure({ autoescape: false }) env.addFilter('date', dateFilter) @@ -33,11 +34,39 @@ Toolkit.run(async tools => { body: env.renderString(body, templateVariables), title: env.renderString(attributes.title, templateVariables) } - tools.log.debug('Templates compiled', templated) - tools.log.info(`Creating new issue ${templated.title}`) + + if (updateExisting) { + let existingIssue + tools.log.info(`Fetching issues with title "${templated.title}"`) + try { + const existingIssues = await tools.github.search.issuesAndPullRequests({ + q: `is:open is:issue repo:${process.env.GITHUB_REPOSITORY} in:title ${templated.title}`, + order: 'created' + }) + existingIssue = existingIssues.data.items.find(issue => issue.title === templated.title) + } catch (err) { + tools.exit.failure(err) + } + if (existingIssue) { + try { + const issue = await tools.github.issues.update({ + ...tools.context.repo, + issue_number: existingIssue.number, + body: templated.body + }) + core.setOutput('number', String(issue.data.number)) + core.setOutput('url', issue.data.html_url) + tools.exit.success(`Updated issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`) + } catch (err) { + tools.exit.failure(err) + } + } + tools.log.info('No existing issue found to update') + } // Create the new issue + tools.log.info(`Creating new issue ${templated.title}`) try { const issue = await tools.github.issues.create({ ...tools.context.repo, diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap index e93cbdc..7537bc3 100644 --- a/tests/__snapshots__/index.test.js.snap +++ b/tests/__snapshots__/index.test.js.snap @@ -34,6 +34,19 @@ Array [ ] `; +exports[`create-an-issue creates a new issue when updating existing issues is enabled but no issues with the same title exist 1`] = ` +Object { + "assignees": Array [ + "octocat", + "JasonEtco", + ], + "body": "Goodbye!", + "labels": Array [], + "milestone": "1", + "title": "Hello!", +} +`; + exports[`create-an-issue creates a new issue with a milestone passed by input 1`] = ` Object { "assignees": Array [ @@ -179,3 +192,16 @@ Array [ ], ] `; + +exports[`create-an-issue updates an existing issue with the same title 1`] = ` +Object { + "assignees": Array [ + "octocat", + "JasonEtco", + ], + "body": "Goodbye!", + "labels": Array [], + "milestone": "1", + "title": "Hello!", +} +`; diff --git a/tests/index.test.js b/tests/index.test.js index 9725ffa..7e9745e 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -117,6 +117,57 @@ describe('create-an-issue', () => { expect(tools.log.success).toHaveBeenCalled() }) + it('creates a new issue when updating existing issues is enabled but no issues with the same title exist', async () => { + nock.cleanAll() + nock('https://api.github.com') + .get(/\/search\/issues.*/).reply(200, { + items: [] + }) + .post(/\/repos\/.*\/.*\/issues/).reply(200, (_, body) => { + params = body + return { + title: body.title, + number: 1, + html_url: 'www' + } + }) + + process.env.INPUT_UPDATE_EXISTING = 'true' + + await actionFn(tools) + expect(params).toMatchSnapshot() + expect(tools.log.info).toHaveBeenCalledWith('No existing issue found to update') + expect(tools.log.success).toHaveBeenCalled() + }) + + it('updates an existing issue with the same title', async () => { + nock.cleanAll() + nock('https://api.github.com') + .get(/\/search\/issues.*/).reply(200, { + items: [{ number: 1, title: 'Hello!' }] + }) + .patch(/\/repos\/.*\/.*\/issues\/.*/).reply(200, {}) + process.env.INPUT_UPDATE_EXISTING = 'true' + + await actionFn(tools) + expect(params).toMatchSnapshot() + expect(tools.exit.success).toHaveBeenCalled() + }) + + it('exits when updating an issue fails', async () => { + nock.cleanAll() + nock('https://api.github.com') + .get(/\/search\/issues.*/).reply(200, { + items: [{ number: 1, title: 'Hello!' }] + }) + .patch(/\/repos\/.*\/.*\/issues\/.*/).reply(500, { + message: 'Updating issue failed' + }) + + await actionFn(tools) + expect(tools.exit.failure).toHaveBeenCalled() + }) + it('logs a helpful error if creating an issue throws an error', async () => { nock.cleanAll() nock('https://api.github.com')