diff --git a/.github/actions/deploy-to-gh-pages/action.yml b/.github/actions/deploy-to-gh-pages/action.yml new file mode 100644 index 0000000..b59bc99 --- /dev/null +++ b/.github/actions/deploy-to-gh-pages/action.yml @@ -0,0 +1,34 @@ +name: Deploy to gh-pages +description: Deploy built docs to a subdirectory on gh-pages + +inputs: + target: + description: Subdirectory to deploy to + required: true + commit-message: + description: Commit message + required: true + +runs: + using: composite + steps: + - shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + TARGET="${{ inputs.target }}" + git fetch origin gh-pages 2>/dev/null || true + if git rev-parse --verify origin/gh-pages >/dev/null 2>&1; then + git checkout -B gh-pages origin/gh-pages + else + git checkout --orphan gh-pages + git rm -rf . + git clean -fd + fi + touch .nojekyll + rm -rf "$TARGET" + mkdir -p "$(dirname "$TARGET")" + cp -r /tmp/built-site "$TARGET" + git add .nojekyll "$TARGET" + git diff --cached --quiet || git commit -m "${{ inputs.commit-message }}" + git push origin gh-pages diff --git a/.github/workflows/docs-pr-cleanup.yml b/.github/workflows/docs-pr-cleanup.yml new file mode 100644 index 0000000..1dc43c7 --- /dev/null +++ b/.github/workflows/docs-pr-cleanup.yml @@ -0,0 +1,36 @@ +name: Documentation (PR Cleanup) + +on: + pull_request: + types: [closed] + +permissions: + contents: write + +concurrency: + group: docs-deploy + cancel-in-progress: false + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Remove PR preview + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git fetch origin gh-pages 2>/dev/null || true + if ! git rev-parse --verify origin/gh-pages >/dev/null 2>&1; then + echo "gh-pages branch does not exist, nothing to clean up" + exit 0 + fi + git checkout -B gh-pages origin/gh-pages + if [ ! -d "pr/${{ github.event.number }}" ]; then + echo "No preview directory found for PR #${{ github.event.number }}" + exit 0 + fi + git rm -rf "pr/${{ github.event.number }}" + git commit -m "Remove PR #${{ github.event.number }} preview" + git push origin gh-pages diff --git a/.github/workflows/docs-pr-preview.yml b/.github/workflows/docs-pr-preview.yml new file mode 100644 index 0000000..3afb6fd --- /dev/null +++ b/.github/workflows/docs-pr-preview.yml @@ -0,0 +1,68 @@ +name: Documentation (PR Preview) + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + +concurrency: + group: docs-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: astral-sh/setup-uv@v7 + + - name: Build docs + run: | + uv run --group docs python scripts/override_site_url.py "pr/${{ github.event.number }}" + uv run --group docs zensical build --clean --config-file zensical.local.toml + cp -r site /tmp/built-site + + - name: Get preview URL + id: preview + run: | + url=$(uv run --group docs python -c "import tomlkit; print(tomlkit.loads(open('zensical.local.toml').read()).get('project', {}).get('site_url', ''))") + echo "url=${url}" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/deploy-to-gh-pages + with: + target: pr/${{ github.event.number }} + commit-message: Deploy PR #${{ github.event.number }} preview + + - name: Post or update PR comment + if: steps.preview.outputs.url != '' + uses: actions/github-script@v7 + with: + script: | + const marker = ''; + const url = '${{ steps.preview.outputs.url }}'; + const body = `${marker}\nšŸ“– Docs preview: ${url}`; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ github.event.number }}, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ github.event.number }}, + body, + }); + } diff --git a/.github/workflows/docs-release.yml b/.github/workflows/docs-release.yml new file mode 100644 index 0000000..37ff5f9 --- /dev/null +++ b/.github/workflows/docs-release.yml @@ -0,0 +1,60 @@ +name: Documentation (Release) + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +concurrency: + group: docs-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: astral-sh/setup-uv@v7 + + - name: Extract version + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Build docs + run: | + uv run --group docs python scripts/override_site_url.py "${{ steps.version.outputs.version }}" + uv run --group docs zensical build --clean --config-file zensical.local.toml + cp -r site /tmp/built-site + + - uses: ./.github/actions/deploy-to-gh-pages + with: + target: ${{ steps.version.outputs.version }} + commit-message: Deploy docs for v${{ steps.version.outputs.version }} + + - name: Update latest and index.html + run: | + TARGET="${{ steps.version.outputs.version }}" + rm -rf latest + cp -r /tmp/built-site latest + # Generate root index.html + REPO_NAME="${{ github.event.repository.name }}" + { + echo '' + echo "${REPO_NAME}" + echo "

${REPO_NAME} documentation

' + } > index.html + git add latest index.html + git diff --cached --quiet || git commit -m "Update latest and index.html for v${TARGET}" + git push origin gh-pages diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c0b4185..0ea5444 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,25 +1,32 @@ name: Documentation + on: push: branches: - main + permissions: - contents: read - pages: write - id-token: write + contents: write + +concurrency: + group: docs-deploy + cancel-in-progress: false + jobs: deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - - uses: actions/configure-pages@v5 - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v7 - - run: uv run --group docs zensical build --clean - - uses: actions/upload-pages-artifact@v4 + + - name: Build docs + run: | + uv run --group docs python scripts/override_site_url.py dev + uv run --group docs zensical build --clean --config-file zensical.local.toml + cp -r site /tmp/built-site + + - uses: ./.github/actions/deploy-to-gh-pages with: - path: site - - uses: actions/deploy-pages@v4 - id: deployment + target: dev + commit-message: Deploy dev docs diff --git a/pyproject.toml b/pyproject.toml index acbd79e..51c23a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,4 +19,4 @@ tag_sign = false check_dirty = false [dependency-groups] -docs = ["mkdocstrings-python", "zensical"] +docs = ["mkdocstrings-python", "zensical", "tomlkit"] diff --git a/scripts/override_site_url.py b/scripts/override_site_url.py new file mode 100644 index 0000000..fb29fa3 --- /dev/null +++ b/scripts/override_site_url.py @@ -0,0 +1,20 @@ +'''Override site_url in zensical.toml for subdirectory deployment.''' +import sys +from pathlib import Path +import tomlkit + +INPUT_PATH = Path('zensical.toml') +OUTPUT_PATH = Path('zensical.local.toml') + + +def main(): + config = tomlkit.loads(INPUT_PATH.read_text()) + url = config.get('project', {}).get('site_url') + if url: + subdir = sys.argv[1] + new_url = url.rstrip('/') + '/' + subdir.strip('/') + '/' + config['project']['site_url'] = new_url + OUTPUT_PATH.write_text(tomlkit.dumps(config)) + + +main()