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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .pr_agent.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ push_commands = ["/agentic_review"]
[review_agent]
comments_location_policy = "both"
inline_comments_severity_threshold = 1

# Files owned by another project, not by this repo. The docs site is rendered by
# pi-docsite (PyPI), which writes these on every build; docs/*.md and docs/nav.json
# are authored here and stay in scope. A review comment on rendered HTML is a
# complaint about the generator, not about this repo, so it belongs upstream.
[ignore]
glob = ["docs/*.html", "docs/assets/*", "docs/llms.txt", "docs/llms-full.txt", "docs/search-index.json"]
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- Lint + static checks: `uvx prek run --all-files` (covers ruff, ruff-format, mypy, flake8, gitleaks, detect-secrets, eslint — the only supported way to run them)
- CI gate: `uvx tox` (runs the `unittests` and `unused-code` environments — this is what CI actually runs; `uvx` needs no install. Plain `tox` works if you have it globally)
- Full verify: `uvx prek run --all-files && uvx tox`
- Rebuild docs site: `uv run python scripts/generate_docs.py` (see [Documentation](#documentation))
- Rebuild docs site: `uvx pi-docsite --docs-dir docs --tagline "Keep GitHub pull requests moving with automated checks, approvals, labels, cherry-picks, and release workflows."` (see [Documentation](#documentation); keep this in sync with the same command below)

## Definition of Done
A task is complete when ALL pass:
Expand Down Expand Up @@ -43,17 +43,17 @@ See `docs/` for architecture and configuration reference docs.
`docs/*.md` is the source of truth. `docs/*.html`, `docs/search-index.json`, `docs/llms.txt` and `docs/llms-full.txt` are **generated** — never edit them by hand.

- Update or write the markdown: `docs/<slug>.md`
- Rebuild the site: `uv run python scripts/generate_docs.py` (run from repo root; idempotent, so a clean second run produces no diff)
- Rebuild the site: `uvx pi-docsite --docs-dir docs --tagline "Keep GitHub pull requests moving with automated checks, approvals, labels, cherry-picks, and release workflows."` (run from repo root; idempotent, so a clean second run produces no diff). `--project-name` and `--repo-url` are derived from the git remote — only the tagline needs repeating
- Preview: serve `docs/` over HTTP and open a page (e.g. `uv run python -m http.server -d docs 8000`) — do not open the `.html` via `file://`, the sidebar and search need HTTP
- `scripts/docs_render/` is a vendored copy of the renderer from `myk-org/docsfy` (Jinja templates + Pygments highlighting). It is deliberately committed so docs can be rebuilt without that repo present.
- The renderer is the `pi-docsite` package on PyPI, run through `uvx` — nothing is vendored in this repo and no docs build dependency is committed. Regenerate with whatever `pi-docsite` version is current.

Markdown rules that the renderer depends on:
- Every file starts with exactly one H1 — it becomes the page title and the sidebar label
- Every file has balanced ``` fences. A file whose first code block is missing its opening fence shifts every later block and corrupts the whole page
- Use `| tables |` for tabular data; tables inside a code fence render as raw text
- Cross-link other pages by slug: `[label](other-page.html)`

Adding a new page: create `docs/<slug>.md` with one H1, then add it to the navigation list in `scripts/generate_docs.py` — the sidebar is not auto-discovered, so a page missing from that list will not appear in navigation or search.
Adding a new page: create `docs/<slug>.md` with one H1, then add its slug to `docs/nav.json` — array order is display order. A page missing from `nav.json` is still built, but lands under a fallback group named `More` and the build prints a note naming it.

## When Writing Code

Expand Down
52 changes: 44 additions & 8 deletions docs/assets/copy.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,51 @@
(function() {
// Icon-only so it does not sit over the code on a short block. The word moves
// to title/aria-label, which still gives a tooltip and a screen-reader name.
var ICON_COPY =
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" ' +
'stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<rect x="5.5" y="5.5" width="8" height="9" rx="1.5"/>' +
'<path d="M10.5 3.5v-1a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h1"/></svg>';
var ICON_OK =
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" ' +
'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<path d="M2.5 8.5l3.5 3.5 7-8"/></svg>';
var ICON_FAIL =
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" ' +
'stroke-width="1.5" stroke-linecap="round" aria-hidden="true">' +
'<path d="M4 4l8 8M12 4l-8 8"/></svg>';

function setIcon(btn, icon, label) {
btn.innerHTML = icon;
btn.title = label;
btn.setAttribute('aria-label', label);
}

function flash(btn, icon, label, backTo, backLabel) {
setIcon(btn, icon, label);
btn.classList.add('copied');
setTimeout(function() {
setIcon(btn, backTo, backLabel);
btn.classList.remove('copied');
}, 2000);
}

document.querySelectorAll('pre').forEach(function(pre) {
// Idempotent: this file can run more than once (cached asset, a second
// injection, a client-side navigation). Without this guard every run left
// another copy-btn behind and the block grew a stack of them.
var existing = pre.querySelector('.copy-btn');
if (existing) existing.remove();
var btn = document.createElement('button');
btn.className = 'copy-btn';
btn.textContent = 'Copy';
btn.type = 'button';
setIcon(btn, ICON_COPY, 'Copy code');
btn.addEventListener('click', function() {
var code = pre.querySelector('code');
var text = code ? code.textContent : pre.textContent;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
flash(btn, ICON_OK, 'Copied', ICON_COPY, 'Copy code');
}).catch(function() {
fallbackCopy(text, btn);
});
Expand All @@ -28,14 +64,14 @@
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
var ok = false;
try {
document.execCommand('copy');
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
ok = document.execCommand('copy');
} catch (e) {
btn.textContent = 'Failed';
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
ok = false;
}
document.body.removeChild(textarea);
if (ok) flash(btn, ICON_OK, 'Copied', ICON_COPY, 'Copy code');
else flash(btn, ICON_FAIL, 'Copy failed', ICON_COPY, 'Copy code');
}
})();
50 changes: 42 additions & 8 deletions docs/assets/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,34 @@

var input = overlay.querySelector('.search-modal-input');
var results = overlay.querySelector('.search-modal-results');
var index = [];
var selectedIdx = -1;

// Load index
fetch('search-index.json').then(function(r) { return r.json(); })
.then(function(data) { index = data; }).catch(function() {});
// Load index. Normally fetched once and shared by every page, so nothing is
// inlined and the pages stay small. An inline copy is used when the generator
// was asked to embed it. Note: fetch() of a local file is blocked by CORS on
// file:// in most browsers, so opening the site straight off disk needs the
// inline mode; served over HTTP this is not an issue.
var index = Array.isArray(window.__DOCS_SEARCH_INDEX__) ? window.__DOCS_SEARCH_INDEX__ : null;
var indexError = null;
if (!index) {
fetch('search-index.json').then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}).then(function(data) {
index = data;
// The user may have typed before the fetch resolved. Their first query
// was answered with "Loading search index..." and never re-run, so the
// results stayed hidden until they edited the box again.
if (typeof render === 'function' && input.value.trim()) render();
Comment thread
myakove marked this conversation as resolved.
Resolved
}).catch(function(err) {
indexError = err;
console.error('[docs] search index unavailable:', err,
'— serve the site over HTTP, or build it with the search index inlined.');
// Same reason as the success path: a query typed while the request was
// pending would otherwise stay on "Loading search index..." forever, now
// that the request has failed.
if (typeof render === 'function' && input.value.trim()) render();
});
}

// Open/close
function openModal() {
Expand Down Expand Up @@ -65,12 +87,22 @@
}

// Search logic
input.addEventListener('input', function() {
var q = this.value.toLowerCase().trim();
function render() {
var q = input.value.toLowerCase().trim();
results.innerHTML = '';
selectedIdx = -1;
if (!q) return;

if (!index) {
var err = document.createElement('div');
err.className = 'search-no-results';
err.textContent = indexError
? 'Search needs the site served over HTTP (opening the file directly blocks it).'
: 'Loading search index...';
Comment thread
myakove marked this conversation as resolved.
Resolved
results.appendChild(err);
return;
}

var matches = index.filter(function(item) {
return item.title.toLowerCase().includes(q) || item.content.toLowerCase().includes(q);
}).slice(0, 10);
Expand Down Expand Up @@ -105,7 +137,9 @@
empty.textContent = 'No results found';
results.appendChild(empty);
}
});
}

input.addEventListener('input', render);

// Keyboard navigation
input.addEventListener('keydown', function(e) {
Expand Down
77 changes: 75 additions & 2 deletions docs/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@
--bg-tertiary: #44403c;
--bg-sidebar: #292524;
--bg-code: rgba(251, 191, 36, 0.15);
--bg-code-block: #1c1917;
/* Must differ from --bg-primary (#1c1917). It did not, so code blocks were
* invisible against the page in dark mode -- only the code text was legible
* and the block had no boundary at all. --bg-secondary is the palette's own
* next step up. */
--bg-code-block: #292524;
--bg-card: #292524;
--bg-search-result: #292524;

Expand Down Expand Up @@ -340,6 +344,10 @@ a:hover {
display: flex;
flex-direction: column;
min-height: 100vh;
/* Flex items default to min-width:auto, so a wide table inside made this grow
* past the viewport and put a scrollbar on the whole document. 0 lets it
* shrink so the table scrolls in its own box instead. */
min-width: 0;
transition: margin-left var(--transition-normal);
}

Expand Down Expand Up @@ -519,6 +527,33 @@ a:hover {
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.6;
/* Wrapping rather than scrolling: a clipped command is unreadable, and the
* horizontal scrollbar it produced also collided with the copy button in
* the block's top-right corner. Only 3 lines across docs/ exceed 110 chars
* and none of them are alignment-critical. */
white-space: pre-wrap;
overflow-wrap: break-word;
/* Reserve room for the absolutely-positioned copy button even when the
* block is a single short line. A one-line block is only ~22px tall while
* the button plus its top offset needs ~28px, and the block is an overflow
* container, so without this it grew a scrollbar. */
min-height: 34px;
/* Anchors the .copy-btn and .code-label, which are appended as children by
* copy.js and codelabels.js and positioned with position:absolute. Without
* this they resolve against the page instead of the code box, so the copy
* button and language label drift to wherever the block happens to sit. */
position: relative;
}

.article-body .table-wrap {
/* Wide tables scroll inside their own box instead of widening the page. */
overflow-x: auto;
margin-bottom: 24px;
-webkit-overflow-scrolling: touch;
}

.article-body .table-wrap > table {
margin-bottom: 0;
}

.article-body .highlight {
Expand All @@ -527,10 +562,15 @@ a:hover {
padding: 16px 20px;
overflow-x: auto;
margin-bottom: 24px;
/* Same reason as .article-body pre: this wrapper is what a .code-label is
* attached to when the block has one. */
position: relative;
}

.article-body .highlight pre {
margin: 0;
white-space: pre-wrap;
overflow-wrap: break-word;
background: transparent;
padding: 0;
color: var(--text-code-block);
Expand Down Expand Up @@ -1065,6 +1105,12 @@ details > ol:last-child {
}

@media (max-width: 480px) {
/* Below this the top bar cannot fit the brand and the search/badge cluster
* without crowding. The sidebar carries the same brand, and the hamburger
* opens it, so hiding the duplicate is cleaner than shrinking it. */
.top-bar-title {
display: none;
}
.index-title {
font-size: 1.5rem;
}
Expand Down Expand Up @@ -1129,7 +1175,19 @@ details > ol:last-child {
position: absolute;
top: 8px;
right: 8px;
padding: 4px 10px;
/* Above the code text and the block's own scrollbar, which both sit in the
* same corner region on a short block. */
z-index: 2;
/* Must stay shorter than one line of code (~22px at this font-size). The
* block is an overflow container, so a taller button makes a single-line
* block scroll vertically and produces a scrollbar next to the code. */
padding: 3px;
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
font-size: 0.75rem;
background: var(--bg-secondary);
color: var(--text-secondary);
Expand Down Expand Up @@ -1157,6 +1215,13 @@ pre:hover .copy-btn {
border-color: var(--accent);
}

/* Success/failure state: the icon changes, so colour alone carries the result. */
.copy-btn.copied {
background: var(--accent);
color: var(--accent-contrast);
border-color: var(--accent);
}

/* --------------------------------------------------------------------------
Prev/Next Page Navigation
-------------------------------------------------------------------------- */
Expand Down Expand Up @@ -1330,6 +1395,13 @@ pre:hover .copy-btn {
color: var(--text-primary);
text-decoration: none;
letter-spacing: -0.01em;
/* Never break the project name across lines. It sat in a cramped flex row
* next to the search field and badge, and wrapped to two lines at narrow
* widths, which reads as a layout bug rather than a design. */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}

.top-bar-title:hover {
Expand Down Expand Up @@ -1416,6 +1488,7 @@ pre:hover .copy-btn {
position: absolute;
top: 8px;
right: 60px;
z-index: 2;
font-size: 0.7rem;
color: var(--text-secondary);
text-transform: uppercase;
Expand Down
24 changes: 13 additions & 11 deletions docs/automation-recipes.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Automation Recipes - github-webhook-server</title>
<meta name="description" content="Keep GitHub pull requests moving with automated checks, approvals, labels, cherry-picks, and release workflows.">
<link rel="stylesheet" href="assets/style.css">
<link rel="stylesheet" href="assets/style.css?v=e7c0b4379e">
<script>try{var t=localStorage.getItem('theme');if(t==='dark')document.documentElement.setAttribute('data-theme','dark')}catch(e){}</script>
<link rel="alternate" type="text/plain" title="LLM Documentation Index" href="llms.txt">
<link rel="alternate" type="text/plain" title="LLM Full Documentation" href="llms-full.txt">
Expand Down Expand Up @@ -175,8 +175,8 @@ <h3 class="nav-group-title">Reference</h3>
<button class="top-bar-search-btn" id="search-trigger" aria-label="Search">
<kbd>&#8984;K</kbd> Search
</button>
<a href="https://github.com/myk-org/docsfy" target="_blank" rel="noopener" class="docsfy-badge" title="Generated with docsfy">
⚡ docs<span class="brand-accent">fy</span>
<a href="https://github.com/myk-org/github-webhook-server" target="_blank" rel="noopener" class="docsfy-badge" title="documentation">
⚡ pi<span class="brand-accent">-docsite</span>
</a>

<a href="https://github.com/myk-org/github-webhook-server" target="_blank" rel="noopener noreferrer" class="top-bar-github" id="github-link" data-repo-url="https://github.com/myk-org/github-webhook-server">
Expand Down Expand Up @@ -385,21 +385,22 @@ <h3>On this page</h3>

<footer class="page-footer">

Generated with <a href="https://github.com/myk-org/docsfy" target="_blank" rel="noopener">docs<span class="brand-accent">fy</span></a>
Built from <code>docs/*.md</code> by <code>pi-docsite</code>
<span class="footer-sep">&middot;</span>
<span class="footer-llm">LLM-friendly: <a href="llms.txt">llms.txt</a> &middot; <a href="llms-full.txt">llms-full.txt</a></span>
</footer>
</div>

<div class="sidebar-overlay" id="sidebar-overlay"></div>

<script src="assets/theme.js"></script>
<script src="assets/search.js"></script>
<script src="assets/copy.js"></script>
<script src="assets/callouts.js"></script>
<script src="assets/scrollspy.js"></script>
<script src="assets/codelabels.js"></script>
<script src="assets/github.js"></script>
<script src="assets/theme.js?v=e7c0b4379e"></script>

<script src="assets/search.js?v=e7c0b4379e"></script>
<script src="assets/copy.js?v=e7c0b4379e"></script>
<script src="assets/callouts.js?v=e7c0b4379e"></script>
<script src="assets/scrollspy.js?v=e7c0b4379e"></script>
<script src="assets/codelabels.js?v=e7c0b4379e"></script>
<script src="assets/github.js?v=e7c0b4379e"></script>
<script>
(function() {
var toggle = document.getElementById('sidebar-toggle');
Expand Down Expand Up @@ -445,5 +446,6 @@ <h3>On this page</h3>
}
})();
</script>
<!-- generated by pi-docsite -->
</body>
</html>
Loading