From a5f6b5adfd65c9939e20eb889cbd34265b09eeff Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Mon, 21 Sep 2026 16:20:37 -0500 Subject: [PATCH] Register a problem matcher for MSBuild/csc errors and warnings - Add .github/msbuild.json with two owners: one matching file-based diagnostics (File(line,col): error/warning CODE: msg [project]) and one matching engine-level errors without file/line info. - Register the matcher in src/main.ts via the ##[add-matcher] workflow command, resolved relative to __dirname so it works from the ncc bundle in dist/. - Add src/__tests__/msbuild-matcher.test.ts validating the matcher regexes against representative CS/MSB error and warning lines. - Document the new problem matcher in README.md. - Rebuild dist/index.js via npm run build && npm run pack. Fixes #4 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/msbuild.json | 30 ++++++++ README.md | 4 + dist/index.js | 7 ++ src/__tests__/msbuild-matcher.test.ts | 101 ++++++++++++++++++++++++++ src/main.ts | 9 +++ 5 files changed, 151 insertions(+) create mode 100644 .github/msbuild.json create mode 100644 src/__tests__/msbuild-matcher.test.ts diff --git a/.github/msbuild.json b/.github/msbuild.json new file mode 100644 index 0000000..699c1e0 --- /dev/null +++ b/.github/msbuild.json @@ -0,0 +1,30 @@ +{ + "problemMatcher": [ + { + "owner": "msbuild", + "pattern": [ + { + "regexp": "^\\s*(?:\\d+>\\s*)?([^\\s].*?)\\((\\d+)(?:,(\\d+))?(?:,\\d+)*\\)\\s*:\\s*(error|warning)\\s+([A-Za-z]+\\d+)\\s*:\\s*(.*?)(?:\\s+\\[(.+?)\\])?\\s*$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "code": 5, + "message": 6, + "fromPath": 7 + } + ] + }, + { + "owner": "msbuild-no-location", + "pattern": [ + { + "regexp": "^(?!.*\\().*\\b(error|warning)\\s+([A-Za-z]+\\d+)\\s*:\\s*(.*)$", + "severity": 1, + "code": 2, + "message": 3 + } + ] + } + ] +} diff --git a/README.md b/README.md index 1b8ceab..3f42fdb 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ This makes use of the vswhere tool which is a tool delivered by Microsoft to hel vswhere-path: 'C:\path\to\your\tools\' ``` +## Problem matcher + +This action registers a [problem matcher](https://github.com/actions/toolkit/blob/main/docs/problem-matchers.md) for MSBuild/csc-style `error`/`warning` output (for example `error MSB1234:` or `error CS1234:`). Once registered, matching lines in the build log are automatically surfaced as annotations in the GitHub Actions UI and, when applicable, as pull request check annotations. + ## Notes on arguments While the Action enables you to specify a `vswhere` path as well as a `vs-version`, these are more advanced options and when using GitHub-hosted runners you should not need these and is recommended you don't specify them as they are optional. Using these require you to fully understand the runner environment, updates to the tools on the runner, and can cause failures if you are out of sync. For GitHub-hosted runners, omitting these arguments is the preferred usage. diff --git a/dist/index.js b/dist/index.js index 36f05ee..d479667 100644 --- a/dist/index.js +++ b/dist/index.js @@ -42,9 +42,16 @@ if (VS_VERSION !== 'latest') { VSWHERE_EXEC += `-version "${VS_VERSION}" `; } core.debug(`Execution arguments: ${VSWHERE_EXEC}`); +function registerProblemMatcher() { + const matcherPath = path.join(__dirname, '..', '.github', 'msbuild.json'); + core.info(`##[add-matcher]${matcherPath}`); +} function run() { return __awaiter(this, void 0, void 0, function* () { try { + // register the problem matcher so MSBuild/csc errors and warnings are + // annotated in the workflow log and any related pull request + registerProblemMatcher(); // exit if non Windows runner if (IS_WINDOWS === false) { core.setFailed('setup-msbuild can only be run on Windows runners'); diff --git a/src/__tests__/msbuild-matcher.test.ts b/src/__tests__/msbuild-matcher.test.ts new file mode 100644 index 0000000..5823e79 --- /dev/null +++ b/src/__tests__/msbuild-matcher.test.ts @@ -0,0 +1,101 @@ +import * as fs from 'fs' +import * as path from 'path' + +interface ProblemMatcherPattern { + regexp: string + file?: number + line?: number + column?: number + severity?: number + code?: number + message?: number + fromPath?: number +} + +interface ProblemMatcherOwner { + owner: string + pattern: ProblemMatcherPattern[] +} + +interface ProblemMatcherFile { + problemMatcher: ProblemMatcherOwner[] +} + +const matcherPath = path.join(__dirname, '..', '..', '.github', 'msbuild.json') +const matcherFile: ProblemMatcherFile = JSON.parse( + fs.readFileSync(matcherPath, 'utf8') +) + +function findOwner(owner: string): ProblemMatcherOwner { + const found = matcherFile.problemMatcher.find(m => m.owner === owner) + if (!found) { + throw new Error(`No matcher owner named ${owner} found`) + } + return found +} + +describe('msbuild problem matcher', () => { + it('is valid JSON with the expected owners', () => { + expect(matcherFile.problemMatcher.map(m => m.owner)).toEqual([ + 'msbuild', + 'msbuild-no-location' + ]) + }) + + describe('msbuild owner (file-based diagnostics)', () => { + const regexp = new RegExp(findOwner('msbuild').pattern[0].regexp) + + it('matches a csc-style error with file, line, column and project path', () => { + const line = + 'Program.cs(10,5): error CS1002: ; expected [C:\\proj\\proj.csproj]' + const match = regexp.exec(line) + expect(match).not.toBeNull() + expect(match?.[1]).toBe('Program.cs') + expect(match?.[2]).toBe('10') + expect(match?.[3]).toBe('5') + expect(match?.[4]).toBe('error') + expect(match?.[5]).toBe('CS1002') + expect(match?.[6]).toBe('; expected') + expect(match?.[7]).toBe('C:\\proj\\proj.csproj') + }) + + it('matches an MSB-coded warning with file and line only', () => { + const line = + 'C:\\proj\\proj.csproj(15,1): warning MSB3277: Found conflicts [C:\\proj\\proj.csproj]' + const match = regexp.exec(line) + expect(match).not.toBeNull() + expect(match?.[4]).toBe('warning') + expect(match?.[5]).toBe('MSB3277') + }) + + it('matches a diagnostic without a trailing project path', () => { + const line = 'Program.cs(3,1): error CS0246: The type or namespace name could not be found' + const match = regexp.exec(line) + expect(match).not.toBeNull() + expect(match?.[5]).toBe('CS0246') + expect(match?.[6]).toBe( + 'The type or namespace name could not be found' + ) + expect(match?.[7]).toBeUndefined() + }) + }) + + describe('msbuild-no-location owner (engine-level diagnostics)', () => { + const regexp = new RegExp(findOwner('msbuild-no-location').pattern[0].regexp) + + it('matches an MSBuild engine error with no file/line info', () => { + const line = 'MSBUILD : error MSB1009: Project file does not exist.' + const match = regexp.exec(line) + expect(match).not.toBeNull() + expect(match?.[1]).toBe('error') + expect(match?.[2]).toBe('MSB1009') + expect(match?.[3]).toBe('Project file does not exist.') + }) + + it('does not match lines already covered by the file-based owner', () => { + const line = + 'Program.cs(10,5): error CS1002: ; expected [C:\\proj\\proj.csproj]' + expect(regexp.test(line)).toBe(false) + }) + }) +}) diff --git a/src/main.ts b/src/main.ts index b449dc3..95c7b5e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,8 +23,17 @@ if (VS_VERSION !== 'latest') { core.debug(`Execution arguments: ${VSWHERE_EXEC}`) +function registerProblemMatcher(): void { + const matcherPath = path.join(__dirname, '..', '.github', 'msbuild.json') + core.info(`##[add-matcher]${matcherPath}`) +} + async function run(): Promise { try { + // register the problem matcher so MSBuild/csc errors and warnings are + // annotated in the workflow log and any related pull request + registerProblemMatcher() + // exit if non Windows runner if (IS_WINDOWS === false) { core.setFailed('setup-msbuild can only be run on Windows runners')