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
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
71 changes: 71 additions & 0 deletions .github/workflows/unraid-changes.yml
Original file line number Diff line number Diff line change
@@ -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 `<Changes>` 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."
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
17 changes: 14 additions & 3 deletions tests/test_unraid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<Container><Changes>kept</Changes></Container>"
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."
Expand Down
14 changes: 8 additions & 6 deletions tools/unraid_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -65,19 +65,21 @@ def config(match: re.Match[str]) -> str:
return re.sub(r">[^<]*</Config>$", lambda _: f">{escape(default)}</Config>", line)

template = VARIABLE.sub(config, template)
if changelog is None:
return template
changes = f"<Changes>\n{escape(newest_release(changelog))}\n </Changes>"
return re.sub(r"<Changes>.*?</Changes>", 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


Expand Down