Skip to content

fix: refactor PR Validation workflow to use Replicated actions #140

fix: refactor PR Validation workflow to use Replicated actions

fix: refactor PR Validation workflow to use Replicated actions #140

---
name: WG-Easy PR Validation - build, release, install
on:
pull_request:
branches: [main]
paths:
- 'applications/wg-easy/**'
- '.github/workflows/wg-easy-pr-validation.yaml'
workflow_dispatch:
inputs:
test_mode:
description: 'Run in test mode'
required: false
default: 'true'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
APP_DIR: applications/wg-easy
REPLICATED_API_TOKEN: ${{ secrets.WG_EASY_REPLICATED_API_TOKEN }}
REPLICATED_APP: ${{ vars.WG_EASY_REPLICATED_APP }}
HELM_VERSION: "3.17.3"
KUBECTL_VERSION: "v1.30.0"
jobs:
setup:
runs-on: ubuntu-22.04
outputs:
branch-name: ${{ steps.vars.outputs.branch-name }}
channel-name: ${{ steps.vars.outputs.channel-name }}
customer-name: ${{ steps.vars.outputs.customer-name }}
steps:
- name: Set branch and channel variables
id: vars
run: |
# Branch name preserves original case for resource naming (clusters, customers)
BRANCH_NAME="${{ github.head_ref || github.ref_name }}"
# Channel name is normalized to lowercase with hyphens for Replicated channels
CHANNEL_NAME=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | tr '/' '-')
# Customer name includes run number to ensure uniqueness across workflow runs
CUSTOMER_NAME="${CHANNEL_NAME}-${{ github.run_number }}"
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "channel-name=$CHANNEL_NAME" >> $GITHUB_OUTPUT
echo "customer-name=$CUSTOMER_NAME" >> $GITHUB_OUTPUT
echo "Branch: $BRANCH_NAME, Channel: $CHANNEL_NAME, Customer: $CUSTOMER_NAME"
validate-charts:
runs-on: ubuntu-22.04
needs: setup
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Validate charts
uses: ./.github/actions/chart-validate
with:
app-dir: ${{ env.APP_DIR }}
helm-version: ${{ env.HELM_VERSION }}
- name: Validate Taskfile syntax
run: task --list-all
working-directory: ${{ env.APP_DIR }}
build-and-package:
runs-on: ubuntu-22.04
needs: [setup, validate-charts]
outputs:
release-path: ${{ steps.package.outputs.release-path }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Package charts
id: package
uses: ./.github/actions/chart-package
with:
app-dir: ${{ env.APP_DIR }}
helm-version: ${{ env.HELM_VERSION }}
- name: Upload release artifacts
uses: actions/upload-artifact@v4
with:
name: wg-easy-release-${{ github.run_number }}
path: ${{ steps.package.outputs.release-path }}
retention-days: 7
create-resources:
runs-on: ubuntu-22.04
needs: [setup, build-and-package]
outputs:
channel-slug: ${{ steps.set-outputs.outputs.channel-slug }}
release-sequence: ${{ steps.set-outputs.outputs.release-sequence }}
customer-id: ${{ steps.set-outputs.outputs.customer-id }}
license-id: ${{ steps.set-outputs.outputs.license-id }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
name: wg-easy-release-${{ github.run_number }}
path: ${{ env.APP_DIR }}/release
- name: Check if channel exists
id: check-channel
run: |
set -e
echo "Checking for existing channel: ${{ needs.setup.outputs.channel-name }}"
# Get channels with error handling
RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: ${{ env.REPLICATED_API_TOKEN }}" \
"https://api.replicated.com/vendor/v3/apps/${{ env.REPLICATED_APP }}/channels")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" != "200" ]; then
echo "API request failed with HTTP $HTTP_CODE"
echo "Response: $BODY"
echo "channel-exists=false" >> $GITHUB_OUTPUT
exit 0
fi
# Parse JSON response safely
CHANNEL_ID=$(echo "$BODY" | jq -r --arg name "${{ needs.setup.outputs.channel-name }}" \
'if .channels then .channels[] | select(.name == $name) | .id else empty end' 2>/dev/null | head -1)
if [ -n "$CHANNEL_ID" ] && [ "$CHANNEL_ID" != "null" ]; then
echo "Found existing channel: $CHANNEL_ID"
echo "channel-exists=true" >> $GITHUB_OUTPUT
echo "channel-id=$CHANNEL_ID" >> $GITHUB_OUTPUT
echo "channel-slug=${{ needs.setup.outputs.channel-name }}" >> $GITHUB_OUTPUT
else
echo "Channel does not exist"
echo "channel-exists=false" >> $GITHUB_OUTPUT
fi
- name: Create Replicated release
id: release
uses: replicatedhq/replicated-actions/[email protected]
with:
app-slug: ${{ env.REPLICATED_APP }}
api-token: ${{ env.REPLICATED_API_TOKEN }}
yaml-dir: ${{ env.APP_DIR }}/release
promote-channel: ${{ needs.setup.outputs.channel-name }}
- name: Check if customer exists
id: check-customer
run: |
set -e
CUSTOMER_NAME="${{ needs.setup.outputs.customer-name }}"
echo "Checking for existing customer: $CUSTOMER_NAME"
# Get customers with error handling
RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: ${{ env.REPLICATED_API_TOKEN }}" \
"https://api.replicated.com/vendor/v3/customers")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" != "200" ]; then
echo "API request failed with HTTP $HTTP_CODE"
echo "Response: $BODY"
echo "customer-exists=false" >> $GITHUB_OUTPUT
exit 0
fi
# Parse JSON response safely - select most recent customer by creation date
CUSTOMER_DATA=$(echo "$BODY" | jq -r --arg name "$CUSTOMER_NAME" \
'if .customers then .customers[] | select(.name == $name) | {id: .id, created: .createdAt} else empty end' 2>/dev/null \
| jq -s 'sort_by(.created) | reverse | .[0] // empty' 2>/dev/null)
CUSTOMER_ID=$(echo "$CUSTOMER_DATA" | jq -r '.id // empty' 2>/dev/null)
if [ -n "$CUSTOMER_DATA" ] && [ "$CUSTOMER_DATA" != "null" ] && [ "$CUSTOMER_DATA" != "{}" ]; then
CUSTOMER_COUNT=$(echo "$BODY" | jq -r --arg name "$CUSTOMER_NAME" \
'if .customers then [.customers[] | select(.name == $name)] | length else 0 end' 2>/dev/null)
echo "Found $CUSTOMER_COUNT customer(s) with name '$CUSTOMER_NAME', using most recent: $CUSTOMER_ID"
fi
if [ -n "$CUSTOMER_ID" ] && [ "$CUSTOMER_ID" != "null" ]; then
echo "Found existing customer: $CUSTOMER_ID"
echo "customer-exists=true" >> $GITHUB_OUTPUT
echo "customer-id=$CUSTOMER_ID" >> $GITHUB_OUTPUT
# Get license ID for existing customer with error handling
LICENSE_RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: ${{ env.REPLICATED_API_TOKEN }}" \
"https://api.replicated.com/vendor/v3/customer/$CUSTOMER_ID")
LICENSE_HTTP_CODE=$(echo "$LICENSE_RESPONSE" | tail -n1)
LICENSE_BODY=$(echo "$LICENSE_RESPONSE" | sed '$d')
if [ "$LICENSE_HTTP_CODE" = "200" ]; then
LICENSE_ID=$(echo "$LICENSE_BODY" | jq -r '.customer.installationId // empty' 2>/dev/null)
echo "license-id=$LICENSE_ID" >> $GITHUB_OUTPUT
else
echo "Failed to get license ID for customer $CUSTOMER_ID"
echo "customer-exists=false" >> $GITHUB_OUTPUT
fi
else
echo "Customer does not exist"
echo "customer-exists=false" >> $GITHUB_OUTPUT
fi
- name: Create customer
id: create-customer
if: steps.check-customer.outputs.customer-exists == 'false'
uses: replicatedhq/replicated-actions/[email protected]
with:
app-slug: ${{ env.REPLICATED_APP }}
api-token: ${{ env.REPLICATED_API_TOKEN }}
customer-name: ${{ needs.setup.outputs.customer-name }}
channel-slug: ${{ steps.check-channel.outputs.channel-exists == 'true' && steps.check-channel.outputs.channel-slug || steps.release.outputs.channel-slug }}
license-type: dev
- name: Set consolidated outputs
id: set-outputs
run: |
# Set channel outputs
if [ "${{ steps.check-channel.outputs.channel-exists }}" == "true" ]; then
echo "channel-slug=${{ steps.check-channel.outputs.channel-slug }}" >> $GITHUB_OUTPUT
else
echo "channel-slug=${{ steps.release.outputs.channel-slug }}" >> $GITHUB_OUTPUT
fi
echo "release-sequence=${{ steps.release.outputs.release-sequence }}" >> $GITHUB_OUTPUT
# Set customer outputs
if [ "${{ steps.check-customer.outputs.customer-exists }}" == "true" ]; then
echo "customer-id=${{ steps.check-customer.outputs.customer-id }}" >> $GITHUB_OUTPUT
echo "license-id=${{ steps.check-customer.outputs.license-id }}" >> $GITHUB_OUTPUT
else
echo "customer-id=${{ steps.create-customer.outputs.customer-id }}" >> $GITHUB_OUTPUT
echo "license-id=${{ steps.create-customer.outputs.license-id }}" >> $GITHUB_OUTPUT
fi
test-deployment:
runs-on: ubuntu-22.04
needs: [setup, create-resources]
strategy:
matrix:
include:
# k3s single-node configurations (latest patch versions)
- k8s-version: "v1.31.10"
distribution: "k3s"
nodes: 1
instance-type: "r1.small"
timeout-minutes: 15
- k8s-version: "v1.32.6"
distribution: "k3s"
nodes: 1
instance-type: "r1.small"
timeout-minutes: 15
# k3s multi-node configurations
- k8s-version: "v1.32.6"
distribution: "k3s"
nodes: 3
instance-type: "r1.medium"
timeout-minutes: 20
# kind configurations (maximum 1 node supported, distribution-specific patch versions)
- k8s-version: "v1.31.9"
distribution: "kind"
nodes: 1
instance-type: "r1.small"
timeout-minutes: 20
- k8s-version: "v1.32.5"
distribution: "kind"
nodes: 1
instance-type: "r1.small"
timeout-minutes: 20
# EKS configurations (major.minor versions only)
- k8s-version: "v1.31"
distribution: "eks"
nodes: 2
instance-type: "c5.large"
timeout-minutes: 30
- k8s-version: "v1.32"
distribution: "eks"
nodes: 2
instance-type: "c5.large"
timeout-minutes: 30
exclude: []
fail-fast: false
max-parallel: 4
outputs:
cluster-id: ${{ steps.set-cluster-outputs.outputs.cluster-id }}
steps:
- name: Set concurrency group
run: |
echo "CONCURRENCY_GROUP=cluster-${{ needs.setup.outputs.channel-name }}-${{ matrix.k8s-version }}-${{ matrix.distribution }}" >> $GITHUB_ENV
echo "Starting matrix job: ${{ matrix.k8s-version }}-${{ matrix.distribution }}-${{ matrix.nodes }}nodes"
- name: Checkout code
uses: actions/checkout@v4
- name: Setup tools
uses: ./.github/actions/setup-tools
with:
helm-version: ${{ env.HELM_VERSION }}
install-helmfile: 'true'
- name: Configure distribution-specific settings
id: dist-config
run: |
case "${{ matrix.distribution }}" in
"k3s")
echo "cluster-disk-size=20" >> $GITHUB_OUTPUT
echo "cluster-ttl=4h" >> $GITHUB_OUTPUT
echo "resource-priority=high" >> $GITHUB_OUTPUT
;;
"kind")
echo "cluster-disk-size=30" >> $GITHUB_OUTPUT
echo "cluster-ttl=4h" >> $GITHUB_OUTPUT
echo "resource-priority=medium" >> $GITHUB_OUTPUT
;;
"eks")
echo "cluster-disk-size=50" >> $GITHUB_OUTPUT
echo "cluster-ttl=6h" >> $GITHUB_OUTPUT
echo "resource-priority=low" >> $GITHUB_OUTPUT
;;
*)
echo "cluster-disk-size=20" >> $GITHUB_OUTPUT
echo "cluster-ttl=4h" >> $GITHUB_OUTPUT
echo "resource-priority=medium" >> $GITHUB_OUTPUT
;;
esac
# Set resource limits based on node count and instance type
case "${{ matrix.nodes }}" in
"1")
echo "max-parallel-jobs=3" >> $GITHUB_OUTPUT
;;
"2")
echo "max-parallel-jobs=2" >> $GITHUB_OUTPUT
;;
"3")
echo "max-parallel-jobs=1" >> $GITHUB_OUTPUT
;;
*)
echo "max-parallel-jobs=2" >> $GITHUB_OUTPUT
;;
esac
echo "Distribution: ${{ matrix.distribution }}, Nodes: ${{ matrix.nodes }}, Instance: ${{ matrix.instance-type }}"
echo "Resource Priority: $(echo '${{ steps.dist-config.outputs.resource-priority }}' || echo 'medium')"
- name: Check if cluster exists
id: check-cluster
run: |
set -e
CLUSTER_NAME="${{ needs.setup.outputs.channel-name }}-${{ matrix.k8s-version }}-${{ matrix.distribution }}"
echo "Checking for existing cluster: $CLUSTER_NAME"
# Get clusters with error handling
RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: ${{ env.REPLICATED_API_TOKEN }}" \
"https://api.replicated.com/vendor/v3/clusters")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" != "200" ]; then
echo "API request failed with HTTP $HTTP_CODE"
echo "Response: $BODY"
echo "cluster-exists=false" >> $GITHUB_OUTPUT
exit 0
fi
# Parse JSON response safely
CLUSTER_ID=$(echo "$BODY" | jq -r --arg name "$CLUSTER_NAME" \
'if .clusters then .clusters[] | select(.name == $name and .status != "terminated") | .id else empty end' 2>/dev/null | head -1)
if [ -n "$CLUSTER_ID" ] && [ "$CLUSTER_ID" != "null" ]; then
echo "Found existing cluster: $CLUSTER_ID"
echo "cluster-exists=true" >> $GITHUB_OUTPUT
echo "cluster-id=$CLUSTER_ID" >> $GITHUB_OUTPUT
# Export kubeconfig for existing cluster with error handling
KUBECONFIG_RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: ${{ env.REPLICATED_API_TOKEN }}" \
"https://api.replicated.com/vendor/v3/cluster/$CLUSTER_ID/kubeconfig")
KUBECONFIG_HTTP_CODE=$(echo "$KUBECONFIG_RESPONSE" | tail -n1)
KUBECONFIG_BODY=$(echo "$KUBECONFIG_RESPONSE" | sed '$d')
if [ "$KUBECONFIG_HTTP_CODE" = "200" ]; then
# Extract and decode the kubeconfig from JSON response
KUBECONFIG_CONTENT=$(echo "$KUBECONFIG_BODY" | jq -r '.kubeconfig // empty' 2>/dev/null)
if [ -n "$KUBECONFIG_CONTENT" ] && [ "$KUBECONFIG_CONTENT" != "null" ] && [ "$KUBECONFIG_CONTENT" != "empty" ]; then
# Decode base64 kubeconfig content and write to file
echo "$KUBECONFIG_CONTENT" | base64 -d > /tmp/kubeconfig 2>/dev/null || echo "$KUBECONFIG_CONTENT" > /tmp/kubeconfig
if [ -s /tmp/kubeconfig ]; then
# Validate kubeconfig format
if kubectl config view --kubeconfig=/tmp/kubeconfig --minify &>/dev/null; then
echo "KUBECONFIG=/tmp/kubeconfig" >> $GITHUB_ENV
echo "Successfully extracted and validated kubeconfig for existing cluster"
else
echo "Kubeconfig format validation failed, skipping cluster validation"
fi
else
echo "Failed to write kubeconfig to file"
fi
else
echo "Failed to extract kubeconfig from response - content is empty or null"
fi
else
echo "Failed to get kubeconfig for cluster $CLUSTER_ID"
fi
else
echo "Cluster does not exist"
echo "cluster-exists=false" >> $GITHUB_OUTPUT
fi
- name: Create cluster
id: create-cluster
if: steps.check-cluster.outputs.cluster-exists == 'false'
uses: replicatedhq/replicated-actions/[email protected]
with:
api-token: ${{ env.REPLICATED_API_TOKEN }}
kubernetes-distribution: ${{ matrix.distribution }}
kubernetes-version: ${{ matrix.k8s-version }}
cluster-name: ${{ needs.setup.outputs.channel-name }}-${{ matrix.k8s-version }}-${{ matrix.distribution }}
ttl: ${{ steps.dist-config.outputs.cluster-ttl }}
nodes: ${{ matrix.nodes }}
instance-type: ${{ matrix.instance-type }}
disk: ${{ steps.dist-config.outputs.cluster-disk-size }}
export-kubeconfig: 'true'
- name: Set cluster outputs
id: set-cluster-outputs
run: |
if [ "${{ steps.check-cluster.outputs.cluster-exists }}" == "true" ]; then
echo "cluster-id=${{ steps.check-cluster.outputs.cluster-id }}" >> $GITHUB_OUTPUT
else
echo "cluster-id=${{ steps.create-cluster.outputs.cluster-id }}" >> $GITHUB_OUTPUT
fi
- name: Setup cluster ports
working-directory: ${{ env.APP_DIR }}
run: |
task cluster-ports-expose CLUSTER_NAME="${{ needs.setup.outputs.channel-name }}-${{ matrix.k8s-version }}-${{ matrix.distribution }}"
- name: Validate cluster readiness
run: |
echo "Validating cluster readiness for ${{ matrix.distribution }} ${{ matrix.k8s-version }}"
# Check if kubeconfig is accessible and properly formatted
if [ ! -f "$KUBECONFIG" ] || [ ! -s "$KUBECONFIG" ]; then
echo "Warning: kubeconfig file not found or empty, skipping cluster validation"
exit 0
fi
# Test kubectl connectivity
if ! kubectl version --client &>/dev/null; then
echo "Warning: kubectl client not working, skipping cluster validation"
exit 0
fi
# Test cluster connectivity with timeout
if ! timeout 30s kubectl cluster-info &>/dev/null; then
echo "Warning: cluster connectivity test failed, skipping validation"
exit 0
fi
# Wait for cluster to be ready
echo "Waiting for cluster nodes to be ready..."
kubectl wait --for=condition=Ready nodes --all --timeout=300s
# Validate cluster nodes
echo "Cluster nodes:"
kubectl get nodes -o wide
echo "Cluster info:"
kubectl cluster-info
- name: Deploy application
working-directory: ${{ env.APP_DIR }}
run: |
task customer-helm-install \
CUSTOMER_NAME="${{ needs.setup.outputs.customer-name }}" \
CLUSTER_NAME="${{ needs.setup.outputs.channel-name }}-${{ matrix.k8s-version }}-${{ matrix.distribution }}" \
CHANNEL_SLUG="${{ needs.create-resources.outputs.channel-slug }}" \
REPLICATED_LICENSE_ID="${{ needs.create-resources.outputs.license-id }}"
timeout-minutes: ${{ matrix.timeout-minutes }}
- name: Run tests
working-directory: ${{ env.APP_DIR }}
run: task test
timeout-minutes: 10
- name: Run distribution-specific tests
run: |
echo "Running ${{ matrix.distribution }}-specific tests..."
# Test node configuration based on matrix
EXPECTED_NODES=${{ matrix.nodes }}
ACTUAL_NODES=$(kubectl get nodes --no-headers | wc -l)
if [ "$ACTUAL_NODES" -eq "$EXPECTED_NODES" ]; then
echo "✅ Node count validation passed: $ACTUAL_NODES/$EXPECTED_NODES"
else
echo "❌ Node count validation failed: $ACTUAL_NODES/$EXPECTED_NODES"
exit 1
fi
# Distribution-specific storage tests
case "${{ matrix.distribution }}" in
"k3s")
echo "Testing k3s local-path storage..."
kubectl get storageclass local-path -o yaml | grep provisioner | grep rancher.io/local-path
;;
"kind")
echo "Testing kind standard storage..."
kubectl get storageclass standard -o yaml | grep provisioner | grep rancher.io/local-path
;;
"eks")
echo "Testing EKS GP2 storage..."
kubectl get storageclass gp2 -o yaml | grep provisioner | grep ebs.csi.aws.com || echo "EKS storage validation skipped"
;;
esac
# Test cluster resources
echo "Cluster resource utilization:"
kubectl top nodes --no-headers 2>/dev/null || echo "Metrics not available"
echo "Pod distribution across nodes:"
kubectl get pods -A -o wide | awk '{print $7}' | sort | uniq -c
# Performance monitoring
echo "=== Performance Metrics ==="
echo "Test Environment: ${{ matrix.distribution }} ${{ matrix.k8s-version }} (${{ matrix.nodes }} nodes)"
echo "Instance Type: ${{ matrix.instance-type }}"
echo "Priority: ${{ steps.dist-config.outputs.resource-priority }}"
echo "Deployment Timeout: ${{ matrix.timeout-minutes }} minutes"
# Resource consumption validation
echo "=== Resource Validation ==="
kubectl describe nodes | grep -E "(Name:|Allocatable:|Allocated resources:)" | head -20
# Collect performance timings
echo "=== Test Completion Summary ==="
echo "Matrix Job: ${{ matrix.k8s-version }}-${{ matrix.distribution }}-${{ matrix.nodes }}nodes"
echo "Started: $(date -u)"
echo "Status: Complete"
- name: Upload debug logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: debug-logs-${{ github.run_number }}-${{ matrix.k8s-version }}-${{ matrix.distribution }}
path: |
/tmp/*.log
~/.replicated/