[VOL-5495] Add publish/release actions for merge
Signed-off-by: Eric Ball <eball@linuxfoundation.org>
Change-Id: I05ef8897523e7c3d128767c3795b7e11f4b0169a
diff --git a/.github/actions/docker-publish-action/README.md b/.github/actions/docker-publish-action/README.md
new file mode 100644
index 0000000..c82e340
--- /dev/null
+++ b/.github/actions/docker-publish-action/README.md
@@ -0,0 +1,80 @@
+<!--
+SPDX-License-Identifier: Apache-2.0
+SPDX-FileCopyrightText: 2026 The Linux Foundation
+-->
+
+# Docker Publish Action
+
+A composite GitHub Action that builds and pushes Docker images tagged with a branch name and git tags.
+
+## Description
+
+Builds Docker images using `make docker-build` and pushes them with `make docker-push`. Images are tagged with the caller-specified branch name and any git tags pointing at HEAD. Golang-style version tags (e.g., `v1.2.3`) have the leading `v` stripped automatically.
+
+## Usage
+
+```yaml
+- uses: ./.github/actions/docker-publish-action
+ with:
+ docker-registry: "docker.io"
+ docker-repository: "myorg"
+ image-name: "myapp"
+ branch-tag: ${{ github.ref_name }}
+ docker-username: ${{ secrets.DOCKER_USERNAME }}
+ docker-password: ${{ secrets.DOCKER_PASSWORD }}
+```
+
+### Inputs
+
+| Input | Required | Default | Description |
+|-------|----------|---------|-------------|
+| `docker-registry` | No | `docker.io` | Docker registry URL |
+| `docker-repository` | Yes | — | Docker repository prefix |
+| `image-name` | Yes | — | Name of the Docker image |
+| `branch-tag` | Yes | — | Branch name to use as a Docker image tag |
+| `extra-environment-vars` | No | `""` | Extra environment variables for make commands |
+| `docker-username` | Yes | — | Docker registry username |
+| `docker-password` | Yes | — | Docker registry password |
+
+### Example Workflow
+
+```yaml
+name: Publish Docker Image
+
+on:
+ push:
+ branches: [main]
+ tags: ["v*"]
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: ./.github/actions/docker-publish-action
+ with:
+ docker-registry: "docker.io"
+ docker-repository: "voltha"
+ image-name: "voltha-northbound"
+ branch-tag: ${{ github.ref_name }}
+ docker-username: ${{ secrets.DOCKER_USERNAME }}
+ docker-password: ${{ secrets.DOCKER_PASSWORD }}
+```
+
+## How It Works
+
+1. Logs in to the configured Docker registry
+2. Discovers git tags pointing at the current HEAD commit
+3. Runs `make docker-build` with `DOCKER_TAG` set to the `branch-tag` input
+4. Runs `make docker-build` for each git tag (stripping the `v` prefix)
+5. Runs `make docker-push` for the branch tag and each git tag
+6. Logs out of the Docker registry
+
+## Requirements
+
+- The repository must have a `Makefile` with `docker-build` and `docker-push` targets
+- These targets must respect `DOCKER_TAG`, `DOCKER_REGISTRY`, `DOCKER_REPOSITORY`, and `IMAGE_NAME` environment variables
+- The checkout step should use `fetch-depth: 0` to ensure git tags are available
diff --git a/.github/actions/docker-publish-action/action.yaml b/.github/actions/docker-publish-action/action.yaml
new file mode 100644
index 0000000..63e62bd
--- /dev/null
+++ b/.github/actions/docker-publish-action/action.yaml
@@ -0,0 +1,144 @@
+---
+# SPDX-FileCopyrightText: 2026 The Linux Foundation
+# SPDX-License-Identifier: Apache-2.0
+
+name: Docker Publish
+description: "Build and push Docker images tagged with branch name and git tags"
+
+inputs:
+ docker-registry:
+ description: "Docker registry URL"
+ required: false
+ default: "docker.io"
+ docker-repository:
+ description: "Docker repository prefix"
+ required: true
+ image-name:
+ description: "Name of the Docker image"
+ required: true
+ branch-tag:
+ description: "Branch name to use as a Docker image tag"
+ required: true
+ extra-environment-vars:
+ description: "Extra environment variables for make commands"
+ required: false
+ default: ""
+ docker-username:
+ description: "Docker registry username"
+ required: true
+ docker-password:
+ description: "Docker registry password"
+ required: true
+
+runs:
+ using: "composite"
+ steps:
+ - name: Log in to Docker registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ inputs.docker-registry }}
+ username: ${{ inputs.docker-username }}
+ password: ${{ inputs.docker-password }}
+
+ - name: Get git tags on HEAD
+ id: git-tags
+ shell: bash
+ run: |
+ set -eu -o pipefail
+ TAGS=$(git tag --points-at HEAD || true)
+ echo "tags<<EOF" >> "$GITHUB_OUTPUT"
+ echo "$TAGS" >> "$GITHUB_OUTPUT"
+ echo "EOF" >> "$GITHUB_OUTPUT"
+
+ - name: Build Docker image with branch tag
+ shell: bash
+ run: |
+ set -eu -o pipefail
+ DOCKER_REGISTRY="${{ inputs.docker-registry }}"
+ DOCKER_REPOSITORY="${{ inputs.docker-repository }}"
+ IMAGE_NAME="${{ inputs.image-name }}"
+ BRANCH_NAME="${{ inputs.branch-tag }}"
+
+ echo "Building image with branch tag: ${BRANCH_NAME}"
+ ${{ inputs.extra-environment-vars }} \
+ DOCKER_TAG="${BRANCH_NAME}" \
+ DOCKER_REGISTRY="${DOCKER_REGISTRY}" \
+ DOCKER_REPOSITORY="${DOCKER_REPOSITORY}" \
+ IMAGE_NAME="${IMAGE_NAME}" \
+ make docker-build
+
+ - name: Build Docker images for git tags
+ shell: bash
+ run: |
+ set -eu -o pipefail
+ DOCKER_REGISTRY="${{ inputs.docker-registry }}"
+ DOCKER_REPOSITORY="${{ inputs.docker-repository }}"
+ IMAGE_NAME="${{ inputs.image-name }}"
+
+ TAGS='${{ steps.git-tags.outputs.tags }}'
+ if [ -z "$TAGS" ]; then
+ echo "No git tags on HEAD, skipping tag builds"
+ exit 0
+ fi
+
+ while IFS= read -r tag; do
+ [ -z "$tag" ] && continue
+ # Strip leading 'v' from golang-style version tags
+ DOCKER_TAG="${tag#v}"
+ echo "Building image with tag: ${DOCKER_TAG}"
+ ${{ inputs.extra-environment-vars }} \
+ DOCKER_TAG="${DOCKER_TAG}" \
+ DOCKER_REGISTRY="${DOCKER_REGISTRY}" \
+ DOCKER_REPOSITORY="${DOCKER_REPOSITORY}" \
+ IMAGE_NAME="${IMAGE_NAME}" \
+ make docker-build
+ done <<< "$TAGS"
+
+ - name: Push Docker image with branch tag
+ shell: bash
+ run: |
+ set -eu -o pipefail
+ DOCKER_REGISTRY="${{ inputs.docker-registry }}"
+ DOCKER_REPOSITORY="${{ inputs.docker-repository }}"
+ IMAGE_NAME="${{ inputs.image-name }}"
+ BRANCH_NAME="${{ inputs.branch-tag }}"
+
+ echo "Pushing image with branch tag: ${BRANCH_NAME}"
+ ${{ inputs.extra-environment-vars }} \
+ DOCKER_TAG="${BRANCH_NAME}" \
+ DOCKER_REGISTRY="${DOCKER_REGISTRY}" \
+ DOCKER_REPOSITORY="${DOCKER_REPOSITORY}" \
+ IMAGE_NAME="${IMAGE_NAME}" \
+ make docker-push
+
+ - name: Push Docker images for git tags
+ shell: bash
+ run: |
+ set -eu -o pipefail
+ DOCKER_REGISTRY="${{ inputs.docker-registry }}"
+ DOCKER_REPOSITORY="${{ inputs.docker-repository }}"
+ IMAGE_NAME="${{ inputs.image-name }}"
+
+ TAGS='${{ steps.git-tags.outputs.tags }}'
+ if [ -z "$TAGS" ]; then
+ echo "No git tags on HEAD, skipping tag pushes"
+ exit 0
+ fi
+
+ while IFS= read -r tag; do
+ [ -z "$tag" ] && continue
+ # Strip leading 'v' from golang-style version tags
+ DOCKER_TAG="${tag#v}"
+ echo "Pushing image with tag: ${DOCKER_TAG}"
+ ${{ inputs.extra-environment-vars }} \
+ DOCKER_TAG="${DOCKER_TAG}" \
+ DOCKER_REGISTRY="${DOCKER_REGISTRY}" \
+ DOCKER_REPOSITORY="${DOCKER_REPOSITORY}" \
+ IMAGE_NAME="${IMAGE_NAME}" \
+ make docker-push
+ done <<< "$TAGS"
+
+ - name: Log out of Docker registry
+ if: always()
+ shell: bash
+ run: docker logout "${{ inputs.docker-registry }}"
diff --git a/.github/actions/github-release-action/README.md b/.github/actions/github-release-action/README.md
new file mode 100644
index 0000000..db48efc
--- /dev/null
+++ b/.github/actions/github-release-action/README.md
@@ -0,0 +1,68 @@
+<!--
+SPDX-License-Identifier: Apache-2.0
+SPDX-FileCopyrightText: 2026 The Linux Foundation
+-->
+
+# GitHub Release Action
+
+A composite GitHub Action that creates GitHub releases with artifacts using the `gh` CLI tool.
+
+## Description
+
+Builds release artifacts via `make`, generates SHA256 checksums, and creates a GitHub release with all artifacts attached. Supports draft releases for pre-release versions.
+
+## Usage
+
+```yaml
+- uses: ./.github/actions/github-release-action
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+```
+
+### Inputs
+
+| Input | Description | Required | Default |
+|-------|-------------|----------|---------|
+| `release-targets` | Make targets to build release artifacts | No | `release` |
+| `artifact-glob` | Glob pattern for release artifacts | No | `release/*` |
+| `github-token` | GitHub token for authentication | Yes | — |
+| `draft` | Create a draft release | No | `false` |
+
+### Example Workflow
+
+```yaml
+name: Release
+on:
+ push:
+ tags:
+ - "v*"
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: ./.github/actions/github-release-action
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ release-targets: "build package"
+ artifact-glob: "dist/*"
+```
+
+## How It Works
+
+1. Finds a SemVer tag on HEAD (`vX.Y.Z`, `X.Y.Z`, or `vX.Y.Z-devN`)
+2. Builds release artifacts using `make <release-targets>`
+3. Generates SHA256 checksums for all matched artifacts
+4. Creates a GitHub release with artifacts and checksums attached
+5. Marks the release as draft if the tag contains `-dev` or `draft` input is `true`
+
+## Requirements
+
+- Repository must have a SemVer tag on the current commit
+- A `Makefile` with the specified release target(s)
+- `gh` CLI (pre-installed on GitHub Actions runners)
+- `GITHUB_TOKEN` with permissions to create releases
diff --git a/.github/actions/github-release-action/action.yaml b/.github/actions/github-release-action/action.yaml
new file mode 100644
index 0000000..5548106
--- /dev/null
+++ b/.github/actions/github-release-action/action.yaml
@@ -0,0 +1,80 @@
+---
+# SPDX-FileCopyrightText: 2026 The Linux Foundation
+# SPDX-License-Identifier: Apache-2.0
+
+name: "GitHub Release Action"
+description: "Creates GitHub releases with artifacts using the gh CLI tool"
+
+inputs:
+ release-targets:
+ description: "Make targets to build release artifacts"
+ required: false
+ default: "release"
+ artifact-glob:
+ description: "Glob pattern for release artifacts"
+ required: false
+ default: "release/*"
+ github-token:
+ description: "GitHub token for authentication"
+ required: true
+ draft:
+ description: "Create a draft release"
+ required: false
+ default: "false"
+
+runs:
+ using: "composite"
+ steps:
+ - name: Create GitHub Release
+ shell: bash
+ env:
+ GITHUB_TOKEN: ${{ inputs.github-token }}
+ RELEASE_TARGETS: ${{ inputs.release-targets }}
+ ARTIFACT_GLOB: ${{ inputs.artifact-glob }}
+ DRAFT_INPUT: ${{ inputs.draft }}
+ run: |
+ set -eu -o pipefail
+
+ # Determine version from git tags on HEAD
+ VERSION=""
+ for tag in $(git tag --points-at HEAD); do
+ if echo "$tag" | grep -qE '^v?[0-9]+\.[0-9]+\.[0-9]+(-dev[0-9]+)?$'; then
+ VERSION="$tag"
+ break
+ fi
+ done
+
+ if [ -z "$VERSION" ]; then
+ echo "ERROR: No SemVer tag (vX.Y.Z, X.Y.Z, or vX.Y.Z-devN) found on HEAD"
+ exit 1
+ fi
+
+ echo "Version: $VERSION"
+
+ # Build release artifacts
+ make $RELEASE_TARGETS
+
+ # Generate SHA256 checksums for all artifacts
+ CHECKSUM_FILE="checksums-sha256.txt"
+ sha256sum $ARTIFACT_GLOB > "$CHECKSUM_FILE"
+ echo "Generated checksums:"
+ cat "$CHECKSUM_FILE"
+
+ # Determine if this should be a draft release
+ DRAFT_FLAG=""
+ if [[ "$VERSION" == *"-dev"* ]] || [[ "$DRAFT_INPUT" == "true" ]]; then
+ DRAFT_FLAG="--draft"
+ echo "Creating draft release"
+ fi
+
+ # Get release notes from latest commit message
+ RELEASE_NOTES="$(git log -1 --pretty=%B)"
+
+ # Create GitHub release
+ gh release create "$VERSION" \
+ $ARTIFACT_GLOB \
+ "$CHECKSUM_FILE" \
+ --title "$VERSION" \
+ --notes "$RELEASE_NOTES" \
+ --generate-notes \
+ $DRAFT_FLAG
diff --git a/.github/actions/maven-publish-action/README.md b/.github/actions/maven-publish-action/README.md
new file mode 100644
index 0000000..68ae27b
--- /dev/null
+++ b/.github/actions/maven-publish-action/README.md
@@ -0,0 +1,74 @@
+<!--
+SPDX-License-Identifier: Apache-2.0
+SPDX-FileCopyrightText: 2026 The Linux Foundation
+-->
+
+# Maven Publish Action
+
+Build and publish Maven (Java) artifacts to a remote repository.
+
+## Description
+
+A composite GitHub Action that sets up JDK, optionally imports a GPG signing
+key, configures Maven authentication, and executes Maven goals to publish
+artifacts. Based on Jenkins JJB Maven release patterns.
+
+## Usage
+
+```yaml
+- uses: ./.github/actions/maven-publish-action
+ with:
+ server-username: ${{ secrets.OSSRH_USERNAME }}
+ server-password: ${{ secrets.OSSRH_PASSWORD }}
+```
+
+### Inputs
+
+| Input | Required | Default | Description |
+|-------|----------|---------|-------------|
+| `java-version` | No | `11` | Java version to use |
+| `java-distribution` | No | `temurin` | Java distribution |
+| `maven-goals` | No | `-Prelease clean deploy` | Maven goals to execute |
+| `maven-settings-file` | No | `""` | Path to custom Maven settings.xml |
+| `gpg-private-key` | No | `""` | GPG private key for signing |
+| `gpg-passphrase` | No | `""` | GPG passphrase |
+| `server-id` | No | `ossrh` | Maven server ID for deployment |
+| `server-username` | **Yes** | — | Maven server username |
+| `server-password` | **Yes** | — | Maven server password |
+
+### Example Workflow
+
+```yaml
+name: Maven Publish
+on:
+ push:
+ tags:
+ - "v*"
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: ./.github/actions/maven-publish-action
+ with:
+ java-version: "17"
+ server-username: ${{ secrets.OSSRH_USERNAME }}
+ server-password: ${{ secrets.OSSRH_PASSWORD }}
+ gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
+ gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }}
+```
+
+## How It Works
+
+1. Sets up JDK using `actions/setup-java@v4` with the specified distribution and version
+2. Imports the GPG private key for artifact signing (if provided)
+3. Configures Maven settings with server credentials (custom or generated)
+4. Executes Maven with the specified goals using `MAVEN_USERNAME` and `MAVEN_PASSWORD` environment variables for authentication
+
+## Requirements
+
+- Repository must contain a valid `pom.xml`
+- Server credentials must be provided (typically via GitHub Secrets)
+- GPG key required only if the Maven profile performs artifact signing
diff --git a/.github/actions/maven-publish-action/action.yaml b/.github/actions/maven-publish-action/action.yaml
new file mode 100644
index 0000000..1946815
--- /dev/null
+++ b/.github/actions/maven-publish-action/action.yaml
@@ -0,0 +1,96 @@
+---
+# SPDX-FileCopyrightText: 2026 The Linux Foundation
+# SPDX-License-Identifier: Apache-2.0
+
+name: Maven Publish
+description: "Build and publish Maven (Java) artifacts to a remote repository"
+
+inputs:
+ java-version:
+ description: "Java version to use"
+ required: false
+ default: "11"
+ java-distribution:
+ description: "Java distribution"
+ required: false
+ default: "temurin"
+ maven-goals:
+ description: "Maven goals to execute"
+ required: false
+ default: "-Prelease clean deploy"
+ maven-settings-file:
+ description: "Path to custom Maven settings.xml"
+ required: false
+ default: ""
+ gpg-private-key:
+ description: "GPG private key for signing"
+ required: false
+ default: ""
+ gpg-passphrase:
+ description: "GPG passphrase"
+ required: false
+ default: ""
+ server-id:
+ description: "Maven server ID for deployment"
+ required: false
+ default: "ossrh"
+ server-username:
+ description: "Maven server username"
+ required: true
+ server-password:
+ description: "Maven server password"
+ required: true
+
+runs:
+ using: "composite"
+ steps:
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: ${{ inputs.java-distribution }}
+ java-version: ${{ inputs.java-version }}
+ server-id: ${{ inputs.server-id }}
+ server-username: MAVEN_USERNAME
+ server-password: MAVEN_PASSWORD
+ settings-path: ${{ github.workspace }}
+
+ - name: Import GPG key
+ if: inputs.gpg-private-key != ''
+ shell: bash
+ env:
+ GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }}
+ GPG_PASSPHRASE: ${{ inputs.gpg-passphrase }}
+ run: |
+ set -eu -o pipefail
+ echo "Importing GPG private key..."
+ echo "$GPG_PRIVATE_KEY" | gpg --batch --import
+ echo "GPG key imported successfully"
+ if [ -n "$GPG_PASSPHRASE" ]; then
+ echo "allow-preset-passphrase" >> ~/.gnupg/gpg-agent.conf
+ gpgconf --kill gpg-agent
+ gpgconf --launch gpg-agent
+ fi
+
+ - name: Apply custom Maven settings
+ if: inputs.maven-settings-file != ''
+ shell: bash
+ run: |
+ set -eu -o pipefail
+ echo "Using custom Maven settings: ${{ inputs.maven-settings-file }}"
+ cp "${{ inputs.maven-settings-file }}" "${{ github.workspace }}/settings.xml"
+
+ - name: Run Maven
+ shell: bash
+ env:
+ MAVEN_USERNAME: ${{ inputs.server-username }}
+ MAVEN_PASSWORD: ${{ inputs.server-password }}
+ GPG_PASSPHRASE: ${{ inputs.gpg-passphrase }}
+ run: |
+ set -eu -o pipefail
+ MAVEN_ARGS=(-s "${{ github.workspace }}/settings.xml")
+ if [ -n "$GPG_PASSPHRASE" ]; then
+ MAVEN_ARGS+=(-Dgpg.passphrase="$GPG_PASSPHRASE")
+ fi
+ read -r -a MAVEN_GOALS <<< "${{ inputs.maven-goals }}"
+ echo "Running: mvn ${MAVEN_ARGS[*]} ${MAVEN_GOALS[*]}"
+ mvn "${MAVEN_ARGS[@]}" "${MAVEN_GOALS[@]}"
diff --git a/.github/actions/pypi-publish-action/README.md b/.github/actions/pypi-publish-action/README.md
new file mode 100644
index 0000000..3eddfd5
--- /dev/null
+++ b/.github/actions/pypi-publish-action/README.md
@@ -0,0 +1,73 @@
+<!--
+SPDX-License-Identifier: Apache-2.0
+SPDX-FileCopyrightText: 2026 The Linux Foundation
+-->
+
+# PyPI Publish Action
+
+A composite GitHub Action that builds and publishes Python packages to PyPI on SemVer release tags.
+
+## Description
+
+Publishes Python packages to PyPI when the current HEAD has a valid SemVer release tag (vX.Y.Z or X.Y.Z). Supports publishing multiple modules from a single repository using pipe-separated directory paths.
+
+## Usage
+
+```yaml
+- uses: ./.github/actions/pypi-publish-action
+ with:
+ pypi-username: ${{ secrets.PYPI_USERNAME }}
+ pypi-password: ${{ secrets.PYPI_PASSWORD }}
+```
+
+### Inputs
+
+| Input | Required | Default | Description |
+|-------|----------|---------|-------------|
+| `pypi-index` | No | `pypi` | PyPI repository name to publish to |
+| `pypi-username` | Yes | — | PyPI username |
+| `pypi-password` | Yes | — | PyPI password or API token |
+| `module-dirs` | No | `.` | Pipe-separated list of directories containing Python modules to publish |
+| `prep-commands` | No | `""` | Commands to run before building |
+
+### Example Workflow
+
+```yaml
+name: Publish to PyPI
+
+on:
+ push:
+ tags: ["v*"]
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+
+ - uses: ./.github/actions/pypi-publish-action
+ with:
+ pypi-username: ${{ secrets.PYPI_USERNAME }}
+ pypi-password: ${{ secrets.PYPI_PASSWORD }}
+ module-dirs: "lib/core|lib/utils"
+ prep-commands: "make generate"
+```
+
+## How It Works
+
+1. Gets the git tag on HEAD and verifies it is a SemVer release version (vX.Y.Z or X.Y.Z).
+2. Exits gracefully if no valid release tag is found.
+3. Runs any specified prep commands.
+4. For each module directory: verifies `setup.py` exists, installs build dependencies, builds with `python3 -m build`, and uploads with `twine upload`.
+
+## Requirements
+
+- Each module directory must contain a `setup.py`.
+- The checkout step should use `fetch-depth: 0` to ensure git tags are available.
+- Python must be available in the runner environment.
diff --git a/.github/actions/pypi-publish-action/action.yaml b/.github/actions/pypi-publish-action/action.yaml
new file mode 100644
index 0000000..229f834
--- /dev/null
+++ b/.github/actions/pypi-publish-action/action.yaml
@@ -0,0 +1,91 @@
+---
+# SPDX-FileCopyrightText: 2026 The Linux Foundation
+# SPDX-License-Identifier: Apache-2.0
+
+name: PyPI Publish
+description: "Build and publish Python packages to PyPI on SemVer release tags"
+
+inputs:
+ pypi-index:
+ description: "PyPI repository name to publish to"
+ required: false
+ default: "pypi"
+ pypi-username:
+ description: "PyPI username"
+ required: true
+ pypi-password:
+ description: "PyPI password or API token"
+ required: true
+ module-dirs:
+ description: "Pipe-separated list of directories containing Python modules to publish"
+ required: false
+ default: "."
+ prep-commands:
+ description: "Commands to run before building"
+ required: false
+ default: ""
+
+runs:
+ using: "composite"
+ steps:
+ - name: Get and verify SemVer release tag
+ id: tag-check
+ shell: bash
+ run: |
+ set -eu -o pipefail
+ TAG=$(git tag --points-at HEAD | head -n1 || true)
+ if [ -z "$TAG" ]; then
+ echo "No git tag on HEAD, skipping publish"
+ echo "is-release=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ # Match vX.Y.Z or X.Y.Z (SemVer release, no pre-release suffix)
+ if [[ "$TAG" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "Found SemVer release tag: $TAG"
+ echo "is-release=true" >> "$GITHUB_OUTPUT"
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+ else
+ echo "Tag '$TAG' is not a SemVer release version, skipping publish"
+ echo "is-release=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Run prep commands
+ if: steps.tag-check.outputs.is-release == 'true' && inputs.prep-commands != ''
+ shell: bash
+ run: |
+ set -eu -o pipefail
+ echo "Running prep commands..."
+ ${{ inputs.prep-commands }}
+
+ - name: Build and publish packages
+ if: steps.tag-check.outputs.is-release == 'true'
+ shell: bash
+ env:
+ TWINE_USERNAME: ${{ inputs.pypi-username }}
+ TWINE_PASSWORD: ${{ inputs.pypi-password }}
+ run: |
+ set -eu -o pipefail
+
+ echo "Installing build dependencies..."
+ pip install --upgrade pip setuptools wheel twine build
+
+ IFS='|' read -ra DIRS <<< "${{ inputs.module-dirs }}"
+ for dir in "${DIRS[@]}"; do
+ dir=$(echo "$dir" | xargs) # trim whitespace
+ echo "::group::Publishing module in: $dir"
+
+ if [ ! -f "$dir/setup.py" ]; then
+ echo "ERROR: setup.py not found in $dir"
+ exit 1
+ fi
+
+ echo "Building source distribution..."
+ cd "$dir"
+ python3 -m build
+
+ echo "Uploading to PyPI repository: ${{ inputs.pypi-index }}"
+ twine upload -r "${{ inputs.pypi-index }}" dist/*
+
+ cd "$GITHUB_WORKSPACE"
+ echo "::endgroup::"
+ done