Skip to content

Commit

Permalink
chore: label prs based on conventional commit (#1121)
Browse files Browse the repository at this point in the history
<!-- markdownlint-disable MD041 -->
#### What this PR does / why we need it

This makes sure that PR titles pass the conventional commit
specification and we add labels based on its conventional context.

The following structure is used for this verification:

```
// We know this regex looks scary, but it's just to match the Conventional Commit format
// It parses out a Title into several named regex groups, which we can use to extract various semantic patterns:
// - type: The type of change (feat, fix, etc.)
// - scope: The scope of the change (optional and set in brackets)
// - breaking: A flag to indicate a breaking change (!)
// - subject: The subject of the change
// Example: feat(scope)!: add new feature
//          ^^^^ ^^^^^ ^  ^^^^^^^^^^^^^^^
//          type scope    subject
const regex = new RegExp(
`^(((Initial commit)|(Merge [^\\r\\n]+(\\s)[^\\r\\n]+((\\s)((\\s)[^\\r\\n]+)+)*(\\s)?)|^((?<type>${allowedTypes})(\\((?<scope>[\\w\\-]+)\\))?(?<breaking>!?): (?<subject>[^\\r\\n]+((\\s)((\\s)[^\\r\\n]+)+)*))(\\s)?)$)`
´);
```

Success Example:

```
Verify that the PR title follows the Conventional Commit format
Type-to-Label Mapping: {
  feat: 'kind/enhancement',
  fix: 'fix',
  chore: 'chore',
  docs: 'kind/documentation',
  test: 'kind/test',
  perf: 'kind/performance'
}
Scope-to-Label Mapping: { deps: 'dependencies' }
Allowed Types: feat|fix|chore|docs|test|perf
PR Title: chore(deps)!: bump the go group across 1 directory with 13 updates
Regex: /^(((Initial commit)|(Merge [^\r\n]+(\s)[^\r\n]+((\s)((\s)[^\r\n]+)+)*(\s)?)|^((?<type>feat|fix|chore|docs|test|perf)(\((?<scope>[\w\-]+)\))?(?<breaking>!?): (?<subject>[^\r\n]+((\s)((\s)[^\r\n]+)+)*))(\s)?)$)/
Match: true
Adding breaking change label
Adding labels: breaking,chore,dependencies
```

Failure Example:

```
Verify that the PR title follows the Conventional Commit format
Type-to-Label Mapping: {
  feat: 'kind/enhancement',
  fix: 'fix',
  chore: 'chore',
  docs: 'kind/documentation',
  test: 'kind/test',
  perf: 'kind/performance'
}
Scope-to-Label Mapping: { deps: 'dependencies' }
Allowed Types: feat|fix|chore|docs|test|perf
PR Title: my invalid specification
Regex: /^(((Initial commit)|(Merge [^\r\n]+(\s)[^\r\n]+((\s)((\s)[^\r\n]+)+)*(\s)?)|^((?<type>feat|fix|chore|docs|test|perf)(\((?<scope>[\w\-]+)\))?(?<breaking>!?): (?<subject>[^\r\n]+((\s)((\s)[^\r\n]+)+)*))(\s)?)$)/
Match: false
Invalid PR title format. Make sure you named the PR after the specification at https://www.conventionalcommits.org/en/v1.0.0/#specification. Exiting...
```

Example runs:
-
https://github.com/open-component-model/ocm-cicd-playground/actions/runs/11956664562/job/33331956823?pr=11
-
https://github.com/open-component-model/ocm-cicd-playground/actions/runs/11956694707/job/33332056532?pr=11
-
https://github.com/open-component-model/ocm-cicd-playground/actions/runs/11956707588/job/33332099851?pr=11
#### Which issue(s) this PR fixes
<!--
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

This makes sure that our Pull Request Titles are correctly setup via
conventional commits and also automatically labelled.

To configure its label settings (which labels to apply) you can use the
following env variables

```
      - uses: actions/github-script@v7
        env:
          TYPE_TO_LABEL: |
            {
              "feat":"kind/enhancement",
              "fix":"fix",
              "chore":"chore",
              "docs":"kind/documentation",
              "test":"kind/test",
              "perf":"kind/performance"
            }
          SCOPE_TO_LABEL: |
            {
              "deps":"dependencies"
            }
          BREAKING_CHANGE_LABEL: "breaking"
          # ...
```
  • Loading branch information
jakobmoellerdev authored Nov 21, 2024
1 parent 2334bd0 commit a065a76
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 20 deletions.
6 changes: 0 additions & 6 deletions .github/config/labeler.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
# see https://github.com/actions/labeler?tab=readme-ov-file#match-object to configure correctly
feature:
- head-branch: 'feature/*'
fix:
- head-branch: 'fix/*'
chore:
- head-branch: 'chore/*'
dependencies:
- any:
- head-branch: 'dependencies/*'
Expand Down
114 changes: 100 additions & 14 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,104 @@ permissions:
issues: write

jobs:
branch-name-labeler:
conventional-commit-labeler:
name: Label PR based on Conventional Commit Specification
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
env:
TYPE_TO_LABEL: |
{
"feat":"kind/enhancement",
"fix":"fix",
"chore":"chore",
"docs":"kind/documentation",
"test":"kind/test",
"perf":"kind/performance"
}
SCOPE_TO_LABEL: |
{
"deps":"dependencies"
}
BREAKING_CHANGE_LABEL: "breaking"
with:
script: |
console.log("Verify that the PR title follows the Conventional Commit format");
// Parse mappings from environment variables
const typeToLabel = JSON.parse(process.env.TYPE_TO_LABEL);
const scopeToLabel = JSON.parse(process.env.SCOPE_TO_LABEL);
console.log("Type-to-Label Mapping:", typeToLabel);
console.log("Scope-to-Label Mapping:", scopeToLabel);
// Dynamically generate allowed types
const allowedTypes = Object.keys(typeToLabel).join('|');
console.log(`Allowed Types: ${allowedTypes}`);
const prTitle = context.payload.pull_request.title;
console.log(`PR Title: ${prTitle}`);
// We know this regex looks scary, but it's just to match the Conventional Commit format
// It parses out a Title into several named regex groups, which we can use to extract various semantic patterns:
// - type: The type of change (feat, fix, etc.)
// - scope: The scope of the change (optional and set in brackets)
// - breaking: A flag to indicate a breaking change (!)
// - subject: The subject of the change
// Example: feat(scope)!: add new feature
// ^^^^ ^^^^^ ^ ^^^^^^^^^^^^^^^
// type scope subject
const regex = new RegExp(
`^(((Initial commit)|(Merge [^\\r\\n]+(\\s)[^\\r\\n]+((\\s)((\\s)[^\\r\\n]+)+)*(\\s)?)|^((?<type>${allowedTypes})(\\((?<scope>[\\w\\-]+)\\))?(?<breaking>!?): (?<subject>[^\\r\\n]+((\\s)((\\s)[^\\r\\n]+)+)*))(\\s)?)$)`
);
console.log(`Regex: ${regex}`);
const match = prTitle.match(regex);
console.log(`Match: ${match != null}`);

if (match && match.groups) {
const { type, scope, breaking } = match.groups;

// Initialize labels array
const labels = [];

if (breaking) {
console.log("Adding breaking change label");
labels.push(process.env.BREAKING_CHANGE_LABEL);
}

// Add type-based label
if (type && typeToLabel[type]) {
labels.push(typeToLabel[type]);
} else {
console.log(`No label found for type: ${type}`);
}

// Add scope-based label if scope exists
if (scope && scopeToLabel[scope]) {
labels.push(scopeToLabel[scope]);
} else if (scope) {
console.log(`No label found for scope: ${scope}`);
}

if (labels.length > 0) {
console.log(`Adding labels: ${labels}`);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: labels,
});
} else {
console.log("No labels to add.");
}
} else {
console.log("Invalid PR title format. Make sure you named the PR after the specification at https://www.conventionalcommits.org/en/v1.0.0/#specification. Exiting...");
process.exit(1);
}
labeler:
name: Label PR based on Config
permissions:
contents: read
Expand Down Expand Up @@ -54,23 +151,12 @@ jobs:
# github_api_url: 'api.github.com'
# files_to_ignore: ''
verify-labels:
needs: [branch-name-labeler, size-labeler]
needs: [labeler, size-labeler, conventional-commit-labeler]
name: verify labels
runs-on: ubuntu-latest
steps:
- name: PRs should have at least one qualifying label
uses: docker://agilepathway/pull-request-label-checker:latest
with:
one_of: chore,fix,bugfix,bug,kind/bug,feature,enhancement,kind/enhancement,dependencies
repo_token: ${{ secrets.GITHUB_TOKEN }}
semantic-pr-title:
name: ensure pr conforms to semantic commit style
runs-on: ubuntu-latest
steps:
# This enforces Semantic Pull Request titles:
# see https://github.com/amannn/action-semantic-pull-request?tab=readme-ov-file#examples for examples
# see https://www.conventionalcommits.org/en/v1.0.0/#specification for the full specification
# We want this because we use squashing so all squashed commits should conform to our commit style
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}

0 comments on commit a065a76

Please sign in to comment.