Merge remote-tracking branch 'origin/main' into improve-test-coverage

This commit is contained in:
Shaun Kirk Wong
2022-03-25 20:43:58 +00:00
committed by GitHub
6 changed files with 216 additions and 66 deletions

100
README.md
View File

@@ -1,16 +1,26 @@
# actions/add-to-project
🚨 **This action is a work in progress. Please do not use it except for
experimentation until a release has been prepared.** 🚨
Use this action to automatically add issues to a GitHub Project. Note that this
is for [GitHub Projects
(beta)](https://docs.github.com/en/issues/trying-out-the-new-projects-experience/about-projects),
not the original GitHub Projects.
## Current Status
[![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 a work-in-progress. Please do not use it except for
experimentation until a release has been prepared.** 🚨
## Usage
_See [action.yml](action.yml) for [metadata](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions) that defines the inputs, outputs, and runs configuration for this action._
_For more information about workflows, see [Using workflows](https://docs.github.com/en/actions/using-workflows)._
To use the action, create a workflow that runs when issues are opened in your
repository. Run this action in a step, optionally configuring any filters you
may want to add, such as only adding issues with certain labels.
may want to add, such as only adding issues with certain labels. If you want to match all the labels, add `label-operator` input to be `AND`.
```yaml
name: Add bugs to bugs project
@@ -25,25 +35,83 @@ jobs:
name: Add issue to project
runs-on: ubuntu-latest
steps:
# Pointing to a branch name generally isn't the safest way to refer to an action,
# but this is how you can use this action now before we've begun creating releases.
# Another option would be to point to a full commit SHA.
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/<orgName>/projects/<projectNumber>
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
labeled: bug
labeled: bug, new
label-operator: AND
```
#### Further reading and additional resources
- [Inputs](#inputs)
- [Supported Events](#supported-events)
- [How to point the action to a specific branch or commit sha](#how-to-point-the-action-to-a-specific-branch-or-commit-sha)
- [Creating a PAT and adding it to your repository](creating-a-pat-and-adding-it-to-your-repository)
- [Development](#development)
- [Publish to a distribution branch](#publish-to-a-distribution-branch)
## Inputs
- `project-url` is the URL of the GitHub Project to add issues to.
- `github-token` is a [personal access
- <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>`_
- <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
`read:org` scopes.
- `labeled` is a comma-separated list of labels. For an issue to be added to the
project, it must have _one_ of the labels in the list. Omitting this key means
that all issues will be added.
`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_
- <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`.
## Supported Events
Currently this action supports the following [issue events](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues):
- `opened`
- `transferred`
- `labeled`
This ensures that all issues in the workflow's repo are added to the [specified project](#project-url). If [labeled input(s)](#labeled) are defined, then issues will only be added if they contain at least _one_ of the labels in the list.
## How to point the action to a specific branch or commit sha
Pointing to a branch name generally isn't the safest way to refer to an action, but this is how you can use this action now before we've begun creating releases.
```yaml
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/<orgName>/projects/<projectNumber>
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
```
Another option would be to point to a full [commit SHA](https://docs.github.com/en/get-started/quickstart/github-glossary#commit):
```yaml
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@<commitSHA>
with:
project-url: https://github.com/orgs/<orgName>/projects/<projectNumber>
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
```
## Creating a PAT and adding it to your repository
- create a new [personal access
token](https://github.com/settings/tokens/new) with `repo`, `write:org` and
`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)
_See [Encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) for more information_
## Development
@@ -76,3 +144,7 @@ the "dist/" directory.
```
Now, a release can be created from the branch containing the built action.
# License
The scripts and documentation in this project are released under the [MIT License](LICENSE)

View File

@@ -52,11 +52,11 @@ describe('addToProject', () => {
expect(outputs.itemId).toEqual('project-next-item-id')
})
test('adds matching issues with a label filter', async () => {
test('adds matching issues with a label filter without label-operator', async () => {
mockGetInput({
'project-url': 'https://github.com/orgs/github/projects/1',
'github-token': 'gh_token',
labeled: 'bug'
labeled: 'bug, new'
})
github.context.payload = {
@@ -94,6 +94,92 @@ describe('addToProject', () => {
expect(outputs.itemId).toEqual('project-next-item-id')
})
test('does not add un-matching issues with a label filter without label-operator', async () => {
mockGetInput({
'project-url': 'https://github.com/orgs/github/projects/1',
'github-token': 'gh_token',
labeled: 'bug'
})
github.context.payload = {
issue: {
number: 1,
labels: []
}
}
const infoSpy = jest.spyOn(core, 'info')
const gqlMock = mockGraphQL()
await addToProject()
expect(infoSpy).toHaveBeenCalledWith(`Skipping issue 1 because it does not have one of the labels: bug`)
expect(gqlMock).not.toHaveBeenCalled()
})
test('adds matching issues with labels filter with AND label-operator', async () => {
mockGetInput({
'project-url': 'https://github.com/orgs/github/projects/1',
'github-token': 'gh_token',
labeled: 'bug, new',
'label-operator': 'AND'
})
github.context.payload = {
issue: {
number: 1,
labels: [{name: 'bug'}, {name: 'new'}]
}
}
mockGraphQL(
{
test: /getProject/,
return: {
organization: {
projectNext: {
id: 'project-next-id'
}
}
}
},
{
test: /addProjectNextItem/,
return: {
addProjectNextItem: {
projectNextItem: {
id: 'project-next-item-id'
}
}
}
}
)
await addToProject()
expect(outputs.itemId).toEqual('project-next-item-id')
})
test('does not add un-matching issues with labels filter with AND label-operator', async () => {
mockGetInput({
'project-url': 'https://github.com/orgs/github/projects/1',
'github-token': 'gh_token',
labeled: 'bug, new',
'label-operator': 'AND'
})
github.context.payload = {
issue: {
number: 1,
labels: [{name: 'bug'}, {name: 'other'}]
}
}
const infoSpy = jest.spyOn(core, 'info')
const gqlMock = mockGraphQL()
await addToProject()
expect(infoSpy).toHaveBeenCalledWith(`Skipping issue 1 because it doesn't match all the labels: bug, new`)
expect(gqlMock).not.toHaveBeenCalled()
})
test('adds matching issues with multiple label filters', async () => {
mockGetInput({
'project-url': 'https://github.com/orgs/github/projects/1',
@@ -163,27 +249,6 @@ describe('addToProject', () => {
expect(gqlMock).not.toHaveBeenCalled()
})
test('does not add un-matching issues with a label filter', async () => {
mockGetInput({
'project-url': 'https://github.com/orgs/github/projects/1',
'github-token': 'gh_token',
labeled: 'bug'
})
github.context.payload = {
issue: {
number: 1,
labels: []
}
}
const infoSpy = jest.spyOn(core, 'info')
const gqlMock = mockGraphQL()
await addToProject()
expect(infoSpy).toHaveBeenCalledWith(`Skipping issue 1 because it does not have one of the labels: bug`)
expect(gqlMock).not.toHaveBeenCalled()
})
test(`throws an error when url isn't a valid project url`, async () => {
mockGetInput({
'project-url': 'https://github.com/orgs/github/repositories',
@@ -205,28 +270,28 @@ describe('addToProject', () => {
expect(infoSpy).not.toHaveBeenCalled()
expect(gqlMock).not.toHaveBeenCalled()
})
})
test(`throws an error when url isn't under the github.com domain`, async () => {
mockGetInput({
'project-url': 'https://notgithub.com/orgs/github/projects/1',
'github-token': 'gh_token'
})
test(`throws an error when url isn't under the github.com domain`, async () => {
mockGetInput({
'project-url': 'https://notgithub.com/orgs/github/projects/1',
'github-token': 'gh_token'
})
github.context.payload = {
issue: {
number: 1,
labels: []
github.context.payload = {
issue: {
number: 1,
labels: []
}
}
}
const infoSpy = jest.spyOn(core, 'info')
const gqlMock = mockGraphQL()
await expect(addToProject()).rejects.toThrow(
'https://notgithub.com/orgs/github/projects/1. Project URL should match the format https://github.com/<orgs-or-users>/<ownerName>/projects/<projectNumber>'
)
expect(infoSpy).not.toHaveBeenCalled()
expect(gqlMock).not.toHaveBeenCalled()
const infoSpy = jest.spyOn(core, 'info')
const gqlMock = mockGraphQL()
await expect(addToProject()).rejects.toThrow(
'https://notgithub.com/orgs/github/projects/1. Project URL should match the format https://github.com/<orgs-or-users>/<ownerName>/projects/<projectNumber>'
)
expect(infoSpy).not.toHaveBeenCalled()
expect(gqlMock).not.toHaveBeenCalled()
})
})
function mockGetInput(mocks: Record<string, string>): jest.SpyInstance {

View File

@@ -11,6 +11,9 @@ inputs:
labeled:
required: false
description: A comma-separated list of labels to use as a filter for issue to be added
label-operator:
required: false
description: The behavior of the labels filter, AND to match all labels, OR to match any label (default is OR)
runs:
using: 'node16'
main: 'dist/index.js'

14
dist/index.js generated vendored
View File

@@ -51,14 +51,20 @@ function addToProject() {
.split(',')
.map(l => l.trim())
.filter(l => l.length > 0)) !== null && _a !== void 0 ? _a : [];
const labelOperator = core.getInput('label-operator').trim().toLocaleLowerCase();
const octokit = github.getOctokit(ghToken);
const urlMatch = projectUrl.match(urlParse);
const issue = (_b = github.context.payload.issue) !== null && _b !== void 0 ? _b : github.context.payload.pull_request;
const issueLabels = ((_c = issue === null || issue === void 0 ? void 0 : issue.labels) !== null && _c !== void 0 ? _c : []).map((l) => l.name);
// Ensure the issue matches our `labeled` filter, if provided.
if (labeled.length > 0) {
const hasLabel = issueLabels.some(l => labeled.includes(l));
if (!hasLabel) {
// Ensure the issue matches our `labeled` filter based on the label-operator.
if (labelOperator === 'and') {
if (!labeled.every(l => issueLabels.includes(l))) {
core.info(`Skipping issue ${issue === null || issue === void 0 ? void 0 : issue.number} because it doesn't match all the labels: ${labeled.join(', ')}`);
return;
}
}
else {
if (labeled.length > 0 && !issueLabels.some(l => labeled.includes(l))) {
core.info(`Skipping issue ${issue === null || issue === void 0 ? void 0 : issue.number} because it does not have one of the labels: ${labeled.join(', ')}`);
return;
}

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -37,18 +37,22 @@ export async function addToProject(): Promise<void> {
.split(',')
.map(l => l.trim())
.filter(l => l.length > 0) ?? []
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)
// Ensure the issue matches our `labeled` filter, if provided.
if (labeled.length > 0) {
const hasLabel = issueLabels.some(l => labeled.includes(l))
if (!hasLabel) {
// Ensure the issue matches our `labeled` filter based on the label-operator.
if (labelOperator === 'and') {
if (!labeled.every(l => issueLabels.includes(l))) {
core.info(`Skipping issue ${issue?.number} because it doesn't match all the labels: ${labeled.join(', ')}`)
return
}
} else {
if (labeled.length > 0 && !issueLabels.some(l => labeled.includes(l))) {
core.info(`Skipping issue ${issue?.number} because it does not have one of the labels: ${labeled.join(', ')}`)
return
}
}