fix(slice4): make all entry write paths sidecar-aware (§22.4a carried from SLICE-1)
graduate, claim, retire/unretire, _read_meta_entry, mark_entry_reviewed, body extract/wrap (api_branches + api_prs replay) now dual-read and write metadata to the sidecar via write_entry_files + bot.commit_entry_files/ open_entry_pr — a migrated body-only .md no longer crashes entry.parse or re-grows frontmatter; legacy entries lazy-migrate on first metadata write. Existing tests updated to assert the sidecar (INV-2 clean docs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+36
-21
@@ -29,7 +29,7 @@ from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from . import auth, cache, chat as chat_layer, db, entry as entry_mod, funder, models_resolver, projects as projects_mod
|
||||
from . import auth, cache, chat as chat_layer, db, entry as entry_mod, funder, metadata as metadata_mod, models_resolver, projects as projects_mod
|
||||
from .bot import Bot
|
||||
from .config import Config
|
||||
from .gitea import Gitea, GiteaError
|
||||
@@ -40,6 +40,37 @@ log = logging.getLogger(__name__)
|
||||
RFC_FILE_PATH = "RFC.md"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# §22.4a SLICE-4: sidecar-aware body extract/wrap (pure, unit-testable)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _extract_body_pure(rfc, file_contents: str, branch: str, *, is_meta: bool) -> str:
|
||||
"""Editable body of an entry file. Meta-resident files carry a frontmatter
|
||||
envelope (legacy) or are already body-only (migrated, §22.4a); per-RFC repo
|
||||
files are body-only. Dual-read tolerant: a body-only `.md` returns as-is."""
|
||||
if not is_meta:
|
||||
return file_contents
|
||||
return metadata_mod.strip_frontmatter(file_contents)
|
||||
|
||||
|
||||
def _wrap_body_pure(rfc, prior_contents: str, new_body: str, branch: str, *, is_meta: bool) -> str:
|
||||
"""Inverse of `_extract_body_pure`. Under §22.4a the body lives in the `.md`
|
||||
and metadata in the sidecar, so wrapping is identity for body-only files —
|
||||
frontmatter is never re-grown here. A legacy un-migrated meta file still has
|
||||
its metadata in the `.md` frontmatter (no sidecar yet), so preserve it rather
|
||||
than silently dropping it on a pure body edit; it is migrated to body-only on
|
||||
its next *metadata* edit."""
|
||||
nb = new_body if new_body.endswith("\n") else new_body + "\n"
|
||||
if not is_meta:
|
||||
return nb
|
||||
if entry_mod.FRONTMATTER_RE.match(prior_contents):
|
||||
e = entry_mod.parse(prior_contents)
|
||||
e.body = nb
|
||||
return entry_mod.serialize(e)
|
||||
return nb
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request bodies
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1163,28 +1194,12 @@ def make_router(
|
||||
return RFC_FILE_PATH
|
||||
|
||||
def _extract_body(rfc, file_contents: str, branch: str = "main") -> str:
|
||||
"""For super-draft entries (and active-RFC pre-graduation reads
|
||||
per §9.8) the file on disk is the full frontmatter+body envelope;
|
||||
the editable body is entry.body. For active RFCs reading their
|
||||
per-RFC repo the file is just RFC.md and the whole thing is body."""
|
||||
if not _is_meta_target(rfc, branch):
|
||||
return file_contents
|
||||
try:
|
||||
entry = entry_mod.parse(file_contents)
|
||||
except Exception:
|
||||
return file_contents
|
||||
return entry.body
|
||||
return _extract_body_pure(
|
||||
rfc, file_contents, branch, is_meta=_is_meta_target(rfc, branch))
|
||||
|
||||
def _wrap_body(rfc, prior_contents: str, new_body: str, branch: str = "main") -> str:
|
||||
"""Inverse of _extract_body: re-wrap a new body into the entry
|
||||
envelope, preserving the prior frontmatter exactly."""
|
||||
if not _is_meta_target(rfc, branch):
|
||||
return new_body
|
||||
entry = entry_mod.parse(prior_contents)
|
||||
# Ensure exactly one trailing newline so the serializer's
|
||||
# round-trip is stable.
|
||||
entry.body = new_body if new_body.endswith("\n") else new_body + "\n"
|
||||
return entry_mod.serialize(entry)
|
||||
return _wrap_body_pure(
|
||||
rfc, prior_contents, new_body, branch, is_meta=_is_meta_target(rfc, branch))
|
||||
|
||||
async def _refresh_cache_for(rfc) -> None:
|
||||
if _is_meta_resident(rfc):
|
||||
|
||||
@@ -42,7 +42,7 @@ from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from . import auth, cache, db, entry as entry_mod, projects as projects_mod
|
||||
from . import auth, cache, db, entry as entry_mod, metadata as metadata_mod, projects as projects_mod
|
||||
from .bot import Actor, Bot
|
||||
from .config import Config
|
||||
from .gitea import Gitea, GiteaError
|
||||
@@ -341,19 +341,16 @@ def make_router(
|
||||
if _rfc_id_taken(rfc_id, excluding_slug=slug):
|
||||
raise HTTPException(409, f"Integer ID {rfc_id} is already taken")
|
||||
|
||||
# Read the meta-repo entry once — we need the file's sha for the
|
||||
# graduation PR's update_file call and the body to carry through
|
||||
# Dual-read the meta-repo entry once (§22.4a sidecar-aware) — we need its
|
||||
# git state for the graduation commit and the body to carry through
|
||||
# unchanged (meta-only keeps the body in the entry, §13.3).
|
||||
fetched = await gitea.read_file(
|
||||
config.gitea_org, (projects_mod.default_content_repo(config) or ""), f"rfcs/{slug}.md", ref="main",
|
||||
st = await metadata_mod.read_entry_from_git(
|
||||
gitea, config.gitea_org,
|
||||
(projects_mod.default_content_repo(config) or ""), f"rfcs/{slug}.md",
|
||||
)
|
||||
if fetched is None:
|
||||
if st is None:
|
||||
raise HTTPException(409, f"Meta entry rfcs/{slug}.md not found on main")
|
||||
meta_text, meta_sha = fetched
|
||||
try:
|
||||
super_draft_entry = entry_mod.parse(meta_text)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Meta entry malformed: {e}")
|
||||
super_draft_entry = st.entry
|
||||
|
||||
arbiters = json.loads(rfc["arbiters_json"] or "[]") or owners[:1]
|
||||
|
||||
@@ -380,7 +377,8 @@ def make_router(
|
||||
# through graduation rather than dropping them on the rebuild.
|
||||
extra=dict(super_draft_entry.extra),
|
||||
)
|
||||
graduated_contents = entry_mod.serialize(graduated_entry)
|
||||
graduation_files = metadata_mod.write_entry_files(
|
||||
f"rfcs/{slug}.md", graduated_entry, st)
|
||||
|
||||
state = _new_active(
|
||||
slug, rfc_id=rfc_id, owners=owners, arbiters=arbiters,
|
||||
@@ -400,8 +398,7 @@ def make_router(
|
||||
coro = _orchestrate(
|
||||
config=config, gitea=gitea, bot=bot,
|
||||
actor=viewer.as_actor(), state=state,
|
||||
graduated_contents=graduated_contents,
|
||||
meta_file_sha=meta_sha,
|
||||
graduation_files=graduation_files,
|
||||
)
|
||||
if request.query_params.get("_sync") == "1":
|
||||
await coro
|
||||
@@ -481,26 +478,23 @@ def make_router(
|
||||
if already:
|
||||
raise HTTPException(409, f"A claim PR is already open: #{already['pr_number']}")
|
||||
|
||||
fetched = await gitea.read_file(
|
||||
config.gitea_org, (projects_mod.default_content_repo(config) or ""), f"rfcs/{slug}.md", ref="main",
|
||||
st = await metadata_mod.read_entry_from_git(
|
||||
gitea, config.gitea_org,
|
||||
(projects_mod.default_content_repo(config) or ""), f"rfcs/{slug}.md",
|
||||
)
|
||||
if fetched is None:
|
||||
if st is None:
|
||||
raise HTTPException(409, f"Meta entry rfcs/{slug}.md not found on main")
|
||||
meta_text, meta_sha = fetched
|
||||
try:
|
||||
ent = entry_mod.parse(meta_text)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Meta entry malformed: {e}")
|
||||
if viewer.gitea_login in ent.owners:
|
||||
if viewer.gitea_login in st.entry.owners:
|
||||
return {"ok": True, "noop": True}
|
||||
ent.owners = ent.owners + [viewer.gitea_login]
|
||||
new_contents = entry_mod.serialize(ent)
|
||||
ent = metadata_mod.apply_values(
|
||||
st.entry, {"owners": st.entry.owners + [viewer.gitea_login]})
|
||||
files = metadata_mod.write_entry_files(f"rfcs/{slug}.md", ent, st)
|
||||
try:
|
||||
pr = await bot.open_claim_pr(
|
||||
viewer.as_actor(),
|
||||
org=config.gitea_org, meta_repo=(projects_mod.default_content_repo(config) or ""),
|
||||
slug=slug,
|
||||
new_file_contents=new_contents, prior_sha=meta_sha,
|
||||
files=files,
|
||||
)
|
||||
except GiteaError as e:
|
||||
raise HTTPException(502, f"Gitea: {e.detail}")
|
||||
@@ -527,11 +521,12 @@ def make_router(
|
||||
403, "Only this RFC's owners or a site owner may retire it"
|
||||
)
|
||||
prior_state = rfc["state"]
|
||||
entry, sha = await _read_meta_entry(slug)
|
||||
entry.state = "retired"
|
||||
st = await _read_meta_entry(slug)
|
||||
entry = metadata_mod.apply_values(st.entry, {"state": "retired"})
|
||||
files = metadata_mod.write_entry_files(f"rfcs/{slug}.md", entry, st)
|
||||
await _run_state_flip(
|
||||
config=config, gitea=gitea, bot=bot, actor=viewer.as_actor(),
|
||||
slug=slug, new_contents=entry_mod.serialize(entry), prior_sha=sha,
|
||||
slug=slug, files=files,
|
||||
verb="retire", target_state="retired",
|
||||
)
|
||||
_audit(
|
||||
@@ -557,11 +552,12 @@ def make_router(
|
||||
raise HTTPException(403, "Only a site owner may un-retire an RFC")
|
||||
_require_retired(slug)
|
||||
restored = _prior_state_before_retire(slug)
|
||||
entry, sha = await _read_meta_entry(slug)
|
||||
entry.state = restored
|
||||
st = await _read_meta_entry(slug)
|
||||
entry = metadata_mod.apply_values(st.entry, {"state": restored})
|
||||
files = metadata_mod.write_entry_files(f"rfcs/{slug}.md", entry, st)
|
||||
await _run_state_flip(
|
||||
config=config, gitea=gitea, bot=bot, actor=viewer.as_actor(),
|
||||
slug=slug, new_contents=entry_mod.serialize(entry), prior_sha=sha,
|
||||
slug=slug, files=files,
|
||||
verb="unretire", target_state=restored,
|
||||
)
|
||||
_audit(
|
||||
@@ -602,17 +598,17 @@ def make_router(
|
||||
raise HTTPException(409, f"RFC is {row['state']}, not retired")
|
||||
return row
|
||||
|
||||
async def _read_meta_entry(slug: str) -> tuple[entry_mod.Entry, str]:
|
||||
fetched = await gitea.read_file(
|
||||
config.gitea_org, (projects_mod.default_content_repo(config) or ""), f"rfcs/{slug}.md", ref="main",
|
||||
async def _read_meta_entry(slug: str):
|
||||
"""Dual-read an entry from meta-main → EntryGitState (sidecar-aware,
|
||||
§22.4a). A migrated body-only `.md` reads cleanly; never raises on bad
|
||||
metadata (INV-3)."""
|
||||
st = await metadata_mod.read_entry_from_git(
|
||||
gitea, config.gitea_org,
|
||||
(projects_mod.default_content_repo(config) or ""), f"rfcs/{slug}.md",
|
||||
)
|
||||
if fetched is None:
|
||||
if st is None:
|
||||
raise HTTPException(409, f"Meta entry rfcs/{slug}.md not found on main")
|
||||
text, file_sha = fetched
|
||||
try:
|
||||
return entry_mod.parse(text), file_sha
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Meta entry malformed: {e}")
|
||||
return st
|
||||
|
||||
async def _refresh_catalog() -> None:
|
||||
# Inline refresh so the catalog reflects the flip immediately; the
|
||||
@@ -640,8 +636,7 @@ async def _orchestrate(
|
||||
bot: Bot,
|
||||
actor: Actor,
|
||||
state: GraduationState,
|
||||
graduated_contents: str,
|
||||
meta_file_sha: str,
|
||||
graduation_files: list[dict],
|
||||
) -> None:
|
||||
"""Open the flip PR, then merge it. Two steps, no transaction:
|
||||
|
||||
@@ -661,8 +656,7 @@ async def _orchestrate(
|
||||
actor,
|
||||
org=config.gitea_org, meta_repo=(projects_mod.default_content_repo(config) or ""),
|
||||
slug=state.slug,
|
||||
new_file_contents=graduated_contents,
|
||||
prior_sha=meta_file_sha,
|
||||
files=graduation_files,
|
||||
rfc_id=state.rfc_id,
|
||||
owners=state.owners,
|
||||
)
|
||||
@@ -850,12 +844,12 @@ async def _run_state_flip(
|
||||
bot: Bot,
|
||||
actor: Actor,
|
||||
slug: str,
|
||||
new_contents: str,
|
||||
prior_sha: str,
|
||||
files: list[dict],
|
||||
verb: str,
|
||||
target_state: str,
|
||||
) -> None:
|
||||
"""§13.7: open + merge a retire / un-retire frontmatter flip PR. Runs
|
||||
"""§13.7: open + merge a retire / un-retire state-flip PR. The flip is
|
||||
written to the entry's metadata sidecar (§22.4a) via `files` ops. Runs
|
||||
inline (no SSE — the flip is a single quick state change, unlike the
|
||||
multi-step graduation that streams progress). On an open failure
|
||||
nothing was created; on a merge failure the half-open PR/branch is
|
||||
@@ -865,7 +859,7 @@ async def _run_state_flip(
|
||||
pr = await bot.open_retire_flip_pr(
|
||||
actor,
|
||||
org=config.gitea_org, meta_repo=(projects_mod.default_content_repo(config) or ""),
|
||||
slug=slug, new_file_contents=new_contents, prior_sha=prior_sha,
|
||||
slug=slug, files=files,
|
||||
verb=verb, target_state=target_state,
|
||||
)
|
||||
except GiteaError as e:
|
||||
|
||||
+14
-9
@@ -23,7 +23,7 @@ from typing import Any
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from . import auth, cache, chat as chat_layer, db, entry as entry_mod, funder, models_resolver, projects as projects_mod, rfc_links
|
||||
from . import auth, cache, chat as chat_layer, db, entry as entry_mod, funder, metadata as metadata_mod, models_resolver, projects as projects_mod, rfc_links
|
||||
from .bot import Bot
|
||||
from .config import Config
|
||||
from .gitea import Gitea, GiteaError
|
||||
@@ -1009,20 +1009,25 @@ async def _replay_changes(
|
||||
|
||||
|
||||
def _extract_body_for_replay(is_super_draft: bool, content: str) -> str:
|
||||
# §22.4a SLICE-4: a meta-resident entry may be legacy (frontmatter+body) or
|
||||
# migrated (body-only). strip_frontmatter handles both without raising.
|
||||
if not is_super_draft:
|
||||
return content
|
||||
try:
|
||||
return entry_mod.parse(content).body
|
||||
except Exception:
|
||||
return content
|
||||
return metadata_mod.strip_frontmatter(content)
|
||||
|
||||
|
||||
def _wrap_body_for_replay(is_super_draft: bool, prior_content: str, new_body: str) -> str:
|
||||
# §22.4a SLICE-4: identity for body-only (migrated) files — never re-grow
|
||||
# frontmatter; preserve a legacy file's frontmatter until its next metadata
|
||||
# edit migrates it.
|
||||
nb = new_body if new_body.endswith("\n") else new_body + "\n"
|
||||
if not is_super_draft:
|
||||
return new_body
|
||||
entry = entry_mod.parse(prior_content)
|
||||
entry.body = new_body if new_body.endswith("\n") else new_body + "\n"
|
||||
return entry_mod.serialize(entry)
|
||||
return nb
|
||||
if entry_mod.FRONTMATTER_RE.match(prior_content):
|
||||
entry = entry_mod.parse(prior_content)
|
||||
entry.body = nb
|
||||
return entry_mod.serialize(entry)
|
||||
return nb
|
||||
|
||||
|
||||
def _resolution_branch_name(original_branch: str) -> str:
|
||||
|
||||
+48
-77
@@ -856,35 +856,29 @@ class Bot:
|
||||
org: str,
|
||||
meta_repo: str,
|
||||
slug: str,
|
||||
new_file_contents: str,
|
||||
prior_sha: str,
|
||||
files: list[dict],
|
||||
rfc_id: str | None,
|
||||
owners: list[str],
|
||||
) -> dict:
|
||||
"""§13.3 (meta-only): open a PR against the meta repo that flips the
|
||||
entry's frontmatter to `state: active` with the graduation stamps
|
||||
and — **optionally** — the integer `id`, **keeping the body
|
||||
unchanged** (§1 meta-only topology; no repo is created and no body
|
||||
is stripped). When `rfc_id` is None the entry graduates without a
|
||||
number (id stays null, slug is canonical per §2.3, §13.2). Branch
|
||||
name uses the `graduate-<slug>-<6hex>` shape — dash-separated like
|
||||
the other meta-repo branches per the §19.2 path-routing candidate.
|
||||
entry to `state: active` with the graduation stamps and — **optionally**
|
||||
— the integer `id`, **keeping the body unchanged** (§1 meta-only
|
||||
topology; no repo is created and no body is stripped). The graduation
|
||||
metadata is written to the entry's sidecar (§22.4a) via `files`; a legacy
|
||||
`.md` is lazy-migrated to body-only in the same commit. When `rfc_id` is
|
||||
None the entry graduates without a number (id stays null, slug is
|
||||
canonical per §2.3, §13.2). Branch name uses the `graduate-<slug>-<6hex>`
|
||||
shape — dash-separated like the other meta-repo branches per the §19.2
|
||||
path-routing candidate.
|
||||
"""
|
||||
import secrets
|
||||
|
||||
branch = f"graduate-{slug}-{secrets.token_hex(3)}"
|
||||
await self._gitea.create_branch(org, meta_repo, branch, from_branch="main")
|
||||
ae = actor.email or f"{actor.gitea_login}@users.noreply"
|
||||
commit_subject = f"Graduate {slug} → {rfc_id}" if rfc_id else f"Graduate {slug} (no number)"
|
||||
commit_message = _stamp_single(commit_subject, actor)
|
||||
result = await self._gitea.update_file(
|
||||
org, meta_repo, f"rfcs/{slug}.md",
|
||||
content=new_file_contents,
|
||||
sha=prior_sha,
|
||||
message=commit_message,
|
||||
branch=branch,
|
||||
author_name=actor.display_name, author_email=ae,
|
||||
)
|
||||
result = await self.commit_entry_files(
|
||||
actor, org=org, repo=meta_repo, files=files,
|
||||
message=commit_subject, branch=branch)
|
||||
commit_sha = (
|
||||
result.get("commit", {}).get("sha")
|
||||
or result.get("content", {}).get("sha")
|
||||
@@ -973,35 +967,26 @@ class Bot:
|
||||
org: str,
|
||||
meta_repo: str,
|
||||
slug: str,
|
||||
new_file_contents: str,
|
||||
prior_sha: str,
|
||||
files: list[dict],
|
||||
verb: str,
|
||||
target_state: str,
|
||||
) -> dict:
|
||||
"""§13.7: open a PR flipping `rfcs/<slug>.md` to `state:
|
||||
<target_state>` — for retire (`verb='retire'`, target `retired`)
|
||||
or un-retire (`verb='unretire'`, target the restored prior state).
|
||||
Only the frontmatter `state` changes; the body and every other
|
||||
field (including the integer `id`) are kept, so an un-retire
|
||||
restores the entry exactly. Branch shape mirrors graduation's
|
||||
`<verb>-<slug>-<6hex>`.
|
||||
"""§13.7: open a PR flipping an entry to `state: <target_state>` — for
|
||||
retire (`verb='retire'`, target `retired`) or un-retire
|
||||
(`verb='unretire'`, target the restored prior state). The `state` change
|
||||
is written to the entry's metadata sidecar (§22.4a), keeping the `.md`
|
||||
body and every other field, so an un-retire restores the entry exactly.
|
||||
`files` come from `metadata.write_entry_files`. Branch shape mirrors
|
||||
graduation's `<verb>-<slug>-<6hex>`.
|
||||
"""
|
||||
import secrets
|
||||
|
||||
branch = f"{verb}-{slug}-{secrets.token_hex(3)}"
|
||||
await self._gitea.create_branch(org, meta_repo, branch, from_branch="main")
|
||||
ae = actor.email or f"{actor.gitea_login}@users.noreply"
|
||||
verb_title = "Retire" if verb == "retire" else "Un-retire"
|
||||
commit_subject = f"{verb_title} {slug}"
|
||||
commit_message = _stamp_single(commit_subject, actor)
|
||||
result = await self._gitea.update_file(
|
||||
org, meta_repo, f"rfcs/{slug}.md",
|
||||
content=new_file_contents,
|
||||
sha=prior_sha,
|
||||
message=commit_message,
|
||||
branch=branch,
|
||||
author_name=actor.display_name, author_email=ae,
|
||||
)
|
||||
result = await self.commit_entry_files(
|
||||
actor, org=org, repo=meta_repo, files=files,
|
||||
message=f"{verb_title} {slug}", branch=branch)
|
||||
commit_sha = (
|
||||
result.get("commit", {}).get("sha")
|
||||
or result.get("content", {}).get("sha")
|
||||
@@ -1171,30 +1156,23 @@ class Bot:
|
||||
org: str,
|
||||
meta_repo: str,
|
||||
slug: str,
|
||||
new_file_contents: str,
|
||||
prior_sha: str,
|
||||
files: list[dict],
|
||||
) -> dict:
|
||||
"""§13.1: open a PR adding the actor to the entry's `owners:` list.
|
||||
|
||||
Touches only the frontmatter of `rfcs/<slug>.md`. Branch shape is
|
||||
`claim/<slug>` — single attempt per super-draft per actor (Gitea
|
||||
refuses duplicate branch creation, which is the right behavior:
|
||||
if the claim is still open, point the contributor at the existing
|
||||
PR rather than opening a second one).
|
||||
Writes the updated `owners:` to the entry's metadata sidecar (§22.4a)
|
||||
via `files`; a legacy `.md` is lazy-migrated to body-only in the same
|
||||
commit. Branch shape is `claim/<slug>` — single attempt per super-draft
|
||||
per actor (Gitea refuses duplicate branch creation, which is the right
|
||||
behavior: if the claim is still open, point the contributor at the
|
||||
existing PR rather than opening a second one).
|
||||
"""
|
||||
branch = f"claim/{slug}"
|
||||
await self._gitea.create_branch(org, meta_repo, branch, from_branch="main")
|
||||
ae = actor.email or f"{actor.gitea_login}@users.noreply"
|
||||
commit_subject = f"Claim ownership of {slug} for {actor.gitea_login}"
|
||||
commit_message = _stamp_single(commit_subject, actor)
|
||||
result = await self._gitea.update_file(
|
||||
org, meta_repo, f"rfcs/{slug}.md",
|
||||
content=new_file_contents,
|
||||
sha=prior_sha,
|
||||
message=commit_message,
|
||||
branch=branch,
|
||||
author_name=actor.display_name, author_email=ae,
|
||||
)
|
||||
result = await self.commit_entry_files(
|
||||
actor, org=org, repo=meta_repo, files=files,
|
||||
message=commit_subject, branch=branch)
|
||||
commit_sha = (
|
||||
result.get("commit", {}).get("sha")
|
||||
or result.get("content", {}).get("sha")
|
||||
@@ -1233,29 +1211,22 @@ class Bot:
|
||||
reviewed_by: str,
|
||||
reviewed_at: str,
|
||||
) -> None:
|
||||
"""Clear §22.4c unreviewed on an active entry by rewriting its
|
||||
frontmatter on main. Stamps the commit with the §6.5 On-behalf-of
|
||||
trailer and writes an actions-log row, mirroring the graduation
|
||||
stamp's bot-write shape."""
|
||||
"""Clear §22.4c unreviewed on an active entry by writing its metadata
|
||||
sidecar on main (§22.4a). Dual-reads the entry (so a migrated body-only
|
||||
`.md` doesn't crash) and lazy-migrates a legacy `.md` to body-only in the
|
||||
same commit. Stamps the §6.5 On-behalf-of trailer and writes an
|
||||
actions-log row, mirroring the graduation stamp's bot-write shape."""
|
||||
path = f"rfcs/{slug}.md"
|
||||
result = await self._gitea.read_file(org, meta_repo, path, ref="main")
|
||||
if result is None:
|
||||
st = await metadata_mod.read_entry_from_git(self._gitea, org, meta_repo, path)
|
||||
if st is None:
|
||||
raise GiteaError(404, f"{path} not found")
|
||||
text, sha = result
|
||||
e = entry_mod.parse(text)
|
||||
e.unreviewed = False
|
||||
e.reviewed_at = reviewed_at
|
||||
e.reviewed_by = reviewed_by
|
||||
commit_message = _stamp_single(f"Mark {slug} reviewed", actor)
|
||||
result = await self._gitea.update_file(
|
||||
org, meta_repo, path,
|
||||
content=entry_mod.serialize(e),
|
||||
sha=sha,
|
||||
message=commit_message,
|
||||
branch="main",
|
||||
author_name=actor.display_name,
|
||||
author_email=actor.email or f"{actor.gitea_login}@users.noreply",
|
||||
)
|
||||
e = metadata_mod.apply_values(st.entry, {
|
||||
"unreviewed": False, "reviewed_at": reviewed_at, "reviewed_by": reviewed_by,
|
||||
})
|
||||
files = metadata_mod.write_entry_files(path, e, st)
|
||||
result = await self.commit_entry_files(
|
||||
actor, org=org, repo=meta_repo, files=files,
|
||||
message=f"Mark {slug} reviewed", branch="main")
|
||||
commit_sha = (
|
||||
result.get("commit", {}).get("sha")
|
||||
or result.get("content", {}).get("sha")
|
||||
|
||||
Reference in New Issue
Block a user