Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow passing PR title as arg to skip API request #20

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ jobs:

## Arguments

| Argument | Required | Example | Purpose |
| --------------------- | -------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `commitlintRulesPath` | No | `'./commitlint.rules.js'` | A relative path from the repo root to a file containing custom Commitlint rules to override the default ([docs](#customising-lint-rules)) |
| `scopeRegex` | No | `'[A-Z]+-[0-9]+'` | A JS regex (without slashes or flags) used to lint the PR scope ([docs](#linting-scope)) |
| `enforcedScopeTypes` | No | `'feat\|fix'` | A list of PR types where the scope is always required and linted ([docs](#skipping-scope-linting)) |
| Argument | Required | Example | Purpose |
| --------------------- | -------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `prTitle` | No | `${{ github.event.pull_request.title }}` | The title of the pull request if not using a Github token ([docs](#providing-a-pr-title)) |
| `commitlintRulesPath` | No | `'./commitlint.rules.js'` | A relative path from the repo root to a file containing custom Commitlint rules to override the default ([docs](#customising-lint-rules)) |
| `scopeRegex` | No | `'[A-Z]+-[0-9]+'` | A JS regex (without slashes or flags) used to lint the PR scope ([docs](#linting-scope)) |
| `enforcedScopeTypes` | No | `'feat\|fix'` | A list of PR types where the scope is always required and linted ([docs](#skipping-scope-linting)) |

## Usage

Expand All @@ -55,6 +56,17 @@ You can find the option in the root General settings of your Github repo.

> Pull Requests > Allow Squash Merging > Default Commit Message > Pull Request Title

### Providing a PR title

The action allows you to provide the PR title in two ways. You can either include `GITHUB_TOKEN` as an environment variable, which will use the Github API to retrieve the title of the PR, or you can manually pass the title of the pull request as an argument.

```yaml
with:
prTitle: ${{ github.event.pull_request.title }}
```

If a `prTitle` arg is provided, the API request will be skipped. This can be useful to avoid API rate limiting with shared Github access tokens.

### Customising lint rules

Out of the box the action follows the [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) rules, however you can pass a config of override rules to customise to your needs, using the `commitlintRulesPath` arg.
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ runs:
using: 'node20'
main: 'dist/index.js'
inputs:
prTitle:
description: 'Manually provide a PR title as an arg to skip making an API request, using ${{ github.event.pull_request.title }}'
required: false
commitlintRulesPath:
description: 'Relative path to commitlint rules file'
required: false
Expand Down
36 changes: 31 additions & 5 deletions src/lint.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { setFailed, warning, error, info } from '@actions/core';
import { getOctokit } from '@actions/github';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { lint } from './lint';

Expand Down Expand Up @@ -49,22 +50,46 @@ vi.mock('@actions/github', async importOriginal => {
};
});

const mockArgs = ['TOKEN', './', './src/fixtures/commitlint.rules.js'];
const mockArgs = [
'TOKEN',
'./',
undefined,
'./src/fixtures/commitlint.rules.js'
];

describe('Linter', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
});

it('should find and log the PR title', async () => {
it('should skip the API request if PR title provided as an arg and log the PR title', async () => {
mocks.getOctokit.mockImplementation(vi.fn());

const mockArgsWithManualTitle = [
...mockArgs.filter(arg => arg).slice(0, 2),
'chore(FOO-1234): hello i am a valid title passed manually',
...mockArgs
.filter(arg => arg)
.slice(2, mockArgs.filter(arg => arg).length)
];

await lint.apply(null, mockArgsWithManualTitle);

expect(getOctokit).not.toHaveBeenCalled();
expect(info).toHaveBeenCalledWith(
'🕵️ Found PR title in action args: "chore(FOO-1234): hello i am a valid title passed manually"'
);
});

it('should retrieve the pull request from the API if not passed as an arg and log the PR title', async () => {
mocks.getOctokit.mockReturnValue({
rest: {
pulls: {
get: vi.fn().mockReturnValue({
data: {
commits: 1,
title: 'feat(BAR-1234): hello i am a valid title'
title: 'feat(BAR-1234): hello i am a valid title from the API'
}
})
}
Expand All @@ -73,8 +98,9 @@ describe('Linter', () => {

await lint.apply(null, mockArgs);

expect(getOctokit).toHaveBeenCalledWith(mockArgs[0]);
expect(info).toHaveBeenCalledWith(
'🕵️ Found PR title: "feat(BAR-1234): hello i am a valid title"'
'🕵️ Found PR title from Github API: "feat(BAR-1234): hello i am a valid title from the API"'
);
});

Expand Down Expand Up @@ -129,7 +155,7 @@ describe('Linter', () => {

await lint.apply(
null,
mockArgs.filter(arg => arg !== mockArgs[1])
mockArgs.filter(arg => arg && !arg.includes('commitlint.rules.js'))
);

expect(info).toHaveBeenCalledWith('📋 Checking PR title with commitlint');
Expand Down
62 changes: 34 additions & 28 deletions src/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
logLintableScopeFound,
logLintingPrTitle,
logLintingPrTitleWithCustomRules,
logPrTitleFound,
logPrTitleFoundArg,
logPrTitleFoundApi,
logScopeCheckSkipped
} from './outputs/logs';
import {
Expand All @@ -32,35 +33,44 @@ import { errorLinting } from './outputs/errors';
const lint = async (
githubToken?: string,
githubWorkspace?: string,
prTitle?: string,
rulesPath?: string,
enforcedScopeTypes?: Array<string>,
scopeRegex?: RegExp
) => {
if (!githubToken) {
return setFailedMissingToken();
}
let pullRequestTitle = prTitle;

const octokit = github.getOctokit(githubToken);
if (pullRequestTitle) {
logPrTitleFoundArg(pullRequestTitle);
} else {
if (!githubToken) {
return setFailedMissingToken();
}

if (!github.context.payload.pull_request) {
return setFailedPrNotFound();
}
const octokit = github.getOctokit(githubToken);

const {
number: pullNumber,
base: {
user: { login: owner },
repo: { name: repo }
if (!github.context.payload.pull_request) {
return setFailedPrNotFound();
}
} = github.context.payload.pull_request;

const { data: pullRequest } = await octokit.rest.pulls.get({
owner,
repo,
pull_number: pullNumber
});
const {
number: pullNumber,
base: {
user: { login: owner },
repo: { name: repo }
}
} = github.context.payload.pull_request;

const { data: pullRequest } = await octokit.rest.pulls.get({
owner,
repo,
pull_number: pullNumber
});

pullRequestTitle = pullRequest.title;

logPrTitleFound(pullRequest.title);
logPrTitleFoundApi(pullRequestTitle);
}

const commitlintRules = await getLintRules(rulesPath, githubWorkspace);

Expand All @@ -78,13 +88,9 @@ const lint = async (
conventionalChangelog: { parserOpts }
} = await createPreset(null, null);

const lintOutput = await commitlint(
pullRequest.title,
commitlintRules.rules,
{
parserOpts
}
);
const lintOutput = await commitlint(pullRequestTitle, commitlintRules.rules, {
parserOpts
});
lintOutput.warnings.forEach(warn => warnLinting(warn.message));
lintOutput.errors.forEach(err => errorLinting(err.message));

Expand All @@ -94,7 +100,7 @@ const lint = async (
return setFailedDoesNotMatchSpec();
}

const { scope, type } = conventionalCommitsParser.sync(pullRequest.title);
const { scope, type } = conventionalCommitsParser.sync(pullRequestTitle);

if (
!enforcedScopeTypes ||
Expand Down
5 changes: 3 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ try {
const {
githubToken,
githubWorkspace,
prTitle,
rulesPath,
enforcedScopeTypes,
scopeRegex
scopeRegex,
} = getActionConfig();

lint(githubToken, githubWorkspace, rulesPath, enforcedScopeTypes, scopeRegex);
lint(githubToken, githubWorkspace, prTitle, rulesPath, enforcedScopeTypes, scopeRegex);
} catch (e) {
core.setFailed(`Failed to run action with error: ${e}`);
}
16 changes: 12 additions & 4 deletions src/outputs/logs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
logLintableScopeFound,
logLintingPrTitle,
logLintingPrTitleWithCustomRules,
logPrTitleFound,
logPrTitleFoundArg,
logPrTitleFoundApi,
logScopeCheckSkipped
} from './logs';

Expand All @@ -24,10 +25,17 @@ describe('Log outputs', () => {
vi.resetAllMocks();
});

it('`logPrTitleFound` should pass the expected log to the output', () => {
logPrTitleFound(`fix(CDV-2812): Get with friends`);
it('`logPrTitleFoundArg` should pass the expected log to the output', () => {
logPrTitleFoundArg(`fix(CDV-2812): Get with friends`);
expect(info).toHaveBeenCalledWith(
`🕵️ Found PR title: "fix(CDV-2812): Get with friends"`
`🕵️ Found PR title in action args: "fix(CDV-2812): Get with friends"`
);
});

it('`logPrTitleFoundApi` should pass the expected log to the output', () => {
logPrTitleFoundApi(`fix(CDV-2812): Get with friends`);
expect(info).toHaveBeenCalledWith(
`🕵️ Found PR title from Github API: "fix(CDV-2812): Get with friends"`
);
});

Expand Down
7 changes: 5 additions & 2 deletions src/outputs/logs.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as core from '@actions/core';
import { Commit } from 'conventional-commits-parser';

export const logPrTitleFound = (title: string) =>
core.info(`🕵️ Found PR title: "${title}"`);
export const logPrTitleFoundArg = (title: string) =>
core.info(`🕵️ Found PR title in action args: "${title}"`);

export const logPrTitleFoundApi = (title: string) =>
core.info(`🕵️ Found PR title from Github API: "${title}"`);

export const logLintingPrTitle = () =>
core.info(`📋 Checking PR title with commitlint`);
Expand Down
22 changes: 19 additions & 3 deletions src/utils/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ describe('Config utils', () => {
process.env.INPUT_COMMITLINTRULESPATH = './commitlint.rules.js';
process.env.GITHUB_TOKEN = 'asdf';
process.env.GITHUB_WORKSPACE = './';
process.env.INPUT_PR_TITLE = 'chore(TEST-1234): Manually pass a PR title'
});

it('`getActionConfig` returns a valid config object with required values', () => {
const config = getActionConfig();
expect(config).toMatchObject({
rulesPath: expect.any(String),
githubToken: expect.any(String),
githubWorkspace: expect.any(String)
githubWorkspace: expect.any(String),
prTitle: expect.any(String)
});
});

Expand All @@ -27,7 +29,8 @@ describe('Config utils', () => {
enforcedScopeTypes: expect.any(Array),
rulesPath: expect.any(String),
githubToken: expect.any(String),
githubWorkspace: expect.any(String)
githubWorkspace: expect.any(String),
prTitle: expect.any(String)
});
expect(config.enforcedScopeTypes).toEqual(['feat', 'fix']);
});
Expand All @@ -40,7 +43,20 @@ describe('Config utils', () => {
scopeRegex: expect.any(RegExp),
rulesPath: expect.any(String),
githubToken: expect.any(String),
githubWorkspace: expect.any(String)
githubWorkspace: expect.any(String),
prTitle: expect.any(String)
});
});

it('`getActionConfig` returns a valid config object with `undefined` PR title if not provided', () => {
delete process.env.INPUT_PR_TITLE;

const config = getActionConfig();
expect(config).toMatchObject({
rulesPath: expect.any(String),
githubToken: expect.any(String),
githubWorkspace: expect.any(String),
prTitle: undefined
});
});
});
1 change: 1 addition & 0 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const getActionConfig = () => {
return {
githubToken: process.env.GITHUB_TOKEN,
githubWorkspace: process.env.GITHUB_WORKSPACE,
prTitle: process.env.INPUT_PR_TITLE,
rulesPath: process.env.INPUT_COMMITLINTRULESPATH,
...(enforcedScopeTypes ? { enforcedScopeTypes } : {}),
...(scopeRegex ? { scopeRegex } : {})
Expand Down