This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new ee5a3bf  Move HTML generation to the containers and use an ignored 
directory
ee5a3bf is described below

commit ee5a3bfe442ac9317ca0f427e335945ab397ebfd
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Nov 28 19:16:34 2025 +0000

    Move HTML generation to the containers and use an ignored directory
---
 .gitignore                               |   1 +
 Dockerfile.alpine                        |   3 +
 Dockerfile.ubuntu                        |   3 +
 Makefile                                 |  10 +-
 atr/docs/build-processes.html            |  15 ---
 atr/docs/code-conventions.html           | 181 -------------------------
 atr/docs/database.html                   |  35 -----
 atr/docs/developer-guide.html            |  23 ----
 atr/docs/how-to-contribute.html          | 103 ---------------
 atr/docs/index.html                      |  22 ----
 atr/docs/introduction-to-atr.html        |  27 ----
 atr/docs/overview-of-the-code.html       |  46 -------
 atr/docs/running-and-creating-tests.html |  36 -----
 atr/docs/running-the-server.html         |  55 --------
 atr/docs/storage-interface.html          | 100 --------------
 atr/docs/tasks.html                      |  53 --------
 atr/docs/user-guide.html                 |  10 --
 atr/docs/user-interface.html             | 218 -------------------------------
 atr/get/docs.py                          |   2 +-
 19 files changed, 12 insertions(+), 931 deletions(-)

diff --git a/.gitignore b/.gitignore
index 0c7bf91..53a261d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ apptoken.txt
 atr/_version.py
 bootstrap/build/
 dev/
+docs/
 node_modules/
 package-lock.json
 state/
diff --git a/Dockerfile.alpine b/Dockerfile.alpine
index 87b8032..70ee3e3 100644
--- a/Dockerfile.alpine
+++ b/Dockerfile.alpine
@@ -8,6 +8,7 @@ ENV PIP_DEFAULT_TIMEOUT=100 \
 
 RUN apk update && \
     apk add --no-cache \
+      cmark \
       git \
       make \
       patch
@@ -25,6 +26,7 @@ RUN make sync
 
 # generate a version.py module from git information
 RUN make generate-version
+RUN make docs
 
 WORKDIR /opt/atr/.venv/lib/python3.13/site-packages
 RUN patch -p2 < /opt/atr/patches/generics.py.patch
@@ -59,6 +61,7 @@ WORKDIR /opt/atr
 # copy app and wheels from builder
 COPY --from=builder /opt/atr/.venv ./.venv
 COPY --from=builder /opt/atr/atr ./atr
+COPY --from=builder /opt/atr/docs ./docs
 COPY --from=builder /opt/atr/migrations ./migrations
 COPY --from=builder /opt/atr/patches ./patches
 COPY --from=builder /opt/atr/scripts ./scripts
diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu
index 57f7a42..f48f381 100644
--- a/Dockerfile.ubuntu
+++ b/Dockerfile.ubuntu
@@ -9,6 +9,7 @@ ENV PIP_DEFAULT_TIMEOUT=100 \
 
 RUN apt-get update && \
     apt-get install -y \
+      cmark \
       git \
       make \
       patch \
@@ -33,6 +34,7 @@ RUN make sync
 
 # generate a version.py module from git information
 RUN make generate-version
+RUN make docs
 
 WORKDIR /opt/atr/.venv/lib/python3.13/site-packages
 RUN patch -p2 < /opt/atr/patches/generics.py.patch
@@ -73,6 +75,7 @@ WORKDIR /opt/atr
 # copy app and wheels from builder
 COPY --from=builder /opt/atr/.venv ./.venv
 COPY --from=builder /opt/atr/atr ./atr
+COPY --from=builder /opt/atr/docs ./docs
 COPY --from=builder /opt/atr/migrations ./migrations
 COPY --from=builder /opt/atr/scripts ./scripts
 COPY --from=builder /opt/atr/Makefile .
diff --git a/Makefile b/Makefile
index 3bb6f1b..624ac17 100644
--- a/Makefile
+++ b/Makefile
@@ -48,14 +48,12 @@ commit:
        git push
 
 docs:
+       mkdir -p docs
        uv run python3 scripts/docs_check.py
-       rm -f atr/docs/*.html
+       rm -f docs/*.html
        uv run python3 scripts/docs_build.py
-       for fn in atr/docs/*.md; \
-       do \
-         cmark "$$fn" > "$${fn%.md}.html"; \
-       done
-       uv run python3 scripts/docs_post_process.py atr/docs/*.html
+       for fn in atr/docs/*.md; do out=$${fn#atr/}; cmark "$$fn" > 
"$${out%.md}.html"; done
+       uv run python3 scripts/docs_post_process.py docs/*.html
        uv run python3 scripts/docs_check.py
 
 generate-version:
diff --git a/atr/docs/build-processes.html b/atr/docs/build-processes.html
deleted file mode 100644
index 4dec9fc..0000000
--- a/atr/docs/build-processes.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<h1 id="build-processes">3.7. Build processes</h1>
-<p><strong>Up</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Prev</strong>: <code>3.6.</code> <a href="tasks">Tasks</a></p>
-<p><strong>Next</strong>: <code>3.8.</code> <a 
href="running-and-creating-tests">Running and creating tests</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#documentation-build-script">Documentation build script</a></li>
-</ul>
-<h2 id="documentation-build-script">Documentation build script</h2>
-<p>To <strong>regenerate the documentation</strong>, run <code>make 
docs</code>.</p>
-<p>The ATR documentation that you're reading right now is structured like a 
book, with numbered chapters, sections, and navigation links between pages. We 
could maintain all of this by hand, but that would be tedious and error-prone. 
Instead, we use <a 
href="/ref/scripts/docs_build.py"><code>scripts/docs_build.py</code></a> to 
generate the navigation automatically from a single table of contents.</p>
-<p>The script reads the table of contents in <a 
href="/ref/atr/docs/index.md"><code>atr/docs/index.md</code></a>, extracts the 
hierarchy of pages, and then updates every referenced page to include the 
correct navigation links, page numbers, and section listings. This means that 
when we want to reorganize the documentation (say, inserting a new chapter or 
moving sections around) we only need to edit the table of contents, run the 
script, and all the navigation is updated automatically.</p>
-<p>The implementation is straightforward. The <a 
href="/ref/scripts/docs_build.py:parse_toc"><code>parse_toc</code></a> function 
extracts entries from the table of contents section in the index, and <a 
href="/ref/scripts/docs_build.py:build_navigation"><code>build_navigation</code></a>
 computes the up, previous, and next relationships for each page. The <a 
href="/ref/scripts/docs_build.py:update_document"><code>update_document</code></a>
 function is then called for each page, which rewri [...]
-<p>The navigation block itself is generated by <a 
href="/ref/scripts/docs_build.py:generate_navigation_block"><code>generate_navigation_block</code></a>,
 which formats the up, previous, and next links, adds a list of subpages if any 
exist, and includes a table of contents for the page's sections as extracted by 
<a 
href="/ref/scripts/docs_build.py:extract_h2_headings"><code>extract_h2_headings</code></a>.
 This keeps all of the navigational machinery separate from the actual content, 
which [...]
-<p>We also validate that every page in the table of contents exists, and that 
there are no unlinked Markdown files in the documentation directory. The <a 
href="/ref/scripts/docs_build.py:validate_files"><code>validate_files</code></a>
 function performs these checks and fails with a descriptive error if anything 
is wrong. This prevents us from accidentally forgetting to add a page to the 
table of contents, or from leaving old pages lying around that we meant to 
delete.</p>
diff --git a/atr/docs/code-conventions.html b/atr/docs/code-conventions.html
deleted file mode 100644
index ff38331..0000000
--- a/atr/docs/code-conventions.html
+++ /dev/null
@@ -1,181 +0,0 @@
-<h1 id="code-conventions">3.9. Code conventions</h1>
-<p><strong>Up</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Prev</strong>: <code>3.8.</code> <a 
href="running-and-creating-tests">Running and creating tests</a></p>
-<p><strong>Next</strong>: <code>3.10.</code> <a href="how-to-contribute">How 
to contribute</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#python-code">Python code</a></li>
-<li><a href="#html">HTML</a></li>
-<li><a href="#markdown">Markdown</a></li>
-<li><a href="#javascript">JavaScript</a></li>
-</ul>
-<h2 id="python-code">Python code</h2>
-<h3 id="follow-pep-8-rules-by-default">Follow PEP 8 rules by default</h3>
-<p>Follow <a href="https://peps.python.org/pep-0008/";>PEP 8</a> unless 
otherwise indicated in this document. Some of the conventions listed below 
recapitulate or add exceptions to PEP 8 rules.</p>
-<p>Obey all project local lints, e.g. the use of <code>ruff</code> and 
specific <code>ruff</code> rules.</p>
-<h3 id="keep-the-primary-execution-path-to-the-left">Keep the primary 
execution path to the left</h3>
-<p>Structure code so that the most likely, normal, successful execution path 
remains at the level of least indentation. Handle error cases and edge 
conditions early with guard clauses, and then continue with the main logic. 
This makes it easier to identify the primary execution flow.</p>
-<pre><code class="language-python"># Avoid
-def process_data(data):
-    if data is not None:
-        if len(data) &gt; 0:
-            if validate(data):
-                return transform(data)
-            else:
-                raise ValueError("Invalid data")
-        else:
-            raise ValueError("Empty data")
-    else:
-        raise ValueError("No data")
-
-# Prefer
-def process_data(data):
-    if data is None:
-        raise ValueError("No data")
-    if len(data) == 0:
-        raise ValueError("Empty data")
-    if not validate(data):
-        raise ValueError("Invalid data")
-
-    return transform(data)
-</code></pre>
-<h3 id="avoid-excessive-indentation">Avoid excessive indentation</h3>
-<p>When you find yourself nesting code more than two or three levels deep, 
extract the nested logic into separate functions. This improves readability, 
testability, and maintainability. Each function should handle a single, well 
defined piece of logic.</p>
-<h3 id="do-not-use-lint-or-type-checker-ignore-statements">Do not use lint or 
type checker ignore statements</h3>
-<p>You must not use <code># noqa</code>, <code># type: ignore</code>, or 
equivalents such as <code>cast</code>, even to ignore specific errors. The 
single exception to this is when there is a bug in the linter or type 
checker.</p>
-<p>File level lint ignores can be added to the project's 
<code>pyproject.toml</code>, but they must be used sparingly.</p>
-<h3 id="use-double-quotes-for-all-strings">Use double quotes for all 
strings</h3>
-<p>This includes triple quoted strings.</p>
-<h3 id="prefix-private-interfaces-with-a-single-underscore">Prefix private 
interfaces with a single underscore</h3>
-<p>Prefix all private interfaces, e.g. functions, classes, constants, 
variables, with a single underscore. An interface is private when used 
exclusively within its containing module and not referenced by external code, 
templates, or processes.</p>
-<p>Exceptions to this rule include:</p>
-<ul>
-<li>Type variables</li>
-<li>Enumerations</li>
-<li>Methods requiring interface compatibility with their superclass</li>
-<li>Nested functions (which should generally be avoided)</li>
-</ul>
-<p>Scripts are explicitly <em>not</em> an exception. Underscores should be 
used to prefix private interfaces in scripts for consistency, e.g. so that 
linters don't need to carry exceptions, and to ease potential migration to 
modules.</p>
-<h3 id="avoid-nested-functions">Avoid nested functions</h3>
-<p>Function definitions should be at the top level. This is not a hard rule, 
but should only be broken when absolutely necessary.</p>
-<h3 id="use-uppercase-for-top-level-constants">Use UPPERCASE for top level 
constants</h3>
-<p>Define top level constants using <code>UPPERCASE</code> letters. Don't 
forget to apply an underscore prefix to constants which are private to their 
module.</p>
-<p>Do not use uppercase for constants within functions and methods.</p>
-<code></code><h3 id="use-the-final-type-with-all-constants">Use the Final type 
with all constants</h3>
-<p>This pattern must be followed for top level constants, and should be 
followed for function and method level constants too. The longer the function, 
the more important the use of <code>Final</code>.</p>
-<code></code><h3 id="prefix-global-variables-with-global">Prefix global 
variables with global_</h3>
-<p>Top level variables should be avoided. When their use is necessary, prefix 
them with <code>global_</code>, using lowercase letters, to ensure clear 
identification of their scope. Use an underscore prefix too, 
<code>_global_</code>, when the variable is private.</p>
-<h3 id="import-modules-as-their-least-significant-name-part">Import modules as 
their least significant name part</h3>
-<p>Import modules using their least significant name component:</p>
-<pre><code class="language-python"># Prefer
-import a.b.c as c
-
-# Avoid
-import a.b.c
-</code></pre>
-<p>This convention aligns with Go's package naming practices. Follow <a 
href="https://go.dev/blog/package-names";>Go naming rules</a> for all 
modules.</p>
-<p>This only applies to modules outside of the Python standard library. The 
standard library module <code>os.path</code>, for example, must always be 
imported using the form <code>import os.path</code>, and <em>not</em> 
<code>import os.path as path</code>.</p>
-<p>Furthermore, if a third party module to be imported would conflict with a 
Python standard library module, then that third party module must be imported 
with one extra level.</p>
-<pre><code class="language-python"># Prefer
-import asyncio.subprocess
-import sqlalchemy.ext as ext
-import aiofiles.os
-
-# Avoid
-import asyncio.subprocess as subprocess
-import sqlalchemy.ext.asyncio as asyncio
-import aiofiles.os.path as path
-</code></pre>
-<p>It's possible to use <code>from a.b import c</code> instead of <code>import 
a.b.c as c</code> when <code>c</code> is a module, but we prefer the latter 
form because it makes it clear that <code>c</code> must be a module, whereas in 
the former <code>from a.b import c</code> form, <code>c</code> could be any 
interface.</p>
-<p>TODO: There's a question as to whether we could actually use <code>import 
aiofiles.os.path as path</code> since we import <code>os.path</code> as 
<code>os.path</code> and not <code>path</code>.</p>
-<p>TODO: Sometimes we're using <code>as</code> for standard library modules. 
We should decide what to do about this.</p>
-<h3 id="avoid-duplicated-module-names">Avoid duplicated module names</h3>
-<p>Try to avoid using, for example, <code>baking/apple/pie.py</code> and 
<code>baking/cherry/pie.py</code> because these will both be imported as 
<code>pie</code> and one will have to be renamed.</p>
-<p>If there are duplicates imported within a single file, they should be 
disambiguated by the next level up. In the pie example, that would be 
<code>import baking.apple as apple</code> and then <code>apple.pie</code>, and 
<code>import baking.cherry as cherry</code> and <code>cherry.pie</code>.</p>
-<h3 id="never-import-names-directly-from-modules">Never import names directly 
from modules</h3>
-<p>Avoid importing specific names from modules:</p>
-<pre><code class="language-python"># Prefer
-import p.q.r as r
-r.s()
-
-# Avoid
-from p.q.r import s
-s()
-</code></pre>
-<p>The <code>collections.abc</code>, <code>types</code>, and 
<code>typing</code> modules are an exception to this rule. Always import 
<code>collections.abc</code>, <code>types</code> and <code>typing</code> 
interfaces directly using the <code>from</code> syntax:</p>
-<pre><code class="language-python"># Prefer
-from typing import Final
-
-CONSTANT: Final = "CONSTANT"
-
-# Avoid
-import typing
-
-CONSTANT: typing.Final = "CONSTANT"
-</code></pre>
-<h3 id="use-concise-typing-patterns">Use concise typing patterns</h3>
-<p>Do not use <code>List</code> or <code>Optional</code> etc. from the typing 
module.</p>
-<pre><code class="language-python"># Prefer
-def example() -&gt; list[str | None]:
-    return ["a", "c", None]
-
-# Avoid
-from typing import List, Optional
-
-def example() -&gt; List[Optional[str]]:
-    return ["a", "c", None]
-</code></pre>
-<h3 id="never-name-interfaces-after-their-module">Never name interfaces after 
their module</h3>
-<p>Do not name interfaces with the same identifier as their containing module. 
For example, in a module named <code>example</code>, the function names 
<code>example</code> and <code>example_function</code> are prohibited.</p>
-<h3 id="keep-modules-small-and-focused">Keep modules small and focused</h3>
-<p>Maintain modules with a reasonable number of interfaces. Though no strict 
limits are enforced, modules containing numerous classes, constants, or 
functions should be considered for logical subdivision. Exceptions may be made 
when closely related functionality necessitates grouping multiple interfaces 
within a single module.</p>
-<h3 id="sort-functions-alphabetically">Sort functions alphabetically</h3>
-<p>Wherever possible, the order of functions within each module should be 
alphabetical by name. Take advantage of this convention by grouping related 
functions under a common prefix (including grouping helper functions with their 
caller), and using numbers in the names of functions called in serial order.</p>
-<h3 id="keep-cyclomatic-complexity-below-10">Keep cyclomatic complexity below 
10</h3>
-<p>We limit function complexity to a score of 10. If the linter complains, 
your function is doing too much.</p>
-<p>Cyclomatic complexity counts the number of independent paths through code: 
more if and else branches, loops, and exception handlers means higher 
complexity. Complex code is harder to test, maintain, and understand. The 
easiest way to fix high complexity is usually to refactor a chunk of related 
logic into a separate helper function.</p>
-<h3 
id="replace-synchronous-calls-with-asynchronous-counterparts-in-async-code">Replace
 synchronous calls with asynchronous counterparts in async code</h3>
-<p>Our use of blockbuster enables automatic detection of synchronous function 
calls within asynchronous code. When detected, replace these calls with their 
asynchronous equivalents without performance testing. The conversion process 
typically requires minimal, trivial effort.</p>
-<p>Exceptions to this rule apply only in these scenarios:</p>
-<ul>
-<li>When dealing with third party dependencies</li>
-<li>When the asynchronous equivalent function is unknown</li>
-</ul>
-<p>If either exception applies, either submit a brief issue with the 
blockbuster traceback, notify the team via Slack, or add a code comment if part 
of another commit. An ATR Tooling engineer will address the issue without 
requiring significant time investment from you.</p>
-<h3 id="always-use-parentheses-to-group-complex-nested-subexpressions">Always 
use parentheses to group complex nested subexpressions</h3>
-<p>Complex subexpressions are those which contain a keyword or operator.</p>
-<pre><code class="language-python"># Avoid
-a or b and c == d or not e or f
-
-# Prefer
-(a or b) and (c == d) or (not e) or f
-</code></pre>
-<p>Because <code>f</code> is not a complex expression, it does not get 
parenthesised. Also because this rule is about subexpressions only, we do not 
put parentheses around the top level.</p>
-<pre><code class="language-python"># Avoid
-if (a or b):
-    ...
-
-# Prefer
-if a or b:
-    ...
-</code></pre>
-<h3 id="use-terse-comments-on-their-own-lines">Use terse comments on their own 
lines</h3>
-<p>Place comments on dedicated lines preceding the relevant code block. 
Comments at the ends of lines are strictly reserved for linter or type checker 
directives. This convention enhances code scannability for such directives. 
General comments must not appear at the end of code lines. Keep comments 
concise, using sentence case without terminal punctuation. Each sentence 
forming a comment must occupy its own line. Comments must not include 
information about what has changed from earlier c [...]
-<code></code><h3 id="prefer-explicit-checks-over-assert">Prefer explicit 
checks over assert</h3>
-<p>We do not use <code>assert</code>. If you need to guard against invalid 
states or inputs, use standard <code>if</code> checks and raise appropriate 
exceptions. If you need to help type checkers understand the type of a variable 
within a specific code block, in other words if you need to narrow a type, then 
use <code>if isinstance(...)</code> or <code>if not isinstance(...)</code> as 
appropriate.</p>
-<code></code><h3 
id="never-use-case-when-pattern-matching-exhaustive-types">Never use case _ 
when pattern matching exhaustive types</h3>
-<p>Using <code>case _</code> breaks type checking in such situations.</p>
-<h3 id="use-f-string-interpolation-instead-of-printf-style-formatting">Use 
f-string interpolation instead of printf style formatting</h3>
-<p>This should be adhered to even in contexts where printf style is usually 
expected, such as in <code>log.info</code> calls, unless there is a reason not 
to, such as when there are specific printf style flags which have no f-string 
equivalent.</p>
-<p>This convention is not enforced by any checks. Enforcement is via code 
review. See <a 
href="https://github.com/apache/tooling-trusted-releases/issues/339";>issue 
#339</a> for a discussion.</p>
-<h2 id="html">HTML</h2>
-<h3 id="use-sentence-case-for-headings-form-labels-and-submission-buttons">Use 
sentence case for headings, form labels, and submission buttons</h3>
-<p>We write headings, form labels, and submission buttons in the form "This is 
some text", and not "This is Some Text" or "This Is Some Text". This follows 
the <a 
href="https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style#Section_headings";>Wikipedia
 style for headings</a>.</p>
-<h3 id="use-bootstrap-classes-for-all-style">Use Bootstrap classes for all 
style</h3>
-<p>We use Bootstrap classes for style, and avoid custom classes unless 
absolutely necessary. If you think that you have to resort to a custom class, 
consult the list of <a href="https://bootstrapclasses.com/";>Bootstrap 
classes</a> for guidance. There is usually a class for what you want to 
achieve, and if there isn't then you may be making things too complicated. 
Complicated, custom style is difficult for a team to maintain. If you still 
believe that a new class is strictly warranted, th [...]
-<h2 id="markdown">Markdown</h2>
-<code></code><code></code><h3 
id="use-for-emphasis-and-for-strong-emphasis">Use _ for emphasis and ** for 
strong emphasis</h3>
-<p>Do not use <code>*</code> for emphasis or <code>__</code> for strong 
emphasis.</p>
-<h2 id="javascript">JavaScript</h2>
-<h3 id="do-not-use-javascript-unless-necessary">Do not use JavaScript unless 
necessary</h3>
-<p>It is often possible to avoid using JavaScript without significant loss of 
functionality, but it may require a little more thought. JavaScript is not, 
however, something to avoid by rote. User experiences can be significantly 
improved with thoughtful application of JavaScript. Therefore, default to not 
using JavaScript, but consider how it could be used concisely and with care to 
improve UX.</p>
diff --git a/atr/docs/database.html b/atr/docs/database.html
deleted file mode 100644
index 1db1eb5..0000000
--- a/atr/docs/database.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<h1 id="database">3.3. Database</h1>
-<p><strong>Up</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Prev</strong>: <code>3.2.</code> <a 
href="overview-of-the-code">Overview of the code</a></p>
-<p><strong>Next</strong>: <code>3.4.</code> <a 
href="storage-interface">Storage interface</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#introduction">Introduction</a></li>
-<li><a href="#core-models">Core models</a></li>
-<li><a href="#other-features">Other features</a></li>
-</ul>
-<h2 id="introduction">Introduction</h2>
-<p>ATR stores all of its data in a SQLite database. The database schema is 
defined in <a href="/ref/atr/models/sql.py"><code>models.sql</code></a> using 
<a href="https://sqlmodel.tiangolo.com/";>SQLModel</a>, which uses <a 
href="https://docs.pydantic.dev/latest/";>Pydantic</a> for data validation and 
<a href="https://www.sqlalchemy.org/";>SQLAlchemy</a> for database operations. 
This page explains the main features of the database schema to help you 
understand how data is structured in ATR.</p>
-<h2 id="core-models">Core models</h2>
-<p>The three most important models in ATR are <a 
href="/ref/atr/models/sql.py:Committee"><code>Committee</code></a>, <a 
href="/ref/atr/models/sql.py:Project"><code>Project</code></a>, and <a 
href="/ref/atr/models/sql.py:Release"><code>Release</code></a>.</p>
-<p>A <a href="/ref/atr/models/sql.py:Committee"><code>Committee</code></a> 
represents a PMC or PPMC at the ASF. Each committee has a name, which is the 
primary key, and a full name for display purposes. Committees can have child 
committees, which is used for the relationship between the Incubator PMC and 
individual podling PPMCs. Committees also have lists of committee members and 
committers stored as JSON arrays.</p>
-<p>A <a href="/ref/atr/models/sql.py:Project"><code>Project</code></a> belongs 
to a committee and can have multiple releases. Projects have a name as the 
primary key, along with metadata such as a description and category and 
programming language tags. Each project can optionally have a <a 
href="/ref/atr/models/sql.py:ReleasePolicy"><code>ReleasePolicy</code></a> that 
defines how releases should be handled, including e.g. vote templates and 
GitHub workflow configuration.</p>
-<p>A <a href="/ref/atr/models/sql.py:Release"><code>Release</code></a> belongs 
to a project and represents a specific version of software which is voted on by 
a committee. The primary key is a name derived from the project name and 
version. Releases have a phase that indicates their current state in the 
release process, from draft composition to final publication. Each release can 
have multiple <a 
href="/ref/atr/models/sql.py:Revision"><code>Revision</code></a> instances 
before final pub [...]
-<h2 id="other-features">Other features</h2>
-<p>The models themselves are the most important components, but to support 
those models we need other components such as enumerations, column types, 
automatically populated fields, computed properties, and constraints.</p>
-<h3 id="enumerations">Enumerations</h3>
-<p>ATR uses Python enumerations to ensure that certain fields only contain 
valid values. The most important enumeration is <a 
href="/ref/atr/models/sql.py:ReleasePhase"><code>ReleasePhase</code></a>, which 
defines the four phases of a release: <code>RELEASE_CANDIDATE_DRAFT</code> for 
composing, <code>RELEASE_CANDIDATE</code> for voting, 
<code>RELEASE_PREVIEW</code> for finishing, and <code>RELEASE</code> for 
completed releases.</p>
-<p>The <a href="/ref/atr/models/sql.py:TaskStatus"><code>TaskStatus</code></a> 
enumeration defines the states a task can be in: <code>QUEUED</code>, 
<code>ACTIVE</code>, <code>COMPLETED</code>, or <code>FAILED</code>. The <a 
href="/ref/atr/models/sql.py:TaskType"><code>TaskType</code></a> enumeration 
lists all the different types of background tasks that ATR can execute, from 
signature checks to SBOM generation.</p>
-<p>The <a 
href="/ref/atr/models/sql.py:DistributionPlatform"><code>DistributionPlatform</code></a>
 enumeration is more complex, as each value contains not just a name but a <a 
href="/ref/atr/models/sql.py:DistributionPlatformValue"><code>DistributionPlatformValue</code></a>
 with template URLs and configuration for different package distribution 
platforms like PyPI, npm, and Maven Central.</p>
-<h3 id="special-column-types">Special column types</h3>
-<p>SQLite does not support all the data types we need, so we use SQLAlchemy 
type decorators to handle conversions. The <a 
href="/ref/atr/models/sql.py:UTCDateTime"><code>UTCDateTime</code></a> type 
ensures that all datetime values are stored in UTC and returned as 
timezone-aware datetime objects. When Python code provides a datetime with 
timezone information, the type decorator converts it to UTC before storing. 
When reading from the database, it adds back the UTC timezone information.</p>
-<p>The <a 
href="/ref/atr/models/sql.py:ResultsJSON"><code>ResultsJSON</code></a> type 
handles storing task results. It automatically serializes Pydantic models to 
JSON when writing to the database, and deserializes them back to the 
appropriate result model when reading.</p>
-<h3 id="automatic-field-population">Automatic field population</h3>
-<p>Some fields are populated automatically using SQLAlchemy event listeners. 
When a new <a href="/ref/atr/models/sql.py:Revision"><code>Revision</code></a> 
is created, the <a 
href="/ref/atr/models/sql.py:populate_revision_sequence_and_name"><code>populate_revision_sequence_and_name</code></a>
 function runs before the database insert. This function queries for the 
highest existing sequence number for the release, increments it, and sets both 
the <code>seq</code> and <code>number</code> fi [...]
-<p>The <a 
href="/ref/atr/models/sql.py:check_release_name"><code>check_release_name</code></a>
 function runs before inserting a release. If the release name is empty, it 
automatically generates it from the project name and version using the <a 
href="/ref/atr/models/sql.py:release_name"><code>release_name</code></a> helper 
function.</p>
-<h3 id="computed-properties">Computed properties</h3>
-<p>Some properties are computed dynamically rather than stored in the 
database. The <code>Release.latest_revision_number</code> property is 
implemented as a SQLAlchemy column property using a correlated subquery. This 
means that when you access <code>release.latest_revision_number</code>, 
SQLAlchemy automatically executes a query to find the highest revision number 
for that release. The query is defined once in <a 
href="/ref/atr/models/sql.py:RELEASE_LATEST_REVISION_NUMBER"><code>RELEASE [...]
-<p>Projects have many computed properties that provide access to release 
policy settings with appropriate defaults. For example, 
<code>Project.policy_start_vote_template</code> returns the custom vote 
template if one is configured, or falls back to 
<code>Project.policy_start_vote_default</code> if not. This pattern allows 
projects to customize their release process while providing sensible 
defaults.</p>
-<h3 id="constraints-and-validation">Constraints and validation</h3>
-<p>Database constraints ensure data integrity. The <a 
href="/ref/atr/models/sql.py:Task"><code>Task</code></a> model includes a check 
constraint that validates the status transitions. A task must start in 
<code>QUEUED</code> state, can only transition to <code>ACTIVE</code> when 
<code>started</code> and <code>pid</code> are set, and can only reach 
<code>COMPLETED</code> or <code>FAILED</code> when the <code>completed</code> 
timestamp is set. These constraints prevent invalid state transi [...]
-<p>Unique constraints ensure that certain combinations of fields are unique. 
The <code>Release</code> model has a unique constraint on <code>(project_name, 
version)</code> to prevent creating duplicate releases for the same project 
version. The <code>Revision</code> model has two unique constraints: one on 
<code>(release_name, seq)</code> and another on <code>(release_name, 
number)</code>, ensuring that revision numbers are unique within a release.</p>
diff --git a/atr/docs/developer-guide.html b/atr/docs/developer-guide.html
deleted file mode 100644
index f6e3f98..0000000
--- a/atr/docs/developer-guide.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<h1 id="developer-guide">3. Developer guide</h1>
-<p><strong>Up</strong>: <a href=".">Documentation</a></p>
-<p><strong>Prev</strong>: <code>2.</code> <a href="user-guide">User 
guide</a></p>
-<p><strong>Next</strong>: (none)</p>
-<p><strong>Pages</strong>:</p>
-<ul>
-<li><code>3.1.</code> <a href="running-the-server">Running the server</a></li>
-<li><code>3.2.</code> <a href="overview-of-the-code">Overview of the 
code</a></li>
-<li><code>3.3.</code> <a href="database">Database</a></li>
-<li><code>3.4.</code> <a href="storage-interface">Storage interface</a></li>
-<li><code>3.5.</code> <a href="user-interface">User interface</a></li>
-<li><code>3.6.</code> <a href="tasks">Tasks</a></li>
-<li><code>3.7.</code> <a href="build-processes">Build processes</a></li>
-<li><code>3.8.</code> <a href="running-and-creating-tests">Running and 
creating tests</a></li>
-<li><code>3.9.</code> <a href="code-conventions">Code conventions</a></li>
-<li><code>3.10.</code> <a href="how-to-contribute">How to contribute</a></li>
-</ul>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#introduction">Introduction</a></li>
-</ul>
-<h2 id="introduction">Introduction</h2>
-<p>This is a guide for developers of ATR, explaining how to make changes to 
the ATR source code. For more information about how to contribute those changes 
back to us, please read the <a href="how-to-contribute">contribution 
guide</a>.</p>
diff --git a/atr/docs/how-to-contribute.html b/atr/docs/how-to-contribute.html
deleted file mode 100644
index ca36ef8..0000000
--- a/atr/docs/how-to-contribute.html
+++ /dev/null
@@ -1,103 +0,0 @@
-<h1 id="how-to-contribute">3.10. How to contribute</h1>
-<p><strong>Up</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Prev</strong>: <code>3.9.</code> <a href="code-conventions">Code 
conventions</a></p>
-<p><strong>Next</strong>: (none)</p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#introduction">Introduction</a></li>
-<li><a href="#finding-something-to-work-on">Finding something to work 
on</a></li>
-<li><a href="#pull-request-workflow">Pull request workflow</a></li>
-<li><a href="#commit-message-style">Commit message style</a></li>
-<li><a href="#asf-contribution-policies">ASF contribution policies</a></li>
-<li><a href="#special-considerations-for-atr">Special considerations for 
ATR</a></li>
-<li><a href="#getting-help">Getting help</a></li>
-</ul>
-<h2 id="introduction">Introduction</h2>
-<p>ATR is developed by ASF Tooling in public as open source code, and we 
welcome high quality contributions from external contributors. Whether you are 
fixing a typographical error in documentation, improving an error message, 
implementing a new feature, or addressing a security issue, your contribution 
helps to improve ATR for all of our users.</p>
-<p>This page explains how to contribute code and documentation to ATR. We 
recommend reading the <a href="introduction-to-atr">platform introduction</a> 
and <a href="overview-of-the-code">overview of the code</a> first to understand 
the purpose of ATR and how the codebase is structured. You should also read the 
<a href="code-conventions">code conventions</a> page; we expect all 
contributions to follow those conventions.</p>
-<h2 id="finding-something-to-work-on">Finding something to work on</h2>
-<p>The easiest way to find something to work on is to look at our <a 
href="https://github.com/apache/tooling-trusted-releases/issues";>issue 
tracker</a> on GitHub. We label <a 
href="https://github.com/apache/tooling-trusted-releases/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22";>issues
 that are suitable for new contributors</a> as <code>good first issue</code>. 
These are typically small, well-defined tasks that do not require deep 
familiarity with the entire code [...]
-<p>If you find a bug that is not already reported in the issue tracker, or if 
you have an idea for a new feature, please <a 
href="https://github.com/apache/tooling-trusted-releases/issues/new";>create a 
new issue</a> to discuss it with other developers before you start working on 
it. This helps to ensure that your contribution will be accepted, and that you 
do not duplicate work that is already in progress. For small changes such as 
fixing typographical errors or improving documentation c [...]
-<h2 id="pull-request-workflow">Pull request workflow</h2>
-<p>Once you have identified something to work on, the process of contributing 
is as follows:</p>
-<ol>
-<li>
-<p><strong>Fork the repository.</strong> Create a personal fork of the <a 
href="https://github.com/apache/tooling-trusted-releases";>ATR repository</a> on 
GitHub.</p>
-</li>
-<li>
-<p><strong>Clone your fork.</strong> Clone your fork to your local machine and 
set up your development environment. Follow the instructions in the <a 
href="running-the-server">running the server</a> guide to get ATR running 
locally. Please <a href="#getting-help">ask us for help</a> if you encounter 
any problems with this step.</p>
-</li>
-<li>
-<p><strong>Create a branch.</strong> <a 
href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-and-deleting-branches-within-your-repository";>Create
 a new branch</a> for your work. Use a descriptive name that indicates what you 
are working on, such as <code>fix-typo-in-docs</code> or 
<code>improve-error-messages</code>.</p>
-</li>
-<li>
-<p><strong>Make your changes.</strong> Implement your fix or feature, 
following our <a href="code-conventions">code conventions</a>. If you are 
changing code, ensure that your changes do not break existing functionality. 
Whenever you change code, and especially if you are adding a new feature, 
consider <a href="running-and-creating-tests">adding a test</a>.</p>
-</li>
-<li>
-<p><strong>Commit your changes.</strong> Write clear, concise commit messages 
following <a href="#commit-message-style">our commit message style</a>. Each 
commit should represent a logical unit of work, but we are not particularly 
strict about this.</p>
-</li>
-<li>
-<p><strong>Push your branch.</strong> Push your branch to your fork on 
GitHub.</p>
-</li>
-<li>
-<p><strong>Create a pull request (PR).</strong> The PR should be from your 
branch to the <code>main</code> branch of the ATR repository. In the PR 
description, explain what your changes do and why they are needed. If your PR 
addresses an existing issue, reference that issue by number.</p>
-</li>
-<li>
-<p><strong>Participate in code review.</strong> A member of the Tooling team 
will review your PR and may request changes. <em>We strongly recommend enabling 
the option to allow maintainers to edit your PR when you create it.</em> Even 
if you allow us to make changes, we may still ask you to make the changes 
yourself. Also, because of the stringent security and usability requirements 
for ATR, we accept only <a href="#special-considerations-for-atr">high quality 
contributions</a>.</p>
-</li>
-</ol>
-<p>You can also <a 
href="https://lists.apache.org/[email protected]";>email 
patches</a> if you prefer not to use GitHub. Please use standard Git patch 
formatting, as if you were e.g. contributing to the Linux Kernel.</p>
-<h2 id="commit-message-style">Commit message style</h2>
-<p>We follow a consistent style for commit messages. The first line of the 
commit message is called the subject line, and should follow these 
guidelines:</p>
-<ul>
-<li><strong>Use the imperative mood.</strong> The subject line should complete 
the sentence "If applied, this commit will...".</li>
-<li><strong>Use sentence case.</strong> Start with a capital letter, but do 
not use a full stop at the end.</li>
-<li><strong>Use articles as appropriate before nouns</strong>. Write about "a 
feature" not just "feature". Say, for example, "fix a bug", and not "fix 
bug".</li>
-<li><strong>Be specific and descriptive.</strong> Prefer "Fix a bug in vote 
resolution for tied votes" to "Fix a bug" or "Update the vote code".</li>
-<li><strong>Keep it concise.</strong> Aim for 50 to 72 characters. If you need 
more space to explain your changes, use the commit body.</li>
-</ul>
-<p><strong>Examples of good subject lines:</strong></p>
-<pre><code>Add distribution platform validation to the compose phase
-Fix a bug with sorting version numbers containing release candidates
-Move code to delete releases to the storage interface
-Update dependencies
-</code></pre>
-<p><strong>Examples of poor subject lines:</strong></p>
-<pre><code>fixed stuff
-Updated the code.
-refactoring vote resolution logic
-</code></pre>
-<p>Most commits do not need a body. The subject line alone is sufficient for 
small, focused changes. If, however, your commit is complex or requires 
additional explanation, add a body separated from the subject line by a blank 
line. In the body, explain what the change does and why it was necessary. We 
typically use itemized lists for this, using asterisks. You do not need to 
explain how the change works.</p>
-<h2 id="asf-contribution-policies">ASF contribution policies</h2>
-<p>As an Apache Software Foundation project, ATR follows the standard ASF 
contribution and licensing policies. These policies ensure that the ASF has the 
necessary rights to distribute your contributions, and that contributors retain 
their rights to use their contributions for other purposes.</p>
-<h3 id="contributor-license-agreement">Contributor License Agreement</h3>
-<p>Before we can accept your first contribution as an individual contributor, 
you must sign the <a 
href="https://www.apache.org/licenses/contributor-agreements.html#clas";>Apache 
Individual Contributor License Agreement</a> (ICLA). This is a one-time 
requirement, and you do not need to sign a new ICLA for each contribution. The 
ICLA grants the ASF the right to distribute and build upon your work within 
Apache, while you retain full rights to use your original contributions for any 
other p [...]
-<p>If your employer holds rights to your work, then you may also need to 
submit a <a 
href="https://www.apache.org/licenses/contributor-agreements.html#clas";>Corporate
 Contributor License Agreement</a> (CCLA). Please consult with your employer to 
determine whether this is necessary.</p>
-<h3 id="licensing">Licensing</h3>
-<p>All contributions to ATR are licensed under the <a 
href="https://www.apache.org/licenses/LICENSE-2.0";>Apache License 2.0</a>. By 
submitting a pull request, you agree that your contributions will be licensed 
under this license. If you include any third party code or dependencies in your 
contribution, you must ensure that they are compatible with the Apache License 
2.0. The ASF maintains a list of <a 
href="https://www.apache.org/legal/resolved.html#category-a";>Category A 
licenses</a> th [...]
-<h3 id="code-of-conduct">Code of conduct</h3>
-<p>All contributors to ATR are expected to follow the <a 
href="https://www.apache.org/foundation/policies/conduct.html";>ASF Code of 
Conduct</a>, and any other applicable policies of the ASF.</p>
-<h2 id="special-considerations-for-atr">Special considerations for ATR</h2>
-<p>ATR is developed by ASF Tooling, which is an initiative of the ASF rather 
than a top-level project (TLP). This means that ATR follows the development 
processes and governance structure of Tooling, which may differ slightly from 
those of other ASF projects. There are also significant security considerations 
for ATR, which places additional requirements on contributions.</p>
-<h3 id="security-focus">Security focus</h3>
-<p>The primary goal of ATR is to deter and minimize supply chain attacks on 
ASF software releases. Since security is our highest priority, we scrutinize 
all contributions for potential vulnerabilities. To assist us when you make a 
contribution, please:</p>
-<ul>
-<li>Follow secure coding practices. Review best practice guidelines to learn 
how to avoid vulnerabilities such as injection attacks, cross-site scripting, 
and insecure deserialization.</li>
-<li>Validate all user inputs and sanitize all outputs.</li>
-<li>Use well established, independently audited, and actively maintained 
libraries rather than implementing cryptographic or security sensitive 
functionality yourself.</li>
-<li>Always consider the security implications of your changes. If you are 
unsure of the implications of your changes, ask the team for guidance.</li>
-<li>Report any security issues you discover in ATR responsibly. Do not open a 
public issue for security vulnerabilities. Instead, follow the <a 
href="https://www.apache.org/security/";>ASF security reporting process</a>.</li>
-</ul>
-<h3 id="high-quality-standards">High quality standards</h3>
-<p>Because of the critical nature of ATR, we maintain very high standards for 
code quality. This means that the review process may take longer than you 
expect, and we may request more extensive changes than you are accustomed to. 
We appreciate your patience and understanding. Our goal is to ensure that ATR 
remains as secure and reliable as possible.</p>
-<h3 id="access-controls">Access controls</h3>
-<p>We strongly encourage all contributors to enable two-factor authentication 
on their GitHub accounts, preferably with a <a 
href="https://en.wikipedia.org/wiki/WebAuthn#Passkey_branding";>passkey</a>.</p>
-<h2 id="getting-help">Getting help</h2>
-<p>If you have questions about contributing to ATR, or if you need help with 
any step of the contribution process, please reach out to the team. You can:</p>
-<ul>
-<li>Ask questions on the <a 
href="https://lists.apache.org/[email protected]";>dev mailing 
list</a>, which is the primary forum for ATR development discussions.</li>
-<li>Comment on the relevant issue or pull request in the <a 
href="https://github.com/apache/tooling-trusted-releases/issues";>issue 
tracker</a>.</li>
-<li>Chat with us in the <a 
href="https://the-asf.slack.com/archives/C049WADAAQG";>#apache-trusted-releases 
channel</a> on ASF Slack.</li>
-<li>Read the rest of the <a href="developer-guide">developer guide</a> for 
detailed information about how ATR works and how to make changes to it.</li>
-</ul>
-<p>We welcome all types of contribution, and are happy to help you get 
started. Thank you for your interest in contributing to ATR.</p>
diff --git a/atr/docs/index.html b/atr/docs/index.html
deleted file mode 100644
index a043467..0000000
--- a/atr/docs/index.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<h1 id="apache-trusted-releases-atr-documentation">Apache Trusted Releases 
(ATR) documentation</h1>
-<p>Welcome to the documentation for the <strong>ATR</strong> platform.</p>
-<p>NOTE: This documentation is a work in progress.</p>
-<h2 id="table-of-contents">Table of contents</h2>
-<ul>
-<li><code>1.</code> <a href="introduction-to-atr">Introduction to ATR</a></li>
-<li><code>2.</code> <a href="user-guide">User guide</a></li>
-<li><code>3.</code> <a href="developer-guide">Developer guide</a>
-<ul>
-<li><code>3.1.</code> <a href="running-the-server">Running the server</a></li>
-<li><code>3.2.</code> <a href="overview-of-the-code">Overview of the 
code</a></li>
-<li><code>3.3.</code> <a href="database">Database</a></li>
-<li><code>3.4.</code> <a href="storage-interface">Storage interface</a></li>
-<li><code>3.5.</code> <a href="user-interface">User interface</a></li>
-<li><code>3.6.</code> <a href="tasks">Tasks</a></li>
-<li><code>3.7.</code> <a href="build-processes">Build processes</a></li>
-<li><code>3.8.</code> <a href="running-and-creating-tests">Running and 
creating tests</a></li>
-<li><code>3.9.</code> <a href="code-conventions">Code conventions</a></li>
-<li><code>3.10.</code> <a href="how-to-contribute">How to contribute</a></li>
-</ul>
-</li>
-</ul>
diff --git a/atr/docs/introduction-to-atr.html 
b/atr/docs/introduction-to-atr.html
deleted file mode 100644
index 2d0f316..0000000
--- a/atr/docs/introduction-to-atr.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<h1 id="introduction-to-atr">1. Introduction to ATR</h1>
-<p><strong>Up</strong>: <a href=".">Documentation</a></p>
-<p><strong>Prev</strong>: (none)</p>
-<p><strong>Next</strong>: <code>2.</code> <a href="user-guide">User 
guide</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#what-is-atr">What is ATR?</a></li>
-<li><a href="#who-are-atr-users">Who are ATR users?</a></li>
-<li><a href="#what-is-atr-like-to-use">What is ATR like to use?</a></li>
-<li><a href="#who-develops-atr">Who develops ATR?</a></li>
-</ul>
-<h2 id="what-is-atr">What is ATR?</h2>
-<p>ATR is a platform through which committees of <a 
href="https://www.apache.org/";>Apache Software Foundation</a> (ASF) projects 
can make official ASF software releases. Official ASF releases are endorsed as 
an "<a 
href="https://www.apache.org/legal/release-policy.html#release-definition";>act 
of the Foundation</a>". It is therefore important that the foundation - its 
board, members, committees, and contributors - and the general public can have 
confidence in the releases.</p>
-<p>What sort of confidence in releases is required? All parties need to be 
certain that the software available for download is exactly that which was 
intended to be published by the applicable project management committee (PMC), 
and by the foundation. This may seem trivial, but software distribution 
platforms such as ATR now operate in extremely adversarial environments. In the 
years before ATR was launched, <a 
href="https://en.wikipedia.org/wiki/Supply_chain_attack";>supply chain attacks 
[...]
-<p>The end goal of supply chain attacks is almost always to cause harm to 
users. Harms are wide-ranging and can include unwanted features, the extraction 
of money from the user, surveillance and exfiltration of data, and material 
damage. The exact methods of supply chain attacks vary, but the general 
principle is to modify some legitimate software between the time that it was 
written and the time that it was received by the end user, without the 
modification being noticed. If software is [...]
-<p><strong>The goal of ATR is to deter and minimize the risk of supply chain 
attacks.</strong> ATR does not ensure the quality of software received 
legitimately from PMCs. The foundation as a whole, of course, has the goal of 
establishing the highest quality of software to be produced, but that is not 
the responsibility of ATR as a platform. The responsibility of ATR is to ensure 
that the software it distributes to end users is the legitimate submission of 
each of our constituent PMCs. I [...]
-<h2 id="who-are-atr-users">Who are ATR users?</h2>
-<p>There are two kinds of ATR user: our participants who use ATR to publish 
their software, and ASF software end users who use ATR to obtain that software. 
This guide is primarily written for the former, our participants who are 
publishing their software. Skilled end users may be interested in reading this 
guide for the purpose of learning the purported security claims that we make, 
reviewing the implementation strategies that we picked to achieve them, and 
ascertaining the likelihood th [...]
-<p>It is important to remember that security is a complex and rapidly evolving 
field, as the parties are involved in an ongoing game of cat and mouse. 
Software producers are often under tight budget and time constraints, forced to 
prioritize properties other than security, working in environments known to be 
insecure, using practices known to be suboptimal, and deploying to 
architectures with known vulnerabilities. Attackers race to find mistakes 
before producers, and use them to their o [...]
-<p>In this guide, we document how ATR is situated in this complex security 
landscape. But we also document the day-to-day operation of ATR: which forms to 
use, which buttons to press, how to make the release process simple, 
convenient, and well understood, but always with the goal of producing software 
as it was intended to be.</p>
-<h2 id="what-is-atr-like-to-use">What is ATR like to use?</h2>
-<p>Security of ASF release processes is the primary goal of ATR, but 
outstanding usability is also necessary to achieve this goal. The ASF has been 
in operation since 1999, and has needed release procedures from the very start. 
ATR is the next step in the evolution of those procedures, but the release 
managers (RMs) responsible for releasing ASF software are accustomed to the 
existing procedures. Convenience is a visceral property with a disproportionate 
effect. If ATR were secure but le [...]
-<p>As such, we offer a choice of interfaces when using ATR. We have a 
web-based interface, a JSON API, and a command-line interface (CLI). We try to 
make functionality as available as possible across all three interfaces. We 
also plan to add a text user interface (TUI), which is a kind of hybrid of the 
web-based interface and the CLI. The intention of having so many interfaces is 
that users can choose the ones which are most convenient for them at each 
step.</p>
-<p>Speaking of steps, what are the steps to release software on ATR? We have 
kept this as simple as possible. First, the project's participants compose a 
candidate release from existing files. Second, as per ASF policy, the PMC votes 
on that candidate release. Third, if the vote passes, the PMC officially 
publishes and announces the erstwhile candidate release as a finished, official 
release. That's the whole process for the majority of PMCs, but of course there 
are many details and cons [...]
-<h2 id="who-develops-atr">Who develops ATR?</h2>
-<p>ATR is developed by ASF Tooling, an ASF initiative launched in 2025, and 
responsible for streamlining development, automating repetitive tasks, reducing 
technical debt, and enhancing collaboration throughout the ASF. The source code 
of ATR is developed in public as open source code, and ASF Tooling welcomes 
high quality contributions to the codebase from external contributors, whether 
from existing ASF contributors or members of the public. Because of the 
stringent security and usabil [...]
-<p>This manual is an integral part of ATR, and contributions to this manual 
are therefore treated like any of the rest of the code. We welcome all types of 
contribution, whether that be writing entire pages or correcting small 
typographical errors. The easiest path to contribution is to <a 
href="https://github.com/apache/tooling-trusted-releases/compare";>create a pull 
request</a> on <a href="https://github.com/apache/tooling-trusted-releases";>our 
GitHub repository</a>. You can also <a hr [...]
diff --git a/atr/docs/overview-of-the-code.html 
b/atr/docs/overview-of-the-code.html
deleted file mode 100644
index d785866..0000000
--- a/atr/docs/overview-of-the-code.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<h1 id="overview-of-the-code">3.2. Overview of the code</h1>
-<p><strong>Up</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Prev</strong>: <code>3.1.</code> <a 
href="running-the-server">Running the server</a></p>
-<p><strong>Next</strong>: <code>3.3.</code> <a href="database">Database</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#introduction">Introduction</a></li>
-<li><a href="#hypercorn-and-asgi">Hypercorn and ASGI</a></li>
-<li><a href="#routes-and-database">Routes and database</a></li>
-<li><a href="#user-interface">User interface</a></li>
-<li><a href="#scheduling-and-tasks">Scheduling and tasks</a></li>
-<li><a href="#api">API</a></li>
-<li><a href="#other-important-interfaces">Other important interfaces</a></li>
-</ul>
-<h2 id="introduction">Introduction</h2>
-<p>This page is a high level view of the ATR code. References to symbols in 
this section are given without their <code>atr.</code> prefix, for brevity, and 
are linked to the respective source code. You should understand e.g. <a 
href="/ref/atr/server.py:app"><code>server.app</code></a> to mean 
<code>atr.server.app</code>.</p>
-<h2 id="hypercorn-and-asgi">Hypercorn and ASGI</h2>
-<p>ATR is an <a 
href="https://github.com/apache/infrastructure-asfquart";>ASFQuart</a> 
application, running in <a 
href="https://hypercorn.readthedocs.io/en/latest/index.html";>Hypercorn</a>. The 
entry point for Hypercorn is <a 
href="/ref/atr/server.py:app"><code>server.app</code></a>, which is then called 
with a few standard arguments <a 
href="https://asgi.readthedocs.io/en/latest/specs/main.html#overview";>as per 
the ASGI specification</a>. We create the <code>app</code> object using the f 
[...]
-<pre><code class="language-python">app = create_app(config.get())
-</code></pre>
-<p>The <a 
href="/ref/atr/server.py:create_app"><code>server.create_app</code></a> 
function performs a lot of setup, and if you're interested in how the server 
works then you can read it and the functions it calls to understand the process 
further. In general, however, when developing ATR we do not make modifications 
at the ASFQuart, Quart, and Hypercorn levels very often.</p>
-<h2 id="routes-and-database">Routes and database</h2>
-<p>Users request ATR pages over HTTPS, and the ATR server processes those 
requests in route handlers. Most of those handlers are in <a 
href="/ref/atr/get/"><code>get</code></a> and <a 
href="/ref/atr/post/"><code>post</code></a>, with some helper code in <a 
href="/ref/atr/shared/"><code>shared</code></a>. What each handler does varies, 
of course, from handler to handler, but most perform at least one access to the 
ATR SQLite database.</p>
-<p>The path of the SQLite database is configured in <a 
href="/ref/atr/config.py:SQLITE_DB_PATH"><code>config.AppConfig.SQLITE_DB_PATH</code></a>
 by default, and will usually appear as <code>state/atr.db</code> with related 
<code>shm</code> and <code>wal</code> files. We do not expect ATR to have so 
many users that we need to scale beyond SQLite.</p>
-<p>We use <a href="https://sqlmodel.tiangolo.com/";>SQLModel</a>, an ORM 
utilising <a href="https://docs.pydantic.dev/latest/";>Pydantic</a> and <a 
href="https://www.sqlalchemy.org/";>SQLAlchemy</a>, to create Python models for 
the ATR database. The core models file is <a 
href="/ref/atr/models/sql.py"><code>models.sql</code></a>. The most important 
individual SQLite models in this module are <a 
href="/ref/atr/models/sql.py:Committee"><code>Committee</code></a>, <a 
href="/ref/atr/models/sql. [...]
-<p>It is technically possible to interact with SQLite directly, but we do not 
do that in the ATR source. We use various interfaces in <a 
href="/ref/atr/db/__init__.py"><code>db</code></a> for reads, and interfaces in 
<a href="/ref/atr/storage/"><code>storage</code></a> for writes. We plan to 
move the <code>db</code> code into <code>storage</code> too eventually, because 
<code>storage</code> is designed to have read components and write components. 
There is also a legacy <a href="/ref/atr [...]
-<p>These interfaces, including route handlers in <a 
href="/ref/atr/get/"><code>get</code></a>, <a 
href="/ref/atr/post/"><code>post</code></a>, and <a 
href="/ref/atr/shared/"><code>shared</code></a>, along with <a 
href="/ref/atr/models/sql.py"><code>models.sql</code></a> and <a 
href="/ref/atr/storage/"><code>storage</code></a>, are where the majority of 
activity happens when developing ATR.</p>
-<h2 id="user-interface">User interface</h2>
-<p>ATR provides a web interface for users to interact with the platform, and 
the implementation of that interface is split across several modules. The web 
interface uses server-side rendering almost entirely, where HTML is generated 
on the server and sent to the browser.</p>
-<p>The template system in ATR is <a 
href="https://jinja.palletsprojects.com/";>Jinja2</a>, always accessed through 
the ATR <a href="/ref/atr/template.py"><code>template</code></a> module. 
Template files in Jinja2 syntax are stored in <a 
href="/ref/atr/templates/"><code>templates/</code></a>, and route handlers 
render them using the asynchronous <a 
href="/ref/atr/template.py:render"><code>template.render</code></a> 
function.</p>
-<p>Template rendering can be slow if templates are loaded from disk on every 
request. To address this, we use <a 
href="/ref/atr/preload.py"><code>preload</code></a> to load all templates into 
memory before the server starts serving requests. The <a 
href="/ref/atr/preload.py:setup_template_preloading"><code>preload.setup_template_preloading</code></a>
 function registers a startup hook that finds and caches every template 
file.</p>
-<p>The ATR user interface includes many HTML forms. We use <a 
href="https://docs.pydantic.dev/latest/";>Pydantic</a> for form handling, 
accessed through the ATR <a href="/ref/atr/form.py"><code>form</code></a> 
module. The <a href="/ref/atr/form.py:Form"><code>form.Form</code></a> base 
class extends <code>pydantic.BaseModel</code> to provide form-specific 
functionality. Each form field is defined using Pydantic type annotations along 
with the <a href="/ref/atr/form.py:label"><code>form.lab [...]
-<p>In addition to templates, we sometimes need to generate HTML 
programmatically in Python. For this we use <a 
href="https://htpy.dev/";>htpy</a>, another third party library, for building 
HTML using Python syntax. The ATR <a 
href="/ref/atr/htm.py"><code>htm</code></a> module extends htpy with a <a 
href="/ref/atr/htm.py:Block"><code>Block</code></a> class that makes it easier 
to build complex HTML structures incrementally. Using htpy means that we get 
type checking for our HTML generation [...]
-<p>Refer to the <a href="user-interface">full user interface development 
guide</a> for more information about this topic.</p>
-<h2 id="scheduling-and-tasks">Scheduling and tasks</h2>
-<p>Many operations in ATR are too slow to run during an HTTP request, so we 
run them asynchronously in background worker processes. The task scheduling 
system in ATR is built from three components: a task queue stored in the SQLite 
database, a worker manager that spawns and monitors worker processes, and the 
worker processes themselves that claim and execute tasks.</p>
-<p>The task queue is stored in a table defined by the <a 
href="/ref/atr/models/sql.py:Task"><code>Task</code></a> model in <a 
href="/ref/atr/models/sql.py"><code>models.sql</code></a>. Each task has a 
status, a type, arguments encoded as JSON, and metadata such as when it was 
added and which user created it. When route handlers need to perform slow 
operations, they create a new <code>Task</code> row with status 
<code>QUEUED</code> and commit it to the database.</p>
-<p>The ATR <a href="/ref/atr/manager.py"><code>manager</code></a> module 
provides the <a 
href="/ref/atr/manager.py:WorkerManager"><code>WorkerManager</code></a> class, 
which maintains a pool of worker processes. When the ATR server starts, the 
manager spawns a configurable number of worker processes and monitors them 
continuously. The manager checks every few seconds whether workers are still 
running, whether any tasks have exceeded their time limits, and whether the 
worker pool needs to [...]
-<p>The ATR <a href="/ref/atr/worker.py"><code>worker</code></a> module 
implements the workers. Each worker process runs in a loop. It claims the 
oldest queued task from the database, executes it, records the result, and then 
claims the next task atomically using an <code>UPDATE ... WHERE</code> 
statement. After a worker has processed a fixed number of tasks, it exits 
voluntarily to help to avoid memory leaks. The manager then spawns a fresh 
worker to replace it. Task execution happens in [...]
-<p>Tasks themselves are defined in the ATR <a 
href="/ref/atr/tasks/"><code>tasks</code></a> directory. The <a 
href="/ref/atr/tasks/__init__.py"><code>tasks</code></a> module contains 
functions for queueing tasks and resolving task types to their handler 
functions. Task types include operations such as importing keys, generating 
SBOMs, sending messages, and importing files from SVN. The most common category 
of task is automated checks on release artifacts. These checks are implemented 
in  [...]
-<h2 id="api">API</h2>
-<p>The ATR API provides programmatic access to most ATR functionality. API 
endpoints are defined in <a 
href="/ref/atr/api/__init__.py"><code>api</code></a>, and their URL paths are 
prefixed with <code>/api/</code>. The API uses <a 
href="https://www.openapis.org/";>OpenAPI</a> for documentation, which is 
automatically generated from the endpoint definitions and served at 
<code>/api/docs</code>. Users send requests with a <a 
href="https://en.wikipedia.org/wiki/JSON_Web_Token";>JWT</a> create [...]
-<p>API request and response models are defined in <a 
href="/ref/atr/models/api.py"><code>models.api</code></a> using Pydantic. Each 
endpoint has an associated request model that validates incoming data, and a 
response model that validates outgoing data. The API returns JSON in all cases, 
with appropriate HTTP status codes.</p>
-<h2 id="other-important-interfaces">Other important interfaces</h2>
-<p>ATR uses ASF OAuth for user login, and then determines what actions each 
user can perform based on their committee memberships. The ATR <a 
href="/ref/atr/principal.py"><code>principal</code></a> module handles 
authorization by checking whether users are members of relevant committees. It 
queries and caches LDAP to get committee membership information. The <a 
href="/ref/atr/principal.py:Authorisation"><code>Authorisation</code></a> class 
provides methods to check whether a user is a me [...]
-<p>The server configuration in <a 
href="/ref/atr/config.py"><code>config</code></a> determines a lot of global 
state, and the <a href="/ref/atr/util.py"><code>util</code></a> module contains 
lots of useful code which is used throughout ATR.</p>
diff --git a/atr/docs/running-and-creating-tests.html 
b/atr/docs/running-and-creating-tests.html
deleted file mode 100644
index c957c65..0000000
--- a/atr/docs/running-and-creating-tests.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<h1 id="running-and-creating-tests">3.8. Running and creating tests</h1>
-<p><strong>Up</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Prev</strong>: <code>3.7.</code> <a href="build-processes">Build 
processes</a></p>
-<p><strong>Next</strong>: <code>3.9.</code> <a href="code-conventions">Code 
conventions</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#running-tests">Running tests</a></li>
-<li><a href="#creating-tests">Creating tests</a></li>
-</ul>
-<h2 id="running-tests">Running tests</h2>
-<p>We currently only have end-to-end browser tests, but we plan to expand 
these as part of <a 
href="https://github.com/apache/tooling-trusted-releases/issues/209";>Issue 
#209</a>. Meanwhile, these browser tests serve as a simple consistency check 
when developing ATR.</p>
-<p>To run the tests, you will need Docker. Other OCI runtimes should work, but 
you will need to edit the test scripts accordingly.</p>
-<h3 id="using-docker-compose">Using Docker Compose</h3>
-<p>The simplest way to run the tests is using Docker Compose, which starts 
both ATR and the Playwright test container:</p>
-<pre><code class="language-shell">sh tests/run-tests.sh
-</code></pre>
-<p>This uses <a 
href="/ref/tests/docker-compose.yml"><code>tests/docker-compose.yml</code></a> 
to orchestrate the test environment. The ATR server runs in one container and 
the Playwright tests run in another, connected via a Docker network. These 
tests are automatically run in our GitHub CI as part of <a 
href="/ref/.github/workflows/build.yml"><code>.github/workflows/build.yml</code></a>.</p>
-<h3 id="using-host-networking">Using host networking</h3>
-<p>If you already have ATR running locally with <code>make serve-local</code>, 
you can run the Playwright tests directly against it instead of using Docker 
Compose:</p>
-<pre><code class="language-shell">make build-playwright &amp;&amp; make 
run-playwright
-</code></pre>
-<p>Where the two <code>make</code> invocations correspond to:</p>
-<pre><code class="language-shell">docker build -t atr-playwright -f 
tests/Dockerfile.playwright playwright
-docker run --net=host -it atr-playwright python3 test.py --skip-slow
-</code></pre>
-<p>In other words, we build <a 
href="/ref/tests/Dockerfile.playwright"><code>tests/Dockerfile.playwright</code></a>,
 and then run <a 
href="/ref/playwright/test.py"><code>playwright/test.py</code></a> inside that 
container using host networking to access your locally running ATR instance. 
Replace <code>docker</code> with the name of your Docker-compatible OCI runtime 
to use an alternative runtime.</p>
-<h3 id="test-duration">Test duration</h3>
-<p>The tests should, as of 14 Oct 2025, take about 40 to 50 seconds to run in 
Docker Compose, and 20 to 25 seconds to run on the host. The last line of the 
test output should be <code>Tests finished successfully</code>, and if the 
tests do not complete successfully there should be an obvious Python 
backtrace.</p>
-<h2 id="creating-tests">Creating tests</h2>
-<p>You can add tests to <code>playwright/test.py</code>. If you're feeling 
particularly adventurous, you can add separate unit tests etc., but it's okay 
to add tests only to the Playwright test script until <a 
href="https://github.com/apache/tooling-trusted-releases/issues/209";>Issue 
#209</a> is resolved.</p>
-<h3 id="how-the-tests-work">How the tests work</h3>
-<p>The browser tests use <a href="https://playwright.dev/";>Playwright</a>, 
which is a cross-browser, cross-platform web testing framework. It's a bit like 
the older <a href="https://en.wikipedia.org/wiki/PhantomJS";>PhantomJS</a>, now 
discontinued, which allows you to operate a browser through scripting. 
Playwright took the same concept and improved the user experience by adding 
better methods for polling browser state. Most interactions with a browser take 
some time to complete, and in P [...]
-<p>We use the official Playwright OCI container, install a few dependencies 
(<code>apt-get</code> is available in the container), and then run 
<code>test.py</code>.</p>
-<p>The <code>test.py</code> script calls <a 
href="/ref/playwright/test.py:run_tests"><code>run_tests</code></a> from its 
<code>main</code>, which sets up all the context, but the main action takes 
place in <a href="/ref/playwright/test.py:test_all"><code>test_all</code></a>. 
This function removes any state accidentally left over from a previous run, 
then runs tests of certain components. Because ATR is stateful, the order of 
the tests is important. When adding a test, please be careful t [...]
-<p>We want to make it more clear which Playwright tests depend on which, and 
have more isolated tests. Reusing context, however, helps to speed up the 
tests.</p>
-<p>The actual test cases themselves tend to use helpers such as <a 
href="/ref/playwright/test.py:go_to_path"><code>go_to_path</code></a> and <a 
href="/ref/playwright/test.py:wait_for_path"><code>wait_for_path</code></a>, 
and then call <a 
href="https://docs.python.org/3/library/logging.html#logging.info";><code>logging.info</code></a>
 to print information to the console. Try to keep logging messages terse and 
informative.</p>
diff --git a/atr/docs/running-the-server.html b/atr/docs/running-the-server.html
deleted file mode 100644
index 8f5762b..0000000
--- a/atr/docs/running-the-server.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<h1 id="running-the-server">3.1. Running the server</h1>
-<p><strong>Up</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Prev</strong>: (none)</p>
-<p><strong>Next</strong>: <code>3.2.</code> <a 
href="overview-of-the-code">Overview of the code</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#introduction">Introduction</a></li>
-<li><a href="#get-the-source">Get the source</a></li>
-<li><a href="#install-dependencies">Install dependencies</a></li>
-<li><a href="#run-the-server">Run the server</a></li>
-<li><a href="#load-the-site">Load the site</a></li>
-</ul>
-<h2 id="introduction">Introduction</h2>
-<p>To develop ATR locally, we manage dependencies using <a 
href="https://docs.astral.sh/uv/";>uv</a>. To run ATR on ASF hardware, we run it 
in containers managed by Puppet, but since this guide is about development, we 
focus on using uv.</p>
-<h2 id="get-the-source">Get the source</h2>
-<p><a href="https://github.com/apache/tooling-trusted-releases/fork";>Fork the 
source code</a> of <a 
href="https://github.com/apache/tooling-trusted-releases";>ATR on GitHub</a>, 
and then <a 
href="https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository";>clone
 your fork locally</a>.</p>
-<p>There are lots of files and directories in the root of the ATR Git 
repository. The most important thing to know is that <code>atr/</code> contains 
the source code. ATR is a Python application based on <a 
href="https://github.com/apache/infrastructure-asfquart";>ASFQuart</a>, which is 
based on <a href="https://github.com/pallets/quart";>Quart</a>. The Quart web 
framework is an asynchronous version of <a 
href="https://github.com/pallets/flask";>Flask</a>, a very widely used 
synchronous web [...]
-<h2 id="install-dependencies">Install dependencies</h2>
-<p>To run ATR locally after cloning the source, you will need to install the 
following dependencies:</p>
-<ul>
-<li><a href="https://github.com/commonmark/cmark";>cmark</a> (optional; for 
rebuilding documentation)</li>
-<li>Any <a href="https://en.wikipedia.org/wiki/POSIX";>POSIX</a> compliant <a 
href="https://frippery.org/make/";>make</a></li>
-<li><a href="https://github.com/FiloSottile/mkcert";>mkcert</a></li>
-<li><a href="https://www.python.org/downloads/release/python-3138/";>Python 
3.13</a></li>
-<li><a href="https://docs.astral.sh/uv/#installation";>uv</a></li>
-</ul>
-<p>You can install Python 3.13 through your package manager or through uv. 
Here is how to install these dependencies on <a 
href="https://en.wikipedia.org/wiki/Alpine_Linux";>Alpine Linux</a>:</p>
-<pre><code class="language-shell">apk add cmark curl git make mkcert@testing
-curl -LsSf https://astral.sh/uv/install.sh | env 
UV_INSTALL_DIR="/usr/local/bin" sh
-uv python install 3.13
-</code></pre>
-<p>For macOS these instructions become:</p>
-<pre><code class="language-shell">brew install cmark mkcert
-curl -LsSf https://astral.sh/uv/install.sh | sh
-rehash
-uv python install 3.13
-</code></pre>
-<p>ATR should work in any POSIX style environment.</p>
-<h2 id="run-the-server">Run the server</h2>
-<p>Then, to run the server:</p>
-<pre><code class="language-shell">cd tooling-trusted-releases/
-mkdir state
-make certs-local
-make serve-local
-</code></pre>
-<p>The <code>certs-local</code> step runs <code>mkcert localhost.apache.org 
localhost 127.0.0.1 ::1</code> to generate a locally trusted TLS certificate. 
If the certificate is not trusted, you may have to follow the <a 
href="https://github.com/FiloSottile/mkcert/blob/master/README.md";>mkcert 
guide</a> to resolve the issue.</p>
-<p>ATR requires TLS even for development because login is performed through 
the actual ASF OAuth server. This way, the development behavior aligns closely 
with the production behavior. We try to minimize differences between 
development and production environments.</p>
-<h2 id="load-the-site">Load the site</h2>
-<p>ATR will then be served on various hosts, but we recommend using only 
<code>localhost.apache.org</code>. This requires adding an entry to your 
<code>/etc/hosts</code> and potentially restarting your DNS server. If you do 
this, the following link should work:</p>
-<p><a 
href="https://localhost.apache.org:8080/";><code>https://localhost.apache.org:8080/</code></a></p>
-<p>If you do not want to change your <code>/etc/hosts</code>, you can use 
<code>127.0.0.1</code>. You should not use <code>localhost</code>. The 
following link should work:</p>
-<p><a 
href="https://127.0.0.1:8080/";><code>https://127.0.0.1:8080/</code></a></p>
-<p>Pick one or the other, because logging into the site on one host does not 
log you in to the site on any other host.</p>
-<p>It will take one or two minutes for the server to fetch committee and 
project information from the ASF website. Until the fetch is complete, no 
existing committees and projects will show.</p>
-<p>Developers without LDAP credentials will be unable to perform 
<code>rsync</code> writes and certain tasks may also fail. To enable these 
actions to succeed, visit <code>/user/cache</code> and press the "Cache me!" 
button. This writes your session information to the ATR state directory, where 
it will be consulted instead of an LDAP lookup if it exists. The same page also 
allows you to clear your session cache data. When you clear your session cache 
data, the <code>atr/principal.py</cod [...]
diff --git a/atr/docs/storage-interface.html b/atr/docs/storage-interface.html
deleted file mode 100644
index af3c8df..0000000
--- a/atr/docs/storage-interface.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<h1 id="storage-interface">3.4. Storage interface</h1>
-<p><strong>Up</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Prev</strong>: <code>3.3.</code> <a href="database">Database</a></p>
-<p><strong>Next</strong>: <code>3.5.</code> <a href="user-interface">User 
interface</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#introduction">Introduction</a></li>
-<li><a href="#how-do-we-read-from-storage">How do we read from 
storage?</a></li>
-<li><a href="#how-do-we-write-to-storage">How do we write to storage?</a></li>
-<li><a href="#how-do-we-add-new-storage-functionality">How do we add new 
storage functionality?</a></li>
-<li><a href="#how-do-we-use-outcomes">How do we use outcomes?</a></li>
-<li><a href="#what-about-audit-logging">What about audit logging?</a></li>
-</ul>
-<h2 id="introduction">Introduction</h2>
-<p>All database writes, and some reads, in ATR go through the <a 
href="/ref/atr/storage/__init__.py"><code>storage</code></a> interface. This 
interface <strong>enforces permissions</strong>, <strong>centralizes audit 
logging</strong>, and <strong>provides type-safe access</strong> to the 
database. In other words, avoid calling <a 
href="/ref/atr/db/__init__.py"><code>db</code></a> directly in route handlers 
if possible.</p>
-<p>The storage interface recognizes several permission levels: general public 
(unauthenticated visitors), foundation committer (any ASF account), committee 
participant (committers and PMC members), committee member (PMC members only), 
and foundation admin (infrastructure administrators). Each level inherits from 
the previous one, so for example committee members can do everything committee 
participants can do, plus additional operations.</p>
-<p>The storage interface does not make it impossible to bypass authorization, 
because you can always import <code>db</code> directly and write to the 
database. But it makes bypassing authorization an explicit choice that requires 
deliberate action, and it makes the safer path the easier path. This is a 
pragmatic approach to security: we cannot prevent all mistakes, but we can make 
it harder to make them accidentally.</p>
-<h2 id="how-do-we-read-from-storage">How do we read from storage?</h2>
-<p>Reading from storage is a work in progress. There are some existing 
methods, but most of the functionality is currently in <code>db</code> or 
<code>db.interaction</code>, and much work is required to migrate this to the 
storage interface. We have given this less priority because reads are generally 
safe, with the exception of a few components such as user tokens, which should 
be given greater migration priority.</p>
-<h2 id="how-do-we-write-to-storage">How do we write to storage?</h2>
-<p>To write to storage we open a write session, request specific permissions, 
use the exposed functionality, and then handle the outcome. Here is an actual 
example from <a 
href="/ref/atr/post/start.py"><code>post/start.py</code></a>:</p>
-<pre><code class="language-python">async with storage.write(session) as write:
-    wacp = await write.as_project_committee_participant(project_name)
-    new_release, _project = await wacp.release.start(project_name, version)
-</code></pre>
-<p>The <code>wacp</code> object, short for <code>w</code>rite <code>a</code>s 
<code>c</code>ommittee <code>p</code>articipant, provides access to 
domain-specific writers: <code>announce</code>, <code>checks</code>, 
<code>distributions</code>, <code>keys</code>, <code>policy</code>, 
<code>project</code>, <code>release</code>, <code>sbom</code>, 
<code>ssh</code>, <code>tokens</code>, and <code>vote</code>.</p>
-<p>The write session takes an optional <a 
href="/ref/atr/web.py:Committer"><code>Committer</code></a> or ASF UID, 
typically <code>session.uid</code> from the logged-in user. If you omit the 
UID, the session determines it automatically from the current request context. 
The write object checks LDAP memberships and raises <a 
href="/ref/atr/storage/__init__.py:AccessError"><code>storage.AccessError</code></a>
 if the user is not authorized for the requested permission level.</p>
-<p>Because projects belong to committees, we provide <a 
href="/ref/atr/storage/__init__.py:as_project_committee_member"><code>write.as_project_committee_member(project_name)</code></a>
 and <a 
href="/ref/atr/storage/__init__.py:as_project_committee_participant"><code>write.as_project_committee_participant(project_name)</code></a>,
 which look up the project's committee and authenticate the user as a member or 
participant of that committee. This is convenient when, for example, the URL 
prov [...]
-<p>Here is a more complete example from <a 
href="/ref/atr/api/__init__.py"><code>api/__init__.py</code></a> that shows the 
classic three step pattern:</p>
-<pre><code class="language-python">async with storage.write(asf_uid) as write:
-    # 1. Request permissions
-    wafc = write.as_foundation_committer()
-
-    # 2. Use the exposed functionality
-    outcome = await wafc.keys.ensure_stored_one(data.key)
-
-    # 3. Handle the outcome
-    key = outcome.result_or_raise()
-</code></pre>
-<p>In this case we decide to raise as soon as there is any error. We could 
also choose to display a warning, ignore the error, collect multiple outcomes 
for batch processing, or handle it in any other way appropriate for the 
situation.</p>
-<h2 id="how-do-we-add-new-storage-functionality">How do we add new storage 
functionality?</h2>
-<p>Add methods to classes in the <a 
href="/ref/atr/storage/writers/"><code>storage/writers</code></a> or <a 
href="/ref/atr/storage/readers/"><code>storage/readers</code></a> directories. 
Code to perform any action associated with public keys that involves writing to 
storage, for example, goes in <a 
href="/ref/atr/storage/writers/keys.py"><code>storage/writers/keys.py</code></a>.</p>
-<p>Classes in writer and reader modules must be named to match the permission 
hierarchy:</p>
-<pre><code class="language-python">class GeneralPublic:
-    def __init__(
-        self,
-        write: storage.Write,
-        write_as: storage.WriteAsGeneralPublic,
-        data: db.Session,
-    ) -&gt; None:
-        self.__write = write
-        self.__write_as = write_as
-        self.__data = data
-
-class FoundationCommitter(GeneralPublic):
-    def __init__(
-        self,
-        write: storage.Write,
-        write_as: storage.WriteAsFoundationCommitter,
-        data: db.Session
-    ) -&gt; None:
-        super().__init__(write, write_as, data)
-        self.__write = write
-        self.__write_as = write_as
-        self.__data = data
-
-class CommitteeParticipant(FoundationCommitter):
-    def __init__(
-        self,
-        write: storage.Write,
-        write_as: storage.WriteAsCommitteeParticipant,
-        data: db.Session,
-        committee_name: str,
-    ) -&gt; None:
-        super().__init__(write, write_as, data)
-        self.__committee_name = committee_name
-
-class CommitteeMember(CommitteeParticipant):
-    ...
-</code></pre>
-<p>This hierarchy that this creates is: <code>GeneralPublic</code> → 
<code>FoundationCommitter</code> → <code>CommitteeParticipant</code> → 
<code>CommitteeMember</code>. You can add methods at any level. A method on 
<code>CommitteeMember</code> is only available to committee members, while a 
method on <code>FoundationCommitter</code> is available to everyone who has 
logged in.</p>
-<p>Use <code>__private_methods</code> for helper code that is not part of the 
public interface. Use <code>public_methods</code> for operations that should be 
available to callers at the appropriate permission level. Consider returning <a 
href="/ref/atr/storage/outcome.py:Outcome"><code>Outcome</code></a> types to 
allow callers flexibility in error handling. Refer to the <a 
href="#how-do-we-use-outcomes">section on using outcomes</a> for more 
details.</p>
-<p>After adding a new writer module, register it in the appropriate 
<code>WriteAs*</code> classes in <a 
href="/ref/atr/storage/__init__.py"><code>storage/__init__.py</code></a>. For 
example, when adding the <code>distributions</code> writer, it was necessary to 
add <code>self.distributions = writers.distributions.CommitteeMember(write, 
self, data, committee_name)</code> to the <a 
href="/ref/atr/storage/__init__.py:WriteAsCommitteeMember"><code>WriteAsCommitteeMember</code></a>
 class.</p>
-<h2 id="how-do-we-use-outcomes">How do we use outcomes?</h2>
-<p>Consider using <strong>outcome types</strong> from <a 
href="/ref/atr/storage/outcome.py"><code>storage.outcome</code></a> when 
returning results from writer methods. Outcomes let you represent both success 
and failure without raising exceptions, which gives callers flexibility in how 
they handle errors.</p>
-<p>An <a 
href="/ref/atr/storage/outcome.py:Outcome"><code>Outcome[T]</code></a> is 
either a <a 
href="/ref/atr/storage/outcome.py:Result"><code>Result[T]</code></a> wrapping a 
successful value, or an <a 
href="/ref/atr/storage/outcome.py:Error"><code>Error[T]</code></a> wrapping an 
exception. You can check which it is with the <code>ok</code> property or 
pattern matching, extract the value with <code>result_or_raise()</code>, or 
extract the error with <code>error_or_raise()</code>.</p>
-<p>Here is an example from <a 
href="/ref/atr/post/keys.py"><code>post/keys.py</code></a> that processes 
multiple keys and collects outcomes:</p>
-<pre><code class="language-python">async with storage.write() as write:
-    wacm = write.as_committee_member(selected_committee)
-    outcomes = await wacm.keys.ensure_associated(keys_text)
-
-success_count = outcomes.result_count
-error_count = outcomes.error_count
-</code></pre>
-<p>The <code>ensure_associated</code> method returns an <a 
href="/ref/atr/storage/outcome.py:List"><code>outcome.List</code></a>, which is 
a collection of outcomes. Some keys might import successfully, and others might 
fail because they are malformed or already exist. The caller can inspect the 
list to see how many succeeded and how many failed, and present that 
information to the user.</p>
-<p>The <code>outcome.List</code> class provides many useful methods: <a 
href="/ref/atr/storage/outcome.py:results"><code>results()</code></a> to get 
only the successful values, <a 
href="/ref/atr/storage/outcome.py:errors"><code>errors()</code></a> to get only 
the exceptions, <a 
href="/ref/atr/storage/outcome.py:result_count"><code>result_count</code></a> 
and <a 
href="/ref/atr/storage/outcome.py:error_count"><code>error_count</code></a> to 
count them, and <a href="/ref/atr/storage/outcome [...]
-<p>Use outcomes when an operation might fail for some items but succeed for 
others, or when you want to give the caller control over error handling. Do not 
use them when failure should always raise an exception, such as authorization 
failures or database connection errors. Those should be raised immediately.</p>
-<h2 id="what-about-audit-logging">What about audit logging?</h2>
-<p>Storage write operations can be logged to <a 
href="/ref/atr/config.py:STORAGE_AUDIT_LOG_FILE"><code>config.AppConfig.STORAGE_AUDIT_LOG_FILE</code></a>,
 which is <code>state/storage-audit.log</code> by default. Each log entry is a 
JSON object containing the timestamp, the action name, and relevant parameters. 
When you write a storage method that should be audited, call 
<code>self.__write_as.append_to_audit_log(**kwargs)</code> with whatever 
parameters are relevant to that specific oper [...]
-<p>Audit logging must be done manually because the values to log are often 
those computed during method execution, not just those passed as arguments 
which could be logged automatically. When deleting a release, for example, we 
log <code>asf_uid</code> (instance attribute), <code>project_name</code> 
(argument), and <code>version</code> (argument), but when issuing a JWT from a 
PAT, we log <code>asf_uid</code> (instance attribute) and <code>pat_hash</code> 
(<em>computed</em>). Each operat [...]
diff --git a/atr/docs/tasks.html b/atr/docs/tasks.html
deleted file mode 100644
index 518aa8e..0000000
--- a/atr/docs/tasks.html
+++ /dev/null
@@ -1,53 +0,0 @@
-<h1 id="tasks">3.6. Tasks</h1>
-<p><strong>Up</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Prev</strong>: <code>3.5.</code> <a href="user-interface">User 
interface</a></p>
-<p><strong>Next</strong>: <code>3.7.</code> <a href="build-processes">Build 
processes</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#introduction">Introduction</a></li>
-<li><a href="#checks">Checks</a></li>
-</ul>
-<h2 id="introduction">Introduction</h2>
-<p>Tasks are computations run in the background in dedicated worker processes 
orchestrated by a manager.</p>
-<h2 id="checks">Checks</h2>
-<p>One important subset of task in ATR is the check. Checks are run when a 
release manager adds or modifies files in a release candidate draft. The 
outputs of the checks alert the release manager to potential issues.</p>
-<p>There are several checks for correctness that are already built out, and 
this how-to provides pointers for developers wishing to add new checks for 
relevant pieces of a release. Currently as of <code>alpha-2</code> ATR has 
checks for the following:</p>
-<ol>
-<li>Correct hashing (<code>sql.TaskType.HASHING_CHECK</code>, 
<code>hashing.check</code>)</li>
-<li>Compliant license (<code>sql.TaskType.LICENSE_FILES</code>, 
<code>license.files</code>)</li>
-<li>Compliant license headers (<code>sql.TaskType.LICENSE_HEADERS</code>, 
<code>license.headers</code>)</li>
-<li>File paths (<code>sql.TaskType.PATHS_CHECK</code>, 
<code>paths.check</code>)</li>
-<li>RAT results (<code>sql.TaskType.RAT_CHECK</code>, 
<code>rat.check</code>)</li>
-<li>Correct signature (<code>sql.TaskType.SIGNATURE_CHECK</code>, 
<code>signature.check</code>)</li>
-<li>Tarball integrity (<code>sql.TaskType.TARGZ_INTEGRITY</code>, 
<code>targz.integrity</code>)</li>
-<li>Tarball structure (<code>sql.TaskType.TARGZ_STRUCTURE</code>, 
<code>targz.structure</code>)</li>
-<li>Zip file integrity (<code>sql.TaskType.ZIPFORMAT_INTEGRITY</code>, 
<code>zipformat.integrity</code>)</li>
-<li>Zip file structure (<code>sql.TaskType.ZIPFORMAT_STRUCTURE</code>, 
<code>zipformat.structure</code>)</li>
-</ol>
-<h3 id="adding-a-task-check-module">Adding a task check module</h3>
-<p>In <code>atr/tasks/checks</code> you will find several modules that perform 
these check tasks, including <code>hashing.py</code>, <code>license.py</code>, 
etc. To write a new check task, add a module here that performs the checks 
needed.</p>
-<h3 id="importing-and-using-a-check-module">Importing and using a check 
module</h3>
-<p>In <code>atr/tasks/__init__.py</code> you will see imports for existing 
modules where you can add an import for new check task, for example:</p>
-<pre><code class="language-python">import atr.tasks.checks.hashing as hashing
-import atr.tasks.checks.license as license
-</code></pre>
-<p>And in the <code>resolve</code> function you will see where those modules 
are exercised where you can add a <code>case</code> statement for the new 
task:</p>
-<pre><code class="language-python">def resolve(task_type: sql.TaskType) -&gt; 
Callable[..., Awaitable[results.Results | None]]:  # noqa: C901
-    match task_type:
-        case sql.TaskType.HASHING_CHECK:
-            return hashing.check
-        case sql.TaskType.KEYS_IMPORT_FILE:
-            return keys.import_file
-        case sql.TaskType.LICENSE_FILES:
-            return license.files
-        case sql.TaskType.LICENSE_HEADERS:
-            return license.headers
-</code></pre>
-<h3 id="defining-a-task-type">Defining a task type</h3>
-<p>In <code>atr/models/sql.py</code> you will find the <code>TaskType</code> 
class where you can add a new mapping for the task:</p>
-<pre><code class="language-python">class TaskType(str, enum.Enum):
-    HASHING_CHECK = "hashing_check"
-    KEYS_IMPORT_FILE = "keys_import_file"
-    LICENSE_FILES = "license_files"
-    LICENSE_HEADERS = "license_headers"
-</code></pre>
diff --git a/atr/docs/user-guide.html b/atr/docs/user-guide.html
deleted file mode 100644
index f232dbc..0000000
--- a/atr/docs/user-guide.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<h1 id="user-guide">2. User guide</h1>
-<p><strong>Up</strong>: <a href=".">Documentation</a></p>
-<p><strong>Prev</strong>: <code>1.</code> <a 
href="introduction-to-atr">Introduction to ATR</a></p>
-<p><strong>Next</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#introduction">Introduction</a></li>
-</ul>
-<h2 id="introduction">Introduction</h2>
-<p>This is a work in progress. Meanwhile, you can read the <a 
href="/tutorial">ATR tutorial</a>.</p>
diff --git a/atr/docs/user-interface.html b/atr/docs/user-interface.html
deleted file mode 100644
index cf4527c..0000000
--- a/atr/docs/user-interface.html
+++ /dev/null
@@ -1,218 +0,0 @@
-<h1 id="user-interface">3.5. User interface</h1>
-<p><strong>Up</strong>: <code>3.</code> <a href="developer-guide">Developer 
guide</a></p>
-<p><strong>Prev</strong>: <code>3.4.</code> <a 
href="storage-interface">Storage interface</a></p>
-<p><strong>Next</strong>: <code>3.6.</code> <a href="tasks">Tasks</a></p>
-<p><strong>Sections</strong>:</p>
-<ul>
-<li><a href="#introduction">Introduction</a></li>
-<li><a href="#jinja2-templates">Jinja2 templates</a></li>
-<li><a href="#forms">Forms</a></li>
-<li><a href="#programmatic-html">Programmatic HTML</a></li>
-<li><a href="#the-htmblock-class">The htm.Block class</a></li>
-<li><a href="#how-a-route-renders-ui">How a route renders UI</a></li>
-</ul>
-<h2 id="introduction">Introduction</h2>
-<p>ATR uses server-side rendering almost exclusively: the server generates 
HTML and sends it to the browser, which displays it. We try to avoid 
client-side scripting, and in the rare cases where we need dynamic front end 
components we use plain TypeScript without recourse to any third party 
framework. (We have some JavaScript too, but we aim to use TypeScript only.) 
Sometimes we incur a full page load where perhaps it would be more ideal to 
update a fragment of the DOM in place, but page [...]
-<p>The UI is built from three main pieces: <a 
href="https://jinja.palletsprojects.com/";>Jinja2</a> for templates, <a 
href="https://wtforms.readthedocs.io/";>WTForms</a> for HTML forms, and <a 
href="https://htpy.dev/";>htpy</a> for programmatic HTML generation. We style 
everything with <a href="https://getbootstrap.com/";>Bootstrap</a>, which we 
customize slightly.</p>
-<h2 id="jinja2-templates">Jinja2 templates</h2>
-<p>Templates live in <a 
href="/ref/atr/templates/"><code>templates/</code></a>. Each template is a 
Jinja2 file that defines HTML structure with placeholders for dynamic content. 
Route handlers render templates by calling <a 
href="/ref/atr/template.py:render"><code>template.render</code></a>, which is 
an alias for <a 
href="/ref/atr/template.py:render_sync"><code>template.render_sync</code></a>. 
The function is asynchronous and takes a template name plus keyword arguments 
for the template  [...]
-<p>Here is an example from <a 
href="/ref/atr/get/keys.py:add"><code>get/keys.py</code></a>:</p>
-<pre><code class="language-python">return await template.render(
-    "keys-add.html",
-    asf_id=session.uid,
-    user_committees=participant_of_committees,
-    form=form,
-    key_info=key_info,
-    algorithms=shared.algorithms,
-)
-</code></pre>
-<p>The template receives these variables and can access them directly. If you 
pass a variable called <code>form</code>, the template can use <code>{{ form 
}}</code> to render it. <a 
href="https://jinja.palletsprojects.com/en/stable/templates/#list-of-control-structures";>Jinja2
 has control structures</a> like <code>{% for %}</code> and <code>{% if 
%}</code>, which you use when iterating over data or conditionally showing 
content.</p>
-<p>Templates are loaded into memory at server startup by <a 
href="/ref/atr/preload.py:setup_template_preloading"><code>preload.setup_template_preloading</code></a>.
 This means that changing a template requires restarting the server in 
development, which can be configured to happen automatically, but it also means 
that rendering is fast because we never do a disk read during request handling. 
The preloading scans <a href="/ref/atr/templates/"><code>templates/</code></a> 
recursively and ca [...]
-<p>Template rendering happens in a thread pool to avoid blocking the async 
event loop. The function <a 
href="/ref/atr/template.py:_render_in_thread"><code>_render_in_thread</code></a>
 uses <code>asyncio.to_thread</code> to execute Jinja2's synchronous 
<code>render</code> method.</p>
-<h2 id="forms">Forms</h2>
-<p>HTML forms in ATR are handled by <a 
href="https://docs.pydantic.dev/latest/";>Pydantic</a> models accessed through 
our <a href="/ref/atr/form.py"><code>form</code></a> module. Each form is a 
class that inherits from <a 
href="/ref/atr/form.py:Form"><code>form.Form</code></a>, which itself inherits 
from <code>pydantic.BaseModel</code>. Form fields are defined as class 
attributes using Pydantic type annotations, with the <a 
href="/ref/atr/form.py:label"><code>form.label</code></a> functio [...]
-<p>Here is a typical form definition from <a 
href="/ref/atr/shared/keys.py"><code>shared/keys.py</code></a>:</p>
-<pre><code class="language-python">class AddOpenPGPKeyForm(form.Form):
-    public_key: str = form.label(
-        "Public OpenPGP key",
-        'Your public key should be in ASCII-armored format, starting with 
"-----BEGIN PGP PUBLIC KEY BLOCK-----"',
-        widget=form.Widget.TEXTAREA,
-    )
-    selected_committees: form.StrList = form.label(
-        "Associate key with committees",
-        "Select the committees with which to associate your key.",
-    )
-
-    @pydantic.model_validator(mode="after")
-    def validate_at_least_one_committee(self) -&gt; "AddOpenPGPKeyForm":
-        if not self.selected_committees:
-            raise ValueError("You must select at least one committee to 
associate with this key")
-        return self
-</code></pre>
-<h3 id="field-types-and-labels">Field types and labels</h3>
-<p>The <a href="/ref/atr/form.py:label"><code>form.label</code></a> function 
is used to add metadata to Pydantic fields. The first argument is the label 
text, the second (optional) argument is documentation text that appears below 
the field, and you can pass additional keyword arguments like 
<code>widget=form.Widget.TEXTAREA</code> to specify the HTML widget type.</p>
-<p>Fields use Pydantic type annotations to define their data type:</p>
-<ul>
-<li><code>str</code> - text input (default widget: 
<code>Widget.TEXT</code>)</li>
-<li><code>form.Email</code> - email input with validation</li>
-<li><code>form.URL</code> - URL input with validation</li>
-<li><code>form.Bool</code> - checkbox</li>
-<li><code>form.Int</code> - number input</li>
-<li><code>form.StrList</code> - multiple checkboxes that collect strings</li>
-<li><code>form.File</code> - single file upload</li>
-<li><code>form.FileList</code> - multiple file upload</li>
-<li><code>form.Enum[EnumType]</code> - dropdown select from enum values</li>
-<li><code>form.Set[EnumType]</code> - multiple checkboxes from enum values</li>
-</ul>
-<p>Empty values for fields are allowed by default in most cases, but URL is an 
exception.</p>
-<p>The <code>widget</code> parameter in <a 
href="/ref/atr/form.py:label"><code>form.label</code></a> lets you override the 
default widget for a field type. Available widgets include: 
<code>TEXTAREA</code>, <code>CHECKBOXES</code>, <code>SELECT</code>, 
<code>RADIO</code>, <code>HIDDEN</code>, and others from the 
<code>form.Widget</code> enum. Common reasons to override:</p>
-<ul>
-<li>HIDDEN: for values passed from the route, not entered by the user</li>
-<li>TEXTAREA: for multi-line text input</li>
-<li>RADIO: for mutually exclusive choices</li>
-<li>CUSTOM: for fully custom rendering</li>
-</ul>
-<p>From <a 
href="/ref/atr/shared/projects.py:AddProjectForm"><code>projects.AddProjectForm</code></a>:</p>
-<pre><code class="language-python">committee_name: str = form.label("Committee 
name", widget=form.Widget.HIDDEN)
-</code></pre>
-<p>From <a 
href="/ref/atr/shared/resolve.py:SubmitForm"><code>resolve.SubmitForm</code></a>:</p>
-<pre><code class="language-python">email_body: str = form.label("Email body", 
widget=form.Widget.TEXTAREA)
-</code></pre>
-<p>From <a 
href="/ref/atr/shared/resolve.py:SubmitForm"><code>resolve.SubmitForm</code></a>:</p>
-<pre><code class="language-python">vote_result: Literal["Passed", "Failed"] = 
form.label("Vote result", widget=form.Widget.RADIO)
-</code></pre>
-<p>From <a 
href="/ref/atr/shared/vote.py:CastVoteForm"><code>vote.CastVoteForm</code></a>:</p>
-<pre><code class="language-python">decision: Literal["+1", "0", "-1"] = 
form.label("Your vote", widget=form.Widget.CUSTOM)
-</code></pre>
-<h3 id="using-forms-in-routes">Using forms in routes</h3>
-<p>To use a form in a route, use the <a 
href="/ref/atr/blueprints/post.py:committer"><code>@post.committer()</code></a> 
decorator to get the session and auth the user, and the <a 
href="/ref/atr/blueprints/post.py:form"><code>@post.form()</code></a> decorator 
to parse and validate input data:</p>
-<pre><code class="language-python">@post.committer("/keys/add")
[email protected](shared.keys.AddOpenPGPKeyForm)
-async def add(session: web.Committer, add_openpgp_key_form: 
shared.keys.AddOpenPGPKeyForm) -&gt; web.WerkzeugResponse:
-    """Add a new public signing key to the user's account."""
-    try:
-        key_text = add_openpgp_key_form.public_key
-        selected_committee_names = add_openpgp_key_form.selected_committees
-
-        # Process the validated form data...
-        async with storage.write() as write:
-            # ...
-
-        await quart.flash("OpenPGP key added successfully.", "success")
-    except web.FlashError as e:
-        await quart.flash(str(e), "error")
-    except Exception as e:
-        log.exception("Error adding OpenPGP key:")
-        await quart.flash(f"An unexpected error occurred: {e!s}", "error")
-
-    return await session.redirect(get.keys.keys)
-</code></pre>
-<p>The <a href="/ref/atr/form.py:validate"><code>form.validate</code></a> 
function should only be called manually when the request comes from JavaScript, 
as in <a 
href="/ref/atr/post/preview.py:announce_preview"><code>announce_preview</code></a>.
 It takes the form class, the form data dictionary, and an optional context 
dictionary. If validation succeeds, it returns an instance of your form class 
with validated data. If validation fails, it raises a 
<code>pydantic.ValidationError</code>.</p>
-<p>The error handling uses <a 
href="/ref/atr/form.py:flash_error_data"><code>form.flash_error_data</code></a> 
to prepare error information for display, and <a 
href="/ref/atr/form.py:flash_error_summary"><code>form.flash_error_summary</code></a>
 to create a user-friendly summary of all validation errors.</p>
-<h3 id="rendering-forms">Rendering forms</h3>
-<p>The <code>form</code> module provides the <a 
href="/ref/atr/form.py:render"><code>form.render</code></a> function (or <a 
href="/ref/atr/form.py:render_block"><code>form.render_block</code></a> for use 
with <a href="/ref/atr/htm.py:Block"><code>htm.Block</code></a>) that generates 
Bootstrap-styled HTML. This function creates a two-column layout with labels on 
the left and inputs on the right:</p>
-<pre><code class="language-python">form.render_block(
-    page,
-    model_cls=shared.keys.AddOpenPGPKeyForm,
-    action=util.as_url(post.keys.add),
-    submit_label="Add OpenPGP key",
-    cancel_url=util.as_url(keys),
-    defaults={
-        "selected_committees": committee_choices,
-    },
-)
-</code></pre>
-<p>The <code>defaults</code> parameter accepts a dictionary to populate 
initial field values. For checkbox/radio groups and select dropdowns, you can 
pass a list of <code>(value, label)</code> tuples to dynamically provide 
choices. The <code>render</code> function returns htpy elements which you can 
embed in templates or return directly from route handlers.</p>
-<p>Key rendering parameters:</p>
-<ul>
-<li><code>action</code> - form submission URL (defaults to current path)</li>
-<li><code>submit_label</code> - text for the submit button</li>
-<li><code>cancel_url</code> - if provided, adds a cancel link next to 
submit</li>
-<li><code>defaults</code> - dictionary of initial values or dynamic 
choices</li>
-<li><code>textarea_rows</code> - number of rows for textarea widgets (default: 
12)</li>
-<li><code>wider_widgets</code> - use wider input column (default: False)</li>
-<li><code>border</code> - add borders between fields (default: False)</li>
-</ul>
-<h2 id="programmatic-html">Programmatic HTML</h2>
-<p>Sometimes you need to generate HTML in Python rather than in a template. 
For this we use <a href="https://htpy.dev/";>htpy</a>, which provides a Python 
API for building HTML elements. You import <code>htpy</code> and then use it 
like this:</p>
-<pre><code class="language-python">import htpy
-
-element = htpy.div(".container")[
-    htpy.h1["Release Candidate"],
-    htpy.p["This is a release candidate."],
-]
-</code></pre>
-<p>The square brackets syntax is how htpy accepts children. The parentheses 
syntax is for attributes. If you want a div with an id, you write 
<code>htpy.div(id="content")</code>. If you want a div with a class, you can 
use CSS selector syntax like <code>htpy.div(".my-class")</code> or you can use 
<code>htpy.div(class_="my-class")</code>, remembering to use the underscore in 
<code>class_</code>.</p>
-<p>You can nest elements arbitrarily, mix strings and elements, and pass lists 
of elements. Converting an htpy element to a string renders it as HTML. 
Templates can therefore render htpy elements directly by passing them as 
variables.</p>
-<p>The htpy library provides type annotations for HTML elements. It does not 
validate attribute names or values, so you can pass nonsensical attributes 
without error. We plan to fix this by adding stricter types in our 
<code>htm</code> wrapper. The main benefit to using <code>htpy</code> (via 
<code>htm</code>) is having a clean Python API for HTML generation rather than 
concatenating strings or using templating.</p>
-<h2 id="the-htmblock-class">The htm.Block class</h2>
-<p>The ATR <a href="/ref/atr/htm.py"><code>htm</code></a> module extends htpy 
with a <a href="/ref/atr/htm.py:Block"><code>Block</code></a> class that makes 
it easier to build complex HTML structures incrementally. You create a block, 
append elements to it, and then collect them into a final element. Here is the 
typical usage pattern:</p>
-<pre><code class="language-python">import atr.htm as htm
-
-div = htm.Block()
-div.h1["Release Information"]
-div.p["The release was created on ", release.created.isoformat(), "."]
-if release.released:
-    div.p["It was published on ", release.released.isoformat(), "."]
-return div.collect()
-</code></pre>
-<p>The block class provides properties for common HTML elements like 
<code>h1</code>, <code>h2</code>, <code>p</code>, <code>div</code>, 
<code>ul</code>, and so on. When you access these properties, you get back a <a 
href="/ref/atr/htm.py:BlockElementCallable"><code>BlockElementCallable</code></a>,
 which you can call to create an element with attributes or use square brackets 
to add grandchildren of the block. The element is automatically appended to the 
block's internal list of children.</p>
-<p>The <code>collect</code> method assembles all of the elements into a single 
htpy element. If you created the block with an outer element like 
<code>htm.Block(htpy.div(".container"))</code>, that element wraps all the 
children. If you created the block with no outer element, <code>collect</code> 
wraps everything in a div. You can also pass a <code>separator</code> argument 
to <code>collect</code>, which inserts a text separator between elements.</p>
-<p>The block class is useful when you are building HTML in a loop or when you 
have conditional elements. Instead of managing a list of elements manually, you 
can let the block class do it for you: append elements as you go, and at the 
end call <code>collect</code> to get the final result. This is cleaner than 
concatenating strings or maintaining lists yourself.</p>
-<p>The block class also adds a <code>data-src</code> attribute to elements, 
which records which function created the element. If you see an element in the 
browser inspector with <code>data-src="atr.get.keys:keys"</code>, you know that 
it came from the <code>keys</code> function in <code>get/keys.py</code>. The 
source is extracted automatically using <a 
href="/ref/atr/log.py:caller_name"><code>log.caller_name</code></a>.</p>
-<h2 id="how-a-route-renders-ui">How a route renders UI</h2>
-<p>A typical route that renders UI first authenticates the user, loads data 
from the database, builds HTML using htpy, and renders it using a template. GET 
and POST requests are handled by separate routes, with form validation 
automatically handled by the <a 
href="/ref/atr/blueprints/post.py:form"><code>@post.form()</code></a> 
decorator. Here is a simplified example from <a 
href="/ref/atr/get/keys.py:add"><code>get/keys.py</code></a>:</p>
-<pre><code class="language-python">@get.committer("/keys/add")
-async def add(session: web.Committer) -&gt; str:
-    """Add a new public signing key to the user's account."""
-    async with storage.write() as write:
-        participant_of_committees = await write.participant_of_committees()
-
-    committee_choices = [(c.name, c.display_name or c.name) for c in 
participant_of_committees]
-
-    page = htm.Block()
-    page.p[htm.a(".atr-back-link", href=util.as_url(keys))["← Back to Manage 
keys"]]
-    page.div(".my-4")[
-        htm.h1(".mb-4")["Add your OpenPGP key"],
-        htm.p["Add your public key to use for signing release artifacts."],
-    ]
-
-    form.render_block(
-        page,
-        model_cls=shared.keys.AddOpenPGPKeyForm,
-        action=util.as_url(post.keys.add),
-        submit_label="Add OpenPGP key",
-        cancel_url=util.as_url(keys),
-        defaults={
-            "selected_committees": committee_choices,
-        },
-    )
-    ...
-    return await template.blank(
-        "Add your OpenPGP key",
-        content=page.collect(),
-        description="Add your public signing key to your ATR account.",
-    )
-</code></pre>
-<p>The route is decorated with <a 
href="/ref/atr/blueprints/get.py:committer"><code>@get.committer()</code></a>, 
which handles authentication and provides a <code>session</code> object that is 
an instance of <a 
href="/ref/atr/web.py:Committer"><code>web.Committer</code></a> with a range of 
useful properties and methods.</p>
-<p>The function builds the UI using an <a 
href="/ref/atr/htm.py:Block"><code>htm.Block</code></a> object, which provides 
a convenient API for incrementally building HTML. The form is rendered directly 
into the block using <a 
href="/ref/atr/form.py:render_block"><code>form.render_block()</code></a>, 
which generates all the necessary HTML with Bootstrap styling.</p>
-<p>Finally, the route returns the rendered HTML using <a 
href="/ref/atr/template.py:blank"><code>template.blank()</code></a>, which 
renders a minimal template with just a title and content area.</p>
-<p>Form submission is handled by a separate POST route:</p>
-<pre><code class="language-python">@post.committer("/keys/add")
[email protected](shared.keys.AddOpenPGPKeyForm)
-async def add(session: web.Committer, add_openpgp_key_form: 
shared.keys.AddOpenPGPKeyForm) -&gt; web.WerkzeugResponse:
-    """Add a new public signing key to the user's account."""
-    try:
-        key_text = add_openpgp_key_form.public_key
-        selected_committee_names = add_openpgp_key_form.selected_committees
-
-        # Process the validated form data...
-
-        await quart.flash("OpenPGP key added successfully.", "success")
-    except web.FlashError as e:
-        await quart.flash(str(e), "error")
-
-    return await session.redirect(get.keys.keys)
-</code></pre>
-<p>The <a 
href="/ref/atr/blueprints/post.py:form"><code>@post.form()</code></a> decorator 
handles form validation automatically. If validation fails, it flashes error 
messages and redirects back to the GET route. If validation succeeds, the 
validated form instance is injected into the route handler as a parameter.</p>
-<p>Bootstrap CSS classes are applied automatically by the form rendering 
functions. The functions use classes like <code>form-control</code>, 
<code>form-select</code>, <code>btn-primary</code>, <code>is-invalid</code>, 
and <code>invalid-feedback</code>. We currently use Bootstrap 5. If you 
generate HTML manually with htpy, you can apply Bootstrap classes yourself by 
using the CSS selector syntax like <code>htpy.div(".container")</code> or the 
class attribute like <code>htpy.div(class_="c [...]
diff --git a/atr/get/docs.py b/atr/get/docs.py
index 40e6047..b700433 100644
--- a/atr/get/docs.py
+++ b/atr/get/docs.py
@@ -59,7 +59,7 @@ async def page(session: web.Committer | None, page: str) -> 
str:
 
 
 async def _serve_docs_page(page: str) -> str:
-    docs_dir = pathlib.Path(config.get().PROJECT_ROOT) / "atr" / "docs"
+    docs_dir = pathlib.Path(config.get().PROJECT_ROOT) / "docs"
 
     if not page.endswith(".html"):
         page = f"{page}.html"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to