From ec5077636682e8813710b590a8363cb8e5141ccd Mon Sep 17 00:00:00 2001 From: Christian Bobach Date: Mon, 27 Nov 2023 10:53:46 +0100 Subject: [PATCH 1/2] Add retry possibility Will add retry-limit to list of inputs and make additional calls to the GraphQL API if it for some reason fails. --- README.md | 1 + __tests__/add-to-project.test.ts | 25 +++++++++++- action.yml | 3 ++ src/add-to-project.ts | 67 +++++++++++++++++++------------- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d637a1f4..e73d69a0 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ jobs: _See [Creating a PAT and adding it to your repository](#creating-a-pat-and-adding-it-to-your-repository) for more details_ - `labeled` **(optional)** is a comma-separated list of labels used to filter applicable issues. When this key is provided, an issue must have _one_ of the labels in the list to be added to the project. Omitting this key means that any issue will be added. - `label-operator` **(optional)** is the behavior of the labels filter, either `AND`, `OR` or `NOT` that controls if the issue should be matched with `all` `labeled` input or any of them, default is `OR`. +- `retry-limit` **(optional)** is the number of retries done to add an item to a board, default is 0. ## Supported Events diff --git a/__tests__/add-to-project.test.ts b/__tests__/add-to-project.test.ts index 9c79f363..6d380e63 100644 --- a/__tests__/add-to-project.test.ts +++ b/__tests__/add-to-project.test.ts @@ -1,7 +1,7 @@ import * as core from '@actions/core' import * as github from '@actions/github' -import {addToProject, mustGetOwnerTypeQuery} from '../src/add-to-project' +import {addToProject, mustGetOwnerTypeQuery, withRetries} from '../src/add-to-project' describe('addToProject', () => { let outputs: Record @@ -800,6 +800,29 @@ describe('mustGetOwnerTypeQuery', () => { }) }) +describe('withRetries', () => { + test('should succeed on successful callback', async () => { + withRetries(0, 0, () => { + return Promise.resolve('some string') + }).then(data => { + expect(data).toBe('some string') + }) + }) + + test('should fail when reaching retry limit', async () => { + let invocations = 0 + + expect(() => + withRetries(0, 3, () => { + invocations = invocations + 1 + throw new Error('some error') + }), + ).toThrow('some error') + + expect(invocations).toEqual(4) + }) +}) + function mockGetInput(mocks: Record): jest.SpyInstance { const mock = (key: string) => mocks[key] ?? '' return jest.spyOn(core, 'getInput').mockImplementation(mock) diff --git a/action.yml b/action.yml index 1ff0969e..eafe2836 100644 --- a/action.yml +++ b/action.yml @@ -16,6 +16,9 @@ inputs: label-operator: required: false description: The behavior of the labels filter, AND to match all labels, OR to match any label, NOT to exclude any listed label (default is OR) + retry-limit: + required: false + description: The number of retries used to add an item to a project (default is 0) outputs: itemId: description: The ID of the item that was added to the project diff --git a/src/add-to-project.ts b/src/add-to-project.ts index 0dc1bfde..45438ce2 100644 --- a/src/add-to-project.ts +++ b/src/add-to-project.ts @@ -43,6 +43,8 @@ export async function addToProject(): Promise { .map(l => l.trim().toLowerCase()) .filter(l => l.length > 0) ?? [] const labelOperator = core.getInput('label-operator').trim().toLocaleLowerCase() + const inputRetryLimit = core.getInput('retry-limit') + const retryLimit = inputRetryLimit ? Number(inputRetryLimit) : 0 const octokit = github.getOctokit(ghToken) @@ -117,47 +119,60 @@ export async function addToProject(): Promise { if (issueOwnerName === projectOwnerName) { core.info('Creating project item') - const addResp = await octokit.graphql( - `mutation addIssueToProject($input: AddProjectV2ItemByIdInput!) { - addProjectV2ItemById(input: $input) { - item { - id + const addResp = await withRetries(0, retryLimit, () => + octokit.graphql( + `mutation addIssueToProject($input: AddProjectV2ItemByIdInput!) { + addProjectV2ItemById(input: $input) { + item { + id + } } - } - }`, - { - input: { - projectId, - contentId, + }`, + { + input: { + projectId, + contentId, + }, }, - }, + ), ) core.setOutput('itemId', addResp.addProjectV2ItemById.item.id) } else { core.info('Creating draft issue in project') - const addResp = await octokit.graphql( - `mutation addDraftIssueToProject($projectId: ID!, $title: String!) { - addProjectV2DraftIssue(input: { - projectId: $projectId, - title: $title - }) { - projectItem { - id + const addResp = await withRetries(0, retryLimit, () => + octokit.graphql( + `mutation addDraftIssueToProject($projectId: ID!, $title: String!) { + addProjectV2DraftIssue(input: { + projectId: $projectId, + title: $title + }) { + projectItem { + id + } } - } - }`, - { - projectId, - title: issue?.html_url, - }, + }`, + { + projectId, + title: issue?.html_url, + }, + ), ) core.setOutput('itemId', addResp.addProjectV2DraftIssue.projectItem.id) } } +export function withRetries(retryNumber: number, retryLimit: number, callback: () => Promise): Promise { + try { + return callback() + } catch (err) { + if (retryNumber < retryLimit) return withRetries(retryNumber + 1, retryLimit, callback) + else throw err + } +} + export function mustGetOwnerTypeQuery(ownerType?: string): 'organization' | 'user' { const ownerTypeQuery = ownerType === 'orgs' ? 'organization' : ownerType === 'users' ? 'user' : null From 6e668bda6f081b8ca3b12816aa6756aaf519ba3f Mon Sep 17 00:00:00 2001 From: Christian Bobach Date: Mon, 27 Nov 2023 11:05:55 +0100 Subject: [PATCH 2/2] Specify that a fork is necessary to contribute back --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e73d69a0..82f5956e 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ Use the [Add To GitHub Projects](https://github.com/marketplace/actions/add-to-g ## Development To get started contributing to this project, clone it and install dependencies. +If you have made any changes that you want to contribute back to the project fork this repository and create a PR on that fork. Note that this action runs in Node.js 16.x, so we recommend using that version of Node (see "engines" in this action's package.json for details).