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
256 changes: 256 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
name: Release

on:
push:
tags:
- "*"
workflow_dispatch:
inputs:
release_tag:
description: "Existing release tag to publish, for example 0.5.0"
required: true
type: string

concurrency:
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }}
cancel-in-progress: false

env:
PYTHON_VERSION: "3.12.6"

jobs:
resolve_release:
name: Resolve Release
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.release.outputs.release_tag }}
package_version: ${{ steps.release.outputs.package_version }}
release_sha: ${{ steps.release.outputs.release_sha }}
steps:
- name: Check out release ref
uses: actions/[email protected]
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Resolve tag and package version
id: release
shell: bash
run: |
set -euo pipefail

if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
release_tag="${{ inputs.release_tag }}"
else
release_tag="${{ github.ref_name }}"
fi

if [[ ! "$release_tag" =~ ^([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
echo "Release tags must use canonical X.Y.Z format." >&2
exit 1
fi

# Python script to parse version from pyproject.toml
package_version="$release_tag"
declared_version="$(python - <<'PY'
import tomllib
from pathlib import Path
with Path("pyproject.toml").open("rb") as handle:
print(tomllib.load(handle)["project"]["version"])
PY
)"

runtime_version="$(python - <<'PY'
import sys
from pathlib import Path
sys.path.insert(0, str(Path("src").resolve()))
import signify
print(signify.__version__)
PY
)"

if [ "$declared_version" != "$package_version" ]; then
echo "pyproject.toml version '$declared_version' does not match tag '$release_tag'." >&2
exit 1
fi

if [ "$runtime_version" != "$package_version" ]; then
echo "Runtime version '$runtime_version' does not match tag '$release_tag'." >&2
exit 1
fi

{
echo "release_tag=$release_tag"
echo "package_version=$package_version"
echo "release_sha=$(git rev-parse HEAD)"
} >> "$GITHUB_OUTPUT"

assert_ci_green:
name: Require Green Tests
needs: resolve_release
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: Wait for Tests workflow success on release commit
uses: actions/github-script@v7
env:
TARGET_SHA: ${{ needs.resolve_release.outputs.release_sha }}
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const workflowId = "test.yaml";
const targetSha = process.env.TARGET_SHA;
const maxAttempts = 60;
const delayMs = 30000;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const response = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: workflowId,
head_sha: targetSha,
per_page: 100,
});

const runs = response.data.workflow_runs || [];
const successfulRun = runs.find((run) => run.conclusion === "success");
if (successfulRun) {
core.info(`Found successful Tests workflow run ${successfulRun.id} for ${targetSha}.`);
return;
}

const pendingRun = runs.find((run) => run.status !== "completed");
if (!pendingRun && runs.length > 0) {
const conclusions = runs.map((run) => `${run.id}:${run.conclusion}`).join(", ");
core.setFailed(`No successful Tests workflow run found for ${targetSha}. Seen runs: ${conclusions}`);
return;
}

if (attempt === maxAttempts) {
core.setFailed(`Timed out waiting for a successful Tests workflow run for ${targetSha}.`);
return;
}

core.info(`Attempt ${attempt}/${maxAttempts}: waiting for Tests workflow success on ${targetSha}.`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}

build_dist:
name: Build Release Artifacts
needs:
- resolve_release
- assert_ci_green
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out release tag
uses: actions/[email protected]
with:
ref: ${{ needs.resolve_release.outputs.release_tag }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: pip
cache-dependency-path: |
pyproject.toml

- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "0.6.14"

- name: Build and validate distributions
run: |
make dist-check

- name: Smoke install wheel
shell: bash
run: |
set -euo pipefail
python -m venv smoke-venv
./smoke-venv/bin/python -m pip install --upgrade pip
./smoke-venv/bin/python -m pip install --no-deps dist/*.whl
./smoke-venv/bin/python -c "import signify; print(signify.__version__)"

- name: Upload release artifacts
uses: actions/upload-artifact@v4
with:
name: release-dist-${{ needs.resolve_release.outputs.release_tag }}
path: dist/*
if-no-files-found: error

publish_pypi:
name: Publish To PyPI
needs:
- resolve_release
- build_dist
runs-on: ubuntu-latest
permissions:
contents: read
env:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
steps:
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
name: release-dist-${{ needs.resolve_release.outputs.release_tag }}
path: dist

- name: Check PyPI token availability
shell: bash
run: |
set -euo pipefail
if [ -z "${PYPI_API_TOKEN:-}" ]; then
echo "Missing repository secret PYPI_API_TOKEN." >&2
echo "Add a project-scoped PyPI token to repository secrets before publishing releases." >&2
exit 1
fi

- name: Publish package distributions
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
password: ${{ secrets.PYPI_API_TOKEN }}

publish_github_release:
name: Publish GitHub Release
needs:
- resolve_release
- publish_pypi
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out release tag
uses: actions/[email protected]
with:
ref: ${{ needs.resolve_release.outputs.release_tag }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Extract changelog section
run: |
python scripts/extract_changelog_section.py \
--version "${{ needs.resolve_release.outputs.package_version }}" \
--input docs/changelog.md \
--output release-notes.md

- name: Create or update GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.resolve_release.outputs.release_tag }}
name: ${{ needs.resolve_release.outputs.release_tag }}
body_path: release-notes.md
2 changes: 2 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
branches:
- 'main'
- 'development'
tags:
- '*'
pull_request:
workflow_dispatch:

Expand Down
30 changes: 28 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ INTEGRATION_WORKERS ?= 2
INTEGRATION_DIST ?= loadscope
INTEGRATION_TARGETS ?= tests/integration

.PHONY: help sync test test-fast test-ci test-integration test-integration-ci test-integration-parallel test-integration-parallel-ci build release docs clean
.PHONY: help sync test test-fast test-ci test-integration test-integration-ci test-integration-parallel test-integration-parallel-ci build dist-check release-patch release-minor release-major release-bump docs clean guard-clean-worktree

help: ## Show available maintainer tasks
@awk 'BEGIN {FS = ":.*## "}; /^[a-zA-Z0-9_-]+:.*## / {printf "%-18s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
Expand Down Expand Up @@ -51,11 +51,37 @@ test-integration-parallel-ci: ## Run integration tests in parallel with CI-frien
build: ## Build source and wheel distributions
@$(UV_CACHE) $(UV) build

release: clean build ## Build release artifacts and validate them with twine
dist-check: clean build ## Build release artifacts and validate them with twine
@$(UV_CACHE) $(UV_ENV) $(UV) run --with twine twine check dist/*

release-patch: ## Prepare and commit a patch release
@$(MAKE) release-bump BUMP=patch

release-minor: ## Prepare and commit a minor release
@$(MAKE) release-bump BUMP=minor

release-major: ## Prepare and commit a major release
@$(MAKE) release-bump BUMP=major

release-bump: guard-clean-worktree
@VERSION_BEFORE=$$($(UV_CACHE) $(UV_ENV) $(UV) run python -c "import tomllib; from pathlib import Path; print(tomllib.load(Path('pyproject.toml').open('rb'))['project']['version'])"); \
$(UV) version --bump $(BUMP); \
VERSION_AFTER=$$($(UV_CACHE) $(UV_ENV) $(UV) run python -c "import tomllib; from pathlib import Path; print(tomllib.load(Path('pyproject.toml').open('rb'))['project']['version'])"); \
echo "Preparing release $$VERSION_AFTER from $$VERSION_BEFORE"; \
$(UV_CACHE) $(UV) lock; \
$(UV_CACHE) $(UV_ENV) $(UV) run --group dev towncrier build --yes --version "$$VERSION_AFTER"; \
git add -A pyproject.toml uv.lock docs/changelog.md newsfragments src/signify/__init__.py; \
git commit -m "chore(release): $$VERSION_AFTER"

docs: ## Build the Sphinx documentation
@./venv/bin/python -m sphinx -b html docs docs/_build/html

guard-clean-worktree:
@if [ -n "$$(git status --porcelain)" ]; then \
echo "Release preparation requires a clean git worktree."; \
git status --short; \
exit 1; \
fi

clean: ## Remove build, docs, and test artifacts
@rm -rf build dist docs/_build .pytest_cache .ruff_cache .uv-cache src/signifypy.egg-info
6 changes: 4 additions & 2 deletions docs/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ This documentation set serves two audiences:
- contributors who need the generated API reference for the underlying modules

Start with :doc:`maintainer_features` for the current request families, route
ownership, workflow notes, and test coverage. Then use the API reference pages
to drill down into the implementation modules.
ownership, workflow notes, and test coverage. Use :doc:`releasing` for the
maintained PyPI release workflow and :doc:`changelog` for versioned release
notes. Then use the API reference pages to drill down into the implementation
modules.
33 changes: 33 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Changelog

This page tracks SignifyPy release notes. New release entries are generated
from Towncrier fragments during release preparation.

<!-- towncrier release notes start -->

## 0.4.1

### Fixed

- Added boot agent validation on connect. ([#137](https://github.com/WebOfTrust/signifypy/pull/137))
- Fixed Randy (random) key manager implementation ([#138](https://github.com/WebOfTrust/signifypy/pull/138))


## 0.4.0

- Added maintained client accessors for config reads, external request signing,
passcode-save and passcode-delete helpers, and the remaining compatibility
wrappers needed to close the current parity stack.
- Expanded test coverage across fast suites and live integration workflows for
provisioning, identifier rename compatibility, multisig choreography, and
credential exchange paths.
- Refreshed maintainer documentation, parity planning, and harness guidance so
the documented client surface better matches the code and tests.

## 0.1.1

- Published a maintenance follow-up to the initial SignifyPy release line.

## 0.1.0

- Published the first public SignifyPy package release.
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Welcome to Signifypy's documentation!

README
maintainer_features
releasing
changelog

API Reference
=============
Expand Down
Loading
Loading