diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0eb3c9e..fa08245 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,3 +98,11 @@ jobs: uses: ./.github/workflows/dockerhub.yml secrets: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + + unraid-changes: + needs: release + if: ${{ !contains(github.ref_name, '-') }} + uses: ./.github/workflows/unraid-changes.yml + secrets: + RELEASE_APP_CLIENT_ID: ${{ secrets.RELEASE_APP_CLIENT_ID }} + RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} diff --git a/.github/workflows/unraid-changes.yml b/.github/workflows/unraid-changes.yml new file mode 100644 index 0000000..4635a3f --- /dev/null +++ b/.github/workflows/unraid-changes.yml @@ -0,0 +1,71 @@ +name: Unraid changes + +on: + workflow_dispatch: + workflow_call: + secrets: + RELEASE_APP_CLIENT_ID: + required: true + RELEASE_APP_PRIVATE_KEY: + required: true + +permissions: + contents: read + +jobs: + update: + runs-on: ubuntu-24.04 + steps: + # A pull request opened with GITHUB_TOKEN starts no checks, and main requires them. + - id: app + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + persist-credentials: false + - run: python3 tools/unraid_template.py + - name: Open a pull request that merges when its checks pass + env: + GH_TOKEN: ${{ steps.app.outputs.token }} + run: | + set -euo pipefail + if git diff --quiet; then + echo "The Unraid template already has the newest release notes." + exit 0 + fi + version=$(sed -n 's/^version = "\(.*\)"$/\1/p' pyproject.toml) + branch="docs/unraid-changes-$version" + title="Docs: $version release notes in the Unraid template" + if [ -n "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then + echo "A pull request for $branch is already open." + exit 0 + fi + git config user.name SandObserver + git config user.email 260779319+SandObserver@users.noreply.github.com + git switch -c "$branch" + git commit -qam "$title" + git push --force "https://x-access-token:$GH_TOKEN@github.com/$GITHUB_REPOSITORY.git" "HEAD:$branch" + body=$(sed "s/VERSION/$version/" <<'BODY' + **Summary** + + - Copy the VERSION section of CHANGELOG.md into `` in `templates/posteryard.xml`. + + **Testing** + + Written by `tools/unraid_template.py`. CI runs the full checks. + + **Checklist** + - [x] Format, lint, type checks and tests pass. + - [ ] `DESIGN_VERSION` bumped and `tests/golden` written again if rendered images change. + - [ ] `CHANGELOG.md` updated under `Unreleased` for user-visible changes. + BODY + ) + url=$(gh pr create --base main --head "$branch" --title "$title" --label documentation --body "$body") + number=${url##*/} + gh pr merge "$url" --auto --squash --delete-branch --subject "$title (#$number)" \ + || echo "::notice::Auto-merge is off for this repository. Merge $url by hand." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d246510..f0ba140 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,7 +92,7 @@ UPDATE_GOLDEN=1 uv run pytest tests/test_golden.py A change to the database schema adds a new entry at the end of `MIGRATIONS` in `src/posteryard/store.py`. Never edit an entry that was released. -A new setting also goes into the Unraid template, `templates/posteryard.xml`, and into `site/settings.json`. The template takes each setting's description and default from its row in the `README.md` Settings table, and its `Changes` from the newest dated `CHANGELOG.md` section. Add the field with its name, display and mask, then run `uv run python tools/unraid_template.py`. `tests/test_unraid.py` fails when the template is out of date, a template default does not load, or a template setting has no README row. +A new setting also goes into the Unraid template, `templates/posteryard.xml`, and into `site/settings.json`. The template takes each setting's description and default from its row in the `README.md` Settings table. Add the field with its name, display and mask, then run `uv run python tools/unraid_template.py`. `tests/test_unraid.py` fails when the settings are out of date, a template default does not load, or a template setting has no README row. ## Website @@ -118,4 +118,4 @@ The Plex preview shows either 12 random movies or 12 random shows from `site/pos ## Releases -Semantic versioning. Move the `Unreleased` entries in [CHANGELOG.md](CHANGELOG.md) under the new version with its date, set `version` in `pyproject.toml`, run `uv run python tools/unraid_template.py` to copy the notes into the Unraid template, merge, and push a `vX.Y.Z` tag. The tag runs the CI checks, then pushes `X.Y.Z`, `X.Y` and `latest` to `ghcr.io/sandobserver/posteryard` and `docker.io/sandobserver/posteryard`, publishes a GitHub release with the version's CHANGELOG section, and rewrites the Docker Hub page from `README.md` with `tools/dockerhub_description.py`. Docker Hub needs the repository secret `DOCKERHUB_TOKEN`, an access token with Read, Write and Delete. Docker Hub refuses to move an `X.Y.Z` tag, so a version cannot be published twice. The `Docker Hub page` workflow updates the page by hand. The release stops if the tag, `pyproject.toml` and a dated CHANGELOG section do not agree. The image carries an SBOM and build provenance. A tag with a suffix, such as `v1.0.0-rc.1`, publishes a prerelease: only the `1.0.0-rc.1` image tag, and a GitHub release marked as prerelease. +Semantic versioning. Move the `Unreleased` entries in [CHANGELOG.md](CHANGELOG.md) under the new version with its date, set `version` in `pyproject.toml`, merge, and push a `vX.Y.Z` tag. The tag runs the CI checks, then pushes `X.Y.Z`, `X.Y` and `latest` to `ghcr.io/sandobserver/posteryard` and `docker.io/sandobserver/posteryard`, publishes a GitHub release with the version's CHANGELOG section, and rewrites the Docker Hub page from `README.md` with `tools/dockerhub_description.py`, and opens a pull request that copies the release notes into the Unraid template and merges when its checks pass. That pull request needs the release GitHub App, through the secrets `RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY`. Docker Hub needs the repository secret `DOCKERHUB_TOKEN`, an access token with Read, Write and Delete. Docker Hub refuses to move an `X.Y.Z` tag, so a version cannot be published twice. The `Docker Hub page` workflow updates the page by hand. The release stops if the tag, `pyproject.toml` and a dated CHANGELOG section do not agree. The image carries an SBOM and build provenance. A tag with a suffix, such as `v1.0.0-rc.1`, publishes a prerelease: only the `1.0.0-rc.1` image tag, and a GitHub release marked as prerelease. diff --git a/tests/test_unraid.py b/tests/test_unraid.py index b962586..55db200 100644 --- a/tests/test_unraid.py +++ b/tests/test_unraid.py @@ -75,14 +75,25 @@ def test_the_template_images_are_in_the_repository() -> None: assert (TEMPLATE.findtext("Icon") or "").endswith(".png") -def test_the_template_matches_the_readme_settings_and_the_newest_release() -> None: +def test_the_template_matches_the_readme_settings() -> None: template = (ROOT / "templates" / "posteryard.xml").read_text() readme = (ROOT / "README.md").read_text() - changelog = (ROOT / "CHANGELOG.md").read_text() - assert TOOL.render(template, readme, changelog) == template, "Run: uv run python tools/unraid_template.py" + assert TOOL.render(template, readme, None) == template, "Run: uv run python tools/unraid_template.py" assert TOOL.main(["--check"]) == 0 +def test_the_template_changes_are_a_released_version() -> None: + changes = (TEMPLATE.findtext("Changes") or "").strip() + heading = re.match(r"### (\S+) - (\d{4}-\d{2}-\d{2})\n", changes) + assert heading + assert f"## [{heading.group(1)}] - {heading.group(2)}" in (ROOT / "CHANGELOG.md").read_text() + + +def test_settings_only_keeps_the_changes() -> None: + template = "kept" + assert TOOL.render(template, "\n## Settings\n\n## Next\n", None) == template + + def test_a_readme_line_becomes_plain_text_without_links_to_other_pages() -> None: line = "Use **Apple** art from [TMDB](https://example.com) for `true`. See [Collections](docs/x.md#c). Next." assert TOOL.plain(line) == "Use Apple art from TMDB for true. Next." diff --git a/tools/unraid_template.py b/tools/unraid_template.py index f2b4c38..7afb39e 100644 --- a/tools/unraid_template.py +++ b/tools/unraid_template.py @@ -3,8 +3,8 @@ Each Unraid variable gets its description and default from its row in the README Settings table. The Changes element gets the newest dated CHANGELOG section, never Unreleased. -Run after a README settings change and in every release PR: uv run python tools/unraid_template.py ---check exits 1 when the template is out of date. +Run after a README settings change: uv run python tools/unraid_template.py +The Unraid changes workflow runs it after each release. --check exits 1 when the settings are out of date. """ import re @@ -52,7 +52,7 @@ def newest_release(changelog: str) -> str: return f"### {heading.group(1)} - {heading.group(2)}\n\n" + "\n".join(lines).strip() -def render(template: str, readme: str, changelog: str) -> str: +def render(template: str, readme: str, changelog: str | None) -> str: settings = readme_settings(readme) def config(match: re.Match[str]) -> str: @@ -65,19 +65,21 @@ def config(match: re.Match[str]) -> str: return re.sub(r">[^<]*$", lambda _: f">{escape(default)}", line) template = VARIABLE.sub(config, template) + if changelog is None: + return template changes = f"\n{escape(newest_release(changelog))}\n " return re.sub(r".*?", lambda _: changes, template, count=1, flags=re.S) def main(argv: list[str]) -> int: current = TEMPLATE.read_text() - wanted = render(current, (ROOT / "README.md").read_text(), (ROOT / "CHANGELOG.md").read_text()) + readme = (ROOT / "README.md").read_text() if "--check" in argv: - if current != wanted: + if render(current, readme, None) != current: print("templates/posteryard.xml is out of date. Run: uv run python tools/unraid_template.py") return 1 return 0 - TEMPLATE.write_text(wanted) + TEMPLATE.write_text(render(current, readme, (ROOT / "CHANGELOG.md").read_text())) return 0