Release 0.2.0: graduation merge race fix + VITE_APP_NAME + versioning infrastructure

- Fix §13.3 step 4 race against Gitea's async mergeability
  computation: gitea.wait_for_mergeable polls the mergeable
  field before calling merge, and bot._merge_with_retry rides
  out the transient 'Please try again later' response.
- Frontend now requires VITE_APP_NAME at build time
  (frontend/vite.config.js fails the build if unset). Header
  brand, landing H1, and browser title read the deployment's
  configured name.
- Establish framework versioning: VERSION (canonical) +
  frontend/package.json#version mirror; CHANGELOG.md as the
  release log with RFC 2119 / 8174 normative-language
  upgrade-steps; SPEC.md §20 as the binding policy; CLAUDE.md
  capturing the separation-of-concerns rule; docs/DEPLOYMENTS.md
  as the downstream operating manual; docs/QUICKSTART-VISION.md
  staking the future one-command-deploy capability.
- Surgical fix to SPEC.md opening so the spec no longer asserts
  OHM is the corpus the framework produces.
- SPEC.md §19.2 gains the 'Deployment-supplied subject framing'
  candidate that drives the 0.3.0 release.
This commit is contained in:
Ben Stull
2026-05-26 06:37:48 -07:00
parent 46049d296b
commit 29c96ea300
14 changed files with 1027 additions and 37 deletions
+41
View File
@@ -12,13 +12,17 @@ and never reaches around it.
"""
from __future__ import annotations
import asyncio
import base64
import logging
from typing import Any
import httpx
from .config import Config
log = logging.getLogger(__name__)
class GiteaError(Exception):
def __init__(self, status: int, detail: str):
@@ -247,6 +251,43 @@ class Gitea:
},
)
async def wait_for_mergeable(
self,
owner: str,
repo: str,
number: int,
*,
timeout_seconds: float = 30.0,
poll_interval_seconds: float = 0.5,
) -> bool:
"""Block until Gitea finishes computing the PR's mergeability,
or the timeout expires.
Gitea computes the `mergeable` field in a background job after a
PR is opened or its head commit changes. Until that job finishes,
`mergeable` is `null` and `POST /pulls/{n}/merge` rejects with
`405 "Please try again later"`. Callers about to merge should
wait here first so the merge call doesn't race the computation.
Returns the final `mergeable` value (True / False). Raises
TimeoutError if the field never becomes non-null within the
budget — the caller can then surface a retryable error to the
operator instead of silently failing the merge step.
"""
deadline = asyncio.get_event_loop().time() + timeout_seconds
while True:
pr = await self.get_pull(owner, repo, number)
if pr is not None:
mergeable = pr.get("mergeable")
if mergeable is not None:
return bool(mergeable)
if asyncio.get_event_loop().time() >= deadline:
raise TimeoutError(
f"Gitea did not compute mergeability for "
f"{owner}/{repo}#{number} within {timeout_seconds:.1f}s"
)
await asyncio.sleep(poll_interval_seconds)
async def close_pull(self, owner: str, repo: str, number: int) -> None:
await self._request(
"PATCH",