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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: CI

on:
push:
pull_request:

# Two packages, two jobs. They share a repository and an idea; they share no
# build.
jobs:
js:
name: js (${{ matrix.node }})
runs-on: ubuntu-latest
defaults:
run:
working-directory: js
strategy:
fail-fast: false
matrix:
node: ['18', '20', '22']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
cache-dependency-path: js/package-lock.json
- run: npm ci
- run: npm run typecheck
- run: npm test
- run: npm run build

python:
name: python (${{ matrix.python }})
runs-on: ubuntu-latest
defaults:
run:
working-directory: python
strategy:
fail-fast: false
matrix:
python: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
# Installing the package rather than putting src/ on the path is what
# proves the packaging works, not only the source tree.
- run: python -m pip install --upgrade pip build
- run: python -m pip install .
- run: python -m unittest discover -s tests -v
- run: python -m build
39 changes: 39 additions & 0 deletions .github/workflows/release-npm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Release (npm)

# Manual only. A release is a decision, not a side effect of a merge.
on:
workflow_dispatch:
inputs:
tag:
description: 'npm dist-tag to publish under'
required: false
default: 'latest'

permissions:
contents: read
id-token: write # npm provenance

jobs:
publish:
name: Publish @kingdom-community/github-docs
runs-on: ubuntu-latest
environment: npm
defaults:
run:
working-directory: js
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: npm
cache-dependency-path: js/package-lock.json
- run: npm ci
# Never publish something that was not just proven to build and pass.
- run: npm run typecheck
- run: npm test
- run: npm run build
- run: npm publish --provenance --access public --tag "${{ inputs.tag }}"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
33 changes: 33 additions & 0 deletions .github/workflows/release-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Release (PyPI)

# Manual only. A release is a decision, not a side effect of a merge.
on:
workflow_dispatch:

permissions:
contents: read
id-token: write # PyPI trusted publishing

jobs:
publish:
name: Publish github-docs
runs-on: ubuntu-latest
environment: pypi
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python -m pip install --upgrade pip build
- run: python -m pip install .
# Never publish something that was not just proven to pass.
- run: python -m unittest discover -s tests -v
- run: python -m build
# Trusted publishing: configure this repo + workflow as a publisher on
# PyPI, and no long-lived API token has to exist anywhere.
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: python/dist
134 changes: 127 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,129 @@
# github-docs

Run your community's documentation out of a GitHub repository. This repo will
ship two packages that share one idea: the docs live as markdown in a git repo,
the website reads them, and edits land as pull requests. `js/` will hold
`@kingdom-community/github-docs`, a TypeScript reader that fetches markdown for
rendering and never lets an upstream failure become a 5xx or leak a token.
`python/` will hold `github-docs`, a stdlib-only writer that commits an edit to
a per-file branch and opens (or reuses) a pull request for it.
**Run your community's documentation out of a GitHub repository.**

Your rules page, your onboarding guide, your staff handbook: write them as
markdown, keep them in a git repo, and let the repo be the source of truth. Your
website renders them. Edits from your web app arrive as pull requests, so
someone reviews a change to the rules before it is the rules.

That is one idea with two halves, and the two halves were built in different
languages — so this repo ships two packages.

| | Package | Language | Does |
|---|---|---|---|
| **Read** | [`@kingdom-community/github-docs`](./js) | TypeScript | Fetches markdown from the repo for rendering |
| **Write** | [`github-docs`](./python) | Python (stdlib only) | Lands edits as pull requests |

They are independent. Use one, use the other, use both. They do not talk to each
other; they talk to the same repository.

## The shape of it

```
acme-guild/handbook (markdown in git)
▲ │
│ pull request │ fetch
│ ▼
github-docs (Python) @kingdom-community/github-docs (TS)
your admin/staff app your website
```

## Read side — `js/`

```bash
npm install @kingdom-community/github-docs
```

```ts
import {createDocsClient} from '@kingdom-community/github-docs';

const docs = createDocsClient({
repo: 'acme-guild/handbook',
documents: ['handbook/rules.md', 'handbook/getting-started.md']
});

const result = await docs.fetchMarkdown('rules');
if (result.status === 'ok') {
render(result.markdown);
} else {
renderPanel(`Read it on GitHub: ${docs.webUrl('rules')}`);
}
```

Two rules run through all of it, and they are the reason it is worth installing
rather than writing four lines of `fetch`:

1. **Failure is a value, not an exception.** Every function returns a
discriminated result. GitHub having a bad minute produces a readable panel
and an HTTP 200, never a 5xx. A page that has to remember to catch is a page
that will one day forget.
2. **Nothing upstream is ever quoted back.** No response body, no header, no
request URL, and above all no token appears in a returned value or in an
error. There is no path by which a GitHub error page reaches your HTML.

It also ships the URL-scheme allowlist that keeps `[click me](javascript:…)` in
a community-authored document from becoming script execution in a reader's
browser — including the spellings that survive a naive
`startsWith('javascript:')`.

[Full documentation →](./js/README.md)

## Write side — `python/`

```bash
pip install github-docs
```

```python
from github_docs import GitHubDocsClient, GitHubDocsConfig

docs = GitHubDocsClient(GitHubDocsConfig(
repo="acme-guild/handbook",
token=os.environ["DOCS_GITHUB_TOKEN"],
allowed_roots=("handbook", "policies"),
))

result = docs.save_file("handbook/rules.md", new_text, author="mod99")
print(result.pr_url) # https://github.com/acme-guild/handbook/pull/42
```

Never a direct push. A save commits to a per-file branch and opens a pull
request — or finds the open one from the last save and adds to it, so repeated
edits to the same page update one PR instead of piling up duplicates. The pull
request *is* the review mechanism, which is why there is no diff or version UI
to build.

Standard library `urllib` only. No `requests`, no dependency tree.

[Full documentation →](./python/README.md)

## Why the split

The read side runs in a website's render layer, where the language is
TypeScript and the constraint is that an outage must not become an error page.
The write side runs in an admin app, where the language is Python and the
constraint is that nothing bypasses review. Different jobs, different failure
modes, different code. Sharing a repository keeps the two descriptions of "how
this community's docs work" from drifting apart.

## Development

```bash
# read side
cd js && npm install && npm test

# write side
cd python && python3 -m unittest discover -s tests -v
```

CI runs both on every push and pull request.

## License

MIT.

## Origins

Extracted from the website and infrastructure stack behind a Minecraft
community server, generalised and released under MIT.
21 changes: 21 additions & 0 deletions js/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Daniel McCoy Stephenson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading