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

Assigns labels based on branch names #203

Merged
merged 119 commits into from May 24, 2023
Merged
Show file tree
Hide file tree
Changes from 117 commits
Commits
Show all changes
119 commits
Select commit Hold shift + click to select a range
2bf42e4
Add branch to MatchConfig interface
joshdales Aug 7, 2021
ad73546
Add function for checking branches
joshdales Aug 7, 2021
6c50d09
include checkBranch in checkMatch
joshdales Aug 7, 2021
ee0e0eb
Add a new fixture and test for the branch checking
joshdales Aug 7, 2021
a01b9ae
Add another test to make sure that partial branch naming works
joshdales Aug 7, 2021
bce88a9
Update new test description
joshdales Aug 7, 2021
765934f
Run build
joshdales Aug 7, 2021
827e118
Fix PR branch labeler
amiel Sep 15, 2021
8aa7614
Run build
amiel Sep 15, 2021
cb5f448
Use correct branch name and update tests
amiel Sep 15, 2021
2ced1f3
Run build
amiel Sep 15, 2021
d9ed3e8
Format
amiel Sep 15, 2021
79c0cc7
Include a test for branching
amiel Sep 15, 2021
27a1d89
Update src/labeler.ts
amiel Sep 16, 2021
7624214
Merge pull request #1 from bentohq/fix-branch-labeler
joshdales Sep 16, 2021
2d63815
Allow branch config to be an array as well
joshdales Sep 29, 2021
ab49f7a
Add tests for array branh labelling
joshdales Sep 29, 2021
2246b66
Run build
joshdales Sep 29, 2021
89f6b77
minor adjustment for successful branch matching.
joshdales Oct 29, 2021
7aadc17
Update the tests for applying multiple branch based labels
joshdales Oct 29, 2021
71fc664
run the build script
joshdales Oct 29, 2021
818399d
Merge branch 'main' into main
joshdales Jun 11, 2022
6e27606
Update README with reference to branch options
joshdales Jun 11, 2022
0ad789c
Merge branch 'main' into main
joshdales Aug 30, 2022
0861fa5
Run the build command
joshdales Aug 30, 2022
4c74e84
Fix typo in the README
joshdales Sep 22, 2022
7f8d8e4
Merge branch 'main' into main
joshdales Jan 11, 2023
c54c5a2
Run prettier
joshdales Jan 11, 2023
7b1327b
Run the build command
joshdales Jan 11, 2023
8c59ecc
Rename the getBranchName helper
joshdales Jan 28, 2023
f2b2513
Update the matching to use a regexp rather than minimatch
joshdales Jan 28, 2023
2343710
Move all the branch checking into its own file
joshdales Jan 28, 2023
cd3a8df
Create a test file for branch
joshdales Jan 28, 2023
0b6e68d
Add options for getting the head or base branch
joshdales Jan 28, 2023
2daf35a
Add an extra test now that we can check the base branch
joshdales Jan 28, 2023
231de6b
Remove the branch option and replace with just head-branch and base-b…
joshdales Jan 29, 2023
922ffdf
Remove deprecated IMinimatch import from labeler.ts
joshdales Jan 29, 2023
7a5c525
Create new interfaces for MatchConfig
joshdales Jan 29, 2023
969899d
Add the changedFiles key and update logic in labeler
joshdales Jan 29, 2023
7d17531
Update the labeler tests
joshdales Jan 29, 2023
b071d82
Run the build command
joshdales Jan 29, 2023
0eb9d49
reverse the conditions of checkMatch
joshdales Jan 29, 2023
09f0853
Fix some typos in the branch checks
joshdales Feb 19, 2023
ed31b27
Rename some functions and variables to match what they are doing
joshdales Feb 19, 2023
da83a18
Make sure that the changed files config values are an array
joshdales Feb 19, 2023
56347d5
Add unit tests for toBranchMatchConfig
joshdales Feb 21, 2023
8943ca2
Merge pull request #3 from joshdales/new-config-structure
joshdales Feb 21, 2023
e5b1bdd
Update the README with documentation about the new config structure
joshdales Feb 21, 2023
2e10ffb
Reference minimatch in docs
joshdales Mar 3, 2023
2a5bc55
Fix bad test descriptions
joshdales Mar 3, 2023
394a01b
Make getBranchName argument non-optional
joshdales Mar 3, 2023
5e6bdf6
update the example workflow in the readme
joshdales Mar 3, 2023
84e83a9
Merge branch 'main' into main
joshdales Mar 8, 2023
f40b387
Correct errors and typos in the README
joshdales Mar 17, 2023
90ef370
Merge branch 'actions:main' into main
joshdales Mar 18, 2023
3eec5d8
Update the syntax for checking a match
joshdales Mar 15, 2023
fc5eb71
Revert "reverse the conditions of checkMatch"
joshdales Mar 15, 2023
e4486e9
Throw an error if the config is the wrong type
joshdales Mar 18, 2023
e939550
Simplfy the conditions in toBranchMatchConfig
joshdales Mar 18, 2023
51b763c
Update comments in getMatchConfigs to represent updated types
joshdales Mar 18, 2023
c08f5fa
Condense assignment of further config options in toChangedFilesMatchC…
joshdales Mar 18, 2023
9f259ee
Rename checkGlobs to checkMatchConfigs and check each property
joshdales Mar 18, 2023
1ce9b35
Move all changedFiles logic into it's own file
joshdales Mar 18, 2023
ef108a9
Convert the yaml output to a matchConfig in getLabelConfigMapFromObject
joshdales Mar 18, 2023
a988f4e
Add todo tests for changedFiles
joshdales Mar 18, 2023
3af9a47
Add unit tests for toChangedFilesMatchConfig
joshdales Mar 18, 2023
c31ee1f
Add more unit tests for changedFiles.ts
joshdales Mar 18, 2023
17694aa
Update the argument type for toMatchConfig
joshdales Mar 18, 2023
e9a1777
Fix linting and formatting
joshdales Mar 18, 2023
65b7640
Add unit tests for toMatchConfig
joshdales Mar 18, 2023
7741e57
Run the build command
joshdales Mar 18, 2023
64ce5e9
Update README with better description for branches
joshdales Mar 20, 2023
cc1e025
Update object assignment in toChangedFilesMatchConfig
joshdales Mar 20, 2023
d0d3628
Add extra tests and use toEqual matcher.
joshdales Mar 20, 2023
92990c0
Don't allow empty changed-files objects through
joshdales Mar 20, 2023
d31255f
Make sure that empty config options don't accidently label things
joshdales Mar 20, 2023
b25e3a8
Better wording for the new test
joshdales Mar 20, 2023
1c9c27e
Run the build command
joshdales Mar 21, 2023
8e6367d
Merge branch 'main' into main
joshdales Mar 23, 2023
9bfc999
Run build and fix bad merge
joshdales Mar 23, 2023
5d0a66e
Run the build command again
joshdales Mar 23, 2023
e51b118
Change the structure of the config
joshdales Mar 25, 2023
a9e07ce
Add some new tests
joshdales Mar 25, 2023
3bec922
Add any and all functions for both checks
joshdales Mar 25, 2023
432b275
Get all the tests passings
joshdales Mar 25, 2023
4967646
Run the build command
joshdales Mar 25, 2023
4554c0d
Add a bunch more tests
joshdales Mar 25, 2023
5ac9519
Update the README
joshdales Mar 25, 2023
ef6ab1b
Add function for checking if any path matches
joshdales Mar 25, 2023
62f22bd
Run the build command
joshdales Mar 25, 2023
0b2cfb0
Im an idiot, bad copy pasta
joshdales Mar 25, 2023
29382eb
Build command
joshdales Mar 25, 2023
fa7f98c
Yikes, still missed that
joshdales Mar 25, 2023
210043e
Run the build command
joshdales Mar 25, 2023
938f9c9
Update the readme a little more
joshdales Mar 26, 2023
3ddce51
Update the debug values
joshdales Mar 27, 2023
4be192c
Run the build command
joshdales Mar 27, 2023
67604ee
Update more debugging statements
joshdales Mar 27, 2023
b1a2f85
Update debugging indentation on the branch checks
joshdales Mar 27, 2023
2f1dfd1
Adjust the indenting again
joshdales Mar 27, 2023
7f169bc
Merge pull request #4 from joshdales/another-config-setup
joshdales Mar 27, 2023
d4d4a10
Have a single isMatch for checking changed files
joshdales Apr 13, 2023
2637d23
Add test for when not all globs match any changed file
joshdales Apr 13, 2023
c1b0ca7
Run the build command
joshdales Apr 13, 2023
2a3422a
Better description for the new test
joshdales Apr 13, 2023
13e75b4
minor update to the readme
joshdales Apr 13, 2023
68a2598
Merge branch 'actions:main' into main
joshdales Apr 13, 2023
11812c3
Revert "Have a single isMatch for checking changed files"
joshdales May 5, 2023
9488def
Update tests and build
joshdales May 5, 2023
3aa0d43
Better test description
joshdales May 5, 2023
9cfddd0
Consolidate the new any change files test into the old one
joshdales May 5, 2023
51cc5e0
Update text in test descriptions and logging
joshdales May 11, 2023
09645fd
Move the allowed Matchconfig keys into a constant
joshdales May 11, 2023
34a5bf6
Update the validation when there are no options in the matchConfigs
joshdales May 11, 2023
4ac1764
Add a guard clause to stop false changed-files positives
joshdales May 11, 2023
a5bed11
Run the build command
joshdales May 11, 2023
a256a58
Add check for empty objects in checkAll
joshdales May 15, 2023
57d3407
Better check for empty configs in checkAll
joshdales May 17, 2023
3352df1
Bring test I accidently deleted
joshdales May 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 61 additions & 24 deletions README.md
Expand Up @@ -2,84 +2,121 @@

[![Basic validation](https://github.com/actions/labeler/actions/workflows/basic-validation.yml/badge.svg?branch=main)](https://github.com/actions/labeler/actions/workflows/basic-validation.yml)

Automatically label new pull requests based on the paths of files being changed.
Automatically label new pull requests based on the paths of files being changed or the branch name.

## Usage

### Create `.github/labeler.yml`

Create a `.github/labeler.yml` file with a list of labels and [minimatch](https://github.com/isaacs/minimatch) globs to match to apply the label.
Create a `.github/labeler.yml` file with a list of labels and config options to match and apply the label.

The key is the name of the label in your repository that you want to add (eg: "merge conflict", "needs-updating") and the value is the path (glob) of the changed files (eg: `src/**/*`, `tests/*.spec.js`) or a match object.
The key is the name of the label in your repository that you want to add (eg: "merge conflict", "needs-updating") and the value is a match object.

#### Match Object

For more control over matching, you can provide a match object instead of a simple path glob. The match object is defined as:
The match object allows control over the matching options, you can specify the label to be applied based on the files that have changed or the name of either the base branch or the head branch. For the changed files options you provide a [path glob](https://github.com/isaacs/minimatch#minimatch), and for the branches you provide a regexp to match against the branch name.

The base match object is defined as:
```yml
- any: ['list', 'of', 'globs']
all: ['list', 'of', 'globs']
- changed-files: ['list', 'of', 'globs']
- base-branch: ['list', 'of', 'regexps']
- head-branch: ['list', 'of', 'regexps']
```

One or both fields can be provided for fine-grained matching. Unlike the top-level list, the list of path globs provided to `any` and `all` must ALL match against a path for the label to be applied.
There are two top level keys of `any` and `all`, which both accept the same config options:
```yml
- any:
- changed-files: ['list', 'of', 'globs']
- base-branch: ['list', 'of', 'regexps']
- head-branch: ['list', 'of', 'regexps']
- all:
- changed-files: ['list', 'of', 'globs']
- base-branch: ['list', 'of', 'regexps']
- head-branch: ['list', 'of', 'regexps']
```

One or all fields can be provided for fine-grained matching.
The fields are defined as follows:
* `any`: match ALL globs against ANY changed path
* `all`: match ALL globs against ALL changed paths
* `all`: all of the provided options must match in order for the label to be applied
* `any`: if any of the provided options match then a label will be applied
* `base-branch`: match regexps against the base branch name
* `changed-files`: match glob patterns against the changed paths
* `head-branch`: match regexps against the head branch name

A simple path glob is the equivalent to `any: ['glob']`. More specifically, the following two configurations are equivalent:
If a base option is provided without a top-level key then it will default to `any`. More specifically, the following two configurations are equivalent:
```yml
label1:
- example1/*
- changed-files: example1/*
```
and
```yml
label1:
- any: ['example1/*']
- any:
- changed-files: ['example1/*']
```

From a boolean logic perspective, top-level match objects are `OR`-ed together and individual match rules within an object are `AND`-ed. Combined with `!` negation, you can write complex matching rules.
From a boolean logic perspective, top-level match objects, and options within `all` are `AND`-ed together and individual match rules within the `any` object are `OR`-ed. If path globs are combined with `!` negation, you can write complex matching rules.

#### Basic Examples

```yml
# Add 'label1' to any changes within 'example' folder or any subfolders
label1:
- example/**/*
- changed-files: example/**/*

# Add 'label2' to any file changes within 'example2' folder
label2: example2/*
label2:
- changed-files: example2/*

# Add label3 to any change to .txt files within the entire repository. Quotation marks are required for the leading asterisk
label3:
- '**/*.txt'
- changed-files: '**/*.txt'

# Add 'label4' to any PR where the head branch name starts with 'example4'
label4:
- head-branch: '^example4'

# Add 'label5' to any PR where the base branch name starts with 'example5'
label5:
- base-branch: '^example5'
```

#### Common Examples

```yml
# Add 'repo' label to any root file changes
repo:
- '*'
- changed-files: '*'

# Add '@domain/core' label to any change within the 'core' package
'@domain/core':
- package/core/*
- package/core/**/*
- changed-files:
- package/core/*
- package/core/**/*

# Add 'test' label to any change to *.spec.js files within the source dir
test:
- src/**/*.spec.js
- changed-files: src/**/*.spec.js

# Add 'source' label to any change to src files within the source dir EXCEPT for the docs sub-folder
source:
- any: ['src/**/*', '!src/docs/*']
- changed-files:
- any: ['src/**/*', '!src/docs/*']

# Add 'frontend` label to any change to *.js files as long as the `main.js` hasn't changed
frontend:
- any: ['src/**/*.js']
all: ['!src/main.js']
- any:
- changed-files: ['src/**/*.js']
- all:
- changed-files: ['!src/main.js']

# Add 'feature' label to any PR where the head branch name starts with `feature` or has a `feature` section in the name
feature:
- head-branch: ['^feature', 'feature']

# Add 'release' label to any PR that is opened against the `main` branch
release:
- base-branch: 'main'
```

### Create Workflow
Expand All @@ -98,7 +135,7 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- uses: actions/labeler@v5
```

#### Inputs
Expand Down
8 changes: 7 additions & 1 deletion __mocks__/@actions/github.ts
@@ -1,7 +1,13 @@
export const context = {
payload: {
pull_request: {
number: 123
number: 123,
head: {
ref: 'head-branch-name'
},
base: {
ref: 'base-branch-name'
}
}
},
repo: {
Expand Down
210 changes: 210 additions & 0 deletions __tests__/branch.test.ts
@@ -0,0 +1,210 @@
import {
getBranchName,
checkAnyBranch,
checkAllBranch,
toBranchMatchConfig,
BranchMatchConfig
} from '../src/branch';
import * as github from '@actions/github';

jest.mock('@actions/core');
jest.mock('@actions/github');

describe('getBranchName', () => {
describe('when the pull requests base branch is requested', () => {
it('returns the base branch name', () => {
const result = getBranchName('base');
expect(result).toEqual('base-branch-name');
});
});

describe('when the pull requests head branch is requested', () => {
it('returns the head branch name', () => {
const result = getBranchName('head');
expect(result).toEqual('head-branch-name');
});
});
});

describe('checkAllBranch', () => {
beforeEach(() => {
github.context.payload.pull_request!.head = {
ref: 'test/feature/123'
};
github.context.payload.pull_request!.base = {
ref: 'main'
};
});

describe('when a single pattern is provided', () => {
describe('and the pattern matches the head branch', () => {
it('returns true', () => {
const result = checkAllBranch(['^test'], 'head');
expect(result).toBe(true);
});
});

describe('and the pattern does not match the head branch', () => {
it('returns false', () => {
const result = checkAllBranch(['^feature/'], 'head');
expect(result).toBe(false);
});
});
});

describe('when multiple patterns are provided', () => {
describe('and not all patterns matched', () => {
it('returns false', () => {
const result = checkAllBranch(['^test/', '^feature/'], 'head');
expect(result).toBe(false);
});
});

describe('and all patterns match', () => {
it('returns true', () => {
const result = checkAllBranch(['^test/', '/feature/'], 'head');
expect(result).toBe(true);
});
});

describe('and no patterns match', () => {
it('returns false', () => {
const result = checkAllBranch(['^feature/', '/test$'], 'head');
expect(result).toBe(false);
});
});
});

describe('when the branch to check is specified as the base branch', () => {
describe('and the pattern matches the base branch', () => {
it('returns true', () => {
const result = checkAllBranch(['^main$'], 'base');
expect(result).toBe(true);
});
});
});
});

describe('checkAnyBranch', () => {
beforeEach(() => {
github.context.payload.pull_request!.head = {
ref: 'test/feature/123'
};
github.context.payload.pull_request!.base = {
ref: 'main'
};
});

describe('when a single pattern is provided', () => {
describe('and the pattern matches the head branch', () => {
it('returns true', () => {
const result = checkAnyBranch(['^test'], 'head');
expect(result).toBe(true);
});
});

describe('and the pattern does not match the head branch', () => {
it('returns false', () => {
const result = checkAnyBranch(['^feature/'], 'head');
expect(result).toBe(false);
});
});
});

describe('when multiple patterns are provided', () => {
describe('and at least one pattern matches', () => {
it('returns true', () => {
const result = checkAnyBranch(['^test/', '^feature/'], 'head');
expect(result).toBe(true);
});
});

describe('and all patterns match', () => {
it('returns true', () => {
const result = checkAnyBranch(['^test/', '/feature/'], 'head');
expect(result).toBe(true);
});
});

describe('and no patterns match', () => {
it('returns false', () => {
const result = checkAnyBranch(['^feature/', '/test$'], 'head');
expect(result).toBe(false);
});
});
});

describe('when the branch to check is specified as the base branch', () => {
describe('and the pattern matches the base branch', () => {
it('returns true', () => {
const result = checkAnyBranch(['^main$'], 'base');
expect(result).toBe(true);
});
});
});
});

describe('toBranchMatchConfig', () => {
describe('when there are no branch keys in the config', () => {
const config = {'changed-files': [{any: ['testing']}]};

it('returns an empty object', () => {
const result = toBranchMatchConfig(config);
expect(result).toEqual({});
});
});

describe('when the config contains a head-branch option', () => {
const config = {'head-branch': ['testing']};

it('sets headBranch in the matchConfig', () => {
const result = toBranchMatchConfig(config);
expect(result).toEqual<BranchMatchConfig>({
headBranch: ['testing']
});
});

describe('and the matching option is a string', () => {
const stringConfig = {'head-branch': 'testing'};

it('sets headBranch in the matchConfig', () => {
const result = toBranchMatchConfig(stringConfig);
expect(result).toEqual<BranchMatchConfig>({
headBranch: ['testing']
});
});
});
});

describe('when the config contains a base-branch option', () => {
const config = {'base-branch': ['testing']};
it('sets baseBranch in the matchConfig', () => {
const result = toBranchMatchConfig(config);
expect(result).toEqual<BranchMatchConfig>({
baseBranch: ['testing']
});
});

describe('and the matching option is a string', () => {
const stringConfig = {'base-branch': 'testing'};

it('sets baseBranch in the matchConfig', () => {
const result = toBranchMatchConfig(stringConfig);
expect(result).toEqual<BranchMatchConfig>({
baseBranch: ['testing']
});
});
});
});

describe('when the config contains both a base-branch and head-branch option', () => {
const config = {'base-branch': ['testing'], 'head-branch': ['testing']};
it('sets headBranch and baseBranch in the matchConfig', () => {
const result = toBranchMatchConfig(config);
expect(result).toEqual<BranchMatchConfig>({
baseBranch: ['testing'],
headBranch: ['testing']
});
});
});
});