Skip to content

Commit 9ed2201

Browse files
committed
ci: pre-commit & generate actions
1 parent deb48ef commit 9ed2201

File tree

5 files changed

+539
-0
lines changed

5 files changed

+539
-0
lines changed

.github/logToCs.py

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
#!/usr/bin/env python3
2+
# pylint: disable=invalid-name
3+
"""
4+
Convert a log to CheckStyle format.
5+
6+
Url: https://github.com/mdeweerd/LogToCheckStyle
7+
8+
The log can then be used for generating annotations in a github action.
9+
10+
Note: this script is very young and "quick and dirty".
11+
Patterns can be added to "PATTERNS" to match more messages.
12+
13+
# Examples
14+
15+
Assumes that logToCs.py is available as .github/logToCs.py.
16+
17+
## Example 1:
18+
19+
20+
```yaml
21+
- run: |
22+
pre-commit run -all-files | tee pre-commit.log
23+
.github/logToCs.py pre-commit.log pre-commit.xml
24+
- uses: staabm/annotate-pull-request-from-checkstyle-action@v1
25+
with:
26+
files: pre-commit.xml
27+
notices-as-warnings: true # optional
28+
```
29+
30+
## Example 2:
31+
32+
33+
```yaml
34+
- run: |
35+
pre-commit run --all-files | tee pre-commit.log
36+
- name: Add results to PR
37+
if: ${{ always() }}
38+
run: |
39+
.github/logToCs.py pre-commit.log | cs2pr
40+
```
41+
42+
Author(s):
43+
- https://github.com/mdeweerd
44+
45+
License: MIT License
46+
47+
"""
48+
49+
import argparse
50+
import re
51+
import sys
52+
import xml.etree.ElementTree as ET # nosec
53+
54+
55+
def convert_to_checkstyle(messages):
56+
"""
57+
Convert provided message to CheckStyle format.
58+
"""
59+
root = ET.Element("checkstyle")
60+
for message in messages:
61+
fields = parse_message(message)
62+
if fields:
63+
add_error_entry(root, **fields)
64+
return ET.tostring(root, encoding="utf-8").decode("utf-8")
65+
66+
67+
def convert_text_to_checkstyle(text):
68+
"""
69+
Convert provided message to CheckStyle format.
70+
"""
71+
root = ET.Element("checkstyle")
72+
for fields in parse_file(text):
73+
if fields:
74+
add_error_entry(root, **fields)
75+
return ET.tostring(root, encoding="utf-8").decode("utf-8")
76+
77+
78+
ANY_REGEX = r".*?"
79+
FILE_REGEX = r"\s*(?P<file_name>\S.*?)\s*?"
80+
EOL_REGEX = r"[\r\n]"
81+
LINE_REGEX = r"\s*(?P<line>\d+?)\s*?"
82+
COLUMN_REGEX = r"\s*(?P<column>\d+?)\s*?"
83+
SEVERITY_REGEX = r"\s*(?P<severity>error|warning|notice|style|info)\s*?"
84+
MSG_REGEX = r"\s*(?P<message>.+?)\s*?"
85+
MULTILINE_MSG_REGEX = r"\s*(?P<message>(?:.|.[\r\n])+)"
86+
# cpplint confidence index
87+
CONFIDENCE_REGEX = r"\s*\[(?P<confidence>\d+)\]\s*?"
88+
89+
90+
# List of message patterns, add more specific patterns earlier in the list
91+
# Creating patterns by using constants makes them easier to define and read.
92+
PATTERNS = [
93+
# beautysh
94+
# File ftp.sh: error: "esac" before "case" in line 90.
95+
re.compile(
96+
f"^File {FILE_REGEX}:{SEVERITY_REGEX}:"
97+
f" {MSG_REGEX} in line {LINE_REGEX}.$"
98+
),
99+
# beautysh
100+
# File socks4echo.sh: error: indent/outdent mismatch: -2.
101+
re.compile(f"^File {FILE_REGEX}:{SEVERITY_REGEX}: {MSG_REGEX}$"),
102+
# ESLint (JavaScript Linter), RoboCop, shellcheck
103+
# path/to/file.js:10:2: Some linting issue
104+
# path/to/file.rb:10:5: Style/Indentation: Incorrect indentation detected
105+
# path/to/script.sh:10:1: SC2034: Some shell script issue
106+
re.compile(f"^{FILE_REGEX}:{LINE_REGEX}:{COLUMN_REGEX}: {MSG_REGEX}$"),
107+
# Cpplint default output:
108+
# '%s:%s: %s [%s] [%d]\n'
109+
# % (filename, linenum, message, category, confidence)
110+
re.compile(f"^{FILE_REGEX}:{LINE_REGEX}:{MSG_REGEX}{CONFIDENCE_REGEX}$"),
111+
# MSVC
112+
# file.cpp(10): error C1234: Some error message
113+
re.compile(
114+
f"^{FILE_REGEX}\\({LINE_REGEX}\\):{SEVERITY_REGEX}{MSG_REGEX}$"
115+
),
116+
# Java compiler
117+
# File.java:10: error: Some error message
118+
re.compile(f"^{FILE_REGEX}:{LINE_REGEX}:{SEVERITY_REGEX}:{MSG_REGEX}$"),
119+
# Python
120+
# File ".../logToCs.py", line 90 (note: code line follows)
121+
re.compile(f'^File "{FILE_REGEX}", line {LINE_REGEX}$'),
122+
# Pylint, others
123+
# path/to/file.py:10: [C0111] Missing docstring
124+
# others
125+
re.compile(f"^{FILE_REGEX}:{LINE_REGEX}: {MSG_REGEX}$"),
126+
# Shellcheck:
127+
# In script.sh line 76:
128+
re.compile(
129+
f"^In {FILE_REGEX} line {LINE_REGEX}:{EOL_REGEX}?"
130+
f"({MULTILINE_MSG_REGEX})?{EOL_REGEX}{EOL_REGEX}"
131+
),
132+
]
133+
134+
# Severities available in CodeSniffer report format
135+
SEVERITY_NOTICE = "notice"
136+
SEVERITY_WARNING = "warning"
137+
SEVERITY_ERROR = "error"
138+
139+
140+
def strip_ansi(text: str):
141+
"""
142+
Strip ANSI escape sequences from string (colors, etc)
143+
"""
144+
return re.sub(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])", "", text)
145+
146+
147+
def parse_file(text):
148+
"""
149+
Parse all messages in a file
150+
151+
Returns the fields in a dict.
152+
"""
153+
# regex required to allow same group names
154+
import regex # pylint: disable=import-outside-toplevel
155+
156+
patterns = [pattern.pattern for pattern in PATTERNS]
157+
# patterns = [PATTERNS[0].pattern]
158+
159+
full_regex = "(?:(?:" + (")|(?:".join(patterns)) + "))"
160+
results = []
161+
162+
for fields in regex.finditer(
163+
full_regex, strip_ansi(text), regex.MULTILINE
164+
):
165+
if not fields:
166+
continue
167+
result = fields.groupdict()
168+
169+
if len(result) == 0:
170+
continue
171+
severity = result.get("severity", None)
172+
confidence = result.pop("confidence", None)
173+
174+
if confidence is not None:
175+
# Convert confidence level of cpplint
176+
# to warning, etc.
177+
confidence = int(confidence)
178+
179+
if confidence <= 1:
180+
severity = SEVERITY_NOTICE
181+
elif confidence >= 5:
182+
severity = SEVERITY_ERROR
183+
else:
184+
severity = SEVERITY_WARNING
185+
186+
if severity is None:
187+
severity = SEVERITY_ERROR
188+
else:
189+
severity = severity.lower()
190+
191+
if severity in ["info", "style"]:
192+
severity = SEVERITY_NOTICE
193+
194+
result["severity"] = severity
195+
196+
results.append(result)
197+
198+
return results
199+
200+
201+
def parse_message(message):
202+
"""
203+
Parse message until it matches a pattern.
204+
205+
Returns the fields in a dict.
206+
"""
207+
for pattern in PATTERNS:
208+
fields = pattern.match(message)
209+
if not fields:
210+
continue
211+
result = fields.groupdict()
212+
if len(result) == 0:
213+
continue
214+
215+
if "confidence" in result:
216+
# Convert confidence level of cpplint
217+
# to warning, etc.
218+
confidence = int(result["confidence"])
219+
del result["confidence"]
220+
221+
if confidence <= 1:
222+
severity = SEVERITY_NOTICE
223+
elif confidence >= 5:
224+
severity = SEVERITY_ERROR
225+
else:
226+
severity = SEVERITY_WARNING
227+
result["severity"] = severity
228+
229+
if "severity" not in result:
230+
result["severity"] = SEVERITY_ERROR
231+
else:
232+
result["severity"] = result["severity"].lower()
233+
234+
if result["severity"] in ["info", "style"]:
235+
result["severity"] = SEVERITY_NOTICE
236+
237+
return result
238+
239+
# Nothing matched
240+
return None
241+
242+
243+
def add_error_entry( # pylint: disable=too-many-arguments
244+
root,
245+
severity,
246+
file_name,
247+
line=None,
248+
column=None,
249+
message=None,
250+
source=None,
251+
):
252+
"""
253+
Add error information to the CheckStyle output being created.
254+
"""
255+
file_element = find_or_create_file_element(root, file_name)
256+
error_element = ET.SubElement(file_element, "error")
257+
error_element.set("severity", severity)
258+
if line:
259+
error_element.set("line", line)
260+
if column:
261+
error_element.set("column", column)
262+
if message:
263+
error_element.set("message", message)
264+
if source:
265+
# To verify if this is a valid attribute
266+
error_element.set("source", source)
267+
268+
269+
def find_or_create_file_element(root, file_name):
270+
"""
271+
Find/create file element in XML document tree.
272+
"""
273+
for file_element in root.findall("file"):
274+
if file_element.get("name") == file_name:
275+
return file_element
276+
file_element = ET.SubElement(root, "file")
277+
file_element.set("name", file_name)
278+
return file_element
279+
280+
281+
def main():
282+
"""
283+
Parse the script arguments and get the conversion done.
284+
"""
285+
parser = argparse.ArgumentParser(
286+
description="Convert messages to Checkstyle XML format."
287+
)
288+
parser.add_argument(
289+
"input", help="Input file. Use '-' for stdin.", nargs="?", default="-"
290+
)
291+
parser.add_argument(
292+
"output",
293+
help="Output file. Use '-' for stdout.",
294+
nargs="?",
295+
default="-",
296+
)
297+
parser.add_argument(
298+
"-i",
299+
"--input-named",
300+
help="Named input file. Overrides positional input.",
301+
)
302+
parser.add_argument(
303+
"-o",
304+
"--output-named",
305+
help="Named output file. Overrides positional output.",
306+
)
307+
308+
args = parser.parse_args()
309+
310+
if args.input == "-" and args.input_named:
311+
with open(args.input_named, encoding="utf_8") as input_file:
312+
text = input_file.read()
313+
elif args.input != "-":
314+
with open(args.input, encoding="utf_8") as input_file:
315+
text = input_file.read()
316+
else:
317+
text = sys.stdin.read()
318+
319+
try:
320+
checkstyle_xml = convert_text_to_checkstyle(text)
321+
except ImportError:
322+
checkstyle_xml = convert_to_checkstyle(re.split(r"[\r\n]+", text))
323+
324+
if args.output == "-" and args.output_named:
325+
with open(args.output_named, "w", encoding="utf_8") as output_file:
326+
output_file.write(checkstyle_xml)
327+
elif args.output != "-":
328+
with open(args.output, "w", encoding="utf_8") as output_file:
329+
output_file.write(checkstyle_xml)
330+
else:
331+
print(checkstyle_xml)
332+
333+
334+
if __name__ == "__main__":
335+
main()

.github/workflows/generate.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
name: Generate files (documentation, autotools)
3+
on:
4+
push:
5+
paths: [man.md, aclocal.m4, configure.ac]
6+
workflow_dispatch:
7+
jobs:
8+
convert_via_pandoc:
9+
runs-on: ubuntu-22.04
10+
steps:
11+
- uses: actions/checkout@v3
12+
- uses: dorny/paths-filter@v2
13+
id: changes
14+
with:
15+
filters: |
16+
man:
17+
- 'man.md'
18+
autotools:
19+
- 'aclocal.m4'
20+
- 'configure.ac'
21+
- uses: docker://pandoc/core:2.17
22+
if: ${{ github.event_name == 'workflow_dispath' || steps.changes.outputs.man == 'true' }}
23+
with:
24+
args: -s man.md -t man -o shc.1
25+
- uses: docker://pandoc/core:2.17
26+
if: ${{ github.event_name == 'workflow_dispath' || steps.changes.outputs.man == 'true' }}
27+
with:
28+
args: -s man.md -t html -o man.html
29+
- run: |-
30+
./autogen.sh
31+
if: ${{ github.event_name == 'workflow_dispath' || steps.changes.outputs.autotools == 'true' }}
32+
- name: Commit changes
33+
if: ${{ github.event_name == 'workflow_dispath' || steps.changes.outputs.man == 'true' || steps.changes.outputs.autotools }}
34+
run: |-
35+
for r in $(git remote) ; do git remote get-url --all $r ; done
36+
git config user.name github-actions
37+
git config user.email github-actions@github.com
38+
git commit -a -m "ci: Github Action Generate Files"
39+
git push

0 commit comments

Comments
 (0)