diff --git a/.github/actions/compare_versions/action.yaml b/.github/actions/compare_versions/action.yaml new file mode 100644 index 0000000..35b0be5 --- /dev/null +++ b/.github/actions/compare_versions/action.yaml @@ -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" diff --git a/.github/workflows/build-and-push-images.yaml b/.github/workflows/build-and-push-images.yaml index c5bc679..0b7f66a 100644 --- a/.github/workflows/build-and-push-images.yaml +++ b/.github/workflows/build-and-push-images.yaml @@ -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: @@ -14,6 +14,8 @@ on: - "**.go" - "**/Dockerfile" - ".github/workflows/build-and-push-images.yaml" + pull_request: + types: [opened, synchronize] jobs: setup: @@ -25,7 +27,7 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set outputs id: set_outputs @@ -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" @@ -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<> $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" @@ -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 @@ -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" @@ -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 diff --git a/.github/workflows/create-release.yaml b/.github/workflows/create-release.yaml new file mode 100644 index 0000000..83974fd --- /dev/null +++ b/.github/workflows/create-release.yaml @@ -0,0 +1,189 @@ +name: Create Release + +on: + push: + branches: + - main + paths: + - "VERSION" + - "**.go" + - "**/Dockerfile" + - ".github/workflows/**" + +jobs: + validate_version: + name: Validate VERSION file + runs-on: ubuntu-latest + outputs: + version: ${{ steps.read_version.outputs.version }} + tags: ${{ steps.generate_tags.outputs.tags }} + is_prerelease: ${{ steps.generate_tags.outputs.is_prerelease }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check VERSION file exists + id: check_version_file + run: | + if [ ! -f "VERSION" ]; then + echo "❌ VERSION file not found in repository root" + exit 1 + fi + echo "✅ VERSION file exists" + + - name: Read and validate VERSION + id: read_version + shell: bash + 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 + + # Find the latest existing release tag (if any) for comparison + LATEST_TAG=$(git tag -l 'v*' --sort=-version:refname | head -n 1 || echo "") + echo "latest_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "✅ Valid version: $VERSION" + + - name: Compare VERSION with latest release using shared action + id: compare_with_latest + if: steps.read_version.outputs.latest_tag != '' + uses: ./.github/actions/compare_versions + with: + v1: ${{ steps.read_version.outputs.version }} + v2: ${{ steps.read_version.outputs.latest_tag }} + + - name: Enforce VERSION is greater than latest release + if: steps.read_version.outputs.latest_tag != '' + shell: bash + run: | + VERSION="${{ steps.read_version.outputs.version }}" + LATEST_TAG="${{ steps.read_version.outputs.latest_tag }}" + RESULT="${{ steps.compare_with_latest.outputs.result }}" + + if [ "$RESULT" = "eq" ]; then + echo "❌ VERSION is the same as latest release" + echo "Current VERSION: $VERSION" + echo "Latest release: $LATEST_TAG" + echo "Please update the VERSION file with a greater semantic version" + exit 1 + elif [ "$RESULT" = "lt" ]; then + echo "❌ VERSION is less than latest release" + echo "Current VERSION: $VERSION" + echo "Latest release: $LATEST_TAG" + echo "Please update the VERSION file with a greater semantic version" + exit 1 + fi + + echo "✅ VERSION is greater than latest release" + echo "Latest release: $LATEST_TAG" + echo "New version: $VERSION" + + - name: Note when no previous releases exist + if: steps.read_version.outputs.latest_tag == '' + run: | + echo "✅ No previous releases found, this will be the first release" + + - name: Generate tags + id: generate_tags + shell: bash + run: | + VERSION="${{ steps.read_version.outputs.version }}" + # Remove 'v' prefix for parsing (we know it exists from validation) + VERSION_CLEAN=${VERSION#v} + + # Extract major and minor version numbers + IFS='.' read -ra VERSION_PARTS <<< "$VERSION_CLEAN" + MAJOR=${VERSION_PARTS[0]} + MINOR=${VERSION_PARTS[1]} + + VERSION_TAG="$VERSION" + + # Detect if this is a pre-release (presence of a '-' in the version string) + IS_PRERELEASE=false + if [[ "$VERSION_CLEAN" == *-* ]]; then + IS_PRERELEASE=true + fi + + # Generate tags: + if [ "$IS_PRERELEASE" = true ]; then + TAGS="${VERSION_TAG}" + else + TAGS="latest,v${MAJOR},v${MAJOR}.${MINOR},${VERSION_TAG}" + fi + + echo "tags=$TAGS" >> $GITHUB_OUTPUT + echo "Generated tags: $TAGS" + # Expose a literal \"true\"/\"false\" string for prerelease detection + if [ "$IS_PRERELEASE" = true ]; then + echo "is_prerelease=true" >> $GITHUB_OUTPUT + else + echo "is_prerelease=false" >> $GITHUB_OUTPUT + fi + + create_release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: validate_version + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Create Git Tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + VERSION="${{ needs.validate_version.outputs.version }}" + if git rev-parse "$VERSION" >/dev/null 2>&1; then + echo "❌ Tag $VERSION already exists" + exit 1 + fi + git tag -a "$VERSION" -m "Release $VERSION" + git push origin "$VERSION" || { echo "❌ Failed to push tag"; exit 1; } + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.validate_version.outputs.version }} + name: Release ${{ needs.validate_version.outputs.version }} + body: | + Release ${{ needs.validate_version.outputs.version }} + draft: false + prerelease: ${{ needs.validate_version.outputs.is_prerelease == 'true' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Trigger build workflow + uses: actions/github-script@v8 + with: + script: | + const tags = '${{ needs.validate_version.outputs.tags }}'; + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'build-and-push-images.yaml', + ref: 'main', + inputs: { + tag: tags + } + }); + console.log(`Triggered build workflow with tags: ${tags}`); diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..5969682 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v2.1.2