Skip to content

Upgrade a Package

Target audience: Maintainers Goal: Update a StageX package to its latest version, rebuild, and verify reproducibility.

Upgrading is different from adding a new package -- you're modifying existing files, not creating new ones. The core work is updating the version and source hash in package.toml, then verifying the build still works. This guide walks through upgrading py-pygments from v2.18.0 to v2.20.0 as a real-world example.

Prerequisites

  • StageX repo cloned and on a branch based on staging:
    git clone https://codeberg.org/stagex/stagex.git
    git checkout staging
    git checkout -b my/upgrade-py-pygments
    
  • Familiarity with the StageX build system: see the Makefile Targets Reference and Package.toml Format
  • A working StageX build environment (see Build Locally)
  • Existing package directory located at packages/user/py-pygments/

When to Upgrade

Upgrade a package when one of these applies:

  • Security fix -- A CVE has been announced against the current version. py-pygments 2.18.0 has CVE-2026-4539: an inefficient regular expression in AdlLexer allowing ReDoS attacks, fixed in 2.20.0 (GHSA-5239-wwwm-4pmq).
  • Dependency requirement -- A package you want to add requires a newer version. Zensical (our SSG) requires pygments >= 2.20.
  • Feature need -- The new version adds functionality your project needs.

Always check whether the upgrade is compatible before making changes (see the Compatibility Checklist below).

1. Research the Upgrade

Before touching any files, gather information about the new version.

Find the current version

grep ^version packages/user/py-pygments/package.toml

Output: version = "2.18.0"

Find the latest version on PyPI

Use the PyPI JSON API:

curl -s https://pypi.org/pypi/pygments/json | python3 -m json.tool

Or query a specific version:

curl -s https://pypi.org/pypi/pygments/2.20.0/json | python3 -m json.tool

Extract the source hash

PyPI provides SHA256 digests for each source distribution:

import json, urllib.request
data = json.load(urllib.request.urlopen("https://pypi.org/pypi/pygments/2.20.0/json"))
for u in data['urls']:
    if u['packagetype'] == 'sdist':
        print(f"Hash: {u['digests']['sha256']}")
        print(f"URL: {u['url']}")

Output:

Hash: 6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f
URL: https://files.pythonhosted.org/packages/source/P/Pygments/pygments-2.20.0.tar.gz

Review the changelog for breaking changes

Visit the upstream changelog (e.g., https://pygments.org/docs/changes/) and check:

  • Python version requirement -- pygments 2.18.0 required >= 3.8; 2.20.0 requires >= 3.9
  • Removed APIs -- any functions or classes deprecated in previous versions
  • Build system changes -- did they switch from setuptools to hatchling, flit, or maturin?
  • New runtime dependencies -- any new COPY --from= lines needed in the Containerfile?
  • Check for CVEs -- search for the version range at GHSA or run:
    curl -s "https://api.github.com/advisories?ecosystem=PIP&severity=HIGH" | python3 -c \
      "import json,sys; [print(a['ghsa_id'],a['summary']) for a in json.load(sys.stdin) if 'pygments' in str(a).lower()]"
    

Check the release bot branch (if available)

StageX has a release bot that creates automated upgrade branches:

git fetch origin
git log --oneline origin/stagex-release-bot/update-version/py-pygments/2.20.0

Reviewing the bot's branch can confirm the exact changes needed and reveal any edge cases.

2. Make the Change

Edit package.toml

Open packages/user/py-pygments/package.toml and change only two fields:

-version = "2.18.0"
+version = "2.20.0"
-hash = "786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"
+hash = "6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"

The format, file, and mirrors fields all use template variables ({version}, {format}), so they adapt automatically -- no changes needed.

Check if the Containerfile needs changes

Review packages/user/py-pygments/Containerfile:

FROM stagex/pallet-cython AS build
COPY --from=stagex/user-py-hatchling . /
COPY --from=stagex/user-py-pathspec . /
COPY --from=stagex/user-py-trove-classifiers . /
COPY --from=stagex/user-py-pluggy . /
ARG VERSION
ADD fetch/py-pygments-${VERSION}.tar.gz .
WORKDIR /pygments-${VERSION}
RUN gpep517 build-wheel --wheel-dir .dist --output-fd 3 3>&1 >&2
RUN --network=none <<-EOF
    python -m installer -d /rootfs .dist/*.whl
    find /rootfs | grep -E "(/__pycache__$|\.pyc$|\.pyo$)" | xargs rm -rf
EOF
FROM stagex/core-filesystem AS package
COPY --from=build /rootfs /

This Containerfile uses ${VERSION} in both ADD and WORKDIR, so it works for any version without changes. The build dependencies (py-hatchling, py-pathspec, py-trove-classifiers, py-pluggy) are unchanged. No modifications needed.

If the Containerfile contained a hardcoded version (e.g., pygments-2.18.0), you would need to update each reference -- but StageX conventions avoid this pattern.

3. Compatibility Checklist

Run through these checks before building. Any failure here means the upgrade is riskier and may require additional work.

Check py-pygments 2.18.0 -> 2.20.0 Action if mismatched
Python version 3.8 -> 3.9 (StageX ships 3.13+) If new version requires a Python not yet in StageX, upgrade pallet-python first
Build system Still hatchling (unchanged) If changed to setuptools/flit/maturin, rewrite the Containerfile
Build dependencies hatchling, pathspec, trove-classifiers, pluggy (unchanged) Add/remove COPY --from= lines
Runtime dependencies None (unchanged) Add COPY --from= lines for new deps
API breakage Check upstream changelog for removed APIs Audit downstream packages that depend on this one
License BSD-2-Clause (unchanged) Update package.toml if changed

Python version bumps are the most common compatibility trap. The new version's requires_python field in setup.cfg, pyproject.toml, or the PyPI JSON response tells you the minimum supported Python. If StageX's Python is below that minimum, you must upgrade pallet-python first or skip the package upgrade.

4. Build and Verify

Fetch the new source tarball

python3 src/fetch.py py-pygments

This downloads py-pygments-2.20.0.tar.gz into the fetch/ directory.

Build the package

make user-py-pygments

If the build succeeds, the image digest is recorded in digests/user.txt.

Verify the digest

tail -1 digests/user.txt

Expected output (actual digest varies by run):

user-py-pygments sha256:abc123...

Verify reproducibility

StageX builds are designed to be deterministic. Rebuild with --no-cache and compare digests:

podman rmi stagex/user-py-pygments 2>/dev/null; make user-py-pygments
tail -1 digests/user.txt

The second build must produce the same digest as the first. If digests differ, the build is non-reproducible -- see Reproduce Builds for troubleshooting.

Test downstream packages

If other StageX packages depend on this one (py-zensical depends on py-pygments), rebuild them to confirm compatibility:

make user-py-zensical

5. Troubleshooting Common Upgrade Failures

Symptom Likely Cause Fix
ERROR: Package ... requires a different Python New version raised Python version floor Check requires_python in PyPI JSON; upgrade pallet-python if needed
Build system changed (e.g., setuptools to hatchling) Upstream changed pyproject.toml Rewrite Containerfile; might need different COPY --from= lines
ModuleNotFoundError for a new dependency Upstream added a runtime dependency Add COPY --from=stagex/user-py-<dep> . / to Containerfile
gpep517: not found or build backend errors Wrong build backend assumptions Check upstream's pyproject.toml [build-system] section
tar: Can't open Wrong WORKDIR path Extract the sdist: tar tzf fetch/py-pygments-${VERSION}.tar.gz \| head -1
Non-reproducible digest Timestamps, network access, or randomized output Ensure --timestamp 1 in build, --network=none during build step, no non-deterministic tools
ADD failed: file not found Fetch step skipped or failed Run python3 src/fetch.py py-pygments first
Package builds but downstream tests fail API/behavior change in new version Audit changelog for breaking changes; may need to pin version or adapt downstream packages

Rollback

If the upgrade breaks downstream packages or introduces issues, revert with git:

git checkout -- packages/user/py-pygments/package.toml
git checkout -- packages/user/py-pygments/Containerfile  # if changed

The old digest in digests/user.txt is preserved in git history -- restore it with:

git checkout HEAD~1 -- digests/user.txt

Then rebuild the previously-working version.

6. Commit and Submit

StageX uses conventional commits. The upgrade changes only package.toml (and possibly Containerfile), so the commit is focused:

git add packages/user/py-pygments/package.toml
git commit -m "feat(user): upgrade py-pygments v2.18.0 -> v2.20.0

Required by zensical (>= 2.20). Also fixes CVE-2026-4539 (ReDoS in AdlLexer).
Python requirement bumped to >= 3.9 (StageX ships 3.13+, no impact).
Build deps (hatchling, pathspec, trove-classifiers, pluggy) unchanged."

Push the branch and open a pull request against staging:

git push origin my/upgrade-py-pygments

See Also