diff --git a/.github/workflows/generic-gateway-chart-cicd.yml b/.github/workflows/generic-gateway-chart-cicd.yml
new file mode 100644
index 0000000..8d048d4
--- /dev/null
+++ b/.github/workflows/generic-gateway-chart-cicd.yml
@@ -0,0 +1,337 @@
+name: Generic Gateway Helm Chart CI/CD (reusable)
+
+on:
+  workflow_call:
+    inputs:
+      chart-path:
+        description: Path to the Helm chart directory
+        required: false
+        default: chart
+        type: string
+      chartmuseum-url:
+        description: ChartMuseum upload endpoint
+        required: false
+        default: https://charts.sf9.io/api/charts
+        type: string
+      helm-version:
+        description: Helm version to use for lint/template/package
+        required: false
+        default: "3.2.4"
+        type: string
+      version-prefix:
+        description: Version prefix in major.minor format
+        required: false
+        default: "1.0"
+        type: string
+      package-destination:
+        description: Directory where packaged chart artifacts are written
+        required: false
+        default: .helm-packages
+        type: string
+      update-dependencies:
+        description: Run helm package with --dependency-update
+        required: false
+        default: true
+        type: boolean
+      validate-routing:
+        description: Validate default/ingress/gateway/dual routing rendering modes
+        required: false
+        default: true
+        type: boolean
+      validate-configmap:
+        description: Validate ConfigMap gating and merged key rendering
+        required: false
+        default: true
+        type: boolean
+    secrets:
+      registry-username:
+        description: ChartMuseum username
+        required: true
+      registry-password:
+        description: ChartMuseum password or token
+        required: true
+      github-token:
+        description: GitHub token retained for baseline secret-name compatibility
+        required: false
+    outputs:
+      version:
+        description: Generated chart semantic version
+        value: ${{ jobs.version.outputs.version }}
+      chart-name:
+        description: Helm chart name read from Chart.yaml
+        value: ${{ jobs.package-and-push.outputs.chart-name }}
+      chart-package:
+        description: Packaged chart path in the runner workspace
+        value: ${{ jobs.package-and-push.outputs.chart-package }}
+      helm-chart:
+        description: ChartMuseum URL where package upload occurred
+        value: ${{ jobs.package-and-push.outputs.chart-url }}
+
+  workflow_dispatch:
+    inputs:
+      chart-path:
+        description: Path to the Helm chart directory
+        required: false
+        default: chart
+        type: string
+      chartmuseum-url:
+        description: ChartMuseum upload endpoint
+        required: false
+        default: https://charts.sf9.io/api/charts
+        type: string
+      helm-version:
+        description: Helm version to use for lint/template/package
+        required: false
+        default: "3.2.4"
+        type: string
+      version-prefix:
+        description: Version prefix in major.minor format
+        required: false
+        default: "1.0"
+        type: string
+      package-destination:
+        description: Directory where packaged chart artifacts are written
+        required: false
+        default: .helm-packages
+        type: string
+      update-dependencies:
+        description: Run helm package with --dependency-update
+        required: false
+        default: true
+        type: boolean
+      validate-routing:
+        description: Validate default/ingress/gateway/dual routing rendering modes
+        required: false
+        default: true
+        type: boolean
+      validate-configmap:
+        description: Validate ConfigMap gating and merged key rendering
+        required: false
+        default: true
+        type: boolean
+
+permissions:
+  contents: read
+
+jobs:
+  version:
+    runs-on: ubuntu-latest
+    outputs:
+      version: ${{ steps.compute.outputs.version }}
+    steps:
+      - name: Compute chart semantic version from run number
+        id: compute
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          PREFIX="${{ inputs.version-prefix }}"
+          if ! [[ "$PREFIX" =~ ^[0-9]+\.[0-9]+$ ]]; then
+            echo "Error: version-prefix must be in '<major>.<minor>' format, got '$PREFIX'" >&2
+            exit 1
+          fi
+
+          patch=$((GITHUB_RUN_NUMBER - 1))
+          if [ "$patch" -lt 0 ]; then
+            patch=0
+          fi
+
+          version="${PREFIX}.${patch}"
+          echo "version=${version}" >> "$GITHUB_OUTPUT"
+          echo "Computed chart version: ${version}"
+
+  validate-chart:
+    runs-on: ubuntu-latest
+    needs: [version]
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Set up Helm
+        uses: azure/setup-helm@v4
+        with:
+          version: ${{ inputs.helm-version }}
+
+      - name: Helm lint
+        shell: bash
+        run: |
+          set -euo pipefail
+          helm lint "${{ inputs.chart-path }}"
+
+      - name: Validate routing render modes
+        if: ${{ inputs.validate-routing }}
+        shell: bash
+        run: |
+          set -euo pipefail
+          CHART_PATH="${{ inputs.chart-path }}"
+
+          check_mode() {
+            local label="$1"
+            local expect_ingress="$2"
+            local expect_httproute="$3"
+            shift 3
+
+            local rendered
+            rendered="$(helm template "$label" "$CHART_PATH" "$@")"
+
+            if [ "$expect_ingress" = "true" ]; then
+              if ! printf '%s\n' "$rendered" | grep -q '^kind: Ingress$'; then
+                echo "Error: expected Ingress for mode '$label' but none was rendered." >&2
+                exit 1
+              fi
+            else
+              if printf '%s\n' "$rendered" | grep -q '^kind: Ingress$'; then
+                echo "Error: did not expect Ingress for mode '$label' but one was rendered." >&2
+                exit 1
+              fi
+            fi
+
+            if [ "$expect_httproute" = "true" ]; then
+              if ! printf '%s\n' "$rendered" | grep -q '^kind: HTTPRoute$'; then
+                echo "Error: expected HTTPRoute for mode '$label' but none was rendered." >&2
+                exit 1
+              fi
+            else
+              if printf '%s\n' "$rendered" | grep -q '^kind: HTTPRoute$'; then
+                echo "Error: did not expect HTTPRoute for mode '$label' but one was rendered." >&2
+                exit 1
+              fi
+            fi
+
+            echo "Mode '$label' rendered as expected."
+          }
+
+          check_mode default false true
+          check_mode ingress-only true false --set ingress.enabled=true --set gateway.enabled=false
+          check_mode gateway-only false true --set ingress.enabled=false --set gateway.enabled=true
+          check_mode dual true true --set ingress.enabled=true --set gateway.enabled=true
+
+      - name: Validate ConfigMap toggle behavior
+        if: ${{ inputs.validate-configmap }}
+        shell: bash
+        run: |
+          set -euo pipefail
+          CHART_PATH="${{ inputs.chart-path }}"
+
+          default_rendered="$(helm template cfg-default "$CHART_PATH")"
+          if printf '%s\n' "$default_rendered" | grep -q '^kind: ConfigMap$'; then
+            echo "Error: ConfigMap should not render by default when config.enabled=false." >&2
+            exit 1
+          fi
+          echo "ConfigMap disabled-by-default behavior is valid."
+
+          enabled_rendered="$(helm template cfg-enabled "$CHART_PATH" \
+            --set config.enabled=true \
+            --set-string environmentVariables.APP_ENV=development \
+            --set-string config.data.FEATURE_FLAG=on)"
+
+          if ! printf '%s\n' "$enabled_rendered" | grep -q '^kind: ConfigMap$'; then
+            echo "Error: ConfigMap should render when config.enabled=true." >&2
+            exit 1
+          fi
+          if ! printf '%s\n' "$enabled_rendered" | grep -Eq '^  name: .*-config$'; then
+            echo "Error: ConfigMap name did not match expected '<app>-config' pattern." >&2
+            exit 1
+          fi
+          if ! printf '%s\n' "$enabled_rendered" | grep -q '^  APP_ENV:'; then
+            echo "Error: APP_ENV key from environmentVariables was not found in ConfigMap data." >&2
+            exit 1
+          fi
+          if ! printf '%s\n' "$enabled_rendered" | grep -q '^  FEATURE_FLAG:'; then
+            echo "Error: FEATURE_FLAG key from config.data was not found in ConfigMap data." >&2
+            exit 1
+          fi
+
+          echo "ConfigMap enabled behavior and merged key rendering are valid."
+
+  package-and-push:
+    runs-on: ubuntu-latest
+    needs: [version, validate-chart]
+    outputs:
+      chart-name: ${{ steps.package.outputs.chart-name }}
+      chart-package: ${{ steps.package.outputs.chart-package }}
+      chart-url: ${{ steps.upload.outputs.chart-url }}
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Set up Helm
+        uses: azure/setup-helm@v4
+        with:
+          version: ${{ inputs.helm-version }}
+
+      - name: Package chart
+        id: package
+        shell: bash
+        run: |
+          set -euo pipefail
+          CHART_PATH="${{ inputs.chart-path }}"
+          DEST="${{ inputs.package-destination }}"
+          VERSION="${{ needs.version.outputs.version }}"
+
+          mkdir -p "$DEST"
+
+          CHART_NAME="$(helm show chart "$CHART_PATH" | awk '$1=="name:" { print $2; exit }')"
+          if [ -z "$CHART_NAME" ]; then
+            echo "Error: failed to resolve chart name from ${CHART_PATH}/Chart.yaml." >&2
+            exit 1
+          fi
+
+          if [ "${{ inputs.update-dependencies }}" = "true" ]; then
+            helm package "$CHART_PATH" --dependency-update --version "$VERSION" --app-version "$VERSION" --destination "$DEST"
+          else
+            helm package "$CHART_PATH" --version "$VERSION" --app-version "$VERSION" --destination "$DEST"
+          fi
+
+          PACKAGE_FILE="${DEST}/${CHART_NAME}-${VERSION}.tgz"
+          if [ ! -f "$PACKAGE_FILE" ]; then
+            echo "Error: expected package '${PACKAGE_FILE}' was not created." >&2
+            ls -la "$DEST" >&2
+            exit 1
+          fi
+
+          echo "chart-name=$CHART_NAME" >> "$GITHUB_OUTPUT"
+          echo "chart-package=$PACKAGE_FILE" >> "$GITHUB_OUTPUT"
+
+          echo "Packaged ${CHART_NAME} version ${VERSION} at ${PACKAGE_FILE}."
+
+      - name: Upload chart package(s) to ChartMuseum
+        id: upload
+        shell: bash
+        env:
+          CHARTMUSEUM_URL: ${{ inputs.chartmuseum-url }}
+          CHARTMUSEUM_USER: ${{ secrets.registry-username }}
+          CHARTMUSEUM_PASSWORD: ${{ secrets.registry-password }}
+        run: |
+          set -euo pipefail
+
+          if [ -z "${CHARTMUSEUM_URL}" ]; then
+            echo "Error: chartmuseum-url input is empty." >&2
+            exit 1
+          fi
+
+          shopt -s nullglob
+          chart_packages=("${{ inputs.package-destination }}"/*.tgz)
+          if [ ${#chart_packages[@]} -eq 0 ]; then
+            echo "Error: no .tgz chart artifacts found in '${{ inputs.package-destination }}'." >&2
+            exit 1
+          fi
+
+          for package_file in "${chart_packages[@]}"; do
+            echo "Uploading ${package_file} to ${CHARTMUSEUM_URL}"
+            curl --fail --show-error --silent \
+              --data-binary "@${package_file}" \
+              "${CHARTMUSEUM_URL}" \
+              --user "${CHARTMUSEUM_USER}:${CHARTMUSEUM_PASSWORD}"
+          done
+
+          echo "chart-url=${CHARTMUSEUM_URL}" >> "$GITHUB_OUTPUT"
+
+      - name: Workflow summary
+        shell: bash
+        run: |
+          echo "### Helm Chart Package + Push" >> "$GITHUB_STEP_SUMMARY"
+          echo "- Chart Name: ${{ steps.package.outputs.chart-name }}" >> "$GITHUB_STEP_SUMMARY"
+          echo "- Version: ${{ needs.version.outputs.version }}" >> "$GITHUB_STEP_SUMMARY"
+          echo "- Package: ${{ steps.package.outputs.chart-package }}" >> "$GITHUB_STEP_SUMMARY"
+          echo "- Upload Endpoint: ${{ steps.upload.outputs.chart-url }}" >> "$GITHUB_STEP_SUMMARY"
