Skip to content

Commit 15270d0

Browse files
authored
Merge pull request #51 from githubnext/copilot/support-metric-direction-lower
Support `metric_direction: lower` in program frontmatter
2 parents 7063470 + 3b945e4 commit 15270d0

4 files changed

Lines changed: 383 additions & 52 deletions

File tree

.github/workflows/scripts/autoloop_scheduler.py

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,14 @@ def parse_link_header(header):
155155

156156

157157
def parse_program_frontmatter(content):
158-
"""Parse optional YAML frontmatter for ``schedule`` and ``target-metric``.
158+
"""Parse optional YAML frontmatter for ``schedule``, ``target-metric``, and ``metric_direction``.
159159
160-
Returns ``(schedule_delta, target_metric, target_metric_invalid_value)``.
160+
Returns ``(schedule_delta, target_metric, target_metric_invalid_value,
161+
metric_direction, metric_direction_invalid_value)``.
162+
163+
``metric_direction`` is one of ``"higher"`` (default) or ``"lower"``.
164+
Invalid values fall back to ``"higher"`` and the raw string is returned in
165+
the fifth element so the caller can warn.
161166
The third element is the raw string of an invalid ``target-metric`` value
162167
(so the caller can warn), or ``None`` when the value parsed cleanly or was
163168
absent.
@@ -167,20 +172,41 @@ def parse_program_frontmatter(content):
167172
schedule_delta = None
168173
target_metric = None
169174
target_metric_invalid = None
175+
metric_direction = "higher"
176+
metric_direction_invalid = None
170177
fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content_stripped, re.DOTALL)
171178
if not fm_match:
172-
return schedule_delta, target_metric, target_metric_invalid
179+
return (
180+
schedule_delta,
181+
target_metric,
182+
target_metric_invalid,
183+
metric_direction,
184+
metric_direction_invalid,
185+
)
173186
for line in fm_match.group(1).split("\n"):
174-
if line.strip().startswith("schedule:"):
187+
stripped = line.strip()
188+
if stripped.startswith("schedule:"):
175189
schedule_str = line.split(":", 1)[1].strip()
176190
schedule_delta = parse_schedule(schedule_str)
177-
if line.strip().startswith("target-metric:"):
191+
if stripped.startswith("target-metric:"):
178192
raw = line.split(":", 1)[1].strip()
179193
try:
180194
target_metric = float(raw)
181195
except (ValueError, TypeError):
182196
target_metric_invalid = raw
183-
return schedule_delta, target_metric, target_metric_invalid
197+
if stripped.startswith("metric_direction:") or stripped.startswith("metric-direction:"):
198+
raw = line.split(":", 1)[1].strip().strip('"').strip("'").lower()
199+
if raw in ("higher", "lower"):
200+
metric_direction = raw
201+
else:
202+
metric_direction_invalid = raw
203+
return (
204+
schedule_delta,
205+
target_metric,
206+
target_metric_invalid,
207+
metric_direction,
208+
metric_direction_invalid,
209+
)
184210

185211

186212
def is_unconfigured(content):
@@ -363,12 +389,22 @@ def _parse_target_metric_from_file(path):
363389
"""Re-parse a program file to extract its ``target-metric``, if any."""
364390
try:
365391
with open(path) as f:
366-
_, target_metric, _ = parse_program_frontmatter(f.read())
392+
_, target_metric, _, _, _ = parse_program_frontmatter(f.read())
367393
return target_metric
368394
except (OSError, ValueError, TypeError):
369395
return None
370396

371397

398+
def _parse_metric_direction_from_file(path):
399+
"""Re-parse a program file to extract its ``metric_direction`` (default ``"higher"``)."""
400+
try:
401+
with open(path) as f:
402+
_, _, _, direction, _ = parse_program_frontmatter(f.read())
403+
return direction or "higher"
404+
except (OSError, ValueError, TypeError):
405+
return "higher"
406+
407+
372408
# ---------------------------------------------------------------------------
373409
# Existing PR lookup (single-PR-per-program invariant)
374410
# ---------------------------------------------------------------------------
@@ -459,23 +495,25 @@ def select_program(due, forced_program=None, all_programs=None, unconfigured=Non
459495
"""Pick the program to run.
460496
461497
Returns ``(selected, selected_file, selected_issue, selected_target_metric,
462-
deferred, error)``. ``error`` is a string describing why a forced selection
463-
failed (and the caller should ``sys.exit(1)``); otherwise it is ``None``.
498+
selected_metric_direction, deferred, error)``. ``error`` is a string describing
499+
why a forced selection failed (and the caller should ``sys.exit(1)``);
500+
otherwise it is ``None``. ``selected_metric_direction`` is one of
501+
``"higher"`` (default) or ``"lower"``.
464502
"""
465503
all_programs = all_programs or {}
466504
unconfigured = unconfigured or []
467505
issue_programs = issue_programs or {}
468506
if forced_program:
469507
if forced_program not in all_programs:
470508
return (
471-
None, None, None, None, [],
509+
None, None, None, None, "higher", [],
472510
"requested program '{}' not found. Available programs: {}".format(
473511
forced_program, list(all_programs.keys())
474512
),
475513
)
476514
if forced_program in unconfigured:
477515
return (
478-
None, None, None, None, [],
516+
None, None, None, None, "higher", [],
479517
"requested program '{}' is unconfigured (has placeholders).".format(
480518
forced_program
481519
),
@@ -487,13 +525,25 @@ def select_program(due, forced_program=None, all_programs=None, unconfigured=Non
487525
issue_programs[selected]["issue_number"] if selected in issue_programs else None
488526
)
489527
selected_target_metric = None
528+
selected_metric_direction = None
490529
for p in due:
491530
if p["name"] == forced_program:
492531
selected_target_metric = p.get("target_metric")
532+
selected_metric_direction = p.get("metric_direction")
493533
break
494534
if selected_target_metric is None:
495535
selected_target_metric = _parse_target_metric_from_file(selected_file)
496-
return selected, selected_file, selected_issue, selected_target_metric, deferred, None
536+
if selected_metric_direction is None:
537+
selected_metric_direction = _parse_metric_direction_from_file(selected_file)
538+
return (
539+
selected,
540+
selected_file,
541+
selected_issue,
542+
selected_target_metric,
543+
selected_metric_direction,
544+
deferred,
545+
None,
546+
)
497547

498548
if due:
499549
# Normal scheduling: pick the single most-overdue program.
@@ -502,13 +552,22 @@ def select_program(due, forced_program=None, all_programs=None, unconfigured=Non
502552
selected = due_sorted[0]["name"]
503553
selected_file = due_sorted[0]["file"]
504554
selected_target_metric = due_sorted[0].get("target_metric")
555+
selected_metric_direction = due_sorted[0].get("metric_direction") or "higher"
505556
deferred = [p["name"] for p in due_sorted[1:]]
506557
selected_issue = (
507558
issue_programs[selected]["issue_number"] if selected in issue_programs else None
508559
)
509-
return selected, selected_file, selected_issue, selected_target_metric, deferred, None
560+
return (
561+
selected,
562+
selected_file,
563+
selected_issue,
564+
selected_target_metric,
565+
selected_metric_direction,
566+
deferred,
567+
None,
568+
)
510569

511-
return None, None, None, None, [], None
570+
return None, None, None, None, "higher", [], None
512571

513572

514573
# ---------------------------------------------------------------------------
@@ -574,9 +633,15 @@ def main():
574633
unconfigured.append(name)
575634
continue
576635

577-
schedule_delta, target_metric, invalid_target = parse_program_frontmatter(content)
636+
schedule_delta, target_metric, invalid_target, metric_direction, invalid_direction = parse_program_frontmatter(content)
578637
if invalid_target is not None:
579638
print(" Warning: {} has invalid target-metric value: {}".format(name, invalid_target))
639+
if invalid_direction is not None:
640+
print(
641+
" Warning: {} has invalid metric_direction value: {!r} (must be 'higher' or 'lower'); defaulting to 'higher'".format(
642+
name, invalid_direction
643+
)
644+
)
580645

581646
# Read state from repo-memory
582647
state = read_program_state(name)
@@ -613,9 +678,15 @@ def main():
613678
)
614679
continue
615680

616-
due.append({"name": name, "last_run": lr, "file": pf, "target_metric": target_metric})
681+
due.append({
682+
"name": name,
683+
"last_run": lr,
684+
"file": pf,
685+
"target_metric": target_metric,
686+
"metric_direction": metric_direction,
687+
})
617688

618-
selected, selected_file, selected_issue, selected_target_metric, deferred, error = (
689+
selected, selected_file, selected_issue, selected_target_metric, selected_metric_direction, deferred, error = (
619690
select_program(due, forced_program, all_programs, unconfigured, issue_programs)
620691
)
621692

@@ -645,6 +716,7 @@ def main():
645716
"selected_file": selected_file,
646717
"selected_issue": selected_issue,
647718
"selected_target_metric": selected_target_metric,
719+
"selected_metric_direction": selected_metric_direction,
648720
"state_file_size_bytes": get_state_file_size(selected) if selected else 0,
649721
"state_file_max_bytes": STATE_FILE_MAX_BYTES,
650722
"issue_programs": {

0 commit comments

Comments
 (0)