Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 51 additions & 2 deletions repo_policy_sync/src/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ def create_pull_request(
changes: tuple[Change, ...],
head_oid: str,
draft: bool = False,
tool_revision: str,
) -> PullRequest:
self._ensure_automation_labels(repository=repository)
create_command = [
Expand All @@ -528,7 +529,12 @@ def create_pull_request(
"--title",
policy.title,
"--body",
_pull_request_body(policy, changes, head_oid=head_oid),
_pull_request_body(
policy,
changes,
head_oid=head_oid,
tool_revision=tool_revision,
),
]
if draft:
create_command.insert(3, "--draft")
Expand Down Expand Up @@ -642,6 +648,7 @@ def update_pull_request(
changes: tuple[Change, ...],
head_oid: str,
failure: str | None = None,
tool_revision: str,
) -> None:
"""Keep an existing policy-owned pull request's explanation current."""

Expand All @@ -658,7 +665,7 @@ def update_pull_request(
"-f",
f"title={policy.title}",
"-f",
f"body={_pull_request_body(policy, changes, head_oid=head_oid, failure=failure)}",
f"body={_pull_request_body(policy, changes, head_oid=head_oid, failure=failure, tool_revision=tool_revision)}",
]
)

Expand Down Expand Up @@ -785,11 +792,52 @@ def _pull_request_number(url: str) -> int:
return int(match.group(1))


def _tool_revision() -> str:
"""Return the current checkout's short commit hash and dirty marker."""

try:
revision_result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
check=True,
capture_output=True,
text=True,
)
except FileNotFoundError as exc:
raise CommandError("required command is unavailable: git") from exc
except subprocess.CalledProcessError as exc:
detail = exc.stderr.strip() or exc.stdout.strip() or "command failed"
raise CommandError(f"git rev-parse --short HEAD: {detail}") from exc

revision = revision_result.stdout.strip()
if not revision:
raise CommandError("git rev-parse --short HEAD returned no commit hash")

try:
dirty_result = subprocess.run(
["git", "diff-index", "--quiet", "HEAD", "--"],
check=False,
capture_output=True,
text=True,
)
except FileNotFoundError as exc: # pragma: no cover - guarded by rev-parse
raise CommandError("required command is unavailable: git") from exc

if dirty_result.returncode not in (0, 1):
detail = (
dirty_result.stderr.strip()
or dirty_result.stdout.strip()
or "command failed"
)
raise CommandError(f"git diff-index --quiet HEAD --: {detail}")
return f"{revision}-dirty" if dirty_result.returncode == 1 else revision


def _pull_request_body(
policy: Policy,
changes: tuple[Change, ...],
*,
head_oid: str,
tool_revision: str,
failure: str | None = None,
) -> str:
"""Build the concise, policy-centred pull-request template."""
Expand All @@ -815,6 +863,7 @@ def _pull_request_body(
"policy_description": description,
"policy_trigger": _policy_trigger(policy, changes),
"changes": change_lines,
"tool_revision": tool_revision,
"failure_section": _failure_section(failure),
}
for key, value in values.items():
Expand Down
40 changes: 39 additions & 1 deletion repo_policy_sync/src/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
PolicyPullRequestStatus,
TOOL_SLUG,
_pull_request_body,
_tool_revision,
policy_branches,
)
from .models import Change, Policy, Repository
Expand Down Expand Up @@ -107,6 +108,7 @@ def create_pull_request(
changes: tuple[Change, ...],
head_oid: str,
draft: bool = False,
tool_revision: str,
) -> object: ...

def update_pull_request(
Expand All @@ -118,6 +120,7 @@ def update_pull_request(
changes: tuple[Change, ...],
head_oid: str,
failure: str | None = None,
tool_revision: str,
) -> None: ...

def close_pull_request(self, *, repository: str, pull_request: object) -> None: ...
Expand Down Expand Up @@ -187,6 +190,7 @@ def run_policies(
policy_workers: int = DEFAULT_POLICY_WORKERS,
progress: Callable[[str], None] | None = None,
include_pull_request_status: bool = False,
tool_revision: str | None = None,
) -> RunReport:
"""Synchronize repositories, then process each policy across repositories in parallel.

Expand All @@ -198,6 +202,10 @@ def run_policies(
raise RepoPolicySyncError("policy worker count must be at least 1")
if recreate and not apply:
raise RepoPolicySyncError("--recreate requires apply mode")
if apply and tool_revision is None:
# Resolve provenance before repository synchronization or any policy
# branch can be changed remotely by the apply workflow.
tool_revision = _tool_revision()
started = monotonic()
report_progress = progress or _write_progress
try:
Expand Down Expand Up @@ -251,6 +259,7 @@ def run_policies(
workers=policy_workers,
progress=report_progress,
include_pull_request_status=include_pull_request_status,
tool_revision=tool_revision,
)
outcomes.extend(policy_outcomes)
for outcome in policy_outcomes:
Expand Down Expand Up @@ -324,6 +333,7 @@ def _run_policy_across_repositories(
workers: int,
progress: Callable[[str], None],
include_pull_request_status: bool,
tool_revision: str | None,
) -> tuple[RepositoryOutcome, ...]:
"""Evaluate or apply one policy in independent repository checkouts concurrently."""

Expand Down Expand Up @@ -358,6 +368,7 @@ def _run_policy_across_repositories(
recreate=recreate,
allow_dirty_pr=allow_dirty_pr,
include_pull_request_status=include_pull_request_status,
tool_revision=tool_revision,
)
] = (index, repository)
for completed, future in enumerate(as_completed(futures), start=1):
Expand Down Expand Up @@ -390,6 +401,7 @@ def _run_policy_in_repository(
recreate: bool,
allow_dirty_pr: bool,
include_pull_request_status: bool,
tool_revision: str | None,
) -> RepositoryOutcome:
restore_synced_default_branch(checkout=checkout)
if (
Expand All @@ -407,6 +419,7 @@ def _run_policy_in_repository(
recreate=recreate,
allow_dirty_pr=allow_dirty_pr,
include_pull_request_status=include_pull_request_status,
tool_revision=tool_revision,
)


Expand All @@ -422,7 +435,12 @@ def _run_repository(
recreate: bool = False,
allow_dirty_pr: bool = False,
include_pull_request_status: bool = False,
tool_revision: str | None = None,
) -> RepositoryOutcome:
if apply and tool_revision is None:
# Keep direct private callers safe as well as the organization-level
# entry point: provenance must be known before branch mutation.
tool_revision = _tool_revision()
full_name = f"{org}/{repository}"
policy_pr_status = (
_find_policy_pull_request_status(
Expand Down Expand Up @@ -461,6 +479,7 @@ def _run_repository(
changes=(),
head_oid=existing_pr.expected_head_oid,
failure=str(exc),
tool_revision=tool_revision,
)
client.close_pull_request(
repository=full_name, pull_request=existing_pr
Expand Down Expand Up @@ -508,6 +527,7 @@ def _run_repository(
policy=policy,
checkout=checkout,
allow_dirty_pr=allow_dirty_pr,
tool_revision=tool_revision,
)
if not evaluation.changes:
existing_pr = (
Expand Down Expand Up @@ -615,6 +635,7 @@ def _run_repository(
changes=evaluation.changes,
head_oid=existing_pr.expected_head_oid,
failure=str(exc),
tool_revision=tool_revision,
)
client.close_pull_request(
repository=full_name, pull_request=existing_pr
Expand All @@ -637,19 +658,22 @@ def _run_repository(
existing_pr=existing_pr,
changes=evaluation.changes,
allow_dirty_pr=allow_dirty_pr,
tool_revision=tool_revision,
)
if _pull_request_body_changed(
existing_pr,
policy=policy,
changes=evaluation.changes,
head_oid=existing_pr.expected_head_oid,
tool_revision=tool_revision,
):
client.update_pull_request(
repository=full_name,
pull_request=existing_pr,
policy=policy,
changes=evaluation.changes,
head_oid=existing_pr.expected_head_oid,
tool_revision=tool_revision,
)
return RepositoryOutcome(
repository,
Expand Down Expand Up @@ -686,6 +710,7 @@ def _run_repository(
changes=applied.changes,
head_oid=head_oid,
draft=pre_commit_failure is not None,
tool_revision=tool_revision,
)
if pre_commit_failure is not None:
_comment_dirty_pull_request(
Expand All @@ -710,6 +735,7 @@ def _run_repository(
policy=policy,
changes=applied.changes,
head_oid=head_oid,
tool_revision=tool_revision,
)
if pre_commit_failure is not None:
_mark_dirty_pull_request(
Expand Down Expand Up @@ -796,11 +822,17 @@ def _pull_request_body_changed(
policy: Policy,
changes: tuple[Change, ...],
head_oid: str,
tool_revision: str,
) -> bool:
"""Return whether the generated explanation differs from the PR body."""

body = getattr(pull_request, "body", None)
return body != _pull_request_body(policy, changes, head_oid=head_oid)
return body != _pull_request_body(
policy,
changes,
head_oid=head_oid,
tool_revision=tool_revision,
)


def _commit_result_parts(result: CommitResult) -> tuple[str, str | None]:
Expand Down Expand Up @@ -833,6 +865,7 @@ def _recreate_repository(
policy: Policy,
checkout: Path,
allow_dirty_pr: bool = False,
tool_revision: str | None = None,
) -> RepositoryOutcome:
"""Rebuild an existing policy branch from the freshly synced default branch."""

Expand All @@ -856,6 +889,7 @@ def _recreate_repository(
checkout=checkout,
existing_pr=existing_pr,
allow_dirty_pr=allow_dirty_pr,
tool_revision=tool_revision,
)


Expand All @@ -870,6 +904,7 @@ def _recreate_existing_pull_request(
existing_pr: object,
changes: tuple[Change, ...] | None = None,
allow_dirty_pr: bool = False,
tool_revision: str | None = None,
) -> RepositoryOutcome:
"""Rebuild one known policy PR from the freshly synchronized default branch."""

Expand All @@ -896,13 +931,15 @@ def _recreate_existing_pull_request(
policy=policy,
changes=body_changes,
head_oid=existing_pr.expected_head_oid,
tool_revision=tool_revision,
):
client.update_pull_request(
repository=full_name,
pull_request=existing_pr,
policy=policy,
changes=body_changes,
head_oid=existing_pr.expected_head_oid,
tool_revision=tool_revision,
)
return RepositoryOutcome(
repository,
Expand All @@ -927,6 +964,7 @@ def _recreate_existing_pull_request(
policy=policy,
changes=applied.changes,
head_oid=head_oid,
tool_revision=tool_revision,
)
if pre_commit_failure is not None:
_mark_dirty_pull_request(
Expand Down
4 changes: 4 additions & 0 deletions repo_policy_sync/templates/pull_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@

{{ changes }}

## Tool revision

Generated from [eclipse-score/tools](https://github.com/eclipse-score/tools) at commit `{{ tool_revision }}`.

{{ failure_section }}

---
Expand Down
Loading