Skip to content

Commit e3eecbf

Browse files
authored
Merge pull request #40 from githubnext/copilot/extract-scheduler-script
Extract Autoloop scheduler from inline heredoc into a committed Python script
2 parents e930385 + 53ce1c2 commit e3eecbf

10 files changed

Lines changed: 1411 additions & 1044 deletions

File tree

.github/workflows/autoloop.lock.yml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/autoloop.md

Lines changed: 1 addition & 368 deletions
Large diffs are not rendered by default.

.github/workflows/scripts/autoloop_scheduler.py

Lines changed: 545 additions & 0 deletions
Large diffs are not rendered by default.

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ jobs:
1313
- uses: actions/setup-python@v5
1414
with:
1515
python-version: "3.12"
16-
- run: pip install pytest
16+
- run: pip install pytest pyyaml
1717
- run: pytest tests/ -v

AGENTS.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ autoloop/
1212
├── workflows/ ← Agentic Workflow definitions
1313
│ ├── autoloop.md ← main autoloop workflow (compiled by gh-aw)
1414
│ ├── sync-branches.md ← syncs default branch into autoloop/* branches
15-
│ └── shared/ ← shared workflow fragments
16-
│ └── reporting.md
15+
│ ├── shared/ ← shared workflow fragments
16+
│ │ └── reporting.md
17+
│ └── scripts/ ← standalone scripts invoked from steps
18+
│ └── autoloop_scheduler.py ← scheduler (see workflows/autoloop.md)
1719
├── .autoloop/
1820
│ └── programs/ ← research programs (directory-based)
1921
│ ├── function_minimization/
@@ -134,6 +136,7 @@ To deploy the workflow to a repository:
134136
1. Copy `workflows/autoloop.md` to `.github/workflows/autoloop.md` in the target repo
135137
2. Copy `workflows/sync-branches.md` to `.github/workflows/sync-branches.md` in the target repo
136138
3. Copy `workflows/shared/` to `.github/workflows/shared/` in the target repo
137-
4. Run `gh aw compile autoloop` and `gh aw compile sync-branches` to generate the lock files
138-
5. Copy program directories to `.autoloop/programs/` in the target repo
139-
6. Commit and push
139+
4. Copy `workflows/scripts/` to `.github/workflows/scripts/` in the target repo
140+
5. Run `gh aw compile autoloop` and `gh aw compile sync-branches` to generate the lock files
141+
6. Copy program directories to `.autoloop/programs/` in the target repo
142+
7. Commit and push

tests/conftest.py

Lines changed: 26 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,36 @@
1-
"""
2-
Extract scheduling functions directly from the workflow pre-step heredoc.
3-
4-
Instead of duplicating the workflow's JavaScript code in a separate module, we parse
5-
workflows/autoloop.md, extract the JavaScript heredoc, write the function definitions
6-
to a temp CommonJS module, and call them via Node.js subprocess.
1+
"""Test fixtures for the standalone Autoloop scheduler.
72
8-
This ensures tests always run against the actual workflow code.
3+
The scheduler logic lives in ``workflows/scripts/autoloop_scheduler.py`` and is
4+
also distributed at ``.github/workflows/scripts/autoloop_scheduler.py`` (the
5+
dogfooded deploy copy). Tests import the source module directly via importlib.
96
"""
107

11-
import json
8+
import importlib.util
129
import os
13-
import re
14-
import subprocess
15-
import tempfile
16-
from datetime import timedelta
17-
18-
WORKFLOW_PATH = os.path.join(os.path.dirname(__file__), "..", "workflows", "autoloop.md")
19-
20-
# Path to the extracted JS module
21-
_JS_MODULE_PATH = os.path.join(tempfile.gettempdir(), "autoloop_test_functions.cjs")
22-
23-
24-
def _load_workflow_functions():
25-
"""Parse workflows/autoloop.md and extract JS function defs from the pre-step."""
26-
with open(WORKFLOW_PATH) as f:
27-
content = f.read()
28-
29-
# Extract the JavaScript heredoc between JSEOF markers
30-
m = re.search(r"node - << 'JSEOF'\n(.*?)\n\s*JSEOF", content, re.DOTALL)
31-
assert m, "Could not find JSEOF heredoc in workflows/autoloop.md"
32-
source = m.group(1)
33-
34-
# Extract function definitions: everything up to the main() async function.
35-
# Functions are defined before 'async function main()'
36-
lines = source.split("\n")
37-
func_lines = []
38-
for line in lines:
39-
if line.strip().startswith("async function main"):
40-
break
41-
func_lines.append(line)
42-
43-
func_source = "\n".join(func_lines)
44-
45-
# Write to a temp .cjs file with module.exports
46-
with open(_JS_MODULE_PATH, "w") as f:
47-
f.write(func_source)
48-
f.write(
49-
"\n\nmodule.exports = "
50-
"{ parseMachineState, parseSchedule, getProgramName, readProgramState, parseLinkHeader };\n"
51-
)
52-
53-
return True
54-
55-
56-
def _call_js(func_name, *args):
57-
"""Call a JS function from the extracted workflow module and return the result."""
58-
args_json = json.dumps(list(args))
59-
escaped_path = json.dumps(_JS_MODULE_PATH)
60-
script = (
61-
"const m = require(" + escaped_path + ");\n"
62-
"const result = m." + func_name + "(..." + args_json + ");\n"
63-
"process.stdout.write(JSON.stringify(result === undefined ? null : result));\n"
64-
)
65-
result = subprocess.run(
66-
["node", "-e", script],
67-
capture_output=True,
68-
text=True,
69-
timeout=10,
10+
import sys
11+
12+
# Path to the standalone scheduler script (source-of-truth lives in workflows/).
13+
SCHEDULER_PATH = os.path.normpath(
14+
os.path.join(
15+
os.path.dirname(__file__),
16+
"..",
17+
"workflows",
18+
"scripts",
19+
"autoloop_scheduler.py",
7020
)
71-
if result.returncode != 0:
72-
raise RuntimeError("Node.js error calling " + func_name + ": " + result.stderr)
73-
if not result.stdout.strip():
74-
return None
75-
return json.loads(result.stdout)
76-
77-
78-
# Initialize at import time
79-
_load_workflow_functions()
80-
81-
82-
def _parse_schedule_wrapper(s):
83-
"""Python wrapper for JS parseSchedule. Converts milliseconds to timedelta."""
84-
ms = _call_js("parseSchedule", s)
85-
if ms is None:
86-
return None
87-
return timedelta(milliseconds=ms)
21+
)
8822

89-
90-
def _parse_machine_state_wrapper(content):
91-
"""Python wrapper for JS parseMachineState."""
92-
return _call_js("parseMachineState", content)
93-
94-
95-
def _get_program_name_wrapper(pf):
96-
"""Python wrapper for JS getProgramName."""
97-
return _call_js("getProgramName", pf)
23+
_spec = importlib.util.spec_from_file_location("autoloop_scheduler", SCHEDULER_PATH)
24+
autoloop_scheduler = importlib.util.module_from_spec(_spec)
25+
sys.modules["autoloop_scheduler"] = autoloop_scheduler
26+
_spec.loader.exec_module(autoloop_scheduler)
9827

9928

29+
# Backwards-compatible function map (mirrors the previous JS-extracting conftest).
10030
_funcs = {
101-
"parse_schedule": _parse_schedule_wrapper,
102-
"parse_machine_state": _parse_machine_state_wrapper,
103-
"get_program_name": _get_program_name_wrapper,
104-
"read_program_state": lambda name: _call_js("readProgramState", name),
105-
"parse_link_header": lambda header: _call_js("parseLinkHeader", header),
31+
"parse_schedule": autoloop_scheduler.parse_schedule,
32+
"parse_machine_state": autoloop_scheduler.parse_machine_state,
33+
"get_program_name": autoloop_scheduler.get_program_name,
34+
"read_program_state": autoloop_scheduler.read_program_state,
35+
"parse_link_header": autoloop_scheduler.parse_link_header,
10636
}
107-
108-
109-
def _extract_inline_pattern(name):
110-
"""Extract the JavaScript heredoc source from the workflow.
111-
112-
This is a helper for inspecting the full inline source if needed.
113-
"""
114-
with open(WORKFLOW_PATH) as f:
115-
content = f.read()
116-
m = re.search(r"node - << 'JSEOF'\n(.*?)\n\s*JSEOF", content, re.DOTALL)
117-
return m.group(1) if m else ""

0 commit comments

Comments
 (0)