Skip to content

Commit

Permalink
feat: find-project action
Browse files Browse the repository at this point in the history
  • Loading branch information
dsanders11 committed Nov 2, 2023
1 parent 62650d5 commit e079181
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 42 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,23 @@ jobs:
title: Integration Test Project
token: ${{ steps.get-auth-token.outputs.token }}

- name: Find Project
uses: ./find-project/
id: find-project
with:
owner: ${{ matrix.owner }}
title: Integration Test Project
token: ${{ steps.get-auth-token.outputs.token }}

- name: Confirm Project Found
uses: ./github-script/
with:
script: |
if ("${{ steps.copy-project.outputs.number }}" !== "${{ steps.find-project.outputs.number }}") {
throw new Error("Could not find project by title")
}
token: ${{ steps.get-auth-token.outputs.token }}

- name: Get Draft Issue ID
id: get-draft-issue-id
run: |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ private repositories the PAT must also have the `repo` scope.
| [`project-actions/delete-project`](delete-project) | Delete a project |
| [`project-actions/edit-item`](edit-item) | Edit an item on a project |
| [`project-actions/edit-project`](edit-project) | Edit a project |
| [`project-actions/find-project`](find-project) | Find a project |
| [`project-actions/get-item`](get-item) | Get an item on a project |
| [`project-actions/get-project`](get-project) | Get a project |
| [`project-actions/github-script`](github-script) | Modify projects programmatically |
Expand Down
123 changes: 123 additions & 0 deletions __tests__/find-project.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import * as core from '@actions/core';

import * as index from '../src/find-project';
import { findProject } from '../src/lib';
import { mockGetInput } from './utils';

jest.mock('@actions/core');
jest.mock('../src/lib');

// Spy the action's entrypoint
const findProjectActionSpy = jest.spyOn(index, 'findProjectAction');

const owner = 'dsanders11';
const projectNumber = '94';
const projectId = 'project-id';
const fieldCount = 4;
const itemCount = 50;
const shortDescription = 'Description';
const title = 'My Title';
const readme = 'README';
const url = 'url';

describe('findProjectAction', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('requires the title input', async () => {
mockGetInput({ owner });

await index.findProjectAction();
expect(findProjectActionSpy).toHaveReturned();

expect(core.setFailed).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenLastCalledWith(
'Input required and not supplied: title'
);
});

it('handles project not found', async () => {
mockGetInput({ owner, title });
jest.mocked(findProject).mockResolvedValue(null);

await index.findProjectAction();
expect(findProjectActionSpy).toHaveReturned();

expect(core.setFailed).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenLastCalledWith(
`Project not found: ${title}`
);
});

it('handles generic errors', async () => {
mockGetInput({ owner, title });
jest.mocked(findProject).mockImplementation(() => {
throw new Error('Server error');
});

await index.findProjectAction();
expect(findProjectActionSpy).toHaveReturned();

expect(core.setFailed).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenLastCalledWith('Server error');
});

it('stringifies non-errors', async () => {
mockGetInput({ owner, title });
jest.mocked(findProject).mockImplementation(() => {
throw 42; // eslint-disable-line no-throw-literal
});

await index.findProjectAction();
expect(findProjectActionSpy).toHaveReturned();

expect(core.setFailed).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenLastCalledWith('42');
});

it('sets output', async () => {
mockGetInput({ owner, title });
jest.mocked(findProject).mockResolvedValue({
id: projectId,
number: parseInt(projectNumber),
fields: {
totalCount: fieldCount
},
items: {
totalCount: itemCount
},
url,
title,
readme,
shortDescription,
public: true,
closed: false,
owner: {
type: 'Organization',
login: owner
}
});

await index.findProjectAction();
expect(findProjectActionSpy).toHaveReturned();

expect(core.setOutput).toHaveBeenCalledTimes(10);
expect(core.setOutput).toHaveBeenCalledWith('id', projectId);
expect(core.setOutput).toHaveBeenCalledWith('url', url);
expect(core.setOutput).toHaveBeenCalledWith('closed', false);
expect(core.setOutput).toHaveBeenCalledWith('public', true);
expect(core.setOutput).toHaveBeenCalledWith('field-count', fieldCount);
expect(core.setOutput).toHaveBeenCalledWith('item-count', itemCount);
expect(core.setOutput).toHaveBeenCalledWith(
'number',
parseInt(projectNumber)
);
expect(core.setOutput).toHaveBeenCalledWith('readme', readme);
expect(core.setOutput).toHaveBeenCalledWith(
'description',
shortDescription
);
expect(core.setOutput).toHaveBeenCalledWith('title', title);
});
});
23 changes: 23 additions & 0 deletions __tests__/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('lib', () => {
const owner = 'dsanders11';
const projectId = 'project-id';
const projectNumber = '41';
const projectTitle = 'My Cool Project';

beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -1166,6 +1167,28 @@ describe('lib', () => {
});
});

describe('findProject', () => {
const project = { id: projectId, title: projectTitle };

it('handles project not found', async () => {
jest
.mocked(execCliCommand)
.mockResolvedValue(JSON.stringify({ projects: [project] }));
await expect(
lib.findProject(owner, 'A Different Title')
).resolves.toEqual(null);
});

it('returns project details', async () => {
jest
.mocked(execCliCommand)
.mockResolvedValue(JSON.stringify({ projects: [project] }));
await expect(lib.findProject(owner, projectTitle)).resolves.toEqual(
project
);
});
});

describe('getProject', () => {
it('handles project not found', async () => {
mockProjectNotFoundError();
Expand Down
31 changes: 4 additions & 27 deletions dist/find-project.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 8 additions & 15 deletions dist/github-script/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions find-project/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# `project-actions/find-project`

[![Release](https://img.shields.io/github/v/release/dsanders11/project-actions?color=blue)](https://github.com/dsanders11/project-actions/releases)

Find a GitHub project

## Inputs

| Name | Description | Required | Default |
|-------------------|----------------------------------------------------|----------|----------------------------------------------|
| `token` | A GitHub access token - either a classic PAT or a GitHub app installation token. | Yes | |
| `owner` | The owner of the project - either an organization or a user. If not provided, it defaults to the repository owner. | No | `${{ github.repository_owner }}` |
| `title` | The title of the project to find. | Yes | |

## Outputs

| Name | Description |
|-------------------|----------------------------------------------------|
| `id` | The global ID for the project. |
| `closed` | The closed state of the project. |
| `field-count` | The number of fields on the project. |
| `item-count` | The number of items in the project. |
| `number` | The project number of the project. |
| `public` | The public visibility of the project. |
| `readme` | The readme description of the project. |
| `description` | The short description of the project. |
| `title` | The title of the project. |
| `url` | The URL of the project. |

## License

MIT
41 changes: 41 additions & 0 deletions find-project/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Find Project
description: Find a GitHub project
author: David Sanders

inputs:
token:
description: A GitHub access token - either a classic PAT or a GitHub app installation token
required: true
owner:
description: The owner of the project - either an organization or a user
required: false
default: ${{ github.repository_owner }}
title:
description: The title of the project to find
required: true

outputs:
id:
description: The global ID for the project
closed:
description: The closed state of the project
field-count:
description: The number of fields on the project
item-count:
description: The number of items in the project
number:
description: The project number of the project
public:
description: The public visibility of the project
readme:
description: The readme description of the project
description:
description: The short description of the project
title:
description: The title of the project
url:
description: The URL of the project

runs:
using: node20
main: ../dist/find-project.js
4 changes: 4 additions & 0 deletions find-project/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { findProjectAction } from '../src/find-project';

// eslint-disable-next-line @typescript-eslint/no-floating-promises
findProjectAction();
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"package:delete-project": "esbuild delete-project/index.ts --bundle --outfile=dist/delete-project.js --platform=node --target=node20.2",
"package:edit-item": "esbuild edit-item/index.ts --bundle --outfile=dist/edit-item.js --platform=node --target=node20.2",
"package:edit-project": "esbuild edit-project/index.ts --bundle --outfile=dist/edit-project.js --platform=node --target=node20.2",
"package:find-project": "esbuild find-project/index.ts --bundle --outfile=dist/find-project.js --platform=node --target=node20.2",
"package:get-item": "esbuild get-item/index.ts --bundle --outfile=dist/get-item.js --platform=node --target=node20.2",
"package:get-project": "esbuild get-project/index.ts --bundle --outfile=dist/get-project.js --platform=node --target=node20.2",
"package:link-project": "esbuild link-project/index.ts --bundle --outfile=dist/link-project.js --platform=node --target=node20.2",
Expand Down
35 changes: 35 additions & 0 deletions src/find-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as core from '@actions/core';

import { findProject } from './lib';

export async function findProjectAction(): Promise<void> {
try {
// Required inputs
const owner = core.getInput('owner', { required: true });
const title = core.getInput('title', { required: true });

const project = await findProject(owner, title);

if (!project) {
core.setFailed(`Project not found: ${title}`);
return;
}

core.setOutput('closed', project.closed);
core.setOutput('field-count', project.fields.totalCount);
core.setOutput('id', project.id);
core.setOutput('item-count', project.items.totalCount);
core.setOutput('number', project.number);
core.setOutput('public', project.public);
core.setOutput('readme', project.readme);
core.setOutput('description', project.shortDescription);
core.setOutput('title', project.title);
core.setOutput('url', project.url);
} catch (error) {
// Fail the workflow run if an error occurs
if (error instanceof Error && error.stack) core.debug(error.stack);
core.setFailed(
error instanceof Error ? error.message : JSON.stringify(error)
);
}
}
Loading

0 comments on commit e079181

Please sign in to comment.