Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.BOSUN_GH_TOKEN || secrets.GITHUB_TOKEN }}
BOSUN_CACHE: .bosun-cache
BOSUN_CACHE_TTL: '259200' # 3 days; keeps daily runs under the token limit
BOSUN_CACHE_TTL: '86400' # 1 day; refresh ACKs each daily run (rate-limited fetches are caught + retried next run)
run: python -m bosun.static_build --ingest -o public

- name: Upload Pages artifact
Expand Down
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ you can see at a glance where each PR stands across `bitcoin/bitcoin`,
The core (`spec` / `source` / `github` / `ack` / `suggest` / `report` / `daggy`)
is pure stdlib; only the web UI needs Flask.

Live instance: <https://bosun.privkey.io>

## Web app

```bash
Expand All @@ -19,10 +21,14 @@ python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python -m bosun.web --repo me/tmp --ref my-specs # default a fork
```

Pick a spec from the dropdown (defaults to `luke-jr/tmp` @ `knots-spec`), or set
`repo`/`ref` to browse your own fork. Click the legend chips to filter by state
or canonical bucket, click column headers to sort, and **fetch GitHub status
(shown)** to pull live PR state + 0-3 review level for the visible rows.
Pick a spec from the dropdown (defaults to
[`luke-jr/tmp` @ `knots-spec`](https://github.com/luke-jr/tmp/tree/knots-spec)), or
choose **★ open Knots PRs** to triage every open PR in `bitcoinknots/bitcoin`,
including ones not yet in any spec. Filter by source (Core/Knots/GUI), GitHub
labels, review level, or the **ready** quick-filter (open + tested ACK, no NACK);
sort by any column; and **fetch GitHub status (shown)** to pull live PR state and
the 0-3 review level for the visible rows. Or set `repo`/`ref` to browse your own
fork.

Auth uses `GITHUB_TOKEN`, falling back to `gh auth token`; without either the
GitHub limit is 60 requests/hour. Bulk-prefetch a whole spec into the cache:
Expand All @@ -44,7 +50,8 @@ python3 -m http.server -d public 8000 # preview at :8000
```

Deploy is via `.github/workflows/deploy.yml` (push, daily cron, manual dispatch).
Add published specs in `SPECS` in `bosun/static_build.py`.
The open-Knots-PRs view is baked automatically; add more published specs in
`SPECS` in `bosun/static_build.py`.

## Maintenance digest (markdown, for the terminal)

Expand All @@ -58,9 +65,10 @@ python3 -m bosun.report --file knots-next-29.spec -o digest.md

## Daggy-fix helper (for the traditional-git model)

For the proposed DAG-native Knots model, the core operation is: develop a fix on
master, rebase it back to the oldest affected commit, then merge it forward into
each live branch. This automates the two parts that aren't trivial:
For the [proposed DAG-native Knots model](https://gist.github.com/chrisguida/65337b84ffb4acbd09aa5ba073b55d00),
the core operation is: develop a fix on master, rebase it back to the oldest
affected commit, then merge it forward into each live branch. This automates the
two parts that aren't trivial:

```bash
# Where do I rebase back to? Bisect to the bug-introducing commit using a
Expand Down
22 changes: 22 additions & 0 deletions bosun/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,28 @@ def pr_url(prnum: str | None) -> str | None:
return f"https://github.com/{r[0]}/pull/{r[1]}" if r else None


def list_open_prs(repo: str) -> list[dict]:
"""All open (non-draft) PRs in repo, slimmed to what the UI needs. One API
call per 100 PRs; honours GITHUB_TOKEN for rate limits. May raise RateLimited."""
out: list[dict] = []
page = 1
while True:
prs = _get(f"{API}/repos/{repo}/pulls?state=open&per_page=100&page={page}")
for pr in prs:
if pr.get("draft"):
continue
out.append({
"num": pr["number"],
"title": pr.get("title"),
"updated_at": pr.get("updated_at"),
"labels": [lb["name"] for lb in pr.get("labels", [])],
})
if len(prs) < 100:
break
page += 1
return out


def _token() -> str | None:
global _token_cache
if _token_cache is not None:
Expand Down
26 changes: 25 additions & 1 deletion bosun/static_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from . import source, suggest
from .github import _read_cache, pr_status, pr_url, resolve_pr
from .spec import parse_spec
from .web import _PAGE
from .web import _PAGE, KNOTS_REPO, knots_rows

# Specs published on the site. Add entries to publish more.
SPECS = [
Expand Down Expand Up @@ -66,6 +66,28 @@ def _ingest(entries) -> None:
pass


def _build_knots(data, ingest: bool) -> dict:
"""Bake the "open Knots PRs" pseudo-spec (data/__knots__.*) and return its
index entry. Same JSON shape as a spec so the frontend needs no new loader."""
rows = knots_rows(KNOTS_REPO)
if ingest:
for r in rows:
try:
pr_status(KNOTS_REPO, r["lineno"])
except Exception:
pass
rows = knots_rows(KNOTS_REPO) # re-read with the cache now warm
cats = suggest.categorize(rows)
slim = {k: [{f: e.get(f) for f in _SUG_FIELDS} for e in v]
for k, v in cats.items() if isinstance(v, list)}
(data / "__knots__.json").write_text(json.dumps({"file": "__knots__", "entries": rows}))
(data / "__knots__.suggest.json").write_text(
json.dumps({"known": cats["known"], "total": cats["total"], **slim}))
(data / "__knots__.cleanup.diff").write_text("# not applicable to the PR list\n")
return {"slug": "__knots__", "title": "★ open Knots PRs", "repo": KNOTS_REPO,
"ref": "-", "file": "__knots__", "merges": len(rows), "known": cats["known"]}


_STATIC_CSS = """
#url, #go, details.src, #auto, #fetchgh, #refresh { display: none !important; }
.genat { opacity: .6; font-size: .78rem; margin-left: .5rem; }
Expand All @@ -76,6 +98,7 @@ def _ingest(entries) -> None:
# (render, filters, legend, insights, sugEntry, ...) is reused unchanged.
_STATIC_TAIL = r"""loadEntries = async function () {
const slug = $("#spec").value; if (!slug) return;
VIEW = slug === "__knots__" ? "prs" : "spec";
$("#status").textContent = "loading…";
try {
const j = await (await fetch("data/" + slug + ".json")).json();
Expand Down Expand Up @@ -164,6 +187,7 @@ def build(outdir: str, ingest: bool = False) -> None:
"merges": len(merges), "known": cats["known"],
})

index["specs"].insert(0, _build_knots(data, ingest))
(data / "index.json").write_text(json.dumps(index))
(out / "index.html").write_text(_static_html())
(out / "CNAME").write_text(CNAME + "\n")
Expand Down
Loading
Loading