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..7c74feb 100644 --- a/README.md +++ b/README.md @@ -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 +`` — 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. 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 diff --git a/scripts/build_redirects.py b/scripts/build_redirects.py new file mode 100644 index 0000000..e9993d8 --- /dev/null +++ b/scripts/build_redirects.py @@ -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 ```` 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 = """ + + + + +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..368e87b --- /dev/null +++ b/tests/redirects_test.py @@ -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) + )