This repository was archived by the owner on Aug 4, 2026. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 0
docs: publish redirects to commit-check.com ahead of archiving #14
Merged
+284
−7
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
| [](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. | ||
| [](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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| <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()) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| ) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.