This Action simplifies managing a project website. More information is available at infra.apache.org/asf-pelican.html.
| Name | Description | Default |
|---|---|---|
destination |
Pelican output branch | asf-site |
publish |
Publish the site to the destination branch (set false to build only) |
true |
gfm |
Use GitHub Flavored Markdown | true |
output |
Pelican generated output directory | output |
tempdir |
Temporary directory name | ../output.tmp |
debug |
Pelican debug mode | false |
version |
Pelican version to install | 4.11.0.post0 |
requirements |
Extra Python requirements file to install on top of the action | (none) |
fatal |
Value for --fatal option (errors or warnings) |
errors |
Build and publish a site on every push:
jobs:
build-pelican:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: apache/infrastructure-actions/pelican@main
with:
destination: master
gfm: 'true'Build only (useful for pull request checks):
jobs:
build-pelican:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: apache/infrastructure-actions/pelican@main
with:
publish: 'false'| Path | Purpose |
|---|---|
action.yml |
Composite GitHub Action entrypoint. |
Dockerfile |
Docker-based runtime used by Apache CI pipelines. See Docker.md. |
pyproject.toml |
PEP 621 project metadata and dependencies (single source of truth). |
plugin_paths.py |
Helper that injects plugin directories into the Pelican configuration. |
plugins/ |
Bundled ASF Pelican plugins (asfdata, asfgenid, gfm, ...). |
migration/ |
Scripts for migrating legacy infrastructure-pelican sites to this action. |
build-cmark.sh |
Builder script for cmark-gfm used by GFM support. |
The pelican directory is a Python project defined by pyproject.toml
(PEP 517,
PEP 518,
PEP 621,
PEP 639).
Dependencies, metadata and lint configuration all live there — there is no
separate requirements.txt to keep in sync.
This project uses uv as its package manager.
For day-to-day development (editing plugins, running tests, linting) you
have three options — pick whichever matches your workflow.
Tool version requirements. Both uv and hatch need PEP 735
dependency-group support:
| Tool | Minimum version | Why |
|---|---|---|
| uv | 0.5.0 |
[tool.uv].required-version enforces this; PEP 735 landed in 0.4.27. |
| hatch | 1.16.0 |
First release with dependency-groups support in env configs. |
The uv floor is machine-enforced via [tool.uv].required-version in
pyproject.toml; the hatch floor is currently only documented here because
Hatch has no equivalent pyproject.toml field.
Option 1 — uv sync (recommended). Let uv manage the virtual environment
as a project. This creates (or updates) .venv/, resolves every runtime dep
and the PEP 735 dev group in one pass, and writes a uv.lock for
reproducible installs:
uv syncuv sync includes the dev group by default. Any uv run <cmd> invocation
from the project root will automatically use this environment, so you can
skip manual activation:
uv run ruff check .
uv run pytest
uv run pelican --versionOption 2 — manual venv. If you prefer to manage the environment yourself
(e.g. you already have one, or you want to combine it with other projects),
create it, install the project in editable mode, and add the dev group:
uv venv
source .venv/bin/activate
uv pip install -e .
uv pip install --group devOption 3 — Hatch. The repo ships a
[tool.hatch.envs.default] section that pulls the same PEP 735 dev group
via dependency-groups = ["dev"], with uv wired in as the installer:
hatch env create
hatch shellCommon tasks are exposed as named scripts, so you don't need to activate a shell if you just want to run one command:
hatch run lint # ruff check .
hatch run fmt # ruff format .
hatch run test # pytestAll three options resolve the same dev dependency group, so you get
Pelican alongside the project's runtime dependencies and the lint/test
tooling no matter which workflow you choose.
Pelican is intentionally not listed in [project].dependencies. The
composite action installs pelican[markdown] itself via uv tool install,
with the version controlled by the action's version input. Keeping Pelican
in the dev group means there is a single authoritative version source for
the composite action's runtime and a separate one for local development and
the Docker image, with no risk of the two fighting during dependency
resolution.
To reproduce the exact install the composite action performs (Pelican inside an isolated uv tool venv, with this project's runtime dependencies injected), run:
uv tool install 'pelican[markdown]' --with .
pelican --versionTo reproduce the Docker image's install instead (project + locked dev
group, including Pelican, into .venv/), run:
uv sync --frozen
.venv/bin/pelican --versionLint rules and formatter config live under [tool.ruff] in pyproject.toml:
ruff check .
ruff format .pyproject.toml is the source of truth for declared dependency ranges.
uv.lock (committed alongside it) pins every transitive package to a
specific version + hash so the Docker image and uv sync workflows are
fully reproducible. The composite action (action.yml) deliberately does
not consume the lockfile — it installs pelican[markdown]==<version>
fresh on every run so the action's version input stays authoritative.
Adding or changing a dependency. Edit the dependencies list (or the
[dependency-groups].dev list) in pyproject.toml, then refresh the
lockfile:
uv lock # re-resolves only what your edit changed
uv sync # apply the new lockfile to your local .venvCommit pyproject.toml and uv.lock together. Any drift between the two
will cause the Docker build to fail (it runs uv sync --frozen), so the
two files must always move as a pair.
Bumping pinned versions without changing constraints. To pull in newer
patch/minor releases that already satisfy the existing constraints in
pyproject.toml, upgrade the lockfile in place:
uv lock --upgrade # bump every package to its latest allowed
uv lock --upgrade-package foo # bump just one package
uv sync # apply the refreshed lockfileAutomated updates via Dependabot. The repo's
.github/dependabot.yml registers
/pelican/ under the uv package ecosystem, so Dependabot reads
pelican/uv.lock and opens PRs that bump pinned versions on a weekly
schedule with a 4-day cooldown (cooldown.default-days: 4) — newly
released versions have to age four days before Dependabot will propose
them, which avoids picking up brand-new releases that get yanked shortly
after publication. Each Dependabot PR updates uv.lock only; if a bump
needs a wider constraint in pyproject.toml, do that change manually
following the "Adding or changing a dependency" flow above.
See Docker.md for the full workflow. The short version:
docker build -t pelican-asf .
docker run --rm -it -p 8000:8000 -v "$PWD":/site pelican-asfThere are two execution paths and they install Pelican differently. In both
cases the pelican CLI ends up on PATH alongside this project's runtime
dependencies, and plugin_paths is importable from the same Python — so any
dependency that pelicanconf.py needs at runtime lives in the same place as
Pelican itself.
- Composite action (
action.yml) — runs on a GitHub-hosted runner. It provisions a hermetic Python viaactions/setup-python, bootstrapsuvinto that interpreter, runsuv tool install 'pelican[markdown]==<version>' --with <action-path>(optionally layering--with-requirements <user-file>when therequirementsinput is set), optionally buildscmark-gfmifgfm: true, and invokespelican content ...directly. This path does not useuv.lock— Pelican's version comes from theversioninput and the project's runtime deps re-resolve on each run, so users can pin Pelican from their workflow without editing this repo. The tool venv's Python is published to later steps via$PELICAN_TOOL_PYsoplugin_pathsruns inside the same environment as Pelican.
2. Docker image (Dockerfile) — a long-lived image used by Apache CI.
It bootstraps uv, copies pyproject.toml + uv.lock into the image,
then runs uv sync --frozen
to install the project together with the locked dev group (which
includes Pelican) into /opt/pelican-asf/.venv. --frozen makes the
build fail if uv.lock is out of sync with pyproject.toml, so the
image is always a faithful materialisation of the committed lockfile.
The image bakes in the plugins and exposes a pelicanasf wrapper (in
/usr/local/bin) that calls pelican and uses the venv's Python to run
python -m plugin_paths.
The composite action path is intentionally re-resolved on every run so the
version input stays authoritative. The Docker image is intentionally
locked so production rebuilds are byte-reproducible. The two paths therefore
have different update workflows (see Updating dependencies).
The generate_settings.py script is designed to facilitate migrating away from the infra built pelican site generator via .asf.yaml to GitHub Actions.
The script itself takes one argument: the path to the pelicanconf.yaml file. Additionally, the script will look for an .asf.yaml file in the same directory. If an .asf.yaml file is found, the script will generate a GitHub Action workflow file.