repoactive 0.2.3.dev1__tar.gz → 0.2.4.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/PKG-INFO +1 -1
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/pyproject.toml +1 -1
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/config.py +3 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/jj.py +15 -7
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/platforms/base.py +1 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/platforms/github.py +2 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/platforms/gitlab.py +2 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/runner.py +39 -11
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/updates.py +1 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/.gitignore +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/LICENSE +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/README.md +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/__init__.py +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/cli.py +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/constants.py +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/graph.py +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/jobtree.py +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/lock.py +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/platforms/__init__.py +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/progress.py +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/settings.py +0 -0
- {repoactive-0.2.3.dev1 → repoactive-0.2.4.dev1}/src/repoactive/ui.py +0 -0
|
@@ -269,6 +269,7 @@ class JobDefaults(BaseModel):
|
|
|
269
269
|
base_branch: str | None = None
|
|
270
270
|
cooldown_period: _Duration | None = None
|
|
271
271
|
timeout: _Duration | None = "2m"
|
|
272
|
+
auto_merge: bool = False
|
|
272
273
|
|
|
273
274
|
@field_validator("mr_title_prefix", "commit_title_prefix")
|
|
274
275
|
@classmethod
|
|
@@ -288,6 +289,7 @@ _DEFAULTED_FIELDS = (
|
|
|
288
289
|
"base_branch",
|
|
289
290
|
"cooldown_period",
|
|
290
291
|
"timeout",
|
|
292
|
+
"auto_merge",
|
|
291
293
|
)
|
|
292
294
|
|
|
293
295
|
|
|
@@ -323,6 +325,7 @@ class Job(BaseModel):
|
|
|
323
325
|
labels: list[str] = Field(default_factory=list)
|
|
324
326
|
cooldown_period: _Duration | None = None
|
|
325
327
|
timeout: _Duration | None = None
|
|
328
|
+
auto_merge: bool | None = None
|
|
326
329
|
|
|
327
330
|
@field_validator("name")
|
|
328
331
|
@classmethod
|
|
@@ -8,7 +8,7 @@ import tempfile
|
|
|
8
8
|
import time
|
|
9
9
|
from collections.abc import Generator
|
|
10
10
|
from dataclasses import dataclass
|
|
11
|
-
from datetime import datetime
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
14
|
from repoactive.constants import JOB_TRAILER_KEY
|
|
@@ -215,8 +215,8 @@ class JJ:
|
|
|
215
215
|
def edit(self, revision: str) -> None:
|
|
216
216
|
self._run("edit", revision)
|
|
217
217
|
|
|
218
|
-
def restore(self,
|
|
219
|
-
self._run("restore", "--
|
|
218
|
+
def restore(self, *, source_rev: str, destination_rev: str) -> None:
|
|
219
|
+
self._run("restore", "--from", source_rev, "--into", destination_rev)
|
|
220
220
|
|
|
221
221
|
def rebase(self, *onto: str) -> None:
|
|
222
222
|
onto_args = [arg for parent in onto for arg in ("--onto", parent)]
|
|
@@ -400,8 +400,10 @@ class JJ:
|
|
|
400
400
|
if name.strip()
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
def
|
|
404
|
-
|
|
403
|
+
def last_job_commit_date(
|
|
404
|
+
self, *, job_name: str, base: str, since: datetime
|
|
405
|
+
) -> datetime | None:
|
|
406
|
+
"""Return the committer date of the most recent job commit on ``base``, or ``None``.
|
|
405
407
|
|
|
406
408
|
Matches commits that have a ``Repoactive-Job: <job_name>`` trailer and a
|
|
407
409
|
committer date at or after ``since``. Used to throttle jobs: a recent
|
|
@@ -410,16 +412,22 @@ class JJ:
|
|
|
410
412
|
The trailer is matched via jj's trailer parsing, which only considers the
|
|
411
413
|
final paragraph of the description, so a stray matching line in the body
|
|
412
414
|
is correctly ignored.
|
|
415
|
+
|
|
416
|
+
Returns the newest matching committer timestamp, or ``None`` if no match.
|
|
413
417
|
"""
|
|
414
418
|
revset = f'::{base} & committer_date(after:"{_jj_timestamp(since)}")'
|
|
415
419
|
template = f"""
|
|
416
420
|
if (trailers.any(|t| t.key() == "{JOB_TRAILER_KEY}" && t.value() == "{job_name}"),
|
|
417
|
-
"
|
|
421
|
+
committer.timestamp().utc().format("%Y-%m-%dT%H:%M:%S") ++ "\\n",
|
|
418
422
|
""
|
|
419
423
|
)
|
|
420
424
|
"""
|
|
421
425
|
output = self._run("log", "--no-graph", "-r", revset, "-T", template)
|
|
422
|
-
|
|
426
|
+
lines = [line.strip() for line in output.splitlines() if line.strip()]
|
|
427
|
+
if not lines:
|
|
428
|
+
return None
|
|
429
|
+
# jj log returns newest-first; take the first line.
|
|
430
|
+
return datetime.fromisoformat(lines[0]).replace(tzinfo=UTC)
|
|
423
431
|
|
|
424
432
|
def git_push_bookmarks(self, *bookmarks: str) -> None:
|
|
425
433
|
"""Push bookmarks to the remote.
|
|
@@ -63,6 +63,7 @@ _INHERITED_FIELDS = (
|
|
|
63
63
|
"commit_title_prefix",
|
|
64
64
|
"draft",
|
|
65
65
|
"create_mr",
|
|
66
|
+
"auto_merge",
|
|
66
67
|
)
|
|
67
68
|
|
|
68
69
|
|
|
@@ -621,22 +622,43 @@ def _build_generated_jobs(
|
|
|
621
622
|
return emitted
|
|
622
623
|
|
|
623
624
|
|
|
624
|
-
def
|
|
625
|
-
"""
|
|
625
|
+
def _format_duration(seconds: float) -> str:
|
|
626
|
+
"""Format a duration in seconds as a human-readable string like '3d 2h' or '45m'."""
|
|
627
|
+
seconds = int(seconds)
|
|
628
|
+
days, seconds = divmod(seconds, 86400)
|
|
629
|
+
hours, seconds = divmod(seconds, 3600)
|
|
630
|
+
minutes, seconds = divmod(seconds, 60)
|
|
631
|
+
if days:
|
|
632
|
+
parts = [f"{days}d"]
|
|
633
|
+
if hours:
|
|
634
|
+
parts.append(f"{hours}h")
|
|
635
|
+
return " ".join(parts)
|
|
636
|
+
if hours:
|
|
637
|
+
parts = [f"{hours}h"]
|
|
638
|
+
if minutes:
|
|
639
|
+
parts.append(f"{minutes}m")
|
|
640
|
+
return " ".join(parts)
|
|
641
|
+
if minutes:
|
|
642
|
+
return f"{minutes}m"
|
|
643
|
+
return f"{seconds}s"
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _on_cooldown(job: Job, repo_path: Path) -> datetime | None:
|
|
647
|
+
"""Return the last-run timestamp if the job is still on cooldown, else None."""
|
|
626
648
|
delta = job.cooldown_timedelta()
|
|
627
649
|
if delta is None:
|
|
628
|
-
return
|
|
650
|
+
return None
|
|
629
651
|
base = job.base_branch or "trunk()"
|
|
630
652
|
since = datetime.now(UTC) - delta
|
|
631
|
-
|
|
653
|
+
last_run = JJ(repo_path).last_job_commit_date(job_name=job.name, base=base, since=since)
|
|
632
654
|
logger.debug(
|
|
633
|
-
"[%s] cooldown check: base=%s since=%s ->
|
|
655
|
+
"[%s] cooldown check: base=%s since=%s -> last_run=%s",
|
|
634
656
|
job.name,
|
|
635
657
|
base,
|
|
636
658
|
since.isoformat(),
|
|
637
|
-
|
|
659
|
+
last_run,
|
|
638
660
|
)
|
|
639
|
-
return
|
|
661
|
+
return last_run
|
|
640
662
|
|
|
641
663
|
|
|
642
664
|
def _validate_selection(
|
|
@@ -827,8 +849,13 @@ def _dispatch_job( # noqa: PLR0913
|
|
|
827
849
|
# emits nothing, throttling its whole fan-out as a unit (the dual trailer
|
|
828
850
|
# records a recent child landing as the generator's). See
|
|
829
851
|
# docs/adr/0004-job-generators.md.
|
|
830
|
-
if _on_cooldown(resolved_job, repo_path):
|
|
831
|
-
|
|
852
|
+
if last_run := _on_cooldown(resolved_job, repo_path):
|
|
853
|
+
elapsed = datetime.now(UTC) - last_run
|
|
854
|
+
elapsed_str = _format_duration(elapsed.total_seconds())
|
|
855
|
+
print(
|
|
856
|
+
f"==> [{job.name}] on cooldown ({resolved_job.cooldown_period}),"
|
|
857
|
+
f" last run {elapsed_str} ago, skipped"
|
|
858
|
+
)
|
|
832
859
|
summary.on_cooldown.add(job.name)
|
|
833
860
|
# Treat like a no-op run so dependents proceed on the base branch.
|
|
834
861
|
summary.results[job.name] = JobResult(
|
|
@@ -1004,8 +1031,7 @@ def _absorb_results(
|
|
|
1004
1031
|
if not content_unchanged:
|
|
1005
1032
|
if abs_ws is None:
|
|
1006
1033
|
abs_ws = stack.enter_context(repo.temp_workspace("repoactive-absorb"))
|
|
1007
|
-
abs_ws.
|
|
1008
|
-
abs_ws.restore(new_cid)
|
|
1034
|
+
abs_ws.restore(source_rev=new_cid, destination_rev=old_cid)
|
|
1009
1035
|
repo.describe_revision(old_cid, message)
|
|
1010
1036
|
elif _strip_boxquote_and_trailers(
|
|
1011
1037
|
repo.get_description(old_cid)
|
|
@@ -1033,6 +1059,7 @@ def _absorb_results(
|
|
|
1033
1059
|
command_output=result.command_output,
|
|
1034
1060
|
labels=result.job.labels,
|
|
1035
1061
|
draft=result.job.draft,
|
|
1062
|
+
auto_merge=result.job.auto_merge or False,
|
|
1036
1063
|
depends_on=list(result.job.depends_on),
|
|
1037
1064
|
)
|
|
1038
1065
|
plan.updates.append(
|
|
@@ -1287,6 +1314,7 @@ def _apply_plan_publish(
|
|
|
1287
1314
|
description=build_mr_description(update.mr, dependency_links),
|
|
1288
1315
|
labels=update.mr.labels,
|
|
1289
1316
|
draft=update.mr.draft,
|
|
1317
|
+
auto_merge=update.mr.auto_merge,
|
|
1290
1318
|
)
|
|
1291
1319
|
url = platform.ensure_mr(params)
|
|
1292
1320
|
except Exception as e:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|