Skip to content

Commit 235b916

Browse files
committed
Handle zero-count file matchers
1 parent da9d5ae commit 235b916

6 files changed

Lines changed: 347 additions & 26 deletions

File tree

__tests__/matcher/files.test.ts

Lines changed: 239 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,31 @@ const files = {
297297
},
298298
],
299299
8: [{ filename: 'app/1.js' }, { filename: 'app/2.js' }, { filename: 'app/3.js' }],
300+
9: [
301+
{ filename: 'src/index.ts' },
302+
{ filename: 'docs/readme.md' },
303+
],
304+
10: [{ filename: 'lib/index.ts' }],
305+
11: [
306+
{ filename: 'app/feature.ts' },
307+
{ filename: 'docs/readme.md' },
308+
],
309+
12: [
310+
{ filename: 'app/feature.ts' },
311+
{ filename: 'app/helpers.ts' },
312+
{ filename: 'docs/readme.md' },
313+
],
314+
13: [
315+
{ filename: 'src/index.ts' },
316+
{ filename: 'docs/readme.md' },
317+
],
318+
14: [
319+
{ filename: 'lib/a.ts' },
320+
{ filename: 'lib/b.ts' },
321+
{ filename: 'misc/test.ts' },
322+
],
323+
15: [],
324+
16: [{ filename: 'docs/readme.md' }],
300325
};
301326

302327
describe('basic', () => {
@@ -412,7 +437,7 @@ describe('complex', () => {
412437
},
413438
};
414439
const labels = await getMatchedLabels(complex);
415-
expect(labels).toEqual(['any-app', 'NEQ1', 'L']);
440+
expect(labels).toEqual(['all-app', 'any-app', 'none-app', 'all-any', 'NEQ1', 'L', 'mixed-1']);
416441
});
417442

418443
it('2 should have complex labels', async function () {
@@ -432,7 +457,7 @@ describe('complex', () => {
432457
},
433458
};
434459
const labels = await getMatchedLabels(complex);
435-
expect(labels).toEqual(['any-app', 'NEQ1', 'M']);
460+
expect(labels).toEqual(['all-app', 'any-app', 'none-app', 'all-any', 'NEQ1', 'M', 'mixed-1']);
436461
});
437462

438463
it('4 should have complex labels', async function () {
@@ -485,3 +510,215 @@ describe('complex', () => {
485510
expect(labels).toEqual(['all-app', 'any-app', 'NEQ1', 'M', 'mixed-1']);
486511
});
487512
});
513+
514+
describe('all matcher behavior', () => {
515+
const globsConfig: Config = {
516+
version: 'v1',
517+
labels: [
518+
{
519+
label: 'src-only',
520+
matcher: {
521+
files: {
522+
all: ['src/**'],
523+
},
524+
},
525+
},
526+
],
527+
};
528+
529+
beforeEach(() => {
530+
jest.spyOn(github.context, 'repo', 'get').mockImplementation(() => {
531+
return {
532+
owner: 'owner-name',
533+
repo: 'repo-name',
534+
};
535+
});
536+
});
537+
538+
afterAll(() => {
539+
jest.restoreAllMocks();
540+
});
541+
542+
it('matches when each glob is satisfied by at least one file', async function () {
543+
github.context.payload = {
544+
pull_request: {
545+
number: 9,
546+
},
547+
};
548+
549+
const labels = await getMatchedLabels(globsConfig);
550+
expect(labels).toEqual(['src-only']);
551+
});
552+
553+
it('does not match when glob does not match any files', async function () {
554+
github.context.payload = {
555+
pull_request: {
556+
number: 10,
557+
},
558+
};
559+
560+
const labels = await getMatchedLabels(globsConfig);
561+
expect(labels).toEqual([]);
562+
});
563+
});
564+
565+
describe('count matcher scoping', () => {
566+
const scopedCountConfig: Config = {
567+
version: 'v1',
568+
labels: [
569+
{
570+
label: 'app-lte-1',
571+
matcher: {
572+
files: {
573+
any: ['app/**'],
574+
count: { lte: 1 },
575+
},
576+
},
577+
},
578+
{
579+
label: 'app-eq-1',
580+
matcher: {
581+
files: {
582+
any: ['app/**'],
583+
count: { eq: 1 },
584+
},
585+
},
586+
},
587+
{
588+
label: 'src-eq-1',
589+
matcher: {
590+
files: {
591+
all: ['src/**'],
592+
count: { eq: 1 },
593+
},
594+
},
595+
},
596+
{
597+
label: 'lib-eq-2',
598+
matcher: {
599+
files: {
600+
any: ['lib/**'],
601+
all: ['lib/**'],
602+
count: { eq: 2 },
603+
},
604+
},
605+
},
606+
],
607+
};
608+
609+
beforeEach(() => {
610+
jest.spyOn(github.context, 'repo', 'get').mockImplementation(() => {
611+
return {
612+
owner: 'owner-name',
613+
repo: 'repo-name',
614+
};
615+
});
616+
});
617+
618+
afterAll(() => {
619+
jest.restoreAllMocks();
620+
});
621+
622+
it('counts only files matching the any globs', async function () {
623+
github.context.payload = {
624+
pull_request: {
625+
number: 11,
626+
},
627+
};
628+
629+
const labels = await getMatchedLabels(scopedCountConfig);
630+
expect(labels).toEqual(['app-lte-1', 'app-eq-1']);
631+
});
632+
633+
it('ignores unrelated files when evaluating counts', async function () {
634+
github.context.payload = {
635+
pull_request: {
636+
number: 12,
637+
},
638+
};
639+
640+
const labels = await getMatchedLabels(scopedCountConfig);
641+
expect(labels).toEqual([]);
642+
});
643+
644+
it('uses files matching all globs for counts', async function () {
645+
github.context.payload = {
646+
pull_request: {
647+
number: 13,
648+
},
649+
};
650+
651+
const labels = await getMatchedLabels(scopedCountConfig);
652+
expect(labels).toEqual(['src-eq-1']);
653+
});
654+
655+
it('reuses glob-matched files for count checks', async function () {
656+
github.context.payload = {
657+
pull_request: {
658+
number: 14,
659+
},
660+
};
661+
662+
const labels = await getMatchedLabels(scopedCountConfig);
663+
expect(labels).toEqual(['lib-eq-2']);
664+
});
665+
});
666+
667+
describe('zero count handling', () => {
668+
const zeroCountConfig: Config = {
669+
version: 'v1',
670+
labels: [
671+
{
672+
label: 'no-files-eq',
673+
matcher: {
674+
files: {
675+
count: { eq: 0 },
676+
},
677+
},
678+
},
679+
{
680+
label: 'no-files-lte',
681+
matcher: {
682+
files: {
683+
count: { lte: 0 },
684+
},
685+
},
686+
},
687+
],
688+
};
689+
690+
beforeEach(() => {
691+
jest.spyOn(github.context, 'repo', 'get').mockImplementation(() => {
692+
return {
693+
owner: 'owner-name',
694+
repo: 'repo-name',
695+
};
696+
});
697+
});
698+
699+
afterAll(() => {
700+
jest.restoreAllMocks();
701+
});
702+
703+
it('applies labels when no files are present', async function () {
704+
github.context.payload = {
705+
pull_request: {
706+
number: 15,
707+
},
708+
};
709+
710+
const labels = await getMatchedLabels(zeroCountConfig);
711+
expect(labels).toEqual(['no-files-eq', 'no-files-lte']);
712+
});
713+
714+
it('does not apply labels when files exceed zero thresholds', async function () {
715+
github.context.payload = {
716+
pull_request: {
717+
number: 16,
718+
},
719+
};
720+
721+
const labels = await getMatchedLabels(zeroCountConfig);
722+
expect(labels).toEqual([]);
723+
});
724+
});

__tests__/matcher/utils.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
1-
import { matcherRegex } from '../../src/matcher/utils';
1+
import { matcherRegex, matcherRegexAny } from '../../src/matcher/utils';
22

33
it('should always fail', function () {
44
expect(matcherRegex({ regex: undefined, text: 'abc' })).toBeFalsy();
55
});
6+
7+
it('supports slash-delimited regex strings with flags', function () {
8+
expect(matcherRegex({ regex: '/(bug|fix)/i', text: 'BUG: something broke' })).toBeTruthy();
9+
});
10+
11+
it('supports multiple flags on slash-delimited regex strings', function () {
12+
expect(matcherRegex({ regex: '/(bug)/gi', text: 'Spotted a BUG today' })).toBeTruthy();
13+
});
14+
15+
it('supports multiline anchors via regex flags', function () {
16+
expect(matcherRegex({ regex: '/^bug/m', text: 'changelog\nbug fix listed' })).toBeTruthy();
17+
});
18+
19+
it('supports plain regex strings without slashes', function () {
20+
expect(matcherRegex({ regex: 'bug', text: 'BUG: something broke' })).toBeFalsy();
21+
});
22+
23+
it('uses slash-delimited regex for matcherRegexAny', function () {
24+
expect(matcherRegexAny('/(bug|fix)/i', ['Refactor', 'BUG: test failure'])).toBeTruthy();
25+
});
26+
27+
it('logs invalid slash-delimited regex and still matches via fallback', function () {
28+
const errorMock = jest.spyOn(console, 'error').mockImplementation(() => {});
29+
30+
try {
31+
expect(matcherRegex({ regex: '/foo/uubar', text: '/foo/uubar' })).toBeTruthy();
32+
expect(errorMock).toHaveBeenCalledTimes(1);
33+
expect(errorMock.mock.calls[0][0]).toContain('Invalid regex /foo/uubar');
34+
} finally {
35+
errorMock.mockRestore();
36+
}
37+
});

src/config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,14 @@ export async function getConfig(
9191
configPath: string,
9292
configRepo: string,
9393
): Promise<Config> {
94-
const [owner, repo] = configRepo.split('/');
94+
const repoName = configRepo?.trim()
95+
? configRepo
96+
: `${github.context.repo.owner}/${github.context.repo.repo}`;
97+
const [owner, repo] = repoName.split('/');
9598
const response: any = await client.rest.repos.getContent({
9699
owner,
97100
repo,
98-
ref: configRepo === github.context.payload.repository?.full_name ? github.context.sha : undefined,
101+
ref: repoName === github.context.payload.repository?.full_name ? github.context.sha : undefined,
99102
path: configPath,
100103
});
101104

src/main.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ async function addChecks(checks: StatusCheck[]): Promise<void> {
6767
}
6868

6969
const sha = github.context.payload.pull_request?.head.sha as string;
70-
await Promise.all([
70+
await Promise.all(
7171
checks.map((check) => {
72-
client.rest.repos.createCommitStatus({
72+
return client.rest.repos.createCommitStatus({
7373
owner: github.context.repo.owner,
7474
repo: github.context.repo.repo,
7575
sha: sha,
@@ -79,14 +79,16 @@ async function addChecks(checks: StatusCheck[]): Promise<void> {
7979
target_url: check.url,
8080
});
8181
}),
82-
]);
82+
);
8383
}
8484

8585
getConfig(client, configPath, configRepo)
8686
.then(async (config) => {
8787
const labeled = await labels(client, config);
8888
const finalLabels = mergeLabels(labeled, config);
8989

90+
core.info(`Matched labels: ${finalLabels.join(', ') || 'None'}`);
91+
9092
return Promise.all([
9193
addLabels(finalLabels),
9294
removeLabels(finalLabels, config),

0 commit comments

Comments
 (0)