Skip to content

Commit 3a36c8a

Browse files
committed
Add context to the created comment thread
* The `fileGlob` is now matched against all files in the PR instead of stopping at the first match. * If the `fileGlob` only matches one file the comment thread is created with a context for that file so that it shows up within that file in the Azure DevOps UI. * If more files match the glob, no context is added. * Moved error handling in `index.ts` outside the `TaskRunner` to catch all errors and mark the task as failed. * Added logging of matches files
1 parent 3a37163 commit 3a36c8a

File tree

4 files changed

+66
-29
lines changed

4 files changed

+66
-29
lines changed

src/commentator.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,23 @@ import { type IGitApi } from "azure-devops-node-api/GitApi";
22
import * as GitInterfaces from "azure-devops-node-api/interfaces/GitInterfaces";
33
import { type IInputs } from "./inputs";
44
import { isAutoCommentThread } from "./type-guards";
5+
import { type IResultContext } from "./validators/validator";
56

67
export class Commentator {
78
constructor(
89
private readonly inputs: IInputs,
910
private readonly client: IGitApi
1011
) {}
1112

12-
public readonly createComment = async(repositoryId: string, prId: number): Promise<string> => {
13+
public readonly createComment = async(repositoryId: string, prId: number, context?: IResultContext): Promise<string> => {
1314
const commentHash = this.inputs.hashedConditions;
1415

1516
const prThreads = await this.client.getThreads(repositoryId, prId);
1617
const existingThread = prThreads.filter(isAutoCommentThread)
1718
.find(thread => thread.properties.hash.$value === commentHash);
1819

1920
if (existingThread === undefined) {
20-
await this.createNewThread(commentHash, repositoryId, prId);
21+
await this.createNewThread(commentHash, repositoryId, prId, context);
2122
console.log(`New comment created with the hash: ${commentHash}`);
2223
} else {
2324
console.log(`A comment already exists with the hash: ${commentHash}`);
@@ -29,7 +30,8 @@ export class Commentator {
2930
private readonly createNewThread = async(
3031
commentHash: string,
3132
repositoryId: string,
32-
prId: number
33+
prId: number,
34+
context?: IResultContext
3335
): Promise<GitInterfaces.GitPullRequestCommentThread> => {
3436
const thread: GitInterfaces.GitPullRequestCommentThread = {
3537
properties: {
@@ -39,9 +41,18 @@ export class Commentator {
3941
content: this.inputs.comment,
4042
commentType: GitInterfaces.CommentType.Text
4143
}],
42-
status: GitInterfaces.CommentThreadStatus.Active
44+
status: GitInterfaces.CommentThreadStatus.Active,
45+
threadContext: this.getThreadContext(context)
4346
};
4447

4548
return await this.client.createThread(thread, repositoryId, prId);
4649
};
50+
51+
private readonly getThreadContext = (context?: IResultContext): GitInterfaces.CommentThreadContext | undefined => {
52+
if (context?.files === undefined || context.files.length !== 1) {
53+
return undefined;
54+
}
55+
56+
return { filePath: context.files[0] };
57+
};
4758
}

src/index.ts

+8-12
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,16 @@ class TaskRunner {
2121
}
2222

2323
public run = async(): Promise<void> => {
24-
try {
25-
let resultMessage = "One or more conditions were not met";
24+
let resultMessage = "One or more conditions were not met";
2625

27-
const result = await validateAll(this.client, this.inputs, this.repoId, this.prId);
26+
const result = await validateAll(this.client, this.inputs, this.repoId, this.prId);
2827

29-
if (result.conditionMet) {
30-
const commentHash = await this.commentator.createComment(this.repoId, this.prId);
31-
resultMessage = `Conditions succesfully met. Comment hash: ${commentHash}`;
32-
}
33-
34-
setResult(TaskResult.Succeeded, resultMessage);
35-
} catch (err: any) {
36-
console.error(err, err.stack);
37-
setResult(TaskResult.Failed, err.message);
28+
if (result.conditionMet) {
29+
const commentHash = await this.commentator.createComment(this.repoId, this.prId, result.context);
30+
resultMessage = `Conditions successfully met. Comment hash: ${commentHash}`;
3831
}
32+
33+
setResult(TaskResult.Succeeded, resultMessage);
3934
};
4035
}
4136

@@ -49,5 +44,6 @@ class TaskRunner {
4944
await runner.run();
5045
} catch (err: any) {
5146
console.error(err, err.stack);
47+
setResult(TaskResult.Failed, err.message);
5248
}
5349
})();

src/validators/file-glob-validator.ts

+24-12
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ export class FileGlobValidator implements IValidator {
1616
return { conditionMet: true };
1717
}
1818

19-
const matchingChange = await this.getFirstMatchingChange(repositoryId, prId, this.inputs.fileGlob);
20-
if (matchingChange !== undefined) {
21-
console.log("Found one or more matches for the glob expression");
19+
const matchingChanges = await this.getMatchingChanges(repositoryId, prId, this.inputs.fileGlob);
20+
if (matchingChanges.length > 0) {
21+
console.log("Found the following matches for the glob expression:\n " +
22+
matchingChanges.map(c => c.item?.path).join("\n "));
2223
return {
2324
conditionMet: true,
2425
context: {
25-
files: [matchingChange.item?.path ?? ""]
26+
files: matchingChanges.map(change => change.item?.path ?? "")
2627
}
2728
};
2829
}
@@ -31,16 +32,27 @@ export class FileGlobValidator implements IValidator {
3132
return { conditionMet: false };
3233
};
3334

34-
private readonly getFirstMatchingChange = async(repositoryId: string, prId: number, fileGlob: string): Promise<GitInterfaces.GitPullRequestChange | undefined> => {
35+
private readonly getMatchingChanges = async(repositoryId: string, prId: number, fileGlob: string): Promise<GitInterfaces.GitPullRequestChange[]> => {
3536
const lastIterationId = await this.getLastIterationId(repositoryId, prId);
36-
let changes: GitInterfaces.GitPullRequestIterationChanges;
37-
let matchingChange: GitInterfaces.GitPullRequestChange | undefined;
37+
let changes: GitInterfaces.GitPullRequestIterationChanges | undefined;
38+
const matchingChanges: GitInterfaces.GitPullRequestChange[] = [];
39+
const matchesGlob = (changeEntry: GitInterfaces.GitPullRequestChange): boolean =>
40+
minimatch(changeEntry.item?.path ?? "", fileGlob);
41+
3842
do {
39-
changes = await this.client.getPullRequestIterationChanges(repositoryId, prId, lastIterationId);
40-
matchingChange = changes.changeEntries?.find(
41-
entry => minimatch(entry.item?.path ?? "", fileGlob));
42-
} while (matchingChange === undefined && changes.nextTop !== undefined && changes.nextTop > 0);
43-
return matchingChange;
43+
changes = await this.client.getPullRequestIterationChanges(
44+
repositoryId,
45+
prId,
46+
lastIterationId,
47+
undefined,
48+
changes?.nextTop,
49+
changes?.nextSkip);
50+
51+
const matches = changes.changeEntries?.filter(matchesGlob) ?? [];
52+
matchingChanges.push(...matches);
53+
} while (changes.nextTop !== undefined && changes.nextTop > 0);
54+
55+
return matchingChanges;
4456
};
4557

4658
private readonly getLastIterationId = async(repositoryId: string, prId: number): Promise<number> => {

tests/validators/file-glob-validator.test.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,24 @@ describe("FileGlobValidator", () => {
6363
sinon.assert.calledTwice(stubApiClient.getPullRequestIterationChanges);
6464
});
6565

66+
it("should succeed when fileGlob matches multiple files on multiple pages", async() => {
67+
const fileGlob = "/**/*";
68+
const stubInputs = createStubInputs({ fileGlob });
69+
const stubApiClient = createStubGitApi();
70+
stubApiClient.getPullRequestIterationChanges
71+
.onSecondCall().resolves(pageTwoIterationChanges());
72+
const minimatchStub = sinon.stub<[string, string], boolean>()
73+
.callsFake((_: string, __: string) => true);
74+
setMinimatchStub(minimatchStub);
75+
const sut = await createSut(stubApiClient, stubInputs);
76+
77+
const result = await sut.check("foo", 7357);
78+
79+
expect(result.conditionMet).is.true;
80+
expect(result.context?.files).to.have.members(["/foo/bar.txt", "/baz/qux.txt"]);
81+
sinon.assert.calledTwice(stubApiClient.getPullRequestIterationChanges);
82+
});
83+
6684
it("should fail when fileGlob matches no files", async() => {
6785
const fileGlob = "/match/nothing";
6886
const stubInputs = createStubInputs({ fileGlob });
@@ -86,7 +104,7 @@ function createStubGitApi(): StubbedInstance<IGitApi> {
86104
.resolves([{ id: 1 }]);
87105
stubGitApi.getPullRequestIterationChanges
88106
.onFirstCall().resolves(pageOneIterationChanges())
89-
.onSecondCall().throws("The test was not supposed to get second page");
107+
.onSecondCall().resolves({});
90108
return stubGitApi;
91109
}
92110

0 commit comments

Comments
 (0)