Script and action to update dependencies in your pyproject.toml to match the version numbers from the accompanying uv.lock file.
The most basic usage is to check out the repository you want to modify, run uv lock --upgrade, and then run this action to sync the version changes to the project's pyproject.toml.
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.1.0
- run: uv lock --upgrade
- uses: virtlink/sync-pyproject@v1.0.1If your pyproject.toml and uv.lock files live in a subdirectory, pass the optional directory input. If omitted, the action uses the current directory (.).
- uses: virtlink/sync-pyproject@v1.0.1
with:
directory: packages/example| Name | Required | Default | Description |
|---|---|---|---|
directory |
No | . |
Directory containing both pyproject.toml and uv.lock. |
The action installs its own Python dependencies and runs on Python 3.11+.
This script reads the uv.lock file and changes the dependency versions in the pyproject.toml file to match. It updates:
project.dependenciesproject.optional-dependencies.<group>
For each dependency with a simple version comparator (for example, >=1.2.0, ~=2.1, ==3.0.0), the tool preserves the existing operator (>=, ~=, ==, etc.) and replaces only the version token with the lockfile version. Extras and environment markers are preserved. For multi-specifier constraints (for example, >=1.0,<2.0), only the first comparator segment that includes both an operator and a version token is updated.
Dependencies without explicit version constraints, complex constraints that cannot be safely rewritten, and dependencies missing in uv.lock are not changed. Dependency names are matched using UV-compatible normalization, which means that matching is case-insensitive and treats -, _, and . equivalently for distribution names.
Formatting and comments of your pyproject.toml is preserved.
To run the script and see help information:
uv run syncpyproject.py --help- Python
- UV (how to install)
-
First, use UV to update the
uv.lockfile for your project:uv lock --upgrade --script syncpyproject.py
-
Then, run the script. Either specify the directory where your
pyproject.tomlanduv.lockfiles live, or specify nothing and the script will assume the current directory.uv run syncpyproject.py
By default, the script will warn when a dependency version constraint could not be updated. Use --quiet (-q) to suppress warnings and informational output, or --verbose (-v) to prints each dependency that is updated, including old and new version token.
On error, the script exits with a non-zero error code and doesn't change pyproject.toml.
The test suite is run through the dedicated test script, which provides its own isolated environment (including pytest) so that the main script does not need pytest installed:
uv run --locked syncpyproject_test.pyArguments after the script name are forwarded to pytest, for example to run a single test or increase verbosity:
uv run syncpyproject_test.py -v
uv run syncpyproject_test.py tests/test_sync.py::SyncCommandTestsReleases are cut from main with the release.py helper and the Release GitHub Actions workflow.
Before releasing, list every notable change since the last release under the ## [Unreleased] heading in CHANGELOG.md. This section is used as the body of the Git tag. Your working tree must be clean and main must be in sync with origin/main; the script enforces this and aborts otherwise.
To cut a release, run:
uv run --locked release.py v1.0.1The script verifies a clean, pushed working tree, updates every virtlink/sync-pyproject@v1.0.1… reference in README.md to the new version (e.g. @v1.0.1), rotates the changelog's ## [Unreleased] section to ## [1.0.1] - YYYY-MM-DD and starts a fresh ## [Unreleased] heading, commits both changes, creates an annotated tag v1.0.1 whose message combines Release v1.0.1 with the unreleased changelog notes, and pushes the commit and the tag to origin.
Pushing the tag triggers the Release workflow, which re-runs the tests and yamllint, creates a GitHub Release with auto-generated notes, and moves the v1 tag to the same commit so that both @v1 and @v1.0.1 resolve for consumers.
Pre-release versions such as v1.0.1-beta.1 create a GitHub pre-release but do not move the v1 tag, so @v1 keeps pointing at the last stable release.
The release script ships with its own pytest suite — the tests live in release.py itself. Run them directly with uv, which reads the script's inline dependency metadata (including pytest) via --with-requirements:
uv run --with-requirements release.py pytest release.pyArguments after pytest are forwarded as-is, for example to run a single test or increase verbosity:
uv run --with-requirements release.py pytest release.py::test_parse_version_strips_leading_v
uv run --with-requirements release.py pytest release.py -vThe release script is formatted and linted with ruff and type-checked with ty:
uvx ruff check release.py
uvx ruff format --check release.py
uvx --with-requirements release.py --with ty ty check release.pyTo auto-format, replace format --check with format.
release.py and release.py.lock are self-contained and can be copied into another GitHub action's repository with no changes other than the REPOSITORY constant at the top of release.py.
Copyright (C) 2026 Daniel A. A. Pelsmaeker
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.