From bbc62970cb06635f4a1d89f0a721e0a7b4dc854b Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Wed, 14 Oct 2026 13:11:41 -0700 Subject: [PATCH] time: PRs open and merge as the time bot (TIME_TOKEN); Gitea denies both to the Actions identity --- .github/time/time.py | 13 ++++++++----- .github/workflows/heartbeat.yml | 3 ++- .github/workflows/venture.yml | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) 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 }}