diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index cdffc398..f0c55821 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -5,8 +5,12 @@ name: Packaging # identical to the canonical copies at the repository root. # Build backends stay silent when those files go missing, so the artifacts are # inspected explicitly. +# +# Also called by release.yml, which publishes the core distributions from the +# uploaded "dist" artifact. on: + workflow_call: workflow_dispatch: push: branches: [main] @@ -19,6 +23,7 @@ on: - "examples/*/third-party-programs.txt" - ".github/scripts/check_dist_licenses.py" - ".github/workflows/packaging.yml" + - ".github/workflows/release.yml" pull_request: paths: *packaging-paths @@ -42,11 +47,13 @@ jobs: with: # The core package version is derived from the git history. fetch-depth: 0 + persist-credentials: false - name: Set up Python uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: - python-version: "3.x" + # Keeping this pinned, so we have stable release environment + python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 @@ -73,3 +80,10 @@ jobs: - name: Verify license files in all distributions run: python3 .github/scripts/check_dist_licenses.py dist/core/* dist/examples/*.whl + + - name: Upload core distributions + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: dist + path: dist/core/ + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..94ce9272 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,114 @@ +name: Release + +# Publishes the torch-tweak core distributions to PyPI and TestPyPI. +# +# Only release tags are allowed, e.g. v0.5.0 or v0.5.0rc1. +# +on: + workflow_dispatch: + inputs: + target: + description: Package index to publish to + type: choice + options: + - testpypi + - pypi + default: testpypi + +# Never cancel a release midway: a partial upload cannot be redone with the +# same version. +concurrency: + group: release + cancel-in-progress: false + +permissions: + contents: read + +jobs: + validate-tag: + name: Validate release tag + runs-on: ubuntu-latest + + steps: + - name: Require a tagged commit + env: + REF_TYPE: ${{ github.ref_type }} + REF_NAME: ${{ github.ref_name }} + run: | + if [ "${REF_TYPE}" != "tag" ]; then + echo "::error::Run this workflow on a release tag." + exit 1 + fi + if ! printf '%s' "${REF_NAME}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(rc[0-9]+)?$'; then + echo "::error::Tag '${REF_NAME}' does not match vX.Y.Z or vX.Y.ZrcN." + exit 1 + fi + + - name: Checkout Torch Tweak repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Require the tagged commit to be on main + env: + REF_NAME: ${{ github.ref_name }} + run: | + if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/main; then + echo "::error::Tag '${REF_NAME}' points to ${GITHUB_SHA}, which is not on main." + exit 1 + fi + + # Note: packaging.yml also builds the example wheels, which are never + # published, so a broken example blocks the release. + build: + name: Build + needs: validate-tag + uses: $/.github/workflows/packaging.yml + + publish: + name: Publish to ${{ inputs.target }} + needs: build + runs-on: ubuntu-latest + environment: + name: ${{ inputs.target }} + url: >- + ${{ inputs.target == 'pypi' + && format('https://pypi.org/project/torch-tweak/{0}/', steps.version.outputs.version) + || format('https://test.pypi.org/project/torch-tweak/{0}/', steps.version.outputs.version) }} + permissions: + id-token: write # PyPI trusted publishing (OIDC) and attestation signing + + steps: + - name: Download distributions + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: dist + path: dist/core/ + + - name: Require the built version to match the tag + id: version + env: + REF_NAME: ${{ github.ref_name }} + run: | + version="${REF_NAME#v}" + expected="$(printf '%s\n' \ + "torch_tweak-${version}-py3-none-any.whl" \ + "torch_tweak-${version}.tar.gz" | sort)" + actual="$(ls dist/core | sort)" + if [ "${actual}" != "${expected}" ]; then + echo "::error::dist/core does not match version ${version}." + printf 'Expected:\n%s\nActual:\n%s\n' "${expected}" "${actual}" + exit 1 + fi + echo "version=${version}" >> "${GITHUB_OUTPUT}" + + - name: Publish to ${{ inputs.target }} + uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 + with: + attestations: true + packages-dir: dist/core/ + repository-url: >- + ${{ inputs.target == 'pypi' + && 'https://upload.pypi.org/legacy/' + || 'https://test.pypi.org/legacy/' }}