Skip to content

Commit 70ab74b

Browse files
authored
Merge pull request #702 from potiuk/modernize-pelican-pyproject
Modernize pelican project to PEP-standard pyproject.toml with uv and Hatch
2 parents 2b7bd80 + 2e014ef commit 70ab74b

7 files changed

Lines changed: 1249 additions & 79 deletions

File tree

.github/dependabot.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ updates:
3838
cooldown:
3939
default-days: 4
4040
- package-ecosystem: "uv"
41-
directory: "/"
41+
directories:
42+
- "/"
43+
- "/pelican/"
4244
schedule:
4345
interval: "weekly"
4446
cooldown:

pelican/Dockerfile

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,39 +38,50 @@ RUN bash bin/build-cmark.sh ${GFM_VERSION}
3838
# rebase the image to save on image size
3939
FROM python:${PYTHON_VERSION}
4040

41-
# Use the Pelican version as installed on CI pelican builders (2023-06-02)
42-
ARG PELICAN_VERSION=4.5.4
4341
ARG GFM_VERSION=0.28.3.gfm.12 # must agree with copy above
4442

4543
# Where we put GFM and the plugins
4644
ENV WORKDIR=/opt/pelican-asf
4745
ENV LIBCMARKDIR=${WORKDIR}/cmark-gfm-${GFM_VERSION}/lib
46+
# `uv sync` writes a project venv at $WORKDIR/.venv; put its bin dir on
47+
# PATH so the `pelican` CLI is visible to the pelicanasf wrapper at
48+
# runtime.
49+
ENV PATH="/opt/pelican-asf/.venv/bin:$PATH"
4850

4951
RUN apt update && apt upgrade -y
5052

5153
# subversion is used by asfdata.py plugin in template-site to retrieve release information
5254
# wget is used by pagefind.sh (www-site)
5355
RUN apt install subversion wget -y
5456

55-
# Need markdown as fallback for gfm as documented in ASF.YAML
56-
RUN pip install pelican[markdown]==${PELICAN_VERSION}
57-
# [1] https://github.com/apache/infrastructure-asfyaml/blob/main/README.md#pelican_cms
57+
# Bootstrap uv — used for every subsequent Python install.
58+
RUN pip install uv
5859

5960
# Copy the built cmark and ASF
6061
WORKDIR $LIBCMARKDIR
6162
COPY --from=pelican-asf $LIBCMARKDIR .
6263

6364
WORKDIR $WORKDIR
6465

65-
COPY requirements.txt .
66-
# Don't automatically load dependencies; please add them to requirements.txt instead
67-
RUN pip install -r requirements.txt --no-deps
68-
69-
# Could perhaps be added to requirements.txt but that would affect other uses
70-
RUN pip install 'MarkupSafe<2.1.0' # needed for Pelican 4.5.4
66+
# Copy enough of the checkout to let uv build the project. py-modules
67+
# in pyproject.toml requires plugin_paths.py, and readme = "README.md"
68+
# requires README.md to be present when setuptools builds the wheel.
69+
# uv.lock is the source of truth for resolved versions.
70+
COPY pyproject.toml uv.lock plugin_paths.py README.md ./
71+
72+
# Install the project and its dev group (which includes Pelican itself)
73+
# into $WORKDIR/.venv straight from the lockfile. --frozen makes the
74+
# build fail if uv.lock is out of sync with pyproject.toml, so the image
75+
# always reflects the committed lockfile rather than re-resolving at
76+
# build time. The `pelican` CLI ends up at .venv/bin/pelican (already on
77+
# PATH) and the venv's own Python can import `plugin_paths` as a module
78+
# — we use that below so user-supplied pelicanconf.py can import any
79+
# dep that lives in the venv.
80+
# [1] https://github.com/apache/infrastructure-asfyaml/blob/main/README.md#pelican_cms
81+
RUN uv sync --frozen
7182

72-
# Now add the local code; do this last to avoid unnecessary rebuilds
73-
COPY plugin_paths.py .
83+
# Now add the plugins; these are loaded by Pelican via PLUGIN_PATHS at
84+
# runtime and are never imported as a Python package.
7485
COPY plugins plugins
7586
# Tidy up any old builds
7687
RUN { find plugins -type d -name __pycache__ -exec rm -rf {} \; ; true; }
@@ -82,11 +93,28 @@ RUN ln -s /site/.authtokens /root/.authtokens
8293

8394
WORKDIR /site
8495

85-
# Create a pelicanasf wrapper to add required arguments
86-
# The initial hashbang is needed, otherwise docker complains: 'exec format error'
87-
RUN { echo '#!/usr/bin/env bash' >/usr/local/bin/pelicanasf; chmod +x /usr/local/bin/pelicanasf;}
88-
# The -b (bind) is needed to allow connections from the host
89-
RUN echo 'SRC=${1:?source}; shift; python3 -B -m pelican $SRC -b 0.0.0.0 -e "$(python3 $WORKDIR/plugin_paths.py $WORKDIR/plugins)" "$@"' >>/usr/local/bin/pelicanasf;
96+
# Create a pelicanasf wrapper around the `pelican` CLI (from the project
97+
# venv) that injects the PLUGIN_PATHS settings computed by plugin_paths.
98+
# plugin_paths is invoked as a module via the venv's own Python, so the
99+
# user's pelicanconf.py can import any dep that lives in the venv.
100+
# The initial hashbang is required, otherwise docker complains with
101+
# 'exec format error'. -b (bind) allows connections from the host.
102+
RUN TOOLPY="$WORKDIR/.venv/bin/python" && \
103+
{ echo '#!/usr/bin/env bash'; \
104+
echo 'set -euo pipefail'; \
105+
echo "TOOLPY='$TOOLPY'"; \
106+
echo "WORKDIR='$WORKDIR'"; \
107+
echo 'SRC=${1:?source}; shift'; \
108+
echo 'if [[ ! -f pelicanconf.py ]]; then'; \
109+
echo ' echo "pelicanasf: pelicanconf.py not found in $(pwd)." >&2'; \
110+
echo ' echo "Mount a Pelican website checkout (one containing pelicanconf.py) at /site, e.g." >&2'; \
111+
echo ' echo " docker run --rm -it -p 8000:8000 -v \"\$PWD\":/site <image>" >&2'; \
112+
echo ' exit 1'; \
113+
echo 'fi'; \
114+
echo 'PP=$("$TOOLPY" -m plugin_paths "$WORKDIR/plugins")'; \
115+
echo 'exec pelican "$SRC" -b 0.0.0.0 -e "$PP" "$@"'; \
116+
} > /usr/local/bin/pelicanasf && \
117+
chmod +x /usr/local/bin/pelicanasf
90118

91119
# --autoreload (-r) and --listen (-l)
92120
ENTRYPOINT [ "pelicanasf", "content", "-rl"]

pelican/README.md

Lines changed: 236 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,52 +18,267 @@
1818
-->
1919
# ASF Infrastructure Pelican Action
2020

21-
**Note** Starting a branch and draft PR for systemic upgrades to the Pelican Action.
22-
23-
**Note** This Action simplifies managing a project website. More information is available at <a href="https://infra.apache.org/asf-pelican.html" target="_blank">infra.apache.org/asf-pelican.html</a>.
21+
This Action simplifies managing a project website. More information is available at
22+
[infra.apache.org/asf-pelican.html](https://infra.apache.org/asf-pelican.html).
2423

2524
## Inputs
26-
* destination Pelican Output branch (optional) default: asf-site
27-
* publish Publish to destination branch (optional) default: true
28-
* gfm Uses GitHub Flavored Markdown (optional) default: true
29-
* output Pelican generated output directory (optional) default: output
30-
* tempdir Temporary Directory name (optional) default: ../output.tmp
31-
* debug Pelican Debug mode (optional) default: false
32-
* version Pelican Version (default 4.5.4) (optional) default: 4.5.4
33-
* requirements Python Requirements file (optional) default: None
34-
* fatal Value for --fatal option [errors|warnings] - sets exit code to error (default: errors)
35-
36-
## Example Workflow Usage:
3725

38-
```
39-
...
26+
| Name | Description | Default |
27+
| -------------- | --------------------------------------------------------------------------- | ----------------- |
28+
| `destination` | Pelican output branch | `asf-site` |
29+
| `publish` | Publish the site to the destination branch (set `false` to build only) | `true` |
30+
| `gfm` | Use GitHub Flavored Markdown | `true` |
31+
| `output` | Pelican generated output directory | `output` |
32+
| `tempdir` | Temporary directory name | `../output.tmp` |
33+
| `debug` | Pelican debug mode | `false` |
34+
| `version` | Pelican version to install | `4.11.0.post0` |
35+
| `requirements` | Extra Python requirements file to install on top of the action | _(none)_ |
36+
| `fatal` | Value for `--fatal` option (`errors` or `warnings`) | `errors` |
37+
38+
## Example workflows
39+
40+
Build and publish a site on every push:
41+
42+
```yaml
4043
jobs:
4144
build-pelican:
4245
runs-on: ubuntu-latest
4346
steps:
4447
- uses: actions/checkout@v4
45-
with:
4648
- uses: apache/infrastructure-actions/pelican@main
4749
with:
4850
destination: master
4951
gfm: 'true'
5052
```
5153
52-
Example workflow for only building the site, not publishing. Useful for PR tests:
54+
Build only (useful for pull request checks):
5355
54-
```
55-
...
56+
```yaml
5657
jobs:
5758
build-pelican:
5859
runs-on: ubuntu-latest
5960
steps:
6061
- uses: actions/checkout@v4
61-
with:
6262
- uses: apache/infrastructure-actions/pelican@main
6363
with:
6464
publish: 'false'
6565
```
6666
67+
## Project layout
68+
69+
| Path | Purpose |
70+
| ----------------- | ---------------------------------------------------------------------------- |
71+
| `action.yml` | Composite GitHub Action entrypoint. |
72+
| `Dockerfile` | Docker-based runtime used by Apache CI pipelines. See [Docker.md](Docker.md).|
73+
| `pyproject.toml` | PEP 621 project metadata and dependencies (single source of truth). |
74+
| `plugin_paths.py` | Helper that injects plugin directories into the Pelican configuration. |
75+
| `plugins/` | Bundled ASF Pelican plugins (`asfdata`, `asfgenid`, `gfm`, ...). |
76+
| `migration/` | Scripts for migrating legacy infrastructure-pelican sites to this action. |
77+
| `build-cmark.sh` | Builder script for `cmark-gfm` used by GFM support. |
78+
79+
## Working with the project
80+
81+
The `pelican` directory is a Python project defined by `pyproject.toml`
82+
([PEP 517](https://peps.python.org/pep-0517/),
83+
[PEP 518](https://peps.python.org/pep-0518/),
84+
[PEP 621](https://peps.python.org/pep-0621/),
85+
[PEP 639](https://peps.python.org/pep-0639/)).
86+
Dependencies, metadata and lint configuration all live there — there is no
87+
separate `requirements.txt` to keep in sync.
88+
89+
### Local development
90+
91+
This project uses [`uv`](https://docs.astral.sh/uv/) as its package manager.
92+
For day-to-day development (editing plugins, running tests, linting) you
93+
have three options — pick whichever matches your workflow.
94+
95+
**Tool version requirements.** Both `uv` and `hatch` need PEP 735
96+
dependency-group support:
97+
98+
| Tool | Minimum version | Why |
99+
| ----- | --------------- | --------------------------------------------------------------------- |
100+
| uv | `0.5.0` | `[tool.uv].required-version` enforces this; PEP 735 landed in 0.4.27. |
101+
| hatch | `1.16.0` | First release with `dependency-groups` support in env configs. |
102+
103+
The uv floor is machine-enforced via `[tool.uv].required-version` in
104+
`pyproject.toml`; the hatch floor is currently only documented here because
105+
Hatch has no equivalent pyproject.toml field.
106+
107+
**Option 1 — `uv sync` (recommended).** Let uv manage the virtual environment
108+
as a project. This creates (or updates) `.venv/`, resolves every runtime dep
109+
and the PEP 735 `dev` group in one pass, and writes a `uv.lock` for
110+
reproducible installs:
111+
112+
```shell
113+
uv sync
114+
```
115+
116+
`uv sync` includes the `dev` group by default. Any `uv run <cmd>` invocation
117+
from the project root will automatically use this environment, so you can
118+
skip manual activation:
119+
120+
```shell
121+
uv run ruff check .
122+
uv run pytest
123+
uv run pelican --version
124+
```
125+
126+
**Option 2 — manual venv.** If you prefer to manage the environment yourself
127+
(e.g. you already have one, or you want to combine it with other projects),
128+
create it, install the project in editable mode, and add the `dev` group:
129+
130+
```shell
131+
uv venv
132+
source .venv/bin/activate
133+
uv pip install -e .
134+
uv pip install --group dev
135+
```
136+
137+
**Option 3 — [Hatch](https://hatch.pypa.io/).** The repo ships a
138+
`[tool.hatch.envs.default]` section that pulls the same PEP 735 `dev` group
139+
via `dependency-groups = ["dev"]`, with `uv` wired in as the installer:
140+
141+
```shell
142+
hatch env create
143+
hatch shell
144+
```
145+
146+
Common tasks are exposed as named scripts, so you don't need to activate a
147+
shell if you just want to run one command:
148+
149+
```shell
150+
hatch run lint # ruff check .
151+
hatch run fmt # ruff format .
152+
hatch run test # pytest
153+
```
154+
155+
All three options resolve the same `dev` dependency group, so you get
156+
Pelican alongside the project's runtime dependencies and the lint/test
157+
tooling no matter which workflow you choose.
158+
159+
Pelican is intentionally **not** listed in `[project].dependencies`. The
160+
composite action installs `pelican[markdown]` itself via `uv tool install`,
161+
with the version controlled by the action's `version` input. Keeping Pelican
162+
in the `dev` group means there is a single authoritative version source for
163+
the composite action's runtime and a separate one for local development and
164+
the Docker image, with no risk of the two fighting during dependency
165+
resolution.
166+
167+
To reproduce the exact install the **composite action** performs (Pelican
168+
inside an isolated uv tool venv, with this project's runtime dependencies
169+
injected), run:
170+
171+
```shell
172+
uv tool install 'pelican[markdown]' --with .
173+
pelican --version
174+
```
175+
176+
To reproduce the **Docker image's** install instead (project + locked dev
177+
group, including Pelican, into `.venv/`), run:
178+
179+
```shell
180+
uv sync --frozen
181+
.venv/bin/pelican --version
182+
```
183+
184+
### Linting and formatting
185+
186+
Lint rules and formatter config live under `[tool.ruff]` in `pyproject.toml`:
187+
188+
```shell
189+
ruff check .
190+
ruff format .
191+
```
192+
193+
### Updating dependencies
194+
195+
`pyproject.toml` is the source of truth for declared dependency ranges.
196+
`uv.lock` (committed alongside it) pins every transitive package to a
197+
specific version + hash so the Docker image and `uv sync` workflows are
198+
fully reproducible. The composite action (`action.yml`) deliberately does
199+
**not** consume the lockfile — it installs `pelican[markdown]==<version>`
200+
fresh on every run so the action's `version` input stays authoritative.
201+
202+
**Adding or changing a dependency.** Edit the `dependencies` list (or the
203+
`[dependency-groups].dev` list) in `pyproject.toml`, then refresh the
204+
lockfile:
205+
206+
```shell
207+
uv lock # re-resolves only what your edit changed
208+
uv sync # apply the new lockfile to your local .venv
209+
```
210+
211+
Commit `pyproject.toml` and `uv.lock` together. Any drift between the two
212+
will cause the Docker build to fail (it runs `uv sync --frozen`), so the
213+
two files must always move as a pair.
214+
215+
**Bumping pinned versions without changing constraints.** To pull in newer
216+
patch/minor releases that already satisfy the existing constraints in
217+
`pyproject.toml`, upgrade the lockfile in place:
218+
219+
```shell
220+
uv lock --upgrade # bump every package to its latest allowed
221+
uv lock --upgrade-package foo # bump just one package
222+
uv sync # apply the refreshed lockfile
223+
```
224+
225+
**Automated updates via Dependabot.** The repo's
226+
[`.github/dependabot.yml`](../.github/dependabot.yml) registers
227+
`/pelican/` under the `uv` package ecosystem, so Dependabot reads
228+
`pelican/uv.lock` and opens PRs that bump pinned versions on a **weekly**
229+
schedule with a **4-day cooldown** (`cooldown.default-days: 4`) — newly
230+
released versions have to age four days before Dependabot will propose
231+
them, which avoids picking up brand-new releases that get yanked shortly
232+
after publication. Each Dependabot PR updates `uv.lock` only; if a bump
233+
needs a wider constraint in `pyproject.toml`, do that change manually
234+
following the "Adding or changing a dependency" flow above.
235+
236+
### Building the Docker image
237+
238+
See [Docker.md](Docker.md) for the full workflow. The short version:
239+
240+
```shell
241+
docker build -t pelican-asf .
242+
docker run --rm -it -p 8000:8000 -v "$PWD":/site pelican-asf
243+
```
244+
245+
### How the action runs
246+
247+
There are two execution paths and they install Pelican differently. In both
248+
cases the `pelican` CLI ends up on `PATH` alongside this project's runtime
249+
dependencies, and `plugin_paths` is importable from the same Python — so any
250+
dependency that `pelicanconf.py` needs at runtime lives in the same place as
251+
Pelican itself.
252+
253+
1. **Composite action** (`action.yml`) — runs on a GitHub-hosted runner.
254+
It provisions a hermetic Python via [`actions/setup-python`][setup-python],
255+
bootstraps `uv` into that interpreter, runs
256+
`uv tool install 'pelican[markdown]==<version>' --with <action-path>`
257+
(optionally layering `--with-requirements <user-file>` when the
258+
`requirements` input is set), optionally builds `cmark-gfm` if `gfm: true`,
259+
and invokes `pelican content ...` directly. This path **does not** use
260+
`uv.lock` — Pelican's version comes from the `version` input and the
261+
project's runtime deps re-resolve on each run, so users can pin Pelican
262+
from their workflow without editing this repo. The tool venv's Python is
263+
published to later steps via `$PELICAN_TOOL_PY` so `plugin_paths` runs
264+
inside the same environment as Pelican.
265+
266+
[setup-python]: https://github.com/actions/setup-python
267+
2. **Docker image** (`Dockerfile`) — a long-lived image used by Apache CI.
268+
It bootstraps `uv`, copies `pyproject.toml` + `uv.lock` into the image,
269+
then runs [`uv sync --frozen`](https://docs.astral.sh/uv/reference/cli/#uv-sync)
270+
to install the project together with the locked `dev` group (which
271+
includes Pelican) into `/opt/pelican-asf/.venv`. `--frozen` makes the
272+
build fail if `uv.lock` is out of sync with `pyproject.toml`, so the
273+
image is always a faithful materialisation of the committed lockfile.
274+
The image bakes in the plugins and exposes a `pelicanasf` wrapper (in
275+
`/usr/local/bin`) that calls `pelican` and uses the venv's Python to run
276+
`python -m plugin_paths`.
277+
278+
The composite action path is intentionally re-resolved on every run so the
279+
`version` input stays authoritative. The Docker image is intentionally
280+
locked so production rebuilds are byte-reproducible. The two paths therefore
281+
have different update workflows (see [Updating dependencies](#updating-dependencies)).
67282

68283
# Pelican Migration Scripts
69284

0 commit comments

Comments
 (0)