Skip to content
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
4 changes: 4 additions & 0 deletions .github/release-notes.md.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## Release resources

- [Release website](https://mboworks.github.io/mbo/site/tag/@TAG@/)
- [Coverage report](https://mboworks.github.io/mbo/coverage/tag/@VERSION@/)
8 changes: 8 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ jobs:
with:
python-version: "3.13"
- run: python3 -m unittest discover -s tools -p release_site_test.py
- run: python3 -m unittest discover -s tools -p release_notes_test.py
- name: Verify configured documentation and generated links
env:
GH_TOKEN: ${{ github.token }}
run: |
# This is a disposable build on the runner, never a Pages publication.
python3 tools/release_site.py . "${RUNNER_TEMP}/release-site-check" \
--repository "${GITHUB_REPOSITORY}" --tag 0.0.0-verification --latest ""

trunk:
runs-on: ubuntu-latest
Expand Down
20 changes: 19 additions & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ on:
type: string
required: false

config_path:
description: Optional config file on main for backfill (empty uses the tag config)
type: string
required: false

permissions: {}

# Share the lock and retained branch with coverage: every deployment includes both.
Expand Down Expand Up @@ -92,9 +97,22 @@ jobs:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
LATEST_TAG: ${{ steps.release.outputs.latest }}
CONFIG_PATH: ${{ inputs.config_path }}
run: |
set -euo pipefail
config_args=()
if [[ -n "${CONFIG_PATH}" ]]; then
config_file="$(realpath --canonicalize-existing "source/${CONFIG_PATH}")"
if [[ "${config_file}" != "${GITHUB_WORKSPACE}/source/"* ]]; then
echo "Configuration must be a tracked file inside the main checkout" >&2
exit 1
fi
git -C source ls-files --error-unmatch -- "${CONFIG_PATH}" >/dev/null
config_args=(--config "${config_file}")
fi
python3 source/tools/release_site.py release site \
--repository "${GITHUB_REPOSITORY}" --tag "${RELEASE_TAG}" --latest "${LATEST_TAG}"
--repository "${GITHUB_REPOSITORY}" --tag "${RELEASE_TAG}" --latest "${LATEST_TAG}" \
"${config_args[@]}"
- name: Retain and stage complete Pages tree
env:
RELEASE_TAG: ${{ steps.release.outputs.tag }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release_prep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,6 @@ Copy [dev.MODULE.bazel](https://github.com/mboworks/${PACKAGE_NAME}/blob/main/ba
include("//:dev.MODULE.bazel")
\`\`\`
EOF

printf '\n'
bash tools/release_notes.sh "${TAG}"
54 changes: 49 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,10 @@ library's only NOTICE entry is courtesy).

## Release website

Release notes use `.github/release-notes.md.template`, rendered by
`tools/release_notes.sh TAG`, to link to that tag's versioned website and related
release resources. The existing changelog and installation notes remain included.

The [website](https://mboworks.github.io/mbo/) forwards to the latest published
stable release at `site/tag/<tag>/`, preserving the exact Git tag name.
Each release keeps its converted HTML, images, and configured files. Retrying
Expand Down Expand Up @@ -433,12 +437,18 @@ site directory. For example:

Use existing source files in the actual configuration. `pages` converts Markdown;
optional `files` copies other files unchanged. `README.md` must map to `index.html`.
The generated `documents.html`, `release.json`, and `assets/` paths are reserved.
The generated `documents.html`, `release.json`, `release-site.json`, and `assets/`
paths are reserved. Destination paths cannot have hidden components (names starting
with a dot), because the Pages artifact uploader excludes them. Hidden source
paths remain valid; for example, `.github/workflows/README.md` maps to
`workflows/index.html`.
Navigation links support `{owner}`, `{repo}`, `{tag}`, `{version}`, and `{commit}`.
`{version}` omits a leading `v` for compatibility with coverage report paths.
The configuration and content come from the release tag. Links to configured
pages follow their destination mappings; other local source links use the exact
release commit. Embedded images are copied, including remote badges. Markdown
By default, the configuration and content come from the release tag. Every linked
local Markdown page (including directory README links) must have a `pages` mapping.
Publication fails for an omitted mapping, a missing generated file, or a broken
anchor within the snapshot. Links to configured pages follow their destination
mappings; other local source links use the exact release commit. Embedded images are copied, including remote badges. Markdown
conversion uses the [GitHub Markdown API](https://docs.github.com/en/rest/markdown/markdown)
at publication time; browsing the result requires no Markdown renderer or CDN.

Expand All @@ -447,11 +457,45 @@ on `coverage-pages` and deploys the complete Pages tree. Coverage and site
publication share a concurrency group to preserve both trees. GitHub's latest
stable release selects the root redirect; backfilling an older release does not
make it latest. The workflow can also be dispatched with a published tag to retry
publication (the tag must contain `release-site.json`). Enable GitHub Pages with
publication. Enable GitHub Pages with
**GitHub Actions** as its source, and set the repository's About website to
`https://mboworks.github.io/mbo/`.

### Backfill a historical release

No new release or tag change is needed. Manually dispatch `Publish release site`
with `tag` set to the historical release and `config_path` set to a tracked JSON
file on `main`. For `0.15.0`, use the current `release-site.json`:

```sh
gh workflow run pages.yml --repo mboworks/mbo --ref main \
-f tag=0.15.0 -f config_path=release-site.json
```

The override changes only the publication layout; all Markdown and copied files
still come from the selected tag. A configuration can serve multiple historical
tags when its sources exist in each tag. For a different historical layout, add
another configuration on `main` and select its repository-relative path. Missing
sources or links fail publication instead of falling back to newer content.
Automatic release publication continues to use the configuration from the tag.

Each new snapshot retains the exact configuration as `release-site.json` and
records its SHA-256, whether it came from the tag or an override, and the source
commit in `release.json`. Retrying a published tag does not replace its HTML or
configuration, even if the selected override has changed since publication.

To verify a backfill locally without deploying, check out the historical tag in
`/tmp/mbo-0.15.0` and run from the current publisher checkout:

```sh
python3 tools/release_site.py /tmp/mbo-0.15.0 /tmp/mbo-site-preview \
--repository mboworks/mbo --tag 0.15.0 --latest 0.15.0 \
--config release-site.json
```

Local regression tests: `python3 -m unittest discover -s tools -p release_site_test.py`.
CI also converts the configured documentation and checks the generated links in
a disposable runner directory. It never commits, retains, or deploys that preview.

Release coverage links select `https://mboworks.github.io/mbo/coverage/tag/<version>/`,
matching the coverage publisher rather than the moving main-branch report.
13 changes: 12 additions & 1 deletion release-site.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@
"mbo/file/README.md": "mbo/file/README.html",
"mbo/hash/README.md": "mbo/hash/README.html",
"mbo/hash/measurements/README.md": "mbo/hash/measurements/README.html",
"mbo/types/README.md": "mbo/types/README.html"
"mbo/types/README.md": "mbo/types/README.html",
".github/workflows/README.md": "workflows/index.html",
"AGENTS.md": "AGENTS.html",
"CLAUDE.md": "CLAUDE.html",
"CODE_OF_CONDUCT.md": "CODE_OF_CONDUCT.html",
"GIT_RULES.md": "GIT_RULES.html",
"RULES.md": "RULES.html",
"STYLE_CPP.md": "STYLE_CPP.html",
"STYLE_SH.md": "STYLE_SH.html",
"TODO.md": "TODO.html",
"mbo/diff/TODO.md": "mbo/diff/TODO.html",
"mbo/types/REFLECTION.md": "mbo/types/REFLECTION.html"
},
"links": [
{
Expand Down
28 changes: 28 additions & 0 deletions tools/release_notes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash

# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Render release-note links without building, publishing, or changing repository state.
set -euo pipefail

TAG="${1:?Usage: release_notes.sh TAG}"
if [[ ! "${TAG}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid release tag: ${TAG}" >&2
exit 1
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
sed -e "s|@TAG@|${TAG}|g" -e "s|@VERSION@|${TAG#v}|g" \
"${ROOT}/.github/release-notes.md.template"
52 changes: 52 additions & 0 deletions tools/release_notes_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors
# SPDX-License-Identifier: Apache-2.0
"""Exercise the release-note renderer with the actual repository template."""

from pathlib import Path
import subprocess
import tempfile
import unittest

ROOT = Path(__file__).resolve().parents[1]
REPO = "mbo"


class ReleaseNotesTest(unittest.TestCase):
def render(self, tag):
with tempfile.TemporaryDirectory() as cwd:
return subprocess.run(
["bash", str(ROOT / "tools/release_notes.sh"), tag],
cwd=cwd, text=True, capture_output=True, check=False)

def test_release_resources(self):
for tag in ("1.2.3", "v1.2.3", "v1.2.3-rc.1"):
with self.subTest(tag=tag):
result = self.render(tag)
self.assertEqual(result.returncode, 0, result.stderr)
notes = result.stdout
base = f"https://mboworks.github.io/{REPO}"
self.assertIn(f"{base}/site/tag/{tag}/", notes)
self.assertNotIn("@TAG@", notes)
self.assertNotIn("@VERSION@", notes)
version = tag.removeprefix("v")
if REPO in ("mbo", "xff", "carve"):
self.assertIn(f"{base}/coverage/tag/{version}/", notes)
else:
self.assertNotIn("/coverage/", notes)
if REPO == "xff":
self.assertIn(f"{base}/releases/{version}/)", notes)
self.assertIn(f"{base}/releases/{version}/XFF.md", notes)
if REPO == "coderef":
self.assertIn(f"/blob/{tag}/CHANGELOG.md", notes)
self.assertIn(f"{base}/site/tag/{tag}/schema/v1.json", notes)

def test_reject_invalid_tags_without_partial_notes(self):
for tag in ("", "main", "../1.2.3", "v1.2.3|bad", "1.2.3\nmain"):
with self.subTest(tag=tag):
result = self.render(tag)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(result.stdout, "")


if __name__ == "__main__":
unittest.main()
Loading
Loading