diff --git a/.github/actions/docker-publish-action/README.md b/.github/actions/docker-publish-action/README.md new file mode 100644 index 0000000..3f3b61a --- /dev/null +++ b/.github/actions/docker-publish-action/README.md @@ -0,0 +1,78 @@ + + +# Docker Publish Action + +A composite GitHub Action that builds and pushes Docker images tagged with branch names and git tags. + +## Description + +Builds Docker images using `make docker-build` and pushes them with `make docker-push`. Images are tagged with the current 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" + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} +``` + +### Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `docker-registry` | Yes | — | Docker registry URL | +| `docker-repository` | Yes | — | Docker repository prefix | +| `image-name` | Yes | — | Name of the Docker image | +| `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" + extra-environment-vars: "GOFLAGS=-buildvcs=false" + 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 name. +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..7fb1702 --- /dev/null +++ b/.github/actions/docker-publish-action/action.yaml @@ -0,0 +1,142 @@ +--- +# 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 + 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: + # yamllint disable-line rule:line-length + 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<> "$GITHUB_OUTPUT" + echo "$TAGS" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Build Docker image with branch tag + shell: bash + run: | + set -eu -o pipefail + BRANCH_NAME="${GITHUB_REF_NAME}" + DOCKER_REGISTRY="${{ inputs.docker-registry }}" + DOCKER_REPOSITORY="${{ inputs.docker-repository }}" + IMAGE_NAME="${{ inputs.image-name }}" + + 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 + BRANCH_NAME="${GITHUB_REF_NAME}" + DOCKER_REGISTRY="${{ inputs.docker-registry }}" + DOCKER_REPOSITORY="${{ inputs.docker-repository }}" + IMAGE_NAME="${{ inputs.image-name }}" + + 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 @@ + + +# 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 ` +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 @@ + + +# 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..97fc302 --- /dev/null +++ b/.github/actions/maven-publish-action/action.yaml @@ -0,0 +1,95 @@ +--- +# 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="$MAVEN_ARGS -Dgpg.passphrase=$GPG_PASSPHRASE" + fi + echo "Running: mvn $MAVEN_ARGS ${{ inputs.maven-goals }}" + mvn $MAVEN_ARGS ${{ inputs.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 @@ + + +# 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..58ff792 --- /dev/null +++ b/.github/actions/pypi-publish-action/action.yaml @@ -0,0 +1,90 @@ +--- +# 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 + 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 "Installing build dependencies..." + pip install --upgrade pip setuptools wheel twine build + + 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