name: LFX Release run-name: LFX Release ${{ github.event.inputs.version || 'dev' }} by @${{ github.actor }} on: workflow_dispatch: inputs: version: description: "Version to release (e.g., 0.1.0)" required: true type: string publish_pypi: description: "Publish to PyPI" required: true type: boolean default: true build_docker: description: "Build and publish Docker images" required: true type: boolean default: true pre_release: description: "Mark as pre-release" required: false type: boolean default: false create_github_release: description: "Create GitHub release" required: true type: boolean default: true env: PYTHON_VERSION: "3.13" permissions: contents: write packages: write jobs: validate-version: name: Validate Version runs-on: ubuntu-latest outputs: should_release: ${{ steps.check.outputs.should_release }} current_version: ${{ steps.check.outputs.current_version }} steps: - name: Checkout code uses: actions/checkout@v5 - name: Setup Environment uses: astral-sh/setup-uv@v6 with: enable-cache: true cache-dependency-glob: "uv.lock" python-version: ${{ env.PYTHON_VERSION }} prune-cache: false - name: Check version id: check run: | cd src/lfx # Use uv tree to get package info, consistent with nightly workflow name=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $1}') version=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $2}') # Strip leading 'v' if present version=$(echo $version | sed 's/^v//') echo "current_version=$version" >> $GITHUB_OUTPUT if [ "$version" != "${{ github.event.inputs.version }}" ]; then echo "❌ Version mismatch: package has $version but input is ${{ github.event.inputs.version }}" echo "Please update the version in pyproject.toml first" echo "should_release=false" >> $GITHUB_OUTPUT exit 1 fi # Check if version already exists on PyPI if curl -s "https://pypi.org/pypi/lfx/json" | jq -r '.releases | keys[]' | grep -q "^${{ github.event.inputs.version }}$"; then echo "❌ Version ${{ github.event.inputs.version }} already exists on PyPI" echo "should_release=false" >> $GITHUB_OUTPUT exit 1 fi echo "✅ Version ${{ github.event.inputs.version }} is valid and not yet released" echo "should_release=true" >> $GITHUB_OUTPUT run-tests: name: Run Tests needs: validate-version if: needs.validate-version.outputs.should_release == 'true' runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - name: Checkout code uses: actions/checkout@v5 - name: Setup Environment uses: astral-sh/setup-uv@v6 with: enable-cache: true cache-dependency-glob: "uv.lock" python-version: ${{ matrix.python-version }} prune-cache: false - name: Run LFX tests run: | cd src/lfx make test - name: Test CLI installation run: | cd src/lfx uv pip install . uv run lfx --help uv run lfx run --help uv run lfx serve --help release-lfx: name: Build and Release LFX needs: [validate-version, run-tests] runs-on: ubuntu-latest outputs: version: ${{ steps.check-version.outputs.version }} steps: - name: Checkout code uses: actions/checkout@v5 - name: Setup Environment uses: astral-sh/setup-uv@v6 with: enable-cache: true cache-dependency-glob: "uv.lock" python-version: ${{ env.PYTHON_VERSION }} prune-cache: false - name: Install LFX dependencies run: uv sync --dev --package lfx - name: Verify Version id: check-version run: | cd src/lfx # Use uv tree to get package info, consistent with nightly workflow name=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $1}') version=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $2}') # Verify package name if [ "$name" != "lfx" ]; then echo "Package name $name does not match lfx. Exiting the workflow." exit 1 fi # Strip leading 'v' if present version=$(echo $version | sed 's/^v//') # Verify version matches input if [ "$version" != "${{ github.event.inputs.version }}" ]; then echo "Version $version does not match input ${{ github.event.inputs.version }}. Exiting the workflow." exit 1 fi echo "version=$version" >> $GITHUB_OUTPUT - name: Build distribution run: | cd src/lfx rm -rf dist/ uv build --wheel --out-dir dist - name: Check build artifacts run: | cd src/lfx ls -la dist/ # Verify wheel contents unzip -l dist/*.whl | grep -E "(lfx/__main__.py|lfx/cli/run.py|lfx/cli/commands.py)" - name: Test installation from wheel run: | cd src/lfx uv pip install dist/*.whl --force-reinstall uv run lfx --help echo "LFX CLI test completed successfully" - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: lfx-dist path: src/lfx/dist/ retention-days: 5 - name: Publish to PyPI if: github.event.inputs.publish_pypi == 'true' env: UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} run: | cd src/lfx uv publish dist/*.whl build-docker: name: Build Docker Images needs: [validate-version, run-tests] if: github.event.inputs.build_docker == 'true' runs-on: ubuntu-latest strategy: matrix: variant: [production, alpine] steps: - name: Checkout code uses: actions/checkout@v5 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Docker System Info and Cleanup run: | echo "=== Docker System Usage Before Cleanup ===" docker system df || true docker buildx du || true echo "=== Cleaning up Docker System ===" docker system prune -af --volumes || true docker buildx prune -af || true echo "=== Docker System Usage After Cleanup ===" docker system df || true docker buildx du || true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Prepare Docker metadata id: meta uses: docker/metadata-action@v5 with: images: | langflowai/lfx ghcr.io/langflow-ai/lfx tags: | type=raw,value=${{ github.event.inputs.version }}${{ matrix.variant == 'alpine' && '-alpine' || '' }} type=raw,value=latest${{ matrix.variant == 'alpine' && '-alpine' || '' }},enable=${{ github.event.inputs.pre_release == 'false' }} labels: | org.opencontainers.image.title=LFX org.opencontainers.image.description=Langflow Executor - CLI tool for running Langflow AI workflows org.opencontainers.image.vendor=Langflow org.opencontainers.image.version=${{ github.event.inputs.version }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: src/lfx/docker/Dockerfile${{ matrix.variant == 'alpine' && '.alpine' || '' }} platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | LFX_VERSION=${{ github.event.inputs.version }} create-release: name: Create GitHub Release needs: [release-lfx, build-docker] if: always() && github.event.inputs.create_github_release == 'true' && needs.release-lfx.result == 'success' runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v5 - name: Download artifacts uses: actions/download-artifact@v5 with: name: lfx-dist path: dist/ - name: Generate release notes id: notes run: | cat > release_notes.md << EOF # LFX ${{ github.event.inputs.version }} ## 🚀 Installation ### PyPI \`\`\`bash pip install lfx==${{ github.event.inputs.version }} # or uv pip install lfx==${{ github.event.inputs.version }} # or run without installing uvx lfx@${{ github.event.inputs.version }} --help \`\`\` ### Docker \`\`\`bash # Standard image docker pull langflowai/lfx:${{ github.event.inputs.version }} # Alpine image (smaller) docker pull langflowai/lfx:${{ github.event.inputs.version }}-alpine # Run a flow docker run --rm -v \$(pwd):/app/data langflowai/lfx:${{ github.event.inputs.version }} lfx run flow.json --input-value "Hello" \`\`\` ## 📦 What's New ## 📋 Checksums \`\`\` $(cd dist && sha256sum *) \`\`\` --- **Full Changelog**: https://github.com/${{ github.repository }}/compare/v${{ needs.validate-version.outputs.current_version }}...lfx-v${{ github.event.inputs.version }} EOF - name: Create Release uses: softprops/action-gh-release@v2 with: tag_name: lfx-v${{ github.event.inputs.version }} name: LFX ${{ github.event.inputs.version }} body_path: release_notes.md draft: false prerelease: ${{ github.event.inputs.pre_release }} files: | dist/* generate_release_notes: true test-release: name: Test Release needs: [release-lfx, build-docker] if: always() && (needs.release-lfx.result == 'success' || needs.build-docker.result == 'success') runs-on: ubuntu-latest steps: - name: Wait for PyPI propagation if: needs.release-lfx.result == 'success' run: sleep 60 - name: Test PyPI installation if: needs.release-lfx.result == 'success' run: | # Test installation using uv uv pip install lfx==${{ github.event.inputs.version }} uv run lfx --help - name: Test Docker image if: needs.build-docker.result == 'success' run: | # Test standard image docker run --rm langflowai/lfx:${{ github.event.inputs.version }} lfx --help # Test alpine image docker run --rm langflowai/lfx:${{ github.event.inputs.version }}-alpine lfx --help # Test with a simple flow cat > test_flow.json << 'EOF' { "nodes": [], "edges": [] } EOF docker run --rm -v $(pwd):/app/data langflowai/lfx:${{ github.event.inputs.version }} \ lfx run /app/data/test_flow.json --input-value "test" || true notify: name: Notify Release Status needs: [create-release, test-release] if: always() runs-on: ubuntu-latest steps: - name: Notify success if: needs.create-release.result == 'success' run: | echo "✅ LFX ${{ github.event.inputs.version }} released successfully!" echo "PyPI: https://pypi.org/project/lfx/${{ github.event.inputs.version }}/" echo "Docker Hub: https://hub.docker.com/r/langflowai/lfx/tags" echo "GitHub Release: https://github.com/${{ github.repository }}/releases/tag/lfx-v${{ github.event.inputs.version }}" - name: Notify failure if: needs.create-release.result != 'success' run: | echo "❌ LFX ${{ github.event.inputs.version }} release failed!" exit 1