Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4f8c786
fix(coverage): scope Rust evidence to changed packages
seonghobae Aug 21, 2026
e2d7c14
docs(changelog): record scoped Rust coverage
seonghobae Aug 21, 2026
9cb5a40
test(opencode): refresh review workflow snapshot
seonghobae Aug 21, 2026
8ec2c9f
fix(coverage): discover nested Rust manifests
seonghobae Aug 21, 2026
6aa186b
test(coverage): execute Rust manifest selection contract
seonghobae Aug 21, 2026
92810bf
test(coverage): fail on deleted root Rust lockfile
seonghobae Aug 21, 2026
94ab7c0
fix(coverage): keep deleted root lockfile workspace-wide
seonghobae Aug 21, 2026
91c16eb
test(coverage): refresh exact review workflow pin
seonghobae Aug 21, 2026
e960321
test(ci): keep coverage contracts composable
seonghobae Aug 21, 2026
c267298
Merge branch 'main' into fix/rust-coverage-package-scope
opencode-agent[bot] Aug 21, 2026
80d13a1
fix(coverage): inherit Rust workspace baseline for crates
seonghobae Aug 21, 2026
3ea59f4
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 21, 2026
db1802f
fix(coverage): normalize root Rust workspace manifest
seonghobae Aug 21, 2026
2a5ab45
fix(coverage): avoid duplicate Rust workspace runs
seonghobae Aug 21, 2026
92c4afc
Merge branch 'main' into fix/rust-coverage-package-scope
opencode-agent[bot] Aug 21, 2026
7d66872
Merge branch 'main' into fix/rust-coverage-package-scope
opencode-agent[bot] Aug 21, 2026
fae577e
Merge branch 'main' into fix/rust-coverage-package-scope
opencode-agent[bot] Aug 21, 2026
f71db4b
fix(ci): refresh audit lock and scheduler assertion
seonghobae Aug 21, 2026
be6534b
test(ci): restore complete docstring coverage
seonghobae Aug 21, 2026
0a88e24
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 21, 2026
471f34d
fix(review): close coverage and credential contract gaps
seonghobae Aug 29, 2026
5772721
fix(review): trigger coverage for deleted manifests
seonghobae Aug 29, 2026
1db47e2
Merge main into fix/rust-coverage-package-scope, updating stale
claude Aug 30, 2026
923fa07
Merge remote-tracking branch 'origin/main' into fix/rust-coverage-pac…
claude Aug 30, 2026
047ad58
fix(rust-coverage): include rename endpoints; enforce workspace bound…
claude Aug 30, 2026
2b1abb0
Merge remote-tracking branch 'origin/main' into work-pr1187
claude Sep 4, 2026
3add872
fix(tests): sync approved_gh_token_assignments with merged workflow
claude Sep 4, 2026
d359776
Merge origin/main; recompute the review-dispatch blob pin
claude Sep 5, 2026
72a9d72
Merge origin/main; recompute the review-dispatch blob pin a second time
claude Sep 5, 2026
3da6596
Merge origin/main into fix/rust-coverage-package-scope
claude Sep 6, 2026
541cadd
Merge origin/main into fix/rust-coverage-package-scope
Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions .github/workflows/opencode-review-dispatch.yml
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -1199,7 +1199,18 @@ jobs:
if [ -n "${PR_BASE_SHA:-}" ] && [ -n "${PR_HEAD_SHA:-}" ] \
&& trusted_git rev-parse --verify --quiet "$PR_BASE_SHA^{commit}" >/dev/null \
&& trusted_git rev-parse --verify --quiet "$PR_HEAD_SHA^{commit}" >/dev/null; then
trusted_git diff --name-only --find-renames "$PR_BASE_SHA" "$PR_HEAD_SHA"
# --name-status (not --name-only) so a detected rename/copy
# surfaces BOTH its old and new path. --name-only collapses a
# rename to a single line naming only the destination, so a
# Rust source file renamed away to a non-Rust extension (or
# moved into a different Cargo package) would otherwise vanish
# from every consumer's changed-file inventory on the origin
# side -- has_changed_rust_files would miss it entirely, and
# rust_coverage_manifests would credit only the destination
# package. R/C status lines carry a third tab-separated field
# (the new path); every other status carries exactly the path.
trusted_git diff --name-status --find-renames "$PR_BASE_SHA" "$PR_HEAD_SHA" |
awk -F'\t' 'NF >= 3 { print $2; print $3; next } { print $2 }'
Comment on lines +1212 to +1213

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Shared inventory broadens other selectors

changed_files_for_coverage also feeds JavaScript discovery and implementation scanning. Add regression coverage for renamed and deleted non-Rust origins.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1212 to +1213

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Quoted paths remain unsupported

Git still C-quotes names containing tabs, newlines, quotes, or backslashes. Coverage selectors cannot map those legal paths to repository files.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

else
trusted_git ls-files
fi
Expand All @@ -1218,6 +1229,11 @@ jobs:
return "$rc"
}

has_changed_rust_files() {
changed_files_for_coverage |
awk '$0 ~ /(^|\/)Cargo\.(toml|lock)$/ || $0 ~ /\.rs$/ { found=1 } END { exit found ? 0 : 1 }'
Comment on lines +1232 to +1234

@devin-ai-integration devin-ai-integration Bot Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Deleted Cargo paths remain covered

has_changed_rust_files retains deleted manifests and lockfiles. Nested deletions resolve upward, while deleted root lockfiles still select full-workspace coverage.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}

tracked_python_projects_with_tests() {
trusted_git ls-files 'pyproject.toml' '*/pyproject.toml' 'requirements.txt' '*/requirements.txt' \
| while IFS= read -r pyproject_file; do
Expand Down Expand Up @@ -2000,30 +2016,49 @@ jobs:
}

rust_coverage_manifests() {
if [ -f Cargo.toml ]; then
# A repository-wide coverage run is required when the workspace
# manifest or lockfile changed. Otherwise measure only the
# changed Rust package(s); building every workspace member at once
# can exhaust the review runner disk before tests begin.
if changed_files_for_coverage \
| awk '$0 == "Cargo.toml" || $0 == "Cargo.lock" { found=1 } END { exit found ? 0 : 1 }'; then
printf '%s\n' Cargo.toml
return 0
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
fi
changed_files_for_coverage \
local manifests
manifests="$(changed_files_for_coverage \
| while IFS= read -r changed_path; do
case "$changed_path" in
Cargo.toml|Cargo.lock|*.rs) ;;
Cargo.toml|Cargo.lock|*/Cargo.toml|*/Cargo.lock|*.rs) ;;
*) continue ;;
esac
candidate_dir="$(dirname "$changed_path")"
while [ "$candidate_dir" != "." ] && [ "$candidate_dir" != "/" ]; do
while :; do
if [ -f "${candidate_dir}/Cargo.toml" ]; then
printf '%s\n' "${candidate_dir}/Cargo.toml"
if [ "$candidate_dir" = "." ]; then
printf '%s\n' Cargo.toml
else
printf '%s\n' "${candidate_dir}/Cargo.toml"
fi
break
Comment thread
seonghobae marked this conversation as resolved.
fi
[ "$candidate_dir" = "." ] && break
Comment thread
seonghobae marked this conversation as resolved.
next_dir="$(dirname "$candidate_dir")"
if [ "$next_dir" = "$candidate_dir" ]; then
break
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
fi
candidate_dir="$next_dir"
done
done \
| sort -u
| sort -u)"
# A root workspace run already covers every member; do not repeat
# nested package runs when root and member sources changed together.
if printf '%s\n' "$manifests" \
| awk '$0 == "Cargo.toml" { found=1 } END { exit found ? 0 : 1 }'; then
printf '%s\n' Cargo.toml
elif [ -n "$manifests" ]; then
printf '%s\n' "$manifests"
fi
Comment thread
seonghobae marked this conversation as resolved.
}

rust_coverage_fail_under_lines() {
Expand Down Expand Up @@ -2241,7 +2276,7 @@ jobs:
run_r_test_coverage
fi

if has_changed_tracked_files 'Cargo.toml' 'Cargo.lock' '*.rs'; then
if has_changed_rust_files; then
measured_any=1
run_rust_test_coverage
fi
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,10 @@ Semantic Versioning where the repository publishes a release.

### Fixed

- Scope OpenCode Rust coverage evidence to changed Cargo packages while
retaining full-workspace coverage for root workspace and lockfile changes,
preventing large repositories from exhausting the review runner before
coverage starts.
- Prefer the job-scoped `github.token` when the central OpenCode dispatch
publishes a commit status back to the same `.github` repository. The job's
declared `statuses: write` permission now reaches the endpoint instead of an
Expand Down
84 changes: 80 additions & 4 deletions scripts/ci/rust_coverage_threshold.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import argparse
import fnmatch
import tomllib
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -36,18 +37,93 @@ def resolve_minimum_lines(document: dict[str, Any]) -> float | None:
break
if selected_path is None:
return None
return _validate_minimum_lines(selected_path, value)


def _validate_minimum_lines(path: str, value: Any) -> float:
"""Validate one repository-owned coverage baseline and normalize it."""
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise ValueError(f"{selected_path} must be a number from 0 to 100")
raise ValueError(f"{path} must be a number from 0 to 100")
threshold = float(value)
if not 0 <= threshold <= 100:
raise ValueError(f"{selected_path} must be between 0 and 100")
raise ValueError(f"{path} must be between 0 and 100")
return threshold


def _relative_posix_path(path: Path, base: Path) -> str | None:
"""Return ``path`` relative to ``base`` as a POSIX string, or None if unrelated."""
try:
return path.relative_to(base).as_posix()
except ValueError:
return None


def _workspace_excludes_package(
workspace_dir: Path, package_dir: Path, workspace_document: dict[str, Any]
) -> bool:
"""Return whether a workspace's ``exclude`` patterns cover the package directory.

Cargo's own automatic workspace-root discovery walks upward from a
package's manifest and skips an ancestor workspace that excludes it,
continuing the search further out rather than treating the excluded
workspace as authoritative. Mirroring that here keeps an excluded (or
otherwise independent) package from inheriting a coverage baseline that
was never configured for it.
"""
workspace_table = workspace_document.get("workspace")
if not isinstance(workspace_table, dict):
return False
excludes = workspace_table.get("exclude")
if not isinstance(excludes, list):
return False
relative = _relative_posix_path(package_dir, workspace_dir)
if relative is None:
return False
for pattern in excludes:
if not isinstance(pattern, str):
continue
normalized_pattern = pattern.rstrip("/")
if relative == normalized_pattern or fnmatch.fnmatch(relative, normalized_pattern):
return True
if relative.startswith(f"{normalized_pattern}/"):
return True
Comment on lines +85 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Exclusion matching approximates Cargo

fnmatch.fnmatch differs from Cargo's glob semantics, especially across path separators. Complex workspace exclusions need parity tests or Cargo-derived membership.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

return False


def read_minimum_lines(manifest: Path) -> float | None:
"""Read and resolve one Cargo manifest's line-coverage baseline."""
"""Read a package baseline, falling back to its nearest workspace baseline."""
manifest = manifest.resolve()
document = tomllib.loads(manifest.read_text(encoding="utf-8"))
return resolve_minimum_lines(document)
threshold = resolve_minimum_lines(document)
if threshold is not None:
return threshold
Comment on lines +95 to +99

@devin-ai-integration devin-ai-integration Bot Aug 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Package thresholds retain precedence

Local package metadata wins before ancestor lookup. Explicit zero remains valid, and malformed inherited values fail instead of reverting to the central default.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


package_dir = manifest.parent
for parent in manifest.parents:
workspace_manifest = parent / "Cargo.toml"
if not workspace_manifest.is_file():
continue
workspace_document = tomllib.loads(workspace_manifest.read_text(encoding="utf-8"))
if "workspace" not in workspace_document:
continue
if _workspace_excludes_package(parent, package_dir, workspace_document):
# This ancestor's workspace explicitly excludes the package, so
# it is not this package's workspace root. Keep walking further
# out for an unrelated ancestor workspace that might still
# legitimately claim it, matching Cargo's own root-discovery
# rule for excluded members.
continue
# This is the package's actual (nearest, non-excluding) Cargo
# workspace root -- whether the sole enclosing workspace or a
# nested, independent one. Stop here even when it configures no
# baseline: crossing this boundary to search an even-more-outer,
# unrelated workspace would attribute a threshold that was never
# configured for this package's own workspace.
workspace_value = _nested_value(workspace_document, METADATA_PATHS[1])
if workspace_value is not None:
return _validate_minimum_lines(METADATA_PATHS[1], workspace_value)
Comment on lines +102 to +124

@devin-ai-integration devin-ai-integration Bot Aug 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Explicit workspace baselines are ignored

Without local metadata, read_minimum_lines ignores the explicit package.workspace target and searches ancestors. Out-of-tree members receive 100% or an unrelated baseline.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

return None
return None
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.


def main() -> int:
Expand Down
Loading
Loading