Skip to content

Commit 6446b33

Browse files
congxiao-wxxclaude
andcommitted
ci: add lint, format-check, and security gates to CI pipeline
Add three new CI enforcement layers that were previously missing: - ruff lint + format check job (was only in Makefile, never enforced in CI) - pip-audit dependency vulnerability scanning job - Expanded ruff rules: S (bandit/security), B (bugbear), UP (pyupgrade) Fix all resulting lint violations across src/ and tests/: - B904: add proper exception chains (raise from e) - E501: wrap long lines in help strings and docstrings - S110: annotate intentional try/except/pass with noqa + justification - S101: replace assert with proper guard in invoke_cmd - B007: prefix unused loop variable with underscore - UP022: use capture_output instead of stdout/stderr=PIPE Add matching Makefile targets (format-check, security) and pin pip-audit in dev dependencies for reproducible local runs. Change-Id: I94d2355be83f4c44b144f217d2a0d0d5b74f5e2f Co-developed-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 09c0ec0 commit 6446b33

55 files changed

Lines changed: 3054 additions & 1364 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,31 @@ env:
1919
PIP_DISABLE_PIP_VERSION_CHECK: '1'
2020

2121
jobs:
22+
# ---------------------------------------------------------------------
23+
# Lint & format check with ruff.
24+
# ---------------------------------------------------------------------
25+
lint:
26+
name: Lint (ruff)
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- uses: actions/setup-python@v5
32+
with:
33+
python-version: '3.11'
34+
cache: 'pip'
35+
36+
- name: Install project + dev deps
37+
run: |
38+
python -m pip install --upgrade pip
39+
python -m pip install -e ".[dev]"
40+
41+
- name: ruff check
42+
run: ruff check src/ tests/
43+
44+
- name: ruff format check
45+
run: ruff format --check src/ tests/
46+
2247
# ---------------------------------------------------------------------
2348
# Static type check with mypy.
2449
# ---------------------------------------------------------------------
@@ -123,3 +148,26 @@ jobs:
123148
agentrun --version
124149
agentrun --help
125150
ar --version
151+
152+
# ---------------------------------------------------------------------
153+
# Dependency vulnerability scan with pip-audit.
154+
# Catches known CVEs in transitive dependencies before they ship.
155+
# ---------------------------------------------------------------------
156+
security:
157+
name: Security (pip-audit)
158+
runs-on: ubuntu-latest
159+
steps:
160+
- uses: actions/checkout@v4
161+
162+
- uses: actions/setup-python@v5
163+
with:
164+
python-version: '3.11'
165+
cache: 'pip'
166+
167+
- name: Install project + dev deps
168+
run: |
169+
python -m pip install --upgrade pip
170+
python -m pip install -e ".[dev]"
171+
172+
- name: Audit dependencies
173+
run: pip-audit

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help install dev lint test test-unit test-integration test-cov coverage clean build build-linux build-macos build-all
1+
.PHONY: help install dev lint format-check security test test-unit test-integration test-cov coverage clean build build-linux build-macos build-all
22

33
PYTHON ?= python3
44
VENV := .venv
@@ -26,6 +26,12 @@ dev: ## Install with dev dependencies
2626
lint: ## Run ruff linter
2727
$(BIN)/ruff check src/ tests/
2828

29+
format-check: ## Check code formatting (ruff format)
30+
$(BIN)/ruff format --check src/ tests/
31+
32+
security: ## Audit dependencies for known vulnerabilities
33+
$(BIN)/pip-audit
34+
2935
test: ## Run all tests (unit + integration)
3036
$(BIN)/pytest tests/unit tests/integration -v
3137

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dev = [
4242
"ruff>=0.14.0",
4343
"mypy>=1.11.0",
4444
"types-PyYAML>=6.0",
45+
"pip-audit>=2.7.0",
4546
]
4647

4748
[project.optional-dependencies]
@@ -52,14 +53,18 @@ dev = [
5253
"ruff>=0.14.0",
5354
"mypy>=1.11.0",
5455
"types-PyYAML>=6.0",
56+
"pip-audit>=2.7.0",
5557
]
5658

5759
[tool.ruff]
5860
line-length = 88
5961
target-version = "py310"
6062

6163
[tool.ruff.lint]
62-
select = ["E", "F", "I", "W"]
64+
select = ["E", "F", "I", "W", "S", "B", "UP"]
65+
66+
[tool.ruff.lint.per-file-ignores]
67+
"tests/**" = ["S101", "S105", "S108", "S603", "S607"]
6368

6469
[tool.mypy]
6570
# Start permissive: only check annotated code. Tighten incrementally by

src/agentrun_cli/_utils/config.py

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import json
2424
import os
2525
from pathlib import Path
26-
from typing import TYPE_CHECKING, Any, Optional
26+
from typing import TYPE_CHECKING
2727

2828
if TYPE_CHECKING:
2929
from agentrun.utils.config import Config
@@ -42,7 +42,7 @@ def load_config() -> dict:
4242
"""Load the config file. Returns an empty structure if the file is missing."""
4343
if not CONFIG_FILE.exists():
4444
return {"profiles": {}, "defaults": {"profile": "default", "output": "json"}}
45-
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
45+
with open(CONFIG_FILE, encoding="utf-8") as f:
4646
return json.load(f)
4747

4848

@@ -54,27 +54,27 @@ def save_config(config: dict) -> None:
5454
f.write("\n")
5555

5656

57-
def get_profile(profile_name: Optional[str] = None) -> dict:
57+
def get_profile(profile_name: str | None = None) -> dict:
5858
"""Return the settings dict for the given profile (or the default one)."""
5959
config = load_config()
6060
name = profile_name or config.get("defaults", {}).get("profile", "default")
6161
return config.get("profiles", {}).get(name, {})
6262

6363

64-
def set_profile_value(key: str, value: str, profile_name: Optional[str] = None) -> None:
64+
def set_profile_value(key: str, value: str, profile_name: str | None = None) -> None:
6565
"""Set a single key inside a profile and save."""
6666
config = load_config()
6767
name = profile_name or config.get("defaults", {}).get("profile", "default")
6868
config.setdefault("profiles", {}).setdefault(name, {})[key] = value
6969
save_config(config)
7070

7171

72-
def get_profile_value(key: str, profile_name: Optional[str] = None) -> Optional[str]:
72+
def get_profile_value(key: str, profile_name: str | None = None) -> str | None:
7373
"""Read a single key from the active profile."""
7474
return get_profile(profile_name).get(key)
7575

7676

77-
def _env(*names: str) -> Optional[str]:
77+
def _env(*names: str) -> str | None:
7878
"""Return the first non-empty env var value, or None."""
7979
for name in names:
8080
val = os.getenv(name)
@@ -84,8 +84,8 @@ def _env(*names: str) -> Optional[str]:
8484

8585

8686
def build_sdk_config(
87-
profile_name: Optional[str] = None,
88-
region: Optional[str] = None,
87+
profile_name: str | None = None,
88+
region: str | None = None,
8989
) -> "Config":
9090
"""Build an ``agentrun.utils.config.Config`` from CLI context.
9191
@@ -101,28 +101,35 @@ def build_sdk_config(
101101

102102
profile = get_profile(profile_name)
103103

104-
ak = (profile.get("access_key_id")
105-
or _env("AGENTRUN_ACCESS_KEY_ID", "ALIBABA_CLOUD_ACCESS_KEY_ID")
106-
or None)
107-
sk = (profile.get("access_key_secret")
108-
or _env("AGENTRUN_ACCESS_KEY_SECRET", "ALIBABA_CLOUD_ACCESS_KEY_SECRET")
109-
or None)
110-
token = (profile.get("security_token")
111-
or _env("AGENTRUN_SECURITY_TOKEN", "ALIBABA_CLOUD_SECURITY_TOKEN")
112-
or None)
113-
account = (profile.get("account_id")
114-
or _env("AGENTRUN_ACCOUNT_ID", "FC_ACCOUNT_ID")
115-
or None)
116-
rid = (region
117-
or profile.get("region")
118-
or _env("AGENTRUN_REGION", "FC_REGION")
119-
or None)
120-
control_endpoint = (profile.get("control_endpoint")
121-
or _env("AGENTRUN_CONTROL_ENDPOINT")
122-
or None)
123-
data_endpoint = (profile.get("data_endpoint")
124-
or _env("AGENTRUN_DATA_ENDPOINT")
125-
or None)
104+
ak = (
105+
profile.get("access_key_id")
106+
or _env("AGENTRUN_ACCESS_KEY_ID", "ALIBABA_CLOUD_ACCESS_KEY_ID")
107+
or None
108+
)
109+
sk = (
110+
profile.get("access_key_secret")
111+
or _env("AGENTRUN_ACCESS_KEY_SECRET", "ALIBABA_CLOUD_ACCESS_KEY_SECRET")
112+
or None
113+
)
114+
token = (
115+
profile.get("security_token")
116+
or _env("AGENTRUN_SECURITY_TOKEN", "ALIBABA_CLOUD_SECURITY_TOKEN")
117+
or None
118+
)
119+
account = (
120+
profile.get("account_id")
121+
or _env("AGENTRUN_ACCOUNT_ID", "FC_ACCOUNT_ID")
122+
or None
123+
)
124+
rid = (
125+
region or profile.get("region") or _env("AGENTRUN_REGION", "FC_REGION") or None
126+
)
127+
control_endpoint = (
128+
profile.get("control_endpoint") or _env("AGENTRUN_CONTROL_ENDPOINT") or None
129+
)
130+
data_endpoint = (
131+
profile.get("data_endpoint") or _env("AGENTRUN_DATA_ENDPOINT") or None
132+
)
126133

127134
# Propagate resolved values to env vars so that SDK-internal Config()
128135
# instances (created without explicit config) also pick them up.

src/agentrun_cli/_utils/error.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@
1414

1515
import functools
1616
import sys
17-
from typing import Callable
17+
from collections.abc import Callable
1818

1919
import click
2020

2121
from agentrun_cli._utils.output import echo_error
2222

23-
2423
EXIT_SUCCESS = 0
2524
EXIT_NOT_FOUND = 1
2625
EXIT_BAD_INPUT = 2

src/agentrun_cli/_utils/inner_client.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
high-level SDK resource classes.
66
"""
77

8-
from typing import Optional
9-
108
from agentrun_cli._utils.config import build_sdk_config
119

1210

1311
def get_agentrun_client(
14-
profile_name: Optional[str] = None,
15-
region: Optional[str] = None,
12+
profile_name: str | None = None,
13+
region: str | None = None,
1614
):
1715
"""Build a low-level AgentRun API client from CLI context.
1816

src/agentrun_cli/_utils/output.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"""
99

1010
import json
11-
import sys
12-
from typing import Any, List, Optional, Sequence
11+
from collections.abc import Sequence
12+
from typing import Any
1313

1414
import click
1515

@@ -19,7 +19,7 @@ def echo_json(data: Any) -> None:
1919
click.echo(json.dumps(data, indent=2, ensure_ascii=False, default=str))
2020

2121

22-
def echo_table(rows: Sequence[dict], columns: Optional[List[str]] = None) -> None:
22+
def echo_table(rows: Sequence[dict], columns: list[str] | None = None) -> None:
2323
"""Render a list of dicts as a rich table.
2424
2525
Falls back to JSON if ``rich`` is unavailable.
@@ -44,7 +44,7 @@ def echo_table(rows: Sequence[dict], columns: Optional[List[str]] = None) -> Non
4444
Console().print(table)
4545

4646

47-
def echo_quiet(data: Any, field: Optional[str] = None) -> None:
47+
def echo_quiet(data: Any, field: str | None = None) -> None:
4848
"""Print only the most relevant value — useful for shell pipelines.
4949
5050
Heuristic for picking the value:
@@ -68,7 +68,9 @@ def echo_quiet(data: Any, field: Optional[str] = None) -> None:
6868
click.echo(str(data))
6969

7070

71-
def format_output(ctx: click.Context, data: Any, quiet_field: Optional[str] = None) -> None:
71+
def format_output(
72+
ctx: click.Context, data: Any, quiet_field: str | None = None
73+
) -> None:
7274
"""Route *data* to the appropriate formatter based on ``ctx.obj["output"]``."""
7375
fmt = (ctx.obj or {}).get("output", "json")
7476

@@ -92,7 +94,7 @@ def format_output(ctx: click.Context, data: Any, quiet_field: Optional[str] = No
9294
echo_json(data)
9395

9496

95-
def echo_error(error_type: str, message: str, hint: Optional[str] = None) -> None:
97+
def echo_error(error_type: str, message: str, hint: str | None = None) -> None:
9698
"""Write a structured JSON error to stderr.
9799
98100
When *hint* is provided it is included as a ``hint`` field in the JSON

src/agentrun_cli/_utils/super_agent_render.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import enum
1414
import json
1515
import sys
16-
from typing import Optional
1716

1817
import click
1918

@@ -24,9 +23,7 @@ class RenderMode(str, enum.Enum):
2423
TEXT_ONLY = "text-only"
2524

2625

27-
def pick_render_mode(
28-
*, is_tty: bool, raw: bool, text_only: bool
29-
) -> RenderMode:
26+
def pick_render_mode(*, is_tty: bool, raw: bool, text_only: bool) -> RenderMode:
3027
"""Resolve render mode from TTY + user flags.
3128
3229
Raises ValueError if --raw and --text-only are both set.
@@ -43,7 +40,7 @@ def pick_render_mode(
4340
_TOOL_RESULT_PREVIEW_LIMIT = 200
4441

4542

46-
def _event_name(event, payload: dict) -> Optional[str]:
43+
def _event_name(event, payload: dict) -> str | None:
4744
"""Resolve the AG-UI event type.
4845
4946
The real server streams SSE with an empty ``event:`` field and puts the
@@ -65,18 +62,14 @@ def __init__(
6562
self,
6663
mode: RenderMode,
6764
*,
68-
use_color: Optional[bool] = None,
65+
use_color: bool | None = None,
6966
stream=None,
7067
):
7168
self.mode = mode
72-
self._conversation_id: Optional[str] = None
69+
self._conversation_id: str | None = None
7370
self._out = stream if stream is not None else sys.stdout
7471
if use_color is None:
75-
use_color = (
76-
self._out.isatty()
77-
if hasattr(self._out, "isatty")
78-
else False
79-
)
72+
use_color = self._out.isatty() if hasattr(self._out, "isatty") else False
8073
self._use_color = use_color
8174

8275
def set_conversation_id(self, conv_id: str) -> None:
@@ -95,7 +88,7 @@ def finish(self) -> None:
9588
"""Called at stream end. Flushes anything buffered."""
9689
try:
9790
self._out.flush()
98-
except Exception:
91+
except Exception: # noqa: S110 — broken-pipe on flush is harmless
9992
pass
10093

10194
def finish_with_envelope(self) -> None:
@@ -110,7 +103,7 @@ def finish_with_envelope(self) -> None:
110103
self._out.write(json.dumps(envelope, ensure_ascii=False) + "\n")
111104
try:
112105
self._out.flush()
113-
except Exception:
106+
except Exception: # noqa: S110 — broken-pipe on flush is harmless
114107
pass
115108

116109
# ───────────────────────────────────────── internal renderers
@@ -164,9 +157,7 @@ def _render_pretty(self, event) -> None:
164157
msg = payload.get("message", "") if payload else ""
165158
self._write_error(f"✖ run error: {msg}\n")
166159
elif name == "RUN_FINISHED":
167-
self._out.write(
168-
"─────────────────────────────────────────────\n"
169-
)
160+
self._out.write("─────────────────────────────────────────────\n")
170161

171162
def _write_meta(self, text: str) -> None:
172163
if self._use_color:

0 commit comments

Comments
 (0)