mirror of
https://github.com/actions/add-to-project.git
synced 2025-12-12 04:57:09 +00:00
Add some simple unit tests
This commit is contained in:
148
__tests__/add-to-project.test.ts
Normal file
148
__tests__/add-to-project.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import {addToProject} from '../src/add-to-project'
|
||||
|
||||
describe('addToProject', () => {
|
||||
let outputs: Record<string, string>
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(process.stdout, 'write').mockImplementation(() => true)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetInput({
|
||||
'project-url': 'https://github.com/orgs/github/projects/1',
|
||||
'github-token': 'gh_token'
|
||||
})
|
||||
|
||||
outputs = mockSetOutput()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
github.context.payload = {}
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
test('adds an issue to the project', async () => {
|
||||
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('adds 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: [{name: 'bug'}]
|
||||
}
|
||||
}
|
||||
|
||||
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 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()
|
||||
})
|
||||
})
|
||||
|
||||
function mockGetInput(mocks: Record<string, string>): jest.SpyInstance {
|
||||
const mock = (key: string) => mocks[key] ?? ''
|
||||
return jest.spyOn(core, 'getInput').mockImplementation(mock)
|
||||
}
|
||||
|
||||
function mockSetOutput(): Record<string, string> {
|
||||
const output: Record<string, string> = {}
|
||||
jest.spyOn(core, 'setOutput').mockImplementation((key, value) => (output[key] = value))
|
||||
return output
|
||||
}
|
||||
|
||||
function mockGraphQL(...mocks: {test: RegExp; return: unknown}[]): jest.Mock {
|
||||
const mock = jest.fn().mockImplementation((query: string) => {
|
||||
const match = mocks.find(m => m.test.test(query))
|
||||
|
||||
if (match) {
|
||||
return match.return
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected GraphQL query: ${query}`)
|
||||
})
|
||||
|
||||
jest.spyOn(github, 'getOctokit').mockImplementation(() => {
|
||||
return {
|
||||
graphql: mock
|
||||
} as unknown as ReturnType<typeof github.getOctokit>
|
||||
})
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
|
||||
test('sanity check', () => {
|
||||
expect(true).toBeTruthy()
|
||||
})
|
||||
53
dist/index.js
generated
vendored
53
dist/index.js
generated
vendored
@@ -1,7 +1,7 @@
|
||||
require('./sourcemap-register.js');/******/ (() => { // webpackBootstrap
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ 3109:
|
||||
/***/ 6672:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
"use strict";
|
||||
@@ -35,12 +35,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.addToProject = void 0;
|
||||
const core = __importStar(__nccwpck_require__(2186));
|
||||
const github = __importStar(__nccwpck_require__(5438));
|
||||
// TODO: Ensure this (and the Octokit client) works for non-github.com URLs, as well.
|
||||
// https://github.com/orgs|users/<ownerName>/projects/<projectNumber>
|
||||
const urlParse = /^(?:https:\/\/)?github\.com\/(?<ownerType>orgs|users)\/(?<ownerName>[^/]+)\/projects\/(?<projectNumber>\d+)/;
|
||||
function run() {
|
||||
function addToProject() {
|
||||
var _a, _b, _c, _d, _e, _f, _g, _h;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const projectUrl = core.getInput('project-url', { required: true });
|
||||
@@ -104,14 +105,7 @@ function run() {
|
||||
core.setOutput('itemId', addResp.addProjectNextItem.projectNextItem.id);
|
||||
});
|
||||
}
|
||||
run()
|
||||
.catch(err => {
|
||||
core.setFailed(err.message);
|
||||
process.exit(1);
|
||||
})
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
exports.addToProject = addToProject;
|
||||
function mustGetOwnerTypeQuery(ownerType) {
|
||||
const ownerTypeQuery = ownerType === 'orgs' ? 'organization' : ownerType === 'users' ? 'user' : null;
|
||||
if (!ownerTypeQuery) {
|
||||
@@ -121,6 +115,45 @@ function mustGetOwnerTypeQuery(ownerType) {
|
||||
}
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 3109:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
const core = __importStar(__nccwpck_require__(2186));
|
||||
const add_to_project_1 = __nccwpck_require__(6672);
|
||||
(0, add_to_project_1.addToProject)()
|
||||
.catch(err => {
|
||||
core.setFailed(err.message);
|
||||
process.exit(1);
|
||||
})
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 7351:
|
||||
|
||||
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
25
package-lock.json
generated
25
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@github/prettier-config": "^0.0.4",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/node": "^12.12.6",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"@vercel/ncc": "^0.33.1",
|
||||
@@ -26,6 +27,10 @@
|
||||
"prettier": "2.5.1",
|
||||
"ts-jest": "^27.1.3",
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0 <17.0.0",
|
||||
"npm": ">= 7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
@@ -1305,6 +1310,16 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest": {
|
||||
"version": "27.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz",
|
||||
"integrity": "sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"jest-diff": "^27.0.0",
|
||||
"pretty-format": "^27.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
|
||||
@@ -7324,6 +7339,16 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/jest": {
|
||||
"version": "27.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz",
|
||||
"integrity": "sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"jest-diff": "^27.0.0",
|
||||
"pretty-format": "^27.0.0"
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@github/prettier-config": "^0.0.4",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/node": "^12.12.6",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"@vercel/ncc": "^0.33.1",
|
||||
|
||||
122
src/add-to-project.ts
Normal file
122
src/add-to-project.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
|
||||
// TODO: Ensure this (and the Octokit client) works for non-github.com URLs, as well.
|
||||
// https://github.com/orgs|users/<ownerName>/projects/<projectNumber>
|
||||
const urlParse =
|
||||
/^(?:https:\/\/)?github\.com\/(?<ownerType>orgs|users)\/(?<ownerName>[^/]+)\/projects\/(?<projectNumber>\d+)/
|
||||
|
||||
interface ProjectNodeIDResponse {
|
||||
organization?: {
|
||||
projectNext: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
user?: {
|
||||
projectNext: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ProjectAddItemResponse {
|
||||
addProjectNextItem: {
|
||||
projectNextItem: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function addToProject(): Promise<void> {
|
||||
const projectUrl = core.getInput('project-url', {required: true})
|
||||
const ghToken = core.getInput('github-token', {required: true})
|
||||
const labeled =
|
||||
core
|
||||
.getInput('labeled')
|
||||
.split(',')
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0) ?? []
|
||||
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) {
|
||||
core.info(`Skipping issue ${issue?.number} because it does not have one of the labels: ${labeled.join(', ')}`)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
core.debug(`Project URL: ${projectUrl}`)
|
||||
|
||||
if (!urlMatch) {
|
||||
throw new Error(
|
||||
`Invalid project URL: ${projectUrl}. Project URL should match the format https://github.com/<orgs-or-users>/<ownerName>/projects/<projectNumber>`
|
||||
)
|
||||
}
|
||||
|
||||
const ownerName = urlMatch.groups?.ownerName
|
||||
const projectNumber = parseInt(urlMatch.groups?.projectNumber ?? '', 10)
|
||||
const ownerType = urlMatch.groups?.ownerType
|
||||
const ownerTypeQuery = mustGetOwnerTypeQuery(ownerType)
|
||||
|
||||
core.debug(`Org name: ${ownerName}`)
|
||||
core.debug(`Project number: ${projectNumber}`)
|
||||
core.debug(`Owner type: ${ownerType}`)
|
||||
|
||||
// First, use the GraphQL API to request the project's node ID.
|
||||
const idResp = await octokit.graphql<ProjectNodeIDResponse>(
|
||||
`query getProject($ownerName: String!, $projectNumber: Int!) {
|
||||
${ownerTypeQuery}(login: $ownerName) {
|
||||
projectNext(number: $projectNumber) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
ownerName,
|
||||
projectNumber
|
||||
}
|
||||
)
|
||||
|
||||
const projectId = idResp[ownerTypeQuery]?.projectNext.id
|
||||
const contentId = issue?.node_id
|
||||
|
||||
core.debug(`Project node ID: ${projectId}`)
|
||||
core.debug(`Content ID: ${contentId}`)
|
||||
|
||||
// Next, use the GraphQL API to add the issue to the project.
|
||||
const addResp = await octokit.graphql<ProjectAddItemResponse>(
|
||||
`mutation addIssueToProject($input: AddProjectNextItemInput!) {
|
||||
addProjectNextItem(input: $input) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
input: {
|
||||
contentId,
|
||||
projectId
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
core.setOutput('itemId', addResp.addProjectNextItem.projectNextItem.id)
|
||||
}
|
||||
|
||||
function mustGetOwnerTypeQuery(ownerType?: string): 'organization' | 'user' {
|
||||
const ownerTypeQuery = ownerType === 'orgs' ? 'organization' : ownerType === 'users' ? 'user' : null
|
||||
|
||||
if (!ownerTypeQuery) {
|
||||
throw new Error(`Unsupported ownerType: ${ownerType}. Must be one of 'orgs' or 'users'`)
|
||||
}
|
||||
|
||||
return ownerTypeQuery
|
||||
}
|
||||
124
src/main.ts
124
src/main.ts
@@ -1,117 +1,7 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import {addToProject} from './add-to-project'
|
||||
|
||||
// TODO: Ensure this (and the Octokit client) works for non-github.com URLs, as well.
|
||||
// https://github.com/orgs|users/<ownerName>/projects/<projectNumber>
|
||||
const urlParse =
|
||||
/^(?:https:\/\/)?github\.com\/(?<ownerType>orgs|users)\/(?<ownerName>[^/]+)\/projects\/(?<projectNumber>\d+)/
|
||||
|
||||
interface ProjectNodeIDResponse {
|
||||
organization?: {
|
||||
projectNext: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
user?: {
|
||||
projectNext: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ProjectAddItemResponse {
|
||||
addProjectNextItem: {
|
||||
projectNextItem: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const projectUrl = core.getInput('project-url', {required: true})
|
||||
const ghToken = core.getInput('github-token', {required: true})
|
||||
const labeled =
|
||||
core
|
||||
.getInput('labeled')
|
||||
.split(',')
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0) ?? []
|
||||
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) {
|
||||
core.info(`Skipping issue ${issue?.number} because it does not have one of the labels: ${labeled.join(', ')}`)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
core.debug(`Project URL: ${projectUrl}`)
|
||||
|
||||
if (!urlMatch) {
|
||||
throw new Error(
|
||||
`Invalid project URL: ${projectUrl}. Project URL should match the format https://github.com/<orgs-or-users>/<ownerName>/projects/<projectNumber>`
|
||||
)
|
||||
}
|
||||
|
||||
const ownerName = urlMatch.groups?.ownerName
|
||||
const projectNumber = parseInt(urlMatch.groups?.projectNumber ?? '', 10)
|
||||
const ownerType = urlMatch.groups?.ownerType
|
||||
const ownerTypeQuery = mustGetOwnerTypeQuery(ownerType)
|
||||
|
||||
core.debug(`Org name: ${ownerName}`)
|
||||
core.debug(`Project number: ${projectNumber}`)
|
||||
core.debug(`Owner type: ${ownerType}`)
|
||||
|
||||
// First, use the GraphQL API to request the project's node ID.
|
||||
const idResp = await octokit.graphql<ProjectNodeIDResponse>(
|
||||
`query getProject($ownerName: String!, $projectNumber: Int!) {
|
||||
${ownerTypeQuery}(login: $ownerName) {
|
||||
projectNext(number: $projectNumber) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
ownerName,
|
||||
projectNumber
|
||||
}
|
||||
)
|
||||
|
||||
const projectId = idResp[ownerTypeQuery]?.projectNext.id
|
||||
const contentId = issue?.node_id
|
||||
|
||||
core.debug(`Project node ID: ${projectId}`)
|
||||
core.debug(`Content ID: ${contentId}`)
|
||||
|
||||
// Next, use the GraphQL API to add the issue to the project.
|
||||
const addResp = await octokit.graphql<ProjectAddItemResponse>(
|
||||
`mutation addIssueToProject($input: AddProjectNextItemInput!) {
|
||||
addProjectNextItem(input: $input) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
input: {
|
||||
contentId,
|
||||
projectId
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
core.setOutput('itemId', addResp.addProjectNextItem.projectNextItem.id)
|
||||
}
|
||||
|
||||
run()
|
||||
addToProject()
|
||||
.catch(err => {
|
||||
core.setFailed(err.message)
|
||||
process.exit(1)
|
||||
@@ -119,13 +9,3 @@ run()
|
||||
.then(() => {
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
function mustGetOwnerTypeQuery(ownerType?: string): 'organization' | 'user' {
|
||||
const ownerTypeQuery = ownerType === 'orgs' ? 'organization' : ownerType === 'users' ? 'user' : null
|
||||
|
||||
if (!ownerTypeQuery) {
|
||||
throw new Error(`Unsupported ownerType: ${ownerType}. Must be one of 'orgs' or 'users'`)
|
||||
}
|
||||
|
||||
return ownerTypeQuery
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user