diff --git a/.github/workflows/docs-gen-and-push.yaml b/.github/workflows/docs-gen-and-push.yaml new file mode 100644 index 0000000..d83076d --- /dev/null +++ b/.github/workflows/docs-gen-and-push.yaml @@ -0,0 +1,45 @@ +name: Generate and push docs + +on: + # So we can trigger manually if needed + workflow_dispatch: + # To confirm any changes to docs build successfully, without deploying them + pull_request: + # Pushes to branches do the full build + deployment + push: + branches: + - main + - "release-*" + paths: + - "cmd/**" + - "docs/**" + - "pkg/**" + - ".github/workflows/docs-gen-and-push.yaml" + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }} + +jobs: + generate-and-push: + name: Generate and push docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2 + + - run: git fetch origin gh-pages + - run: git fetch origin '+refs/tags/v*:refs/tags/v*' --no-tags + + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # tag=v5.4.0 + with: + go-version: v1.23.7 + cache: true + + - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 #tag=v5.5.0 + with: + python-version: '3.10' + cache: 'pip' + + - run: make deploy-docs diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml deleted file mode 100644 index 022fc26..0000000 --- a/.github/workflows/gh-pages.yaml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build and publish mkdocs - -on: - push: - branches: - - main - -permissions: - contents: write - -jobs: - - deploy: - - runs-on: ubuntu-latest - - steps: - - - uses: actions/checkout@v4 - - - name: Configure Git Credentials - run: | - git config user.name kcp-ci-bot - git config user.email no-reply@kcp.io - - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - cache: 'pip' - - - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - - - uses: actions/cache@v4 - with: - key: mkdocs-material-${{ env.cache_id }} - path: .cache - restore-keys: | - mkdocs-material- - - - run: cd docs && pip install -r requirements.txt - - - run: cd docs && mkdocs gh-deploy --force \ No newline at end of file diff --git a/Makefile b/Makefile index 1b0324f..43ae379 100644 --- a/Makefile +++ b/Makefile @@ -144,3 +144,26 @@ imports: $(GIMPS) verify: ./hack/verify-boilerplate.sh ./hack/verify-licenses.sh + + +### docs + +VENVDIR=$(abspath docs/venv) +REQUIREMENTS_TXT=docs/requirements.txt + +.PHONY: local-docs +local-docs: venv ## Run mkdocs serve + . $(VENV)/activate; \ + VENV=$(VENV) cd docs && mkdocs serve + +.PHONY: serve-docs +serve-docs: venv ## Serve docs + . $(VENV)/activate; \ + VENV=$(VENV) REMOTE=$(REMOTE) BRANCH=$(BRANCH) docs/scripts/serve-docs.sh + +.PHONY: deploy-docs +deploy-docs: venv ## Deploy docs + . $(VENV)/activate; \ + REMOTE=$(REMOTE) BRANCH=$(BRANCH) docs/scripts/deploy-docs.sh + +include Makefile.venv diff --git a/Makefile.venv b/Makefile.venv new file mode 100644 index 0000000..c79b9bb --- /dev/null +++ b/Makefile.venv @@ -0,0 +1,274 @@ +# +# SEAMLESSLY MANAGE PYTHON VIRTUAL ENVIRONMENT WITH A MAKEFILE +# +# https://github.com/sio/Makefile.venv v2022.07.20 +# +# +# Insert `include Makefile.venv` at the bottom of your Makefile to enable these +# rules. +# +# When writing your Makefile use '$(VENV)/python' to refer to the Python +# interpreter within virtual environment and '$(VENV)/executablename' for any +# other executable in venv. +# +# This Makefile provides the following targets: +# venv +# Use this as a dependency for any target that requires virtual +# environment to be created and configured +# python, ipython +# Use these to launch interactive Python shell within virtual environment +# shell, bash, zsh +# Launch interactive command line shell. "shell" target launches the +# default shell Makefile executes its rules in (usually /bin/sh). +# "bash" and "zsh" can be used to refer to the specific desired shell. +# show-venv +# Show versions of Python and pip, and the path to the virtual environment +# clean-venv +# Remove virtual environment +# $(VENV)/executable_name +# Install `executable_name` with pip. Only packages with names matching +# the name of the corresponding executable are supported. +# Use this as a lightweight mechanism for development dependencies +# tracking. E.g. for one-off tools that are not required in every +# developer's environment, therefore are not included into +# requirements.txt or setup.py. +# Note: +# Rules using such target or dependency MUST be defined below +# `include` directive to make use of correct $(VENV) value. +# Example: +# codestyle: $(VENV)/pyflakes +# $(VENV)/pyflakes . +# See `ipython` target below for another example. +# +# This Makefile can be configured via following variables: +# PY +# Command name for system Python interpreter. It is used only initially to +# create the virtual environment +# Default: python3 +# REQUIREMENTS_TXT +# Space separated list of paths to requirements.txt files. +# Paths are resolved relative to current working directory. +# Default: requirements.txt +# +# Non-existent files are treated as hard dependencies, +# recipes for creating such files must be provided by the main Makefile. +# Providing empty value (REQUIREMENTS_TXT=) turns off processing of +# requirements.txt even when the file exists. +# SETUP_PY +# Space separated list of paths to setup.py files. +# Corresponding packages will be installed into venv in editable mode +# along with all their dependencies +# Default: setup.py +# +# Non-existent and empty values are treated in the same way as for REQUIREMENTS_TXT. +# WORKDIR +# Parent directory for the virtual environment. +# Default: current working directory. +# VENVDIR +# Python virtual environment directory. +# Default: $(WORKDIR)/.venv +# +# This Makefile was written for GNU Make and may not work with other make +# implementations. +# +# +# Copyright (c) 2019-2020 Vitaly Potyarkin +# +# Licensed under the Apache License, Version 2.0 +# +# + + +# +# Configuration variables +# + +WORKDIR?=. +VENVDIR?=$(WORKDIR)/.venv +REQUIREMENTS_TXT?=$(wildcard requirements.txt) # Multiple paths are supported (space separated) +SETUP_PY?=$(wildcard setup.py) # Multiple paths are supported (space separated) +SETUP_CFG?=$(foreach s,$(SETUP_PY),$(wildcard $(patsubst %setup.py,%setup.cfg,$(s)))) +MARKER=.initialized-with-Makefile.venv + + +# +# Python interpreter detection +# + +_PY_AUTODETECT_MSG=Detected Python interpreter: $(PY). Use PY environment variable to override + +ifeq (ok,$(shell test -e /dev/null 2>&1 && echo ok)) +NULL_STDERR=2>/dev/null +else +NULL_STDERR=2>NUL +endif + +ifndef PY +_PY_OPTION:=python3 +ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) +PY=$(_PY_OPTION) +endif +endif + +ifndef PY +_PY_OPTION:=$(VENVDIR)/bin/python +ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) +PY=$(_PY_OPTION) +$(info $(_PY_AUTODETECT_MSG)) +endif +endif + +ifndef PY +_PY_OPTION:=$(subst /,\,$(VENVDIR)/Scripts/python) +ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) +PY=$(_PY_OPTION) +$(info $(_PY_AUTODETECT_MSG)) +endif +endif + +ifndef PY +_PY_OPTION:=py -3 +ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) +PY=$(_PY_OPTION) +$(info $(_PY_AUTODETECT_MSG)) +endif +endif + +ifndef PY +_PY_OPTION:=python +ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) +PY=$(_PY_OPTION) +$(info $(_PY_AUTODETECT_MSG)) +endif +endif + +ifndef PY +define _PY_AUTODETECT_ERR +Could not detect Python interpreter automatically. +Please specify path to interpreter via PY environment variable. +endef +$(error $(_PY_AUTODETECT_ERR)) +endif + + +# +# Internal variable resolution +# + +VENV=$(VENVDIR)/bin +EXE= +# Detect windows +ifeq (win32,$(shell $(PY) -c "import __future__, sys; print(sys.platform)")) +VENV=$(VENVDIR)/Scripts +EXE=.exe +endif + +touch=touch $(1) +ifeq (,$(shell command -v touch $(NULL_STDERR))) +# https://ss64.com/nt/touch.html +touch=type nul >> $(subst /,\,$(1)) && copy /y /b $(subst /,\,$(1))+,, $(subst /,\,$(1)) +endif + +RM?=rm -f +ifeq (,$(shell command -v $(firstword $(RM)) $(NULL_STDERR))) +RMDIR:=rd /s /q +else +RMDIR:=$(RM) -r +endif + + +# +# Virtual environment +# + +.PHONY: venv +venv: $(VENV)/$(MARKER) + +.PHONY: clean-venv +clean-venv: + -$(RMDIR) "$(VENVDIR)" + +.PHONY: show-venv +show-venv: venv + @$(VENV)/python -c "import sys; print('Python ' + sys.version.replace('\n',''))" + @$(VENV)/pip --version + @echo venv: $(VENVDIR) + +.PHONY: debug-venv +debug-venv: + @echo "PATH (Shell)=$$PATH" + @$(MAKE) --version + $(info PATH (GNU Make)="$(PATH)") + $(info SHELL="$(SHELL)") + $(info PY="$(PY)") + $(info REQUIREMENTS_TXT="$(REQUIREMENTS_TXT)") + $(info SETUP_PY="$(SETUP_PY)") + $(info SETUP_CFG="$(SETUP_CFG)") + $(info VENVDIR="$(VENVDIR)") + $(info VENVDEPENDS="$(VENVDEPENDS)") + $(info WORKDIR="$(WORKDIR)") + + +# +# Dependencies +# + +ifneq ($(strip $(REQUIREMENTS_TXT)),) +VENVDEPENDS+=$(REQUIREMENTS_TXT) +endif + +ifneq ($(strip $(SETUP_PY)),) +VENVDEPENDS+=$(SETUP_PY) +endif +ifneq ($(strip $(SETUP_CFG)),) +VENVDEPENDS+=$(SETUP_CFG) +endif + +$(VENV): + $(PY) -m venv $(VENVDIR) + $(VENV)/python -m pip install --upgrade pip setuptools wheel + +$(VENV)/$(MARKER): $(VENVDEPENDS) | $(VENV) +ifneq ($(strip $(REQUIREMENTS_TXT)),) + $(VENV)/pip install $(foreach path,$(REQUIREMENTS_TXT),-r $(path)) +endif +ifneq ($(strip $(SETUP_PY)),) + $(VENV)/pip install $(foreach path,$(SETUP_PY),-e $(dir $(path))) +endif + $(call touch,$(VENV)/$(MARKER)) + + +# +# Interactive shells +# + +.PHONY: python +python: venv + exec $(VENV)/python + +.PHONY: ipython +ipython: $(VENV)/ipython + exec $(VENV)/ipython + +.PHONY: shell +shell: venv + . $(VENV)/activate && exec $(notdir $(SHELL)) + +.PHONY: bash zsh +bash zsh: venv + . $(VENV)/activate && exec $@ + + +# +# Commandline tools (wildcard rule, executable name must match package name) +# + +ifneq ($(EXE),) +$(VENV)/%: $(VENV)/%$(EXE) ; +.PHONY: $(VENV)/% +.PRECIOUS: $(VENV)/%$(EXE) +endif + +$(VENV)/%$(EXE): $(VENV)/$(MARKER) + $(VENV)/pip install --upgrade $* + $(call touch,$@) diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..85b8a87 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +venv +generated +__pycache__ diff --git a/docs/scripts/deploy-docs.sh b/docs/scripts/deploy-docs.sh new file mode 100755 index 0000000..9af0a7d --- /dev/null +++ b/docs/scripts/deploy-docs.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +# Copyright 2025 The KCP Authors. +# +# 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. + +set -o errexit +set -o nounset +set -o pipefail +set -o xtrace + +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +cd "$REPO_ROOT/docs" + +if [[ "${GITHUB_EVENT_NAME:-}" == "pull_request" ]]; then + # For PRs, we don't want to use GITHUB_REF_NAME, which will be something like merge/1234; instead, we want to use + # the branch the PR is targeting, such as main or release-0.11 + VERSION=$GITHUB_BASE_REF +else + if [[ -n "${GITHUB_REF_NAME:-}" ]]; then + VERSION="${VERSION:-$GITHUB_REF_NAME}" + else + VERSION=${VERSION:-$(git rev-parse --abbrev-ref HEAD)} + fi + + if echo "$VERSION" | grep '^release-[0-9]'; then + VERSION=v$(echo "$VERSION" | cut -d - -f 2) + elif echo "$VERSION" | grep '^v[0-9]\+\.[0-9]\+'; then + VERSION=$(echo "$VERSION" | grep -o '^v[0-9]\+\.[0-9]\+') + fi +fi + +MIKE_OPTIONS=() +MIKE_DEPLOY_OPTIONS=() +MIKE_ALIASES=() + +if [[ -n "${REMOTE:-}" ]]; then + MIKE_OPTIONS+=(--remote "$REMOTE") +fi + +if [[ -n "${BRANCH:-}" ]]; then + MIKE_OPTIONS+=(--branch "$BRANCH") +fi + +LATEST=$(git describe --tags --match="v[0-9]*" `git rev-list --tags --max-count=1` | grep -o '^v[0-9]\+\.[0-9]\+') +if [[ "${LATEST:-}" == "${VERSION:-}" ]]; then + MIKE_DEPLOY_OPTIONS+=(--update-aliases) + MIKE_ALIASES+=(latest) +fi + +if [[ -n "${CI:-}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || [[ "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" ]]; then + # Only push to gh-pages if we're in GitHub Actions (CI is set) and we have a non-PR event. + MIKE_OPTIONS+=(--push) + fi + + # Always set git user info in CI because even if we're not pushing, we need it + git config user.name kcp-ci-bot + git config user.email no-reply@kcp.io +else + MIKE_OPTIONS+=(--ignore-remote-status) +fi + +mike deploy "${MIKE_OPTIONS[@]}" "${MIKE_DEPLOY_OPTIONS[@]}" "$VERSION" "${MIKE_ALIASES[@]}" + +if [[ -n "${CI:-}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || [[ "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" ]]; then + if [[ "${LATEST:-}" == "${VERSION:-}" ]]; then + # only set the default if we pushed before, otherwise the "latest" alias might not yet exist. + mike set-default "${MIKE_OPTIONS[@]}" latest + fi + fi +fi diff --git a/docs/scripts/serve-docs.sh b/docs/scripts/serve-docs.sh new file mode 100755 index 0000000..c807436 --- /dev/null +++ b/docs/scripts/serve-docs.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Copyright 2025 The KCP Authors. +# +# 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. + +set -o errexit +set -o nounset +set -o pipefail + +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +cd "$REPO_ROOT/docs" + +MIKE_OPTIONS=() + +if [[ -n "${REMOTE:-}" ]]; then + MIKE_OPTIONS+=(--remote "$REMOTE") +fi + +if [[ -n "${BRANCH:-}" ]]; then + MIKE_OPTIONS+=(--branch "$BRANCH") +fi + +# for local docs testing, we don't care what the remote branch looks like. +MIKE_OPTIONS+=(--ignore-remote-status) + +mike set-default "${MIKE_OPTIONS[@]}" --allow-undefined main +mike serve "${MIKE_OPTIONS[@]}"