refactor(ci): use only one file for publishing to pypi #25
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
| name: Release to PyPI | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - "packages/**" | |
| - ".github/workflows/release.yml" | |
| workflow_dispatch: | |
| inputs: | |
| bump_type: | |
| description: "Version bump type" | |
| required: false | |
| type: choice | |
| default: "patch" | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| prerelease: | |
| description: "Is this a pre-release? (e.g., b1, rc1) - leave empty for stable release" | |
| required: false | |
| type: string | |
| release_type: | |
| description: "Release type" | |
| required: false | |
| type: choice | |
| default: "alpha" | |
| options: | |
| - alpha | |
| - stable | |
| jobs: | |
| # Run all CI checks first | |
| ci: | |
| uses: ./.github/workflows/ci.yml | |
| release: | |
| name: ${{ github.event_name == 'push' && 'Alpha Release' || (github.event.inputs.release_type == 'alpha' && 'Alpha Release' || 'Stable Release') }} | |
| needs: ci | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write # Required for PyPI trusted publishing | |
| contents: write # Required for creating releases and pushing tags | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v4 | |
| - name: Install commitizen | |
| run: uv pip install --system commitizen | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'stable' | |
| - name: Configure git | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'stable' | |
| - name: Determine release mode | |
| id: mode | |
| run: | | |
| if [ "${{ github.event_name }}" == "push" ]; then | |
| echo "mode=alpha" >> $GITHUB_OUTPUT | |
| echo "Auto-triggered alpha release" | |
| elif [ "${{ github.event.inputs.release_type }}" == "alpha" ]; then | |
| echo "mode=alpha" >> $GITHUB_OUTPUT | |
| echo "Manual alpha release" | |
| else | |
| echo "mode=stable" >> $GITHUB_OUTPUT | |
| echo "Manual stable release" | |
| fi | |
| - name: Calculate version | |
| id: version | |
| run: | | |
| MODE="${{ steps.mode.outputs.mode }}" | |
| if [ "$MODE" == "alpha" ]; then | |
| # Alpha version calculation | |
| TOTAL_COMMITS=$(git rev-list --count HEAD) | |
| CURRENT_VERSION=$(grep -m 1 -A 5 '^\[project\]' pyproject.toml | grep 'version = ' | sed 's/version = "\(.*\)"/\1/') | |
| BASE_VERSION=$(echo $CURRENT_VERSION | sed 's/a.*//') | |
| VERSION="${BASE_VERSION}a${TOTAL_COMMITS}" | |
| echo "Alpha version: $VERSION" | |
| echo "is_prerelease=true" >> $GITHUB_OUTPUT | |
| else | |
| # Stable release with commitizen | |
| if [ -n "${{ github.event.inputs.prerelease }}" ]; then | |
| # Prerelease (e.g., 0.1.0b1, 0.1.0rc1) | |
| cz bump --increment ${{ github.event.inputs.bump_type }} --yes --no-verify | |
| NEW_VERSION=$(grep -m 1 -A 5 '^\[project\]' pyproject.toml | grep 'version = ' | sed 's/version = "\(.*\)"/\1/') | |
| VERSION="${NEW_VERSION}${{ github.event.inputs.prerelease }}" | |
| echo "Prerelease version: $VERSION" | |
| echo "is_prerelease=true" >> $GITHUB_OUTPUT | |
| else | |
| # Stable release (e.g., 0.1.0, 1.0.0) | |
| cz bump --increment ${{ github.event.inputs.bump_type }} --yes | |
| VERSION=$(grep -m 1 -A 5 '^\[project\]' pyproject.toml | grep 'version = ' | sed 's/version = "\(.*\)"/\1/') | |
| echo "Stable version: $VERSION" | |
| echo "is_prerelease=false" >> $GITHUB_OUTPUT | |
| fi | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Final version: $VERSION" | |
| - name: Update version files | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| IS_PRERELEASE="${{ steps.version.outputs.is_prerelease }}" | |
| python3 << 'PYTHON_SCRIPT' | |
| import re | |
| version = "${{ steps.version.outputs.version }}" | |
| is_prerelease = "${{ steps.version.outputs.is_prerelease }}" == "true" | |
| base_version = version.split('a')[0].split('b')[0].split('rc')[0] | |
| # Update pyproject.toml files | |
| toml_files = [ | |
| 'pyproject.toml', | |
| 'packages/myfy-core/pyproject.toml', | |
| 'packages/myfy-web/pyproject.toml', | |
| 'packages/myfy-cli/pyproject.toml', | |
| 'packages/myfy-frontend/pyproject.toml', | |
| 'packages/myfy/pyproject.toml' | |
| ] | |
| for file in toml_files: | |
| with open(file, 'r') as f: | |
| content = f.read() | |
| content = re.sub( | |
| r'(\[project\][^\[]*\nname = [^\n]+\n)version = "[^"]*"', | |
| r'\1version = "' + version + '"', | |
| content, | |
| count=1 | |
| ) | |
| with open(file, 'w') as f: | |
| f.write(content) | |
| # Update version.py files | |
| version_files = [ | |
| 'packages/myfy-core/myfy/core/version.py', | |
| 'packages/myfy-web/myfy/web/version.py', | |
| 'packages/myfy-cli/myfy_cli/version.py', | |
| 'packages/myfy-frontend/myfy/frontend/version.py', | |
| 'packages/myfy/myfy/version.py' | |
| ] | |
| for file in version_files: | |
| with open(file, 'r') as f: | |
| content = f.read() | |
| content = re.sub(r'__version__ = "[^"]*"', f'__version__ = "{version}"', content) | |
| with open(file, 'w') as f: | |
| f.write(content) | |
| # Update dependency constraints | |
| dep_files = [ | |
| 'packages/myfy-web/pyproject.toml', | |
| 'packages/myfy-cli/pyproject.toml', | |
| 'packages/myfy-frontend/pyproject.toml', | |
| 'packages/myfy/pyproject.toml' | |
| ] | |
| # For prereleases (alpha/beta/rc), use exact version | |
| # For stable releases, use compatible release | |
| if is_prerelease: | |
| constraint = f'=={version}' | |
| else: | |
| constraint = f'~={base_version}' | |
| for file in dep_files: | |
| with open(file, 'r') as f: | |
| content = f.read() | |
| content = re.sub(r'myfy-core[~=]=?[0-9a-z.]+', f'myfy-core{constraint}', content) | |
| content = re.sub(r'myfy-web[~=]=?[0-9a-z.]+', f'myfy-web{constraint}', content) | |
| content = re.sub(r'myfy-cli[~=]=?[0-9a-z.]+', f'myfy-cli{constraint}', content) | |
| with open(file, 'w') as f: | |
| f.write(content) | |
| print(f"Version updated to {version}") | |
| PYTHON_SCRIPT | |
| - name: Commit and tag (stable releases only) | |
| if: steps.mode.outputs.mode == 'stable' | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| git add . | |
| git commit -m "bump: version to ${VERSION}" || true | |
| git tag -a "v${VERSION}" -m "Release v${VERSION}" || true | |
| git push origin main | |
| git push origin "v${VERSION}" | |
| - name: Build packages | |
| run: | | |
| # Build in dependency order using --package flag | |
| echo "Building myfy-core..." | |
| uv build --package myfy-core --out-dir packages/myfy-core/dist | |
| echo "Building myfy-web..." | |
| uv build --package myfy-web --out-dir packages/myfy-web/dist | |
| echo "Building myfy-cli..." | |
| uv build --package myfy-cli --out-dir packages/myfy-cli/dist | |
| echo "Building myfy-frontend..." | |
| uv build --package myfy-frontend --out-dir packages/myfy-frontend/dist | |
| echo "Building myfy meta-package..." | |
| uv build --package myfy --out-dir packages/myfy/dist | |
| echo "All packages built successfully" | |
| - name: List built packages | |
| run: | | |
| echo "myfy-core:" | |
| ls -lh packages/myfy-core/dist/ | |
| echo -e "\nmyfy-web:" | |
| ls -lh packages/myfy-web/dist/ | |
| echo -e "\nmyfy-cli:" | |
| ls -lh packages/myfy-cli/dist/ | |
| echo -e "\nmyfy-frontend:" | |
| ls -lh packages/myfy-frontend/dist/ | |
| echo -e "\nmyfy:" | |
| ls -lh packages/myfy/dist/ | |
| - name: Generate release notes (stable releases only) | |
| if: steps.mode.outputs.mode == 'stable' | |
| id: release_notes | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # Create release notes from git log | |
| echo "## What's Changed" > release_notes.md | |
| echo "" >> release_notes.md | |
| # Get commits since last tag | |
| PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") | |
| if [ -n "$PREVIOUS_TAG" ]; then | |
| git log ${PREVIOUS_TAG}..HEAD --pretty=format:"- %s (%h)" >> release_notes.md | |
| else | |
| echo "Initial release" >> release_notes.md | |
| fi | |
| echo "" >> release_notes.md | |
| echo "## Packages" >> release_notes.md | |
| echo "- myfy-core $VERSION" >> release_notes.md | |
| echo "- myfy-web $VERSION" >> release_notes.md | |
| echo "- myfy-cli $VERSION" >> release_notes.md | |
| echo "- myfy-frontend $VERSION" >> release_notes.md | |
| echo "- myfy $VERSION" >> release_notes.md | |
| - name: Create GitHub Release (stable releases only) | |
| if: steps.mode.outputs.mode == 'stable' | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: v${{ steps.version.outputs.version }} | |
| name: Release v${{ steps.version.outputs.version }} | |
| body_path: release_notes.md | |
| draft: false | |
| prerelease: ${{ steps.version.outputs.is_prerelease }} | |
| files: | | |
| packages/myfy-core/dist/* | |
| packages/myfy-web/dist/* | |
| packages/myfy-cli/dist/* | |
| packages/myfy-frontend/dist/* | |
| packages/myfy/dist/* | |
| - name: Publish myfy-core to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: packages/myfy-core/dist/ | |
| verbose: true | |
| - name: Publish myfy-web to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: packages/myfy-web/dist/ | |
| verbose: true | |
| - name: Publish myfy-cli to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: packages/myfy-cli/dist/ | |
| verbose: true | |
| - name: Publish myfy-frontend to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: packages/myfy-frontend/dist/ | |
| verbose: true | |
| - name: Publish myfy to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: packages/myfy/dist/ | |
| verbose: true | |
| validate-release: | |
| name: Validate Release on Python ${{ matrix.python-version }} | |
| needs: release | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| python-version: ["3.12", "3.13"] | |
| steps: | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Wait for PyPI propagation | |
| run: sleep 60 | |
| - name: Install and validate all packages | |
| run: | | |
| pip install myfy[all] --no-cache-dir | |
| python -c " | |
| import myfy | |
| import myfy.core | |
| import myfy.web | |
| import myfy_cli | |
| import myfy.frontend | |
| print(f'myfy version: {myfy.__version__}') | |
| print(f'myfy-core version: {myfy.core.__version__}') | |
| print(f'myfy-web version: {myfy.web.__version__}') | |
| print(f'myfy-cli version: {myfy_cli.__version__}') | |
| print(f'myfy-frontend version: {myfy.frontend.__version__}') | |
| # Verify all versions match | |
| versions = [myfy.__version__, myfy.core.__version__, myfy.web.__version__, myfy_cli.__version__, myfy.frontend.__version__] | |
| assert len(set(versions)) == 1, f'Version mismatch: {versions}' | |
| print('✓ All versions match') | |
| " | |
| - name: Test CLI | |
| run: | | |
| myfy --help | |
| echo "✓ CLI works" |