Skip to content
This repository was archived by the owner on Aug 4, 2026. It is now read-only.

Commit 056c83c

Browse files
shenxianpengclaude
andauthored
docs: publish redirects to commit-check.com ahead of archiving (#14)
Step 1 of retiring this repository. **Merge and verify this before flipping the archive switch** — see below for why the order is not interchangeable. ## The problem with archiving first Archiving makes a repository read-only and stops its Actions, but it does **not** unpublish its GitHub Pages site. Whatever was deployed last keeps being served indefinitely. So archiving as the first step would leave a complete copy of the old site live at `commit-check.github.io` forever — competing with commit-check.com in search results, and serving pre-v2.13.0 content including rule names the tool no longer prints (`subject_imperative` rather than `subject-imperative`). And because Actions do not run on an archived repository, there would be no way to fix it without unarchiving. Hence: redirects first, archive second. ## What this does Replaces the mkdocs build with `scripts/build_redirects.py`, which emits one redirect stub per URL the old site served. The map was taken from that site's **own build output**, not from reading the config. All but one entry is the same path — the pages, the blog and its archive, author and category indexes carried over unchanged, and the posts kept their filenames and `created` dates, so the generated slugs match byte for byte: | Old URL | Redirects to | |---|---| | `/` | `commit-check.com/` | | `/getting-started/` | `commit-check.com/getting-started/` | | `/blog/`, `/blog/archive/2026/`, `/blog/author/team/`, `/blog/category/{announcements,updates}/` | same paths | | the four blog posts | same paths | | `/projects/` | `commit-check.com/` — folded into the Ecosystem section of the new landing page | GitHub Pages has no server-side redirect table, so each stub is a `rel=canonical` plus a `<meta refresh>`. The canonical is what transfers search ranking; the script carries `location.hash` across so deep links keep their place, and uses `location.replace` so the stub does not trap a reader in the back button. A `404.html` catches anything the map missed. ## The guard `tests/redirects_test.py` builds the old mkdocs site and compares its URLs against the map **in both directions**: - a URL the site serves with no redirect → a link that breaks permanently - a redirect for a URL the site never served → the map was hand-edited and drifted CI runs it before the deploy step. Verified by deleting one entry: the test names the missing URL and fails. This matters more than a usual test because the deploy is one-shot. After archiving, the artifact from the last successful run is what Pages serves for good. ## What is kept `docs/` stays exactly as it is — this repository remains the historical source of the old site, it just no longer publishes it. `mkdocs` is still a CI dependency because the guard needs it to build the site it is checking against. The README now leads with the migration notice and points at [commit-check/commit-check.com](https://github.com/commit-check/commit-check.com). Worth noting the README cannot be edited after archiving, which is why the notice ships in this PR rather than later. ## After merging 1. Confirm the Pages deploy succeeded and spot-check a couple of URLs (`/projects/` and one blog post are the interesting ones). 2. Then archive the repository in Settings → General → Danger Zone. --- _Generated by [Claude Code](https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn)_ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added static redirect pages for legacy site URLs, including project links. * Redirects preserve URL paths and fragments while sending visitors to the new site. * Added a fallback page for unmatched URLs. * **Documentation** * Updated the README to explain the archived site, redirect behavior, URL mappings, and the new destination. * **Bug Fixes** * Added validation to identify missing redirects and invalid redirect destinations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b405bde commit 056c83c

5 files changed

Lines changed: 284 additions & 7 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
name: Deploy Documentation
1+
name: Deploy redirects
2+
3+
# This site moved to https://commit-check.com. What this repository publishes
4+
# is a set of redirect stubs, built by scripts/build_redirects.py rather than
5+
# by mkdocs.
6+
#
7+
# The deploy matters more than usual: once this repository is archived, Actions
8+
# stop running and Pages keeps serving the last artifact indefinitely. The
9+
# redirect map is therefore checked on every run — a URL left out is a link
10+
# that breaks permanently.
11+
212
on:
313
push:
414
branches: [main]
@@ -19,10 +29,20 @@ jobs:
1929
with:
2030
python-version: '3.x'
2131

22-
- name: Install Dependencies
23-
run: pipx run nox -s docs
32+
# mkdocs is still needed: the check builds the old site and compares the
33+
# URLs it produces against the redirect map.
34+
- name: Install dependencies
35+
run: |
36+
python -m pip install --upgrade pip
37+
python -m pip install pytest -r docs/requirements.txt
38+
39+
- name: Check every old URL still has a forwarding address
40+
run: pytest tests/ -q
41+
42+
- name: Build redirects
43+
run: python scripts/build_redirects.py
2444

25-
- name: Upload docs build as artifact
45+
- name: Upload redirects as artifact
2646
uses: actions/upload-pages-artifact@v5
2747
with:
2848
name: ${{ github.event.repository.name }}_docs

README.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,47 @@
1-
# commit-check.github.io
1+
# commit-check.github.io — moved to commit-check.com
22

3-
[![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)
3+
> [!IMPORTANT]
4+
> **This repository is no longer the source of the Commit Check website.**
5+
>
6+
> The documentation, the landing page and the blog now live at
7+
> **[commit-check.com](https://commit-check.com)**, built from
8+
> **[commit-check/commit-check.com](https://github.com/commit-check/commit-check.com)**.
9+
>
10+
> Open issues and pull requests about the website there, not here.
411
5-
This repository contains the source code for the [commit-check.github.io](https://commit-check.github.io) website.
12+
[![Website](https://img.shields.io/static/v1?label=Website&message=commit-check.com&color=2c9ccd&logo=git&logoColor=white)](https://commit-check.com)
13+
14+
## What this repository is now
15+
16+
It is kept as the historical source of the old site. The `docs/` directory is
17+
still here and still readable, but it is no longer published.
18+
19+
What `commit-check.github.io` serves is a set of redirect stubs — one for every
20+
URL the old site had — pointing at the page that replaced it. The pages, the
21+
blog and its archive, author and category indexes all kept their paths, so the
22+
redirects are one-to-one; `/projects/` is the exception, having been folded into
23+
the Ecosystem section of the new landing page.
24+
25+
| Old URL | Now |
26+
|---|---|
27+
| `commit-check.github.io/` | [commit-check.com/](https://commit-check.com/) |
28+
| `commit-check.github.io/getting-started/` | [commit-check.com/getting-started/](https://commit-check.com/getting-started/) |
29+
| `commit-check.github.io/blog/…` | [commit-check.com/blog/…](https://commit-check.com/blog/) — same paths |
30+
| `commit-check.github.io/projects/` | [commit-check.com/](https://commit-check.com/) |
31+
32+
The redirects are generated by `scripts/build_redirects.py`, and `tests/`
33+
verifies the map still covers every URL the old site served. GitHub Pages has
34+
no server-side redirect table, so each stub is a `rel=canonical` plus a
35+
`<meta refresh>` — the canonical is what moves search ranking to the new URL.
36+
37+
Two hosts serve this repository, so both are covered: GitHub Pages gets the
38+
stubs, and `netlify.toml` gives the Netlify project real 301s and stops it
39+
building the old site from a command configured in its web UI.
40+
41+
## Why the redirects come before the archive
42+
43+
Archiving a repository makes it read-only and stops its Actions from running,
44+
but it does **not** unpublish its GitHub Pages site: whatever was deployed last
45+
keeps being served. So the redirects have to be deployed *before* the archive
46+
switch is flipped. Afterwards there is no way to change what this domain serves
47+
without unarchiving first.

netlify.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# This repository is also wired to a Netlify project, which until now built the
2+
# mkdocs site from a command configured in Netlify's web UI. That command is
3+
# invisible from here and outlives any change to the GitHub Actions workflow,
4+
# so without this file Netlify would keep publishing the old site after the
5+
# GitHub Pages deploy had been switched to redirects — and the deploy preview
6+
# on a pull request would show the old site too, which makes the redirects look
7+
# broken when they are not.
8+
#
9+
# Settings here override the UI, so the build is defined in version control.
10+
11+
[build]
12+
command = "python scripts/build_redirects.py"
13+
publish = "site"
14+
15+
# Netlify, unlike GitHub Pages, can serve a real 301. These take precedence
16+
# over the stub files the build writes, which stay as the fallback for hosts
17+
# without a redirect table.
18+
#
19+
# Order matters: Netlify takes the first rule that matches, so the one page
20+
# that did not survive as its own URL has to come before the catch-all.
21+
[[redirects]]
22+
from = "/projects/*"
23+
to = "https://commit-check.com/"
24+
status = 301
25+
force = true
26+
27+
# Everything else kept its path on the new site, so the tail is carried across
28+
# unchanged. This also covers URLs the stub list does not enumerate.
29+
[[redirects]]
30+
from = "/*"
31+
to = "https://commit-check.com/:splat"
32+
status = 301
33+
force = true

scripts/build_redirects.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Build the redirect-only site this repository now publishes.
2+
3+
The site moved to https://commit-check.com. This repository is kept as the
4+
historical source — ``docs/`` is still here and still readable — but what it
5+
publishes is a set of redirect stubs, one per URL the old site served, so links
6+
already out in the world land on the page that replaced them.
7+
8+
Why stubs rather than a server redirect: GitHub Pages serves static files and
9+
has no redirect table, so a ``<meta refresh>`` plus a ``rel=canonical`` is the
10+
only mechanism available. The canonical link is what transfers search ranking
11+
to the new URL; the meta refresh and the script are what move a reader.
12+
13+
Deliberately no ``robots: noindex`` on these stubs. It reads like the tidy thing
14+
to do, but it contradicts the canonical: one says "consolidate this page onto
15+
that URL", the other says "drop this page from the index", and a crawler that
16+
honours the second may never act on the first — or carry the noindex across to
17+
the target. A migration wants the canonical to be believed.
18+
19+
Why this runs instead of ``mkdocs build``: once this repository is archived,
20+
Actions stop running and the last deployed artifact is what Pages serves
21+
forever. That artifact needs to be the redirects, so the redirects have to be
22+
deployed *before* the archive switch is flipped, not after.
23+
24+
Run with ``python scripts/build_redirects.py`` — output goes to ``site/``.
25+
"""
26+
27+
from __future__ import annotations
28+
29+
import sys
30+
from pathlib import Path
31+
32+
NEW_SITE = "https://commit-check.com"
33+
34+
#: Every URL the mkdocs site served, taken from its own build output, mapped to
35+
#: the path that replaced it on the new site.
36+
#
37+
# All but one are the same path: the pages, the blog, its archive, author and
38+
# category indexes were carried over unchanged, and the posts kept their
39+
# filenames and ``created`` dates, so the generated slugs match byte for byte.
40+
#
41+
# ``/projects/`` is the exception. It was folded into the Ecosystem section of
42+
# the new landing page, so it redirects to the root rather than to a page that
43+
# does not exist.
44+
SAME_PATH = [
45+
"/",
46+
"/getting-started/",
47+
"/blog/",
48+
"/blog/2026/06/21/ai-native-json-output-and-a-python-api/",
49+
"/blog/2026/06/21/from-zero-config-to-org-wide-policy/",
50+
"/blog/2026/06/21/one-policy-file-for-your-git-history/",
51+
"/blog/2026/07/06/ai-attribution-governance-enforcing-ai-disclosure-policies-at-the-ci-level/",
52+
"/blog/archive/2026/",
53+
"/blog/author/team/",
54+
"/blog/category/announcements/",
55+
"/blog/category/updates/",
56+
]
57+
58+
REDIRECTS = {path: path for path in SAME_PATH} | {"/projects/": "/"}
59+
60+
# The fragment is carried across by the script: a reader following a deep link
61+
# into a page should keep their place. ``location.replace`` rather than
62+
# ``location.href`` so the stub does not land in the back-button history and
63+
# trap them in a loop between the two sites.
64+
TEMPLATE = """<!doctype html>
65+
<html lang="en">
66+
<head>
67+
<meta charset="utf-8">
68+
<meta name="viewport" content="width=device-width, initial-scale=1">
69+
<title>Moved to commit-check.com</title>
70+
<link rel="canonical" href="{target}">
71+
<meta http-equiv="refresh" content="0; url={target}">
72+
<script>location.replace("{target}" + location.hash);</script>
73+
<style>
74+
body {{ font-family: system-ui, sans-serif; margin: 4rem auto; max-width: 34rem;
75+
padding: 0 1rem; line-height: 1.6; }}
76+
a {{ color: #2c9ccd; }}
77+
</style>
78+
</head>
79+
<body>
80+
<h1>This site has moved</h1>
81+
<p>The Commit Check documentation, landing page and blog are now published at
82+
<a href="{target}">{target}</a>.</p>
83+
<p>If you are not redirected automatically, follow the link above.</p>
84+
</body>
85+
</html>
86+
"""
87+
88+
89+
def main() -> int:
90+
site = Path(__file__).resolve().parent.parent / "site"
91+
for old, new in REDIRECTS.items():
92+
target = NEW_SITE + new
93+
page = site / old.strip("/") / "index.html"
94+
page.parent.mkdir(parents=True, exist_ok=True)
95+
page.write_text(TEMPLATE.format(target=target), encoding="utf-8")
96+
97+
# Pages serves this for any path with no file of its own, which covers the
98+
# URLs this list missed — a stray deep link, a page from an older layout.
99+
# It points at the new site's root because there is nothing better to guess.
100+
(site / "404.html").write_text(
101+
TEMPLATE.format(target=NEW_SITE + "/"), encoding="utf-8"
102+
)
103+
104+
# Without this, Pages runs the output through Jekyll, which skips files and
105+
# directories whose names begin with an underscore.
106+
(site / ".nojekyll").write_text("", encoding="utf-8")
107+
108+
print(f"wrote {len(REDIRECTS)} redirects + 404 fallback to {site}")
109+
return 0
110+
111+
112+
if __name__ == "__main__":
113+
sys.exit(main())

tests/redirects_test.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Check the redirect map against the site it is replacing.
2+
3+
This repository publishes redirects now, and once it is archived the artifact
4+
from the last successful run is what GitHub Pages serves for good — Actions do
5+
not run on an archived repository, so there is no second chance to fix a URL
6+
that was left out. That makes the map worth checking while it can still change.
7+
8+
The check builds the mkdocs site the old way and compares the URLs it produces
9+
against the redirect map, in both directions: a URL the old site served with no
10+
redirect is a link that will break, and a redirect for a URL the old site never
11+
served is a sign the map was edited by hand and drifted.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import subprocess
17+
import sys
18+
import tempfile
19+
from pathlib import Path
20+
21+
import pytest
22+
23+
ROOT = Path(__file__).resolve().parent.parent
24+
sys.path.insert(0, str(ROOT / "scripts"))
25+
26+
from build_redirects import REDIRECTS # noqa: E402
27+
28+
29+
def _urls_the_old_site_served() -> set[str]:
30+
"""Build the mkdocs site and return every URL it publishes."""
31+
with tempfile.TemporaryDirectory() as tmp:
32+
result = subprocess.run(
33+
[sys.executable, "-m", "mkdocs", "build", "--site-dir", tmp],
34+
cwd=ROOT,
35+
capture_output=True,
36+
text=True,
37+
)
38+
if result.returncode != 0:
39+
# Not a skip. A skip exits pytest 0, the workflow reads that as a
40+
# pass and deploys redirects that were never checked against the
41+
# site they replace — on the one deploy that cannot be redone.
42+
pytest.fail(f"mkdocs build failed: {result.stderr.strip()[:200]}")
43+
site = Path(tmp)
44+
urls = set()
45+
for page in site.rglob("index.html"):
46+
rel = page.parent.relative_to(site).as_posix()
47+
# ``relative_to`` gives "." for the site root, which is "/".
48+
urls.add("/" if rel == "." else f"/{rel}/")
49+
return urls
50+
51+
52+
def test_every_published_url_has_a_redirect():
53+
"""Nothing the old site served may be left without a forwarding address."""
54+
missing = sorted(_urls_the_old_site_served() - set(REDIRECTS))
55+
assert not missing, (
56+
"these URLs are served by the current site but have no redirect, so "
57+
"they will break when this repository is archived:\n "
58+
+ "\n ".join(missing)
59+
)
60+
61+
62+
def test_no_redirect_points_at_a_url_that_never_existed():
63+
"""A redirect for a URL the site never served means the map drifted."""
64+
served = _urls_the_old_site_served()
65+
invented = sorted(set(REDIRECTS) - served)
66+
assert not invented, (
67+
"these redirects are for URLs the site does not serve:\n "
68+
+ "\n ".join(invented)
69+
)

0 commit comments

Comments
 (0)