From abef494b419c6bfb779b325c1128044ea0b97b71 Mon Sep 17 00:00:00 2001 From: Aaron Gonzales Date: Thu, 13 Aug 2026 23:05:59 +0000 Subject: [PATCH 1/2] ci(release): publish tagged releases from triggering ref Signed-off-by: Aaron Gonzales --- .github/workflows/release.yml | 66 ++++++------ .mise/tasks/build/wheel | 6 +- .mise/tasks/docs.toml | 10 +- DEVELOPMENT.md | 8 -- tests/tools/test_mise_configuration.py | 134 +++++++++++-------------- 5 files changed, 90 insertions(+), 134 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b8d0200..84dacb59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,12 +13,11 @@ name: "Release NeMo Anonymizer" on: + push: + tags: + - "v*" workflow_dispatch: inputs: - release-ref: - description: Full SHA (not short) or tag of the commit to build - required: true - type: string dry-run: description: "Dry Run: only publish to Test PyPI" required: true @@ -44,27 +43,22 @@ jobs: outputs: version: ${{ steps.build.outputs.version }} steps: - - name: Checkout workflow + - name: Checkout triggering ref uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true - uses: ./.github/actions/setup-python-env with: checkout: "false" dependency-profile: "runtime" - - name: Checkout release target - uses: actions/checkout@v6 - with: - ref: ${{ inputs.release-ref }} - fetch-depth: "0" - fetch-tags: true - path: release-target - - name: Build wheel id: build run: | - mise run build:wheel release-target - WHEEL=$(ls release-target/dist/*.whl) + mise run build:wheel + WHEEL=$(ls dist/*.whl) VERSION=$(echo "$WHEEL" | sed -n 's/.*-\([0-9][^-]*\)-.*/\1/p') echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Built: $WHEEL (version $VERSION)" @@ -72,7 +66,7 @@ jobs: - uses: actions/upload-artifact@v7 with: name: nemo-anonymizer-${{ steps.build.outputs.version }} - path: release-target/dist/*.whl + path: dist/*.whl retention-days: 90 - name: Publish to Test PyPI (always, as pre-flight) @@ -85,10 +79,10 @@ jobs: --skip-existing \ --non-interactive \ --verbose \ - release-target/dist/*.whl + dist/*.whl - name: Publish to PyPI - if: ${{ !inputs.dry-run }} + if: ${{ github.event_name == 'push' || !inputs.dry-run }} env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PERSONAL_TOKEN }} @@ -96,16 +90,18 @@ jobs: uvx twine upload \ --non-interactive \ --verbose \ - release-target/dist/*.whl + dist/*.whl create-gh-release: name: Create GitHub release needs: publish-wheel - if: ${{ inputs.create-gh-release }} + if: ${{ github.event_name == 'push' || inputs.create-gh-release }} runs-on: ubuntu-latest steps: - - name: Checkout workflow + - name: Checkout triggering ref uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Download wheel artifact uses: actions/download-artifact@v8 @@ -116,7 +112,7 @@ jobs: - name: Create GitHub release env: GH_TOKEN: ${{ github.token }} - RELEASE_REF: ${{ inputs.release-ref }} + TARGET_SHA: ${{ github.sha }} VERSION: ${{ needs.publish-wheel.outputs.version }} run: | PRERELEASE_FLAG="" @@ -128,42 +124,36 @@ jobs: dist/*.whl \ --generate-notes \ --title "v${VERSION}" \ - --target "$RELEASE_REF" \ + --target "$TARGET_SHA" \ $PRERELEASE_FLAG deploy-release-docs: name: Deploy release docs needs: [publish-wheel, create-gh-release] - if: ${{ inputs.create-gh-release }} + if: ${{ github.event_name == 'push' || inputs.create-gh-release }} runs-on: ubuntu-latest permissions: contents: write steps: - - name: Checkout workflow + - name: Checkout triggering ref uses: actions/checkout@v6 + with: + fetch-depth: 0 - uses: ./.github/actions/setup-python-env with: checkout: "false" - dependency-profile: "runtime" - - - name: Checkout release target - uses: actions/checkout@v6 - with: - ref: ${{ inputs.release-ref }} - fetch-depth: "0" - fetch-tags: true - path: release-target + dependency-profile: "docs" - name: Configure git for mike run: | - git -C release-target config user.name "github-actions[bot]" - git -C release-target config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" - name: Build docs - run: mise run docs:build release-target + run: mise run docs:build - name: Deploy release docs with mike env: VERSION: ${{ needs.publish-wheel.outputs.version }} - run: mise run docs:deploy "$VERSION" release-target + run: mise run docs:deploy "$VERSION" diff --git a/.mise/tasks/build/wheel b/.mise/tasks/build/wheel index 04baec95..dc353f3b 100755 --- a/.mise/tasks/build/wheel +++ b/.mise/tasks/build/wheel @@ -2,10 +2,8 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 #MISE description="Build the wheel into dist/ using the version derived from git tags. Replaces the existing dist/ directory." -#USAGE arg "[project]" help="Project directory to build" default="." set -euo pipefail -project="${usage_project:-.}" -rm -rf "$project/dist/" -uv build --wheel "$project" --out-dir "$project/dist" +rm -rf dist/ +uv build --wheel diff --git a/.mise/tasks/docs.toml b/.mise/tasks/docs.toml index 6ae6f467..3bcca4d0 100644 --- a/.mise/tasks/docs.toml +++ b/.mise/tasks/docs.toml @@ -7,15 +7,11 @@ run = "uv run --locked --group docs mkdocs serve" ["docs:build"] description = "Build the documentation site in strict mode. Writes the ignored site/ directory." -usage = ''' -arg "[project]" help="Project directory containing mkdocs.yml" default="." -''' -run = 'uv run --directory "${usage_project:-.}" --locked --group docs mkdocs build --strict' +run = "uv run --locked --group docs mkdocs build --strict" ["docs:deploy"] -description = "Deploy versioned documentation with mike from a project directory. Updates the remote gh-pages branch." +description = "Deploy versioned documentation with mike. Updates the remote gh-pages branch." usage = ''' arg "" help="Documentation version to deploy" -arg "[project]" help="Project directory containing mkdocs.yml" default="." ''' -run = 'uv run --directory "${usage_project:-.}" --locked --group docs mike deploy --push --update-aliases "${usage_version?}" latest' +run = 'uv run --locked --group docs mike deploy --push --update-aliases "${usage_version?}" latest' diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 70dac780..ccd79e21 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -278,14 +278,6 @@ Build a wheel locally: mise run build:wheel ``` -Release automation can apply the current repository commands to a separate project checkout: - -```bash -mise run build:wheel path/to/project -mise run docs:build path/to/project -mise run docs:deploy VERSION path/to/project -``` - Release tags use `vMAJOR.MINOR.PATCH` for stable releases and `vMAJOR.MINOR.PATCHrcN` for release candidates, while the Python package version is the unprefixed version. Release publishing is handled by `.github/workflows/release.yml`. diff --git a/tests/tools/test_mise_configuration.py b/tests/tools/test_mise_configuration.py index 0a30fbbc..28d0e81e 100644 --- a/tests/tools/test_mise_configuration.py +++ b/tests/tools/test_mise_configuration.py @@ -27,6 +27,63 @@ def test_github_setup_requires_signed_mise_installer() -> None: assert install_step.get("env", {}).get("MISE_REQUIRE_SIGNED_INSTALL") == "1" +def test_release_workflow_uses_triggering_ref_and_completes_tag_releases() -> None: + workflow_path = REPO_ROOT / ".github/workflows/release.yml" + workflow_text = workflow_path.read_text(encoding="utf-8") + workflow = yaml.safe_load(workflow_text) + triggers = workflow.get("on", workflow[True]) + dispatch_inputs = triggers["workflow_dispatch"]["inputs"] + + assert triggers["push"]["tags"] == ["v*"] + assert set(dispatch_inputs) == {"dry-run", "create-gh-release"} + assert "release-ref" not in workflow_text + assert "release-target" not in workflow_text + + publish_steps = workflow["jobs"]["publish-wheel"]["steps"] + checkout_steps = [ + step for job in workflow["jobs"].values() for step in job["steps"] if step.get("uses") == "actions/checkout@v6" + ] + assert checkout_steps + assert all("ref" not in step.get("with", {}) for step in checkout_steps) + assert all("path" not in step.get("with", {}) for step in checkout_steps) + + build_step = next(step for step in publish_steps if step.get("id") == "build") + assert "mise run build:wheel" in build_step["run"] + assert "dist/*.whl" in build_step["run"] + + release_steps = workflow["jobs"]["create-gh-release"]["steps"] + release_step = next(step for step in release_steps if step.get("name") == "Create GitHub release") + assert release_step["env"]["TARGET_SHA"] == "${{ github.sha }}" + assert '--target "$TARGET_SHA"' in release_step["run"] + + docs_steps = workflow["jobs"]["deploy-release-docs"]["steps"] + assert next(step for step in docs_steps if step.get("name") == "Build docs")["run"] == "mise run docs:build" + assert ( + next(step for step in docs_steps if step.get("name") == "Deploy release docs with mike")["run"] + == 'mise run docs:deploy "$VERSION"' + ) + + publish_to_pypi = next(step for step in publish_steps if step.get("name") == "Publish to PyPI") + assert publish_to_pypi["if"] == "${{ github.event_name == 'push' || !inputs.dry-run }}" + assert ( + workflow["jobs"]["create-gh-release"]["if"] == "${{ github.event_name == 'push' || inputs.create-gh-release }}" + ) + assert ( + workflow["jobs"]["deploy-release-docs"]["if"] + == "${{ github.event_name == 'push' || inputs.create-gh-release }}" + ) + + +def test_release_tasks_use_current_checkout() -> None: + build_task = (REPO_ROOT / ".mise/tasks/build/wheel").read_text(encoding="utf-8") + docs_tasks = _read_toml(REPO_ROOT / ".mise/tasks/docs.toml") + + assert "usage_project" not in build_task + assert set(docs_tasks["docs:build"]) == {"description", "run"} + assert 'arg "[project]"' not in docs_tasks["docs:deploy"]["usage"] + assert "--directory" not in docs_tasks["docs:deploy"]["run"] + + def test_benchmark_workflow_keeps_setup_on_workflow_revision() -> None: workflow = yaml.safe_load((REPO_ROOT / ".github/workflows/benchmark-ci.yml").read_text(encoding="utf-8")) steps = workflow["jobs"]["benchmark"]["steps"] @@ -56,83 +113,6 @@ def test_benchmark_workflow_keeps_setup_on_workflow_revision() -> None: assert upload_step["with"]["path"] == "benchmark-target/${{ env.BENCHMARK_OUTPUT_DIR }}/" -def test_release_workflow_keeps_setup_on_workflow_revision() -> None: - workflow = yaml.safe_load((REPO_ROOT / ".github/workflows/release.yml").read_text(encoding="utf-8")) - triggers = workflow[True] - - assert "release-ref" in triggers["workflow_dispatch"]["inputs"] - - for job_name in ("publish-wheel", "deploy-release-docs"): - steps = workflow["jobs"][job_name]["steps"] - setup_index = next( - index for index, step in enumerate(steps) if step.get("uses") == "./.github/actions/setup-python-env" - ) - root_checkout_index = next( - index - for index, step in enumerate(steps) - if step.get("uses") == "actions/checkout@v6" and "ref" not in step.get("with", {}) - ) - target_checkout_index = next( - index - for index, step in enumerate(steps) - if step.get("uses") == "actions/checkout@v6" and step.get("with", {}).get("path") == "release-target" - ) - - assert root_checkout_index < setup_index < target_checkout_index - assert steps[setup_index]["with"]["checkout"] == "false" - assert steps[target_checkout_index]["with"] == { - "ref": "${{ inputs.release-ref }}", - "fetch-depth": "0", - "fetch-tags": True, - "path": "release-target", - } - - -def test_release_workflow_runs_repository_commands_against_historical_target() -> None: - workflow = yaml.safe_load((REPO_ROOT / ".github/workflows/release.yml").read_text(encoding="utf-8")) - - publish_steps = workflow["jobs"]["publish-wheel"]["steps"] - build_step = next(step for step in publish_steps if step.get("id") == "build") - assert "mise run build:wheel release-target" in build_step["run"] - assert "release-target/dist/*.whl" in build_step["run"] - assert ( - next(step for step in publish_steps if step.get("uses") == "actions/upload-artifact@v7")["with"]["path"] - == "release-target/dist/*.whl" - ) - assert all( - "release-target/dist/*.whl" in step["run"] - for step in publish_steps - if step.get("name") in {"Publish to Test PyPI (always, as pre-flight)", "Publish to PyPI"} - ) - - release_steps = workflow["jobs"]["create-gh-release"]["steps"] - release_step = next(step for step in release_steps if step.get("name") == "Create GitHub release") - assert release_step["env"]["RELEASE_REF"] == "${{ inputs.release-ref }}" - assert '--target "$RELEASE_REF"' in release_step["run"] - - docs_steps = workflow["jobs"]["deploy-release-docs"]["steps"] - assert ( - "git -C release-target config" - in next(step for step in docs_steps if step.get("name") == "Configure git for mike")["run"] - ) - assert ( - next(step for step in docs_steps if step.get("name") == "Build docs")["run"] - == "mise run docs:build release-target" - ) - assert ( - next(step for step in docs_steps if step.get("name") == "Deploy release docs with mike")["run"] - == 'mise run docs:deploy "$VERSION" release-target' - ) - - build_task = (REPO_ROOT / ".mise/tasks/build/wheel").read_text(encoding="utf-8") - docs_tasks = _read_toml(REPO_ROOT / ".mise/tasks/docs.toml") - assert 'arg "[project]"' in build_task - assert 'uv build --wheel "$project"' in build_task - assert 'arg "[project]"' in docs_tasks["docs:build"]["usage"] - assert "uv run --directory" in docs_tasks["docs:build"]["run"] - assert set(docs_tasks["docs:deploy"]) == {"description", "usage", "run"} - - def test_local_mise_installer_keeps_unsigned_fallback_opt_in() -> None: installer = (REPO_ROOT / "tools/install-mise.sh").read_text(encoding="utf-8") From d64940d0b3262a65a6d96443256a9fac225c1711 Mon Sep 17 00:00:00 2001 From: Aaron Gonzales Date: Fri, 14 Aug 2026 15:59:37 +0000 Subject: [PATCH 2/2] fix(release): reject mismatched version tags Signed-off-by: Aaron Gonzales --- .github/workflows/release.yml | 4 ++++ tests/tools/test_mise_configuration.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84dacb59..d9be9f59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,6 +60,10 @@ jobs: mise run build:wheel WHEEL=$(ls dist/*.whl) VERSION=$(echo "$WHEEL" | sed -n 's/.*-\([0-9][^-]*\)-.*/\1/p') + if [ "$GITHUB_REF_TYPE" = "tag" ] && [ "$GITHUB_REF_NAME" != "v${VERSION}" ]; then + echo "::error::Release tag ${GITHUB_REF_NAME} does not match package version v${VERSION}" + exit 1 + fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Built: $WHEEL (version $VERSION)" diff --git a/tests/tools/test_mise_configuration.py b/tests/tools/test_mise_configuration.py index 28d0e81e..818ac63c 100644 --- a/tests/tools/test_mise_configuration.py +++ b/tests/tools/test_mise_configuration.py @@ -50,6 +50,8 @@ def test_release_workflow_uses_triggering_ref_and_completes_tag_releases() -> No build_step = next(step for step in publish_steps if step.get("id") == "build") assert "mise run build:wheel" in build_step["run"] assert "dist/*.whl" in build_step["run"] + assert 'if [ "$GITHUB_REF_TYPE" = "tag" ] && [ "$GITHUB_REF_NAME" != "v${VERSION}" ]; then' in build_step["run"] + assert "exit 1" in build_step["run"] release_steps = workflow["jobs"]["create-gh-release"]["steps"] release_step = next(step for step in release_steps if step.get("name") == "Create GitHub release")