diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..3869da8
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,68 @@
+name: Documentation
+
+on:
+ push:
+ branches: ["main"]
+ paths:
+ - ".github/workflows/pages.yml"
+ - ".python-version"
+ - "docs/**"
+ - "mkdocs.yml"
+ - "pyproject.toml"
+ - "uv.lock"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ name: Build documentation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pages: read
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v8.1.0
+
+ - name: Set up Python
+ run: uv python install
+
+ - name: Install dependencies
+ run: uv sync --locked --group dev
+
+ - name: Configure GitHub Pages
+ uses: actions/configure-pages@v5
+
+ - name: Build documentation site
+ run: uv run mkdocs build --strict
+
+ - name: Upload GitHub Pages artifact
+ uses: actions/upload-pages-artifact@v5
+ with:
+ path: site
+
+ deploy:
+ name: Deploy documentation
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 24219f7..6de8fee 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -169,6 +169,10 @@ merging are disabled to keep that history consistent.
After a pull request is merged, GitHub deletes its remote branch automatically.
Delete the corresponding local branch after updating `main`.
+When relevant documentation files change, the separate `Documentation`
+workflow publishes the reviewed site after the merge. This deployment is not a
+pull request check and does not replace the required `Quality checks` status.
+
## Documentation terminology
Use the official spelling of tool names: `uv`, pytest, mypy, `rumdl`, and
diff --git a/README.md b/README.md
index 64ae979..b4a01d5 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# Modern Python Project Guide
[](https://github.com/michalmaj/modern-python-project-guide/actions/workflows/ci.yml)
+[](https://michalmaj.github.io/modern-python-project-guide/)
A practical, beginner-friendly guide to building a clean Python project with
`uv`, `pytest`, Ruff, mypy, `rumdl`, MkDocs, `pyproject.toml`, GitHub Actions,
@@ -17,6 +18,8 @@ tool gradually and shows how the pieces fit together in a maintainable project.
- **Learn the complete workflow:** start with
[Why This Guide Exists](docs/00_why_this_guide.md) and follow the main learning
path below.
+- **Browse the published guide:** open the
+ [documentation site](https://michalmaj.github.io/modern-python-project-guide/).
- **Find a command or solve a problem:** jump to the
[reference material](#reference-material).
@@ -34,6 +37,7 @@ By following the guide, you will learn how to:
- check type annotations with mypy,
- lint Markdown documentation with `rumdl`,
- preview and validate a documentation site with MkDocs,
+- publish documentation safely with GitHub Pages,
- automate fast checks with `pre-commit` hooks,
- run automated quality checks in GitHub Actions,
- work with branches, commits, and pull requests.
@@ -61,7 +65,8 @@ understand why each tool and file is introduced.
13. [`pre-commit` hooks](docs/13_pre_commit_hooks.md)
14. [Markdown linting with rumdl](docs/14_markdown_linting.md)
15. [Documentation site with MkDocs](docs/15_documentation_site_with_mkdocs.md)
-16. [Project checklist](docs/09_checklist.md)
+16. [Publishing with GitHub Pages](docs/16_publishing_with_github_pages.md)
+17. [Project checklist](docs/09_checklist.md)
## Reference material
diff --git a/docs/01_project_structure.md b/docs/01_project_structure.md
index ea4d95d..55658d2 100644
--- a/docs/01_project_structure.md
+++ b/docs/01_project_structure.md
@@ -14,7 +14,8 @@ The repository currently uses this structure:
modern-python-project-guide/
├── .github/
│ ├── workflows/
-│ │ └── ci.yml
+│ │ ├── ci.yml
+│ │ └── pages.yml
│ └── pull_request_template.md
├── docs/
│ ├── 00_why_this_guide.md
@@ -107,8 +108,12 @@ Example:
```text
.github/pull_request_template.md
.github/workflows/ci.yml
+.github/workflows/pages.yml
```
+The CI workflow validates pull requests and `main`. The Pages workflow publishes
+the documentation after relevant changes reach `main`.
+
## mkdocs.yml
The `mkdocs.yml` file defines the documentation site, including its navigation,
diff --git a/docs/06_github_actions.md b/docs/06_github_actions.md
index bf3a628..3b0706e 100644
--- a/docs/06_github_actions.md
+++ b/docs/06_github_actions.md
@@ -242,6 +242,11 @@ The workflow builds the MkDocs site with strict validation:
The step validates navigation and rendering without publishing the generated
site. The `site/` output directory remains an ignored local artifact.
+Publishing is handled by the separate `.github/workflows/pages.yml` workflow
+after relevant changes reach `main`. Pull requests never receive deployment
+permissions. See [Publishing with GitHub Pages](16_publishing_with_github_pages.md)
+for the build, artifact, environment, and permission flow.
+
## Tests and coverage
The workflow runs tests and prints a coverage report:
diff --git a/docs/09_checklist.md b/docs/09_checklist.md
index 459b180..9257674 100644
--- a/docs/09_checklist.md
+++ b/docs/09_checklist.md
@@ -204,6 +204,10 @@ When the documentation set grows, a generated site can improve navigation:
- [ ] generated `site/` files are ignored
- [ ] strict site builds run locally and in CI
- [ ] building and publishing are treated as separate operations
+- [ ] a dedicated deployment workflow publishes only from `main`
+- [ ] generated output is uploaded as an artifact instead of committed
+- [ ] deployment permissions are limited to the deployment job
+- [ ] the public site contains no secrets or private material
Preview and validate the site with:
@@ -229,6 +233,17 @@ The project should run checks automatically in CI:
- [ ] CI builds a wheel and source distribution
- [ ] CI status is visible in the README
+## Continuous deployment
+
+If the documentation is published automatically:
+
+- [ ] GitHub Pages uses **GitHub Actions** as its source
+- [ ] the deployment workflow runs after relevant changes reach `main`
+- [ ] pull requests validate the site without publishing it
+- [ ] build and deploy jobs pass the generated site as an artifact
+- [ ] the deployment uses the `github-pages` environment
+- [ ] the published site is checked after deployment
+
CI helps protect the main branch from broken changes.
## Git workflow
@@ -355,5 +370,5 @@ Let the project grow gradually.
## Continue
-- [Previous: Documentation Site with MkDocs](15_documentation_site_with_mkdocs.md)
+- [Previous: Publishing with GitHub Pages](16_publishing_with_github_pages.md)
- [Back to documentation home](index.md)
diff --git a/docs/15_documentation_site_with_mkdocs.md b/docs/15_documentation_site_with_mkdocs.md
index fcb6712..67b9047 100644
--- a/docs/15_documentation_site_with_mkdocs.md
+++ b/docs/15_documentation_site_with_mkdocs.md
@@ -101,13 +101,14 @@ The documentation checks have separate responsibilities:
No single tool replaces the others.
-## Build is not deployment
+## Build and deployment are separate
-The current CI job only proves that the site can be generated. It does not
-publish HTML, change GitHub Pages settings, or require deployment permissions.
+The `Quality checks` CI job only proves that the site can be generated. It does
+not publish HTML or require deployment permissions.
-Publishing should be evaluated and implemented separately after the local site
-has been reviewed.
+After a reviewed change reaches `main`, a separate workflow publishes the
+generated site. Keeping these operations separate prevents pull requests from
+changing the public documentation.
## Rule of thumb
@@ -118,5 +119,5 @@ Use the generated site to improve navigation, search, and previewing.
## Continue
- [Previous: Markdown Linting with rumdl](14_markdown_linting.md)
-- [Next: Project Checklist](09_checklist.md)
+- [Next: Publishing with GitHub Pages](16_publishing_with_github_pages.md)
- [Back to documentation home](index.md)
diff --git a/docs/16_publishing_with_github_pages.md b/docs/16_publishing_with_github_pages.md
new file mode 100644
index 0000000..6e6acf8
--- /dev/null
+++ b/docs/16_publishing_with_github_pages.md
@@ -0,0 +1,116 @@
+# Publishing with GitHub Pages
+
+A strict MkDocs build proves that the documentation can be generated. GitHub
+Pages makes that generated site available to readers on the web.
+
+This project publishes the site at:
+
+
+
+## Build and deployment remain separate
+
+The repository has two workflows with different responsibilities:
+
+- `.github/workflows/ci.yml` validates every pull request and push to `main`,
+- `.github/workflows/pages.yml` publishes documentation after relevant changes
+ reach `main`.
+
+A pull request therefore cannot update the public site. It must first pass the
+normal review and `Quality checks` requirement. The merge commit then starts the
+documentation workflow.
+
+## One-time repository setting
+
+The repository owner must enable GitHub Pages once:
+
+1. Open the repository **Settings**.
+2. Select **Pages** under **Code, planning, and automation**.
+3. Under **Build and deployment**, set **Source** to **GitHub Actions**.
+
+The workflow does not publish from a `gh-pages` branch and does not commit the
+generated `site/` directory.
+
+## When deployment runs
+
+The documentation workflow runs when a push to `main` changes its own workflow,
+the Python version, documentation sources, MkDocs configuration, or dependency
+files. It can also be started manually from the **Actions** tab.
+
+The path filter avoids redeploying an unchanged site after unrelated source-code
+changes. Manual dispatch remains available for recovery or verification.
+
+## Build job
+
+The `build` job recreates the site from a clean checkout:
+
+1. check out the repository,
+2. install `uv` and the configured Python version,
+3. install locked development dependencies,
+4. configure GitHub Pages,
+5. run `uv run mkdocs build --strict`,
+6. upload `site/` as a GitHub Pages artifact.
+
+The artifact is the boundary between building and deploying. It contains the
+generated static site, but it is not committed to the repository.
+
+## Deploy job
+
+The `deploy` job waits for the build job. It deploys the uploaded artifact to
+the `github-pages` environment and reports the resulting public URL.
+
+The workflow grants only the permissions it needs:
+
+- `contents: read` reads repository files,
+- `pages: write` creates the Pages deployment,
+- `id-token: write` lets GitHub verify the deployment with OpenID Connect.
+
+The write permissions belong only to the deploy job. The normal CI workflow
+does not receive them.
+
+## Why use an environment?
+
+GitHub Pages deployments use the `github-pages` environment. It records
+deployment history and can enforce protection rules.
+
+The workflow also uses a `pages` concurrency group. A running production
+deployment is allowed to finish instead of being cancelled by a newer run.
+
+## Verify a deployment
+
+After merging a documentation change:
+
+1. open the repository **Actions** tab,
+2. select the **Documentation** workflow,
+3. confirm that **Build documentation** and **Deploy documentation** passed,
+4. open the deployment URL shown by the deploy job,
+5. verify the changed page and navigation.
+
+The existing `Quality checks` status remains the branch-protection requirement.
+Deployment happens after merge, so a deployment failure should be fixed with a
+new pull request rather than by weakening that requirement.
+
+## Public output and secrets
+
+Everything in the generated site is public. Documentation sources and build
+configuration must not contain credentials, private notes, or other sensitive
+information.
+
+This workflow needs no repository secrets. GitHub supplies a short-lived token
+with the declared permissions.
+
+## Custom domains are separate
+
+This project uses the default `github.io` address. A custom domain would require
+additional DNS and repository settings and is intentionally outside this setup.
+
+## Rule of thumb
+
+Validate on pull requests. Publish only from the protected default branch.
+
+Keep generated files out of Git and deploy a reproducible artifact instead.
+
+## Continue
+
+- [Previous: Documentation Site with MkDocs](15_documentation_site_with_mkdocs.md)
+- [Next: Project Checklist](09_checklist.md)
+- [Back to documentation home](index.md)
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index 98d9c62..05b1946 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -140,6 +140,8 @@ These parts are already included in the repository.
- [x] Keep Markdown linting separate from link checks and human review
- [x] Add an MkDocs site with explicit navigation and strict CI builds
- [x] Keep documentation sources readable without the generated site
+- [x] Publish the documentation site with GitHub Pages
+- [x] Keep validation and deployment in separate workflows
## Next
@@ -169,10 +171,6 @@ They may be added later as separate chapters.
- [ ] Explain publishing to PyPI
-### Documentation site
-
-- [ ] Decide whether to publish the MkDocs site with GitHub Pages
-
### Release automation
- [ ] Explain versioning
@@ -192,7 +190,7 @@ These topics were deliberately excluded from the original foundation:
- Docker,
- publishing to PyPI,
- release automation,
-- documentation site generation,
+- documentation site generation and deployment,
- pre-commit hooks,
- coverage gates,
- complex multi-job CI,
diff --git a/docs/common_questions.md b/docs/common_questions.md
index 5d4c02c..61bbc4d 100644
--- a/docs/common_questions.md
+++ b/docs/common_questions.md
@@ -389,14 +389,22 @@ They point readers toward the same material, but they appear in different
contexts. Keeping the site landing page inside `docs/` also follows MkDocs'
normal source layout.
-## Why does CI build the site without publishing it?
+## Why does pull request CI build the site without publishing it?
A strict build verifies configuration, navigation, links, anchors, and
rendering without changing external state.
Publishing requires a separate deployment workflow, permissions, and GitHub
-Pages settings. Those decisions should be reviewed independently after the
-local site has proved useful.
+Pages settings. In this repository, that workflow runs only after relevant
+changes reach `main`, so a pull request cannot update the public site.
+
+## Where is the documentation site published?
+
+The site is available at
+.
+
+The `Documentation` workflow builds it from the same Markdown sources and
+deploys a generated artifact. The generated `site/` directory is not committed.
## Rule of thumb
diff --git a/docs/github_actions_logs.md b/docs/github_actions_logs.md
index 7fee454..598b481 100644
--- a/docs/github_actions_logs.md
+++ b/docs/github_actions_logs.md
@@ -84,6 +84,17 @@ Do not debug everything at once.
Start with the failed step.
+The separate `Documentation` workflow runs after relevant changes reach `main`.
+It has two jobs:
+
+```text
+Build documentation
+Deploy documentation
+```
+
+The build job creates and uploads the site artifact. The deploy job publishes
+that artifact to the `github-pages` environment.
+
## Step: Checkout repository
### What it does
@@ -811,6 +822,22 @@ CI checks repetitive things.
Humans still review meaning and quality.
+## What a green documentation deployment means
+
+A green `Documentation` workflow means:
+
+- the site was built strictly from locked dependencies,
+- the generated `site/` directory was uploaded as an artifact,
+- GitHub Pages accepted the deployment,
+- GitHub reported a public deployment URL.
+
+It does not prove that every page looks or reads as intended. Open the published
+site and inspect the changed page after deployment.
+
+If `Build documentation` fails, reproduce the strict build locally. If `Deploy
+documentation` fails, inspect Pages settings, workflow permissions, the
+`github-pages` environment, and the first useful deployment error.
+
## Rule of thumb
When CI fails, ask:
diff --git a/docs/glossary.md b/docs/glossary.md
index 9c3df89..82d150f 100644
--- a/docs/glossary.md
+++ b/docs/glossary.md
@@ -213,6 +213,14 @@ uv run mkdocs build --strict
The build reads `mkdocs.yml` and the Markdown files in `docs/`. It writes
generated HTML to the ignored `site/` directory.
+## GitHub Pages
+
+GitHub Pages hosts static websites generated from repository content.
+
+In this guide, a dedicated GitHub Actions workflow builds the MkDocs site after
+relevant changes reach `main`, uploads `site/` as an artifact, and deploys it to
+the `github-pages` environment.
+
## Package
The word “package” can mean different things in Python.
diff --git a/docs/index.md b/docs/index.md
index f32df97..1777395 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,7 @@ modern tools and a pull-request-based workflow.
The same Markdown files remain readable directly on GitHub. MkDocs adds site
navigation, search, and a local preview without replacing the repository README.
+Reviewed changes are published here from the protected `main` branch.
## Choose your path
@@ -24,9 +25,10 @@ The generated site provides:
- navigation across the complete guide,
- full-text search,
- a consistent reading layout,
-- strict build validation in continuous integration.
+- strict build validation in continuous integration,
+- automatic publication after relevant changes reach `main`.
-The site is built locally and in CI. Publishing it is a separate, later step.
+Building and publishing remain separate operations with different permissions.
## Start learning
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 868da96..21f2341 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -497,6 +497,32 @@ If port 8000 is already in use, choose another address:
uv run mkdocs serve --dev-addr 127.0.0.1:8001
```
+## GitHub Pages deployment fails
+
+### Problem
+
+The `Documentation` workflow builds the site but cannot deploy it.
+
+### Possible causes
+
+- Pages is not configured to use **GitHub Actions** as its source,
+- the deploy job is missing `pages: write` or `id-token: write`,
+- the build job did not upload the `github-pages` artifact,
+- an environment protection rule is waiting for approval,
+- GitHub Pages or Actions is temporarily unavailable.
+
+### Fix
+
+Open **Settings → Pages** and confirm that **Source** is **GitHub Actions**.
+Then inspect the first useful error in the failed `Deploy documentation` job.
+
+Do not commit `site/` or create a `gh-pages` branch to work around this workflow.
+Fix the configuration in a new pull request and rerun the deployment after it
+reaches `main`.
+
+If the workflow passed but the site still shows old content, open the deployment
+URL reported by the job and confirm that the expected commit triggered the run.
+
## A pre-commit hook fails or changes files
### Problem
diff --git a/mkdocs.yml b/mkdocs.yml
index d8ef06b..50d4ba0 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,5 +1,6 @@
site_name: Modern Python Project Guide
site_description: A beginner-friendly guide to modern Python project tooling.
+site_url: https://michalmaj.github.io/modern-python-project-guide/
repo_name: michalmaj/modern-python-project-guide
repo_url: https://github.com/michalmaj/modern-python-project-guide
@@ -25,6 +26,7 @@ nav:
- pre-commit hooks: 13_pre_commit_hooks.md
- Markdown linting with rumdl: 14_markdown_linting.md
- Documentation site with MkDocs: 15_documentation_site_with_mkdocs.md
+ - Publishing with GitHub Pages: 16_publishing_with_github_pages.md
- Project checklist: 09_checklist.md
- Quickstarts:
- Create a project with uv: quickstart_uv.md