Files
marketplace/skills/wiki/scripts/wiki_push.py
T
naudachu 596cf853e8 fix: keep request payloads out of the issue store
`labels.py` handed `_gitea.api` the issue store as a place to put the
request file, and on a checkout without a store that quietly created
`tmp/issues/.payload/`. Bootstrapping a repository's labels touches no
issue at all, so the one rule the store has — nothing materializes it as
a side effect of a write — was broken by an operation that has no
business knowing the store exists.

Where a request body goes was never the caller's decision to make. It is
now the transport's: `tmp/payload/`, resolved from `_gitea.py`'s own
location the way both domains resolve theirs, so every caller — sync and
wiki alike — writes to one directory whatever it was invoked from, and
`out_root` is gone from `api`, `add_dependency` and all six call sites.
The directory is created by the first write of a run and not before: a
`--dry-run` leaves nothing behind. `tmp/` is already gitignored.

The name carries the distinction the old path lost. A store holds the
only copy of something; this holds debris kept for a retry or a
post-mortem, and deleting it costs nothing. A dotdir sitting among an
issue's files claimed otherwise, and `ls tmp/issues` started lying about
what existed.

tests/test_payload_root.py runs the real `labels.py` in a throwaway repo
against a fake `tea` on PATH: no store appears, the payloads land in
tmp/payload/, a dry run writes nothing, and a run from a subdirectory
still resolves to the repo root. Two source checks keep the callers from
drifting apart again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 17:25:33 +05:00

152 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""
wiki_push.py — publish a local space to a wiki.
wiki_push.py -m "Import the simple-chains discussion"
wiki_push.py --prefix "Simple Chains/Ideas" -m "Rework B-04"
wiki_push.py -m "Fix the send-timing table" Simple-Chains/Ideas/Send-timing.md
Every page in the selection is compared against `pushed` — the hash of what was
last published — and only the ones that differ are sent. That is the whole of
change detection: no timestamps, no drift model.
A page with no `sub_url` is created; a page with one is edited in place. The
title comes from the manifest, never re-derived from the file, because sending
a different title to the edit endpoint is a RENAME and leaves nothing behind at
the old address.
Pushing is additive. A page deleted locally is NOT deleted in the wiki — the
manifest simply stops mentioning it. Removing a published page is an explicit
act; do it in the web UI or with a DELETE through /tea:use.
Usage:
wiki_push.py -m MESSAGE [--space SPACE] [--repo owner/repo]
[--prefix TITLE] [--dry-run] [--out DIR] [PATH-or-TITLE ...]
"""
import argparse
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
sys.path.insert(0, os.path.join(_HERE, "..", "..", "sync", "scripts"))
sys.path.insert(0, os.path.join(_HERE, "..", "..", "page", "scripts"))
import _gitea # noqa: E402
import page # noqa: E402
import wikimap # noqa: E402
def select(manifest, prefix, targets):
"""The pages to consider, in tree order.
A positional argument matches a manifest path or a title, exactly. Exact
because a near-miss that silently selects nothing is indistinguishable from
a clean no-op run, and the operator finds out only when the page never
appears."""
pages = (page.children_of(manifest, prefix) if prefix
else page.sorted_pages(manifest))
if not targets:
return pages, []
want, chosen, hit = set(targets), [], set()
for p, e in pages:
if p in want or e.get("title") in want:
chosen.append((p, e))
hit.add(p if p in want else e.get("title"))
return chosen, sorted(want - hit)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("targets", nargs="*", metavar="PATH-or-TITLE")
ap.add_argument("-m", "--message", required=True,
help="wiki commit message for this push")
ap.add_argument("--repo", help="owner/repo (default: the repo in CWD)")
ap.add_argument("--space", help="local space (default: the owner/repo slug)")
ap.add_argument("--prefix", default="", help="only titles at or under this")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--out", help="wiki cache root (default: <repo>/tmp/wiki)")
a = ap.parse_args()
login = _gitea.require_login()
base = _gitea.repo_base(a.repo)
slug = _gitea.repo_slug(login, a.repo)
space = a.space or slug
root = a.out or page.WIKI_ROOT
space_dir = page.space_root(space, root)
if not os.path.isdir(space_dir):
_gitea.die("no such space: %s (looked in %s). Import or pull first."
% (space, space_dir))
manifest = page.load_manifest(space, root)
if not manifest["pages"]:
_gitea.die("space %s has no pages in its manifest" % space)
chosen, missing = select(manifest, a.prefix, a.targets)
for t in missing:
_gitea.warn("not in the manifest: %s" % t)
if not chosen:
_gitea.die("nothing selected")
created = updated = skipped = 0
for relpath, e in chosen:
title = e.get("title")
full = os.path.join(space_dir, relpath)
if not title:
_gitea.warn("%s has no title in the manifest; skipped" % relpath)
continue
if not os.path.isfile(full):
_gitea.warn("%s is in the manifest but not on disk; skipped" % relpath)
continue
with open(full, encoding="utf-8") as f:
text = f.read()
h = page.body_hash(text)
if e.get("sub_url") and h == e.get("pushed"):
skipped += 1
continue
verb = "create" if not e.get("sub_url") else "update"
print("%-7s %-44s %s" % (verb, relpath, title))
if a.dry_run:
created += verb == "create"
updated += verb == "update"
continue
if verb == "create":
payload = wikimap.new_payload(title, text, a.message)
got = _gitea.api(login, "%s/wiki/new" % base, method="POST",
payload=payload, payload_name="wiki-new")
else:
payload = wikimap.edit_payload(title, text, a.message)
got = _gitea.api(login, wikimap.page_endpoint(base, e["sub_url"]),
method="PATCH", payload=payload,
payload_name="wiki-edit")
if not isinstance(got, dict) or not got.get("sub_url"):
_gitea.warn("%s: no page returned; the manifest is unchanged for it"
% title)
continue
# sub_url comes back from Gitea and is stored as given. It is the only
# address this page has, and it is not something we could have computed.
e.update(wikimap.from_payload(got))
e["synced"] = _gitea.now_iso()
e["pushed"] = h
manifest["pages"][relpath] = e
created += verb == "create"
updated += verb == "update"
if a.dry_run:
print("\ndry run — %d to create, %d to update, %d unchanged"
% (created, updated, skipped))
return 0
page.save_manifest(manifest, root)
print("\n%d created, %d updated, %d unchanged -> %s wiki"
% (created, updated, skipped, slug))
return 0
if __name__ == "__main__":
sys.exit(main())