diff --git a/README.md b/README.md index 6575c84..785e44b 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ not the original GitHub projects. [![build-test](https://github.com/actions/add-to-project/actions/workflows/test.yml/badge.svg)](https://github.com/actions/add-to-project/actions/workflows/test.yml) -🚨 **This action is in beta, however the API is stable. Some breaking changes might occur between versions, but it is not likely to break as long as you use a specific SHA or version number** 🚨 +> **NOTE:** This Action (currently) only supports auto-adding Issues/Pull Requests to a Project which lives in the same organization as your target Repository. -> **NOTE:** This Action (currently) only supports auto-adding Issues to a Project which lives in the same organization as your target Repository. +> **NOTE:** This action no longer uses the deprecated ProjectNext API. If you are looking for the old version of that action, please check `project_next` branch. ## Usage @@ -80,11 +80,10 @@ jobs: ## Inputs -- `project-url` **(required)** is the URL of the GitHub project to add issues to. +- `project-url` **(required)** is the URL of the GitHub project to add issues to. _eg: `https://github.com/orgs|users//projects/`_ - `github-token` **(required)** is a [personal access - token](https://github.com/settings/tokens/new) with the `repo`, `write:org` and - `read:org` scopes. + token](https://github.com/settings/tokens/new) with the `project` scope. _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` that controls if the issue should be matched with `all` `labeled` input or any of them, default is `OR`. @@ -138,10 +137,10 @@ jobs: - create a new [personal access token](https://github.com/settings/tokens/new) with `repo`, `write:org` and - `read:org` scopes + `read:org` scopes _See [Creating a personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) for more information_ -- add the newly created PAT as a repository secret, this secret will be referenced by the [github-token input](#github-token) +- add the newly created PAT as a repository secret, this secret will be referenced by the [github-token input](#github-token) _See [Encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) for more information_ ## Development diff --git a/__tests__/add-to-project.test.ts b/__tests__/add-to-project.test.ts index 8f0f8ed..918056f 100644 --- a/__tests__/add-to-project.test.ts +++ b/__tests__/add-to-project.test.ts @@ -1,5 +1,6 @@ import * as core from '@actions/core' import * as github from '@actions/github' + import {addToProject, mustGetOwnerTypeQuery} from '../src/add-to-project' describe('addToProject', () => { @@ -29,18 +30,18 @@ describe('addToProject', () => { test: /getProject/, return: { organization: { - projectNext: { - id: 'project-next-id' + projectV2: { + id: 'project-id' } } } }, { - test: /addProjectNextItem/, + test: /addProjectV2ItemById/, return: { - addProjectNextItem: { - projectNextItem: { - id: 'project-next-item-id' + addProjectV2ItemById: { + projectItem: { + id: 'project-item-id' } } } @@ -49,7 +50,7 @@ describe('addToProject', () => { await addToProject() - expect(outputs.itemId).toEqual('project-next-item-id') + expect(outputs.itemId).toEqual('project-item-id') }) test('adds matching issues with a label filter without label-operator', async () => { @@ -71,18 +72,18 @@ describe('addToProject', () => { test: /getProject/, return: { organization: { - projectNext: { - id: 'project-next-id' + projectV2: { + id: 'project-id' } } } }, { - test: /addProjectNextItem/, + test: /addProjectV2ItemById/, return: { - addProjectNextItem: { - projectNextItem: { - id: 'project-next-item-id' + addProjectV2ItemById: { + projectItem: { + id: 'project-item-id' } } } @@ -91,7 +92,7 @@ describe('addToProject', () => { await addToProject() - expect(outputs.itemId).toEqual('project-next-item-id') + expect(outputs.itemId).toEqual('project-item-id') }) test('adds matching pull-requests with a label filter without label-operator', async () => { @@ -114,18 +115,18 @@ describe('addToProject', () => { test: /getProject/, return: { organization: { - projectNext: { - id: 'project-next-id' + projectV2: { + id: 'project-id' } } } }, { - test: /addProjectNextItem/, + test: /addProjectV2ItemById/, return: { - addProjectNextItem: { - projectNextItem: { - id: 'project-next-item-id' + addProjectV2ItemById: { + projectItem: { + id: 'project-item-id' } } } @@ -134,7 +135,7 @@ describe('addToProject', () => { await addToProject() - expect(outputs.itemId).toEqual('project-next-item-id') + expect(outputs.itemId).toEqual('project-item-id') }) test('does not add un-matching issues with a label filter without label-operator', async () => { @@ -178,18 +179,18 @@ describe('addToProject', () => { test: /getProject/, return: { organization: { - projectNext: { - id: 'project-next-id' + projectV2: { + id: 'project-id' } } } }, { - test: /addProjectNextItem/, + test: /addProjectV2ItemById/, return: { - addProjectNextItem: { - projectNextItem: { - id: 'project-next-item-id' + addProjectV2ItemById: { + projectItem: { + id: 'project-item-id' } } } @@ -198,7 +199,7 @@ describe('addToProject', () => { await addToProject() - expect(outputs.itemId).toEqual('project-next-item-id') + expect(outputs.itemId).toEqual('project-item-id') }) test('does not add un-matching issues with labels filter with AND label-operator', async () => { @@ -242,18 +243,18 @@ describe('addToProject', () => { test: /getProject/, return: { organization: { - projectNext: { - id: 'project-next-id' + projectV2: { + id: 'project-id' } } } }, { - test: /addProjectNextItem/, + test: /addProjectV2ItemById/, return: { - addProjectNextItem: { - projectNextItem: { - id: 'project-next-item-id' + addProjectV2ItemById: { + projectItem: { + id: 'project-item-id' } } } @@ -266,7 +267,7 @@ describe('addToProject', () => { expect(gqlMock).toHaveBeenCalled() expect(infoSpy).not.toHaveBeenCalled() - expect(outputs.itemId).toEqual('project-next-item-id') + expect(outputs.itemId).toEqual('project-item-id') }) test('does not add un-matching issues with multiple label filters', async () => { @@ -312,18 +313,18 @@ describe('addToProject', () => { test: /getProject/, return: { organization: { - projectNext: { - id: 'project-next-id' + projectV2: { + id: 'project-id' } } } }, { - test: /addProjectNextItem/, + test: /addProjectV2ItemById/, return: { - addProjectNextItem: { - projectNextItem: { - id: 'project-next-item-id' + addProjectV2ItemById: { + projectItem: { + id: 'project-item-id' } } } @@ -336,7 +337,7 @@ describe('addToProject', () => { expect(gqlMock).toHaveBeenCalled() expect(infoSpy).not.toHaveBeenCalled() - expect(outputs.itemId).toEqual('project-next-item-id') + expect(outputs.itemId).toEqual('project-item-id') }) test(`throws an error when url isn't a valid project url`, async () => { @@ -402,18 +403,18 @@ describe('addToProject', () => { test: /getProject/, return: { organization: { - projectNext: { - id: 'project-next-id' + projectV2: { + id: 'project-id' } } } }, { - test: /addProjectNextItem/, + test: /addProjectV2ItemById/, return: { - addProjectNextItem: { - projectNextItem: { - id: 'project-next-item-id' + addProjectV2ItemById: { + projectItem: { + id: 'project-item-id' } } } @@ -424,7 +425,8 @@ describe('addToProject', () => { expect(gqlMock).toHaveBeenNthCalledWith(1, expect.stringContaining('organization(login: $ownerName)'), { ownerName: 'github', - projectNumber: 1 + projectNumber: 1, + headers: {'GraphQL-Features': 'memex_graphql_projectv2'} }) }) @@ -447,18 +449,18 @@ describe('addToProject', () => { test: /getProject/, return: { organization: { - projectNext: { - id: 'project-next-id' + projectV2: { + id: 'project-id' } } } }, { - test: /addProjectNextItem/, + test: /addProjectV2ItemById/, return: { - addProjectNextItem: { - projectNextItem: { - id: 'project-next-item-id' + addProjectV2ItemById: { + projectItem: { + id: 'project-item-id' } } } @@ -469,7 +471,8 @@ describe('addToProject', () => { expect(gqlMock).toHaveBeenNthCalledWith(1, expect.stringContaining('user(login: $ownerName)'), { ownerName: 'monalisa', - projectNumber: 1 + projectNumber: 1, + headers: {'GraphQL-Features': 'memex_graphql_projectv2'} }) }) }) diff --git a/src/add-to-project.ts b/src/add-to-project.ts index 19c6715..24e5115 100644 --- a/src/add-to-project.ts +++ b/src/add-to-project.ts @@ -8,21 +8,21 @@ const urlParse = interface ProjectNodeIDResponse { organization?: { - projectNext: { + projectV2: { id: string } } user?: { - projectNext: { + projectV2: { id: string } } } interface ProjectAddItemResponse { - addProjectNextItem: { - projectNextItem: { + addProjectV2ItemById: { + projectItem: { id: string } } @@ -40,6 +40,7 @@ export async function addToProject(): Promise { const labelOperator = core.getInput('label-operator').trim().toLocaleLowerCase() const octokit = github.getOctokit(ghToken) + const urlMatch = projectUrl.match(urlParse) const issue = github.context.payload.issue ?? github.context.payload.pull_request const issueLabels: string[] = (issue?.labels ?? []).map((l: {name: string}) => l.name) @@ -76,20 +77,21 @@ export async function addToProject(): Promise { // First, use the GraphQL API to request the project's node ID. const idResp = await octokit.graphql( - `query getProject($ownerName: String!, $projectNumber: Int!) { + `query getProject($ownerName: String!, $projectNumber: Int!) { ${ownerTypeQuery}(login: $ownerName) { - projectNext(number: $projectNumber) { + projectV2(number: $projectNumber) { id } } }`, { ownerName, - projectNumber + projectNumber, + headers: {'GraphQL-Features': 'memex_graphql_projectv2'} } ) - const projectId = idResp[ownerTypeQuery]?.projectNext.id + const projectId = idResp[ownerTypeQuery]?.projectV2.id const contentId = issue?.node_id core.debug(`Project node ID: ${projectId}`) @@ -97,22 +99,23 @@ export async function addToProject(): Promise { // Next, use the GraphQL API to add the issue to the project. const addResp = await octokit.graphql( - `mutation addIssueToProject($input: AddProjectNextItemInput!) { - addProjectNextItem(input: $input) { - projectNextItem { + `mutation addIssueToProject($input: AddProjectV2ItemByIdInput!) { + addProjectV2ItemById(input: $input) { + projectItem { id } } }`, { input: { + projectId, contentId, - projectId + headers: {'GraphQL-Features': 'memex_graphql_projectv2'} } } ) - core.setOutput('itemId', addResp.addProjectNextItem.projectNextItem.id) + core.setOutput('itemId', addResp.addProjectV2ItemById.projectItem.id) } export function mustGetOwnerTypeQuery(ownerType?: string): 'organization' | 'user' {