🛞 fix(ci): brew install 🛞 #76
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: Pull Request | |
| on: | |
| pull_request: | |
| branches: | |
| - master | |
| permissions: | |
| contents: write # Needed to push QR code updates | |
| pull-requests: write | |
| id-token: write # Required for OIDC trusted publishing to TestPyPI | |
| jobs: | |
| test: | |
| name: Test | |
| uses: ./.github/workflows/test.yml | |
| qr: | |
| name: QR Codes | |
| needs: test | |
| runs-on: ubuntu-latest | |
| # Skip if triggered by QR code update commit to prevent infinite loop | |
| if: "!contains(github.event.head_commit.message, '[skip-qr]')" | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.head_ref }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install dependencies | |
| run: make install | |
| - name: Generate QR codes | |
| run: make qr | |
| - name: Commit if changed | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add assets/ | |
| if ! git diff --cached --quiet; then | |
| git commit -m "Update QR codes [skip-qr]" | |
| git push | |
| fi | |
| version: | |
| name: Version Preview | |
| needs: test # Only run after tests pass | |
| runs-on: ubuntu-latest | |
| outputs: | |
| pypi_version: ${{ steps.set-version.outputs.pypi_version }} | |
| environment: | |
| name: testpypi | |
| url: https://test.pypi.org/p/${{ github.event.repository.name }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 # Need full history for version calculation | |
| - name: Calculate next version (dry run) | |
| id: version | |
| uses: anothrNick/github-tag-action@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| DRY_RUN: true | |
| DEFAULT_BUMP: minor | |
| WITH_V: true | |
| PRERELEASE: true | |
| PRERELEASE_SUFFIX: rc | |
| BRANCH_HISTORY: compare | |
| INITIAL_VERSION: 1.0.3 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Calculate prerelease version | |
| id: set-version | |
| run: | | |
| # Get base version from tag action (e.g., v1.2.0-rc.0) | |
| BASE_TAG="${NEW_TAG}" | |
| # Strip 'v' prefix and '-rc.X' suffix to get base version (e.g., 1.2.0) | |
| BASE_VERSION="${BASE_TAG#v}" | |
| BASE_VERSION="${BASE_VERSION%-rc.*}" | |
| # Create unique prerelease version: {base}rc{pr_number}.dev{run_number} | |
| # e.g., 1.2.0rc42.dev7 for PR #42, run #7 | |
| PYPI_VERSION="${BASE_VERSION}rc${{ github.event.pull_request.number }}.dev${{ github.run_number }}" | |
| echo "Tag version: $NEW_TAG" | |
| echo "PyPI version: $PYPI_VERSION" | |
| echo "pypi_version=$PYPI_VERSION" >> $GITHUB_OUTPUT | |
| env: | |
| NEW_TAG: ${{ steps.version.outputs.new_tag }} | |
| - name: Build distributions | |
| env: | |
| SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.set-version.outputs.pypi_version }} | |
| run: | | |
| uv build --sdist | |
| uv build --wheel | |
| - name: Publish to TestPyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| repository-url: https://test.pypi.org/legacy/ | |
| # Uses OIDC trusted publishing - configure at: | |
| # https://test.pypi.org/manage/account/publishing/ | |
| - name: Comment version info on PR | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const oldTag = '${{ steps.version.outputs.old_tag }}' || 'none'; | |
| const newTag = '${{ steps.version.outputs.new_tag }}'; | |
| const part = '${{ steps.version.outputs.part }}'; | |
| // Extract base version (remove -rc.X suffix for display) | |
| const baseVersion = newTag.replace(/-rc\.\d+$/, ''); | |
| const body = `## 📦 Version Preview | |
| - **Current version:** \`${oldTag}\` | |
| - **Bump type:** \`${part}\` | |
| - **Release version:** \`${baseVersion}\` | |
| - **TestPyPI version:** \`${newTag}\` | |
| When this PR is merged, version will be bumped to \`${baseVersion}\`. | |
| To change the bump type, include in commit message: \`#major\`, \`#minor\`, or \`#patch\``; | |
| // Find existing bot comment with version preview | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| const botComment = comments.find(c => | |
| c.user.type === 'Bot' && | |
| c.body.includes('## 📦 Version Preview') | |
| ); | |
| if (botComment) { | |
| // Check if version info changed by comparing release version and bump type | |
| const oldVersionMatch = botComment.body.match(/Release version:\*\* `([^`]+)`/); | |
| const oldBumpMatch = botComment.body.match(/Bump type:\*\* `([^`]+)`/); | |
| const oldReleaseVersion = oldVersionMatch ? oldVersionMatch[1] : ''; | |
| const oldBumpType = oldBumpMatch ? oldBumpMatch[1] : ''; | |
| if (oldReleaseVersion === baseVersion && oldBumpType === part) { | |
| console.log('Version info unchanged, skipping comment update'); | |
| return; | |
| } | |
| // Update existing comment | |
| await github.rest.issues.updateComment({ | |
| comment_id: botComment.id, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| console.log('Updated existing version comment'); | |
| } else { | |
| // Create new comment | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| console.log('Created new version comment'); | |
| } | |
| smoke-pypi: | |
| name: Smoke PyPI | |
| needs: version | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Wait for TestPyPI indexing | |
| run: | | |
| NAME="${{ github.event.repository.name }}" | |
| VERSION="${{ needs.version.outputs.pypi_version }}" | |
| MAX_ATTEMPTS=40 | |
| SLEEP_SECONDS=15 | |
| echo "Waiting for ${NAME}==${VERSION} to be available on TestPyPI..." | |
| for i in $(seq 1 $MAX_ATTEMPTS); do | |
| if curl -sf "https://test.pypi.org/pypi/${NAME}/${VERSION}/json" > /dev/null 2>&1; then | |
| echo "Package available after $((i * SLEEP_SECONDS)) seconds" | |
| exit 0 | |
| fi | |
| echo "Attempt $i/$MAX_ATTEMPTS: Package not yet available, waiting ${SLEEP_SECONDS}s..." | |
| sleep $SLEEP_SECONDS | |
| done | |
| echo "Timed out waiting for package after $((MAX_ATTEMPTS * SLEEP_SECONDS)) seconds" | |
| exit 1 | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Install from TestPyPI | |
| run: | | |
| pip install --index-url https://test.pypi.org/simple/ \ | |
| --extra-index-url https://pypi.org/simple/ \ | |
| ${{ github.event.repository.name }}==${{ needs.version.outputs.pypi_version }} | |
| - name: Run smoke tests | |
| run: python tests/smoke.py ${{ github.event.repository.name }} | |
| smoke-homebrew: | |
| name: Smoke Homebrew | |
| needs: test | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Create local tap and generate formula | |
| run: | | |
| NAME="${{ github.event.repository.name }}" | |
| REPO="${{ github.repository }}" | |
| # Create a local tap | |
| brew tap-new local/tap | |
| TAP_PATH="$(brew --repository)/Library/Taps/local/homebrew-tap/Formula" | |
| mkdir -p "$TAP_PATH" | |
| # Get description from pyproject.toml | |
| DESCRIPTION=$(grep '^description' pyproject.toml | sed 's/description = "\(.*\)"/\1/') | |
| # Convert name to Ruby class name (capitalize, remove hyphens) | |
| CLASS_NAME=$(echo "$NAME" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1' | tr -d ' ') | |
| # Build source line for HEAD install (points to PR branch) | |
| SOURCE_LINE="head \"https://github.com/${REPO}.git\", branch: \"${{ github.head_ref }}\"" | |
| # Generate formula from template into the tap | |
| sed -e "s|{{NAME}}|${NAME}|g" \ | |
| -e "s|{{CLASS_NAME}}|${CLASS_NAME}|g" \ | |
| -e "s|{{DESCRIPTION}}|${DESCRIPTION}|g" \ | |
| -e "s|{{HOMEPAGE}}|https://github.com/${REPO}|g" \ | |
| -e "s|{{SOURCE_LINE}}|${SOURCE_LINE}|g" \ | |
| .github/templates/formula.rb.template > "$TAP_PATH/${NAME}.rb" | |
| echo "Generated formula at $TAP_PATH/${NAME}.rb:" | |
| cat "$TAP_PATH/${NAME}.rb" | |
| - name: Install formula from HEAD | |
| run: brew install --HEAD local/tap/${{ github.event.repository.name }} | |
| - name: Run formula test | |
| run: brew test local/tap/${{ github.event.repository.name }} | |
| - name: Verify CLI works | |
| run: | | |
| ${{ github.event.repository.name }} version | |
| ${{ github.event.repository.name }} --help | |