Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
pull_request:
branches:
- main
push:
branches:
- main

permissions:
contents: read

jobs:
test:
name: Test and build
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Build action
run: npm run build
47 changes: 47 additions & 0 deletions __tests__/branch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const request = jest.fn();

jest.mock('octokit', () => ({
Octokit: jest.fn().mockImplementation(() => ({request}))
}));

import {branch} from '../src/branch';

describe('branch', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
jest.restoreAllMocks();
});

it('returns the repository default branch', async () => {
request.mockResolvedValue({
data: {
default_branch: 'develop'
}
});

await expect(branch('token', 'MonkeyECX', 'service')).resolves.toBe(
'develop'
);

expect(request).toHaveBeenCalledWith('GET /repos/{owner}/{repo}', {
owner: 'MonkeyECX',
repo: 'service',
headers: {
'X-GitHub-Api-Version': '2022-11-28',
accept: 'application/vnd.github.v3+json'
}
});
});

it('rethrows request errors with their original message', async () => {
request.mockRejectedValue(new Error('repo not found'));

await expect(branch('token', 'MonkeyECX', 'missing')).rejects.toThrow(
'repo not found'
);
});
});
110 changes: 109 additions & 1 deletion __tests__/commits.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,112 @@
import {getCommits, getIssuesAndPullRequests} from '../src/commits';
const mockRequest = jest.fn();

jest.mock('octokit', () => ({
Octokit: jest.fn().mockImplementation(() => ({request: mockRequest}))
}));

jest.mock('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn()
}));

import * as core from '@actions/core';
import {
commits,
extractIssueKeys,
getCommits,
getIssuesAndPullRequests
} from '../src/commits';

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

it('returns unique repositories found in commits and pull requests', async () => {
mockRequest
.mockResolvedValueOnce({
data: [
{
commit: {
message: 'Implement TR-7359'
}
}
]
})
.mockResolvedValueOnce({
data: {
items: [
{
repository: {
full_name: 'MonkeyECX/api'
}
}
]
}
})
.mockResolvedValueOnce({
data: {
items: []
}
})
.mockResolvedValueOnce({
data: {
items: [
{
repository_url: 'https://api.github.com/repos/MonkeyECX/api'
},
{
repository_url: 'https://api.github.com/repos/MonkeyECX/front'
}
]
}
})
.mockResolvedValueOnce({
data: {
items: []
}
});

await expect(
commits('token', 'sha', 'MonkeyECX/current')
).resolves.toStrictEqual(['MonkeyECX/api', 'MonkeyECX/front']);
});

it('does not search repositories when the commit message has no Jira key', async () => {
mockRequest.mockResolvedValueOnce({
data: [
{
commit: {
message: 'Refactor 16-17 flat parameters'
}
}
]
});

await expect(
commits('token', 'sha', 'MonkeyECX/current')
).resolves.toBeUndefined();

expect(mockRequest).toHaveBeenCalledTimes(1);
expect(core.warning).toHaveBeenCalledWith('String does not contain issueKeys');
});
});

describe('extractIssueKeys', () => {
it('extracts Jira keys that start with letters', () => {
expect(
extractIssueKeys('Fix TR-7359 and follow up on FM-4896')
).toStrictEqual(['TR-7359', 'FM-4896']);
});

it('does not treat numeric ranges as Jira keys', () => {
expect(
extractIssueKeys(
'Step1Origin and Step2Parameters took 16-17 flat parameters each'
)
).toBeUndefined();
});
});

describe('getCommits', () => {
it('does not request search results beyond GitHub first 1000 results limit', async () => {
Expand Down
48 changes: 48 additions & 0 deletions __tests__/dispatch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const request = jest.fn();

jest.mock('octokit', () => ({
Octokit: jest.fn().mockImplementation(() => ({request}))
}));

import {dispatch} from '../src/dispatch';

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

it('dispatches a workflow with the provided ref and inputs', async () => {
const response = {status: 204};
request.mockResolvedValue(response);

await expect(
dispatch('token', 'MonkeyECX', 'service', 'deploy.yml', 'main', {
foo: 'bar'
})
).resolves.toBe(response);

expect(request).toHaveBeenCalledWith(
'POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches',
{
owner: 'MonkeyECX',
repo: 'service',
workflow_id: 'deploy.yml',
ref: 'main',
inputs: {
foo: 'bar'
},
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
}
);
});

it('rethrows request errors with their original message', async () => {
request.mockRejectedValue(new Error('workflow missing'));

await expect(
dispatch('token', 'MonkeyECX', 'service', 'deploy.yml', 'main', {})
).rejects.toThrow('workflow missing');
});
});
69 changes: 69 additions & 0 deletions __tests__/get-inputs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const getInput = jest.fn();
const info = jest.fn();

jest.mock('@actions/core', () => ({
getInput,
info
}));

import {getInputs, showInputs} from '../src/get-inputs';

describe('getInputs', () => {
beforeEach(() => {
jest.clearAllMocks();
getInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
github_token: 'token',
commit_sha: 'abc123',
workflow_id: 'deploy.yml',
actual_repository: 'MonkeyECX/service',
trigger_workflow: 'TRUE'
};

return inputs[name] || '';
});
});

it('maps GitHub action inputs to the internal inputs object', () => {
expect(getInputs()).toStrictEqual({
GithubToken: 'token',
CommitSHA: 'abc123',
WorkflowID: 'deploy.yml',
ActualRepository: 'MonkeyECX/service',
TriggerWorkflow: true
});
});

it('parses trigger_workflow as false by default', () => {
getInput.mockImplementation((name: string) =>
name === 'trigger_workflow' ? '' : 'value'
);

expect(getInputs().TriggerWorkflow).toBe(false);
});

it('rejects conflicting jekyll options', () => {
getInput.mockImplementation((name: string) =>
['enable_jekyll', 'disable_nojekyll'].includes(name) ? 'true' : ''
);

expect(() => getInputs()).toThrow('Use either of enable_jekyll or disable_nojekyll');
});
});

describe('showInputs', () => {
it('logs all non-secret inputs', () => {
showInputs({
GithubToken: 'secret',
CommitSHA: 'abc123',
WorkflowID: 'deploy.yml',
ActualRepository: 'MonkeyECX/service',
TriggerWorkflow: false
});

expect(info).toHaveBeenCalledWith(expect.stringContaining('CommitSHA: abc123'));
expect(info).toHaveBeenCalledWith(
expect.not.stringContaining('secret')
);
});
});
Loading
Loading