mirror of
https://github.com/actions/add-to-project.git
synced 2025-12-11 04:32:47 +00:00
Update Action to use new ProjectsV2 api
ProjectsNext API is going to be deprecated in favour of new ProjectsV2.
This commit is contained in:
13
README.md
13
README.md
@@ -9,9 +9,9 @@ not the original GitHub projects.
|
|||||||
|
|
||||||
[](https://github.com/actions/add-to-project/actions/workflows/test.yml)
|
[](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
|
## Usage
|
||||||
|
|
||||||
@@ -80,11 +80,10 @@ jobs:
|
|||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
- <a name="project-url">`project-url`</a> **(required)** is the URL of the GitHub project to add issues to.
|
- <a name="project-url">`project-url`</a> **(required)** is the URL of the GitHub project to add issues to.
|
||||||
_eg: `https://github.com/orgs|users/<ownerName>/projects/<projectNumber>`_
|
_eg: `https://github.com/orgs|users/<ownerName>/projects/<projectNumber>`_
|
||||||
- <a name="github-token">`github-token`</a> **(required)** is a [personal access
|
- <a name="github-token">`github-token`</a> **(required)** is a [personal access
|
||||||
token](https://github.com/settings/tokens/new) with the `repo`, `write:org` and
|
token](https://github.com/settings/tokens/new) with the `project` scope.
|
||||||
`read:org` scopes.
|
|
||||||
_See [Creating a PAT and adding it to your repository](#creating-a-pat-and-adding-it-to-your-repository) for more details_
|
_See [Creating a PAT and adding it to your repository](#creating-a-pat-and-adding-it-to-your-repository) for more details_
|
||||||
- <a name="labeled">`labeled`</a> **(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.
|
- <a name="labeled">`labeled`</a> **(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.
|
||||||
- <a name="labeled">`label-operator`</a> **(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`.
|
- <a name="labeled">`label-operator`</a> **(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
|
- create a new [personal access
|
||||||
token](https://github.com/settings/tokens/new) with `repo`, `write:org` and
|
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_
|
_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_
|
_See [Encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) for more information_
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
|
|
||||||
import {addToProject, mustGetOwnerTypeQuery} from '../src/add-to-project'
|
import {addToProject, mustGetOwnerTypeQuery} from '../src/add-to-project'
|
||||||
|
|
||||||
describe('addToProject', () => {
|
describe('addToProject', () => {
|
||||||
@@ -29,18 +30,18 @@ describe('addToProject', () => {
|
|||||||
test: /getProject/,
|
test: /getProject/,
|
||||||
return: {
|
return: {
|
||||||
organization: {
|
organization: {
|
||||||
projectNext: {
|
projectV2: {
|
||||||
id: 'project-next-id'
|
id: 'project-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /addProjectNextItem/,
|
test: /addProjectV2ItemById/,
|
||||||
return: {
|
return: {
|
||||||
addProjectNextItem: {
|
addProjectV2ItemById: {
|
||||||
projectNextItem: {
|
projectItem: {
|
||||||
id: 'project-next-item-id'
|
id: 'project-item-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,7 @@ describe('addToProject', () => {
|
|||||||
|
|
||||||
await 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 () => {
|
test('adds matching issues with a label filter without label-operator', async () => {
|
||||||
@@ -71,18 +72,18 @@ describe('addToProject', () => {
|
|||||||
test: /getProject/,
|
test: /getProject/,
|
||||||
return: {
|
return: {
|
||||||
organization: {
|
organization: {
|
||||||
projectNext: {
|
projectV2: {
|
||||||
id: 'project-next-id'
|
id: 'project-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /addProjectNextItem/,
|
test: /addProjectV2ItemById/,
|
||||||
return: {
|
return: {
|
||||||
addProjectNextItem: {
|
addProjectV2ItemById: {
|
||||||
projectNextItem: {
|
projectItem: {
|
||||||
id: 'project-next-item-id'
|
id: 'project-item-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +92,7 @@ describe('addToProject', () => {
|
|||||||
|
|
||||||
await 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 () => {
|
test('adds matching pull-requests with a label filter without label-operator', async () => {
|
||||||
@@ -114,18 +115,18 @@ describe('addToProject', () => {
|
|||||||
test: /getProject/,
|
test: /getProject/,
|
||||||
return: {
|
return: {
|
||||||
organization: {
|
organization: {
|
||||||
projectNext: {
|
projectV2: {
|
||||||
id: 'project-next-id'
|
id: 'project-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /addProjectNextItem/,
|
test: /addProjectV2ItemById/,
|
||||||
return: {
|
return: {
|
||||||
addProjectNextItem: {
|
addProjectV2ItemById: {
|
||||||
projectNextItem: {
|
projectItem: {
|
||||||
id: 'project-next-item-id'
|
id: 'project-item-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +135,7 @@ describe('addToProject', () => {
|
|||||||
|
|
||||||
await 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 () => {
|
test('does not add un-matching issues with a label filter without label-operator', async () => {
|
||||||
@@ -178,18 +179,18 @@ describe('addToProject', () => {
|
|||||||
test: /getProject/,
|
test: /getProject/,
|
||||||
return: {
|
return: {
|
||||||
organization: {
|
organization: {
|
||||||
projectNext: {
|
projectV2: {
|
||||||
id: 'project-next-id'
|
id: 'project-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /addProjectNextItem/,
|
test: /addProjectV2ItemById/,
|
||||||
return: {
|
return: {
|
||||||
addProjectNextItem: {
|
addProjectV2ItemById: {
|
||||||
projectNextItem: {
|
projectItem: {
|
||||||
id: 'project-next-item-id'
|
id: 'project-item-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,7 +199,7 @@ describe('addToProject', () => {
|
|||||||
|
|
||||||
await 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 () => {
|
test('does not add un-matching issues with labels filter with AND label-operator', async () => {
|
||||||
@@ -242,18 +243,18 @@ describe('addToProject', () => {
|
|||||||
test: /getProject/,
|
test: /getProject/,
|
||||||
return: {
|
return: {
|
||||||
organization: {
|
organization: {
|
||||||
projectNext: {
|
projectV2: {
|
||||||
id: 'project-next-id'
|
id: 'project-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /addProjectNextItem/,
|
test: /addProjectV2ItemById/,
|
||||||
return: {
|
return: {
|
||||||
addProjectNextItem: {
|
addProjectV2ItemById: {
|
||||||
projectNextItem: {
|
projectItem: {
|
||||||
id: 'project-next-item-id'
|
id: 'project-item-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,7 +267,7 @@ describe('addToProject', () => {
|
|||||||
|
|
||||||
expect(gqlMock).toHaveBeenCalled()
|
expect(gqlMock).toHaveBeenCalled()
|
||||||
expect(infoSpy).not.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 () => {
|
test('does not add un-matching issues with multiple label filters', async () => {
|
||||||
@@ -312,18 +313,18 @@ describe('addToProject', () => {
|
|||||||
test: /getProject/,
|
test: /getProject/,
|
||||||
return: {
|
return: {
|
||||||
organization: {
|
organization: {
|
||||||
projectNext: {
|
projectV2: {
|
||||||
id: 'project-next-id'
|
id: 'project-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /addProjectNextItem/,
|
test: /addProjectV2ItemById/,
|
||||||
return: {
|
return: {
|
||||||
addProjectNextItem: {
|
addProjectV2ItemById: {
|
||||||
projectNextItem: {
|
projectItem: {
|
||||||
id: 'project-next-item-id'
|
id: 'project-item-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,7 +337,7 @@ describe('addToProject', () => {
|
|||||||
|
|
||||||
expect(gqlMock).toHaveBeenCalled()
|
expect(gqlMock).toHaveBeenCalled()
|
||||||
expect(infoSpy).not.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 () => {
|
test(`throws an error when url isn't a valid project url`, async () => {
|
||||||
@@ -402,18 +403,18 @@ describe('addToProject', () => {
|
|||||||
test: /getProject/,
|
test: /getProject/,
|
||||||
return: {
|
return: {
|
||||||
organization: {
|
organization: {
|
||||||
projectNext: {
|
projectV2: {
|
||||||
id: 'project-next-id'
|
id: 'project-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /addProjectNextItem/,
|
test: /addProjectV2ItemById/,
|
||||||
return: {
|
return: {
|
||||||
addProjectNextItem: {
|
addProjectV2ItemById: {
|
||||||
projectNextItem: {
|
projectItem: {
|
||||||
id: 'project-next-item-id'
|
id: 'project-item-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,7 +425,8 @@ describe('addToProject', () => {
|
|||||||
|
|
||||||
expect(gqlMock).toHaveBeenNthCalledWith(1, expect.stringContaining('organization(login: $ownerName)'), {
|
expect(gqlMock).toHaveBeenNthCalledWith(1, expect.stringContaining('organization(login: $ownerName)'), {
|
||||||
ownerName: 'github',
|
ownerName: 'github',
|
||||||
projectNumber: 1
|
projectNumber: 1,
|
||||||
|
headers: {'GraphQL-Features': 'memex_graphql_projectv2'}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -447,18 +449,18 @@ describe('addToProject', () => {
|
|||||||
test: /getProject/,
|
test: /getProject/,
|
||||||
return: {
|
return: {
|
||||||
organization: {
|
organization: {
|
||||||
projectNext: {
|
projectV2: {
|
||||||
id: 'project-next-id'
|
id: 'project-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /addProjectNextItem/,
|
test: /addProjectV2ItemById/,
|
||||||
return: {
|
return: {
|
||||||
addProjectNextItem: {
|
addProjectV2ItemById: {
|
||||||
projectNextItem: {
|
projectItem: {
|
||||||
id: 'project-next-item-id'
|
id: 'project-item-id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,7 +471,8 @@ describe('addToProject', () => {
|
|||||||
|
|
||||||
expect(gqlMock).toHaveBeenNthCalledWith(1, expect.stringContaining('user(login: $ownerName)'), {
|
expect(gqlMock).toHaveBeenNthCalledWith(1, expect.stringContaining('user(login: $ownerName)'), {
|
||||||
ownerName: 'monalisa',
|
ownerName: 'monalisa',
|
||||||
projectNumber: 1
|
projectNumber: 1,
|
||||||
|
headers: {'GraphQL-Features': 'memex_graphql_projectv2'}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ const urlParse =
|
|||||||
|
|
||||||
interface ProjectNodeIDResponse {
|
interface ProjectNodeIDResponse {
|
||||||
organization?: {
|
organization?: {
|
||||||
projectNext: {
|
projectV2: {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user?: {
|
user?: {
|
||||||
projectNext: {
|
projectV2: {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectAddItemResponse {
|
interface ProjectAddItemResponse {
|
||||||
addProjectNextItem: {
|
addProjectV2ItemById: {
|
||||||
projectNextItem: {
|
projectItem: {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,7 @@ export async function addToProject(): Promise<void> {
|
|||||||
const labelOperator = core.getInput('label-operator').trim().toLocaleLowerCase()
|
const labelOperator = core.getInput('label-operator').trim().toLocaleLowerCase()
|
||||||
|
|
||||||
const octokit = github.getOctokit(ghToken)
|
const octokit = github.getOctokit(ghToken)
|
||||||
|
|
||||||
const urlMatch = projectUrl.match(urlParse)
|
const urlMatch = projectUrl.match(urlParse)
|
||||||
const issue = github.context.payload.issue ?? github.context.payload.pull_request
|
const issue = github.context.payload.issue ?? github.context.payload.pull_request
|
||||||
const issueLabels: string[] = (issue?.labels ?? []).map((l: {name: string}) => l.name)
|
const issueLabels: string[] = (issue?.labels ?? []).map((l: {name: string}) => l.name)
|
||||||
@@ -76,20 +77,21 @@ export async function addToProject(): Promise<void> {
|
|||||||
|
|
||||||
// First, use the GraphQL API to request the project's node ID.
|
// First, use the GraphQL API to request the project's node ID.
|
||||||
const idResp = await octokit.graphql<ProjectNodeIDResponse>(
|
const idResp = await octokit.graphql<ProjectNodeIDResponse>(
|
||||||
`query getProject($ownerName: String!, $projectNumber: Int!) {
|
`query getProject($ownerName: String!, $projectNumber: Int!) {
|
||||||
${ownerTypeQuery}(login: $ownerName) {
|
${ownerTypeQuery}(login: $ownerName) {
|
||||||
projectNext(number: $projectNumber) {
|
projectV2(number: $projectNumber) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
{
|
{
|
||||||
ownerName,
|
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
|
const contentId = issue?.node_id
|
||||||
|
|
||||||
core.debug(`Project node ID: ${projectId}`)
|
core.debug(`Project node ID: ${projectId}`)
|
||||||
@@ -97,22 +99,23 @@ export async function addToProject(): Promise<void> {
|
|||||||
|
|
||||||
// Next, use the GraphQL API to add the issue to the project.
|
// Next, use the GraphQL API to add the issue to the project.
|
||||||
const addResp = await octokit.graphql<ProjectAddItemResponse>(
|
const addResp = await octokit.graphql<ProjectAddItemResponse>(
|
||||||
`mutation addIssueToProject($input: AddProjectNextItemInput!) {
|
`mutation addIssueToProject($input: AddProjectV2ItemByIdInput!) {
|
||||||
addProjectNextItem(input: $input) {
|
addProjectV2ItemById(input: $input) {
|
||||||
projectNextItem {
|
projectItem {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
{
|
{
|
||||||
input: {
|
input: {
|
||||||
|
projectId,
|
||||||
contentId,
|
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' {
|
export function mustGetOwnerTypeQuery(ownerType?: string): 'organization' | 'user' {
|
||||||
|
|||||||
Reference in New Issue
Block a user