lintro 0.3.2__py3-none-any.whl → 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lintro might be problematic. Click here for more details.
- lintro/__init__.py +1 -1
- lintro/formatters/tools/__init__.py +12 -0
- lintro/formatters/tools/actionlint_formatter.py +82 -0
- lintro/formatters/tools/bandit_formatter.py +100 -0
- lintro/parsers/__init__.py +21 -0
- lintro/parsers/actionlint/__init__.py +1 -0
- lintro/parsers/actionlint/actionlint_issue.py +24 -0
- lintro/parsers/actionlint/actionlint_parser.py +67 -0
- lintro/tools/__init__.py +4 -0
- lintro/tools/core/tool_base.py +6 -4
- lintro/tools/implementations/tool_actionlint.py +151 -0
- lintro/tools/implementations/tool_bandit.py +445 -0
- lintro/tools/implementations/tool_darglint.py +2 -2
- lintro/tools/implementations/tool_hadolint.py +1 -1
- lintro/tools/implementations/tool_yamllint.py +1 -1
- lintro/tools/tool_enum.py +4 -0
- lintro/utils/console_logger.py +21 -5
- lintro/utils/formatting.py +4 -2
- lintro/utils/tool_executor.py +11 -6
- lintro/utils/tool_utils.py +19 -0
- {lintro-0.3.2.dist-info → lintro-0.4.2.dist-info}/METADATA +35 -28
- {lintro-0.3.2.dist-info → lintro-0.4.2.dist-info}/RECORD +26 -19
- {lintro-0.3.2.dist-info → lintro-0.4.2.dist-info}/licenses/LICENSE +1 -1
- {lintro-0.3.2.dist-info → lintro-0.4.2.dist-info}/WHEEL +0 -0
- {lintro-0.3.2.dist-info → lintro-0.4.2.dist-info}/entry_points.txt +0 -0
- {lintro-0.3.2.dist-info → lintro-0.4.2.dist-info}/top_level.txt +0 -0
lintro/__init__.py
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
"""Tool-specific table formatters package exports."""
|
|
2
2
|
|
|
3
|
+
from lintro.formatters.tools.actionlint_formatter import (
|
|
4
|
+
ActionlintTableDescriptor,
|
|
5
|
+
format_actionlint_issues,
|
|
6
|
+
)
|
|
7
|
+
from lintro.formatters.tools.bandit_formatter import (
|
|
8
|
+
BanditTableDescriptor,
|
|
9
|
+
format_bandit_issues,
|
|
10
|
+
)
|
|
3
11
|
from lintro.formatters.tools.darglint_formatter import (
|
|
4
12
|
DarglintTableDescriptor,
|
|
5
13
|
format_darglint_issues,
|
|
@@ -22,6 +30,10 @@ from lintro.formatters.tools.yamllint_formatter import (
|
|
|
22
30
|
)
|
|
23
31
|
|
|
24
32
|
__all__ = [
|
|
33
|
+
"ActionlintTableDescriptor",
|
|
34
|
+
"format_actionlint_issues",
|
|
35
|
+
"BanditTableDescriptor",
|
|
36
|
+
"format_bandit_issues",
|
|
25
37
|
"DarglintTableDescriptor",
|
|
26
38
|
"format_darglint_issues",
|
|
27
39
|
"HadolintTableDescriptor",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Formatter for actionlint issues."""
|
|
2
|
+
|
|
3
|
+
from lintro.formatters.core.table_descriptor import TableDescriptor
|
|
4
|
+
from lintro.formatters.styles.csv import CsvStyle
|
|
5
|
+
from lintro.formatters.styles.grid import GridStyle
|
|
6
|
+
from lintro.formatters.styles.html import HtmlStyle
|
|
7
|
+
from lintro.formatters.styles.json import JsonStyle
|
|
8
|
+
from lintro.formatters.styles.markdown import MarkdownStyle
|
|
9
|
+
from lintro.formatters.styles.plain import PlainStyle
|
|
10
|
+
from lintro.parsers.actionlint.actionlint_issue import ActionlintIssue
|
|
11
|
+
from lintro.utils.path_utils import normalize_file_path_for_display
|
|
12
|
+
|
|
13
|
+
FORMAT_MAP = {
|
|
14
|
+
"plain": PlainStyle(),
|
|
15
|
+
"grid": GridStyle(),
|
|
16
|
+
"markdown": MarkdownStyle(),
|
|
17
|
+
"html": HtmlStyle(),
|
|
18
|
+
"json": JsonStyle(),
|
|
19
|
+
"csv": CsvStyle(),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ActionlintTableDescriptor(TableDescriptor):
|
|
24
|
+
"""Table descriptor for rendering Actionlint issues.
|
|
25
|
+
|
|
26
|
+
Provides the column schema and row extraction logic so any output style
|
|
27
|
+
can render a uniform table across formats (grid, markdown, html, csv, json).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def get_columns(self) -> list[str]:
|
|
31
|
+
"""Return the ordered column headers for Actionlint issues.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
list[str]: Column names used by formatters.
|
|
35
|
+
"""
|
|
36
|
+
return ["File", "Line", "Column", "Level", "Code", "Message"]
|
|
37
|
+
|
|
38
|
+
def get_rows(self, issues: list[ActionlintIssue]) -> list[list[str]]:
|
|
39
|
+
"""Convert Actionlint issues to a list of row values.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
issues: Parsed Actionlint issues to render.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
list[list[str]]: One row per issue matching the column order.
|
|
46
|
+
"""
|
|
47
|
+
rows: list[list[str]] = []
|
|
48
|
+
for issue in issues:
|
|
49
|
+
rows.append(
|
|
50
|
+
[
|
|
51
|
+
normalize_file_path_for_display(issue.file),
|
|
52
|
+
str(issue.line),
|
|
53
|
+
str(issue.column),
|
|
54
|
+
issue.level,
|
|
55
|
+
issue.code or "",
|
|
56
|
+
issue.message,
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
return rows
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def format_actionlint_issues(
|
|
63
|
+
issues: list[ActionlintIssue],
|
|
64
|
+
format: str = "grid",
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Format Actionlint issues using the selected output style.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
issues: Parsed Actionlint issues to format.
|
|
70
|
+
format: Output style key (plain, grid, markdown, html, json, csv).
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
str: Rendered table content for the chosen style.
|
|
74
|
+
"""
|
|
75
|
+
descriptor = ActionlintTableDescriptor()
|
|
76
|
+
formatter = FORMAT_MAP.get(format, GridStyle())
|
|
77
|
+
columns = descriptor.get_columns()
|
|
78
|
+
rows = descriptor.get_rows(issues)
|
|
79
|
+
# JsonStyle may accept tool_name but others do not; keep simple
|
|
80
|
+
if isinstance(formatter, JsonStyle):
|
|
81
|
+
return formatter.format(columns=columns, rows=rows, tool_name="actionlint")
|
|
82
|
+
return formatter.format(columns=columns, rows=rows)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Bandit formatter for security issue output."""
|
|
2
|
+
|
|
3
|
+
from lintro.formatters.core.table_descriptor import TableDescriptor
|
|
4
|
+
from lintro.parsers.bandit.bandit_issue import BanditIssue
|
|
5
|
+
from lintro.utils.path_utils import normalize_file_path_for_display
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BanditTableDescriptor(TableDescriptor):
|
|
9
|
+
"""Table descriptor for Bandit security issues."""
|
|
10
|
+
|
|
11
|
+
def get_columns(self) -> list[str]:
|
|
12
|
+
"""Get column headers for Bandit issues table.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
list[str]: List of column headers.
|
|
16
|
+
"""
|
|
17
|
+
return ["File", "Line", "Test ID", "Severity", "Confidence", "Issue"]
|
|
18
|
+
|
|
19
|
+
def get_rows(self, issues: list[BanditIssue]) -> list[list[str]]:
|
|
20
|
+
"""Convert Bandit issues to table rows.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
issues: list[BanditIssue]: List of Bandit issues.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
list[list[str]]: List of table rows.
|
|
27
|
+
"""
|
|
28
|
+
rows = []
|
|
29
|
+
for issue in issues:
|
|
30
|
+
# Get severity icon
|
|
31
|
+
severity_icon = self._get_severity_icon(issue.issue_severity)
|
|
32
|
+
|
|
33
|
+
rows.append(
|
|
34
|
+
[
|
|
35
|
+
normalize_file_path_for_display(issue.file),
|
|
36
|
+
str(issue.line),
|
|
37
|
+
issue.test_id,
|
|
38
|
+
f"{severity_icon} {issue.issue_severity}",
|
|
39
|
+
issue.issue_confidence,
|
|
40
|
+
issue.issue_text,
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
return rows
|
|
44
|
+
|
|
45
|
+
def _get_severity_icon(self, severity: str) -> str:
|
|
46
|
+
"""Get icon for severity level.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
severity: str: Severity level.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
str: Icon character.
|
|
53
|
+
"""
|
|
54
|
+
return {
|
|
55
|
+
"HIGH": "🔴",
|
|
56
|
+
"MEDIUM": "🟠",
|
|
57
|
+
"LOW": "🟡",
|
|
58
|
+
"UNKNOWN": "⚪",
|
|
59
|
+
}.get(severity.upper(), "⚪")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def format_bandit_issues(
|
|
63
|
+
issues: list[BanditIssue],
|
|
64
|
+
format: str = "grid",
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Format Bandit issues using the appropriate output style.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
issues: list[BanditIssue]: List of Bandit issues to format.
|
|
70
|
+
format: str: Output format (grid, plain, markdown, html, json, csv).
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
str: Formatted issues string.
|
|
74
|
+
"""
|
|
75
|
+
from lintro.formatters.styles.csv import CsvStyle
|
|
76
|
+
from lintro.formatters.styles.grid import GridStyle
|
|
77
|
+
from lintro.formatters.styles.html import HtmlStyle
|
|
78
|
+
from lintro.formatters.styles.json import JsonStyle
|
|
79
|
+
from lintro.formatters.styles.markdown import MarkdownStyle
|
|
80
|
+
from lintro.formatters.styles.plain import PlainStyle
|
|
81
|
+
|
|
82
|
+
style_map = {
|
|
83
|
+
"grid": GridStyle(),
|
|
84
|
+
"plain": PlainStyle(),
|
|
85
|
+
"markdown": MarkdownStyle(),
|
|
86
|
+
"html": HtmlStyle(),
|
|
87
|
+
"json": JsonStyle(),
|
|
88
|
+
"csv": CsvStyle(),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
style_key = (format or "grid").lower()
|
|
92
|
+
style = style_map.get(style_key, GridStyle())
|
|
93
|
+
descriptor = BanditTableDescriptor()
|
|
94
|
+
|
|
95
|
+
columns = descriptor.get_columns()
|
|
96
|
+
rows = descriptor.get_rows(issues)
|
|
97
|
+
|
|
98
|
+
if style_key == "json":
|
|
99
|
+
return style.format(columns=columns, rows=rows, tool_name="bandit")
|
|
100
|
+
return style.format(columns=columns, rows=rows)
|
lintro/parsers/__init__.py
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Parser modules for Lintro tools."""
|
|
2
|
+
|
|
3
|
+
from lintro.parsers import (
|
|
4
|
+
actionlint,
|
|
5
|
+
bandit,
|
|
6
|
+
darglint,
|
|
7
|
+
hadolint,
|
|
8
|
+
prettier,
|
|
9
|
+
ruff,
|
|
10
|
+
yamllint,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"actionlint",
|
|
15
|
+
"bandit",
|
|
16
|
+
"darglint",
|
|
17
|
+
"hadolint",
|
|
18
|
+
"prettier",
|
|
19
|
+
"ruff",
|
|
20
|
+
"yamllint",
|
|
21
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Actionlint parser package."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Issue model for actionlint output."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ActionlintIssue:
|
|
8
|
+
"""Represents a single actionlint issue parsed from CLI output.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
file: File path where the issue occurred.
|
|
12
|
+
line: Line number of the issue (1-based).
|
|
13
|
+
column: Column number of the issue (1-based).
|
|
14
|
+
level: Severity level (e.g., "error", "warning").
|
|
15
|
+
code: Optional rule/code identifier, when present.
|
|
16
|
+
message: Human-readable message describing the issue.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
file: str
|
|
20
|
+
line: int
|
|
21
|
+
column: int
|
|
22
|
+
level: str
|
|
23
|
+
code: str | None
|
|
24
|
+
message: str
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Parser for actionlint CLI output.
|
|
2
|
+
|
|
3
|
+
This module parses the default text output produced by the ``actionlint``
|
|
4
|
+
binary into structured ``ActionlintIssue`` objects so that Lintro can render
|
|
5
|
+
uniform tables and reports across styles.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Iterable
|
|
12
|
+
|
|
13
|
+
from lintro.parsers.actionlint.actionlint_issue import ActionlintIssue
|
|
14
|
+
|
|
15
|
+
_LINE_RE: re.Pattern[str] = re.compile(
|
|
16
|
+
r"^(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+):\s*(?:(?P<level>error|warning):\s*)?(?P<msg>.*?)(?:\s*\[(?P<code>[A-Za-z0-9_\-\.]+)\])?$",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_actionlint_output(output: str | None) -> list[ActionlintIssue]:
|
|
21
|
+
"""Parse raw actionlint output into structured issues.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
output: Raw stdout/stderr combined output from actionlint.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
list[ActionlintIssue]: Parsed issues from the tool output.
|
|
28
|
+
"""
|
|
29
|
+
if not output:
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
issues: list[ActionlintIssue] = []
|
|
33
|
+
for line in _iter_nonempty_lines(output):
|
|
34
|
+
m = _LINE_RE.match(line.strip())
|
|
35
|
+
if not m:
|
|
36
|
+
continue
|
|
37
|
+
file_path = m.group("file")
|
|
38
|
+
line_no = int(m.group("line"))
|
|
39
|
+
col_no = int(m.group("col"))
|
|
40
|
+
level = m.group("level") or "error"
|
|
41
|
+
msg = m.group("msg").strip()
|
|
42
|
+
code = m.group("code")
|
|
43
|
+
issues.append(
|
|
44
|
+
ActionlintIssue(
|
|
45
|
+
file=file_path,
|
|
46
|
+
line=line_no,
|
|
47
|
+
column=col_no,
|
|
48
|
+
level=level,
|
|
49
|
+
code=code,
|
|
50
|
+
message=msg,
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
return issues
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _iter_nonempty_lines(text: str) -> Iterable[str]:
|
|
57
|
+
"""Iterate non-empty lines from a text block.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
text: Input text to split into lines.
|
|
61
|
+
|
|
62
|
+
Yields:
|
|
63
|
+
Non-empty lines stripped of surrounding whitespace.
|
|
64
|
+
"""
|
|
65
|
+
for ln in text.splitlines():
|
|
66
|
+
if ln.strip():
|
|
67
|
+
yield ln
|
lintro/tools/__init__.py
CHANGED
|
@@ -6,6 +6,8 @@ from lintro.models.core.tool_config import ToolConfig
|
|
|
6
6
|
from lintro.tools.core.tool_manager import ToolManager
|
|
7
7
|
|
|
8
8
|
# Import core implementations after Tool class definition to avoid circular imports
|
|
9
|
+
from lintro.tools.implementations.tool_actionlint import ActionlintTool
|
|
10
|
+
from lintro.tools.implementations.tool_bandit import BanditTool
|
|
9
11
|
from lintro.tools.implementations.tool_darglint import DarglintTool
|
|
10
12
|
from lintro.tools.implementations.tool_hadolint import HadolintTool
|
|
11
13
|
from lintro.tools.implementations.tool_prettier import PrettierTool
|
|
@@ -32,6 +34,8 @@ __all__ = [
|
|
|
32
34
|
"ToolEnum",
|
|
33
35
|
"tool_manager",
|
|
34
36
|
"AVAILABLE_TOOLS",
|
|
37
|
+
"ActionlintTool",
|
|
38
|
+
"BanditTool",
|
|
35
39
|
"DarglintTool",
|
|
36
40
|
"HadolintTool",
|
|
37
41
|
"PrettierTool",
|
lintro/tools/core/tool_base.py
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
|
-
import subprocess
|
|
5
|
+
import subprocess # nosec B404 - subprocess used safely with shell=False
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
9
11
|
from lintro.enums.tool_type import ToolType
|
|
10
12
|
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
11
13
|
|
|
@@ -107,9 +109,9 @@ class BaseTool(ABC):
|
|
|
107
109
|
continue
|
|
108
110
|
if line_stripped not in self.exclude_patterns:
|
|
109
111
|
self.exclude_patterns.append(line_stripped)
|
|
110
|
-
except Exception:
|
|
112
|
+
except Exception as e:
|
|
111
113
|
# Non-fatal if ignore file can't be read
|
|
112
|
-
|
|
114
|
+
logger.debug(f"Could not read .lintro-ignore: {e}")
|
|
113
115
|
|
|
114
116
|
# Load default options from config
|
|
115
117
|
if hasattr(self.config, "options") and self.config.options:
|
|
@@ -146,7 +148,7 @@ class BaseTool(ABC):
|
|
|
146
148
|
FileNotFoundError: If command executable is not found.
|
|
147
149
|
"""
|
|
148
150
|
try:
|
|
149
|
-
result = subprocess.run(
|
|
151
|
+
result = subprocess.run( # nosec B603 - args list, shell=False
|
|
150
152
|
cmd,
|
|
151
153
|
capture_output=True,
|
|
152
154
|
text=True,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Actionlint integration for lintro.
|
|
2
|
+
|
|
3
|
+
This module wires the `actionlint` CLI into Lintro's tool system. It discovers
|
|
4
|
+
GitHub Actions workflow files, executes `actionlint`, parses its output into
|
|
5
|
+
structured issues, and returns a normalized `ToolResult`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess # nosec B404 - used safely with shell disabled
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
from lintro.enums.tool_type import ToolType
|
|
16
|
+
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
17
|
+
from lintro.parsers.actionlint.actionlint_parser import parse_actionlint_output
|
|
18
|
+
from lintro.tools.core.tool_base import BaseTool
|
|
19
|
+
from lintro.utils.tool_utils import walk_files_with_excludes
|
|
20
|
+
|
|
21
|
+
# Defaults
|
|
22
|
+
ACTIONLINT_DEFAULT_TIMEOUT: int = 30
|
|
23
|
+
ACTIONLINT_DEFAULT_PRIORITY: int = 40
|
|
24
|
+
ACTIONLINT_FILE_PATTERNS: list[str] = ["*.yml", "*.yaml"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ActionlintTool(BaseTool):
|
|
29
|
+
"""GitHub Actions workflow linter (actionlint).
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
name: Tool name used across the system.
|
|
33
|
+
description: Human-readable description for listings/help.
|
|
34
|
+
can_fix: Whether the tool can apply fixes (actionlint cannot).
|
|
35
|
+
config: `ToolConfig` with defaults for priority, file patterns, and
|
|
36
|
+
`ToolType` classification.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name: str = "actionlint"
|
|
40
|
+
description: str = "Static checker for GitHub Actions workflows"
|
|
41
|
+
can_fix: bool = False
|
|
42
|
+
config: ToolConfig = field(
|
|
43
|
+
default_factory=lambda: ToolConfig(
|
|
44
|
+
priority=ACTIONLINT_DEFAULT_PRIORITY,
|
|
45
|
+
conflicts_with=[],
|
|
46
|
+
file_patterns=ACTIONLINT_FILE_PATTERNS,
|
|
47
|
+
tool_type=ToolType.LINTER | ToolType.INFRASTRUCTURE,
|
|
48
|
+
options={
|
|
49
|
+
"timeout": ACTIONLINT_DEFAULT_TIMEOUT,
|
|
50
|
+
# Option placeholders for future extension (e.g., color, format)
|
|
51
|
+
},
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _build_command(self) -> list[str]:
|
|
56
|
+
"""Build the base actionlint command.
|
|
57
|
+
|
|
58
|
+
We intentionally avoid flags here for maximum portability across
|
|
59
|
+
platforms and actionlint versions. The tool's default text output
|
|
60
|
+
follows the conventional ``file:line:col: message [CODE]`` format,
|
|
61
|
+
which our parser handles directly without requiring a custom format
|
|
62
|
+
switch.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The base command list for invoking actionlint.
|
|
66
|
+
"""
|
|
67
|
+
return ["actionlint"]
|
|
68
|
+
|
|
69
|
+
def check(self, paths: list[str]) -> ToolResult:
|
|
70
|
+
"""Check GitHub Actions workflow files with actionlint.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
paths: File or directory paths to search for workflow files.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
A `ToolResult` containing success status, aggregated output (if any),
|
|
77
|
+
issue count, and parsed issues.
|
|
78
|
+
"""
|
|
79
|
+
self._validate_paths(paths=paths)
|
|
80
|
+
if not paths:
|
|
81
|
+
return ToolResult(
|
|
82
|
+
name=self.name,
|
|
83
|
+
success=True,
|
|
84
|
+
output="No files to check.",
|
|
85
|
+
issues_count=0,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
candidate_yaml_files: list[str] = walk_files_with_excludes(
|
|
89
|
+
paths=paths,
|
|
90
|
+
file_patterns=self.config.file_patterns,
|
|
91
|
+
exclude_patterns=self.exclude_patterns,
|
|
92
|
+
include_venv=self.include_venv,
|
|
93
|
+
)
|
|
94
|
+
# Restrict to GitHub Actions workflow location
|
|
95
|
+
workflow_files: list[str] = []
|
|
96
|
+
for file_path in candidate_yaml_files:
|
|
97
|
+
norm = file_path.replace("\\", "/")
|
|
98
|
+
if "/.github/workflows/" in norm:
|
|
99
|
+
workflow_files.append(file_path)
|
|
100
|
+
logger.debug(f"Files to check (actionlint): {workflow_files}")
|
|
101
|
+
|
|
102
|
+
if not workflow_files:
|
|
103
|
+
return ToolResult(
|
|
104
|
+
name=self.name,
|
|
105
|
+
success=True,
|
|
106
|
+
output="No GitHub workflow files found to check.",
|
|
107
|
+
issues_count=0,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
timeout: int = self.options.get("timeout", ACTIONLINT_DEFAULT_TIMEOUT)
|
|
111
|
+
all_outputs: list[str] = []
|
|
112
|
+
all_issues = []
|
|
113
|
+
all_success = True
|
|
114
|
+
|
|
115
|
+
base_cmd = self._build_command()
|
|
116
|
+
for file_path in workflow_files:
|
|
117
|
+
cmd = base_cmd + [file_path]
|
|
118
|
+
try:
|
|
119
|
+
success, output = self._run_subprocess(cmd=cmd, timeout=timeout)
|
|
120
|
+
issues = parse_actionlint_output(output)
|
|
121
|
+
if not success:
|
|
122
|
+
all_success = False
|
|
123
|
+
if issues:
|
|
124
|
+
all_outputs.append(output)
|
|
125
|
+
all_issues.extend(issues)
|
|
126
|
+
except subprocess.TimeoutExpired:
|
|
127
|
+
all_success = False
|
|
128
|
+
all_outputs.append(f"Timeout while checking {file_path}")
|
|
129
|
+
except Exception as e: # pragma: no cover
|
|
130
|
+
all_success = False
|
|
131
|
+
all_outputs.append(f"Error checking {file_path}: {e}")
|
|
132
|
+
|
|
133
|
+
combined_output = "\n".join(all_outputs) if all_outputs else None
|
|
134
|
+
return ToolResult(
|
|
135
|
+
name=self.name,
|
|
136
|
+
success=all_success,
|
|
137
|
+
output=combined_output,
|
|
138
|
+
issues_count=len(all_issues),
|
|
139
|
+
issues=all_issues,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def fix(self, paths: list[str]) -> ToolResult:
|
|
143
|
+
"""Raise since actionlint cannot apply automatic fixes.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
paths: File or directory paths (ignored).
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
NotImplementedError: Actionlint does not support auto-fixing.
|
|
150
|
+
"""
|
|
151
|
+
raise NotImplementedError("actionlint cannot automatically fix issues.")
|