Skip to content
93 changes: 93 additions & 0 deletions .github/actions/compare_versions/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: "Compare semantic versions"
description: "Compares two semantic versions (with optional v-prefix, pre-release, and build metadata) and returns gt/lt/eq."

inputs:
v1:
description: "First version to compare (e.g., v1.2.3, 1.2.3-rc1)"
required: true
v2:
description: "Second version to compare (e.g., v1.2.3, 1.2.3-rc1)"
required: true

outputs:
result:
description: "Comparison result: gt (v1 > v2), lt (v1 < v2), eq (v1 == v2)"
value: ${{ steps.compare.outputs.result }}
exit_code:
description: "Numeric comparison code: 0 (v1 > v2), 1 (v1 < v2), 2 (v1 == v2)"
value: ${{ steps.compare.outputs.exit_code }}

runs:
using: "composite"
steps:
- name: Compare versions
id: compare
shell: bash
run: |
v1="${{ inputs.v1 }}"
v2="${{ inputs.v2 }}"

# Function to compare semantic versions
version_compare() {
local v1=$1
local v2=$2

# Remove 'v' prefix
v1=${v1#v}
v2=${v2#v}

# Remove pre-release and build metadata for comparison
v1_clean=${v1%%-*}
v1_clean=${v1_clean%%+*}
v2_clean=${v2%%-*}
v2_clean=${v2_clean%%+*}

# Split into major.minor.patch
IFS='.' read -ra V1_PARTS <<< "$v1_clean"
IFS='.' read -ra V2_PARTS <<< "$v2_clean"

# Compare major
if [ "${V1_PARTS[0]}" -gt "${V2_PARTS[0]}" ]; then
return 0 # v1 > v2
elif [ "${V1_PARTS[0]}" -lt "${V2_PARTS[0]}" ]; then
return 1 # v1 < v2
fi

# Compare minor
if [ "${V1_PARTS[1]}" -gt "${V2_PARTS[1]}" ]; then
return 0 # v1 > v2
elif [ "${V1_PARTS[1]}" -lt "${V2_PARTS[1]}" ]; then
return 1 # v1 < v2
fi

# Compare patch
if [ "${V1_PARTS[2]}" -gt "${V2_PARTS[2]}" ]; then
return 0 # v1 > v2
elif [ "${V1_PARTS[2]}" -lt "${V2_PARTS[2]}" ]; then
return 1 # v1 < v2
fi

return 2 # v1 == v2
}

version_compare "$v1" "$v2"
compare_rc=$?

case "$compare_rc" in
0)
result="gt"
;;
1)
result="lt"
;;
2)
result="eq"
;;
*)
echo "Unexpected comparison result code: $compare_rc"
exit 1
;;
esac

echo "result=$result" >> "$GITHUB_OUTPUT"
echo "exit_code=$compare_rc" >> "$GITHUB_OUTPUT"
192 changes: 172 additions & 20 deletions .github/workflows/build-and-push-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: "Image tag for manual builds. If not set, the branch name is used for non-main branches."
description: "Comma-separated list of image tags for manual builds (e.g., 'preprod' or 'latest,v2,v2.1,v2.1.2'). If not set, the branch name is used for non-main branches."
required: false
type: string
push:
Expand All @@ -14,6 +14,8 @@ on:
- "**.go"
- "**/Dockerfile"
- ".github/workflows/build-and-push-images.yaml"
pull_request:
types: [opened, synchronize]

jobs:
setup:
Expand All @@ -25,7 +27,7 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Set outputs
id: set_outputs
Expand All @@ -39,11 +41,11 @@ jobs:
steps:
- name: Checkout code
id: checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Set up Go
id: setup_go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.25"

Expand All @@ -52,22 +54,167 @@ jobs:
run: |
go test ./...

check_version:
name: Check VERSION file
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.ref }}

- name: Checkout main branch
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/checkout@v6
with:
ref: main
path: main-branch

- name: Check VERSION file exists
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
run: |
if [ ! -f "VERSION" ]; then
echo "❌ VERSION file not found in repository root"
echo "Please create a VERSION file with a semantic version (e.g., v1.0.0)"
exit 1
fi
echo "✅ VERSION file exists"

- name: Check VERSION file updated and greater
id: check_version_file_updated_and_greater
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
run: |
if [ -f "main-branch/VERSION" ]; then
PR_VERSION=$(cat VERSION | xargs)
MAIN_VERSION=$(cat main-branch/VERSION | xargs)

# Check if versions are the same
if [ "$PR_VERSION" == "$MAIN_VERSION" ]; then
echo "❌ VERSION file has not been updated"
echo "Current VERSION: $PR_VERSION"
echo "Main branch VERSION: $MAIN_VERSION"
echo "Please update the VERSION file with a new semantic version"
exit 1
fi

echo "PR_VERSION=$PR_VERSION" >> "$GITHUB_OUTPUT"
echo "MAIN_VERSION=$MAIN_VERSION" >> "$GITHUB_OUTPUT"
else
echo "✅ VERSION file is new (not present in main branch)"
fi

- name: Compare PR and main VERSION using shared action
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.check_version_file_updated_and_greater.outcome == 'success' && steps.check_version_file_updated_and_greater.outputs.PR_VERSION != ''
id: compare_pr_main_version
uses: ./.github/actions/compare_versions
with:
v1: ${{ steps.check_version_file_updated_and_greater.outputs.PR_VERSION }}
v2: ${{ steps.check_version_file_updated_and_greater.outputs.MAIN_VERSION }}

- name: Enforce PR VERSION is greater than main VERSION
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.compare_pr_main_version.outputs.result != ''
run: |
PR_VERSION="${{ steps.check_version_file_updated_and_greater.outputs.PR_VERSION }}"
MAIN_VERSION="${{ steps.check_version_file_updated_and_greater.outputs.MAIN_VERSION }}"
RESULT="${{ steps.compare_pr_main_version.outputs.result }}"

if [ "$RESULT" = "eq" ]; then
echo "❌ VERSION is the same as main branch (after removing metadata)"
echo "PR VERSION: $PR_VERSION"
echo "Main branch VERSION: $MAIN_VERSION"
echo "Please update the VERSION file with a greater semantic version"
exit 1
elif [ "$RESULT" = "lt" ]; then
echo "❌ VERSION is less than main branch version"
echo "PR VERSION: $PR_VERSION"
echo "Main branch VERSION: $MAIN_VERSION"
echo "Please update the VERSION file with a greater semantic version"
exit 1
fi

echo "✅ VERSION file has been updated and is greater"
echo "Main branch: $MAIN_VERSION"
echo "PR branch: $PR_VERSION"

- name: Validate VERSION format
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
run: |
VERSION=$(cat VERSION | xargs)

# Check that version starts with 'v'
if [[ ! "$VERSION" == v* ]]; then
echo "❌ Version must start with 'v' prefix"
echo "Current version: $VERSION"
echo "Expected format: v1.0.0 (semantic versioning with 'v' prefix)"
exit 1
fi

# Remove 'v' prefix for validation
VERSION_CLEAN=${VERSION#v}

# Validate semantic versioning format (major.minor.patch)
if [[ ! $VERSION_CLEAN =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ Invalid version format: $VERSION"
echo "Expected format: v1.0.0 (semantic versioning with 'v' prefix)"
exit 1
fi

echo "✅ Valid version: $VERSION"

- name: Skip check for non-PR events
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "⏭️ Skipping VERSION check (not a PR or PR is from a fork)"

parse_tags:
name: Parse tags
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.parse_tags.outputs.tags }}
has_custom_tags: ${{ steps.parse_tags.outputs.has_custom_tags }}
steps:
- name: Parse tags
id: parse_tags
shell: bash
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ github.event.inputs.tag }}" ]; then
# Parse comma-separated tags and output as multiline string for docker/metadata-action
IFS=',' read -ra TAGS <<< "${{ github.event.inputs.tag }}"
TAGS_OUTPUT=""
for tag in "${TAGS[@]}"; do
tag=$(echo "$tag" | xargs) # trim whitespace
if [ -n "$tag" ]; then
TAGS_OUTPUT="${TAGS_OUTPUT}type=raw,value=${tag}"$'\n'
fi
done
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo "$TAGS_OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "has_custom_tags=true" >> $GITHUB_OUTPUT
else
echo "has_custom_tags=false" >> $GITHUB_OUTPUT
fi

health-checker:
name: Build health-checker image
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
# Run for all configured events, but skip pull requests from forks
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
environment: ${{ github.ref_name == 'main' && 'prod' || 'dev' }}
needs:
- setup
- test
- check_version
- parse_tags
steps:
- name: Checkout code
id: checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Set up Go
id: setup_go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.25"

Expand All @@ -85,17 +232,18 @@ jobs:
with:
images: ghcr.io/project-aethermesh/aetherlay/aetherlay-hc
tags: |
type=raw,value=latest,enable=${{ github.ref_name == github.event.repository.default_branch }}
type=sha,format=short,enable=${{ github.ref_name == github.event.repository.default_branch }}
type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }}
type=ref,event=branch,enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag == '' && github.ref_name != github.event.repository.default_branch }}
type=raw,value=latest,enable=${{ github.ref_name == github.event.repository.default_branch && github.event_name != 'pull_request' }}
type=sha,format=short,enable=${{ github.ref_name == github.event.repository.default_branch && github.event_name != 'pull_request' }}
type=raw,value=pr-${{ github.event.pull_request.number }},enable=${{ github.event_name == 'pull_request' }}
${{ needs.parse_tags.outputs.tags }}
type=ref,event=branch,enable=${{ github.event_name == 'workflow_dispatch' && needs.parse_tags.outputs.has_custom_tags != 'true' && github.ref_name != github.event.repository.default_branch }}
labels: |
org.opencontainers.image.vendor=Project Aethermesh
org.opencontainers.image.licenses=AGPL-3.0

- name: Build and push health-checker image
id: container_image_hc
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: ./services/health-checker/Dockerfile
Expand All @@ -106,19 +254,22 @@ jobs:
load-balancer:
name: Build load-balancer image
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
# Run for all configured events, but skip pull requests from forks
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
environment: ${{ github.ref_name == 'main' && 'prod' || 'dev' }}
needs:
- setup
- test
- check_version
- parse_tags
steps:
- name: Checkout code
id: checkout_lb
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Set up Go
id: setup_go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.25"

Expand All @@ -136,17 +287,18 @@ jobs:
with:
images: ghcr.io/project-aethermesh/aetherlay/aetherlay-lb
tags: |
type=raw,value=latest,enable=${{ github.ref_name == github.event.repository.default_branch }}
type=sha,format=short,enable=${{ github.ref_name == github.event.repository.default_branch }}
type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }}
type=ref,event=branch,enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag == '' && github.ref_name != github.event.repository.default_branch }}
type=raw,value=latest,enable=${{ github.ref_name == github.event.repository.default_branch && github.event_name != 'pull_request' }}
type=sha,format=short,enable=${{ github.ref_name == github.event.repository.default_branch && github.event_name != 'pull_request' }}
type=raw,value=pr-${{ github.event.pull_request.number }},enable=${{ github.event_name == 'pull_request' }}
${{ needs.parse_tags.outputs.tags }}
type=ref,event=branch,enable=${{ github.event_name == 'workflow_dispatch' && needs.parse_tags.outputs.has_custom_tags != 'true' && github.ref_name != github.event.repository.default_branch }}
labels: |
org.opencontainers.image.vendor=Project Aethermesh
org.opencontainers.image.licenses=AGPL-3.0

- name: Build and push load-balancer image
id: container_image_lb
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: ./services/load-balancer/Dockerfile
Expand Down
Loading