From fc167e3cd6b3d1f92548e2f5db1029b097b74d22 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Tue, 26 May 2026 01:51:50 -0300 Subject: [PATCH] fix: harden release workflow verification Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/_build.yml | 53 +++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 15 ++++++--- .github/workflows/cut-release.yml | 22 ++++++++++--- .github/workflows/release.yml | 20 +++++++++--- .github/workflows/staging.yml | 15 ++++++--- CHANGELOG.md | 8 +++++ 6 files changed, 114 insertions(+), 19 deletions(-) diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index 9bbea281..7297298f 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -23,6 +23,17 @@ name: _build on: workflow_call: + inputs: + checkout_ref: + description: "Optional ref to check out, used by manual release reruns." + required: false + type: string + default: "" + release_tag: + description: "Optional v-prefixed tag that should define the package version." + required: false + type: string + default: "" jobs: build: @@ -30,8 +41,23 @@ jobs: steps: - uses: actions/checkout@v6 with: + ref: ${{ inputs.checkout_ref || github.ref }} fetch-depth: 0 # Full history required for setuptools-scm + - name: Pin package version for release tags + if: ${{ inputs.release_tag != '' || startsWith(github.ref, 'refs/tags/v') }} + run: | + TAG="${{ inputs.release_tag }}" + TAG="${TAG:-$GITHUB_REF_NAME}" + VERSION="${TAG#v}" + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Release tag '$TAG' did not resolve to a semver package version." + exit 1 + fi + echo "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_AGENTOPS_TOOLKIT=$VERSION" >> "$GITHUB_ENV" + echo "EXPECTED_PACKAGE_VERSION=$VERSION" >> "$GITHUB_ENV" + echo "Release package version pinned to $VERSION from $TAG" + - name: Install uv uses: astral-sh/setup-uv@v7 with: @@ -52,6 +78,33 @@ jobs: - name: Build package run: uv build + - name: Assert release artifact version + run: | + if [ -z "${EXPECTED_PACKAGE_VERSION:-}" ]; then + echo "No release package version was pinned; skipping artifact version assertion." + exit 0 + fi + python - <<'PY' + import os + import sys + from pathlib import Path + + expected = os.environ["EXPECTED_PACKAGE_VERSION"] + dist = Path("dist") + matches = [ + *sorted(dist.glob(f"agentops_toolkit-{expected}.tar.gz")), + *sorted(dist.glob(f"agentops_toolkit-{expected}-*.whl")), + ] + if not matches: + print(f"::error::Expected dist artifact for version {expected}, found:") + for artifact in sorted(dist.iterdir()): + print(f" - {artifact.name}") + sys.exit(1) + print("Release artifacts:") + for artifact in matches: + print(f" - {artifact.name}") + PY + - name: Show build info run: | ls -la dist/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35999650..11f7f5c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -169,14 +169,19 @@ jobs: run: | for i in 1 2 3 4 5; do echo "Attempt $i: installing agentops-toolkit==${{ steps.version.outputs.version }}" - pip install \ + if pip install \ "agentops-toolkit==${{ steps.version.outputs.version }}" \ --index-url https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ \ - && break - echo "Not available yet, waiting 30s..." - sleep 30 + --extra-index-url https://pypi.org/simple/; then + exit 0 + fi + if [ "$i" -lt 5 ]; then + echo "Not available yet, waiting 30s..." + sleep 30 + fi done + echo "::error::agentops-toolkit==${{ steps.version.outputs.version }} was not available from TestPyPI after 5 attempts." + exit 1 - name: Smoke test run: | diff --git a/.github/workflows/cut-release.yml b/.github/workflows/cut-release.yml index f4ffa68f..efd3a7a1 100644 --- a/.github/workflows/cut-release.yml +++ b/.github/workflows/cut-release.yml @@ -68,13 +68,27 @@ jobs: - name: Update CHANGELOG run: | DATE=$(date +%Y-%m-%d) - # Only insert versioned section if it doesn't already exist if grep -q "## \[${{ env.version }}\]" CHANGELOG.md; then echo "CHANGELOG already has [${{ env.version }}] entry — skipping insertion" - else - sed -i "/adheres to \[Semantic Versioning\]/a \\\n## [${{ env.version }}] - $DATE\n" CHANGELOG.md - echo "CHANGELOG updated with [${{ env.version }}] - $DATE" + exit 0 fi + VERSION="${{ env.version }}" DATE="$DATE" python - <<'PY' + import os + import sys + from pathlib import Path + + path = Path("CHANGELOG.md") + text = path.read_text(encoding="utf-8") + marker = "## [Unreleased]" + version = os.environ["VERSION"] + date = os.environ["DATE"] + if marker not in text: + print("::error::CHANGELOG.md is missing the ## [Unreleased] section.") + sys.exit(1) + replacement = f"{marker}\n\n## [{version}] - {date}" + path.write_text(text.replace(marker, replacement, 1), encoding="utf-8") + PY + echo "CHANGELOG updated with [${{ env.version }}] - $DATE" - name: Sync plugin versions run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74286d4e..45d45eeb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,6 +65,9 @@ jobs: # Reusable build: test + package build: uses: ./.github/workflows/_build.yml + with: + checkout_ref: ${{ inputs.tag || github.ref }} + release_tag: ${{ inputs.tag || (startsWith(github.ref, 'refs/tags/v') && github.ref_name) || '' }} # Publish to TestPyPI for final pre-release verification publish-testpypi: @@ -107,6 +110,8 @@ jobs: if [ -n "${{ inputs.tag }}" ]; then VERSION="${{ inputs.tag }}" VERSION="${VERSION#v}" + elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then + VERSION="${GITHUB_REF_NAME#v}" else pip install setuptools-scm VERSION=$(python -m setuptools_scm) @@ -118,14 +123,19 @@ jobs: run: | for i in 1 2 3 4 5; do echo "Attempt $i: installing agentops-toolkit==${{ steps.version.outputs.version }}" - pip install \ + if pip install \ "agentops-toolkit==${{ steps.version.outputs.version }}" \ --index-url https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ \ - && break - echo "Not available yet, waiting 30s..." - sleep 30 + --extra-index-url https://pypi.org/simple/; then + exit 0 + fi + if [ "$i" -lt 5 ]; then + echo "Not available yet, waiting 30s..." + sleep 30 + fi done + echo "::error::agentops-toolkit==${{ steps.version.outputs.version }} was not available from TestPyPI after 5 attempts." + exit 1 - name: Smoke test — version and help run: | diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index e7575c7e..722f82fd 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -97,14 +97,19 @@ jobs: run: | for i in 1 2 3 4 5; do echo "Attempt $i: installing agentops-toolkit==${{ steps.version.outputs.version }}" - pip install \ + if pip install \ "agentops-toolkit==${{ steps.version.outputs.version }}" \ --index-url https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ \ - && break - echo "Not available yet, waiting 30s..." - sleep 30 + --extra-index-url https://pypi.org/simple/; then + exit 0 + fi + if [ "$i" -lt 5 ]; then + echo "Not available yet, waiting 30s..." + sleep 30 + fi done + echo "::error::agentops-toolkit==${{ steps.version.outputs.version }} was not available from TestPyPI after 5 attempts." + exit 1 - name: Smoke test — version and help run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 25591132..992c14cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres ## [Unreleased] +### Fixed +- **Release workflow verification.** Release builds now pin package versions from + the release tag, assert the generated distribution matches that version, and + fail TestPyPI verification immediately when the expected package is not + available. + +## [0.2.1] - 2026-05-26 + ### Changed - Consolidated the tutorial set into two quickstarts plus one end-to-end Foundry + AgentOps workshop, with the quickstarts now covering the broader