Skip to content

Commit 3ac8527

Browse files
authored
Merge pull request #25 from mtrdesign/feature/ci-code-checks
ci: add ci-code-checks workflow (ruff + pytest) and tidy job names
2 parents f0030f5 + f6dc347 commit 3ac8527

53 files changed

Lines changed: 448 additions & 434 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,22 @@
1-
name: Publish WhyGraph Docker image
1+
name: cd-deploy-whygraph
22

33
# Builds and pushes ghcr.io/mtrdesign/whygraph — the self-contained
44
# scanning-service image the `whygraph` host shim (scripts/install.sh)
5-
# runs. Triggers:
6-
# - any push to main that touches the image, the package source, or this workflow
7-
# - manual workflow_dispatch (with optional pinned codegraph version + release tag)
5+
# runs. Publishing is tied to releases: cutting a GitHub release (tagged
6+
# vX.Y.Z) builds the image and tags it to match that version, so the image
7+
# tag follows the release version 1:1.
88

99
on:
10-
push:
11-
branches: [main]
12-
paths:
13-
- 'docker/whygraph/**'
14-
- 'src/whygraph/**'
15-
- 'pyproject.toml'
16-
- '.github/workflows/publish-whygraph-image.yml'
17-
workflow_dispatch:
18-
inputs:
19-
codegraph_version:
20-
description: 'npm version of @colbymchenry/codegraph to bake in (e.g. 1.2.3 or "latest")'
21-
required: false
22-
default: 'latest'
23-
release_tag:
24-
description: 'Optional human-readable release tag (e.g. v1.2.3) — published alongside :latest and :sha-XXXX'
25-
required: false
26-
default: ''
10+
release:
11+
types: [published]
2712

2813
permissions:
2914
contents: read
3015
packages: write
3116

3217
jobs:
3318
build-and-push:
19+
name: Build and push image
3420
runs-on: ubuntu-latest
3521
steps:
3622
- name: Checkout
@@ -49,15 +35,20 @@ jobs:
4935
username: ${{ github.actor }}
5036
password: ${{ secrets.GITHUB_TOKEN }}
5137

38+
# The release tag drives the image tags. A release `v1.2.3` publishes
39+
# :1.2.3, :1.2, :1 and :latest, so consumers can pin as loosely or as
40+
# tightly as they like while the shim's default :latest tracks the
41+
# newest release.
5242
- name: Compute image tags
5343
id: meta
5444
uses: docker/metadata-action@v5
5545
with:
5646
images: ghcr.io/mtrdesign/whygraph
5747
tags: |
58-
type=raw,value=latest,enable={{is_default_branch}}
59-
type=sha,prefix=sha-
60-
type=raw,value=${{ github.event.inputs.release_tag }},enable=${{ github.event.inputs.release_tag != '' }}
48+
type=semver,pattern={{version}}
49+
type=semver,pattern={{major}}.{{minor}}
50+
type=semver,pattern={{major}}
51+
type=raw,value=latest
6152
6253
- name: Build and push
6354
uses: docker/build-push-action@v6
@@ -69,6 +60,6 @@ jobs:
6960
tags: ${{ steps.meta.outputs.tags }}
7061
labels: ${{ steps.meta.outputs.labels }}
7162
build-args: |
72-
CODEGRAPH_VERSION=${{ github.event.inputs.codegraph_version || 'latest' }}
63+
CODEGRAPH_VERSION=latest
7364
cache-from: type=gha
7465
cache-to: type=gha,mode=max
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: ci-code-checks
2+
3+
# Runs on every pull request: lints with ruff (lint + format check) and runs
4+
# the pytest suite. Both jobs run in parallel and gate the PR.
5+
6+
on:
7+
pull_request:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
lint:
14+
name: Lint (ruff)
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Set up uv
21+
uses: astral-sh/setup-uv@v5
22+
with:
23+
enable-cache: true
24+
25+
- name: Install dependencies
26+
run: uv sync --dev
27+
28+
- name: Ruff lint
29+
run: uv run ruff check src/ tests/
30+
31+
- name: Ruff format check
32+
run: uv run ruff format --check src/ tests/
33+
34+
test:
35+
name: Unit tests (pytest)
36+
runs-on: ubuntu-latest
37+
steps:
38+
- name: Checkout
39+
uses: actions/checkout@v4
40+
41+
- name: Set up uv
42+
uses: astral-sh/setup-uv@v5
43+
with:
44+
enable-cache: true
45+
46+
- name: Install dependencies
47+
run: uv sync --dev
48+
49+
- name: Run pytest
50+
run: uv run pytest

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,9 @@ packages = ["src/whygraph"]
3030
[tool.pytest.ini_options]
3131
testpaths = ["tests"]
3232

33+
[tool.ruff]
34+
target-version = "py312"
35+
line-length = 88
36+
3337
[dependency-groups]
34-
dev = ["pytest>=8"]
38+
dev = ["pytest>=8", "ruff>=0.8"]

src/whygraph/analyze/llm_descriptor.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,12 @@ def __init__(
125125
self._describe_prompt = (
126126
describe_prompt
127127
if describe_prompt is not None
128-
else resolve(
129-
_PROMPT_COMPONENT, "describe", client.provider, client.model
130-
)
128+
else resolve(_PROMPT_COMPONENT, "describe", client.provider, client.model)
131129
)
132130
self._synthesis_prompt = (
133131
synthesis_prompt
134132
if synthesis_prompt is not None
135-
else resolve(
136-
_PROMPT_COMPONENT, "synthesis", client.provider, client.model
137-
)
133+
else resolve(_PROMPT_COMPONENT, "synthesis", client.provider, client.model)
138134
)
139135

140136
def __repr__(self) -> str:

src/whygraph/analyze/rationale_generator.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,7 @@ def _format_evidence(evidence: Sequence[CommitEvidence]) -> str:
139139
n_prs = sum(len(item.pull_requests) for item in evidence)
140140
n_issues = sum(len(item.issues) for item in evidence)
141141
blocks = [
142-
f"Evidence: {len(evidence)} commit(s), {n_prs} PR(s), "
143-
f"{n_issues} issue(s)."
142+
f"Evidence: {len(evidence)} commit(s), {n_prs} PR(s), {n_issues} issue(s)."
144143
]
145144
for item in evidence:
146145
lines = _format_commit(item.commit)
@@ -224,8 +223,7 @@ def _format_symbol_context(context: SymbolContext) -> str:
224223
lines.append("")
225224
lines.extend(
226225
_format_relations(
227-
f"Called by ({len(context.callers)} caller(s) — "
228-
"blast radius of a change):",
226+
f"Called by ({len(context.callers)} caller(s) — blast radius of a change):",
229227
context.callers,
230228
)
231229
)
@@ -305,9 +303,7 @@ def _parse_rationale_json(text: str) -> dict:
305303
if not isinstance(value, list) or not all(
306304
isinstance(item, str) for item in value
307305
):
308-
raise RationaleError(
309-
f"rationale key {key!r} must be a list of strings"
310-
)
306+
raise RationaleError(f"rationale key {key!r} must be a list of strings")
311307
return parsed
312308

313309

@@ -355,9 +351,7 @@ def __init__(
355351
self._rationale_prompt = (
356352
rationale_prompt
357353
if rationale_prompt is not None
358-
else resolve(
359-
_PROMPT_COMPONENT, "rationale", client.provider, client.model
360-
)
354+
else resolve(_PROMPT_COMPONENT, "rationale", client.provider, client.model)
361355
)
362356

363357
def __repr__(self) -> str:

src/whygraph/assets.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,7 @@ def packaged_assets_for(target: AgentTarget) -> Traversable:
110110
"""
111111
if target.assets_subdir is None:
112112
raise ValueError(
113-
f"agent {target.name!r} has no bundled assets "
114-
"(assets_subdir is None)"
113+
f"agent {target.name!r} has no bundled assets (assets_subdir is None)"
115114
)
116115
return resources.files("whygraph") / "assets" / target.assets_subdir
117116

@@ -167,17 +166,19 @@ def install_assets(
167166
If ``target`` has no bundled assets configured.
168167
"""
169168
if not target.has_assets:
170-
raise ValueError(
171-
f"agent {target.name!r} has no bundled assets to install"
172-
)
169+
raise ValueError(f"agent {target.name!r} has no bundled assets to install")
173170
src: Traversable | Path = (
174171
source if source is not None else packaged_assets_for(target)
175172
)
176-
assert target.assets_dest is not None # for type checkers; has_assets guarantees this
173+
assert (
174+
target.assets_dest is not None
175+
) # for type checkers; has_assets guarantees this
177176
dest_root = project_root.joinpath(*target.assets_dest)
178177
merge_set = frozenset(target.assets_merge_files)
179178
result = InstallResult()
180-
_copy_tree(src, dest_root, rel_prefix=(), merge_set=merge_set, force=force, result=result)
179+
_copy_tree(
180+
src, dest_root, rel_prefix=(), merge_set=merge_set, force=force, result=result
181+
)
181182
return result
182183

183184

@@ -282,7 +283,11 @@ def _merge_block(
282283

283284
# No markers — append the block after the existing content,
284285
# ensuring exactly one blank line of separation.
285-
separator = "" if existing.endswith("\n\n") else ("\n" if existing.endswith("\n") else "\n\n")
286+
separator = (
287+
""
288+
if existing.endswith("\n\n")
289+
else ("\n" if existing.endswith("\n") else "\n\n")
290+
)
286291
dest_path.write_text(existing + separator + block, encoding="utf-8")
287292
result.overwritten.append(dest_path)
288293

src/whygraph/cli/commands/hooks.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ def uninstall_cmd() -> None:
145145
removed_any = True
146146

147147
console.print(
148-
"Auto-rescan hooks uninstalled." if removed_any else "No WhyGraph hooks were installed."
148+
"Auto-rescan hooks uninstalled."
149+
if removed_any
150+
else "No WhyGraph hooks were installed."
149151
)
150152

151153

@@ -163,7 +165,9 @@ def status_cmd() -> None:
163165

164166
console.print(f"Hooks dir: {hooks_dir}")
165167
helper = project_root / HELPER_RELPATH
166-
console.print(f"Helper: {'present' if helper.exists() else 'missing'} ({helper})")
168+
console.print(
169+
f"Helper: {'present' if helper.exists() else 'missing'} ({helper})"
170+
)
167171
for name in HOOK_NAMES:
168172
hp = hooks_dir / name
169173
if hp.exists() and SENTINEL in hp.read_text():

src/whygraph/cli/commands/scan.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,7 @@ def _refresh_codegraph(project_root: Path, *, image: str | None) -> None:
221221
try:
222222
refresh_codegraph_index(project_root, image=image)
223223
except CodeGraphBootstrapError as exc:
224-
console.print(
225-
Text(f"CodeGraph refresh skipped — {exc}", style="yellow")
226-
)
224+
console.print(Text(f"CodeGraph refresh skipped — {exc}", style="yellow"))
227225

228226

229227
def _select_github_client(
@@ -306,7 +304,10 @@ def _render_scan_panel(
306304
]
307305
if github_client is None:
308306
rows.append(
309-
("GitHub", Text(_github_skip_reason(config, remote_enabled), style="yellow"))
307+
(
308+
"GitHub",
309+
Text(_github_skip_reason(config, remote_enabled), style="yellow"),
310+
)
310311
)
311312
else:
312313
rows.append(

src/whygraph/cli/preflight.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,7 @@ def run_preflight(project_root: Path) -> None:
8787
if c.status == "missing" and c.hint:
8888
console.print(f" install: {c.hint}")
8989

90-
hard_missing = [
91-
c.name for c in checks if c.status == "missing" and not c.soft
92-
]
90+
hard_missing = [c.name for c in checks if c.status == "missing" and not c.soft]
9391
if hard_missing:
9492
names = ", ".join(hard_missing)
9593
raise PreflightError(

src/whygraph/core/config.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,9 @@ def __post_init__(self) -> None:
197197
try:
198198
LogLevel[self.level.upper()]
199199
except KeyError as exc:
200-
raise ConfigError(
201-
f"invalid logging.level: {self.level!r}"
202-
) from exc
200+
raise ConfigError(f"invalid logging.level: {self.level!r}") from exc
203201
if self.max_bytes < 1:
204-
raise ConfigError(
205-
f"logging.max_bytes must be >= 1, got {self.max_bytes}"
206-
)
202+
raise ConfigError(f"logging.max_bytes must be >= 1, got {self.max_bytes}")
207203
if self.backup_count < 0:
208204
raise ConfigError(
209205
f"logging.backup_count must be >= 0, got {self.backup_count}"

0 commit comments

Comments
 (0)