Release open-mle #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Builds and publishes open-mle package to PyPI. | |
| # | |
| # Manually triggered workflow for releasing the open-mle CLI package. | |
| # | |
| # Handles version bumping, building, and publishing to PyPI with authentication. | |
| name: "🚀 Package Release" | |
| run-name: "Release open-mle" | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| dangerous-nonmaster-release: | |
| required: false | |
| type: boolean | |
| default: false | |
| description: "Release from a non-master branch (danger!) - Only use for hotfixes" | |
| env: | |
| PYTHON_VERSION: "3.12" | |
| UV_FROZEN: "true" | |
| UV_NO_SYNC: "true" | |
| WORKING_DIR: "libs/openmle-cli" | |
| permissions: | |
| contents: write # Required for creating GitHub releases | |
| jobs: | |
| # Build the distribution package and extract version info | |
| # Runs in isolated environment with minimal permissions for security | |
| build: | |
| if: github.ref == 'refs/heads/master' || inputs.dangerous-nonmaster-release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| pkg-name: ${{ steps.check-version.outputs.pkg-name }} | |
| version: ${{ steps.check-version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Set up Python + uv | |
| uses: "./.github/actions/uv_setup" | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| # We want to keep this build stage *separate* from the release stage, | |
| # so that there's no sharing of permissions between them. | |
| # (Release stage has trusted publishing and GitHub repo contents write access) | |
| # | |
| # Otherwise, a malicious `build` step (e.g. via a compromised dependency) | |
| # could get access to our GitHub or PyPI credentials. | |
| # | |
| # Per the trusted publishing GitHub Action: | |
| # > It is strongly advised to separate jobs for building [...] | |
| # > from the publish job. | |
| # https://github.com/pypa/gh-action-pypi-publish#non-goals | |
| - name: Build project for distribution | |
| run: uv build | |
| working-directory: ${{ env.WORKING_DIR }} | |
| - name: Upload build | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: dist | |
| path: ${{ env.WORKING_DIR }}/dist/ | |
| - name: Check version | |
| id: check-version | |
| shell: python | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: | | |
| import os | |
| import tomllib | |
| with open("pyproject.toml", "rb") as f: | |
| data = tomllib.load(f) | |
| pkg_name = data["project"]["name"] | |
| version = data["project"]["version"] | |
| with open(os.environ["GITHUB_OUTPUT"], "a") as f: | |
| f.write(f"pkg-name={pkg_name}\n") | |
| f.write(f"version={version}\n") | |
| release-notes: | |
| needs: | |
| - build | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| release-body: ${{ steps.generate-release-body.outputs.release-body }} | |
| tag: ${{ steps.check-tags.outputs.tag }} | |
| prev-tag: ${{ steps.check-tags.outputs.prev-tag }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| path: open-mle | |
| sparse-checkout: | | |
| ${{ env.WORKING_DIR }} | |
| ref: ${{ github.ref }} | |
| fetch-depth: 0 # this fetches entire commit history | |
| - name: Check tags | |
| id: check-tags | |
| shell: bash | |
| working-directory: open-mle/${{ env.WORKING_DIR }} | |
| env: | |
| PKG_NAME: ${{ needs.build.outputs.pkg-name }} | |
| VERSION: ${{ needs.build.outputs.version }} | |
| run: | | |
| # Handle regular versions and pre-release versions differently | |
| if [[ "$VERSION" == *"-"* ]]; then | |
| # This is a pre-release version (contains a hyphen) | |
| BASE_VERSION=${VERSION%%-*} | |
| REGEX="^$PKG_NAME==$BASE_VERSION\$" | |
| PREV_TAG=$(git tag --sort=-creatordate | (grep -P "$REGEX" || true) | head -1) | |
| if [ -z "$PREV_TAG" ]; then | |
| REGEX="^$PKG_NAME==\\d+\\.\\d+\\.\\d+\$" | |
| PREV_TAG=$(git tag --sort=-creatordate | (grep -P "$REGEX" || true) | head -1) | |
| fi | |
| else | |
| # Regular version handling | |
| PREV_TAG="$PKG_NAME==${VERSION%.*}.$(( ${VERSION##*.} - 1 ))"; [[ "${VERSION##*.}" -eq 0 ]] && PREV_TAG="" | |
| if [ -z "$PREV_TAG" ]; then | |
| REGEX="^$PKG_NAME==\\d+\\.\\d+\\.\\d+\$" | |
| PREV_TAG=$(git tag --sort=-creatordate | (grep -P "$REGEX" || true) | head -1) | |
| fi | |
| fi | |
| if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$PKG_NAME==0.0.0" ]; then | |
| echo "No previous tag found - first release" | |
| else | |
| GIT_TAG_RESULT=$(git tag -l "$PREV_TAG") | |
| if [ -z "$GIT_TAG_RESULT" ]; then | |
| echo "Previous tag $PREV_TAG not found in git repo" | |
| exit 1 | |
| fi | |
| fi | |
| TAG="${PKG_NAME}==${VERSION}" | |
| if [ "$TAG" == "$PREV_TAG" ]; then | |
| echo "No new version to release" | |
| exit 1 | |
| fi | |
| echo tag="$TAG" >> $GITHUB_OUTPUT | |
| echo prev-tag="$PREV_TAG" >> $GITHUB_OUTPUT | |
| - name: Generate release body | |
| id: generate-release-body | |
| working-directory: open-mle | |
| env: | |
| PKG_NAME: ${{ needs.build.outputs.pkg-name }} | |
| TAG: ${{ steps.check-tags.outputs.tag }} | |
| PREV_TAG: ${{ steps.check-tags.outputs.prev-tag }} | |
| WORKING_DIR: ${{ env.WORKING_DIR }} | |
| run: | | |
| PREAMBLE="Changes since $PREV_TAG" | |
| if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$PKG_NAME==0.0.0" ]; then | |
| PREAMBLE="Initial release" | |
| PREV_TAG=$(git rev-list --max-parents=0 HEAD) | |
| fi | |
| { | |
| echo 'release-body<<EOF' | |
| echo $PREAMBLE | |
| echo | |
| git log --format="%s" "$PREV_TAG"..HEAD -- "$WORKING_DIR" | |
| echo EOF | |
| } >> "$GITHUB_OUTPUT" | |
| test-pypi-publish: | |
| needs: | |
| - build | |
| - release-notes | |
| runs-on: ubuntu-latest | |
| permissions: | |
| # This permission is used for trusted publishing: | |
| # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/download-artifact@v6 | |
| with: | |
| name: dist | |
| path: ${{ env.WORKING_DIR }}/dist/ | |
| - name: Publish to test PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: ${{ env.WORKING_DIR }}/dist/ | |
| verbose: true | |
| print-hash: true | |
| repository-url: https://test.pypi.org/legacy/ | |
| skip-existing: true | |
| attestations: false | |
| pre-release-checks: | |
| needs: | |
| - build | |
| - release-notes | |
| - test-pypi-publish | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Set up Python + uv | |
| uses: "./.github/actions/uv_setup" | |
| id: setup-python | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - uses: actions/download-artifact@v6 | |
| with: | |
| name: dist | |
| path: ${{ env.WORKING_DIR }}/dist/ | |
| - name: Import dist package | |
| shell: bash | |
| working-directory: ${{ env.WORKING_DIR }} | |
| env: | |
| PKG_NAME: ${{ needs.build.outputs.pkg-name }} | |
| VERSION: ${{ needs.build.outputs.version }} | |
| run: | | |
| uv venv | |
| VIRTUAL_ENV=.venv uv pip install dist/*.whl | |
| # For open-mle, the module name is openmle_cli | |
| IMPORT_NAME="openmle_cli" | |
| uv run python -c "import $IMPORT_NAME; print(dir($IMPORT_NAME))" | |
| - name: Import test dependencies | |
| run: uv sync --group test | |
| working-directory: ${{ env.WORKING_DIR }} | |
| - name: Import published package (again) | |
| working-directory: ${{ env.WORKING_DIR }} | |
| shell: bash | |
| env: | |
| PKG_NAME: ${{ needs.build.outputs.pkg-name }} | |
| VERSION: ${{ needs.build.outputs.version }} | |
| run: | | |
| VIRTUAL_ENV=.venv uv pip install dist/*.whl | |
| - name: Run unit tests | |
| run: make test || echo "No tests found, skipping..." | |
| working-directory: ${{ env.WORKING_DIR }} | |
| publish: | |
| # Publishes the package to PyPI | |
| needs: | |
| - build | |
| - release-notes | |
| - test-pypi-publish | |
| - pre-release-checks | |
| runs-on: ubuntu-latest | |
| permissions: | |
| # This permission is used for trusted publishing: | |
| # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ | |
| id-token: write | |
| defaults: | |
| run: | |
| working-directory: ${{ env.WORKING_DIR }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Set up Python + uv | |
| uses: "./.github/actions/uv_setup" | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - uses: actions/download-artifact@v6 | |
| with: | |
| name: dist | |
| path: ${{ env.WORKING_DIR }}/dist/ | |
| - name: Publish package distributions to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: ${{ env.WORKING_DIR }}/dist/ | |
| verbose: true | |
| print-hash: true | |
| attestations: false | |
| mark-release: | |
| # Marks the GitHub release with the new version tag | |
| needs: | |
| - build | |
| - release-notes | |
| - test-pypi-publish | |
| - pre-release-checks | |
| - publish | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| defaults: | |
| run: | |
| working-directory: ${{ env.WORKING_DIR }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Set up Python + uv | |
| uses: "./.github/actions/uv_setup" | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - uses: actions/download-artifact@v6 | |
| with: | |
| name: dist | |
| path: ${{ env.WORKING_DIR }}/dist/ | |
| - name: Create Tag | |
| uses: ncipollo/release-action@v1 | |
| with: | |
| artifacts: "${{ env.WORKING_DIR }}/dist/*" | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| generateReleaseNotes: false | |
| tag: ${{ needs.build.outputs.pkg-name }}==${{ needs.build.outputs.version }} | |
| body: ${{ needs.release-notes.outputs.release-body }} | |
| commit: ${{ github.sha }} | |
| makeLatest: true |