-
Notifications
You must be signed in to change notification settings - Fork 0
Contributing and Releasing
This page covers the yacron2 developer workflow (environment, tests, linters, type checks, pre-commit) and the fully automated GitHub Actions release pipeline that builds, publishes, tags, and containerizes each version. Version numbers are derived from git tags via setuptools_scm and are never hand-edited.
yacron2 targets Python 3.10+; 3.10, 3.11, 3.12, 3.13 and 3.14 are the tested interpreters (pyproject.toml requires-python = ">=3.10", classifiers for 3.10 through 3.14).
yacron2 runs natively on Windows, Linux, and macOS (WSL is not required). All OS-specific behavior is isolated in yacron2/platform.py (grp/pwd are guarded there, not imported unconditionally at load time on Windows), so the package and its full test suite run natively on every supported OS, and pip install yacron2 works on Windows. See Running on Windows for the platform-specific details.
Linting and type checking do not import the package and run on any platform. mypy is pinned to the linux platform (pyproject.toml [tool.mypy] platform = "linux"), so type-checking is identical on every OS: it type-checks the POSIX API surface, and the Windows branches are runtime-guarded.
Clone and install the editable package with the dev extra:
git clone https://github.com/ptweezy/yacron2
cd yacron2
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]" # or: pip install -r requirements_dev.txtThe editable dev install (pip install -e ".[dev]") and the checks (pytest, ruff, mypy) all run natively on Windows too. Use .venv\Scripts\activate to enter the venv as shown above.
The dev optional-dependency group (pyproject.toml) and the equivalent requirements_dev.txt both pull in: mypy, mypy-extensions, pytest, pytest-asyncio, pytest-cov, ruff, and tox. The console entry point yacron2 = yacron2.__main__:main is installed by the editable install (see Command-Line Reference).
All CI checks are driven by tox (tox.ini). The default envlist is py310, py311, py312, py313, py314, lint, mypy.
tox # all envs: py310-py314, lint, mypy
tox -e lint # ruff check + ruff format --check
tox -e mypy # mypy
tox -e py # pytest on the current interpreter| Env | Installs package | What it runs |
|---|---|---|
py313, py314
|
yes (-rrequirements_dev.txt, PYTHONPATH={toxinidir}) |
pytest --color=yes -vv |
lint |
no (skip_install = true) |
ruff check yacron2 then ruff format --check yacron2
|
mypy |
no (skip_install = true, basepython=python3) |
mypy -p yacron2 --ignore-missing-imports |
The lint and mypy envs deliberately skip installing the package: ruff and mypy analyze the source tree directly, so they avoid imposing the project's requires-python on the lint/type-check interpreter.
pyproject.toml configures the tooling:
-
ruff:
target-version = "py313",line-length = 79. Lint rule sets selected:B,B9(bugbear),C(mccabe complexity),E(pycodestyle errors),F(pyflakes),W(pycodestyle warnings),I(import sorting).pyupgrade(UP) is present but commented out.max-complexity = 20. -
mypy:
no_implicit_optional = true,warn_no_return = true,warn_return_any = true,strict_optional = true. -
pytest:
asyncio_mode = "auto",testpaths = ["tests"]. -
bandit:
exclude_dirs = ["tests"].
pre-commit runs ruff and bandit on staged changes (.pre-commit-config.yaml):
pip install pre-commit
pre-commit installConfigured hooks:
| Repo | Rev | Hook(s) | Args |
|---|---|---|---|
PyCQA/bandit |
1.9.4 |
bandit |
-c pyproject.toml --severity-level=medium, with bandit[toml]
|
astral-sh/ruff-pre-commit |
v0.15.18 |
ruff (lint), ruff-format
|
ruff runs with --fix
|
Note pre-commit's ruff runs with --fix (auto-applies fixes), whereas tox -e lint runs ruff check (no fix) plus ruff format --check (verify only). pre-commit is not pinned in the dev extra; install it separately as shown.
.github/workflows/tox.yml runs on every push and pull_request (read-only permissions). It has three jobs: tox-lint (tox -e lint) and tox-mypy (tox -e mypy) on ubuntu-latest, plus a tox matrix running tox -e py (fail-fast: false). The matrix runs the full test suite on both Linux and Windows: os is [ubuntu-latest, windows-latest] across Python 3.10, 3.11, 3.12, 3.13, and 3.14, with an experimental ubuntu-latest/3.15 row (continue-on-error, never gates) and an extra windows-11-arm row pinned to Python 3.14 to exercise Windows ARM64 (the released .exe targets it too). macOS (macos-latest) is still optionally commented out, since macOS is POSIX like Linux.
A second per-commit gate is .github/workflows/docker.yml, which builds the container image on every commit on every branch (push: branches: ["**"]) as linux/amd64 build-only (no push, tagged ci-build), so a broken Dockerfile fails CI before a release. See Production and Container Deployment.
Releases are fully automated by .github/workflows/release.yml. You never edit a version by hand; setuptools_scm derives the version from git tags (version_file = "yacron2/version.py").
A release runs when either:
- A push to
mainin which any commit introduced by the push has a release marker anywhere in its message, not just the tip commit. The scanned range isBEFORE..AFTER(the commits new in the push); on a brand-new branch whereBEFOREis all-zeros (or unresolvable) it falls back to the tip commit only. - A manual
workflow_dispatchrun, choosing the bump level from a dropdown (minordefault, ormajor/patch).
Valid markers (case-insensitive; the bump level is optional):
| Marker | Bump | 1.0.5 → |
|---|---|---|
[release] |
minor | 1.1.0 |
[release:major] |
major | 2.0.0 |
[release:minor] |
minor | 1.1.0 |
[release:patch] |
patch | 1.0.6 |
If several markers appear across the pushed commits, the most significant bump wins (major > minor > patch); a bare [release] counts as minor.
The marker match is performed in the decide job with grep -oiE '\[release(:(major|minor|patch))?\]' over the commit message bodies.
Footgun: literal-marker substring match. The match is a plain substring against commit message text (not anchored to its own line, not requiring any surrounding structure). Writing a literal
[release:patch](or[release]) anywhere in a commit message (even in prose describing the release process) will trigger a publish. Do not quote a marker verbatim in a commit message unless you mean it. Only commit messages are scanned; file contents are never scanned (this page can name the markers freely).
The release.yml jobs run in dependency order. Top-level permissions default to contents: read; only the release job (contents: write + id-token: write) and the docker job (packages: write) opt up to the write scopes they need.
-
decide: determinesrelease(true/false) andbump. Trigger logic lives in a real shell script rather than a fuzzycontains()expression. All downstream jobs are gated onneeds.decide.outputs.release == 'true'. -
version: computes the next version once, so every builder and the publish job build at the same number. Finds the latest tag matching^[0-9]+\.[0-9]+\.[0-9]+$(viagit tag -l | … | sort -V | tail -n1, defaulting to0.0.0), applies the bump, and refuses with an error if the computed tag already exists (refs/tags/$new). -
gate: checks out full history and runstoxacross Python 3.10–3.14 (toxwith no-erunspy310, py311, py312, py313, py314, lint, mypy). A red build means no release. -
Native binary builds (run before publishing, so a broken build fails the run instead of producing a half-finished release). Each job pins
pyinstaller==6.21.0, runspip install .to bakeSETUPTOOLS_SCM_PRETEND_VERSION(the computed version) intoyacron2/version.py, runspyinstaller pyinstaller/yacron2.spec, and smoke-tests the bundle withdist/yacron2 --version:-
binaries: Linux glibc, 64-bit,amd64onubuntu-24.04andarm64onubuntu-24.04-arm(native runners, no QEMU). Built on Python 3.14. Artifactsyacron2-linux-amd64,yacron2-linux-arm64. -
binaries-glibc-extra: Linux glibc for the arches with no native GitHub runner (i686,armv7,ppc64le,s390x,riscv64), built inside apython:3.14-slim(Debian) container viadocker run --platform(the nativebinariesjob covers onlyamd64/arm64and PyInstaller is not a cross-compiler, so these need a foreign-arch container).i686(linux/386) runs natively on theubuntu-24.04runner;armv7(linux/arm/v7),ppc64le(linux/ppc64le),s390x(linux/s390x) andriscv64(linux/riscv64) run under QEMU (docker/setup-qemu-action). Installsbuild-essential libffi-dev zlib1g-dev(the spec setsstrip=True;ppc64le/s390xhave full manylinux wheels, while the i686 aiohttp stack, propcache-on-armv7, and multidict/frozenlist/ruamel.yaml.clib onriscv64compile from sdist). Artifactsyacron2-linux-{i686,armv7,ppc64le,s390x,riscv64}(no-muslsuffix; they sit beside the 64-bit glibc binaries). -
binaries-musl: Linux musl/Alpine,amd64,arm64,i686,armv7,ppc64le,s390x,riscv64andarmv6, built inside apython:3.14-alpinecontainer viadocker run(so checkout/upload stay on the glibc host).amd64/arm64use their native runners;i686(linux/386) runs natively on theubuntu-24.04runner andarmv7/ppc64le/s390x/riscv64/armv6under QEMU.armv6is musl-only (the Debian/glibc image ships no arm32v6, so it has nobinaries-glibc-extracounterpart). Installsbuild-base libffi-dev zlib-dev(the spec setsstrip=True, and headers cover any dep that compiles from sdist, notably the i686 aiohttp stack, which ships nomusllinux_i686wheels, and the whole C-ext stack onarmv6). Artifactsyacron2-linux-{amd64,arm64,i686,armv7,ppc64le,s390x,riscv64,armv6}-musl. -
binaries-macos: macOS,arm64onmacos-15(Apple Silicon) andamd64onmacos-15-intel. Built on Python 3.14. After the smoke test it asserts the native arch withfile(so Rosetta cannot let a mislabelled x86_64 build pass on the arm64 runner). Artifactsyacron2-macos-arm64,yacron2-macos-amd64. -
binaries-windows: Windows,amd64onwindows-latestandarm64on thewindows-11-armrunner (both native; PyInstaller is not a cross-compiler). Built on Python 3.14 with the samepyinstaller==6.21.0pin anddist/yacron2.exe --versionsmoke test as the other jobs; any C-extension dep lacking awin_arm64wheel compiles from sdist via the runner's Visual Studio ARM64 toolchain. There is no Windows code-signing step (the binaries ship unsigned, like the Linux binaries). Artifactsyacron2-windows-amd64.exe,yacron2-windows-arm64.exe. See Running on Windows.
-
-
release: runs only after all builders succeed, withpermissions: contents: writeandid-token: write. In order:- Builds the wheel + sdist with
python -m build(atSETUPTOOLS_SCM_PRETEND_VERSION) and validates withtwine check. -
Publishes the wheel + sdist to PyPI via Trusted Publishing / OIDC (
pypa/gh-action-pypi-publish@release/v1), no API token. -
Only after a successful publish: creates an annotated tag
X.Y.Zand pushes it; downloads every per-arch binary artifact (patternyacron2-*,merge-multiple: true); extracts the release notes; and creates the GitHub Release.
- Builds the wheel + sdist with
-
docker: afterrelease, callsdocker.ymlviaworkflow_callwith the new version to build and push the multi-arch image (see below).
Because no file is committed back to the repo, a release never re-triggers the workflow. Because the tag is created after publishing, a failed publish leaves no orphan tag and a re-run cleanly retries the same version.
The macOS binaries are Developer ID signed (hardened runtime) and notarized when the signing secrets are configured; if absent, the "Sign and notarize" step warns and exits 0, shipping an unsigned binary (a release is never blocked on signing setup). The secrets are MACOS_CERT_P12_BASE64, MACOS_CERT_PASSWORD, MACOS_SIGN_IDENTITY, MACOS_NOTARY_KEY_BASE64, MACOS_NOTARY_KEY_ID, MACOS_NOTARY_ISSUER_ID.
Signing imports the cert into a throwaway randomly-keyed keychain, signs with codesign --options runtime --timestamp --entitlements pyinstaller/entitlements.plist, verifies, then notarizes via xcrun notarytool submit … --wait. Because a one-file binary cannot be stapled, notarization publishes the ticket online and Gatekeeper validates on first run, so end users do not need xattr -d com.apple.quarantine.
pyinstaller/entitlements.plist enables the three hardened-runtime entitlements a PyInstaller one-file binary needs (com.apple.security.cs.allow-unsigned-executable-memory, …allow-jit, …disable-library-validation) so the unpacked CPython runtime can load and execute its embedded .so/.dylib files.
The "Build release notes from HISTORY.md" step extracts this version's section from HISTORY.md (everything between its ## X.Y.Z (…) header and the next ## header, with leading blank lines stripped) into release-notes.md. If there is no matching section it warns and the body is auto-generated only. The Release uses that section as body_path with generate_release_notes: true (the curated notes are prepended above GitHub's auto-generated "What's Changed" / compare link). Keep HISTORY.md entries headed exactly ## X.Y.Z (date) so the matcher (index($0, "## " ver " ") == 1) finds them.
The GitHub Release (softprops/action-gh-release@v3) attaches:
-
dist/*.whl,dist/*.tar.gz -
yacron2-linux-{amd64,arm64,i686,armv7,ppc64le,s390x,riscv64}(glibc) - the same seven arches with a
-muslsuffix, e.g.yacron2-linux-amd64-musl…yacron2-linux-riscv64-musl, plusyacron2-linux-armv6-musl(armv6 is musl-only) -
yacron2-macos-amd64,yacron2-macos-arm64 -
yacron2-windows-amd64.exe,yacron2-windows-arm64.exe
The download-artifact pattern yacron2-* must stay broad enough to match all of them: a too-narrow pattern silently drops artifacts it misses rather than erroring.
The official image is built and pushed by .github/workflows/docker.yml, built from the top-level Dockerfile. It runs in three modes:
-
Per-commit gate (
pushto any branch): builds all six release arches (linux/amd64,linux/arm64,linux/386,linux/arm/v7,linux/ppc64le,linux/s390x; everything butamd64via QEMU emulation) and does not push (taggedci-build). Catches arch-specificDockerfileor dependency breakage before a release. -
On release (invoked by
release.ymlviaworkflow_callwith the version): builds multi-archlinux/amd64,linux/arm64,linux/386,linux/arm/v7,linux/ppc64le,linux/s390xand pushes to GHCR asghcr.io/ptweezy/yacron2:<version>and:latest(and Docker Hub ifDOCKERHUB_USERNAME/DOCKERHUB_TOKENsecrets are set).workflow_callis used rather thanon: releasebecause a Release created by the defaultGITHUB_TOKENdoes not emit a triggeringrelease: publishedevent. -
Manual (
workflow_dispatch): (re)builds and pushes any existing release tag (defaults to the latest release), e.g. to backfill an image or retry a failed push.
The image build passes the computed version with --build-arg VERSION=X.Y.Z; a plain local docker build . leaves it empty and setuptools_scm reads the version from .git. See Production and Container Deployment.
The self-contained binaries are produced from pyinstaller/yacron2.spec. The spec analyzes the entry script pyinstaller/yacron2 (which simply calls yacron2.__main__:main) and emits a single-file console executable named yacron2 with strip=True, upx=False, debug=False, console=True. PyInstaller is pinned to 6.21.0 consistently across the release jobs and the local Dockerfile.
The version is baked in by installing the package under SETUPTOOLS_SCM_PRETEND_VERSION before running PyInstaller, so the bundled yacron2/version.py carries the release version (verified by the --version smoke test). PyInstaller is not a cross-compiler, so each architecture/libc is built on a matching native runner or container.
pyinstaller/Dockerfile builds a glibc binary reproducibly on ubuntu:24.04: it installs build deps and upx-ucl, uses pyenv to install CPython 3.13.14 with --enable-shared, creates a venv, pip install pyinstaller==6.21.0, installs the package, runs the entry script (python pyinstaller/yacron2 --version), runs pyinstaller pyinstaller/yacron2.spec, and smoke-tests dist/yacron2 --version.
pyinstaller/Makefile wraps that: make (target all) builds the image, copies dist/yacron2 out of the container, and runs dist/yacron2 --version.
The standalone binaries unpack their embedded runtime to a temp directory at startup; the temp directory must be writable and executable. See Installation and Troubleshooting and FAQ.
This wiki documents yacron2. See the README and the changelog.
yacron2 is a fork of gjcarneiro/yacron.
- Getting Started
- Configuration
- Job Behavior
- Integrations
- Reference and Development