Skip to content

Commit 7ff643a

Browse files
author
Dominikus Nold
committed
fix: address PR hardening annotations
1 parent c87d750 commit 7ff643a

16 files changed

Lines changed: 323 additions & 110 deletions

.github/workflows/pr-orchestrator.yml

Lines changed: 81 additions & 73 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ All notable changes to this project will be documented in this file.
1010

1111
---
1212

13+
## [0.47.8] - 2026-06-13
14+
15+
### Fixed
16+
17+
- **PR hardening follow-up**: address PR review annotations and failing CI by
18+
pinning orchestrator actions to immutable SHAs, preserving precise coverage
19+
threshold comparisons, making Semgrep SAST result parsing fail closed, and
20+
normalizing versioned dependency constraints before dedupe.
21+
22+
---
23+
1324
## [0.47.7] - 2026-06-12
1425

1526
### Fixed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "specfact-cli"
7-
version = "0.47.7"
7+
version = "0.47.8"
88
description = "AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, and spec/contract evidence for AI-assisted and brownfield delivery."
99
readme = "README.md"
1010
requires-python = ">=3.11"

scripts/check_version_sources.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _changed_files_vs_git_ref(root: Path, git_ref: str) -> list[str]:
8383
def _is_packaged_artifact(path_str: str) -> bool:
8484
"""True when staged paths imply a release/version bump must accompany the commit."""
8585
normalized = path_str.replace("\\", "/")
86-
if normalized in {"pyproject.toml", "setup.py"}:
86+
if normalized == "setup.py":
8787
return True
8888
if normalized.startswith("src/"):
8989
return True
@@ -93,6 +93,24 @@ def _is_packaged_artifact(path_str: str) -> bool:
9393
return normalized.startswith("resources/")
9494

9595

96+
def _candidate_requires_versioning(
97+
root: Path,
98+
candidate_files: set[str],
99+
compare_ref: str,
100+
current_version: str,
101+
) -> bool:
102+
"""Return whether changed files imply a package version/changelog bundle."""
103+
for path in candidate_files:
104+
normalized = path.replace("\\", "/")
105+
if normalized == "pyproject.toml":
106+
if _version_bumped_vs_git_ref(root, current_version, compare_ref):
107+
return True
108+
continue
109+
if _is_packaged_artifact(normalized):
110+
return True
111+
return False
112+
113+
96114
def _parse_semver(version: str) -> tuple[int, int, int] | None:
97115
match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", version.strip())
98116
if match is None:
@@ -255,9 +273,9 @@ def main(argv: list[str] | None = None) -> int:
255273
sys.stderr.write(_REMEDIATION)
256274
return 1
257275

258-
if any(_is_packaged_artifact(path) for path in candidate_files):
276+
compare_ref = changed_vs_ref if changed_vs_ref else "HEAD"
277+
if _candidate_requires_versioning(root, candidate_files, compare_ref, unique[0]):
259278
required_files = changed_files if changed_files else staged_files
260-
compare_ref = changed_vs_ref if changed_vs_ref else "HEAD"
261279
return _enforce_packaged_artifact_versioning(root, required_files, unique[0], compare_ref)
262280
return 0
263281

scripts/semgrep_sast_gate.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,37 @@
66
import argparse
77
import json
88
import sys
9+
from collections.abc import Mapping
910
from pathlib import Path
10-
from typing import Any
11+
from typing import Any, cast
12+
13+
from beartype import beartype
14+
from icontract import ensure
1115

1216

1317
FindingKey = tuple[str, str, int]
1418

1519

1620
def _finding_key(result: dict[str, Any]) -> FindingKey:
17-
check_id = str(result.get("check_id", ""))
18-
path = str(result.get("path", ""))
19-
start_raw = result.get("start", {})
20-
start_line = int(start_raw.get("line", 0)) if isinstance(start_raw, dict) else 0
21+
check_id = result.get("check_id")
22+
path = result.get("path")
23+
start = result.get("start")
24+
if not isinstance(check_id, str) or not check_id.strip():
25+
raise ValueError("Semgrep result missing non-empty check_id")
26+
if not isinstance(path, str) or not path.strip():
27+
raise ValueError("Semgrep result missing non-empty path")
28+
if not isinstance(start, dict):
29+
raise ValueError("Semgrep result missing integer start.line")
30+
start_line = cast(Mapping[str, object], start).get("line")
31+
if not isinstance(start_line, int):
32+
raise ValueError("Semgrep result missing integer start.line")
2133
return (check_id, path, start_line)
2234

2335

36+
def _emit_stdout(message: str) -> None:
37+
sys.stdout.write(f"{message}\n")
38+
39+
2440
def _load_json(path: Path) -> dict[str, Any]:
2541
try:
2642
payload = json.loads(path.read_text(encoding="utf-8"))
@@ -56,9 +72,20 @@ def _load_results(path: Path) -> list[dict[str, Any]]:
5672
raw_results = payload.get("results")
5773
if not isinstance(raw_results, list):
5874
raise SystemExit(f"::error::{path} missing Semgrep results list")
59-
return [result for result in raw_results if isinstance(result, dict)]
60-
61-
75+
results: list[dict[str, Any]] = []
76+
for index, result in enumerate(raw_results):
77+
if not isinstance(result, dict):
78+
raise SystemExit(f"::error::{path} results[{index}] must be an object")
79+
try:
80+
_finding_key(result)
81+
except ValueError as exc:
82+
raise SystemExit(f"::error::{path} results[{index}] is malformed: {exc}") from exc
83+
results.append(result)
84+
return results
85+
86+
87+
@beartype
88+
@ensure(lambda result: result in {0, 1}, "exit code must be 0 or 1")
6289
def main() -> int:
6390
parser = argparse.ArgumentParser(description=__doc__)
6491
parser.add_argument("--results", type=Path, required=True, help="Semgrep JSON results file")
@@ -71,15 +98,15 @@ def main() -> int:
7198
new_findings = sorted(current - baseline)
7299
resolved_findings = sorted(baseline - current)
73100

74-
print(f"Semgrep SAST findings: {len(current)} current, {len(baseline)} accepted baseline")
101+
_emit_stdout(f"Semgrep SAST findings: {len(current)} current, {len(baseline)} accepted baseline")
75102
if resolved_findings:
76-
print(f"Semgrep SAST baseline can shrink by {len(resolved_findings)} finding(s)")
103+
_emit_stdout(f"Semgrep SAST baseline can shrink by {len(resolved_findings)} finding(s)")
77104
if not new_findings:
78-
print("Semgrep SAST gate passed: no new findings outside baseline")
105+
_emit_stdout("Semgrep SAST gate passed: no new findings outside baseline")
79106
return 0
80107

81108
for check_id, path, line in new_findings:
82-
print(f"::error file={path},line={line}::New Semgrep SAST finding: {check_id}")
109+
_emit_stdout(f"::error file={path},line={line}::New Semgrep SAST finding: {check_id}")
83110
return 1
84111

85112

scripts/sign-module.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,11 @@ if changed or untracked:
168168
PY
169169
fi
170170

171-
python3 scripts/sign-modules.py "${ARGS[@]}" "$MANIFEST"
171+
if [[ "${#ARGS[@]}" -gt 0 ]]; then
172+
python3 scripts/sign-modules.py "${ARGS[@]}" "$MANIFEST"
173+
else
174+
python3 scripts/sign-modules.py "$MANIFEST"
175+
fi
172176

173177
# Emit checksum line for legacy pipeline compatibility.
174178
python3 - "$MANIFEST" <<'PY'

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
if __name__ == "__main__":
88
_setup = setup(
99
name="specfact-cli",
10-
version="0.47.7",
10+
version="0.47.8",
1111
description=(
1212
"AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, "
1313
"and spec/contract evidence for AI-assisted and brownfield delivery."

src/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
"""
44

55
# Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py
6-
__version__ = "0.47.7"
6+
__version__ = "0.47.8"

src/specfact_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,6 @@ def _install_progressive_disclosure() -> None:
7676
# keeps missing-command and missing-parameter UX consistent outside the root CLI too.
7777
_install_progressive_disclosure()
7878

79-
__version__ = "0.47.7"
79+
__version__ = "0.47.8"
8080

8181
__all__ = ["__version__"]

src/specfact_cli/registry/dependency_resolver.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ def _collect_constraints(modules: list[ModulePackageMetadata]) -> list[str]:
132132
constraints.append(normalized)
133133
seen.add(normalized)
134134
for vd in meta.pip_dependencies_versioned or []:
135-
spec = vd.version_specifier or ""
136-
s = f"{vd.name}{spec}" if spec else vd.name
135+
name = vd.name.strip()
136+
spec = (vd.version_specifier or "").strip()
137+
s = f"{name}{spec}" if spec else name
137138
normalized = s.strip()
138139
if normalized and normalized not in seen:
139140
constraints.append(normalized)

0 commit comments

Comments
 (0)