mirror of
https://github.com/actions/runner.git
synced 2025-12-13 10:05:23 +00:00
feat: default fromPath for problem matchers (#3802)
This commit is contained in:
@@ -250,6 +250,42 @@ Two problem matchers can be used:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Default from path
|
||||||
|
|
||||||
|
The problem matcher can specify a `fromPath` property at the top level, which applies when a specific pattern doesn't provide a value for `fromPath`. This is useful for tools that don't include project file information in their output.
|
||||||
|
|
||||||
|
For example, given the following compiler output that doesn't include project file information:
|
||||||
|
|
||||||
|
```
|
||||||
|
ClassLibrary.cs(16,24): warning CS0612: 'ClassLibrary.Helpers.MyHelper.Name' is obsolete
|
||||||
|
```
|
||||||
|
|
||||||
|
A problem matcher with a default from path can be used:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "csc-minimal",
|
||||||
|
"fromPath": "ClassLibrary/ClassLibrary.csproj",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+)\\((\\d+),(\\d+)\\): (error|warning) (.+): (.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"severity": 4,
|
||||||
|
"code": 5,
|
||||||
|
"message": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures that the file is rooted to the correct path when there's not enough information in the error messages to extract a `fromPath`.
|
||||||
|
|
||||||
#### Mitigate regular expression denial of service (ReDos)
|
#### Mitigate regular expression denial of service (ReDos)
|
||||||
|
|
||||||
If a matcher exceeds a 1 second timeout when processing a line, retry up to two three times total.
|
If a matcher exceeds a 1 second timeout when processing a line, retry up to two three times total.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace GitHub.Runner.Worker
|
|||||||
public sealed class IssueMatcher
|
public sealed class IssueMatcher
|
||||||
{
|
{
|
||||||
private string _defaultSeverity;
|
private string _defaultSeverity;
|
||||||
|
private string _defaultFromPath;
|
||||||
private string _owner;
|
private string _owner;
|
||||||
private IssuePattern[] _patterns;
|
private IssuePattern[] _patterns;
|
||||||
private IssueMatch[] _state;
|
private IssueMatch[] _state;
|
||||||
@@ -29,6 +30,7 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
_owner = config.Owner;
|
_owner = config.Owner;
|
||||||
_defaultSeverity = config.Severity;
|
_defaultSeverity = config.Severity;
|
||||||
|
_defaultFromPath = config.FromPath;
|
||||||
_patterns = config.Patterns.Select(x => new IssuePattern(x, timeout)).ToArray();
|
_patterns = config.Patterns.Select(x => new IssuePattern(x, timeout)).ToArray();
|
||||||
Reset();
|
Reset();
|
||||||
}
|
}
|
||||||
@@ -59,6 +61,19 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string DefaultFromPath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_defaultFromPath == null)
|
||||||
|
{
|
||||||
|
_defaultFromPath = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _defaultFromPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IssueMatch Match(string line)
|
public IssueMatch Match(string line)
|
||||||
{
|
{
|
||||||
// Single pattern
|
// Single pattern
|
||||||
@@ -69,7 +84,7 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
if (regexMatch.Success)
|
if (regexMatch.Success)
|
||||||
{
|
{
|
||||||
return new IssueMatch(null, pattern, regexMatch.Groups, DefaultSeverity);
|
return new IssueMatch(null, pattern, regexMatch.Groups, DefaultSeverity, DefaultFromPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -110,7 +125,7 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return
|
// Return
|
||||||
return new IssueMatch(runningMatch, pattern, regexMatch.Groups, DefaultSeverity);
|
return new IssueMatch(runningMatch, pattern, regexMatch.Groups, DefaultSeverity, DefaultFromPath);
|
||||||
}
|
}
|
||||||
// Not the last pattern
|
// Not the last pattern
|
||||||
else
|
else
|
||||||
@@ -184,7 +199,7 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
public sealed class IssueMatch
|
public sealed class IssueMatch
|
||||||
{
|
{
|
||||||
public IssueMatch(IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null)
|
public IssueMatch(IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null, string defaultFromPath = null)
|
||||||
{
|
{
|
||||||
File = runningMatch?.File ?? GetValue(groups, pattern.File);
|
File = runningMatch?.File ?? GetValue(groups, pattern.File);
|
||||||
Line = runningMatch?.Line ?? GetValue(groups, pattern.Line);
|
Line = runningMatch?.Line ?? GetValue(groups, pattern.Line);
|
||||||
@@ -198,6 +213,11 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
Severity = defaultSeverity;
|
Severity = defaultSeverity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(FromPath) && !string.IsNullOrEmpty(defaultFromPath))
|
||||||
|
{
|
||||||
|
FromPath = defaultFromPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string File { get; }
|
public string File { get; }
|
||||||
@@ -282,6 +302,9 @@ namespace GitHub.Runner.Worker
|
|||||||
[DataMember(Name = "pattern")]
|
[DataMember(Name = "pattern")]
|
||||||
private IssuePatternConfig[] _patterns;
|
private IssuePatternConfig[] _patterns;
|
||||||
|
|
||||||
|
[DataMember(Name = "fromPath")]
|
||||||
|
private string _fromPath;
|
||||||
|
|
||||||
public string Owner
|
public string Owner
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -318,6 +341,24 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string FromPath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_fromPath == null)
|
||||||
|
{
|
||||||
|
_fromPath = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _fromPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_fromPath = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IssuePatternConfig[] Patterns
|
public IssuePatternConfig[] Patterns
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -896,5 +896,173 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
Assert.Equal("not-working", match.Message);
|
Assert.Equal("not-working", match.Message);
|
||||||
Assert.Equal("my-project.proj", match.FromPath);
|
Assert.Equal("my-project.proj", match.FromPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void Matcher_SinglePattern_DefaultFromPath()
|
||||||
|
{
|
||||||
|
var config = JsonUtility.FromString<IssueMatchersConfig>(@"
|
||||||
|
{
|
||||||
|
""problemMatcher"": [
|
||||||
|
{
|
||||||
|
""owner"": ""myMatcher"",
|
||||||
|
""fromPath"": ""subdir/default-project.csproj"",
|
||||||
|
""pattern"": [
|
||||||
|
{
|
||||||
|
""regexp"": ""^file:(.+) line:(.+) column:(.+) severity:(.+) code:(.+) message:(.+)$"",
|
||||||
|
""file"": 1,
|
||||||
|
""line"": 2,
|
||||||
|
""column"": 3,
|
||||||
|
""severity"": 4,
|
||||||
|
""code"": 5,
|
||||||
|
""message"": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
");
|
||||||
|
config.Validate();
|
||||||
|
var matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
var match = matcher.Match("file:my-file.cs line:123 column:45 severity:real-bad code:uh-oh message:not-working");
|
||||||
|
Assert.Equal("my-file.cs", match.File);
|
||||||
|
Assert.Equal("123", match.Line);
|
||||||
|
Assert.Equal("45", match.Column);
|
||||||
|
Assert.Equal("real-bad", match.Severity);
|
||||||
|
Assert.Equal("uh-oh", match.Code);
|
||||||
|
Assert.Equal("not-working", match.Message);
|
||||||
|
Assert.Equal("subdir/default-project.csproj", match.FromPath);
|
||||||
|
|
||||||
|
// Test that a pattern-specific fromPath overrides the default
|
||||||
|
config = JsonUtility.FromString<IssueMatchersConfig>(@"
|
||||||
|
{
|
||||||
|
""problemMatcher"": [
|
||||||
|
{
|
||||||
|
""owner"": ""myMatcher"",
|
||||||
|
""fromPath"": ""subdir/default-project.csproj"",
|
||||||
|
""pattern"": [
|
||||||
|
{
|
||||||
|
""regexp"": ""^file:(.+) line:(.+) column:(.+) severity:(.+) code:(.+) message:(.+) fromPath:(.+)$"",
|
||||||
|
""file"": 1,
|
||||||
|
""line"": 2,
|
||||||
|
""column"": 3,
|
||||||
|
""severity"": 4,
|
||||||
|
""code"": 5,
|
||||||
|
""message"": 6,
|
||||||
|
""fromPath"": 7
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
");
|
||||||
|
config.Validate();
|
||||||
|
matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
match = matcher.Match("file:my-file.cs line:123 column:45 severity:real-bad code:uh-oh message:not-working fromPath:my-project.proj");
|
||||||
|
Assert.Equal("my-file.cs", match.File);
|
||||||
|
Assert.Equal("123", match.Line);
|
||||||
|
Assert.Equal("45", match.Column);
|
||||||
|
Assert.Equal("real-bad", match.Severity);
|
||||||
|
Assert.Equal("uh-oh", match.Code);
|
||||||
|
Assert.Equal("not-working", match.Message);
|
||||||
|
Assert.Equal("my-project.proj", match.FromPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void Matcher_MultiplePatterns_DefaultFromPath()
|
||||||
|
{
|
||||||
|
var config = JsonUtility.FromString<IssueMatchersConfig>(@"
|
||||||
|
{
|
||||||
|
""problemMatcher"": [
|
||||||
|
{
|
||||||
|
""owner"": ""myMatcher"",
|
||||||
|
""fromPath"": ""subdir/default-project.csproj"",
|
||||||
|
""pattern"": [
|
||||||
|
{
|
||||||
|
""regexp"": ""^file:(.+)$"",
|
||||||
|
""file"": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""regexp"": ""^severity:(.+)$"",
|
||||||
|
""severity"": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""regexp"": ""^line:(.+) column:(.+) code:(.+) message:(.+)$"",
|
||||||
|
""line"": 1,
|
||||||
|
""column"": 2,
|
||||||
|
""code"": 3,
|
||||||
|
""message"": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
");
|
||||||
|
config.Validate();
|
||||||
|
var matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
var match = matcher.Match("file:my-file.cs");
|
||||||
|
Assert.Null(match);
|
||||||
|
match = matcher.Match("severity:real-bad");
|
||||||
|
Assert.Null(match);
|
||||||
|
match = matcher.Match("line:123 column:45 code:uh-oh message:not-working");
|
||||||
|
Assert.Equal("my-file.cs", match.File);
|
||||||
|
Assert.Equal("123", match.Line);
|
||||||
|
Assert.Equal("45", match.Column);
|
||||||
|
Assert.Equal("real-bad", match.Severity);
|
||||||
|
Assert.Equal("uh-oh", match.Code);
|
||||||
|
Assert.Equal("not-working", match.Message);
|
||||||
|
Assert.Equal("subdir/default-project.csproj", match.FromPath);
|
||||||
|
|
||||||
|
// Test that pattern-specific fromPath overrides the default
|
||||||
|
config = JsonUtility.FromString<IssueMatchersConfig>(@"
|
||||||
|
{
|
||||||
|
""problemMatcher"": [
|
||||||
|
{
|
||||||
|
""owner"": ""myMatcher"",
|
||||||
|
""fromPath"": ""subdir/default-project.csproj"",
|
||||||
|
""pattern"": [
|
||||||
|
{
|
||||||
|
""regexp"": ""^file:(.+) fromPath:(.+)$"",
|
||||||
|
""file"": 1,
|
||||||
|
""fromPath"": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""regexp"": ""^severity:(.+)$"",
|
||||||
|
""severity"": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""regexp"": ""^line:(.+) column:(.+) code:(.+) message:(.+)$"",
|
||||||
|
""line"": 1,
|
||||||
|
""column"": 2,
|
||||||
|
""code"": 3,
|
||||||
|
""message"": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
");
|
||||||
|
config.Validate();
|
||||||
|
matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
match = matcher.Match("file:my-file.cs fromPath:my-project.proj");
|
||||||
|
Assert.Null(match);
|
||||||
|
match = matcher.Match("severity:real-bad");
|
||||||
|
Assert.Null(match);
|
||||||
|
match = matcher.Match("line:123 column:45 code:uh-oh message:not-working");
|
||||||
|
Assert.Equal("my-file.cs", match.File);
|
||||||
|
Assert.Equal("123", match.Line);
|
||||||
|
Assert.Equal("45", match.Column);
|
||||||
|
Assert.Equal("real-bad", match.Severity);
|
||||||
|
Assert.Equal("uh-oh", match.Code);
|
||||||
|
Assert.Equal("not-working", match.Message);
|
||||||
|
Assert.Equal("my-project.proj", match.FromPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -937,6 +937,62 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async void MatcherDefaultFromPath()
|
||||||
|
{
|
||||||
|
var matchers = new IssueMatchersConfig
|
||||||
|
{
|
||||||
|
Matchers =
|
||||||
|
{
|
||||||
|
new IssueMatcherConfig
|
||||||
|
{
|
||||||
|
Owner = "my-matcher-1",
|
||||||
|
FromPath = "workflow-repo/some-project/some-project.proj",
|
||||||
|
Patterns = new[]
|
||||||
|
{
|
||||||
|
new IssuePatternConfig
|
||||||
|
{
|
||||||
|
Pattern = @"(.+): (.+)",
|
||||||
|
File = 1,
|
||||||
|
Message = 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
using (var hostContext = Setup(matchers: matchers))
|
||||||
|
using (_outputManager)
|
||||||
|
{
|
||||||
|
// Setup github.workspace, github.repository
|
||||||
|
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
|
||||||
|
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
|
||||||
|
Directory.CreateDirectory(workDirectory);
|
||||||
|
var workspaceDirectory = Path.Combine(workDirectory, "workspace");
|
||||||
|
Directory.CreateDirectory(workspaceDirectory);
|
||||||
|
_executionContext.Setup(x => x.GetGitHubContext("workspace")).Returns(workspaceDirectory);
|
||||||
|
_executionContext.Setup(x => x.GetGitHubContext("repository")).Returns("my-org/workflow-repo");
|
||||||
|
|
||||||
|
// Setup a git repository
|
||||||
|
var repositoryPath = Path.Combine(workspaceDirectory, "workflow-repo");
|
||||||
|
await CreateRepository(hostContext, repositoryPath, "https://github.com/my-org/workflow-repo");
|
||||||
|
|
||||||
|
// Create a test file
|
||||||
|
var filePath = Path.Combine(repositoryPath, "some-project", "some-directory", "some-file.txt");
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
|
||||||
|
File.WriteAllText(filePath, "");
|
||||||
|
|
||||||
|
// Process
|
||||||
|
Process("some-directory/some-file.txt: some error");
|
||||||
|
Assert.Equal(1, _issues.Count);
|
||||||
|
Assert.Equal("some error", _issues[0].Item1.Message);
|
||||||
|
Assert.Equal("some-project/some-directory/some-file.txt", _issues[0].Item1.Data["file"]);
|
||||||
|
Assert.Equal(0, _commands.Count);
|
||||||
|
Assert.Equal(0, _messages.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
|
|||||||
Reference in New Issue
Block a user