Release Python #8
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 Python | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| package: | |
| description: 'Python package to release' | |
| required: true | |
| type: choice | |
| options: | |
| - afm-core | |
| - afm-langchain | |
| branch: | |
| description: 'Branch to release from' | |
| required: false | |
| default: 'main' | |
| type: string | |
| concurrency: | |
| group: release-langchain-interpreter | |
| cancel-in-progress: false | |
| jobs: | |
| validate: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| release_version: ${{ steps.version.outputs.RELEASE_VERSION }} | |
| next_version: ${{ steps.version.outputs.NEXT_VERSION }} | |
| tag: ${{ steps.version.outputs.TAG }} | |
| steps: | |
| - name: Validate branch format | |
| env: | |
| BRANCH: ${{ inputs.branch }} | |
| run: | | |
| if [[ ! "$BRANCH" =~ ^[a-zA-Z0-9._/-]+$ ]]; then | |
| echo "::error::Invalid branch name: $BRANCH" | |
| exit 1 | |
| fi | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.branch }} | |
| fetch-depth: 0 | |
| - name: Read and validate version | |
| id: version | |
| env: | |
| PACKAGE: ${{ inputs.package }} | |
| run: | | |
| VERSION_FILE="langchain-interpreter/packages/$PACKAGE/pyproject.toml" | |
| if [ ! -f "$VERSION_FILE" ]; then | |
| echo "::error::Version file not found: $VERSION_FILE" | |
| exit 1 | |
| fi | |
| RELEASE_VERSION=$(grep '^version = ' "$VERSION_FILE" | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Version in $VERSION_FILE must be in format X.Y.Z, got: $RELEASE_VERSION" | |
| exit 1 | |
| fi | |
| MAJOR=$(echo "$RELEASE_VERSION" | cut -d. -f1) | |
| MINOR=$(echo "$RELEASE_VERSION" | cut -d. -f2) | |
| PATCH=$(echo "$RELEASE_VERSION" | cut -d. -f3) | |
| NEXT_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" | |
| TAG="$PACKAGE-v$RELEASE_VERSION" | |
| echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_OUTPUT | |
| echo "NEXT_VERSION=$NEXT_VERSION" >> $GITHUB_OUTPUT | |
| echo "TAG=$TAG" >> $GITHUB_OUTPUT | |
| echo "Releasing $PACKAGE $RELEASE_VERSION (tag: $TAG), next will be $NEXT_VERSION" | |
| - name: Check tag doesn't already exist | |
| env: | |
| TAG: ${{ steps.version.outputs.TAG }} | |
| run: | | |
| if git rev-parse "$TAG" >/dev/null 2>&1; then | |
| echo "::error::Tag $TAG already exists. Use re-release workflow to overwrite." | |
| exit 1 | |
| fi | |
| - name: Check release branch doesn't already exist | |
| env: | |
| TAG: ${{ steps.version.outputs.TAG }} | |
| VERSION: ${{ steps.version.outputs.RELEASE_VERSION }} | |
| PACKAGE: ${{ inputs.package }} | |
| run: | | |
| RELEASE_BRANCH="release-$PACKAGE-$VERSION" | |
| if git ls-remote --exit-code --heads origin "$RELEASE_BRANCH" >/dev/null 2>&1; then | |
| echo "::error::Release branch $RELEASE_BRANCH already exists. Use re-release workflow to overwrite." | |
| exit 1 | |
| fi | |
| test: | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: langchain-interpreter | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.branch }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies and run tests | |
| run: | | |
| uv sync --dev | |
| uv run pytest packages/afm-core/tests/ packages/afm-langchain/tests/ | |
| pypi-publish: | |
| needs: [validate, test] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.branch }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Build packages | |
| working-directory: langchain-interpreter | |
| env: | |
| PACKAGE: ${{ inputs.package }} | |
| run: | | |
| if [ "$PACKAGE" = "afm-core" ]; then | |
| uv build --package afm-core | |
| uv build --package afm-cli | |
| elif [ "$PACKAGE" = "afm-langchain" ]; then | |
| uv build --package afm-langchain | |
| fi | |
| - name: Publish to PyPI | |
| working-directory: langchain-interpreter | |
| env: | |
| PACKAGE: ${{ inputs.package }} | |
| UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} | |
| run: | | |
| if [ "$PACKAGE" = "afm-core" ]; then | |
| uv publish dist/afm_core-* | |
| uv publish dist/afm_cli-* | |
| elif [ "$PACKAGE" = "afm-langchain" ]; then | |
| uv publish dist/afm_langchain-* | |
| fi | |
| docker: | |
| needs: [validate, pypi-publish] | |
| if: inputs.package == 'afm-langchain' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| packages: write | |
| security-events: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.branch }} | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Determine Docker tags | |
| id: docker-tags | |
| env: | |
| OWNER: ${{ github.repository_owner }} | |
| VERSION: ${{ needs.validate.outputs.release_version }} | |
| UPDATE_LATEST: ${{ inputs.branch == 'main' || inputs.branch == 'dev' }} | |
| run: | | |
| OWNER_LOWER=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]') | |
| IMAGE_NAME="ghcr.io/$OWNER_LOWER/afm-langchain-interpreter" | |
| TAGS="$IMAGE_NAME:v$VERSION" | |
| if [ "$UPDATE_LATEST" = "true" ]; then | |
| TAGS="$TAGS,$IMAGE_NAME:latest" | |
| fi | |
| echo "TAGS=$TAGS" >> $GITHUB_OUTPUT | |
| echo "IMAGE_NAME=$IMAGE_NAME" >> $GITHUB_OUTPUT | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: langchain-interpreter | |
| push: true | |
| platforms: linux/amd64,linux/arm64 | |
| tags: ${{ steps.docker-tags.outputs.TAGS }} | |
| labels: | | |
| org.opencontainers.image.source=https://github.com/${{ github.repository }} | |
| org.opencontainers.image.version=${{ needs.validate.outputs.release_version }} | |
| org.opencontainers.image.revision=${{ github.sha }} | |
| org.opencontainers.image.title=AFM LangChain Interpreter | |
| org.opencontainers.image.licenses=Apache-2.0 | |
| annotations: | | |
| index:org.opencontainers.image.source=https://github.com/${{ github.repository }} | |
| index:org.opencontainers.image.licenses=Apache-2.0 | |
| - name: Scan Docker image for vulnerabilities | |
| uses: aquasecurity/trivy-action@0.33.1 | |
| with: | |
| image-ref: ${{ steps.docker-tags.outputs.IMAGE_NAME }}:v${{ needs.validate.outputs.release_version }} | |
| format: 'sarif' | |
| output: 'trivy-results.sarif' | |
| severity: 'CRITICAL,HIGH' | |
| limit-severities-for-sarif: true | |
| exit-code: '1' | |
| - name: Upload Trivy scan results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@v4 | |
| if: always() | |
| with: | |
| sarif_file: 'trivy-results.sarif' | |
| finalize: | |
| needs: [validate, pypi-publish, docker] | |
| if: | | |
| ${{ !cancelled() | |
| && needs.validate.result == 'success' | |
| && needs.pypi-publish.result == 'success' | |
| && (needs.docker.result == 'success' | |
| || needs.docker.result == 'skipped') }} | |
| uses: ./.github/workflows/release-finalize.yml | |
| with: | |
| tag: ${{ needs.validate.outputs.tag }} | |
| implementation: langchain-interpreter | |
| version: ${{ needs.validate.outputs.release_version }} | |
| branch: ${{ inputs.branch }} | |
| is_rerelease: false | |
| permissions: | |
| contents: write | |
| bump-version: | |
| needs: [validate, finalize] | |
| if: | | |
| ${{ !cancelled() | |
| && needs.validate.result == 'success' | |
| && needs.finalize.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.branch }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Bump version to next | |
| env: | |
| BRANCH: ${{ inputs.branch }} | |
| PACKAGE: ${{ inputs.package }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git pull origin "$BRANCH" --rebase | |
| cd langchain-interpreter | |
| if [ "$PACKAGE" = "afm-core" ]; then | |
| uv version --bump patch --package afm-core --frozen | |
| uv version --bump patch --package afm-cli --frozen | |
| # Sync the afm-core exact pin in afm-cli's dependencies | |
| NEW_CORE_VERSION=$(uv version --short --package afm-core) | |
| sed -i "s/\"afm-core==.*\"/\"afm-core==$NEW_CORE_VERSION\"/" packages/afm-cli/pyproject.toml | |
| elif [ "$PACKAGE" = "afm-langchain" ]; then | |
| uv version --bump patch --package afm-langchain --frozen | |
| fi | |
| cd .. | |
| git add langchain-interpreter/packages/ | |
| git commit -m "Bump langchain-interpreter $PACKAGE versions to next patch" | |
| git push origin "$BRANCH" |