From 7789eb930c261481b475f9b7ac3642803c9547d7 Mon Sep 17 00:00:00 2001 From: Meni Yakove Date: Thu, 1 Oct 2026 12:14:21 +0300 Subject: [PATCH 1/3] refactor(docs): replace vendored docsfy renderer with pi-docsite package scripts/generate_docs.py and scripts/docs_render/ were a vendored copy of myk-org/docsfy. That generator is now published as `pi-docsite` on PyPI, so the vendored copy is deleted and the site is rebuilt with `uvx pi-docsite`. The vendored copy was already a strict subset of the package; the package adds: - docs/nav.json as the sidebar layout, replacing the NAVIGATION list that used to be hardcoded in the generator. A page missing from nav.json is still built but lands under a fallback group and the build prints a note naming it. - Per-page `nav_group` / `nav_order` frontmatter overrides. - Stale HTML is only deleted when it carries the generator's marker, so a hand-written 404.html survives a rebuild. - `?v=` cache-busting on asset URLs, so a rebuilt asset is not served stale from a browser cache. - Wide tables are wrapped in a scrollable div instead of widening the page. - The header badge reads `pi-docsite` and links to this repo. It previously hardcoded the label "pi-config" while pointing the href at this repo. - The footer says "Built from docs/*.md by pi-docsite" Sidebar group order, page titles and llms.txt are unchanged: llms.txt is byte-identical before and after. Rebuilding twice produces identical bytes. Drops the docs build dependencies (markdown, jinja2, pygments) from the dev group, since the renderer is no longer imported from this repo, and removes the [tool.mypy] namespace_packages workaround that existed only for scripts/docs_render/. Docs site regenerated with pi-docsite 4.7.2. Assisted-by: PI (gpt-6-luna) Signed-off-by: Meni Yakove --- AGENTS.md | 8 +- docs/assets/copy.js | 52 +- docs/assets/search.js | 50 +- docs/assets/style.css | 77 +- docs/automation-recipes.html | 24 +- docs/configuration-reference.html | 116 +- docs/configure-repositories.html | 36 +- docs/debug-with-the-log-viewer.html | 28 +- docs/enable-ai-features.html | 36 +- docs/environment-variables.html | 28 +- docs/index.html | 24 +- docs/log-viewer-and-mcp-api.html | 72 +- docs/manage-pull-requests.html | 36 +- docs/nav.json | 35 + docs/quick-start.html | 28 +- docs/run-pull-request-commands.html | 40 +- docs/search-index.json | 2 +- docs/secure-webhooks-and-pull-requests.html | 32 +- docs/set-up-checks-and-release-workflows.html | 32 +- docs/set-up-owners-and-reviewers.html | 28 +- docs/supported-github-events.html | 28 +- docs/webhook-and-health-api.html | 48 +- pyproject.toml | 6 - scripts/docs_render/__init__.py | 0 scripts/docs_render/renderer.py | 703 -------- scripts/docs_render/static/callouts.js | 26 - scripts/docs_render/static/codelabels.js | 75 - scripts/docs_render/static/copy.js | 41 - scripts/docs_render/static/github.js | 38 - scripts/docs_render/static/scrollspy.js | 49 - scripts/docs_render/static/search.js | 125 -- scripts/docs_render/static/style.css | 1545 ----------------- scripts/docs_render/static/theme.js | 22 - scripts/docs_render/templates/_doc_base.html | 105 -- scripts/docs_render/templates/_sidebar.html | 45 - scripts/docs_render/templates/_theme.html | 28 - scripts/docs_render/templates/index.html | 51 - scripts/docs_render/templates/page.html | 41 - scripts/docs_render/test_renderer.py | 498 ------ scripts/generate_docs.py | 207 --- scripts/test_generate_docs.py | 60 - uv.lock | 57 - 42 files changed, 535 insertions(+), 4047 deletions(-) create mode 100644 docs/nav.json delete mode 100644 scripts/docs_render/__init__.py delete mode 100644 scripts/docs_render/renderer.py delete mode 100644 scripts/docs_render/static/callouts.js delete mode 100644 scripts/docs_render/static/codelabels.js delete mode 100644 scripts/docs_render/static/copy.js delete mode 100644 scripts/docs_render/static/github.js delete mode 100644 scripts/docs_render/static/scrollspy.js delete mode 100644 scripts/docs_render/static/search.js delete mode 100644 scripts/docs_render/static/style.css delete mode 100644 scripts/docs_render/static/theme.js delete mode 100644 scripts/docs_render/templates/_doc_base.html delete mode 100644 scripts/docs_render/templates/_sidebar.html delete mode 100644 scripts/docs_render/templates/_theme.html delete mode 100644 scripts/docs_render/templates/index.html delete mode 100644 scripts/docs_render/templates/page.html delete mode 100644 scripts/docs_render/test_renderer.py delete mode 100644 scripts/generate_docs.py delete mode 100644 scripts/test_generate_docs.py diff --git a/AGENTS.md b/AGENTS.md index ab1dc5432..3d5da4c60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` (see [Documentation](#documentation)) ## Definition of Done A task is complete when ALL pass: @@ -43,9 +43,9 @@ 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/.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 @@ -53,7 +53,7 @@ Markdown rules that the renderer depends on: - 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/.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/.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 diff --git a/docs/assets/copy.js b/docs/assets/copy.js index c7e2a7a04..6339d08a1 100644 --- a/docs/assets/copy.js +++ b/docs/assets/copy.js @@ -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 = + ''; + var ICON_OK = + ''; + var ICON_FAIL = + ''; + + 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); }); @@ -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'); } })(); diff --git a/docs/assets/search.js b/docs/assets/search.js index a8fc6de6d..e2d77671c 100644 --- a/docs/assets/search.js +++ b/docs/assets/search.js @@ -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(); + }).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() { @@ -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...'; + results.appendChild(err); + return; + } + var matches = index.filter(function(item) { return item.title.toLowerCase().includes(q) || item.content.toLowerCase().includes(q); }).slice(0, 10); @@ -105,7 +137,9 @@ empty.textContent = 'No results found'; results.appendChild(empty); } - }); + } + + input.addEventListener('input', render); // Keyboard navigation input.addEventListener('keydown', function(e) { diff --git a/docs/assets/style.css b/docs/assets/style.css index 82aa789bf..da80cdefd 100644 --- a/docs/assets/style.css +++ b/docs/assets/style.css @@ -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; @@ -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); } @@ -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 { @@ -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); @@ -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; } @@ -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); @@ -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 -------------------------------------------------------------------------- */ @@ -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 { @@ -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; diff --git a/docs/automation-recipes.html b/docs/automation-recipes.html index a7223056c..529556271 100644 --- a/docs/automation-recipes.html +++ b/docs/automation-recipes.html @@ -5,7 +5,7 @@ Automation Recipes - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -385,7 +385,7 @@

On this page

@@ -393,13 +393,14 @@

On this page

- - - - - - - + + + + + + + + + diff --git a/docs/configuration-reference.html b/docs/configuration-reference.html index c1729fbd6..85e4a7371 100644 --- a/docs/configuration-reference.html +++ b/docs/configuration-reference.html @@ -5,7 +5,7 @@ Configuration Reference - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -206,7 +206,7 @@

Configuration Reference

Reference for config.yaml, repository entries under repositories.<repo-id>, and repository-local .github-webhook-server.yaml overrides. See Configure Repositories for rollout patterns, Set Up Checks and Release Workflows for workflow examples, Enable AI Features for AI setup, Secure Webhooks and Pull Requests for hardening, Supported GitHub Events for event coverage, and Environment Variables for non-YAML settings.

Files and Resolution

- +
@@ -227,8 +227,8 @@

Files and Resolution

-
Scope label Root of .github-webhook-server.yaml
- +
+
@@ -248,8 +248,8 @@

Files and Resolution

-
File Optional. Overrides only the keys the webhook runtime reads from the repository after startup.
- +
+
@@ -278,7 +278,7 @@

Files and Resolution

-
Resolution rule A repo-scoped block replaces the global block instead of deep-merging it. Repeat required subkeys when overriding.
+

Warning: .github-webhook-server.yaml is not consulted for name, log-level, log-file, mask-sensitive-data, github-tokens, events, default-status-checks, protected-branches, allow-commands-on-draft-prs, test-oracle, or pr-size-thresholds. Put those keys in config.yaml.

Note: A repo-local branch-protection block only affects webhook-time required_conversation_resolution handling. GitHub branch protection updates at startup still come from config.yaml.

@@ -302,7 +302,7 @@

Files and Resolution

Global Keys

Logging

- +
@@ -349,7 +349,7 @@

Logging

-
Key Applies log masking across the server unless a repo-level config.yaml override is present.
+
log-level: INFO
 log-file: webhook-server.log
 mcp-log-file: mcp_server.log
@@ -358,7 +358,7 @@ 

Logging

GitHub Access and Network

- +
@@ -440,7 +440,7 @@

GitHub Access and Network

-
Key Used in production mode; ignored when dev reload mode is enabled.
+

Warning: If verify-github-ips or verify-cloudflare-ips is enabled and no allowlist loads successfully, the server fails closed and does not start.

@@ -459,7 +459,7 @@

GitHub Access and Network

Global Repository Defaults

- +
@@ -513,7 +513,7 @@

Global Repository Defaults

-
Key Omitted blocks draft-PR commands, [] allows all, and a non-empty list allows only the listed commands.
+
default-status-checks:
   - ci/external
   - policy/manual-approval
@@ -529,7 +529,7 @@ 

Global Repository Defaults

docker

Where: Global

- +
@@ -555,7 +555,7 @@

docker

-
Key Used for startup docker.io login.
+
docker:
   username: dockerhub-user
   password: <dockerhub-token>
@@ -564,7 +564,7 @@ 

docker

Repository Registration

repositories

Where: Global

- +
@@ -597,7 +597,7 @@

repositories

-
Key Used for GitHub API access, webhook registration, and repo setup.
+
repositories:
   github-webhook-server:
     name: my-org/github-webhook-server
@@ -608,7 +608,7 @@ 

repositories

Repository Keys

Repo Keys Read From config.yaml Only

Where: Repo

- +
@@ -669,7 +669,7 @@

Repo Keys Read From config.ya

-
KeyOmitted blocks draft-PR commands, [] allows all, non-empty list allows only the listed commands.
+

Note: For event behavior after delivery, see Supported GitHub Events.

@@ -696,7 +696,7 @@

Repo Keys Read From config.ya

Repo Keys Read From config.yaml or .github-webhook-server.yaml

Where: Repo/local

- +
@@ -792,7 +792,7 @@

Repo Keys R

-
KeyOverrides whether cherry-pick PRs are assigned to the original PR author.
+

Warning: pre-commit is runtime-disabled until you set it to true, even though the schema advertises a true default.

@@ -815,7 +815,7 @@

Repo Keys R

Shared Blocks

branch-protection

Where: Global or Repo

- +
@@ -869,7 +869,7 @@

branch-protection

-
Key GitHub enforces conversation resolution, and the webhook runtime listens to review-thread events only when this is enabled.
+

Note: Repo branch-protection values overlay global values field by field.

@@ -891,7 +891,7 @@

branch-protection

labels

Where: Global or Repo/local

- +
@@ -924,7 +924,7 @@

labels

-
Key Applies the configured color when matching labels are created or updated.
+

Note: Reviewed-by labels such as approved-*, lgtm-*, commented-*, and changes-requested-* are always enabled.

Tip: Use pr-size-thresholds to control size/* label names and colors.

@@ -944,7 +944,7 @@

labels

welcome-extra-info

Where: Global or Repo/local

- +
@@ -963,7 +963,7 @@

welcome-extra-info

-
Key Adds extra guidance to the welcome comment unless a repo file overrides it.
+

Note: If .github-webhook-server-welcome-message.md exists in the repository, its contents replace welcome-extra-info. An empty file suppresses configured welcome text.

@@ -974,7 +974,7 @@

welcome-extra-info

security-checks

Where: Global or Repo/local

- +
@@ -1014,7 +1014,7 @@

security-checks

-
Key Allows listed committers to pass the identity check; entries are normalized to lowercase.
+

Note: Built-in suspicious-paths defaults are .claude/, .vscode/, .cursor/, .devcontainer/, .pi/, .github/workflows/, and .github/actions/.

Note: The server automatically trusts the GitHub App bot login, web-flow, and the API users behind github-tokens.

@@ -1033,7 +1033,7 @@

security-checks

ai-features

Where: Global or Repo/local

- +
@@ -1094,7 +1094,7 @@

ai-features

-
Key Limits how long the AI cherry-pick resolution step can run.
+

Warning: Repo/local ai-features replaces the global block. Repeat ai-provider and ai-model in every override.

Note: Cherry-picks resolved with AI are never auto-verified.

@@ -1113,7 +1113,7 @@

ai-features

test-oracle

Where: Global or Repo

- +
@@ -1160,7 +1160,7 @@

test-oracle

-
Key Controls when the server runs automatic oracle analysis.
+

Note: approved refers to the /approve command path used by this server. The /test-oracle command works whenever the block is configured.

Warning: Repo test-oracle replaces the global block. .github-webhook-server.yaml does not override this block.

@@ -1178,7 +1178,7 @@

test-oracle

pr-size-thresholds

Where: Global or Repo

- +
@@ -1204,7 +1204,7 @@

pr-size-thresholds

-
Key Sets the label color for the bucket.
+

Note: Built-in thresholds are size/XS for <20, size/S for <50, size/M for <100, size/L for <300, size/XL for <500, and size/XXL otherwise.

Warning: Repo pr-size-thresholds replaces the global block. .github-webhook-server.yaml does not override this block.

@@ -1224,7 +1224,7 @@

pr-size-thresholds

Repository-Only Blocks

tox

Where: Repo/local

- +
@@ -1264,7 +1264,7 @@

tox

-
Key Used only when tox.python-version is absent.
+

Warning: Use exact branch names and string env lists. The runtime does not expand branch globs, and array branch values are not normalized before execution.

Note: If a tox block exists but no branch key matches the PR base branch, tox still runs with the repository’s default tox configuration.

@@ -1278,7 +1278,7 @@

tox

protected-branches

Where: Repo

- +
@@ -1311,7 +1311,7 @@

protected-branches

-
Key Subtracted from the branch’s required-check list in both paths, removing any entry it names — including the security checks.
+

Warning: Use exact branch names and the object form shown below. The schema accepts array shorthand, but the startup branch-settings path reads the object form.

@@ -1331,7 +1331,7 @@

protected-branches

pypi

Where: Repo/local

- +
@@ -1350,14 +1350,14 @@

pypi

-
Key Enables package publishing on the repository’s release/tag workflow.
+
pypi:
   token: <pypi-token>
 

container

Where: Repo/local

- +
@@ -1432,7 +1432,7 @@

container

-
Key Selects the Dockerfile used for the build.
+
container:
   username: quay-user
   password: <quay-token>
@@ -1449,7 +1449,7 @@ 

container

container.oci-annotations

Where: Repo/local

- +
@@ -1510,7 +1510,7 @@

container.oci-annotations

-
Key Adds the repository name annotation.
+

Warning: container.context must stay under the repository root. Path traversal is rejected at runtime.

@@ -1532,7 +1532,7 @@

container.oci-annotations

custom-check-runs

Where: Repo/local

- +
@@ -1565,7 +1565,7 @@

custom-check-runs

-
Key Mandatory checks join the required-check list; optional checks still run but do not block merge.
+

Warning: Custom check names must be unique and cannot collide with built-in check names: tox, pre-commit, build-container, python-module-install, conventional-title, can-be-merged, security-suspicious-paths, and security-committer-identity.

Note: The server validates that the command has an executable after any leading environment assignments, and that the executable exists on the server.

@@ -1656,7 +1656,7 @@

On this page

@@ -1664,13 +1664,14 @@

On this page

- - - - - - - + + + + + + + + + diff --git a/docs/configure-repositories.html b/docs/configure-repositories.html index 52495f163..4078d92a7 100644 --- a/docs/configure-repositories.html +++ b/docs/configure-repositories.html @@ -5,7 +5,7 @@ Configure Repositories - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -370,7 +370,7 @@

4. Let repository

5. Use the right config layer for the right job

Use this split when you are deciding where a setting belongs:

- +
@@ -395,9 +395,9 @@

5. Use the right config

-
Put the setting hereconventional-title, minimum-lgtm, labels, pr-size-thresholds
+

Precedence is highest to lowest:

- +
@@ -422,7 +422,7 @@

5. Use the right config

-
PriorityThe setting should apply everywhere unless overridden
+

6. Restart when bootstrap settings change

Some settings are applied during startup bootstrap, not during normal PR processing. After changing these in config.yaml, restart the server.

Restart after changes to settings such as:

@@ -439,7 +439,7 @@

6. Restart when bootstrap sett

Advanced Usage

Keep secrets server-side and behavior repo-side

A practical split looks like this:

- +
@@ -468,7 +468,7 @@

Keep secrets server-sid

-
Server-side onlyallow-commands-on-draft-prs
+

This keeps credentials out of Git history while still letting teams tune workflow behavior.

Use repository-specific tokens only where needed

If most repositories can share one token pool, keep github-tokens at the top level. Add a repository-specific token list only when a repository needs different permissions or rate-limit isolation.

@@ -607,7 +607,7 @@

On this page

@@ -615,13 +615,14 @@

On this page

- - - - - - - + + + + + + + + + diff --git a/docs/debug-with-the-log-viewer.html b/docs/debug-with-the-log-viewer.html index 3f1b4d5cf..7b4ae83b7 100644 --- a/docs/debug-with-the-log-viewer.html +++ b/docs/debug-with-the-log-viewer.html @@ -5,7 +5,7 @@ Debug with the Log Viewer - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -241,7 +241,7 @@

2. Open recent history

Start with Refresh if the page is already open and you want the latest historical data from disk.

3. Narrow the results to the run you care about

Use the filters at the top of the page to reduce noise before you inspect a failure.

- +
@@ -278,7 +278,7 @@

3. Narrow the results to

-
If you know thisStart Time and End Time
+

A few practical patterns work well:

  1. Start with Hook ID if you have the delivery from GitHub.
  2. @@ -463,7 +463,7 @@

    On this page

    @@ -471,13 +471,14 @@

    On this page

    - - - - - - - + + + + + + + + + diff --git a/docs/enable-ai-features.html b/docs/enable-ai-features.html index fe42d0875..4b9dd3a04 100644 --- a/docs/enable-ai-features.html +++ b/docs/enable-ai-features.html @@ -5,7 +5,7 @@ Enable AI Features - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -282,7 +282,7 @@

    Step-by-Step

    Use the credential that matches your ai-provider value. The first command checks the webhook server, and the second checks the local AI sidecar; replace 9100 if you set SIDECAR_PORT.

    - +
    @@ -303,7 +303,7 @@

    Step-by-Step

    -
    Provider CURSOR_API_KEY
    +

    Tip: Add ACPX_AGENTS=cursor when you want Cursor model discovery. The example deployment also supports docker exec -it github-webhook-server agent if you prefer Cursor interactive login instead of CURSOR_API_KEY.

    @@ -324,7 +324,7 @@

    Step-by-Step

    timeout-minutes: 10
    - +
    @@ -341,7 +341,7 @@

    Step-by-Step

    -
    Mode The server edits the PR title and turns the check green if the suggestion validates.
    +

    Open a PR with a non-matching title, or retitle an existing PR, to test the feature. If the repo does not have a conventional-title rule, this AI feature stays idle even if ai-features is present.

    Tip: Start with suggest so humans can review the wording before you move to fix.

    @@ -384,7 +384,7 @@

    Step-by-Step

    This block is separate from ai-features. Conventional-title suggestions and cherry-pick AI use the local sidecar, but test-oracle calls the external service at server-url.

    - +
    @@ -405,7 +405,7 @@

    Step-by-Step

    -
    Trigger When new commits are pushed to a PR
    +

    /test-oracle always works on demand when the feature is configured, regardless of the trigger list.

    Note: The automatic approved trigger means the project's /approve command, not a plain GitHub review approval.

    @@ -505,7 +505,7 @@

    On this page

    @@ -513,13 +513,14 @@

    On this page

    - - - - - - - + + + + + + + + + diff --git a/docs/environment-variables.html b/docs/environment-variables.html index c59673695..d127dd3e4 100644 --- a/docs/environment-variables.html +++ b/docs/environment-variables.html @@ -5,7 +5,7 @@ Environment Variables - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -207,7 +207,7 @@

    Environment Variables

    The server reads the variables below at startup. Anything not listed here comes from config.yaml in the data directory, not from the process environment.

    Reference

    - +
    @@ -278,7 +278,7 @@

    Reference

    -
    Variable Timeout baseline for the test session.
    +

    WEBHOOK_SERVER_DATA_DIR

    Read once in webhook_server/libs/config.py. The directory must contain config.yaml; if the file is missing, Config raises FileNotFoundError on startup. The log viewer and @@ -398,7 +398,7 @@

    On this page

    @@ -406,13 +406,14 @@

    On this page

    - - - - - - - + + + + + + + + + diff --git a/docs/index.html b/docs/index.html index d6456184b..ac0e8cd20 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,7 +5,7 @@ github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -353,7 +353,7 @@

    Reference

    @@ -361,13 +361,14 @@

    Reference

    - - - - - - - + + + + + + + + + diff --git a/docs/log-viewer-and-mcp-api.html b/docs/log-viewer-and-mcp-api.html index f968bbff5..d3091b0f0 100644 --- a/docs/log-viewer-and-mcp-api.html +++ b/docs/log-viewer-and-mcp-api.html @@ -5,7 +5,7 @@ Log Viewer and MCP API - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -219,7 +219,7 @@

    Log Viewer and MCP API

    One route, GET /logs/api/step-logs/{hook_id}/{step_name}, additionally requires a private, loopback, or link-local client IP via require_trusted_network. That is a network-range check on request.client.host, not authentication, and it is trivially bypassed by a misconfigured reverse proxy that does not preserve the client address. It is a speed bump, not a control.

    Route summary

    - +
    @@ -290,7 +290,7 @@

    Route summary

    -
    Method Always (started by entrypoint.py)
    +

    The two flags are read once at import time and the routes are then either registered or not:

    LOG_SERVER_ENABLED: bool = os.environ.get("ENABLE_LOG_SERVER") == "true"
     MCP_SERVER_ENABLED: bool = os.environ.get("ENABLE_MCP_SERVER") == "true"
    @@ -334,7 +334,7 @@ 

    GET /logs/api/entries

    Query parameters

    - +
    @@ -411,7 +411,7 @@

    Query parameters

    -
    Parameter >= 0
    +

    All filters are ANDed and matched exactly against the parsed entry fields; search is a case-insensitive substring of message. Time filters compare against the entry timestamp — start_time keeps entries at or after it, end_time keeps entries at or before it. Z suffixes are accepted (parse_datetime_string() rewrites a trailing Z to +00:00 before calling datetime.fromisoformat); an unparseable value is a 400 with Invalid start_time format: ....

    Response

    {
    @@ -454,7 +454,7 @@ 

    GET /logs/api/export

    GET /logs/api/export?format_type=json&level=ERROR&start_time=2024-01-01T00:00:00Z&limit=10000
     
    - +
    @@ -483,7 +483,7 @@

    GET /logs/api/export

    -
    Parameter 1–100000 at the query layer
    +

    The controller rejects limit > 50000 with 413 ("Result set too large"), even though the query parameter accepts up to 100 000.

    Response headers:

    content-type: application/json
    @@ -511,7 +511,7 @@ 

    GET /logs/api/pr-flow/{hook_id}<

    hook_id is the only parameter, and it is flexible — the controller branches on the value:

    - +
    @@ -536,7 +536,7 @@

    GET /logs/api/pr-flow/{hook_id}<

    -
    FormDelivery ID as given
    +

    Entries are streamed from at most 15 files / 10 000 entries and filtered by whichever identifier was recognised. Stages are then matched by regex over the log messages (WORKFLOW_STAGE_PATTERNS): Webhook Received, Validation Complete, Reviewers Assigned, Labels Applied, Checks Started, Checks Complete, Processing Complete.

    {
       "identifier": "72d3162e-cc78-11e3-81ab-4c9367dc0958",
    @@ -591,7 +591,7 @@ 

    GET /logs/api/workflow-steps/{ho

    pr is null when the delivery had no pull request. token_spend is only present when the delivery recorded one.

    A broken summary never yields partial data here — but it also never surfaces as a 500. Three cases, and they are not the same:

    - +
    @@ -616,7 +616,7 @@

    GET /logs/api/workflow-steps/{ho

    -
    SituationThat 500 is swallowed and the request falls through to the text logs. You get a reconstructed 200 timeline in the text-fallback shape, or 404 No data found for hook ID: ... / 404 No workflow steps found for hook ID: ... if the text logs do not cover the delivery
    +

    The swallow is the backward-compatibility path, and it is unconditional — it does not distinguish a missing summary from a corrupt one:

    try:
         # First try JSON logs (more efficient and complete)
    @@ -635,7 +635,7 @@ 

    GET /logs/api/step-logs/{hoo
    GET /logs/api/step-logs/72d3162e-cc78-11e3-81ab-4c9367dc0958/webhook_routing
     
    - +
    @@ -655,7 +655,7 @@

    GET /logs/api/step-logs/{hoo

    -
    Parameter1–100 characters — must equal a step_name from the timeline above, e.g. webhook_routing, clone_repository
    +

    The step is located in the workflow timeline, then a time window is computed: step.timestamp to step.timestamp + duration_ms, or a 60 000 ms default window when the step has no recorded duration. Text .log entries with the same hook_id inside that window are collected, capped at 500 entries (_MAX_STEP_LOGS).

    {
       "step": {
    @@ -687,7 +687,7 @@ 

    WS /logs/ws

    ws.onmessage = (event) => console.log(JSON.parse(event.data));
    - +
    @@ -727,7 +727,7 @@

    WS /logs/ws

    -
    Parameter Log level
    +

    These are query parameters, not message frames. There is no start_time, end_time, or search on the socket — the live stream only supports the six filters above, unlike the REST search.

    Behaviour:

      @@ -761,7 +761,7 @@

      Setup sequence

Exposed tools

FastApiMCP converts every OpenAPI operation into a tool named after its operation_id, then drops anything tagged mcp_exclude. With both flags on, the tool list is:

- +
@@ -798,7 +798,7 @@

Exposed tools

-
Tool GET /logs/api/step-logs/{hook_id}/{step_name}
+

POST /webhook_server carries tags=["mcp_exclude"] and is therefore not exposed as a tool — an AI client cannot post webhooks through MCP. /mcp itself is excluded by include_in_schema=False. The /static mount and the WebSocket are not OpenAPI operations and are not tools either.

Client configuration

Because the transport is stateless and JSON-only, point a Streamable HTTP MCP client at the base URL:

@@ -829,7 +829,7 @@

Loopback tool server (not FastAPI)

It is the git-tool bridge for AI-assisted conflict resolution (runner_handler.py builds http://127.0.0.1:5001/tools/run per tool). It is unreachable from other hosts by design; do not change the bind address.

POST 127.0.0.1:5001/tools/run

JSON body:

- +
@@ -864,10 +864,10 @@

POST 127.0.0.1:5001/tools/run

-
Field Clamped to 1–300 s; falls back to the tool default (30) when unparseable
+

Response: {"success": bool, "output": "..."} — stdout, or stderr when stdout is empty, truncated to 50 000 characters. A timeout returns {"success": false, "output": "Command timed out after Ns"}.

Tool registry

- +
@@ -908,13 +908,13 @@

Tool registry

-
Tool 0
+

Blocked flags are rejected with 403, including combined short options: -rf is caught by expanding it into -r -f and checking each character against the blocked short flags. This is what keeps the AI conflict-resolution path read-only apart from scoped git rm and file edits — no bash, no recursive delete.

Status codes: 400 for a non-JSON body, a missing tool/cwd, or a non-string cwd/args; 403 for a blocked flag or a disallowed subcommand; 404 for an unknown tool name; 200 for anything the subprocess produced.

hook_id and the workflow context

Everything under /logs/api/* that takes a hook_id is keyed by the GitHub delivery ID — the X-GitHub-Delivery header of the original webhook. Per request the server builds a WebhookContext (webhook_server/utils/context.py) and, at the end of processing, writes it as one type: "webhook_summary" line to webhooks_YYYY-MM-DD.json. That summary is what the pr-flow and workflow-steps routes read.

The fields the viewer surfaces:

- +
@@ -967,10 +967,10 @@

hook_id and the workflow

-
FieldOne-line human-readable result
+

Steps are recorded through start_step() / complete_step() / fail_step(). A step that started but never completed is marked status: "started" with duration_ms: null in the summary line, which the timeline reads as "incomplete".

Troubleshooting

- +
@@ -1015,7 +1015,7 @@

Troubleshooting

-
Symptom fastapi_mcp failed to import or the session manager failed to start. Check mcp_server.log and the startup logs.
+

See also

  • Debug with the Log Viewer — task walkthrough for the UI.
  • @@ -1091,7 +1091,7 @@

    On this page

    @@ -1099,13 +1099,14 @@

    On this page

    - - - - - - - + + + + + + + + + diff --git a/docs/manage-pull-requests.html b/docs/manage-pull-requests.html index 7151c1ffa..efb497751 100644 --- a/docs/manage-pull-requests.html +++ b/docs/manage-pull-requests.html @@ -5,7 +5,7 @@ Manage Pull Requests - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -247,7 +247,7 @@

    Step-by-Step

    1. Open the PR or mark it ready for review

    On opened and ready_for_review, the server posts a welcome comment that lists the available PR actions and the current merge requirements. It also assigns reviewers from the matching OWNERS rules, adds the PR author as an assignee when possible, and creates a tracking issue if create-issue-for-new-pr: true.

    At the same time, the server applies the first round of state labels and queues check runs.

    - +
    @@ -280,11 +280,11 @@

    1. Open the PR or mark it rea

    -
    What appears automaticallyThe PR currently meets all merge requirements.
    +

    Any enabled built-in or custom checks are queued at the same time. Common examples are tox, pre-commit, build-container, conventional-title, and security checks.

    2. Review the PR

    GitHub review state and explicit approver sign-off are tracked separately. A normal GitHub Approved review counts as reviewer LGTM feedback, while the explicit approver signal is /approve.

    - +
    @@ -309,7 +309,7 @@

    2. Review the PR

    -
    Reviewer action Satisfies can-be-merged-required-labels.
    +

    Note: The PR author does not count toward LGTM counting for their own PR.

    @@ -328,7 +328,7 @@

    3. Wait for merge readiness

    4. Push more commits

    On synchronize, the server rechecks merge state and reruns the PR workflow for the new head commit. Review-state labels are cleared so the new revision is evaluated fresh.

    If the update is only a clean rebase, the server keeps verification aligned with the new head instead of forcing a full manual re-verify.

    - +
    @@ -345,7 +345,7 @@

    4. Push more commits

    -
    Push result merge state is refreshed, but existing verification can be carried forward to the new head
    +

    5. Merge the PR

    On merge, the server closes the tracking issue it created for the PR. If cherry-pick labels were already attached, the server starts those follow-up cherry-picks after merge.

    It also refreshes merge-state labels on other open PRs so stale needs-rebase or conflict states get revisited after the branch moves forward.

    @@ -461,7 +461,7 @@

    On this page

    @@ -469,13 +469,14 @@

    On this page

    - - - - - - - + + + + + + + + + diff --git a/docs/nav.json b/docs/nav.json new file mode 100644 index 000000000..cb27a564f --- /dev/null +++ b/docs/nav.json @@ -0,0 +1,35 @@ +{ + "groups": [ + { + "name": "Getting Started", + "pages": ["quick-start"] + }, + { + "name": "User Guides", + "pages": [ + "configure-repositories", + "set-up-owners-and-reviewers", + "manage-pull-requests", + "run-pull-request-commands", + "set-up-checks-and-release-workflows", + "enable-ai-features", + "secure-webhooks-and-pull-requests", + "debug-with-the-log-viewer" + ] + }, + { + "name": "Explore", + "pages": ["automation-recipes"] + }, + { + "name": "Reference", + "pages": [ + "configuration-reference", + "environment-variables", + "webhook-and-health-api", + "log-viewer-and-mcp-api", + "supported-github-events" + ] + } + ] +} diff --git a/docs/quick-start.html b/docs/quick-start.html index 6c09d4c60..7cb5ce6ab 100644 --- a/docs/quick-start.html +++ b/docs/quick-start.html @@ -5,7 +5,7 @@ Start Automating a Repository - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -273,7 +273,7 @@

    2. Add the smallest working

    Use these values carefully:

    - +
    @@ -302,7 +302,7 @@

    2. Add the smallest working The full repository name, such as your-org/your-repo

    -
    Setting
    +

    Warning: For a normal public deployment, webhook-ip should include the full webhook path, for example https://your-domain.example/webhook_server.

    Warning: The key under repositories should be the short repository name, not owner/repo. In the example above, the key is your-repo and the full name belongs in name.

    @@ -502,7 +502,7 @@

    On this page

    @@ -510,13 +510,14 @@

    On this page

    - - - - - - - + + + + + + + + + diff --git a/docs/run-pull-request-commands.html b/docs/run-pull-request-commands.html index 859e1615e..77899e6c0 100644 --- a/docs/run-pull-request-commands.html +++ b/docs/run-pull-request-commands.html @@ -5,7 +5,7 @@ Run Pull Request Commands - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -228,7 +228,7 @@

    How commands are recognized

    Who can run what

    - +
    @@ -253,8 +253,8 @@

    Who can run what

    -
    Who Any user who can comment, no permission check
    - +
    +
    @@ -399,7 +399,7 @@

    Who can run what

    -
    Command Maintainer
    +

    Commands on draft pull requests

    Draft PRs are blocked by default. The allow-commands-on-draft-prs configuration key controls the exception list. Set it in config.yaml, either globally or under repositories.<repo> to override the global default for a single @@ -414,7 +414,7 @@

    Commands on draft pull requests

    This key is read from config.yaml only. Unlike most other settings, it is not read from a repository-local .github-webhook-server.yaml — placing it there has no effect and draft PRs stay blocked.

    - +
    @@ -435,7 +435,7 @@

    Commands on draft pull requests

    -
    Value Only the listed commands are allowed; any other command is rejected with a comment naming the allowed ones
    +

    /test-oracle is the one exception to the per-command allowlist — it runs on a draft PR even when it is not listed in allow-commands-on-draft-prs. That exception only applies when the key is set, even to an empty list. If the key is absent, the draft pull request is dropped before any command is evaluated, so /test-oracle does not run either.

    @@ -533,7 +533,7 @@

    /retest <test> [test...]"retest requires an argument" comment. Unknown names get a "No <name> configured for this repository" comment; the valid ones still run.

    Supported names depend on the repository configuration:

    - +
    @@ -566,7 +566,7 @@

    /retest <test> [test...]Any check run defined in custom-check-runs

    -
    Test name
    +
    /retest tox
     
    @@ -731,7 +731,7 @@

    On this page

    @@ -739,13 +739,14 @@

    On this page

    - - - - - - - + + + + + + + + + diff --git a/docs/search-index.json b/docs/search-index.json index 7fe69a69b..c5ec20aa5 100644 --- a/docs/search-index.json +++ b/docs/search-index.json @@ -1 +1 @@ -[{"slug": "quick-start", "title": "Start Automating a Repository", "content": "# Start Automating a Repository\n\nYou want one GitHub repository under automation quickly so new pull requests start getting webhook handling, labels, and review workflow help without manual setup in GitHub. This guide gets you from an empty data directory to a live webhook and a working health check in a few minutes.\n\n## Prerequisites\n\n- Python `3.14`\n- `uv`\n- A GitHub App ID\n- The matching GitHub App private key in PEM format\n- At least one GitHub token that can manage the target repository\n- A repository where the GitHub App is installed\n- A public URL GitHub can reach, or a `smee.io` channel for local testing\n\n## Quick example\n\n```bash\nexport WEBHOOK_SERVER_DATA_DIR=\"$HOME/webhook-server-data\"\nmkdir -p \"$WEBHOOK_SERVER_DATA_DIR\"\n\ncat > \"$WEBHOOK_SERVER_DATA_DIR/config.yaml\" <<'YAML'\ngithub-app-id: 123456\ngithub-tokens:\n - ghp_your_token_here\n\nwebhook-ip: https://your-domain.example/webhook_server\n\nrepositories:\n your-repo:\n name: your-org/your-repo\nYAML\n\ncp /path/to/your/github-app-private-key.pem \\\n \"$WEBHOOK_SERVER_DATA_DIR/webhook-server.private-key.pem\"\n\nuv sync\nWEBHOOK_SERVER_DATA_DIR=\"$WEBHOOK_SERVER_DATA_DIR\" uv run entrypoint.py\n```\n\n```bash\ncurl http://127.0.0.1:5000/webhook_server/healthcheck\n```\n\n```json\n{\"status\":200,\"message\":\"Alive\"}\n```\n\nOnce the server starts, it bootstraps the repository you listed: it creates or updates the webhook and prepares the built-in automation. The health check proves the process is running; the steps below confirm GitHub can actually reach it.\n\n## Step-by-step\n\n### 1. Create the data directory\n\n```bash\nexport WEBHOOK_SERVER_DATA_DIR=\"$HOME/webhook-server-data\"\nmkdir -p \"$WEBHOOK_SERVER_DATA_DIR\"\n```\n\nYour minimal layout should look like this:\n\n```text\n$WEBHOOK_SERVER_DATA_DIR/\n\u251c\u2500\u2500 config.yaml\n\u2514\u2500\u2500 webhook-server.private-key.pem\n```\n\n> **Note:** If you do not set `WEBHOOK_SERVER_DATA_DIR`, the server uses `/home/podman/data`.\n\n### 2. Add the smallest working `config.yaml`\n\n```yaml\ngithub-app-id: 123456\ngithub-tokens"}, {"slug": "configure-repositories", "title": "Configure Repositories", "content": "# Configure Repositories\n\n```yaml\n# config.yaml\ngithub-app-id: 123456\ngithub-tokens:\n - \n - \n\nwebhook-ip: https://your-domain.com/webhook_server\n\ndefault-status-checks:\n - \"WIP\"\n - \"can-be-merged\"\n\nrepositories:\n my-repository:\n name: my-org/my-repository\n events:\n - pull_request\n - issue_comment\n - check_run\n - status\n tox:\n main: all\n pre-commit: true\n protected-branches:\n main:\n include-runs:\n - \"pre-commit.ci - pr\"\n - \"WIP\"\n```\n\n```yaml\n# .github-webhook-server.yaml\nconventional-title: \"feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert\"\nminimum-lgtm: 2\ncreate-issue-for-new-pr: true\n```\n\nThe first file gives the server everything it needs to bootstrap and manage the repository. The second file lets repository maintainers tune day-to-day behavior without editing the server.\n\n## Step-by-step\n\n### 1. Put shared defaults in `config.yaml`\n\nStart with the settings that should apply everywhere unless a repository overrides them.\n\n```yaml\nlog-level: INFO\nlog-file: webhook-server.log\nmask-sensitive-data: true\n\ngithub-app-id: 123456\ngithub-tokens:\n - \n - \n\nwebhook-ip: https://your-domain.com/webhook_server\n\ndefault-status-checks:\n - \"WIP\"\n - \"dpulls\"\n - \"can-be-merged\"\n\nbranch-protection:\n strict: true\n require_code_owner_reviews: true\n dismiss_stale_reviews: false\n required_approving_review_count: 1\n required_linear_history: true\n required_conversation_resolution: true\n```\n\nUse top-level settings for:\n\n- Shared defaults across many repositories\n- Secrets and credentials\n- Startup-time behavior such as webhook creation and branch protection\n- Organization-wide policies\n\n> **Warning:** `webhook-ip` must include the full webhook path, for example `https://your-domain.com/webhook_server`.\n\n### 2. Add each repository under `repositories`\n\nAdd one entry per managed repository. Use the short repository name as the key, and t"}, {"slug": "set-up-owners-and-reviewers", "title": "Set Up OWNERS and Review Rules", "content": "# Set Up OWNERS and Review Rules\n\nThe webhook server does not use GitHub's CODEOWNERS. It reads its own `OWNERS` files from\nyour repository and uses them to decide who must review a pull request, who counts as an\n*approver*, who can run commands, and which reviewers get auto-assigned.\n\n## What `OWNERS` controls\n\n| Key | Effect |\n| --- | --- |\n| `approvers` | Members of this scope who can `/approve`. An approval from this list is required for the `can-be-merged` check only when the scope has at least one approver; a path with no approvers listed imposes no approval requirement. |\n| `reviewers` | Members of this scope who are eligible for reviewer auto-assignment; `/lgtm` is not gated by membership. |\n| `allowed-users` | Users (in addition to collaborators and contributors) who may run commands such as `/retest`, `/cherry-pick`, `/rebase`, `/check-can-merge`, `/assign-reviewers`. |\n| `root-approvers` | Optional boolean. Set to `false` to opt out of requiring an approval from the root `OWNERS` approvers. |\n\n## File discovery rules\n\nThese are exact \u2014 the parser is strict.\n\n- The filename must be exactly `OWNERS` (uppercase, no extension). `OWNERS.yaml`, `owners`,\n `.github/CODEOWNERS`, and `OWNERS_ALIASES` are **not** read.\n- Files are found recursively in the cloned repository, which is checked out at the **base\n branch** of the PR. An `OWNERS` file added only in the PR branch is ignored until merged.\n- Any path with a dot-prefixed directory component (`.git`, `.venv`, `.github`, ...) is skipped.\n- At most 1000 `OWNERS` files are processed per PR.\n- Each file is keyed by its **parent directory**, not by file path. `webhook_server/OWNERS`\n is stored under `webhook_server`; the repo-root `OWNERS` is stored under `.`.\n\n## Supported syntax\n\nEach `OWNERS` file is a YAML **mapping**. If it is not a mapping, or if `approvers`,\n`reviewers`, or `allowed-users` is present but is not a list of strings, the file is rejected\n(logged as `Invalid OWNERS file `) and contribut"}, {"slug": "manage-pull-requests", "title": "Manage Pull Requests", "content": "# Manage Pull Requests\n\nYou want contributors to open a pull request and immediately understand what the server will do next: who gets assigned, which labels and checks appear, and what has to turn green before the PR is ready to merge. This page walks through the default PR lifecycle so authors and reviewers can work with the automation instead of guessing at it.\n\n## Prerequisites\n\n- The server is already connected to the repository. See [Start Automating a Repository](quick-start.html).\n- An `OWNERS` file defines approvers and reviewers. See [Set Up OWNERS and Review Rules](set-up-owners-and-reviewers.html).\n- Any CI checks you expect on PRs are already configured. See [Set Up Checks and Release Workflows](set-up-checks-and-release-workflows.html).\n\n## Quick Example\n\n```yaml\n# .github-webhook-server.yaml\nverified-job: true\ncreate-issue-for-new-pr: true\nminimum-lgtm: 1\nset-auto-merge-prs:\n - main\ncan-be-merged-required-labels:\n - security-reviewed\n```\n\n```yaml\n# OWNERS\napprovers:\n - myakove\n - rnetser\nreviewers:\n - myakove\n - rnetser\n```\n\nOpen a PR to `main`, then have an approver submit a GitHub review with state `Approved` and this review body:\n\n```text\n/approve\n```\n\nWith that setup, the server will:\n\n- assign reviewers from `OWNERS`\n- add a branch label such as `branch-main`\n- add a size label such as `size/M`\n- queue `can-be-merged` and `verified`, plus any enabled checks\n- mark the PR merge-ready only after required checks, required labels, approval, and LGTM requirements are satisfied\n\n## Step-by-Step\n\n### 1. Open the PR or mark it ready for review\n\nOn `opened` and `ready_for_review`, the server posts a welcome comment that lists the available PR actions and the current merge requirements. It also assigns reviewers from the matching `OWNERS` rules, adds the PR author as an assignee when possible, and creates a tracking issue if `create-issue-for-new-pr: true`.\n\nAt the same time, the server applies the first round of state labels and queues check runs.\n\n"}, {"slug": "run-pull-request-commands", "title": "Run Pull Request Commands", "content": "# Run Pull Request Commands\n\nThe webhook server reacts to slash commands posted as pull request comments. Comment `/wip` on a PR and the server\nadds the `wip` label and prefixes the title with `WIP:`.\n\n## How commands are recognized\n\n- Only comments **created** on a PR are processed. Editing or deleting a comment does nothing.\n- Every **line** of the comment that starts with `/` is treated as a command. The leading `/` is stripped, the first\n word is the command, and the rest is the argument.\n- Several commands can be posted in a single comment \u2014 each line is one command, and they run concurrently.\n- A command argument of exactly `cancel` turns the command into a removal (`/wip cancel`).\n- The server reacts with a \ud83d\udc4d to the comment once a command is accepted.\n- Comments containing the server's own welcome-message URL are ignored, so the bot never processes its own output.\n- Unknown commands are silently ignored (logged only).\n\nExample \u2014 one comment, three commands:\n\n```text\n/wip\n/assign-reviewers\n/retest tox\n```\n\n## Who can run what\n\n| Who | Meaning |\n| --- | --- |\n| **Author** | The user who opened the pull request |\n| **Approver** | A repository collaborator/contributor, an approver listed in `OWNERS`, a maintainer, **or** anyone a maintainer/approver has granted with `/add-allowed-user @user` |\n| **Maintainer** | Listed in the `root-approvers` configuration, or otherwise resolved as a repository maintainer |\n| **Anyone** | Any user who can comment, no permission check |\n\n| Command | What it does | Who can run it |\n| --- | --- | --- |\n| `/wip` | Adds the `wip` label and prefixes the title with `WIP: ` | Author, Approver |\n| `/wip cancel` | Removes the `wip` label and strips the `WIP:` prefix from the title | Author, Approver |\n| `/verified` | Adds the `verified` label and sets the `verified` check run to success | Author, Approver |\n| `/verified cancel` | Removes the `verified` label and re-queues the `verified` check run | Author, Approver |\n| `/hold` | Adds the "}, {"slug": "set-up-checks-and-release-workflows", "title": "Set Up Checks and Release Workflows", "content": "# Set Up Checks and Release Workflows\n\nChecks are the pull request gates this server runs itself. It does not call GitHub Actions for tests: it clones the PR, runs commands in a worktree, and writes the results back as GitHub check runs. Release workflows are the same machinery, but triggered by a tag push \u2014 a container image push and a PyPI publish.\n\nThis page covers the two halves: what checks exist and how required-versus-advisory is decided, then how the container and PyPI release blocks work.\n\n## Prerequisites\n\n- A running webhook server with at least one repository configured. See [Start Automating a Repository](quick-start.html) and [Configure Repositories](configure-repositories.html).\n- Python `3.14`, `uv`, and `podman` on the server host \u2014 the built-in checks shell out to `uvx` and `podman`.\n- A container registry account if you want `build-container`; a PyPI API token if you want PyPI publishing.\n- Manage-repositories app access: branch protection, labels, and security settings are written at startup with the highest-rate-limit token, not with the webhook's app token.\n\n## Part 1 \u2014 Checks\n\n### How a check run gets created and reported\n\nEvery check follows the same lifecycle, implemented in `webhook_server/libs/handlers/runner_handler.py`:\n\n1. `set_check_queued` when the PR event is received (only for checks whose feature is enabled).\n2. `set_check_in_progress` when the runner starts.\n3. A git worktree is created from the shared clone and the base branch is merged into it.\n4. The command runs, stdout/stderr are stripped of ANSI codes and redacted, then posted to the check run output.\n5. `set_check_success` or `set_check_failure`.\n\nAll check runs are written to `last_commit.sha`. Output is truncated to 65534 characters to stay under GitHub's limit, keeping the head and the tail, and the full redacted output stays in the server log. Secrets from `pypi.token`, the container registry username and password, and the GitHub token are replaced with `*****` before a"}, {"slug": "enable-ai-features", "title": "Enable AI Features", "content": "# Enable AI Features\n\nTurn on AI help when you want pull requests to spend less maintainer time on bad titles, conflicted cherry-picks, and test selection. With the right settings, the server can suggest or fix PR titles, try to resolve cherry-pick conflicts, and ask a separate oracle service for test recommendations.\n\n## Prerequisites\n\n- A running `github-webhook-server` deployment\n- Access to `config.yaml` or a repository-local `.github-webhook-server.yaml`\n- The provided container image, or another runtime that includes the bundled AI sidecar and supported AI CLIs\n- One supported provider credential for local AI features: `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, or `CURSOR_API_KEY`\n- A reachable `pr-test-oracle` service if you want test recommendations\n\n## Quick Example\n\n```yaml\n# config.yaml\nrepositories:\n my-repository:\n name: my-org/my-repository\n conventional-title: \"feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert\"\n ai-features:\n ai-provider: \"claude\"\n ai-model: \"sonnet\"\n conventional-title:\n enabled: true\n mode: suggest\n resolve-cherry-pick-conflicts-with-ai:\n enabled: true\n test-oracle:\n server-url: \"http://localhost:8000\"\n ai-provider: \"claude\"\n ai-model: \"sonnet\"\n triggers:\n - approved\n```\n\n```yaml\n# docker-compose.yaml\nservices:\n github-webhook-server:\n environment:\n - ANTHROPIC_API_KEY=sk-ant-xxx\n```\n\nSave the config, then restart only if you changed environment variables or mounts. On the next PR event, the title check can show an AI suggestion, the next conflicted cherry-pick can attempt AI resolution, and users allowed to run comment commands can trigger `/test-oracle` on demand.\n\n## Step-by-Step\n\n1. Put shared defaults and repo rules in the right place\n\n```yaml\nai-features:\n ai-provider: \"claude\"\n ai-model: \"sonnet\"\n\ntest-oracle:\n server-url: \"http://localhost:8000\"\n ai-provider: \"claude\"\n ai-model: \"sonnet\"\n\nrepositories:\n my-repository:\n n"}, {"slug": "secure-webhooks-and-pull-requests", "title": "Secure Webhooks and Pull Requests", "content": "# Secure Webhooks and Pull Requests\n\n```yaml\n# config.yaml\nwebhook-secret: your-random-secret-string\nverify-github-ips: true\nmask-sensitive-data: true\n\nrepositories:\n my-repository:\n name: my-org/my-repository\n security-checks:\n mandatory: true\n committer-identity-check: true\n trusted-committers:\n - \"pre-commit-ci[bot]\"\n```\n\nThis gives you the safest baseline for direct GitHub delivery: signed webhooks, GitHub source-IP filtering, masked logs, and blocking committer checks on pull requests. If you do not set `suspicious-paths`, the server already protects common sensitive locations such as `.claude/`, `.vscode/`, `.cursor/`, `.devcontainer/`, `.pi/`, `.github/workflows/`, and `.github/actions/`.\n\n## Step-by-step\n\n1. Set a webhook secret on the server and in GitHub.\n\n```yaml\nwebhook-secret: your-random-secret-string\n```\n\nUse the same value in your GitHub webhook or GitHub App callback settings and in `config.yaml`. If the server manages webhooks for you, it includes that secret when it creates the hook.\n\n> **Warning:** If `webhook-secret` is unset, the server skips signature verification.\n\n\n> **Tip:** See [Webhook and Health API](webhook-and-health-api.html) for the exact webhook callback path.\n\n2. Turn on the right source-IP allowlist for your delivery path.\n\n```yaml\nverify-github-ips: true\n# verify-cloudflare-ips: true\n```\n\n| Delivery path | Setting | Use it when |\n| --- | --- | --- |\n| GitHub connects directly to the server | `verify-github-ips: true` | GitHub is the client your app sees |\n| Cloudflare proxies the webhook | `verify-cloudflare-ips: true` | Cloudflare is the client your app sees |\n| You intentionally accept both paths | Set both to `true` | Either GitHub or Cloudflare may deliver the request |\n\nWhen either allowlist is enabled, the server loads the current CIDR ranges during startup. If you enable IP verification and no valid ranges can be loaded, the service fails closed instead of starting insecurely.\n\n> **Warning:** T"}, {"slug": "debug-with-the-log-viewer", "title": "Debug with the Log Viewer", "content": "# Debug with the Log Viewer\n\nUse this page when a webhook was accepted but the automation still did not do what you expected. The log viewer lets you move from a GitHub delivery ID or PR number to the exact workflow steps and log lines that explain what happened.\n\n## Prerequisites\n\n- A running server instance. See [Start Automating a Repository](quick-start.html) if you have not deployed it yet.\n- `ENABLE_LOG_SERVER=true` in the server environment.\n- Access to the server from localhost, a VPN, or another trusted/private network.\n- A GitHub delivery ID (`X-GitHub-Delivery`) or PR number to investigate.\n\n## Quick Example\n\n```bash\nENABLE_LOG_SERVER=true WEBHOOK_SERVER_DATA_DIR=/path/to/data uv run entrypoint.py\n```\n\n1. Open `http://127.0.0.1:5000/logs`.\n2. Paste the GitHub delivery ID into `Hook ID`.\n3. Click the hook ID in the results to open the workflow timeline.\n4. Click the failed step to see its status, duration, error, and matching log lines.\n\n> **Note:** If you changed the server port in your config, use that port instead of `5000`.\n\n\n> **Warning:** The `/logs` endpoints are unauthenticated. Keep them on trusted networks only. See [Secure Webhooks and Pull Requests](secure-webhooks-and-pull-requests.html) for deployment guidance.\n\n## Step-by-Step\n\n### 1. Enable the viewer and restart the server\n\nIf you run the server directly, start it with `ENABLE_LOG_SERVER=true`.\n\n```bash\nENABLE_LOG_SERVER=true WEBHOOK_SERVER_DATA_DIR=/path/to/data uv run entrypoint.py\n```\n\nIf you run the container, add the same environment variable to the container and restart it.\n\n> **Note:** The value must be the exact string `true`. Other values leave the viewer disabled. See [Environment Variables](environment-variables.html) for the full environment list.\n\n### 2. Open recent history\n\nVisit `/logs` in your browser. The page loads recent entries automatically and shows the newest items first.\n\nStart with **Refresh** if the page is already open and you want the latest historical data from"}, {"slug": "automation-recipes", "title": "Automation Recipes", "content": "# Automation Recipes\n\nShort, copy-pasteable walkthroughs of common maintainer tasks. Every key here is defined in the configuration schema; for full key listings see [Configuration Reference](configuration-reference.html).\n\n## Restrict a repository to specific webhook events\n\n**When you'd want this:** a docs-only or dependency repo doesn't need `push` and `issue_comment` traffic, and a smaller event set means less webhook noise and fewer API calls.\n\nSet `repositories..events` in `config.yaml`. The server creates or edits the repository webhook at startup to match exactly this list. Omit the key to get the default of `[\"*\"]` (all events).\n\n```yaml\n# config.yaml\nrepositories:\n my-docs-site:\n name: org/repo\n events:\n - pull_request\n - issue_comment\n```\n\nVerify what was registered:\n\n```bash\ngh api repos/org/repo/hooks --jq '.[] | select(.config.url | contains(\"hooks\")) | .events'\n```\n\n> **Note:** `events` is read from `config.yaml` only. A repo-local `.github-webhook-server.yaml` is not consulted for it, and the change takes effect on the next server start.\n\n## Enable auto-merge for chosen branches\n\n**When you'd want this:** your release branch should merge on green without a human pressing the button.\n\n```yaml\n# config.yaml\nrepositories:\n my-service:\n name: org/repo\n set-auto-merge-prs:\n - main\n - release\n```\n\nBranch names are matched exactly against the PR base branch. Auto-merge is set with `SQUASH` as the merge method.\n\n### The security interaction\n\nA PR that touches a `security-checks.suspicious-paths` prefix does **not** get auto-merge, even on a configured branch. When a match is found the server:\n\n1. comments `Auto-merge blocked: PR modifies security-sensitive paths: ...` on the PR,\n2. disables auto-merge if it was already enabled on the PR.\n\nDefault prefixes: `.claude/`, `.vscode/`, `.cursor/`, `.devcontainer/`, `.pi/`, `.github/workflows/`, `.github/actions/`.\n\nA maintainer can clear the block with a PR comment:\n\n```\n"}, {"slug": "configuration-reference", "title": "Configuration Reference", "content": "# Configuration Reference\n\nReference for `config.yaml`, repository entries under `repositories.`, and repository-local `.github-webhook-server.yaml` overrides. See [Configure Repositories](configure-repositories.html) for rollout patterns, [Set Up Checks and Release Workflows](set-up-checks-and-release-workflows.html) for workflow examples, [Enable AI Features](enable-ai-features.html) for AI setup, [Secure Webhooks and Pull Requests](secure-webhooks-and-pull-requests.html) for hardening, [Supported GitHub Events](supported-github-events.html) for event coverage, and [Environment Variables](environment-variables.html) for non-YAML settings.\n\n## Files and Resolution\n\n| Scope label | Location |\n|---|---|\n| `Global` | Root of `config.yaml` |\n| `Repo` | `config.yaml` under `repositories.` |\n| `Local` | Root of `.github-webhook-server.yaml` |\n\n| File | Purpose | Effect |\n|---|---|---|\n| `config.yaml` | Server-wide settings and per-repository registration | Required. Defines global defaults, managed repositories, webhook subscriptions, branch settings, and runtime behavior. |\n| `.github-webhook-server.yaml` | Repository-local runtime overrides | Optional. Overrides only the keys the webhook runtime reads from the repository after startup. |\n\n| Resolution rule | Effect |\n|---|---|\n| Scalar and array values | First defined value wins: `Local` -> `Repo` -> `Global`. |\n| `branch-protection` | Repo values overlay global values property by property. |\n| `labels` | Repo/local values overlay global values; `labels.colors` entries merge by key. |\n| `welcome-extra-info` | Empty string clears an inherited value. A repo file named `.github-webhook-server-welcome-message.md` overrides configured text entirely. |\n| `ai-features`, `security-checks`, `test-oracle`, `pr-size-thresholds` | A repo-scoped block replaces the global block instead of deep-merging it. Repeat required subkeys when overriding. |\n\n> **Warning:** `.github-webhook-server.yaml` is not consulted for `"}, {"slug": "environment-variables", "title": "Environment Variables", "content": "# Environment Variables\n\nThe server reads the variables below at startup. Anything not listed here comes from\n`config.yaml` in the data directory, not from the process environment.\n\n## Reference\n\n| Variable | Required? | Default | Purpose |\n| --- | --- | --- | --- |\n| `WEBHOOK_SERVER_DATA_DIR` | Recommended | `/home/podman/data` | Directory holding `config.yaml`, logs, and the webhook key. |\n| `ENABLE_LOG_SERVER` | No | `false` | Serve the log viewer UI, log API, and log WebSocket. |\n| `ENABLE_MCP_SERVER` | No | `false` | Mount the MCP endpoint at `/mcp`. |\n| `WEBHOOK_SERVER_DEV_MODE` | No | unset (production) | Run uvicorn with reload and a single worker. |\n| `SIDECAR_PORT` | No | `9100` | Port for the Pi SDK sidecar started by `entrypoint.sh`. |\n| `SERVER_PORT` | Test-only | none | Local port the E2E smee client forwards webhooks to. |\n| `SMEE_URL` | Test-only | none | Smee.io channel URL used to receive GitHub webhooks in E2E tests. |\n| `TEST_REPO` | Test-only | none | `owner/repo-name` that E2E tests clone and open PRs against. |\n| `DOCKER_COMPOSE_FILE` | Test-only | none | Compose file E2E tests bring the server up with. |\n| `PYTEST_TIMEOUT` | Test-only | `60` (seconds) | Timeout baseline for the test session. |\n\n## `WEBHOOK_SERVER_DATA_DIR`\n\nRead once in `webhook_server/libs/config.py`. The directory must contain `config.yaml`;\nif the file is missing, `Config` raises `FileNotFoundError` on startup. The log viewer and\nthe MCP log file are written under the same directory, so pointing this at a read-only\npath breaks both. The container image creates `/home/podman/data`, which is why that path\nis the default.\n\n```bash\nexport WEBHOOK_SERVER_DATA_DIR=\"$HOME/webhook-server-data\"\nmkdir -p \"$WEBHOOK_SERVER_DATA_DIR\"\n\ncat > \"$WEBHOOK_SERVER_DATA_DIR/config.yaml\" <<'YAML'\ngithub-app-id: 123456\ngithub-tokens:\n - ghp_your_token_here\n\nwebhook-ip: https://your-domain.example/webhook_server\n\nrepositories:\n your-repo:\n name: your-org/your-repo\nYAML\n\nWEBHOOK_SERVER_DATA_D"}, {"slug": "webhook-and-health-api", "title": "Webhook and Health API", "content": "# Webhook and Health API\n\nThe always-available surface is small: a health probe, the GitHub webhook receiver, the `/static` assets used by the log viewer UI, and FastAPI's own `/docs` and `/redoc`. The log viewer API and WebSocket routes are always *registered* too \u2014 `ENABLE_LOG_SERVER` gates *access* at request time rather than registration, so with the flag off `/logs/api/*` answers `404` and `/logs/ws` closes the connection. The log viewer HTML page and `/mcp` are the two routes that are only registered when their feature flag is set.\n\n| Method | Path | Purpose | Registered when |\n| --- | --- | --- | --- |\n| `GET` | `/webhook_server/healthcheck` | Liveness probe | Always |\n| `POST` | `/webhook_server` | GitHub webhook receiver | Always |\n| `GET` | `/static/*` | CSS/JS assets for the log viewer UI | Always (mounted directory) |\n| `GET` | `/logs` | Log viewer UI page | `ENABLE_LOG_SERVER=true` |\n| `GET` | `/logs/api/*` | Log viewer API | Always (access gated at request time by `ENABLE_LOG_SERVER`) |\n| `WS` | `/logs/ws` | Real-time log stream | Always (closes the connection unless `ENABLE_LOG_SERVER=true`) |\n| `GET` | `/docs`, `/redoc` | FastAPI-generated interactive API docs | Always |\n| `GET`/`POST`/`DELETE` | `/mcp` | MCP streamable HTTP transport | `ENABLE_MCP_SERVER=true` |\n\nThe base path is not configurable. In `webhook_server/app.py`:\n\n```python\nAPP_URL_ROOT_PATH: str = \"/webhook_server\"\n```\n\n`GET` uses an f-string on that constant, so it resolves to `/webhook_server/healthcheck`:\n\n```python\n@FASTAPI_APP.get(f\"{APP_URL_ROOT_PATH}/healthcheck\", operation_id=\"healthcheck\")\n```\n\n`POST` passes the constant itself \u2014 **no** trailing segment:\n\n```python\n@FASTAPI_APP.post(\n APP_URL_ROOT_PATH, # -> POST /webhook_server\n operation_id=\"process_webhook\",\n dependencies=[Depends(gate_by_allowlist_ips_dependency)],\n tags=[\"mcp_exclude\"],\n)\n```\n\nBecause the callback path is fixed, `webhook-ip` in `config.yaml` must be the full URL including that path. Th"}, {"slug": "log-viewer-and-mcp-api", "title": "Log Viewer and MCP API", "content": "# Log Viewer and MCP API\n\nReference for the debugging and AI surfaces of the server: the log viewer HTML page, the `/logs/api/*` JSON routes, the `/logs/ws` live stream, and the `/mcp` Model Context Protocol endpoint.\n\n> **\u26a0\ufe0f Security warning \u2014 these endpoints are unauthenticated**\n>\n> `GET /logs`, every `/logs/api/*` route, the `/logs/ws` WebSocket, and `/mcp` have **no authentication of any kind**. There is no auth middleware, no API key, no bearer token, and no session check anywhere in `webhook_server/app.py`. Anyone who can reach the port can read your webhook logs, which contain repository names, PR numbers, GitHub usernames, delivery IDs, error text, and stack traces.\n>\n> - **Never expose these paths to the public internet.** No port-forwarding them to a public host, no public load balancer in front of them, no tunnelling service.\n> - Deploy on a **trusted network only** \u2014 localhost, an internal network, or a VPN.\n> - If remote access is required, front the server with a **reverse proxy that adds authentication** (Basic auth, mTLS, oauth2-proxy, an identity-aware proxy) and only allow it to reach `/logs*` and `/mcp`.\n> - This is the same rule the project's own `AGENTS.md` states: *\"NEVER expose log viewer (`/logs/*`) to public internet \u2014 endpoints are unauthenticated; deploy on trusted networks only.\"*\n\nOne route, `GET /logs/api/step-logs/{hook_id}/{step_name}`, additionally requires a **private, loopback, or link-local client IP** via `require_trusted_network`. That is a network-range check on `request.client.host`, not authentication, and it is trivially bypassed by a misconfigured reverse proxy that does not preserve the client address. It is a speed bump, not a control.\n\n## Route summary\n\n| Method | Path | Purpose | Registered when |\n| --- | --- | --- | --- |\n| `GET` | `/logs` | Log viewer HTML page | `ENABLE_LOG_SERVER=true` |\n| `GET` | `/logs/api/entries` | Filtered, paginated log search | Always; returns `404` if disabled |\n| `GET` | `/logs/api/export`"}, {"slug": "supported-github-events", "title": "Supported GitHub Events", "content": "# Supported GitHub Events\n\nThe server routes eight GitHub webhook event types in `webhook_server/libs/github_api.py` (`GitHubWebHook.process()`). Which of them actually arrive depends on the `repositories..events` subscription configured in `config.yaml`.\n\n| Event | What it does |\n| --- | --- |\n| `ping` | Logged and acknowledged. Token usage is recorded; no further processing. |\n| `push` | Only tag pushes are processed: the repository is cloned at the tag ref and the push handler runs (PyPI upload, container build). Branch pushes and branch/tag deletions are logged and skipped. |\n| `pull_request` | Clones the repository, initializes the OWNERS-file handler, and hands off to the pull request handler (labels, merge eligibility, welcome message on `opened`/`ready_for_review`, CI/CD runs). |\n| `issue_comment` | Clones the repository, initializes the OWNERS-file handler, and hands off to the issue-comment handler (slash commands, reviewer selection). |\n| `pull_request_review` | Clones the repository, initializes the OWNERS-file handler, and hands off to the pull request review handler. |\n| `check_run` | Processed only for `action: completed`, skipping the `can-be-merged` check itself when its conclusion is not `success` and skipping check runs whose `head_sha` no longer matches the PR head. Otherwise clones the repository and hands off to the check run handler, then debounces a merge-eligibility recheck. |\n| `status` | Processed only for terminal commit states that match the current PR head. Re-evaluates can-be-merged (debounced). |\n| `pull_request_review_thread` | Processed only for the `resolved`/`unresolved` actions, and only when `required_conversation_resolution` is enabled. Re-evaluates can-be-merged (debounced). |\n\nAny other event type, or an event for which no pull request can be resolved, is logged and skipped without further processing.\n\n## Restricting events per repository\n\n`config.yaml` accepts an `events` array on each repository entry:\n\n```"}] +[{"slug": "quick-start", "title": "Start Automating a Repository", "content": "# Start Automating a Repository\n\nYou want one GitHub repository under automation quickly so new pull requests start getting webhook handling, labels, and review workflow help without manual setup in GitHub. This guide gets you from an empty data directory to a live webhook and a working health check in a few minutes.\n\n## Prerequisites\n\n- Python `3.14`\n- `uv`\n- A GitHub App ID\n- The matching GitHub App private key in PEM format\n- At least one GitHub token that can manage the target repository\n- A repository where the GitHub App is installed\n- A public URL GitHub can reach, or a `smee.io` channel for local testing\n\n## Quick example\n\n```bash\nexport WEBHOOK_SERVER_DATA_DIR=\"$HOME/webhook-server-data\"\nmkdir -p \"$WEBHOOK_SERVER_DATA_DIR\"\n\ncat > \"$WEBHOOK_SERVER_DATA_DIR/config.yaml\" <<'YAML'\ngithub-app-id: 123456\ngithub-tokens:\n - ghp_your_token_here\n\nwebhook-ip: https://your-domain.example/webhook_server\n\nrepositories:\n your-repo:\n name: your-org/your-repo\nYAML\n\ncp /path/to/your/github-app-private-key.pem \\\n \"$WEBHOOK_SERVER_DATA_DIR/webhook-server.private-key.pem\"\n\nuv sync\nWEBHOOK_SERVER_DATA_DIR=\"$WEBHOOK_SERVER_DATA_DIR\" uv run entrypoint.py\n```\n\n```bash\ncurl http://127.0.0.1:5000/webhook_server/healthcheck\n```\n\n```json\n{\"status\":200,\"message\":\"Alive\"}\n```\n\nOnce the server starts, it bootstraps the repository you listed: it creates or updates the webhook and prepares the built-in automation. The health check proves the process is running; the steps below confirm GitHub can actually reach it.\n\n## Step-by-step\n\n### 1. Create the data directory\n\n```bash\nexport WEBHOOK_SERVER_DATA_DIR=\"$HOME/webhook-server-data\"\nmkdir -p \"$WEBHOOK_SERVER_DATA_DIR\"\n```\n\nYour minimal layout should look like this:\n\n```text\n$WEBHOOK_SERVER_DATA_DIR/\n\u251c\u2500\u2500 config.yaml\n\u2514\u2500\u2500 webhook-server.private-key.pem\n```\n\n> **Note:** If you do not set `WEBHOOK_SERVER_DATA_DIR`, the server uses `/home/podman/data`.\n\n### 2. Add the smallest working `config.yaml`\n\n```yaml\ngithub-app-id: 123456\ngithub-tokens:\n - ghp_your_token_here\n\nwebhook-ip: https://your-domain.example/webhook_server\n\nrepositories:\n your-repo:\n name: your-org/your-repo\n```\n\nUse these values carefully:\n\n| Setting | What to put there |\n|---|---|\n| `github-app-id` | Your GitHub App ID |\n| `github-tokens` | One or more GitHub tokens for repository administration |\n| `webhook-ip` | The callback URL GitHub will call |\n| `repositories.your-repo` | The short repository name, such as `your-repo` |\n| `repositories.your-repo.name` | The full repository name, such as `your-org/your-repo` |\n\n> **Warning:** For a normal public deployment, `webhook-ip` should include the full webhook path, for example `https://your-domain.example/webhook_server`.\n\n\n> **Warning:** The key under `repositories` should be the short repository name, not `owner/repo`. In the example above, the key is `your-repo` and the full name belongs in `name`.\n\n\n> **Warning:** GitHub cannot deliver webhooks to `localhost`. Use a public URL or the `smee.io` workflow in [Advanced Usage](#advanced-usage).\n\n\n> **Tip:** You do not need repo-local overrides for the first run. See [Configure Repositories](configure-repositories.html) for global defaults, per-repo overrides, and self-service settings.\n\n### 3. Add the GitHub App private key\n\n```bash\ncp /path/to/your/github-app-private-key.pem \\\n \"$WEBHOOK_SERVER_DATA_DIR/webhook-server.private-key.pem\"\n```\n\nThe filename matters. The server looks for that exact name in the data directory.\n\nInstall the GitHub App on `your-org/your-repo` before you start the server. You need both the private key and at least one GitHub token for a working setup.\n\n### 4. Install dependencies and start the server\n\n```bash\nuv sync\nWEBHOOK_SERVER_DATA_DIR=\"$WEBHOOK_SERVER_DATA_DIR\" uv run entrypoint.py\n```\n\nBy default, the server listens on `0.0.0.0:5000`. On startup it creates or updates the repository webhook and prepares the repository for automation.\n\n> **Tip:** If you only need one repository working quickly, omit optional keys like `events`, `pre-commit`, `tox`, `container`, and `pypi` until the webhook is live.\n\n### 5. Verify the server is healthy\n\n```bash\ncurl http://127.0.0.1:5000/webhook_server/healthcheck\n```\n\nYou should get:\n\n```json\n{\"status\":200,\"message\":\"Alive\"}\n```\n\nA healthy response means the web process is running and listening on the webhook route.\n\n> **Note:** The health check does not prove that GitHub can reach your `webhook-ip`. It only proves the server is up locally.\n\n### 6. Verify GitHub can deliver to the webhook\n\nAfter the server is running, check the delivery from GitHub:\n\n1. Open the repository in GitHub.\n2. Open the webhook or GitHub App delivery view for that repository.\n3. Send a ping or redeliver the most recent test event.\n4. Confirm the delivery gets an HTTP `200`.\n\nIf you see `200`, the server accepted the event and queued it for processing.\n\n> **Note:** A `200` delivery means the event was accepted for background processing. It does not guarantee every automation step succeeded. See [Webhook and Health API](webhook-and-health-api.html) for the response behavior.\n\n### 7. Prove the repository is actually automated\n\nThe fastest end-to-end proof is a tiny pull request.\n\n1. Open a small PR against `your-org/your-repo`.\n2. Wait for the server to process the `pull_request` event.\n3. Confirm you see automation, such as a welcome comment, labels, or check runs on the PR.\n\nIf that works, the repository is connected and ready for deeper configuration. See [Manage Pull Requests](manage-pull-requests.html) for what happens on new PRs, see [Set Up OWNERS and Review Rules](set-up-owners-and-reviewers.html) before rolling this out to a team, and see [Set Up Checks and Release Workflows](set-up-checks-and-release-workflows.html) to add CI and release automation.\n\n## Advanced Usage\n\n### Add a webhook secret\n\n```yaml\ngithub-app-id: 123456\ngithub-tokens:\n - ghp_your_token_here\n\nwebhook-ip: https://your-domain.example/webhook_server\nwebhook-secret: replace-this-with-a-shared-secret\n\nrepositories:\n your-repo:\n name: your-org/your-repo\n```\n\nAdd `webhook-secret` once the basic flow works. GitHub will sign deliveries, and the server will reject requests with the wrong signature.\n\n> **Tip:** See [Secure Webhooks and Pull Requests](secure-webhooks-and-pull-requests.html) for production hardening after your first repository is connected.\n\n### Use more than one GitHub token\n\n```yaml\ngithub-tokens:\n - ghp_primary_token\n - ghp_backup_token\n```\n\nThe server supports a token pool. This is useful if you want more rate-limit headroom or a backup token when one is exhausted.\n\n### Test locally with `smee.io`\n\n```yaml\nwebhook-ip: https://smee.io/your-channel\n```\n\n```bash\nsmee -u https://smee.io/your-channel -p 5000 -P /webhook_server\nWEBHOOK_SERVER_DATA_DIR=\"$WEBHOOK_SERVER_DATA_DIR\" uv run entrypoint.py\n```\n\nThis is the fastest way to test from a laptop when GitHub cannot reach your machine directly.\n\n> **Warning:** For `smee.io`, use the channel URL as `webhook-ip`. Do not append `/webhook_server` to the `smee.io` URL, because the local forwarding command already adds that path.\n\n### Change the bind address, port, or worker count\n\n```yaml\nip-bind: 0.0.0.0\nport: 8080\nmax-workers: 20\n```\n\nRestart the server after changing these values. If you change `webhook-ip`, restart too so the startup bootstrap can update the repository webhook.\n\nSee [Environment Variables](environment-variables.html) for startup toggles such as log viewer and MCP support.\n\n### Limit the events the repository processes\n\n```yaml\nrepositories:\n your-repo:\n name: your-org/your-repo\n events:\n - pull_request\n - issue_comment\n - push\n```\n\nIf you omit `events`, the webhook is created with `*`. See [Supported GitHub Events](supported-github-events.html) to decide which events you actually want.\n\n## Troubleshooting\n\n- **The server says `config.yaml` was not found**\n - Make sure `WEBHOOK_SERVER_DATA_DIR` points to the directory that contains `config.yaml`.\n - Make sure the file is named exactly `config.yaml`.\n\n- **The server starts, but repository automation never appears**\n - Check that the GitHub App is installed on the repository.\n - Check that the `repositories` key uses the short repo name and `name` uses the full `owner/repo` value.\n - Check that the private key file is named exactly `webhook-server.private-key.pem`.\n\n- **GitHub deliveries fail even though the health check passes**\n - Make sure `webhook-ip` is publicly reachable.\n - Make sure a public deployment uses the full callback path ending in `/webhook_server`.\n - If you set `webhook-secret`, make sure GitHub is using the same shared secret.\n\n- **GitHub shows `200`, but the PR still did not update**\n - The server accepts webhook events immediately and processes them in the background.\n - See [Debug with the Log Viewer](debug-with-the-log-viewer.html) to inspect delivery IDs, step failures, and log output.\n\n## Related Pages\n\n- [Configure Repositories](configure-repositories.html)\n- [Set Up OWNERS and Review Rules](set-up-owners-and-reviewers.html)\n- [Manage Pull Requests](manage-pull-requests.html)\n- [Set Up Checks and Release Workflows](set-up-checks-and-release-workflows.html)\n- [Secure Webhooks and Pull Requests](secure-webhooks-and-pull-requests.html)\n"}, {"slug": "configure-repositories", "title": "Configure Repositories", "content": "# Configure Repositories\n\n```yaml\n# config.yaml\ngithub-app-id: 123456\ngithub-tokens:\n - \n - \n\nwebhook-ip: https://your-domain.com/webhook_server\n\ndefault-status-checks:\n - \"WIP\"\n - \"can-be-merged\"\n\nrepositories:\n my-repository:\n name: my-org/my-repository\n events:\n - pull_request\n - issue_comment\n - check_run\n - status\n tox:\n main: all\n pre-commit: true\n protected-branches:\n main:\n include-runs:\n - \"pre-commit.ci - pr\"\n - \"WIP\"\n```\n\n```yaml\n# .github-webhook-server.yaml\nconventional-title: \"feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert\"\nminimum-lgtm: 2\ncreate-issue-for-new-pr: true\n```\n\nThe first file gives the server everything it needs to bootstrap and manage the repository. The second file lets repository maintainers tune day-to-day behavior without editing the server.\n\n## Step-by-step\n\n### 1. Put shared defaults in `config.yaml`\n\nStart with the settings that should apply everywhere unless a repository overrides them.\n\n```yaml\nlog-level: INFO\nlog-file: webhook-server.log\nmask-sensitive-data: true\n\ngithub-app-id: 123456\ngithub-tokens:\n - \n - \n\nwebhook-ip: https://your-domain.com/webhook_server\n\ndefault-status-checks:\n - \"WIP\"\n - \"dpulls\"\n - \"can-be-merged\"\n\nbranch-protection:\n strict: true\n require_code_owner_reviews: true\n dismiss_stale_reviews: false\n required_approving_review_count: 1\n required_linear_history: true\n required_conversation_resolution: true\n```\n\nUse top-level settings for:\n\n- Shared defaults across many repositories\n- Secrets and credentials\n- Startup-time behavior such as webhook creation and branch protection\n- Organization-wide policies\n\n> **Warning:** `webhook-ip` must include the full webhook path, for example `https://your-domain.com/webhook_server`.\n\n### 2. Add each repository under `repositories`\n\nAdd one entry per managed repository. Use the short repository name as the key, and the full `owner/repo` name inside `name`.\n\n```yaml\nrepositories:\n my-repository:\n name: my-org/my-repository\n\n another-repo:\n name: my-org/another-repo\n```\n\nUse this pattern when you want the same server to manage multiple repositories with shared defaults.\n\n> **Warning:** The key under `repositories` should be the repository name that GitHub sends in webhook payloads, such as `my-repository`, not `my-org/my-repository`.\n\n### 3. Add server-side overrides for repository-specific behavior\n\nPut per-repository overrides directly inside the matching repository block when the setting is repo-specific, secret, or part of startup bootstrap.\n\n```yaml\nrepositories:\n my-repository:\n name: my-org/my-repository\n log-level: DEBUG\n log-file: my-repository.log\n github-tokens:\n - \n events:\n - push\n - pull_request\n - pull_request_review\n - pull_request_review_thread\n - issue_comment\n - check_run\n - status\n tox:\n main: all\n dev: testenv1,testenv2\n pre-commit: true\n protected-branches:\n main:\n include-runs:\n - \"pre-commit.ci - pr\"\n - \"WIP\"\n exclude-runs:\n - \"SonarCloud Code Analysis\"\n```\n\nThis is the right place for:\n\n- Repository-specific event subscriptions\n- Dedicated tokens\n- Protected branch rules\n- Logging overrides\n- Server-controlled credentials such as Slack webhooks, registry passwords, and PyPI tokens\n\nIf you want the repository to receive all events, omit `events` entirely instead of listing everything.\n\n### 4. Let repository maintainers self-serve safe settings\n\nAdd a `.github-webhook-server.yaml` file at the repository root when maintainers should be able to adjust behavior without editing the server.\n\n```yaml\nconventional-title: \"feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert\"\nminimum-lgtm: 2\npre-commit: true\n\nlabels:\n enabled-labels:\n - verified\n - hold\n - size\n colors:\n hold: purple\n\npr-size-thresholds:\n Quick:\n threshold: 20\n color: lightgreen\n Normal:\n threshold: 100\n color: green\n```\n\nThis is a good fit for:\n\n- PR title rules\n- Minimum review thresholds\n- Label behavior\n- PR size labels\n- Draft PR command allowances\n- AI feature behavior\n- Security-check behavior that repository maintainers are allowed to tune\n\n> **Warning:** Do not commit secrets to `.github-webhook-server.yaml`. Keep tokens, passwords, and other credentials in server-side `config.yaml`.\n\n### 5. Use the right config layer for the right job\n\nUse this split when you are deciding where a setting belongs:\n\n| Put the setting here | Best for | Typical examples |\n|---|---|---|\n| Top-level `config.yaml` | Shared defaults and org-wide policy | `default-status-checks`, `branch-protection`, `mask-sensitive-data` |\n| `repositories.` in `config.yaml` | Server-side repo overrides and secrets | `github-tokens`, `protected-branches`, `slack-webhook-url`, `pypi`, `container` |\n| `.github-webhook-server.yaml` | Safe maintainer-controlled behavior | `conventional-title`, `minimum-lgtm`, `labels`, `pr-size-thresholds` |\n\nPrecedence is highest to lowest:\n\n| Priority | Source | Use it when |\n|---|---|---|\n| 1 | `.github-webhook-server.yaml` | Repository maintainers should control the setting |\n| 2 | `repositories.` in `config.yaml` | The server should override defaults for one repository |\n| 3 | Top-level `config.yaml` | The setting should apply everywhere unless overridden |\n\n### 6. Restart when bootstrap settings change\n\nSome settings are applied during startup bootstrap, not during normal PR processing. After changing these in `config.yaml`, restart the server.\n\nRestart after changes to settings such as:\n\n- `webhook-ip`\n- `repositories`\n- `events`\n- `branch-protection`\n- `protected-branches`\n\n> **Note:** The repository-local `.github-webhook-server.yaml` file is used during webhook processing, but not for startup bootstrap. Put bootstrap-critical settings in `config.yaml`.\n\n## Advanced Usage\n\n### Keep secrets server-side and behavior repo-side\n\nA practical split looks like this:\n\n| Server-side only | Safe to hand to repository maintainers |\n|---|---|\n| `github-tokens` | `conventional-title` |\n| `pypi.token` | `minimum-lgtm` |\n| `container.password` | `labels` |\n| `slack-webhook-url` | `pr-size-thresholds` |\n| `github-app-id` | `allow-commands-on-draft-prs` |\n\nThis keeps credentials out of Git history while still letting teams tune workflow behavior.\n\n### Use repository-specific tokens only where needed\n\nIf most repositories can share one token pool, keep `github-tokens` at the top level. Add a repository-specific token list only when a repository needs different permissions or rate-limit isolation.\n\n```yaml\ngithub-tokens:\n - \n - \n\nrepositories:\n my-repository:\n name: my-org/my-repository\n github-tokens:\n - \n```\n\n### Override only the keys you need\n\nThe repository-local file does not need to repeat the full server config. Add only the keys you want to change.\n\n```yaml\n# .github-webhook-server.yaml\nminimum-lgtm: 1\nallow-commands-on-draft-prs:\n - build-and-push-container\n```\n\nThat keeps the repository config small and makes overrides easy to review.\n\n### Use `protected-branches` to control required checks per branch\n\nYou can require all defaults, or include and exclude specific checks for one branch.\n\n```yaml\nprotected-branches:\n dev: []\n main:\n include-runs:\n - \"pre-commit.ci - pr\"\n - \"WIP\"\n exclude-runs:\n - \"SonarCloud Code Analysis\"\n```\n\nUse an empty list when you want the default required checks for that branch. Use `include-runs` and `exclude-runs` when one branch needs a different requirement set. `exclude-runs` is subtracted from the final list in both cases, so it also removes an entry that `include-runs` added.\n\n### Hand off specialized settings to the right pages\n\nKeep this page focused on where configuration lives and how it overrides. For the actual knobs in those feature areas, use the dedicated pages:\n\n- See [Set Up Checks and Release Workflows](set-up-checks-and-release-workflows.html) for `tox`, `pre-commit`, custom checks, container builds, and PyPI settings.\n- See [Enable AI Features](enable-ai-features.html) for `ai-features` and `test-oracle`.\n- See [Secure Webhooks and Pull Requests](secure-webhooks-and-pull-requests.html) for `security-checks`.\n- See [Set Up OWNERS and Review Rules](set-up-owners-and-reviewers.html) for `OWNERS`-driven review behavior.\n\n## Troubleshooting\n\n- **The server says the repository is not configured**\n - Check the key under `repositories`.\n - Make sure `name` contains the full `owner/repo`, but the map key is only the short repository name.\n\n- **The repo-local file is ignored**\n - The file must be named exactly `.github-webhook-server.yaml`.\n - It must live at the repository root.\n - If you changed a bootstrap setting, move that change into `config.yaml` and restart the server.\n\n- **A setting is not taking effect**\n - Check the precedence order: repo-local file overrides repository block, which overrides global defaults.\n - Remove duplicate definitions while debugging so you can see which layer is winning.\n\n- **Branch protection or webhook subscriptions did not update**\n - Restart the server after changing `webhook-ip`, `events`, `branch-protection`, or `protected-branches`.\n\n- **Config loads fail after editing labels**\n - Verify that `labels.enabled-labels` only uses supported label categories.\n - See [Configuration Reference](configuration-reference.html) for the allowed values.\n\n## Related Pages\n\n- [Configuration Reference](configuration-reference.html)\n- [Set Up Checks and Release Workflows](set-up-checks-and-release-workflows.html)\n- [Enable AI Features](enable-ai-features.html)\n- [Secure Webhooks and Pull Requests](secure-webhooks-and-pull-requests.html)\n- [Set Up OWNERS and Review Rules](set-up-owners-and-reviewers.html)\n"}, {"slug": "set-up-owners-and-reviewers", "title": "Set Up OWNERS and Review Rules", "content": "# Set Up OWNERS and Review Rules\n\nThe webhook server does not use GitHub's CODEOWNERS. It reads its own `OWNERS` files from\nyour repository and uses them to decide who must review a pull request, who counts as an\n*approver*, who can run commands, and which reviewers get auto-assigned.\n\n## What `OWNERS` controls\n\n| Key | Effect |\n| --- | --- |\n| `approvers` | Members of this scope who can `/approve`. An approval from this list is required for the `can-be-merged` check only when the scope has at least one approver; a path with no approvers listed imposes no approval requirement. |\n| `reviewers` | Members of this scope who are eligible for reviewer auto-assignment; `/lgtm` is not gated by membership. |\n| `allowed-users` | Users (in addition to collaborators and contributors) who may run commands such as `/retest`, `/cherry-pick`, `/rebase`, `/check-can-merge`, `/assign-reviewers`. |\n| `root-approvers` | Optional boolean. Set to `false` to opt out of requiring an approval from the root `OWNERS` approvers. |\n\n## File discovery rules\n\nThese are exact \u2014 the parser is strict.\n\n- The filename must be exactly `OWNERS` (uppercase, no extension). `OWNERS.yaml`, `owners`,\n `.github/CODEOWNERS`, and `OWNERS_ALIASES` are **not** read.\n- Files are found recursively in the cloned repository, which is checked out at the **base\n branch** of the PR. An `OWNERS` file added only in the PR branch is ignored until merged.\n- Any path with a dot-prefixed directory component (`.git`, `.venv`, `.github`, ...) is skipped.\n- At most 1000 `OWNERS` files are processed per PR.\n- Each file is keyed by its **parent directory**, not by file path. `webhook_server/OWNERS`\n is stored under `webhook_server`; the repo-root `OWNERS` is stored under `.`.\n\n## Supported syntax\n\nEach `OWNERS` file is a YAML **mapping**. If it is not a mapping, or if `approvers`,\n`reviewers`, or `allowed-users` is present but is not a list of strings, the file is rejected\n(logged as `Invalid OWNERS file `) and contributes nothing. Unknown keys are allowed\nand ignored by the validator, with the exception of `root-approvers`, which is meaningful.\n\n```yaml\napprovers:\n - alice\n - bob\nreviewers:\n - carol\n - dave\n```\n\nA real example \u2014 this repository's own root `OWNERS`:\n\n```yaml\napprovers:\n - myakove\n - rnetser\nreviewers:\n - myakove\n - rnetser\n```\n\nA subdirectory file with the opt-out flag:\n\n```yaml\nroot-approvers: false\napprovers:\n - docs-team\nreviewers:\n - writer1\n - writer2\nallowed-users:\n - \"@contractor\"\n```\n\nNotes on values:\n\n- Usernames are **GitHub logins**, matched case-sensitively against labels such as\n `approved-alice`. Write them exactly as the login.\n- `allowed-users` accepts a leading `@` (it is stripped), and is read **only from the repo-root\n `OWNERS`** \u2014 an `allowed-users` list in a subdirectory is ignored.\n- `root-approvers` only affects a subdirectory file. Setting it in the root `OWNERS` has no\n effect, because the root entry is always included anyway.\n\n## How owners are resolved for a PR\n\n1. The webhook server clones the repo and runs\n `git diff --name-only ...` to get the changed files. (It falls back to\n two-dot `..` if there is no merge base.)\n2. It takes the parent directory of every changed file as the set of *changed folders*.\n3. Every `OWNERS` file whose directory is equal to, or an ancestor of, a changed folder is\n matched. So `webhook_server/OWNERS` matches a change to `webhook_server/libs/config.py`, and\n also matches a change to `webhook_server/libs/OWNERS`'s subdirectories.\n4. The root `OWNERS` (key `.`) is added unless a matched subdirectory file opts out with\n `root-approvers: false`. When several files match, the **first match processed** decides the\n opt-out, so don't mix opting-out and non-opting-out files in one PR.\n5. `all_pull_request_approvers` / `all_pull_request_reviewers` are the de-duplicated unions of\n the approvers/reviewers from those matched files.\n\nSeparately, `all_repository_approvers` is the union of approvers from **every** `OWNERS` file in\nthe repository, regardless of what the PR touches. That repo-wide list is what makes a user\neligible to run commands; it is not what gates `/approve`.\n\n## Review commands\n\n### `/approve`\n\n- Only works if the commenter is in `all_pull_request_approvers` (matched by the changed files)\n **or** in the root `OWNERS` approvers. Anyone else is silently ignored.\n- Adds the label `approved-` and removes `changes-requested-`.\n- `/approve cancel` removes it.\n- A `/approve` line anywhere in a submitted pull-request review body does the same thing.\n- Any `/approve` triggers the test oracle with `trigger=\"approved\"`.\n\n### `/lgtm`\n\n- Adds `lgtm-` and removes `changes-requested-`.\n- Available to **any** commenter except the PR author \u2014 it is not restricted to OWNERS\n reviewers. Whether the LGTM *counts* toward `minimum-lgtm` is a separate check.\n- A GitHub review with state `approved` or `commented` also produces the `lgtm-` label.\n- A GitHub review with state `changes_requested` produces `changes-requested-`, which\n blocks the merge check if that user is a matched approver.\n\n### Who may run other commands\n\n`/retest`, `/reprocess`, `/cherry-pick`, `/rebase`, `/check-can-merge`, `/assign-reviewer(s)`,\n`/add-allowed-user`, `/regenerate-welcome`, `/build-and-push-container`, `/security-override`\nrequire the commenter to be in the union of:\n\n- repository collaborators,\n- repository contributors,\n- every approver in the repository (`all_repository_approvers`),\n- the matched PR reviewers (`all_pull_request_reviewers`),\n- root `OWNERS` `allowed-users`.\n\nOtherwise a maintainer or approver can grant access per-PR by commenting\n`/add-allowed-user @someone`. A `/hold` label is managed with a narrower maintainers-only gate.\n\n`/assign-reviewers` adds every matched reviewer as a review request (the PR author is skipped).\n\n## Configuration\n\nThese are **repository-level** settings under `repositories.` in `config.yaml`.\n\n### `minimum-lgtm`\n\n```yaml\nrepositories:\n my-repository:\n name: my-org/my-repository\n minimum-lgtm: 1\n```\n\nDefault `0` (no LGTM requirement). Type: integer. When greater than zero, the welcome comment\nadds a \"LGTM Count\" line to the merge requirements.\n\nHow the count works in `check_if_can_be_merged` \u2192 `_check_if_pr_approved`:\n\n- Eligible LGTM authors = matched PR reviewers + root approvers + root reviewers, **minus the\n PR committer**.\n- `lgtm_count` counts `lgtm-` labels whose login is in that eligible set. An `lgtm` from\n someone outside it is ignored.\n- A PR **passes** if `lgtm_count >= minimum-lgtm`, **or** if `lgtm_count` equals the total number\n of eligible reviewers (i.e. everyone who could have LGTM'd already has). So a repo with two\n eligible reviewers and `minimum-lgtm: 2` is satisfied by two LGTMs even if one is below the\n threshold in other configurations, and `minimum-lgtm` larger than the reviewer count is never\n an impossible requirement.\n- On failure the check-run reports:\n `Missing lgtm from reviewers. Minimum N required, (M given). Reviewers: a, b.`\n\n### `can-be-merged-required-labels`\n\n```yaml\nrepositories:\n my-repository:\n name: my-org/my-repository\n can-be-merged-required-labels:\n - signed-off\n - ci/green\n```\n\nDefault `[]`. Type: array of strings. Every listed label must be present on the PR, otherwise\nthe `can-be-merged` check run fails with `Missing required labels:
    - +
    @@ -264,7 +264,7 @@

    Step-by-step

    -
    Delivery path Either GitHub or Cloudflare may deliver the request
    +

    When either allowlist is enabled, the server loads the current CIDR ranges during startup. If you enable IP verification and no valid ranges can be loaded, the service fails closed instead of starting insecurely.

    Warning: The allowlist check uses the client IP the app actually sees. If another proxy or load balancer sits in front of the server, you may end up validating the proxy IP instead of GitHub or Cloudflare.

    @@ -286,7 +286,7 @@

    Step-by-step

    Use suspicious-paths for path prefixes that should trigger extra review, and keep committer-identity-check: true so the last committer must match the pull request author unless that committer is explicitly trusted.

    - +
    @@ -303,7 +303,7 @@

    Step-by-step

    -
    Setting Security checks still run, but they do not block merge readiness
    +

    Note: trusted-committers is best for expected automation identities. The server already trusts the GitHub App bot, GitHub web-flow, and GitHub API users tied to your configured github-tokens, so you usually only need to add extra bots or org-specific service accounts.

    @@ -439,7 +439,7 @@

    On this page

    @@ -447,13 +447,14 @@

    On this page

    - - - - - - - + + + + + + + + + diff --git a/docs/set-up-checks-and-release-workflows.html b/docs/set-up-checks-and-release-workflows.html index 98c3f68df..c3ef5a6cb 100644 --- a/docs/set-up-checks-and-release-workflows.html +++ b/docs/set-up-checks-and-release-workflows.html @@ -5,7 +5,7 @@ Set Up Checks and Release Workflows - github-webhook-server - + @@ -175,8 +175,8 @@ - - ⚡ docsfy + + ⚡ pi-docsite @@ -228,7 +228,7 @@

    How a check run gets created

    The CI stage is driven by PullRequestHandler._run_pull_request_ci_tasks(). It always schedules tox, pre-commit, python-module-install, and build-container; each of those runners returns early if its feature is not configured. conventional-title is scheduled only when its config is present. The two security checks need no opt-in: with no security-checks block at all, suspicious-paths falls back to DEFAULT_SUSPICIOUS_PATHS and both mandatory and committer-identity-check default to true, so both run. Every validated custom check is scheduled unconditionally.

    The built-in checks

    Check-run names are constants in webhook_server/utils/constants.py, so what the GitHub UI shows is exactly these strings.

    - +
    @@ -283,7 +283,7 @@

    The built-in checks

    -
    Check run Human-driven: a check run named after the verified label, set to success when the label is added.
    +

    Notes per check:

    Background processing

    process_webhook builds a structured context, constructs GithubWebhook, calls await api.process(), and always calls await api.cleanup(). Errors are caught and logged; none of them change the HTTP response:

    - +
    @@ -405,11 +405,11 @@

    Background processing

    -
    Background failure Re-raised (shutdown), not logged as an error
    +

    Each run writes a structured log record (see Debug with the Log Viewer) and logs a summary with the delivery ID prefix. During shutdown the server waits up to 30 seconds for in-flight background tasks, then cancels them.

    Error responses

    All errors are FastAPI's standard JSON error shape.

    - +
    @@ -479,7 +479,7 @@

    Error responses

    -
    Status Failure while loading config for signature verification
    +
    { "detail": "Request signatures didn't match!" }
     
    @@ -517,7 +517,7 @@

    2. Source-IP allowlist

    Other route groups (summary)

    Full reference: Log Viewer and MCP API and Debug with the Log Viewer.

    Log viewer — ENABLE_LOG_SERVER=true

    - +
    @@ -562,7 +562,7 @@

    Log viewer — ENABLE_LOG_SERVER

    -
    MethodReal-time log stream with the same filters as the UI. Closes with 1008 when the log server is disabled.
    +

    Every /logs/api/* route depends on require_log_server_enabled, which returns 404 with Log server is disabled. Set ENABLE_LOG_SERVER=true to enable. when the flag is off.

    MCP — ENABLE_MCP_SERVER=true

    FASTAPI_APP.add_api_route(
    @@ -600,7 +600,7 @@ 

    Full request walkthrough

    Then follow that delivery_id in the log viewer (hook_id filter on /logs/api/entries) to see whether processing actually succeeded.

    For a live OpenAPI schema, /docs and /redoc are served by FastAPI (title webhook-server) with POST /webhook_server hidden from the MCP tool surface via its mcp_exclude tag.

    Configuration keys this API depends on

    - +
    @@ -633,7 +633,7 @@

    Configuration keys this API depe

    -
    KeyLog destinations for the log viewer and MCP server.
    +

    See Configuration Reference for every key and Environment Variables for the feature flags.

    @@ -690,7 +690,7 @@

    On this page

    @@ -698,13 +698,14 @@

    On this page

    - - - - - - - + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml index 9f342df2f..0de9e76da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,9 +38,6 @@ strict_equality = true extra_checks = true warn_unused_configs = true warn_redundant_casts = true -# scripts/docs_render/ is a real package; without this mypy also maps it through the -# implicit "scripts" namespace and reports one file under two module names. -namespace_packages = false [tool.hatch.build.targets.wheel] packages = ["webhook_server"] @@ -111,9 +108,6 @@ build-backend = "hatchling.build" dev = [ "ipdb>=0.13.13", "ipython>=8.12.3", - "jinja2>=3.1.6", - "markdown>=3.11", - "pygments>=2.21.0", "types-aiofiles>=25.1.0.20251011", "types-colorama>=0.4.15.20240311", "types-pyyaml>=6.0.12.20250516", diff --git a/scripts/docs_render/__init__.py b/scripts/docs_render/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/docs_render/renderer.py b/scripts/docs_render/renderer.py deleted file mode 100644 index 2f1858cf6..000000000 --- a/scripts/docs_render/renderer.py +++ /dev/null @@ -1,703 +0,0 @@ -"""Static markdown -> HTML renderer for the documentation site. - -Vendored verbatim (modulo the adaptations below) from ``myk-org/docsfy`` -(``src/docsfy/renderer.py``) so this repo can rebuild ``docs/`` on its own, -without depending on the external docsfy clone at runtime. Only the -deterministic rendering half was taken: no AI generation, no network, no -``docsfy`` package imports. The docsfy images pipeline (``copy_images_to_site``) -is not vendored because this repo ships no ``docsfy-images/`` source. - -Upstream behaviour is preserved, including the "Generated with docsfy" badge, -which keeps working via the ``docsfy_repo_url`` template variable. - -It is a vendored copy, so local modifications are expected. The local -deviations are ``_indent_fenced_blocks`` (see its docstring, ``llms-full.txt`` -only) and the parser-based ``_sanitize_html`` allowlist (upstream ships a -regex stripper that had four separate bypasses); everything else is -byte-identical to upstream. -""" - -from __future__ import annotations - -import html as _html_mod -import logging -import re -import urllib.parse -from html.parser import HTMLParser -from pathlib import Path -from typing import Any - -import markdown -from jinja2 import Environment, FileSystemLoader, select_autoescape - -from webhook_server.utils.helpers import get_logger_with_params - - -def _make_logger() -> logging.Logger: - """Build the project logger, degrading to stdlib when no runtime config exists. - - ``get_logger_with_params()`` constructs a ``Config``, which raises when - ``$WEBHOOK_SERVER_DATA_DIR/config.yaml`` is absent. A docs rebuild must run - on a bare checkout with no webhook-server deployment, so fall back to a - plain stdlib logger rather than aborting over a missing config. A config - that exists but is malformed is left to fail loudly. - """ - try: - return get_logger_with_params() - except FileNotFoundError: - return logging.getLogger(__name__) - - -logger = _make_logger() - -DOCSFY_REPO_URL = "https://github.com/myk-org/docsfy" - -# Vendored from docsfy.generator. Deliberately NOT re.DOTALL, and the title is -# restricted to a single line ([^\n]+ rather than .+). Without these -# constraints, "." matches newlines, so a real multi-paragraph document that -# happens to *end* with this exact phrase would have its entire body greedily -# swallowed as part of the "title" and get misclassified as a stub. -_FAILURE_STUB_RE = re.compile(r"^#[ \t]+[^\n]+\n\n\*Documentation generation failed\.(?: Please re-run\.)?\*\s*$") - - -TEMPLATES_DIR = Path(__file__).parent / "templates" -STATIC_DIR = Path(__file__).parent / "static" -_jinja_env: Environment | None = None - - -def _get_jinja_env() -> Environment: - global _jinja_env - if _jinja_env is None: - _jinja_env = Environment( - loader=FileSystemLoader(str(TEMPLATES_DIR)), - autoescape=select_autoescape(["html"]), - ) - return _jinja_env - - -# Sanitizer allowlist. Anything not listed is dropped. Upstream docsfy strips -# dangerous tags with regexes, which four separate bypasses walked straight through -# (````, ````, an unclosed -# ``

    after

    ') - assert cleaned == "

    after

    " - - -def test_markdown_table_after_prose_with_an_unknown_tag_survives() -> None: - md = ( - 'Unknown names get a *"No configured for this repository"* comment.\n\n' - "Supported names depend on the repository configuration:\n\n" - "| Test name | Runs |\n| --- | --- |\n" - "| `tox` | The Python test suite |\n" - "| `pre-commit` | Runs pre-commit hooks and checks |\n" - ) - content_html, _toc_html = _md_to_html(md) - for expected in ( - "", - "", - "", - "", - "", - "", - "", - "", - "", - "
    Test nameRuns
    toxThe Python test suitepre-commitRuns pre-commit hooks and checks
    ", - ): - assert expected in content_html - assert "configured for this repository" in content_html - - -@pytest.mark.parametrize( - "tag", - ["script", "style", "iframe", "object", "form", "noscript", "template", "svg", "math", "frameset"], -) -def test_code_or_markup_carrying_tags_still_drop_their_contents(tag: str) -> None: - cleaned = _sanitize_html(f"<{tag}>SECRET-PAYLOAD

    after

    ") - assert "SECRET-PAYLOAD" not in cleaned - assert f"<{tag}" not in cleaned - assert cleaned == "

    after

    " - - -def test_void_disallowed_tag_drops_only_itself() -> None: - # `` has no end tag, so it must not open a drop region. - cleaned = _sanitize_html("

    before after

    ") - assert cleaned == "

    before after

    " - - -def test_script_body_does_not_survive_inside_prose_either() -> None: - cleaned = _sanitize_html("

    before after

    ") - assert "alert(1)" not in cleaned - assert " None: - cleaned = _sanitize_html( - '' - ) - assert cleaned == "" - - -def test_unclosed_iframe_srcdoc_is_still_fully_dropped() -> None: - assert _sanitize_html('visible") - assert cleaned == "
    visible
    " - - -def test_event_handler_and_style_attributes_are_dropped_by_default() -> None: - cleaned = _sanitize_html('

    t

    ') - assert cleaned == '

    t

    ' - assert "onclick" not in cleaned - assert "style" not in cleaned - - -# --- Allowlist: URLs -------------------------------------------------------- -def test_dangerous_url_schemes_are_replaced_with_fragment() -> None: - cleaned = _sanitize_html( - 'j' - 'v' - 'pr' - '' - ) - assert cleaned == 'jvpr' - for bad in ("javascript:", "vbscript:", "data:", "evil.example"): - assert bad not in cleaned - - -def test_tab_obfuscated_javascript_url_is_neutralised() -> None: - # Browsers drop TAB/LF/CR before resolving the scheme, so this is live JS. - assert _sanitize_html('x') == 'x' - - -# --- Backslash authority: browsers fold "\" to "/", so "\host" navigates ----- -_BACKSLASH_AUTHORITY_URLS = [ - "\\\\evil.example", # \\evil.example - "/\\evil.example", # /\evil.example - "\\/evil.example", # \/evil.example - "/\\/evil.example", # /\evil.example - "\\\\\\evil.example", # \\\evil.example - "\\\\evil.example/p", # \\evil.example/p - "\\\\evil.example\\p", # \\evil.example\p -] - - -@pytest.mark.parametrize("url", _BACKSLASH_AUTHORITY_URLS) -def test_backslash_authority_is_rejected_in_href(url: str) -> None: - assert _sanitize_html(f'x') == 'x' - - -@pytest.mark.parametrize("url", _BACKSLASH_AUTHORITY_URLS) -def test_backslash_authority_is_rejected_in_src(url: str) -> None: - assert _sanitize_html(f'') == '' - - -@pytest.mark.parametrize("url", _BACKSLASH_AUTHORITY_URLS) -def test_backslash_authority_stays_rejected_behind_html_entities(url: str) -> None: - # \ is a real HTML5 named ref for "\" and / for "/", so entity - # encoding must not be a way around the fold. - encoded = url.replace("\\", "\").replace("/", "/") - assert _sanitize_html(f'x') == 'x' - assert _sanitize_html(f'') == '' - - -@pytest.mark.parametrize("url", _BACKSLASH_AUTHORITY_URLS) -def test_backslash_authority_stays_rejected_behind_noise(url: str) -> None: - # TAB/LF/CR/NUL are dropped by the browser before the authority is parsed. - assert _sanitize_html(f'x') == 'x' - - -def test_legitimate_relative_links_are_unaffected_by_the_fold() -> None: - # The docs link each other and the repo, so these must keep resolving. - html = ( - 'next' - 'readme' - 'css' - 'rel' - 'deep' - ) - assert _sanitize_html(html) == html - - -def test_legitimate_relative_links_survive_markdown_rendering() -> None: - content_html, _toc_html = _md_to_html("See [next](quick-start.html) and [repo](../README.md).\n") - assert 'href="quick-start.html"' in content_html - assert 'href="../README.md"' in content_html - - -@pytest.mark.parametrize( - "url", - ["http://example.com/x", "https://example.com/x", "mailto:someone@example.com", "#section", "#"], -) -def test_allowed_url_shapes_still_pass(url: str) -> None: - assert _sanitize_html(f'x') == f'x' - - -@pytest.mark.parametrize("url", ["javascript:alert(1)", "data:text/html;base64,PHNjcmlwdD4=", "vbscript:msgbox"]) -def test_dangerous_schemes_are_still_rejected(url: str) -> None: - assert _sanitize_html(f'x') == 'x' - assert _sanitize_html(f'') == '' - - -def test_malformed_ipv6_authority_does_not_break_the_render() -> None: - # urlsplit() raises ValueError on "x://["; the sanitizer must not take the - # whole docs build down over it. - assert _sanitize_html('x

    after

    ') == 'x

    after

    ' - - -# --- THE REGRESSION: Pygments + TOC markup must survive ---------------------- -def test_pygments_span_classes_inside_pre_code_survive() -> None: - html = '
    def f():
    ' - assert _sanitize_html(html) == html - assert 'def' in _sanitize_html(html) - assert 'f' in _sanitize_html(html) - assert '():' in _sanitize_html(html) - - -def test_heading_ids_survive_so_table_of_contents_links_keep_working() -> None: - assert _sanitize_html('

    Install

    ') == '

    Install

    ' - - -# --- Malformed input -------------------------------------------------------- -@pytest.mark.parametrize( - "malformed", - [ - "

    unclosed", - "

    stray < lt and > gt

    ", - 'a

    stray closers", - "