Skip to content
Open
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
70 changes: 32 additions & 38 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@
name: "Release NeMo Anonymizer"

on:
push:
tags:
- "v*"
Comment thread
binaryaaron marked this conversation as resolved.
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
Expand All @@ -44,35 +43,34 @@ 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')
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)"

- 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)
Expand All @@ -85,27 +83,29 @@ 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 }}
run: |
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
Expand All @@ -116,7 +116,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=""
Expand All @@ -128,42 +128,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"
6 changes: 2 additions & 4 deletions .mise/tasks/build/wheel
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 3 additions & 7 deletions .mise/tasks/docs.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<version>" 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'
8 changes: 0 additions & 8 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
136 changes: 59 additions & 77 deletions tests/tools/test_mise_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,65 @@ 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"]
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")
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"]
Expand Down Expand Up @@ -56,83 +115,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")

Expand Down
Loading