Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c2e4171
feat: add conventional commit validation workflow
chris11-taylor-nttd Jan 27, 2026
14049e9
fix: do not require task_types
chris11-taylor-nttd Jan 27, 2026
69a0f6b
fix: github-script over node -e
chris11-taylor-nttd Jan 27, 2026
a23ffaa
fix: bump action version
chris11-taylor-nttd Jan 27, 2026
72cef32
fix: continue-on-error
chris11-taylor-nttd Jan 27, 2026
4a0ccee
fix: rework if statements
chris11-taylor-nttd Jan 27, 2026
aefa512
fix: rework if statements
chris11-taylor-nttd Jan 27, 2026
4c559ed
fix: adjust how comment is constructed
chris11-taylor-nttd Jan 27, 2026
316708d
fix: adjust how comment is constructed
chris11-taylor-nttd Jan 27, 2026
f8f7e70
fix: escape backticks
chris11-taylor-nttd Jan 27, 2026
202ca46
adjust verbiage
chris11-taylor-nttd Jan 27, 2026
b93412b
fix: reorder comment retrieval, use to populate replacement
chris11-taylor-nttd Jan 27, 2026
f932088
fix: stray dot
chris11-taylor-nttd Jan 27, 2026
9946808
fix: pr title in comment
chris11-taylor-nttd Jan 27, 2026
723ffc8
fix: cleanup behavior
chris11-taylor-nttd Jan 27, 2026
e56cb34
fix: pass token
chris11-taylor-nttd Jan 27, 2026
a03a5bb
fix: default
chris11-taylor-nttd Jan 27, 2026
e50147f
fix: clear old scope labels by default
chris11-taylor-nttd Jan 27, 2026
e5326bf
fix: clear old scope labels by default
chris11-taylor-nttd Jan 27, 2026
d202360
fix: pass token
chris11-taylor-nttd Jan 27, 2026
a10416c
fix: failure comment only if the validation fails, not prior steps
chris11-taylor-nttd Jan 27, 2026
39c6f61
fix: adjust label removal
chris11-taylor-nttd Jan 27, 2026
44f2b99
fix: adjust label removal
chris11-taylor-nttd Jan 27, 2026
6e0a32d
fix: add note about breaking changes
chris11-taylor-nttd Jan 27, 2026
e0a6471
docs: create workflow config, usage doc
chris11-taylor-nttd Jan 27, 2026
e493413
fix: normalize workflow names
chris11-taylor-nttd Jan 27, 2026
9986d4b
doc: required workflow note
chris11-taylor-nttd Jan 27, 2026
502a9a2
doc: adjust image path
chris11-taylor-nttd Jan 27, 2026
a10dcc2
doc: adjust image path again
chris11-taylor-nttd Jan 27, 2026
c6db9b5
doc: adjust image path yet again
chris11-taylor-nttd Jan 27, 2026
29bd52f
Merge branch 'main' into feat/pr-conventional-commit-title
chris11-taylor-nttd Jan 28, 2026
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
164 changes: 164 additions & 0 deletions .github/workflows/reusable-pr-conventional-commit-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
on:
workflow_call:
inputs:
task_types:
type: string
description: 'A JSON array of task types that are allowed. Default: ["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
required: false
default: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
scope_types:
type: string
description: 'A JSON array of valid scope types. If omitted, any scope is allowed. If given, include the empty scope with "". Example: ["login","signup","checkout","payment", ""]'
required: false
default: ""
title_regex:
type: string
description: 'Regular expression for custom title validation; disabled by default. Example: ''PROJECT-\d{2,5}$'' for trailing issue ticket, ''"^[^:]+: [A-Z]"'' for capitalized title'
required: false
default: ""
add_label:
type: boolean
description: "Whether to add labels to your pull request. Default: true"
required: false
default: true
add_scope_label:
type: boolean
description: 'Whether to add labels to your pull request. Example: the ''login'' scope will be added for this PR title: "fix(login): fix message in login page". Default: true'
required: false
default: true
custom_labels:
type: string
description: 'A JSON object mapping task types to custom label names. Default: {"feat": "feature", "fix": "fix", "docs": "documentation", "test": "test", "ci": "CI/CD", "refactor": "refactor", "perf": "performance", "chore": "chore", "revert": "revert", "wip": "WIP"}'
required: false
default: '{"feat": "feature", "fix": "fix", "docs": "documentation", "test": "test", "ci": "CI/CD", "refactor": "refactor", "perf": "performance", "chore": "chore", "revert": "revert", "wip": "WIP"}'
comment_on_failure:
type: boolean
description: "Whether to comment on the pull request if the title validation fails. Default: true"
required: false
default: true
clear_labels:
type: boolean
description: "Whether to clear existing labels when the workflow is run. This helps clean up scope labels added by this action when the scope changes. Default: true"
required: false
default: true

permissions:
contents: read
pull-requests: write

jobs:
validate-title:
name: Validate PR Title
runs-on: ubuntu-latest
steps:
- name: Validate inputs
uses: actions/github-script@v8
with:
script: |
const taskTypes = `${{ inputs.task_types }}`;
const scopeTypes = `${{ inputs.scope_types }}`;
const titleRegex = `${{ inputs.title_regex }}`;
const customLabels = `${{ inputs.custom_labels }}`;

// Validate task_types is a valid JSON array
if (taskTypes) {
try {
const val = JSON.parse(taskTypes);
if (!Array.isArray(val)) throw new Error('Not an array');
} catch(e) {
core.setFailed("Input 'task_types' is not a valid JSON array.");
}
}

// Validate scope_types is a valid JSON array (if provided)
if (scopeTypes) {
try {
const val = JSON.parse(scopeTypes);
if (!Array.isArray(val)) throw new Error('Not an array');
} catch(e) {
core.setFailed("Input 'scope_types' is not a valid JSON array.");
}
}

// Validate title_regex is a valid regular expression (if provided)
if (titleRegex) {
try {
new RegExp(titleRegex);
} catch(e) {
core.setFailed("Input 'title_regex' is not a valid regular expression.");
}
}

// Validate custom_labels is a valid JSON object (if provided)
if (customLabels) {
try {
const val = JSON.parse(customLabels);
if (typeof val !== 'object' || val === null || Array.isArray(val)) throw new Error('Not an object');
} catch(e) {
core.setFailed("Input 'custom_labels' is not a valid JSON object.");
}
}

- name: Clear labels
id: clear-labels
if: ${{ inputs.clear_labels }}
env:
GH_TOKEN: ${{ github.token }}
run: |
echo '{"labels":[]}' | gh api -X PUT repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels --input -

- name: PR Conventional Commit Validation
id: pr-validation
uses: ytanikin/pr-conventional-commits@1.5.1
with:
task_types: ${{ inputs.task_types }}
scope_types: ${{ inputs.scope_types }}
title_regex: ${{ inputs.title_regex }}
add_label: ${{ inputs.add_label }}
add_scope_label: ${{ inputs.add_scope_label }}
token: ${{ github.token }}
custom_labels: ${{ inputs.custom_labels }}

- name: Create failure comment based on inputs
id: create-failure-comment
if: ${{ failure() && inputs.comment_on_failure }}
run: |
cat << EOC >> pr-title-failure-comment.md
### ❌ Pull Request Title Validation Failed
Your pull request title (\`${{ github.event.pull_request.title }}\`) does not conform to the Conventional Commits specification.

Please ensure your title follows the format: \`<type>(<scope>): <description>\`.
- **Type** must be one of the following: ${{ inputs.task_types }}
${{ inputs.scope_types && format('- **Scope** must be one of the following: {0}', inputs.scope_types) || '- **Scope** is optional and can be any value or omitted entirely.' }}
- **Description** should be a brief summary of the changes.
- If you have a breaking change, ensure the exclamation mark (\`!\`) is placed immediately after the type or scope, e.g., \`feat!: ...\` or \`feat(scope)!: ...\`.
${{ inputs.title_regex && format('- Additionally, your description must match the custom regex: `{0}`.\n', inputs.title_regex) || '' }}
If you have any questions, please refer to the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) documentation for more details.
EOC

- name: Find previous failure comment
id: find-comment
if: ${{ always() && inputs.comment_on_failure }}
uses: peter-evans/find-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: "Pull Request Title Validation Failed"

- name: Comment on Failure
if: ${{ failure() && steps.pr-validation.outcome == 'failure' && inputs.comment_on_failure }}
uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
body-file: pr-title-failure-comment.md
edit-mode: replace

- name: Cleanup Failure Comment on Success
if: ${{ success() && steps.find-comment.outcome == 'success' && inputs.comment_on_failure }}
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ steps.find-comment.outputs.comment-id }}" != "" ]; then
gh api -X DELETE repos/${{ github.repository }}/issues/comments/${{ steps.find-comment.outputs.comment-id }} --silent
fi
Binary file added docs/images/all-required-checks-passed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/required-status-check.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions docs/reusable-pr-conventional-commit-title.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Enforce Conventional Commits on PR titles

This workflow will ensure that your pull request's title matches the Conventional Commits specification. This format is often used to drive downstream workflows like automated versioning and changelogs.

This is intended for use with repositories that utilize Squash merges, as the title of the pull request is what lands on the base branch when a PR is merged.

## Configuration

This workflow exposes several configuration items that you can supply through your repository's workflow invocation that will be passed through to the underlying [ytanikin/pr-conventional-commits](https://github.com/ytanikin/pr-conventional-commits) workflow:

| Configuration Key | Type | Default Value | Notes |
|-------------------|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| task_types | string (JSON array) | ["feat","fix","docs","test","ci","refactor","perf","chore","revert"] | Configures the valid prefixes for your commit message. Prefixes can be given a particular label by using the `custom_labels` setting below. |
| scope_types | string (JSON array) | "" | Valid scopes, e.g. `["login", "signup", "checkout", "payment"]`. If not supplied, any scope is valid. |
| title_regex | string | "" | Regular expression to apply to the title. Typically this is used to validate a ticket number is included, e.g. `.+ JIRA-\d{3,6} .+`. If not specified, bypasses regular expression checking. |
| add_label | boolean | true | Whether to add a label to the pull request with the type of the pull request. |
| add_scope_label | boolean | true | Whether to add a label to the pull request for the scope (if supplied). |
| custom_labels | string (JSON map) | {"feat": "feature", "fix": "fix", "docs": "documentation", "test": "test",<br> "ci": "CI/CD", "refactor": "refactor", "perf": "performance", "chore": "chore",<br> "revert": "revert", "wip": "WIP"} | A map of task_types and their stylized label text. With the default values, a task_type of `perf` will result in a label called `Performance`. |

There are two additional configurations that are not passed to the underlying workflow, but control additional behaviors:

| Configuration Key | Type | Default Value | Notes |
|--------------------|---------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| comment_on_failure | boolean | true | Whether to comment on the pull request if the title validation fails. |
| clear_labels | boolean | true | Whether to clear the existing labels on the pull request when the workflow is run. This helps clean up scope labels added by this action when the scope changes, as they persist if you rename your pull request and change the scope. If you have another labeling solution, turn this off. |


## Usage

To validate your PR titles using our default configuration, add the following workflow to your repository:

```yaml
name: Validate PR Title

on:
pull_request:
types: [opened, reopened, edited]

jobs:
validate-title:
name: Validate PR Title
permissions:
contents: read
pull-requests: write
uses: launchbynttdata/launch-workflows/.github/workflows/reusable-pr-conventional-commit-title.yml@ref
```

Be sure you replace `ref` with an appropriate ref to this repository.

> [!CAUTION]
> By default, your repository likely does not require this workflow to succeed before a change is merged. By making this workflow required, you ensure that a successful run must be achieved prior to merge, which ensures your commit messages are consistent!
>
> To make this workflow required, visit your repository's settings and create a new Ruleset with a required status check, as shown below:

![Required status check in the GitHub settings page](images/required-status-check.png)

By doing so, you should see the `Required` indicator on your pull request's checks section, as shown below:

![Pull request with a required status check showing a passing state](images/all-required-checks-passed.png)
Loading