name: Publish to PyPI on: workflow_dispatch: inputs: release_tag: description: 'Release tag to publish' required: false type: string jobs: build: name: Build packages runs-on: ubuntu-latest permissions: contents: read steps: - name: Determine release tag id: release run: | if [ -n "${{ inputs.release_tag }}" ]; then echo "tag=${{ inputs.release_tag }}" >> $GITHUB_OUTPUT else echo "tag=" >> $GITHUB_OUTPUT fi - uses: actions/checkout@v4 with: ref: ${{ steps.release.outputs.tag || github.ref }} - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v4 - name: Build packages run: | uv build --all-packages rm dist/.gitignore - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: dist path: dist/ publish: name: Publish to PyPI needs: build runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/crewai permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.release_tag || github.ref }} - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.8.4" python-version: "3.12" enable-cache: false - name: Download artifacts uses: actions/download-artifact@v4 with: name: dist path: dist - name: Publish to PyPI env: UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} run: | failed=0 for package in dist/*; do if [[ "$package" == *"crewai_devtools"* ]]; then echo "Skipping private package: $package" continue fi echo "Publishing $package" if ! uv publish "$package"; then echo "Failed to publish $package" failed=1 fi done if [ $failed -eq 1 ]; then echo "Some packages failed to publish" exit 1 fi - name: Build Slack payload if: success() id: slack env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ inputs.release_tag }} run: | payload=$(uv run python -c " import json, re, subprocess, sys with open('lib/crewai/src/crewai/__init__.py') as f: m = re.search(r\"__version__\s*=\s*[\\\"']([^\\\"']+)\", f.read()) version = m.group(1) if m else 'unknown' import os tag = os.environ.get('RELEASE_TAG') or version try: r = subprocess.run(['gh','release','view',tag,'--json','body','-q','.body'], capture_output=True, text=True, check=True) body = r.stdout.strip() except Exception: body = '' blocks = [ {'type':'section','text':{'type':'mrkdwn', 'text':f':rocket: \`crewai v{version}\` published to PyPI'}}, {'type':'section','text':{'type':'mrkdwn', 'text':f' · '}}, {'type':'divider'}, ] if body: heading, items = '', [] for line in body.split('\n'): line = line.strip() if not line: continue hm = re.match(r'^#{2,3}\s+(.*)', line) if hm: if heading and items: skip = heading in ('What\\'s Changed','') or 'Contributors' in heading if not skip: txt = f'*{heading}*\n' + '\n'.join(f'• {i}' for i in items) blocks.append({'type':'section','text':{'type':'mrkdwn','text':txt}}) heading, items = hm.group(1), [] elif line.startswith('- ') or line.startswith('* '): items.append(re.sub(r'\*\*([^*]*)\*\*', r'*\1*', line[2:])) if heading and items: skip = heading in ('What\\'s Changed','') or 'Contributors' in heading if not skip: txt = f'*{heading}*\n' + '\n'.join(f'• {i}' for i in items) blocks.append({'type':'section','text':{'type':'mrkdwn','text':txt}}) blocks.append({'type':'divider'}) blocks.append({'type':'section','text':{'type':'mrkdwn', 'text':f'\`\`\`uv add \"crewai[tools]=={version}\"\`\`\`'}}) print(json.dumps({'blocks':blocks})) ") echo "payload=$payload" >> $GITHUB_OUTPUT - name: Notify Slack if: success() uses: slackapi/slack-github-action@v2.1.0 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook payload: ${{ steps.slack.outputs.payload }}