| Filename | Latest commit message | Latest commit date |
|---|---|---|
The seven Python plugin repositories each carried a hand-copied ci.yml and release.yml, and the copies drifted: the dependency audit a step in some and a job in others, a weekly schedule that ran the whole suite in two, a tag that ran the full CI beside the release gate in six, and a uv bump that was about fifteen edits in eight repositories. None kept uv's cache, so every run cloned the core again, about 90 s of a 3-minute job. action.yml is that job once. In check mode it installs the plugin's Debian packages (WeasyPrint's by default, more by input), uv at the pinned version after checking the installer's sha256, restores uv's cache, runs `uv sync --locked`, saves the cache (never from a pull request, which also restores by exact key alone), then ruff, the catalogue checks and guard, Weblate's gate on its own pull request, compile and pytest, and on request the release gate. In audit mode it exports the lock and runs pip-audit over it, without installing anything, so a plugin runs it as a job of its own and weekly. What differs between plugins is an input. The uv pin is here alone. The steps of a composite action cannot carry timeout-minutes, so every network command runs under coreutils' timeout and the caller puts a step timeout on the uses: line. The README lists the inputs, shows the workflows a plugin needs, and says how to bump uv and cut a release behind the moving v1 tag. AGPL-3.0-or-later, as the plugins are. Refs postulo/postulo#1170 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
| action.yml | ||
| LICENSE | ||
| README.md | ||
plugin-ci
The continuous integration of a Postulo Python plugin, as one composite action for Forgejo Actions (postulo/postulo#1170).
Every plugin repository used to carry its own copy of the same workflow, and the copies drifted: the dependency audit a step in some and a job in others, a weekly schedule that ran the whole suite in two, a tag that ran the full CI beside the release gate in six, and a uv bump that was about fifteen hand edits across eight repositories. A plugin's workflow now checks the repository out and calls this action. What differs between plugins is an input, never a copy, and the pinned uv lives here alone.
What it does
In check mode (the default), in this order:
- Refuses a
modeit does not know, and a shallow checkout when the catalogue guard is to run (the guard reads the history). - Installs the Debian packages the plugin needs: WeasyPrint's libraries by default, which Postulo's settings load through its PDF backend, plus any the plugin adds.
- Installs uv at the pinned version, after checking the installer's sha256.
- Restores uv's cache (its downloads, the core's git checkout, the Python it fetched) with
actions/cache/restore. uv sync --locked --python <python-version>: the plugin, Postulo at the pinned commit or tag, and the test tools.- Saves uv's cache with
actions/cache/save, unless the run is a pull request or the exact key was already restored. ruff check --output-format=full .andruff format --check .postulo-messages extract --check,checkandguard --base <base>, which refuses a translation written outside Weblate.- On Weblate's own pull request (head branch
weblate),postulo-messages gatefor the gated languages. postulo-messages compile(the.mofiles are not committed), thenpytest.- When asked, the release gate:
pytest -m release.
In audit mode it installs uv, exports the lock (uv export, without the project and
without what audit-exclude names) and runs pip-audit --strict over it through uvx.
Nothing is installed and nothing is synced, so the audit is a job of its own, fast, and the
one a weekly schedule runs.
The two actions/cache steps are continue-on-error: a cache that is down costs time and
fails nothing. A pull request restores by exact key only and never saves; a push, a tag or a
schedule may restore an older cache by prefix and saves a new one, as Postulo's own CI does
(postulo/postulo#827).
uv is installed to ~/.local/bin and called by that path; the action does not put it on
PATH. A step of the plugin's own after the action calls ~/.local/bin/uv.
Inputs
Every input is a string, as the inputs of an action are; the booleans are "true" or
"false".
| Input | Default | Mode | What it does |
|---|---|---|---|
mode |
check |
check or audit, as above. |
|
python-version |
3.14 |
check | The Python uv installs the plugin on. Also part of the cache key. |
system-packages |
libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b libfontconfig1 |
check | Debian packages, separated by spaces; empty installs none. |
extra-system-packages |
(none) | check | Installed beside system-packages. postulo-templates: libharfbuzz-subset0. |
sync-args |
(none) | check | Further arguments to uv sync --locked, such as --extra chromium. |
lint |
true |
check | Run ruff's check and format check. |
catalogues |
true |
check | Run postulo-messages extract --check, check and guard. Needs fetch-depth: 0. |
weblate-gate |
fr-FR pt-PT |
check | Languages postulo-messages gate holds Weblate's pull request to; empty skips it. |
tests |
true |
check | Compile the catalogues and run pytest. |
pytest-args |
(none) | check | Further arguments to that pytest. |
release-gate |
false |
check | Run pytest -m release after the tests. |
cache |
true |
check | Keep uv's cache between runs. |
audit-exclude |
postulo |
audit | Locked packages left out of the audit because they are not on PyPI. Postulo is a git dependency the core audits itself. Empty for a project that does not depend on Postulo. |
How a plugin uses it
The action is named by its full address. A bare Postulo/plugin-ci@v1 would be looked
up on the instance's default actions host (data.forgejo.org), not on this Forgejo. It is
named by its major tag, v1, which moves forward with every compatible release (see below).
Time limits go on the steps: this Forgejo honours a step's timeout-minutes and ignores a
job's, and the steps inside a composite action cannot carry one, so the caller's
timeout-minutes on the uses: step bounds the whole action. Inside it, every command that
reaches the network runs under coreutils' timeout as well. Leave out permissions:: this
Forgejo does not support it and warns. A job-level if: fails its job here instead of
skipping it, which is why the weekly audit is a workflow of its own rather than a schedule
in ci.yml.
.forgejo/workflows/ci.yml. No tags: trigger: a tag is a commit main already tested, and
release.yml is the only workflow a tag starts.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
container:
# Node, because Forgejo runs actions/checkout as JavaScript inside the container.
image: node:22-bookworm
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
# The whole history: the translation guard reads every commit it is given.
fetch-depth: 0
timeout-minutes: 5
- name: Install, lint, check the catalogues and test
uses: https://source.tiagoagueda.com/Postulo/plugin-ci@v1
timeout-minutes: 30
audit:
name: Audit the locked dependencies
runs-on: ubuntu-latest
container:
image: node:22-bookworm
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
timeout-minutes: 5
- name: Audit the Python dependencies
uses: https://source.tiagoagueda.com/Postulo/plugin-ci@v1
with:
mode: audit
timeout-minutes: 15
.forgejo/workflows/weekly.yml: the audit alone, every Monday, so an advisory published in
a quiet week is still seen. It is the same audit job as in ci.yml, on:
on:
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:
.forgejo/workflows/release.yml, on push: tags: ["v*"]: the install and the release gate
alone, since lint, the catalogues and the suite already ran on main.
- name: The release gate
uses: https://source.tiagoagueda.com/Postulo/plugin-ci@v1
with:
lint: "false"
catalogues: "false"
tests: "false"
release-gate: "true"
timeout-minutes: 30
.forgejo/workflows/every-python.yml, for a plugin that tests the older Pythons its
requires-python admits (postulo-imap, postulo-apprise): a matrix over python-version,
the tests alone, since lint and the catalogues read the same on every Python.
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
timeout-minutes: 5
- name: Test
uses: https://source.tiagoagueda.com/Postulo/plugin-ci@v1
with:
python-version: ${{ matrix.python-version }}
lint: "false"
catalogues: "false"
timeout-minutes: 30
postulo-templates adds extra-system-packages: libharfbuzz-subset0 to its test and release
steps.
The action is made for the plugins: a repository with Postulo in its dev group and its own
catalogues. postulo-mcp, which has neither, keeps its own workflow; its audit could still
call this action with mode: audit and audit-exclude: "".
Bumping uv
uv's version and the sha256 of that version's install.sh are in one place: the env: of
the Install uv step in action.yml, UV_VERSION and UV_INSTALLER_SHA256. They are the
same two values as Postulo's own workflows. Bump them when the core does, to the core's
values:
-
In Postulo's repository, read the new pair from
.forgejo/workflows/ci.yml. ItsCONTRIBUTING.mdsays how they are found and checked; the sha256 is that ofhttps://astral.sh/uv/<version>/install.sh:curl -LsSf "https://astral.sh/uv/0.12.17/install.sh" | sha256sum -
Change the two lines in
action.yml, open a pull request here, and merge it once a plugin has run it (point a plugin's branch at the branch by@<branch>to try it). -
Cut a new release (below). The plugins pick it up through
v1with no change of their own.
Cutting a release
Every release gets its own tag, v1.0.0, v1.0.1, v1.1.0, which never moves, and the
major tag v1 is moved to the newest of them. A change that keeps every input working (a uv
bump, a new input with a default that keeps today's behaviour) is a minor or patch release
under v1. A change that removes or renames an input, or changes a default so that a plugin
would check less or fail, is v2.0.0, and each plugin moves to @v2 in its own pull request.
git switch main && git pull --ff-only
git tag -s v1.0.1 -m "plugin-ci 1.0.1: uv 0.12.18"
git tag -s -f v1 -m "plugin-ci v1 -> 1.0.1"
git push origin v1.0.1
git push --force origin v1
Moving v1 re-points every plugin at once, which is the purpose of it and also the risk.
Try the new commit from one plugin first, by its tag or commit (@v1.0.1) in a branch, and
only then move v1. A plugin that needs to hold still pins an exact tag or a commit hash.
Licence
AGPL-3.0-or-later, as Postulo's plugins are. See LICENSE.