aboutsummaryrefslogtreecommitdiffziptar.gz
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 09:31:12 +0200
committergodosa <godosa@godosa.eu>2026-10-07 09:31:12 +0200
commit4743282cde2cd3d6ec767e93564b2121c6d643e4 (patch)
treedc7d8af1a52c75e47dc5ea4c6808607c336a5110
parentecf39ec27307f36d9458a22a332c8848bffe79a5 (diff)
downloadworkflow-4743282cde2cd3d6ec767e93564b2121c6d643e4.tar.gz
workflow-4743282cde2cd3d6ec767e93564b2121c6d643e4.zip
split projects 4/6: workflow.toml push, wf push, .wf/push-failed marker
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEAjUkQRrCYX5MZhWxdtj2
-rw-r--r--CHANGES.md1
-rw-r--r--tests/test_split.py36
-rwxr-xr-xwf.py52
3 files changed, 84 insertions, 5 deletions
diff --git a/CHANGES.md b/CHANGES.md
index 418eece..d8d1a66 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,5 @@
# Changes (newest first)
+- 2026-10-07 split projects 4/6: workflow.toml `push` (cwd root, env WF_MAIN), `wf push`, `.wf/push-failed` marker (`wf next` shows it); finish report gains ` push-failed <rc>`. Projects: split projects set push (follow-up migration task).
- 2026-10-07 split projects 3/6: `wf finish` / `wf merge` / `wf wip` across code + private repo: paths under the private root are committed there (with TASKS/archive, message `<msg> (code <sha>)`), code paths on the branch; no code paths → no code commit; report `commit <code|-> books <private>`; `wf done` merge advice names the private books. Projects: nothing.
- 2026-10-07 split projects 2/6: `wf start` makes code worktrees (branch in the code repo, `.wf-home` pointing at the private project); `wf next` multi-session advice names `wf start` in the code repo. Projects: nothing.
- 2026-10-07 split projects 1/6: Config.split, push key, .wf-home discovery. Projects: nothing.
diff --git a/tests/test_split.py b/tests/test_split.py
index d382a9f..4182ce4 100644
--- a/tests/test_split.py
+++ b/tests/test_split.py
@@ -195,3 +195,39 @@ class FinishSplitTest(SplitCli):
code, out, err = self.wf("done", "t-three", "-m", "ok", project=False, cwd=self.wt, env=IDENT)
self.assertEqual(code, 0, out + err)
self.assertIn("commit TASKS.md tasks/archive.md, push home", out)
+
+
+class PushSplitTest(SplitCli):
+ toml = SplitCli.toml + 'push = ["echo pushing", "echo boom; exit 3"]\n'
+
+ def setUp(self):
+ super().setUp()
+ self.wt = self.add_wt()
+ (self.wt / "code.txt").write_text("x\n")
+
+ def test_push_failure_keeps_merge_marks_and_reports(self):
+ code, out, err = self.wf("finish", "t-three", "-m", "ok", "--commit", "impl", "code.txt",
+ project=False, cwd=self.wt, env=IDENT)
+ self.assertEqual(code, 0, out + err)
+ self.assertIn("not pushed (exit 3): echo boom; exit 3\n", out)
+ self.assertIn(" boom\n", out)
+ self.assertIn("rerun: wf push", out)
+ self.assertRegex(out, r"report: commit [0-9a-f]+ books [0-9a-f]+ push-failed 3\n$")
+ self.assertEqual(self.log(self.code), "impl\ninit\n")
+ self.assertTrue((self.root / ".wf" / "push-failed").is_file())
+ code, out, err = self.wf("next", "--brief", project=False, cwd=self.root)
+ self.assertIn("push failed", out)
+
+ def test_wf_push_success_clears_marker(self):
+ self.wf("finish", "t-three", "-m", "ok", "--commit", "impl", "code.txt", project=False, cwd=self.wt, env=IDENT)
+ (self.root / "workflow.toml").write_text(SplitCli.toml + 'push = ["true"]\n')
+ code, out, err = self.wf("push", project=False, cwd=self.root)
+ self.assertEqual(code, 0, out + err)
+ self.assertFalse((self.root / ".wf" / "push-failed").exists())
+
+ def test_push_runs_in_private_root(self):
+ (self.root / "workflow.toml").write_text(SplitCli.toml + 'push = ["pwd > pushed-from.txt"]\n')
+ code, out, err = self.wf("finish", "t-three", "-m", "ok", "--commit", "impl", "code.txt",
+ project=False, cwd=self.wt, env=IDENT)
+ self.assertEqual(code, 0, out + err)
+ self.assertEqual((self.root / "pushed-from.txt").read_text().strip(), str(self.root))
diff --git a/wf.py b/wf.py
index f3b7f28..f166208 100755
--- a/wf.py
+++ b/wf.py
@@ -7,6 +7,7 @@ Change: add "Title. Goal." -p N -e EFFORT · done ID… -m "entry" · prio ID N
move ID SECTION | --before/--after ID · status ID progress NOTE|blocked A-ID|clear
set ID --title/--effort/--after/--ref/--model/--sessions/--cloud · note ID "line"
body ID <stdin (Model:/Sessions:/After:/Ref: kept) · tick ID N|TEXT · setup · merge (in a lane worktree)
+ push (rerun a merge's push: workflow.toml push, else push home)
Orch: orch pick LANE [--id ID] [--recovery WHY] · orch post ID LANE [--result LINE] [--agent A] [--duration S] [--no-next]
Other: report "what happened" · init · migrate [--write] · res … (shared memory/CPU ledger; wf res -h)
batch N [--lanes L,…] [--prep] | --status (unattended batch orchestrator; wf batch -h)
@@ -432,6 +433,8 @@ def cmd_next(args) -> int:
item, skipped = lanes.pick(p.doc, p.archived, p.cfg.lanes, p.cfg.slice_above, lane, model, claims,
other_live(p.cfg), args.owner)
multi = multi_lines(p, args, lane or "all", 1 if warning else 0)
+ if (marker := p.cfg.root / ".wf" / "push-failed").is_file():
+ out += [f"push failed: {marker.read_text().splitlines()[0]} → wf push (in {p.cfg.root})"]
if not args.brief:
awaiting = p.doc.section("awaiting").items
if awaiting:
@@ -1268,13 +1271,44 @@ def cmd_merge(args) -> int:
git_run(top, "switch", "-q", "--detach", master)
if branch:
git_run(top, "branch", "-q", "-d", branch)
- if not args.no_push and any([_push_home(main), bmain != main and _push_home(bmain)]):
- out.append("pushed home")
+ args.push_rc = 0
+ if not args.no_push:
+ args.push_rc, lines = run_push(cfg, [bmain, main])
+ out += lines
out.append(f"merged {branch or 'detached HEAD'} into {master}")
print("\n".join(out))
return 0
+def run_push(cfg: config.Config, repos: list[Path]) -> tuple[int, list[str]]:
+ """Push after a merge: workflow.toml push lines (cwd = root, env WF_MAIN; first nonzero stops and writes
+ .wf/push-failed), else _push_home on each repo. (exit code, lines to print)."""
+ marker = cfg.root / ".wf" / "push-failed"
+ if not cfg.push:
+ pushed = [r for r in dict.fromkeys(repos) if _push_home(r)]
+ return 0, ["pushed home"] if pushed else []
+ env = {**os.environ, "WF_MAIN": str(cfg.root)}
+ for line in cfg.push:
+ r = subprocess.run(line, shell=True, cwd=cfg.root, env=env, capture_output=True, text=True)
+ if r.returncode:
+ tail = (r.stdout + r.stderr).strip().splitlines()[-5:]
+ wf_folder(cfg, "orch") # creates .wf with its .gitignore
+ marker.write_text(f"{line}\nexit {r.returncode}\n" + "\n".join(tail) + "\n")
+ return r.returncode, [f"not pushed (exit {r.returncode}): {line}", *(f" {t}" for t in tail),
+ "rerun: wf push"]
+ marker.unlink(missing_ok=True)
+ return 0, [f"pushed ({len(cfg.push)} push command{'s' * (len(cfg.push) != 1)})"]
+
+
+def cmd_push(args) -> int:
+ """Run the push of a merge again (workflow.toml push, else push home), from the project or a code worktree."""
+ cfg = config.load_at(Path(args.project or Path.cwd()).resolve())
+ repos = [r for r in (config.git_top(cfg.root), config.code_main(cfg)) if r]
+ rc, lines = run_push(cfg, repos)
+ print("\n".join(lines) or "nothing to push (no push key, no remote home)")
+ return rc
+
+
def _push_home(main: Path) -> bool:
"""git push home --all/--tags when remote home exists; True if pushed."""
if "home" not in git_run(main, "remote").stdout.split():
@@ -1371,8 +1405,11 @@ def cmd_finish(args) -> int:
sha = git_run(top, "rev-parse", "--short", "HEAD").stdout.strip()
tool = getattr(args, "tool_commit", None)
print(f"report: commit {sha}" + (f" tool {tool}" if tool else ""), flush=True)
- if not wt and not args.no_push and _push_home(top):
- print("pushed home", flush=True)
+ push_rc = 0
+ if not wt and not args.no_push:
+ push_rc, lines = run_push(cfg, [top])
+ if lines:
+ print("\n".join(lines), flush=True)
if wt:
ns = argparse.Namespace(project=args.project, m=None, no_push=args.no_push,
private=priv, private_msg=args.commit)
@@ -1380,7 +1417,9 @@ def cmd_finish(args) -> int:
if not rc and getattr(ns, "merged_sha", ""):
tool = getattr(args, "tool_commit", None)
books_sha = f" books {ns.books_sha}" if split else ""
- print(f"report: commit {ns.merged_sha}{books_sha}" + (f" tool {tool}" if tool else ""), flush=True)
+ failed = f" push-failed {ns.push_rc}" if getattr(ns, "push_rc", 0) else ""
+ print(f"report: commit {ns.merged_sha}{books_sha}" + (f" tool {tool}" if tool else "") + failed,
+ flush=True)
return rc
return 0
@@ -2178,6 +2217,9 @@ def parser() -> argparse.ArgumentParser:
sp.add_argument("-m", help="bookkeeping commit message (default '<id> done', id from the branch)")
sp.add_argument("--no-push", action="store_true", help="skip git push home")
+ sp = cmd("push", cmd_push, "rerun a merge's push: workflow.toml push lines, else git push home")
+ sp.set_defaults(locks=True)
+
sp = cmd("finish", cmd_finish, "worker's last step in one call: quick_gate → done → commit PATHS (explicit) → "
"wf merge (lane worktree; main tree: TASKS/archive go into the commit). Run verify first. "
"Refuses before done if the gate is red, files outside PATHS are uncommitted, or a PATH is outside "