Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ on:
- "examples/**"
- "compatibility/**"
- "docs/releasing.md"
- "requirements/release.in"
- "requirements/release.txt"
- ".github/workflows/package.yml"
push:
branches:
Expand Down Expand Up @@ -98,10 +100,46 @@ jobs:
mkdir -p dist

- name: Install build and validation tools
run: python -m pip install --upgrade build twine
run: python -m pip install --require-hashes --no-deps -r requirements/release.txt

- name: Build sdist and wheel
run: python -m build --sdist --wheel --outdir dist
env:
SOURCE_DATE_EPOCH: ${{ github.event.head_commit.timestamp || '0' }}
run: |
rm -rf dist-a dist-b "$RUNNER_TEMP/base-cli-build-a" "$RUNNER_TEMP/base-cli-build-b"
mkdir -p dist-a dist-b
mkdir -p "$RUNNER_TEMP/base-cli-build-a" "$RUNNER_TEMP/base-cli-build-b"
git archive "$GITHUB_SHA" | tar -x -C "$RUNNER_TEMP/base-cli-build-a"
git archive "$GITHUB_SHA" | tar -x -C "$RUNNER_TEMP/base-cli-build-b"
(cd "$RUNNER_TEMP/base-cli-build-a" && python -m build --sdist --wheel --outdir "$GITHUB_WORKSPACE/dist-a")
(cd "$RUNNER_TEMP/base-cli-build-b" && python -m build --sdist --wheel --outdir "$GITHUB_WORKSPACE/dist-b")
python - <<'PY'
import gzip
import os
import tarfile
from pathlib import Path

epoch = int(os.environ["SOURCE_DATE_EPOCH"])
for directory in (Path("dist-a"), Path("dist-b")):
for path in directory.glob("*.tar.gz"):
with tarfile.open(path, "r:gz") as source:
members = [(member, source.extractfile(member).read() if member.isfile() else None) for member in source.getmembers()]
temporary = path.with_suffix(path.suffix + ".tmp")
with temporary.open("wb") as raw:
with gzip.GzipFile(filename=path.name, mode="wb", fileobj=raw, mtime=epoch) as compressed:
with tarfile.open(fileobj=compressed, mode="w|") as target:
for member, payload in sorted(members, key=lambda item: item[0].name):
member.mtime = epoch
member.uid = member.gid = 0
member.uname = member.gname = ""
member.pax_headers = {}
target.addfile(member, None if payload is None else __import__("io").BytesIO(payload))
temporary.replace(path)
PY
(cd dist-a && sha256sum * | sort) > dist-a.SHA256SUMS
(cd dist-b && sha256sum * | sort) > dist-b.SHA256SUMS
diff -u dist-a.SHA256SUMS dist-b.SHA256SUMS
cp dist-a/* dist/

- name: Validate artifact contents and metadata
run: python scripts/validate_package_artifact.py dist
Expand Down
20 changes: 20 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ release's assets with `--clobber` instead of creating a second release.

## Independent verification

The release job uses the reviewed, hash-locked toolchain in
`requirements/release.txt`; it does not install mutable latest build or
publishing packages. The lock includes transitive release-path dependencies,
and the PEP 517 backend is pinned to the same setuptools and wheel versions in
`pyproject.toml`. The job builds two clean source archives with the same
`SOURCE_DATE_EPOCH` and rejects digest drift before publishing the reviewed
artifacts.

To intentionally refresh the toolchain, edit the four direct requirements in
`requirements/release.in` and regenerate the lock with:

```bash
uv pip compile requirements/release.in --python-version 3.13 \
--generate-hashes --output-file requirements/release.txt
```

Review the complete diff, run the package workflow on a pull request, and only
then merge the update. Runtime dependency windows are deliberately not tied to
this release-only toolchain.

Download the release metadata artifact from the successful Package workflow
run (the artifact is named `base-cli-release-metadata-<run-id>`), alongside
the wheel or sdist you downloaded from PyPI:
Expand Down
8 changes: 6 additions & 2 deletions lib/python/base_cli/_private_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,16 @@ def _sync_directory(parent_fd: int) -> None:
def _replace_with_retry(source: Path, destination: Path) -> None:
"""Replace a private file, tolerating transient Windows sharing races."""

attempts = 1 if os.name != "nt" else 10
# Antivirus/indexer handles and concurrent writers can hold the destination
# briefly on Windows. Use a bounded, linear backoff long enough for those
# transient sharing violations without making a persistent permission error
# unbounded.
attempts = 1 if os.name != "nt" else 50
for attempt in range(attempts):
try:
os.replace(source, destination)
return
except PermissionError:
if attempt == attempts - 1:
raise
time.sleep(0.001 * (attempt + 1))
time.sleep(0.005 * (attempt + 1))
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[build-system]
# Keep the generated core metadata at 2.3 until the pinned publisher action's
# bundled twine/pkginfo stack accepts Metadata-Version 2.4.
requires = ["setuptools>=68,<77"]
requires = ["setuptools==75.1.0", "wheel==0.44.0"]
build-backend = "setuptools.build_meta"

[project]
Expand Down
4 changes: 4 additions & 0 deletions requirements/release.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build==1.2.2
setuptools==75.1.0
twine==5.1.1
wheel==0.44.0
Loading
Loading