Author:
Hash:
Timestamp:
+177 -36 +/-7 browse
Kevin Schoon [me@kevinschoon.com]
fcc1ff23f92e5308ae954f5a7044f3ab8a7393aa
Fri, 28 Nov 2025 19:50:08 +0000 (1 day ago)
| 1 | diff --git a/assets/ayllu-0.5-demo.mp4 b/assets/ayllu-0.5-demo.mp4 |
| 2 | new file mode 100644 |
| 3 | index 0000000..55b43cb |
| 4 | Binary files /dev/null and b/assets/ayllu-0.5-demo.mp4 differ |
| 5 | diff --git a/assets/style.xsl b/assets/style.xsl |
| 6 | index 10b5101..2b45a6a 100644 |
| 7 | --- a/assets/style.xsl |
| 8 | +++ b/assets/style.xsl |
| 9 | @@ -1,15 +1,13 @@ |
| 10 | <?xml version="1.0" encoding="utf-8"?> |
| 11 | <xsl:stylesheet version="3.0" |
| 12 | xmlns:xsl="http://www.w3.org/1999/XSL/Transform" |
| 13 | - xmlns:atom="http://www.w3.org/2005/Atom" |
| 14 | - xmlns:dc="http://purl.org/dc/elements/1.1/" |
| 15 | - xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"> |
| 16 | + xmlns:atom="http://www.w3.org/2005/Atom"> |
| 17 | <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/> |
| 18 | <xsl:template match="/"> |
| 19 | <html lang="en"> |
| 20 | <head> |
| 21 | <title> |
| 22 | - <xsl:value-of select="/rss/channel/title"/> |
| 23 | + <xsl:value-of select="atom:feed/atom:title"/> |
| 24 | </title> |
| 25 | <link rel="stylesheet" href="/main.css" /> |
| 26 | </head> |
| 27 | @@ -18,29 +16,28 @@ |
| 28 | <article> |
| 29 | <header> |
| 30 | <h1> |
| 31 | - <img class="me" src="/ks_stylized.png"/> |
| 32 | - <a> |
| 33 | - <xsl:attribute name="href"> <xsl:value-of select="/rss/channel/link"/> </xsl:attribute> |
| 34 | - </a> |
| 35 | + <a href="/"><img class="me" src="/ks_stylized.png"/></a> |
| 36 | </h1> |
| 37 | - <h2><xsl:value-of select="/rss/channel/description"/></h2> |
| 38 | + <h2><xsl:value-of select="atom:feed/atom:title"/></h2> |
| 39 | </header> |
| 40 | - <p><strong>This is an <a href="https://en.wikipedia.org/wiki/RSS">RSS feed</a>.</strong></p> |
| 41 | + <p><strong>This is an <a href="https://en.wikipedia.org/wiki/Atom_(web_standard)">Atom feed</a>.</strong></p> |
| 42 | <p>Copy the link from the address bar into your feed reader to receive regular updates.</p> |
| 43 | </article> |
| 44 | - <xsl:for-each select="/rss/channel/item"> |
| 45 | + <xsl:for-each select="atom:feed/atom:entry"> |
| 46 | <article class="card"> |
| 47 | <header> |
| 48 | <h3> |
| 49 | <a> |
| 50 | <xsl:attribute name="href"> |
| 51 | - <xsl:value-of select="link"/> |
| 52 | + <xsl:value-of select="atom:link/@href"/> |
| 53 | </xsl:attribute> |
| 54 | - <xsl:value-of select="title"/> |
| 55 | + <xsl:value-of select="atom:title"/> |
| 56 | </a> |
| 57 | </h3> |
| 58 | </header> |
| 59 | - <xsl:value-of select="description" disable-output-escaping="yes"/> |
| 60 | + <div> |
| 61 | + <xsl:value-of select="atom:content" disable-output-escaping="yes"/> |
| 62 | + </div> |
| 63 | <footer class="rss"> |
| 64 | <xsl:value-of select="author" /><br/> |
| 65 | <b><xsl:value-of select="pubDate"/></b> |
| 66 | diff --git a/content/blog/announcing-ayllu-0.5/README.md b/content/blog/announcing-ayllu-0.5/README.md |
| 67 | new file mode 100644 |
| 68 | index 0000000..7192b45 |
| 69 | --- /dev/null |
| 70 | +++ b/content/blog/announcing-ayllu-0.5/README.md |
| 71 | @@ -0,0 +1,96 @@ |
| 72 | + # Ayllu 0.5 Has Been Released! |
| 73 | + |
| 74 | + `11/21/2025` |
| 75 | + |
| 76 | + <br /> |
| 77 | + |
| 78 | + Hello again and welcome to another sporadic set of release notes for |
| 79 | + Ayllu. This began as a fun experiment for me to hack together a |
| 80 | + development environment which was setup the way I wanted it and also |
| 81 | + as an excuse to learn Rust. It continues to be a fun side project and |
| 82 | + staging ground for basically programming things I find interesting. |
| 83 | + There are no shortages of code forges |
| 84 | + |
| 85 | + |
| 86 | + ## Identities in Ayllu |
| 87 | + |
| 88 | + Most of the popular forges work like traditional web apps: you sign-up |
| 89 | + with an e-mail address and then take on the persona of a developer with |
| 90 | + ability to push and pull in a tightly controlled environment. The ability |
| 91 | + to push code via SSH is usually available via a highly restricted shell |
| 92 | + environment where you can authenticate via public keys and then typically |
| 93 | + run only the commands needed to push or pull from a repository. Ayllu |
| 94 | + instead provides a management TUI interface over SSH as well as support |
| 95 | + for delegation to normal system users. |
| 96 | + |
| 97 | + |
| 98 | + ```toml |
| 99 | + [[identities]] |
| 100 | + username = "hello" |
| 101 | + email = "world@example.org" |
| 102 | + avatar = {} |
| 103 | + tagline = "...." |
| 104 | + authorized_keys = [...] |
| 105 | + gpg_keys = [...] |
| 106 | + [[identities.profiles]] |
| 107 | + url = "" |
| 108 | + ``` |
| 109 | + |
| 110 | + <video controls> <source src="/ayllu-0.5-demo.mp4" type="video/mp4" /> </video> |
| 111 | + |
| 112 | + #### New Binaries |
| 113 | + |
| 114 | + ##### ayllu-keys |
| 115 | + |
| 116 | + The `ayllu-keys` command is a shim for authorizing user access via OpenSSH's |
| 117 | + `AuthorizedKeysCommand`. |
| 118 | + |
| 119 | + ##### ayllu-shell |
| 120 | + |
| 121 | + The `ayllu-shell` command implements a restricted shell environment for |
| 122 | + management of Ayllu related tasks. It's also responsble for delegating |
| 123 | + calls to `git-receive-pack` and `git-upload-pack`. |
| 124 | + |
| 125 | + Here is a quick video of what logging into an account with Ayllu looks |
| 126 | + like today. |
| 127 | + |
| 128 | + |
| 129 | + ## ForgeFeed Specification |
| 130 | + |
| 131 | + I've struggled with what a programmatic API should look like for Ayllu because |
| 132 | + after having written 10,000,000 REST/Protobuf/... web APIs they're really all |
| 133 | + miserable reinventions of the same thing. What I really want is a generalized |
| 134 | + interface for querying forge resources across ANY forge, not just Ayllu. The |
| 135 | + [ForgeFeed](https://forge-feed.org) specification attempts to define a |
| 136 | + common interface. |
| 137 | + |
| 138 | + ## A Primitive Build System |
| 139 | + |
| 140 | + A primitive but functional build system has been added (actually re-enabled) |
| 141 | + to Ayllu and integrated into it's UI. The `ayllu-build` binary when invoked |
| 142 | + will now run an un-sandboxed build of the HEAD at whichever branch is |
| 143 | + selected. This can be invoked manually |
| 144 | + |
| 145 | + ## Towards 1.0 |
| 146 | + |
| 147 | + There is no timeline for when Ayllu will reach 1.0 or exactly what |
| 148 | + it needs to encompass but roughly here are a few items. |
| 149 | + |
| 150 | + * Tooling for Package Managers |
| 151 | + * Functional build system with single host isolation |
| 152 | + * Multiuser Environments |
| 153 | + * A way to accept changes |
| 154 | + * An internal issue tracking system |
| 155 | + |
| 156 | + # User Identities in Ayllu |
| 157 | + |
| 158 | + Code forges typically follow a traditional "web app" model of user accounts |
| 159 | + by allowing people to sign up with their e-mail addresses and log into a web |
| 160 | + interface. |
| 161 | + |
| 162 | + Ayllu uses PAM managed system accounts which are optionally granted a shell |
| 163 | + in the spirit of Unix Timesharing systems. |
| 164 | + |
| 165 | + ## Managing User Accounts in Ayllu |
| 166 | + |
| 167 | + ## Security Implications |
| 168 | diff --git a/content/index.md b/content/index.md |
| 169 | index 543ca64..94b6ed0 100644 |
| 170 | --- a/content/index.md |
| 171 | +++ b/content/index.md |
| 172 | @@ -15,4 +15,4 @@ Contact info (same as above, but a QR code): |
| 173 | <img alt="qr code" class="contact" src="/contact.png"/> |
| 174 | |
| 175 | ### Subscribe |
| 176 | - [RSS](/index.xml) |
| 177 | + [Atom/RSS](/index.xml) |
| 178 | diff --git a/index.xml.jinja b/index.xml.jinja |
| 179 | deleted file mode 100644 |
| 180 | index b92f652..0000000 |
| 181 | --- a/index.xml.jinja |
| 182 | +++ /dev/null |
| 183 | @@ -1 +0,0 @@ |
| 184 | - {{content}} |
| 185 | diff --git a/render.py b/render.py |
| 186 | index eefbd0d..9bbfb78 100755 |
| 187 | --- a/render.py |
| 188 | +++ b/render.py |
| 189 | @@ -1,6 +1,7 @@ |
| 190 | #!/usr/bin/env python |
| 191 | import argparse |
| 192 | import os |
| 193 | + import io |
| 194 | import sys |
| 195 | import xml.etree.ElementTree as ET |
| 196 | from io import StringIO |
| 197 | @@ -15,6 +16,36 @@ from jinja2 import Environment, FunctionLoader |
| 198 | DEVMODE_WEBSOCKET_ADDR = "ws://localhost:8888/ws" |
| 199 | |
| 200 | |
| 201 | + def _set_dates(entries): |
| 202 | + for entry in entries: |
| 203 | + if "date" in entry: |
| 204 | + entry["date"] = datetime.fromisoformat(entry["date"]) |
| 205 | + if "others" in entry: |
| 206 | + return _set_dates(entry["others"]) |
| 207 | + |
| 208 | + |
| 209 | + def _get_dates(entries): |
| 210 | + dates = [] |
| 211 | + for entry in entries: |
| 212 | + if "date" in entry: |
| 213 | + dates.append(entry["date"]) |
| 214 | + if "others" in entry: |
| 215 | + for other in _get_dates(entry["others"]): |
| 216 | + dates.append(other) |
| 217 | + return dates |
| 218 | + |
| 219 | + |
| 220 | + def _get_latest(entries): |
| 221 | + latest = None |
| 222 | + for other in _get_dates(entries): |
| 223 | + if latest is None: |
| 224 | + latest = other |
| 225 | + else: |
| 226 | + if other > latest: |
| 227 | + latest = other |
| 228 | + return latest |
| 229 | + |
| 230 | + |
| 231 | def _load_sitemap(path, current_path): |
| 232 | by_name = dict() |
| 233 | flattened = [] |
| 234 | @@ -69,7 +100,10 @@ def _load_sitemap(path, current_path): |
| 235 | entry["url"] = target |
| 236 | by_name[target] = entry |
| 237 | |
| 238 | - return dict(by_name=by_name, variables=variables, links=entries) |
| 239 | + _set_dates(entries) |
| 240 | + latest = _get_latest(entries) |
| 241 | + |
| 242 | + return dict(by_name=by_name, variables=variables, links=entries, latest=_get_latest(entries)) |
| 243 | |
| 244 | |
| 245 | def _make_tree(links): |
| 246 | @@ -146,29 +180,44 @@ def generate_rss(sitemap): |
| 247 | header = ET.Element( |
| 248 | "xml-stylesheet", attrib={"href": "/feed.xsl", "type": "text/xsl"} |
| 249 | ) |
| 250 | - rss = ET.Element("rss", attrib={"version": "2.0", "encoding": "utf-8"}) |
| 251 | - channel = ET.SubElement(rss, "channel") |
| 252 | |
| 253 | - title = ET.SubElement(channel, "title") |
| 254 | + feed = ET.Element("feed", attrib={"xmlns": "http://www.w3.org/2005/Atom"}) |
| 255 | + |
| 256 | + feed_id = ET.SubElement(feed, "id") |
| 257 | + feed_id.text = "https://kevinschoon.com/" |
| 258 | + title = ET.SubElement(feed, "title") |
| 259 | title.text = "Blog of Kevin Schoon" |
| 260 | - link = ET.SubElement(channel, "link") |
| 261 | + link = ET.SubElement(feed, "link") |
| 262 | + link.attrib["rel"] = "self" |
| 263 | link.attrib["href"] = "https://kevinschoon.com" |
| 264 | - description = ET.SubElement(channel, "description") |
| 265 | - description.text = "Blog of Kevin Schoon" |
| 266 | + updated = ET.SubElement(feed, "updated") |
| 267 | + updated.text = sitemap["latest"].isoformat() |
| 268 | + author = ET.SubElement(feed, "author") |
| 269 | + name = ET.SubElement(author, "name") |
| 270 | + name.text = "Kevin Schoon" |
| 271 | |
| 272 | for entry in sitemap["by_name"]["/blog"]["others"]: |
| 273 | absolute_url = "https://kevinschoon.com/blog/" + entry["name"] |
| 274 | - item = ET.SubElement(channel, "item") |
| 275 | - title_item = ET.SubElement(item, "title") |
| 276 | - link_item = ET.SubElement(item, "link") |
| 277 | - guid_item = ET.SubElement(item, "guid") |
| 278 | - guid_item.text = absolute_url |
| 279 | - description_item = ET.SubElement(item, "description") |
| 280 | - title_item.text = entry["name"] |
| 281 | - link_item.attrib["href"] = absolute_url |
| 282 | - description_item.text = entry["tagline"] |
| 283 | - |
| 284 | - xml_str = ET.tostring(rss, xml_declaration=True, encoding="unicode", method="xml") |
| 285 | + atom_entry = ET.SubElement(feed, "entry") |
| 286 | + link = ET.SubElement(atom_entry, "link") |
| 287 | + link.attrib["href"] = absolute_url |
| 288 | + link.attrib["rel"] = "self" |
| 289 | + _id = ET.SubElement(atom_entry, "id") |
| 290 | + _id.text = absolute_url |
| 291 | + title = ET.SubElement(atom_entry, "title") |
| 292 | + title.text = entry["tagline"] |
| 293 | + content_path = "content/blog/" + entry["name"] + "/README.md" |
| 294 | + content = load_content(content_path) |
| 295 | + content_element = ET.SubElement(atom_entry, "content") |
| 296 | + content_element.attrib["type"] = "xhtml" |
| 297 | + content_element.attrib["xml:lang"] = "en" |
| 298 | + content_tree = ET.parse(io.StringIO(f"<div xmlns=\"http://www.w3.org/1999/xhtml\">{content}</div>")).getroot() |
| 299 | + content_element.append(content_tree) |
| 300 | + updated = ET.SubElement(atom_entry, "updated") |
| 301 | + updated.text = entry["date"].isoformat() |
| 302 | + |
| 303 | + |
| 304 | + xml_str = ET.tostring(feed, xml_declaration=True, encoding="unicode", method="xml") |
| 305 | xml_str = xml_str.replace( |
| 306 | "<?xml version='1.0' encoding='utf-8'?>", |
| 307 | "<?xml version='1.0' encoding='utf-8'?><?xml-stylesheet href='/style.xsl' type='text/xsl'?>", |
| 308 | diff --git a/sitemap.yaml b/sitemap.yaml |
| 309 | index 969527d..11a4b57 100644 |
| 310 | --- a/sitemap.yaml |
| 311 | +++ b/sitemap.yaml |
| 312 | @@ -7,7 +7,7 @@ entries: |
| 313 | others: |
| 314 | - name: announcing-ayllu-0.4 |
| 315 | tagline: Ayllu version 0.4 has been released. |
| 316 | - date: 2025-05-13 |
| 317 | + date: "2025-05-13T01:01:01Z" |
| 318 | - name: building-this-website |
| 319 | tagline: There is no deficiency of static website generators these days and so I had to write my own. |
| 320 | - date: 2023-04-04 |
| 321 | + date: "2023-04-14T01:01:01Z" |