Skip to content
This repository was archived by the owner on Aug 4, 2026. It is now read-only.
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
28 changes: 24 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
name: Deploy Documentation
name: Deploy redirects

# This site moved to https://commit-check.com. What this repository publishes
# is a set of redirect stubs, built by scripts/build_redirects.py rather than
# by mkdocs.
#
# The deploy matters more than usual: once this repository is archived, Actions
# stop running and Pages keeps serving the last artifact indefinitely. The
# redirect map is therefore checked on every run — a URL left out is a link
# that breaks permanently.

on:
push:
branches: [main]
Expand All @@ -19,10 +29,20 @@ jobs:
with:
python-version: '3.x'

- name: Install Dependencies
run: pipx run nox -s docs
# mkdocs is still needed: the check builds the old site and compares the
# URLs it produces against the redirect map.
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest -r docs/requirements.txt

- name: Check every old URL still has a forwarding address
run: pytest tests/ -q

- name: Build redirects
run: python scripts/build_redirects.py

- name: Upload docs build as artifact
- name: Upload redirects as artifact
uses: actions/upload-pages-artifact@v5
with:
name: ${{ github.event.repository.name }}_docs
Expand Down
48 changes: 45 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
# commit-check.github.io
# commit-check.github.io — moved to commit-check.com

[![Website](https://img.shields.io/static/v1?label=Website&message=commit-check.github.io&color=2c9ccd&logo=git&logoColor=white)](https://commit-check.github.io)
> [!IMPORTANT]
> **This repository is no longer the source of the Commit Check website.**
>
> The documentation, the landing page and the blog now live at
> **[commit-check.com](https://commit-check.com)**, built from
> **[commit-check/commit-check.com](https://github.com/commit-check/commit-check.com)**.
>
> Open issues and pull requests about the website there, not here.

This repository contains the source code for the [commit-check.github.io](https://commit-check.github.io) website.
[![Website](https://img.shields.io/static/v1?label=Website&message=commit-check.com&color=2c9ccd&logo=git&logoColor=white)](https://commit-check.com)

## What this repository is now

It is kept as the historical source of the old site. The `docs/` directory is
still here and still readable, but it is no longer published.

What `commit-check.github.io` serves is a set of redirect stubs — one for every
URL the old site had — pointing at the page that replaced it. The pages, the
blog and its archive, author and category indexes all kept their paths, so the
redirects are one-to-one; `/projects/` is the exception, having been folded into
the Ecosystem section of the new landing page.

| Old URL | Now |
|---|---|
| `commit-check.github.io/` | [commit-check.com/](https://commit-check.com/) |
| `commit-check.github.io/getting-started/` | [commit-check.com/getting-started/](https://commit-check.com/getting-started/) |
| `commit-check.github.io/blog/…` | [commit-check.com/blog/…](https://commit-check.com/blog/) — same paths |
| `commit-check.github.io/projects/` | [commit-check.com/](https://commit-check.com/) |

The redirects are generated by `scripts/build_redirects.py`, and `tests/`
verifies the map still covers every URL the old site served. GitHub Pages has
no server-side redirect table, so each stub is a `rel=canonical` plus a
`<meta refresh>` — the canonical is what moves search ranking to the new URL.

Two hosts serve this repository, so both are covered: GitHub Pages gets the
stubs, and `netlify.toml` gives the Netlify project real 301s and stops it
building the old site from a command configured in its web UI.

## Why the redirects come before the archive

Archiving a repository makes it read-only and stops its Actions from running,
but it does **not** unpublish its GitHub Pages site: whatever was deployed last
keeps being served. So the redirects have to be deployed *before* the archive
switch is flipped. Afterwards there is no way to change what this domain serves
without unarchiving first.
33 changes: 33 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# This repository is also wired to a Netlify project, which until now built the
# mkdocs site from a command configured in Netlify's web UI. That command is
# invisible from here and outlives any change to the GitHub Actions workflow,
# so without this file Netlify would keep publishing the old site after the
# GitHub Pages deploy had been switched to redirects — and the deploy preview
# on a pull request would show the old site too, which makes the redirects look
# broken when they are not.
#
# Settings here override the UI, so the build is defined in version control.

[build]
command = "python scripts/build_redirects.py"
publish = "site"

# Netlify, unlike GitHub Pages, can serve a real 301. These take precedence
# over the stub files the build writes, which stay as the fallback for hosts
# without a redirect table.
#
# Order matters: Netlify takes the first rule that matches, so the one page
# that did not survive as its own URL has to come before the catch-all.
[[redirects]]
from = "/projects/*"
to = "https://commit-check.com/"
status = 301
force = true

# Everything else kept its path on the new site, so the tail is carried across
# unchanged. This also covers URLs the stub list does not enumerate.
[[redirects]]
from = "/*"
to = "https://commit-check.com/:splat"
status = 301
force = true
113 changes: 113 additions & 0 deletions scripts/build_redirects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Build the redirect-only site this repository now publishes.

The site moved to https://commit-check.com. This repository is kept as the
historical source — ``docs/`` is still here and still readable — but what it
publishes is a set of redirect stubs, one per URL the old site served, so links
already out in the world land on the page that replaced them.

Why stubs rather than a server redirect: GitHub Pages serves static files and
has no redirect table, so a ``<meta refresh>`` plus a ``rel=canonical`` is the
only mechanism available. The canonical link is what transfers search ranking
to the new URL; the meta refresh and the script are what move a reader.

Deliberately no ``robots: noindex`` on these stubs. It reads like the tidy thing
to do, but it contradicts the canonical: one says "consolidate this page onto
that URL", the other says "drop this page from the index", and a crawler that
honours the second may never act on the first — or carry the noindex across to
the target. A migration wants the canonical to be believed.

Why this runs instead of ``mkdocs build``: once this repository is archived,
Actions stop running and the last deployed artifact is what Pages serves
forever. That artifact needs to be the redirects, so the redirects have to be
deployed *before* the archive switch is flipped, not after.

Run with ``python scripts/build_redirects.py`` — output goes to ``site/``.
"""

from __future__ import annotations

import sys
from pathlib import Path

NEW_SITE = "https://commit-check.com"

#: Every URL the mkdocs site served, taken from its own build output, mapped to
#: the path that replaced it on the new site.
#
# All but one are the same path: the pages, the blog, its archive, author and
# category indexes were carried over unchanged, and the posts kept their
# filenames and ``created`` dates, so the generated slugs match byte for byte.
#
# ``/projects/`` is the exception. It was folded into the Ecosystem section of
# the new landing page, so it redirects to the root rather than to a page that
# does not exist.
SAME_PATH = [
"/",
"/getting-started/",
"/blog/",
"/blog/2026/06/21/ai-native-json-output-and-a-python-api/",
"/blog/2026/06/21/from-zero-config-to-org-wide-policy/",
"/blog/2026/06/21/one-policy-file-for-your-git-history/",
"/blog/2026/07/06/ai-attribution-governance-enforcing-ai-disclosure-policies-at-the-ci-level/",
"/blog/archive/2026/",
"/blog/author/team/",
"/blog/category/announcements/",
"/blog/category/updates/",
]

REDIRECTS = {path: path for path in SAME_PATH} | {"/projects/": "/"}

# The fragment is carried across by the script: a reader following a deep link
# into a page should keep their place. ``location.replace`` rather than
# ``location.href`` so the stub does not land in the back-button history and
# trap them in a loop between the two sites.
TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Moved to commit-check.com</title>
<link rel="canonical" href="{target}">
<meta http-equiv="refresh" content="0; url={target}">
<script>location.replace("{target}" + location.hash);</script>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<style>
body {{ font-family: system-ui, sans-serif; margin: 4rem auto; max-width: 34rem;
padding: 0 1rem; line-height: 1.6; }}
a {{ color: #2c9ccd; }}
</style>
</head>
<body>
<h1>This site has moved</h1>
<p>The Commit Check documentation, landing page and blog are now published at
<a href="{target}">{target}</a>.</p>
<p>If you are not redirected automatically, follow the link above.</p>
</body>
</html>
"""


def main() -> int:
site = Path(__file__).resolve().parent.parent / "site"
for old, new in REDIRECTS.items():
target = NEW_SITE + new
page = site / old.strip("/") / "index.html"
page.parent.mkdir(parents=True, exist_ok=True)
page.write_text(TEMPLATE.format(target=target), encoding="utf-8")

# Pages serves this for any path with no file of its own, which covers the
# URLs this list missed — a stray deep link, a page from an older layout.
# It points at the new site's root because there is nothing better to guess.
(site / "404.html").write_text(
TEMPLATE.format(target=NEW_SITE + "/"), encoding="utf-8"
)

# Without this, Pages runs the output through Jekyll, which skips files and
# directories whose names begin with an underscore.
(site / ".nojekyll").write_text("", encoding="utf-8")

print(f"wrote {len(REDIRECTS)} redirects + 404 fallback to {site}")
return 0


if __name__ == "__main__":
sys.exit(main())
69 changes: 69 additions & 0 deletions tests/redirects_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Check the redirect map against the site it is replacing.

This repository publishes redirects now, and once it is archived the artifact
from the last successful run is what GitHub Pages serves for good — Actions do
not run on an archived repository, so there is no second chance to fix a URL
that was left out. That makes the map worth checking while it can still change.

The check builds the mkdocs site the old way and compares the URLs it produces
against the redirect map, in both directions: a URL the old site served with no
redirect is a link that will break, and a redirect for a URL the old site never
served is a sign the map was edited by hand and drifted.
"""

from __future__ import annotations

import subprocess
import sys
import tempfile
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))

from build_redirects import REDIRECTS # noqa: E402


def _urls_the_old_site_served() -> set[str]:
"""Build the mkdocs site and return every URL it publishes."""
with tempfile.TemporaryDirectory() as tmp:
result = subprocess.run(
[sys.executable, "-m", "mkdocs", "build", "--site-dir", tmp],
cwd=ROOT,
capture_output=True,
text=True,
)
if result.returncode != 0:
# Not a skip. A skip exits pytest 0, the workflow reads that as a
# pass and deploys redirects that were never checked against the
# site they replace — on the one deploy that cannot be redone.
pytest.fail(f"mkdocs build failed: {result.stderr.strip()[:200]}")
site = Path(tmp)
urls = set()
for page in site.rglob("index.html"):
rel = page.parent.relative_to(site).as_posix()
# ``relative_to`` gives "." for the site root, which is "/".
urls.add("/" if rel == "." else f"/{rel}/")
return urls


def test_every_published_url_has_a_redirect():
"""Nothing the old site served may be left without a forwarding address."""
missing = sorted(_urls_the_old_site_served() - set(REDIRECTS))
assert not missing, (
"these URLs are served by the current site but have no redirect, so "
"they will break when this repository is archived:\n "
+ "\n ".join(missing)
)


def test_no_redirect_points_at_a_url_that_never_existed():
"""A redirect for a URL the site never served means the map drifted."""
served = _urls_the_old_site_served()
invented = sorted(set(REDIRECTS) - served)
assert not invented, (
"these redirects are for URLs the site does not serve:\n "
+ "\n ".join(invented)
)