From 4291bc7f10f50ebe9acfc1f8115d5a86d78cdc63 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 12:14:09 +0000 Subject: [PATCH 1/3] docs: publish redirects to commit-check.com ahead of archiving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The site moved to commit-check.com, but this domain still serves a full copy of the old one. Archiving alone would not change that: it makes the repository read-only and stops Actions, and does not unpublish Pages — whatever was deployed last keeps being served. Archiving as the first step would freeze a duplicate of the pre-v2.13.0 documentation online permanently, competing with the real site and showing rule names the tool no longer prints. So the redirects go up first, and the archive switch is flipped afterwards. There is no second chance: Actions do not run on an archived repository. Replaces the mkdocs build with scripts/build_redirects.py, which emits one stub per URL the old site served. The map came from that site's own build output rather than from reading the config, and 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 slugs match exactly. /projects/ is the exception, folded into the Ecosystem section of the new landing page. GitHub Pages has no redirect table, so each stub is a rel=canonical plus a meta refresh, and the script carries the fragment across so deep links keep their place. A 404.html catches anything the map missed. tests/ checks the map against a real mkdocs build in both directions, and CI runs it before the deploy: a URL served with no redirect is a link that breaks for good, and a redirect for a URL that was never served means the map drifted. The docs/ directory stays as the historical source. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn --- .github/workflows/deploy.yml | 28 +++++++-- README.md | 44 +++++++++++++- scripts/build_redirects.py | 108 +++++++++++++++++++++++++++++++++++ tests/redirects_test.py | 66 +++++++++++++++++++++ 4 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 scripts/build_redirects.py create mode 100644 tests/redirects_test.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 93f6ba3..8c0e0ca 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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] @@ -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 diff --git a/README.md b/README.md index 8ed762d..5895d1f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,43 @@ -# 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 +`` — the canonical is what moves search ranking to the new URL. + +## 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. diff --git a/scripts/build_redirects.py b/scripts/build_redirects.py new file mode 100644 index 0000000..8c66988 --- /dev/null +++ b/scripts/build_redirects.py @@ -0,0 +1,108 @@ +"""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 ```` 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. + +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 = """ + + + + +Moved to commit-check.com + + + + + + + +

This site has moved

+

The Commit Check documentation, landing page and blog are now published at +{target}.

+

If you are not redirected automatically, follow the link above.

+ + +""" + + +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()) diff --git a/tests/redirects_test.py b/tests/redirects_test.py new file mode 100644 index 0000000..92094aa --- /dev/null +++ b/tests/redirects_test.py @@ -0,0 +1,66 @@ +"""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: + pytest.skip(f"mkdocs build unavailable: {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) + ) From 5157c717893b07ade7139f8d46676f5d80a10e1b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 12:16:43 +0000 Subject: [PATCH 2/3] fix: point the Netlify project at the redirects too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PR that switched the GitHub Pages deploy to redirects missed that this repository is built by a Netlify project as well. That project has no config in the repository, so its build command lives in Netlify's web UI, where a change to .github/workflows/deploy.yml cannot reach it — it would have gone on building and publishing the old mkdocs site regardless. Two consequences, one of them visible right now: the deploy preview on this pull request is the old site, which makes the redirects look broken to anyone who clicks it, and any production deployment of that project is a third live copy of the old site that neither the Pages redirects nor archiving would touch. Adds a netlify.toml so the build is defined in version control. It runs the same redirect builder, and declares real 301s — Netlify can serve those, unlike Pages — with the /projects/ rule ahead of the catch-all, since Netlify takes the first match and that is the one page that did not survive as its own URL. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn --- README.md | 4 ++++ netlify.toml | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 netlify.toml diff --git a/README.md b/README.md index 5895d1f..7c74feb 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,10 @@ 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 `` — 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, diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..b404d62 --- /dev/null +++ b/netlify.toml @@ -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 From 6e891ff13203d2b1851c513f8672b85752d106ce Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 12:19:46 +0000 Subject: [PATCH 3/3] fix: make the redirect gate actually gate, and stop fighting the canonical MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both from CodeRabbit's review, both real. The coverage check called pytest.skip when the mkdocs build failed. A skip exits pytest 0, so the workflow would have read it as a pass and deployed redirects that had never been compared against the site they replace — on the one deploy that cannot be redone afterwards. The skip bought a quieter local run without mkdocs installed and paid for it with the entire point of the check. It fails now; verified by moving mkdocs.yml aside, which turns both tests red instead of green. The stubs also carried robots: noindex, which contradicts the rel=canonical sitting two lines above it — one asks a crawler to consolidate the page onto the new URL, the other asks it to drop the page, and the second can stop the first from being acted on or carry across to the target. The comment in this file claimed the canonical was what moved search ranking while the markup was undermining it. Removed, with a note on why it is not an oversight. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn --- scripts/build_redirects.py | 7 ++++++- tests/redirects_test.py | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/build_redirects.py b/scripts/build_redirects.py index 8c66988..e9993d8 100644 --- a/scripts/build_redirects.py +++ b/scripts/build_redirects.py @@ -10,6 +10,12 @@ 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 @@ -62,7 +68,6 @@ Moved to commit-check.com -