Skip to content

Commit

Permalink
test: add initial tests for all functionality (#24)
Browse files Browse the repository at this point in the history
- write tests for all files
- switch `ts-jest` to `@swc/jest`
- replace `process.exit(1)` with `throw new Error()` for better
testability
- improve input validation
  • Loading branch information
Balvajs authored Aug 8, 2023
1 parent b8d4c70 commit 31ce6be
Show file tree
Hide file tree
Showing 14 changed files with 698 additions and 225 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ jobs:
## Inputs
| INPUT | TYPE | DEFAULT | DESCRIPTION |
|----------------|---------|------------------------------|-------------------------------------------------------------------------------------------------------|
| -------------- | ------- | ---------------------------- | ----------------------------------------------------------------------------------------------------- |
| days-to-delete | number | `90` | Number of days without activity after which the branch will be deleted |
| dry-run | boolean | `true` | If set to true, the action will only log the branches that would be deleted, but will not delete them |
| repository | string | `"${{ github.repository }}"` | Repository name and owner in format `"owner/repo"` |
| token | string | `"${{ github.token }}"` | GitHub token with `pull-requests: read` and `contents: write` permissions |
\* required

\* required
1 change: 0 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ branding:
icon: git-branch
color: orange


inputs:
days-to-delete:
required: false
Expand Down
45 changes: 23 additions & 22 deletions dist/main.cjs

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

2 changes: 1 addition & 1 deletion dist/main.cjs.map

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import type { Config } from 'jest'

const esModules = [
'chalk',
'node-fetch',
'data-uri-to-buffer',
'fetch-blob',
'formdata-polyfill',
].join('|')

const config: Config = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
'^.+\\.(t|j)s$': '@swc/jest',
},
transformIgnorePatterns: [`/node_modules/(?!${esModules})`],
verbose: true,
testEnvironment: 'node',
}

export default config
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"@semantic-release/commit-analyzer": "10.0.1",
"@semantic-release/git": "10.0.1",
"@semantic-release/release-notes-generator": "11.0.4",
"@swc/core": "1.3.75",
"@swc/jest": "0.2.28",
"@types/jest": "29.5.3",
"@types/node": "16.11.7",
"@typescript-eslint/eslint-plugin": "6.2.1",
"@typescript-eslint/parser": "6.2.1",
Expand All @@ -66,7 +69,6 @@
"prettier": "3.0.1",
"semantic-release": "^21.0.7",
"semantic-release-major-tag": "0.3.2",
"ts-jest": "29.1.1",
"ts-node": "10.9.1",
"tsup": "7.2.0",
"typescript": "5.1.6"
Expand Down
73 changes: 73 additions & 0 deletions src/delete-stale-branches.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { deleteStaleBranches } from './delete-stale-branches.ts'
import { getRepositoryBranches } from './get-branches.ts'
import { getInputs } from './get-inputs.ts'
import { getOctokit } from './get-octokit.ts'

jest.mock('./get-octokit.ts')
jest.mock('./get-branches.ts')
jest.mock('./get-inputs.ts')
jest.spyOn(global.console, 'log').mockImplementation()

const mockGetInputs = (override: Partial<ReturnType<typeof getInputs>> = {}) =>
(
getInputs as unknown as jest.MockedFunction<typeof getInputs>
).mockReturnValue({
ghToken: 'random-token',
daysToDelete: 90,
dryRun: true,
repositoryOwner: 'balvajs',
repositoryName: 'delete-stale-branches',
...override,
})

const mockGetRepositoryBranches = (
branches: Pick<
Awaited<ReturnType<typeof getRepositoryBranches>>[number],
'daysDiff' | 'name'
>[] = [],
) =>
(
getRepositoryBranches as unknown as jest.MockedFunction<
typeof getRepositoryBranches
>
)
// for the deleteStaleBranches only subset of branch properties is needed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.mockResolvedValue(branches as any)

const requestMock = jest.fn()
const getOctokitMock = getOctokit as unknown as jest.MockedFunction<
typeof getOctokit
>
getOctokitMock
// for the deleteStaleBranches only request property is needed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.mockReturnValue({ request: requestMock } as any)

describe('deleteStaleBranches', () => {
beforeEach(() => {
mockGetInputs()
mockGetRepositoryBranches()
})

it('exits early if dry-run is true', async () => {
mockGetInputs({ dryRun: true })

await deleteStaleBranches()

expect(requestMock).not.toBeCalled()
})

it('delete stale branches if dry-run is false', async () => {
mockGetInputs({ dryRun: false })
mockGetRepositoryBranches([
{ daysDiff: 10, name: 'active-branch' },
{ daysDiff: 90, name: 'branch-1-day-before-stale' },
{ daysDiff: 100, name: 'stale-branch' },
])

await deleteStaleBranches()

expect(requestMock).toBeCalledTimes(1)
})
})
105 changes: 105 additions & 0 deletions src/delete-stale-branches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Chalk } from 'chalk'

import { getOctokit } from './get-octokit.ts'
import { getRepositoryBranches } from './get-branches.ts'
import { getInputs } from './get-inputs.ts'

// set level to 2, to be able to print colorful messages also in CI
const chalk = new Chalk({ level: 2 })

const pluralizeBranches = (count: number) =>
count === 1 ? 'branch' : 'branches'

export const deleteStaleBranches = async () => {
const { ghToken, daysToDelete, dryRun, repositoryName, repositoryOwner } =
getInputs()

const octokit = getOctokit({ ghToken })

const branches = await getRepositoryBranches({
octokit,
repositoryOwner,
repositoryName,
})

console.log(
`Found ${branches.length} ${pluralizeBranches(
branches.length,
)} without associated PR.\n\n`,
)

let separatorPrinted = false
for (const { name, daysDiff } of branches) {
const willBeDeleted = daysDiff > daysToDelete

if (!separatorPrinted && willBeDeleted) {
console.log(
`\n====== Following branches are inactive more than ${daysToDelete} days and will be deleted ======\n`,
)
separatorPrinted = true
}

console.log(
`${name} - `,
chalk[willBeDeleted ? 'red' : 'green'](`${daysDiff} days inactive`),
)
}

const branchesToDelete = branches.filter(
({ daysDiff }) => daysDiff > daysToDelete,
)

if (dryRun) {
console.log(
`\n\nDry run. Would delete ${branchesToDelete.length} ${pluralizeBranches(
branchesToDelete.length,
)} that are inactive for more than ${daysToDelete} days.`,
)

return
}

if (!branchesToDelete.length) {
console.log('\n\nNo stale branches found.')

return
}

console.log(
`\n\nDeleting ${branchesToDelete.length} ${pluralizeBranches(
branchesToDelete.length,
)} that ${
branchesToDelete.length === 1 ? 'is' : 'are'
} inactive for more than ${daysToDelete} days...`,
)

const deletionResults = await Promise.allSettled(
branchesToDelete.map(async ({ name }) => {
try {
await octokit.request({
method: 'DELETE',
url: `/repos/${repositoryOwner}/${repositoryName}/git/refs/heads/${name}`,
})
console.log(`Deleted ${name}`)
} catch (e) {
console.error(`Failed to delete ${name}: ${e}`)
}
}),
)

const failedDeletions = deletionResults.filter(
({ status }) => status === 'rejected',
)

if (failedDeletions.length) {
throw new Error(
`${failedDeletions}/${branchesToDelete.length} branch deletions failed.`,
)
}

console.log(
`\nSuccessfully deleted ${branchesToDelete.length} ${pluralizeBranches(
branchesToDelete.length,
)}.`,
)
}
Loading

0 comments on commit 31ce6be

Please sign in to comment.