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

on:
workflow_dispatch:

concurrency:
group: aep-python-release
cancel-in-progress: false

permissions:
contents: read

jobs:
verify:
if: github.repository == 'aep-foundation/aep-python'
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.release.outputs.tag }}
version: ${{ steps.release.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- uses: actions/setup-python@v7
with:
python-version: "3.14"

- uses: astral-sh/setup-uv@v10.0.1
with:
version: "0.11.8"
enable-cache: true
cache-dependency-glob: uv.lock

- name: Validate release
id: release
env:
GH_TOKEN: ${{ github.token }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
echo "Releases must run from main." >&2
exit 1
fi
version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "The project version must be a stable semantic version such as 0.1.0." >&2
exit 1
fi
tag="v$version"
if gh release view "$tag" >/dev/null 2>&1; then
echo "Release $tag already exists." >&2
exit 1
fi
if git rev-parse --verify "refs/tags/$tag" >/dev/null 2>&1; then
tag_commit=$(git rev-list -n 1 "$tag")
if [[ "$tag_commit" != "$GITHUB_SHA" ]]; then
echo "Tag $tag does not identify the selected main commit." >&2
exit 1
fi
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"

- run: uv sync --all-groups --locked --python 3.14

- name: Check out AEP specifications
uses: actions/checkout@v7
with:
repository: aep-foundation/aep-specs
path: .conformance/aep-specs

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true
working-directory: .conformance/aep-specs/ietf

- name: Check out Node.js reference implementation
uses: actions/checkout@v7
with:
repository: aep-foundation/aep-node
path: .interop/aep-node

- uses: actions/setup-node@v6
with:
node-version: 24

- run: corepack enable

- name: Install Node.js reference implementation
run: pnpm --dir .interop/aep-node install --frozen-lockfile

- name: Verify release
run: make verify conformance interoperability
env:
AEP_NODE_DIR: .interop/aep-node
AEP_PYTHON: python
AEP_SPECS_DIR: .conformance/aep-specs

- name: Upload distributions
uses: actions/upload-artifact@v7
with:
name: aep-python-distributions
path: dist/*
if-no-files-found: error
retention-days: 7

- name: Upload verification reports
uses: actions/upload-artifact@v7
with:
name: aep-python-release-reports
path: |
.conformance/reports/*.json
.interop/reports/aep-python-node-interoperability.json
if-no-files-found: error
retention-days: 7

release:
needs: verify
environment: release
permissions:
attestations: write
contents: write
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- uses: actions/setup-python@v7
with:
python-version: "3.14"

- uses: actions/download-artifact@v8
with:
name: aep-python-distributions
path: dist

- name: Download verification reports
uses: actions/download-artifact@v8
with:
name: aep-python-release-reports

- name: Create release tag
env:
TAG: ${{ needs.verify.outputs.tag }}
run: |
if git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag --annotate "$TAG" --message "AEP Python $TAG" "$GITHUB_SHA"
git push origin "$TAG"

- name: Attest distributions
uses: actions/attest-build-provenance@v4
with:
subject-path: dist/*

- name: Check PyPI version
id: registry
env:
VERSION: ${{ needs.verify.outputs.version }}
run: |
if curl --fail --silent --show-error \
"https://pypi.org/pypi/agent-enrollment-protocol/$VERSION/json" >/dev/null; then
echo "published=true" >> "$GITHUB_OUTPUT"
else
echo "published=false" >> "$GITHUB_OUTPUT"
fi

- name: Publish to PyPI
if: steps.registry.outputs.published != 'true'
uses: pypa/gh-action-pypi-publish@release/v1

- name: Verify PyPI consumer
run: ./scripts/verify-consumer.sh
env:
AEP_CONSUMER_SOURCE: registry
AEP_PYTHON: python
AEP_PYTHON_VERSION: ${{ needs.verify.outputs.version }}

- name: Publish GitHub release
run: >-
gh release create "${{ needs.verify.outputs.tag }}"
.conformance/reports/agent.json#aep-python-agent-conformance.json
.conformance/reports/platform.json#aep-python-platform-conformance.json
.conformance/reports/service.json#aep-python-service-conformance.json
.interop/reports/aep-python-node-interoperability.json#aep-python-node-interoperability.json
dist/*
--generate-notes
--title "AEP Python ${{ needs.verify.outputs.tag }}"
--verify-tag
env:
GH_TOKEN: ${{ github.token }}
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ Node.js SDK is checked out elsewhere.
See [`aep-specs`](https://github.com/aep-foundation/aep-specs) for the normative drafts, schemas,
registries, examples, and test vectors.

## Releases

Maintainers run the `Release` workflow from `main`. It verifies the package and a clean consumer,
runs shared conformance and Node.js interoperability, publishes through PyPI Trusted Publishing,
attests the distributions, and creates the matching tag and GitHub release with the verification
reports.

## Security

See [SECURITY.md](./SECURITY.md) for vulnerability reporting.
Expand Down
23 changes: 21 additions & 2 deletions scripts/verify-consumer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,27 @@ trap 'rm -rf "$consumer"' EXIT

python=${AEP_PYTHON:-"$repository/.venv/bin/python"}
"$python" -m venv "$consumer/.venv"
requirement=("$repository"/dist/agent_enrollment_protocol-*.whl)
"$consumer/.venv/bin/python" -m pip install --disable-pip-version-check "${requirement[@]}"
source=${AEP_CONSUMER_SOURCE:-wheel}
if [[ "$source" == "wheel" ]]; then
requirement=("$repository"/dist/agent_enrollment_protocol-*.whl)
"$consumer/.venv/bin/python" -m pip install --disable-pip-version-check "${requirement[@]}"
elif [[ "$source" == "registry" ]]; then
version=${AEP_PYTHON_VERSION:?AEP_PYTHON_VERSION is required for a registry consumer check}
requirement=("agent-enrollment-protocol==$version")
for attempt in {1..12}; do
if "$consumer/.venv/bin/python" -m pip install --disable-pip-version-check \
"${requirement[@]}"; then
break
fi
if [[ "$attempt" == 12 ]]; then
exit 1
fi
sleep 5
done
else
echo "AEP_CONSUMER_SOURCE must be wheel or registry." >&2
exit 1
fi
"$consumer/.venv/bin/python" - <<'PY'
from agent_enrollment_protocol import __version__
from importlib.metadata import version
Expand Down