Skip to content

Commit cb3e220

Browse files
authored
Merge pull request #122 from ai-agent-assembly/v0.0.1/AAASM-2956/fix/version_badges
[AAASM-2956] 🐛 (readme): Add GitHub release badge + cut python-sdk GitHub Release
2 parents a9a7f2c + 645b1d9 commit cb3e220

5 files changed

Lines changed: 244 additions & 5 deletions

File tree

.github/scripts/pep440-to-tag.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Convert a PEP 440 version string to the canonical SemVer release tag.
4+
# Inverse of tag-to-pep440.sh — used by release-python.yml to cut the
5+
# python-sdk's own GitHub Release at the published version.
6+
#
7+
# Examples:
8+
# 0.0.1 -> v0.0.1
9+
# 0.0.1a8 -> v0.0.1-alpha.8
10+
# 0.0.1b2 -> v0.0.1-beta.2
11+
# 0.0.1rc3 -> v0.0.1-rc.3
12+
#
13+
# Usage (CLI):
14+
# .github/scripts/pep440-to-tag.sh 0.0.1b1
15+
#
16+
# Usage (library — preferred from CI/workflows):
17+
# source .github/scripts/pep440-to-tag.sh
18+
# tag="$(pep440_to_tag "0.0.1b1")"
19+
#
20+
# This is the single source of truth for the reverse conversion. The
21+
# release-python.yml `resolve` job sources this file so the workflow and
22+
# the AAASM-2956 fixture suite exercise the exact same code path.
23+
#
24+
# Owner: python-sdk release pipeline (AAASM-2956).
25+
26+
# Conversion function. Reads the PEP 440 version from $1 and prints the
27+
# SemVer tag (with a leading `v`) to stdout. Returns non-zero only if the
28+
# input is empty. `.post`/`.dev` suffixes are not valid SemVer pre-release
29+
# identifiers in this scheme, so they are rejected — those are PyPI-only
30+
# republish forms that do not correspond to a new GitHub Release tag.
31+
pep440_to_tag() {
32+
local version="${1-}"
33+
if [[ -z "$version" ]]; then
34+
echo "pep440_to_tag: missing version argument" >&2
35+
return 1
36+
fi
37+
# Strip any leading `v` the caller may have passed by mistake.
38+
local stripped="${version#v}"
39+
# Expand the PEP 440 pre-release shorthand (a/b/rc) back to the
40+
# hyphenated SemVer form. Anchored on the digits immediately following
41+
# the marker so we never touch the base x.y.z component.
42+
local tag
43+
tag="$(printf '%s\n' "$stripped" | sed -E 's/a([0-9]+)$/-alpha.\1/; s/b([0-9]+)$/-beta.\1/; s/rc([0-9]+)$/-rc.\1/')"
44+
printf 'v%s\n' "$tag"
45+
}
46+
47+
# When executed directly (not sourced), behave as a CLI: take the version
48+
# as $1 and print the converted tag. When sourced, define the function
49+
# only and let the caller drive it.
50+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
51+
set -euo pipefail
52+
pep440_to_tag "${1-}"
53+
fi
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Unit tests for `.github/scripts/pep440-to-tag.sh`.
4+
#
5+
# Exercises the conversion function that release-python.yml's `resolve`
6+
# job sources to turn a published PEP 440 version back into the canonical
7+
# SemVer release tag the create-github-release job cuts. Because the
8+
# workflow sources the same file, these tests cover the actual code path
9+
# CI runs, not a copy of the regex.
10+
#
11+
# Run locally:
12+
# bash .github/scripts/test_pep440_to_tag.sh
13+
#
14+
# Run from CI: see .github/workflows/release-python-conversion-test.yml.
15+
#
16+
# Exits 0 when every fixture passes, 1 otherwise.
17+
#
18+
# Refs AAASM-2956.
19+
20+
set -uo pipefail
21+
22+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23+
# shellcheck source=.github/scripts/pep440-to-tag.sh
24+
source "${SCRIPT_DIR}/pep440-to-tag.sh"
25+
# shellcheck source=.github/scripts/tag-to-pep440.sh
26+
source "${SCRIPT_DIR}/tag-to-pep440.sh"
27+
28+
pass=0
29+
fail=0
30+
total=0
31+
32+
# assert_eq <label> <expected> <actual>
33+
assert_eq() {
34+
local label="$1"
35+
local expected="$2"
36+
local actual="$3"
37+
total=$((total + 1))
38+
if [[ "$expected" == "$actual" ]]; then
39+
printf 'OK %-50s -> %s\n' "$label" "$actual"
40+
pass=$((pass + 1))
41+
else
42+
printf 'FAIL %-50s expected=%q actual=%q\n' "$label" "$expected" "$actual" >&2
43+
fail=$((fail + 1))
44+
fi
45+
}
46+
47+
# check <pep440> <expected_tag>
48+
check() {
49+
local version="$1"
50+
local expected="$2"
51+
local actual
52+
actual="$(pep440_to_tag "$version")"
53+
assert_eq "$version" "$expected" "$actual"
54+
}
55+
56+
echo "== pep440_to_tag fixtures =="
57+
58+
# Stable form (no pre-release suffix).
59+
check "0.0.1" "v0.0.1"
60+
61+
# Alpha pre-release, single + double digit.
62+
check "0.0.1a1" "v0.0.1-alpha.1"
63+
check "0.0.1a10" "v0.0.1-alpha.10"
64+
65+
# Beta — the version this ticket backfills (0.0.1b1 -> v0.0.1-beta.1).
66+
check "0.0.1b1" "v0.0.1-beta.1"
67+
check "0.0.1b2" "v0.0.1-beta.2"
68+
69+
# Release candidate.
70+
check "0.0.1rc3" "v0.0.1-rc.3"
71+
72+
# Multi-digit base components + large pre-release counter — guard against
73+
# greedy matching clobbering the minor/patch numbers.
74+
check "1.23.456a789" "v1.23.456-alpha.789"
75+
76+
# A stray leading "v" the caller passed by mistake is tolerated.
77+
check "v0.0.1b1" "v0.0.1-beta.1"
78+
79+
echo "== roundtrip with tag_to_pep440 =="
80+
# Every canonical tag must survive a tag -> pep440 -> tag roundtrip.
81+
for tag in v0.0.1 v0.0.1-alpha.8 v0.0.1-beta.1 v0.0.1-rc.3 v1.23.456-alpha.789; do
82+
pep="$(tag_to_pep440 "$tag")"
83+
back="$(pep440_to_tag "$pep")"
84+
assert_eq "roundtrip ${tag}" "$tag" "$back"
85+
done
86+
87+
echo "== empty-input guard =="
88+
total=$((total + 1))
89+
if err="$(pep440_to_tag "" 2>&1 >/dev/null)"; then
90+
printf 'FAIL empty input should have exited non-zero (stderr=%q)\n' "$err" >&2
91+
fail=$((fail + 1))
92+
else
93+
printf 'OK empty input rejected (stderr=%q)\n' "$err"
94+
pass=$((pass + 1))
95+
fi
96+
97+
echo
98+
echo "Summary: ${pass} passed, ${fail} failed, ${total} total"
99+
if (( fail > 0 )); then
100+
exit 1
101+
fi

.github/workflows/release-python-conversion-test.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ on:
1515
paths:
1616
- ".github/scripts/tag-to-pep440.sh"
1717
- ".github/scripts/test_tag_to_pep440.sh"
18+
- ".github/scripts/pep440-to-tag.sh"
19+
- ".github/scripts/test_pep440_to_tag.sh"
1820
- ".github/workflows/release-python.yml"
1921
- ".github/workflows/release-python-conversion-test.yml"
2022
pull_request:
@@ -23,6 +25,8 @@ on:
2325
paths:
2426
- ".github/scripts/tag-to-pep440.sh"
2527
- ".github/scripts/test_tag_to_pep440.sh"
28+
- ".github/scripts/pep440-to-tag.sh"
29+
- ".github/scripts/test_pep440_to_tag.sh"
2630
- ".github/workflows/release-python.yml"
2731
- ".github/workflows/release-python-conversion-test.yml"
2832

@@ -43,5 +47,9 @@ jobs:
4347
run: |
4448
shellcheck .github/scripts/tag-to-pep440.sh
4549
shellcheck -x .github/scripts/test_tag_to_pep440.sh
46-
- name: Run fixture suite
50+
shellcheck .github/scripts/pep440-to-tag.sh
51+
shellcheck -x .github/scripts/test_pep440_to_tag.sh
52+
- name: Run tag → PEP 440 fixture suite
4753
run: bash .github/scripts/test_tag_to_pep440.sh
54+
- name: Run PEP 440 → tag fixture suite
55+
run: bash .github/scripts/test_pep440_to_tag.sh

.github/workflows/release-python.yml

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ jobs:
5151
binary_source_tag: ${{ steps.r.outputs.binary_source_tag }}
5252
pypi_version: ${{ steps.r.outputs.pypi_version }}
5353
dry_run: ${{ steps.r.outputs.dry_run }}
54+
release_tag: ${{ steps.r.outputs.release_tag }}
5455
steps:
5556
# Need the working tree on disk so the resolve step can source the
56-
# tag → PEP 440 conversion script (.github/scripts/tag-to-pep440.sh).
57-
# That script is the single source of truth for the conversion,
58-
# shared with the AAASM-2863 unit tests.
57+
# tag → PEP 440 conversion script (.github/scripts/tag-to-pep440.sh)
58+
# and its inverse (.github/scripts/pep440-to-tag.sh). Those scripts
59+
# are the single source of truth for the conversion, shared with the
60+
# AAASM-2863 / AAASM-2956 unit tests.
5961
- uses: actions/checkout@v6
6062
- id: r
6163
env:
@@ -124,12 +126,32 @@ jobs:
124126
echo "::error::pypi_version '$pypi_version' is not valid PEP 440 (use 0.0.1a8.post1 not 0.0.1-alpha.8.1)"
125127
exit 1
126128
fi
129+
# Resolve the canonical SemVer release tag the create-github-release
130+
# job will cut at the published version (AAASM-2956). The
131+
# repository_dispatch event already carries the canonical tag, so use
132+
# it verbatim. The workflow_dispatch path only has the PEP 440
133+
# pypi_version, so derive the tag from it via the single-source-of-
134+
# truth inverse converter. .post/.dev republish forms have no own
135+
# GitHub Release tag, so leave release_tag empty for those.
136+
release_tag=""
137+
if [[ "$dry_run" != "true" ]]; then
138+
if [[ "$EVENT_NAME" == "repository_dispatch" ]]; then
139+
release_tag="$DISPATCH_PAYLOAD_TAG"
140+
elif [[ "$pypi_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(a|b|rc)?[0-9]*$ ]]; then
141+
# shellcheck source=.github/scripts/pep440-to-tag.sh
142+
source "${GITHUB_WORKSPACE}/.github/scripts/pep440-to-tag.sh"
143+
release_tag="$(pep440_to_tag "$pypi_version")"
144+
else
145+
echo "::notice::pypi_version '$pypi_version' is a post/dev republish — no GitHub Release tag will be cut"
146+
fi
147+
fi
127148
{
128149
echo "binary_source_tag=${binary_source_tag}"
129150
echo "pypi_version=${pypi_version}"
130151
echo "dry_run=${dry_run}"
152+
echo "release_tag=${release_tag}"
131153
} >> "$GITHUB_OUTPUT"
132-
echo "Resolved binary_source_tag=${binary_source_tag} pypi_version=${pypi_version} dry_run=${dry_run}"
154+
echo "Resolved binary_source_tag=${binary_source_tag} pypi_version=${pypi_version} dry_run=${dry_run} release_tag=${release_tag}"
133155
134156
build-sdist:
135157
name: Build sdist
@@ -415,6 +437,60 @@ jobs:
415437
uses: pypa/gh-action-pypi-publish@release/v1
416438
# No `with: password:` — Trusted Publisher uses OIDC, no token stored.
417439

440+
# Cut python-sdk's own GitHub Release at the just-published version so the
441+
# repo's release line tracks PyPI (and the README `github/v/release` badge
442+
# resolves) instead of drifting onto a separate source-of-truth tag like
443+
# v0.0.2a1. Runs only on the real-publish path, gated on `publish` so the
444+
# tag is never created for a wheel that failed to upload. See AAASM-2956.
445+
create-github-release:
446+
name: Cut GitHub Release at published version
447+
needs:
448+
- resolve
449+
- publish
450+
runs-on: ubuntu-latest
451+
# Real publish only, and only when resolve produced a SemVer tag (empty
452+
# for .post/.dev republishes, which do not get their own Release).
453+
if: needs.resolve.outputs.dry_run != 'true' && needs.resolve.outputs.release_tag != ''
454+
permissions:
455+
contents: write # create the git tag + GitHub Release
456+
steps:
457+
- uses: actions/checkout@v6
458+
- name: Create tag and GitHub Release
459+
env:
460+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
461+
# client_payload is attacker-controllable on repository_dispatch;
462+
# RELEASE_TAG was validated as a SemVer tag by the resolve job and
463+
# is only ever passed through env (never inlined into a run: body).
464+
RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
465+
PYPI_VERSION: ${{ needs.resolve.outputs.pypi_version }}
466+
run: |
467+
set -euo pipefail
468+
# Defence in depth: re-validate the tag shape here so a future
469+
# change to resolve cannot smuggle an arbitrary string into the
470+
# tag/gh commands below.
471+
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?$ ]]; then
472+
echo "::error::release_tag '$RELEASE_TAG' is not a valid SemVer release tag"
473+
exit 1
474+
fi
475+
# Idempotent: skip if a Release already exists for this tag (e.g. a
476+
# re-run after a partial failure), so the job never errors on retry.
477+
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
478+
echo "::notice::GitHub Release $RELEASE_TAG already exists — nothing to do"
479+
exit 0
480+
fi
481+
prerelease_flag=""
482+
if [[ "$RELEASE_TAG" =~ -(alpha|beta|rc)\. ]]; then
483+
prerelease_flag="--prerelease"
484+
fi
485+
# `gh release create` creates the annotated tag at the current
486+
# commit (the workflow ref) when the tag does not yet exist.
487+
gh release create "$RELEASE_TAG" \
488+
--repo "$GITHUB_REPOSITORY" \
489+
$prerelease_flag \
490+
--title "$RELEASE_TAG" \
491+
--notes "python-sdk agent-assembly==${PYPI_VERSION} — coordinated with agent-assembly ${RELEASE_TAG}."
492+
echo "Created GitHub Release $RELEASE_TAG (agent-assembly==${PYPI_VERSION})"
493+
418494
# Publish the *real* release tag as a tiny artifact so the documentation
419495
# workflow (which is triggered by `workflow_run` after this workflow
420496
# completes) can label the frozen docs snapshot with the human-facing tag

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
[![CI](https://img.shields.io/github/actions/workflow/status/ai-agent-assembly/python-sdk/ci.yaml?branch=master&logo=githubactions&logoColor=white&label=CI)](https://github.com/ai-agent-assembly/python-sdk/actions/workflows/ci.yaml)
44
[![Docs](https://img.shields.io/github/actions/workflow/status/ai-agent-assembly/python-sdk/documentation.yaml?branch=master&logo=readthedocs&logoColor=white&label=docs)](https://github.com/ai-agent-assembly/python-sdk/actions/workflows/documentation.yaml)
55
[![PyPI version](https://img.shields.io/pypi/v/agent-assembly?logo=pypi&logoColor=white)](https://pypi.org/project/agent-assembly/)
6+
[![GitHub release](https://img.shields.io/github/v/release/ai-agent-assembly/python-sdk?include_prereleases&sort=semver&label=release&logo=github)](https://github.com/ai-agent-assembly/python-sdk/releases)
67
[![Python versions](https://img.shields.io/pypi/pyversions/agent-assembly?logo=python&logoColor=white)](https://pypi.org/project/agent-assembly/)
78
[![Coverage](https://img.shields.io/codecov/c/github/ai-agent-assembly/python-sdk?logo=codecov&logoColor=white)](https://codecov.io/gh/ai-agent-assembly/python-sdk)
89
[![Quality Gate](https://img.shields.io/sonar/quality_gate/AI-agent-assembly_python-sdk?server=https%3A%2F%2Fsonarcloud.io&logo=sonarcloud)](https://sonarcloud.io/project/overview?id=AI-agent-assembly_python-sdk)

0 commit comments

Comments
 (0)