diff --git a/.github/time/time.py b/.github/time/time.py
index 571ac48..c796b64 100644
--- a/.github/time/time.py
+++ b/.github/time/time.py
@@ -54,10 +54,14 @@ def http(method: str, url: str, token: str, body=None, timeout: float = 120.0):
raise RuntimeError(f"{method} {url.split('?')[0]} -> {e.code}: {e.read().decode(errors='replace')[:200]}") from None
-def gh(method: str, path: str, body=None):
+def gh(method: str, path: str, body=None, *, as_time: bool = False):
+ """One API call. `as_time` uses TIME_TOKEN, the `time` bot's own token: Gitea lets the Actions identity comment and
+ label but not open or merge pull requests ("user must be a collaborator"), so those two go out as Time itself.
+ Without the secret the call falls back to the Actions token (GitHub, where it is enough)."""
+ token = (os.environ.get("TIME_TOKEN") if as_time else None) or os.environ["GITHUB_TOKEN"]
for attempt in range(5):
try:
- return http(method, f"{GH}{path}", os.environ["GITHUB_TOKEN"], body)
+ return http(method, f"{GH}{path}", token, body)
except RuntimeError as e:
# parallel writers on one branch: a contents PUT loses the race for the branch head with a 409; the file's own
# sha is still right, so the same write goes through on a retry
@@ -623,8 +627,7 @@ def land(card: dict, last: dict) -> None:
comment(n, f"⏱️ Time: approved, but checks are failing on PR #{pr['number']}. Waiting for a fix.",
{"beat": "checks-failed", "pr": pr["number"]})
return
- # risk: an API merge as the Actions token may hit the same pre-receive refusal as the contents API (see git_commit)
- gh("POST", f"/repos/{REPO}/pulls/{pr['number']}/merge", {"Do": "squash", "delete_branch_after_merge": True})
+ gh("POST", f"/repos/{REPO}/pulls/{pr['number']}/merge", {"Do": "squash", "delete_branch_after_merge": True}, as_time=True)
if gh("GET", f"/repos/{REPO}/issues/{n}")["state"] == "open":
gh("PATCH", f"/repos/{REPO}/issues/{n}", {"state": "closed"})
comment(n, f"⏱️ Time: approved and green, merged PR #{pr['number']}. Card done.", {"beat": "merged", "pr": pr["number"]})
@@ -818,7 +821,7 @@ class Hands:
return f"exit {p.returncode}\n{(p.stdout + p.stderr)[-6000:]}{note}"
if name == "open_pr":
pr = gh("POST", f"/repos/{REPO}/pulls", {"title": a["title"][:200], "head": self.branch, "base": default_branch(),
- "body": f"{a['summary']}\n\nCloses #{self.n}\n\nworker: {self.label} via Time"})
+ "body": f"{a['summary']}\n\nCloses #{self.n}\n\nworker: {self.label} via Time"}, as_time=True)
self.pr = pr["number"]
return f"opened PR #{self.pr}"
return f"unknown tool {name}"
diff --git a/.github/workflows/heartbeat.yml b/.github/workflows/heartbeat.yml
index 67d1b3a..1bb4439 100644
--- a/.github/workflows/heartbeat.yml
+++ b/.github/workflows/heartbeat.yml
@@ -35,7 +35,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # user-level secret on teahubxyz (docs/decisions.md on the spine)
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ TIME_TOKEN: ${{ secrets.TIME_TOKEN }} # the `time` bot: opens and merges PRs (Gitea denies that to the Actions identity) # user-level secret on teahubxyz (docs/decisions.md on the spine)
TIME_TRUSTED: ${{ vars.TIME_TRUSTED }} # org owners etc.; collaborators are trusted without being listed
TIME_RUN_IMAGE: node:22-bookworm # the worker's shell runs in a throwaway container, not next to these secrets
TIME_MODE: ${{ vars.TIME_MODE }}
diff --git a/.github/workflows/venture.yml b/.github/workflows/venture.yml
index e4b6bc5..9b14fef 100644
--- a/.github/workflows/venture.yml
+++ b/.github/workflows/venture.yml
@@ -39,6 +39,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ TIME_TOKEN: ${{ secrets.TIME_TOKEN }} # the `time` bot: opens and merges PRs (Gitea denies that to the Actions identity)
TIME_RUN_IMAGE: node:22-bookworm # the worker's shell runs in a throwaway container, not next to these secrets
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} # optional: a backend per venture (cloudflare.py)
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}