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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ jobs:
- name: Checkout Pexrc
uses: actions/checkout@v6
- name: Build pexrc binary for all targets.
run: cargo run -p package -- --profile release -o dist ${{ matrix.targets }}
run: cargo run -p package -- --color always --profile release -o dist ${{ matrix.targets }}
- name: Generate Release Hashes Manifest.
run: uv run dev-cmd generate-release-hashes -- -i dist

tests:
name: "${{ matrix.name }} tests"
Expand Down
142 changes: 142 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
name: Release
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
workflow_dispatch:
inputs:
tag:
description: The tag to manually run a deploy for.
required: true
defaults:
run:
shell: bash
env:
PEXRC_INSTALL_TOOLS: 1

jobs:
setup:
name: Check GitHub Organization
if: github.repository_owner == 'pex-tool'
runs-on: ubuntu-slim
steps:
- name: Noop
if: false
run: echo "This is a dummy step that will never run."

cross-build:
name: "Cross-build pexrc"
needs: setup
runs-on: ubuntu-24.04-arm
# N.B.: We break these up just to save wall time; they all can be built on 1 machine in ~40
# minutes; this gets us to a ~20 minute long-pole.
strategy:
matrix:
include:
- artifact-name: pexrc-shard1
targets: >-
--target aarch64-unknown-linux-gnu
--target aarch64-unknown-linux-musl
- artifact-name: pexrc-shard2
targets: >-
--target armv7-unknown-linux-gnueabihf
--target powerpc64le-unknown-linux-gnu
- artifact-name: pexrc-shard3
targets: >-
--target riscv64gc-unknown-linux-gnu
--target s390x-unknown-linux-gnu
- artifact-name: pexrc-shard4
targets: >-
--target x86_64-unknown-linux-gnu
--target x86_64-unknown-linux-musl
- artifact-name: pexrc-shard5
targets: >-
--target aarch64-apple-darwin
--target x86_64-apple-darwin
- artifact-name: pexrc-shard6
targets: >-
--target aarch64-pc-windows-gnullvm
--target x86_64-pc-windows-gnu
steps:
- name: Checkout Pexrc
uses: actions/checkout@v6
- name: Build pexrc binary for all targets.
run: cargo run -p package -- --color always -o dist --profile release ${{ matrix.targets }}
- name: Upload pexrc artifacts
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact-name }}
path: dist/pexrc*

determine-tag:
name: Determine the release tag to operate against.
needs: setup
runs-on: ubuntu-slim
outputs:
release-tag: ${{ steps.determine-tag.outputs.release-tag }}
release-version: ${{ steps.determine-tag.outputs.release-version }}
steps:
- name: Determine Tag
id: determine-tag
run: |
if [[ -n "${{ github.event.inputs.tag }}" ]]; then
RELEASE_TAG=${{ github.event.inputs.tag }}
else
RELEASE_TAG=${GITHUB_REF#refs/tags/}
fi
if [[ "${RELEASE_TAG}" =~ ^v[0-9]+.[0-9]+.[0-9]+$ ]]; then
echo "release-tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
echo "release-version=${RELEASE_TAG#v}" >> $GITHUB_OUTPUT
else
echo "::error::Release tag '${RELEASE_TAG}' must match 'v\d+.\d+.\d+'."
exit 1
fi

github-release:
name: "Create Github Release"
needs:
- cross-build
- determine-tag
runs-on: ubuntu-24.04-arm
environment: Release
permissions:
id-token: write
attestations: write
artifact-metadata: write
contents: write
discussions: write
steps:
- name: Checkout Pexrc
uses: actions/checkout@v6
- uses: actions/download-artifact@v7
with:
merge-multiple: true
- name: Generate Release Hashes Manifest.
run: uv run dev-cmd generate-release-hashes -- -i dist -o dist/hashes.md
- name: Generate pexrc ${{ needs.determine-tag.outputs.release-tag }} artifact attestations
uses: actions/attest@v4
with:
subject-path: dist/pexrc*
- name: Prepare Changelog
id: prepare-changelog
uses: a-scie/actions/changelog@v1.6
with:
changelog-file: ${{ github.workspace }}/CHANGES.md
version: ${{ needs.determine-tag.outputs.release-version }}
- name: Append Hashes to Changelog
run: |
changelog_tmp="$(mktemp)"
cat "${{ steps.prepare-changelog.outputs.changelog-file }}" <(echo '***') dist/hashes.md \
> "${changelog_tmp}"
mv "${changelog_tmp}" "${{ steps.prepare-changelog.outputs.changelog-file }}"
- name: Create ${{ needs.determine-tag.outputs.release-tag }} Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.determine-tag.outputs.release-tag }}
name: pexrc ${{ needs.determine-tag.outputs.release-version }}
body_path: ${{ steps.prepare-changelog.outputs.changelog-file }}
draft: false
prerelease: false
files: dist/pexrc*
fail_on_unmatched_files: true
discussion_category_name: Announcements
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Release Notes

## 0.1.0

Initial release.

47 changes: 47 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Release Process

## Preparation

### Version Bump and Changelog

1. Bump the version in [`Cargo.toml`](Cargo.toml).
2. Run `cargo run -p package` to update [`Cargo.lock`](Cargo.lock) with the new version
and as a sanity check on the state of the project.
3. Update [`CHANGES.md`](CHANGES.md) with any changes that are likely to be useful to consumers.
4. Open a PR with these changes and land it on https://github.com/pex-tool/pexrc main.

## Release

### Push Release Tag

Sync a local branch with https://github.com/pex-tool/pexrc main and confirm it has the version bump
and changelog update as the tip commit:

```console
:; git log --stat -1 HEAD
commit 4fc958e47826392f9db1f55d227b047e7c701e32 (HEAD -> release/setup)
Author: John Sirois <john.sirois@gmail.com>
Date: Thu Mar 26 08:01:21 2026 -0700

Prepare the 0.1.0 release.

.github/workflows/ci.yml | 2 +-
.github/workflows/release.yml | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++
CHANGES.md | 6 +++
RELEASE.md | 44 ++++++++++++++++
crates/package/src/main.rs | 4 +-
pyproject.toml | 16 ++++--
scripts/generate-release-hashes.py | 79 ++++++++++++++++++++++++++++
7 files changed, 284 insertions(+), 7 deletions(-)
```

Tag the release as `v<version>` and push the tag to https://github.com/pex-tool/pexrc main:
```console
$ git tag --sign -am 'Release 0.1.0' v0.1.0
$ git push --tags https://github.com/pex-tool/pexrc HEAD:main
```

The release is automated and will create a GitHub Release page at
[https://github.com/pex-tool/pexrc/releases/tag/v&lt;version&gt;](
https://github.com/pex-tool/pexrc/releases) with binaries for Linux, Mac and Windows.

4 changes: 2 additions & 2 deletions crates/package/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ fn main() -> anyhow::Result<()> {
fs::create_dir_all(&dist_dir)?;
let count = built.len();
anstream::println!(
"Built {count} {binaries} to {dist_dir}:",
"Packaging {count} {binaries} in {dist_dir}:",
binaries = if count == 1 { "binary" } else { "binaries" },
dist_dir = dist_dir.display()
);
Expand All @@ -229,7 +229,7 @@ fn main() -> anyhow::Result<()> {
fs::remove_file(&dst)?;
}
let (size, fingerprint) = hash_file(src)?;
platform::link_or_copy(src, &dst)?;
platform::symlink_or_link_or_copy(src, &dst, true)?;
fs::write(
dst.with_added_extension("sha256"),
format!(
Expand Down
16 changes: 12 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,24 @@ check-fmt = ["ruff", "format", "--diff"]
lint = ["ruff", "check", "--fix"]
check-lint = ["ruff", "check"]

[tool.dev-cmd.commands.type-check.factors]
[tool.dev-cmd.commands.type-check-pexrc.factors]
py = "The Python version to type check in <major>.<minor> form; i.e.: 3.13."
[tool.dev-cmd.commands.type-check]
[tool.dev-cmd.commands.type-check-pexrc]
args = [
"mypy",
"--python-version", "{-py:{markers.python_version}}",
"--cache-dir", ".mypy_cache_{markers.python_version}",
"python",
]

[tool.dev-cmd.commands.type-check-scripts]
args = ["mypy", "scripts"]

[tool.dev-cmd.commands.generate-release-hashes]
when = "python_version >= '3.10'"
args = ["scripts/generate-release-hashes.py"]
accepts-extra-args = true

[tool.dev-cmd.commands.test]
args = ["python/scripts/run-tests.py"]
accepts-extra-args = true
Expand All @@ -52,14 +60,14 @@ steps = [
"lint",
# Parallelizing the type checks is safe (they don't modify files), and it nets a ~3x
# speedup over running them all serially.
["type-check-py3.{8..14}"],
["type-check-pexrc-py3.{8..14}", "type-check-scripts"],
"test",
]

[tool.dev-cmd.tasks.ci]
description = "Runs all checks used for CI."
# None of the CI checks modify files; so they can all be run in parallel which nets a ~1.5x speedup.
steps = [["check-fmt", "check-lint", "type-check", "test"]]
steps = [["check-fmt", "check-lint", "type-check-pexrc", "type-check-scripts", "test"]]

[tool.dev-cmd]
default = "checks"
Expand Down
79 changes: 79 additions & 0 deletions scripts/generate-release-hashes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright 2026 Pex project contributors.
# SPDX-License-Identifier: Apache-2.0

import hashlib
import io
import os.path
import sys
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Iterator, TextIO


class HashVerifyError(Exception):
pass


def generate_release_hashes(releases_dir: Path, output: TextIO) -> None:
print("|file|sha256|size|", file=output)
print("|----|------|----|", file=output)
for hash_file in sorted(releases_dir.glob("*.sha256")):
expected_sha256, file_name = hash_file.read_text().split(" ", maxsplit=1)
path = releases_dir / file_name.lstrip("*")

digest = hashlib.sha256()
with path.open("rb") as fp:
for chunk in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b""):
digest.update(chunk)
actual_sha256 = digest.hexdigest()
if actual_sha256 != expected_sha256:
raise HashVerifyError(
f"Invalid sha256 hash for {path}:\n"
f"expected: {expected_sha256}\n"
f"found: {actual_sha256}"
)

print(f"|{path.name}|{actual_sha256}|{os.path.getsize(path)}|", file=output)


@contextmanager
def output(output_file: Path | None = None) -> Iterator[TextIO]:
if output_file:
with output_file.open("w") as fp:
yield fp
else:
yield sys.stdout


def main() -> Any:
parser = ArgumentParser(
description="Generate a markdown table or release artifact sizes and hashes",
formatter_class=ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-i",
"--releases-dir",
dest="releases_dir",
default="dist",
type=Path,
help="The directory containing the releases to hash.",
)
parser.add_argument(
"-o",
"--output-file",
dest="output_file",
default=None,
type=Path,
help="A file path to emit the markdown table to. If not specified, defaults to stdout.",
)
options = parser.parse_args()
with output(options.output_file) as fp:
generate_release_hashes(options.releases_dir, fp)


if __name__ == "__main__":
try:
sys.exit(main())
except HashVerifyError as e:
sys.exit(str(e))