[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 }}"