From 0df468d68085b66390f67d410fd7d7f5aa446fc2 Mon Sep 17 00:00:00 2001
From: Musa Misto <64855513+MusaMisto@users.noreply.github.com>
Date: Sun, 14 Jun 2026 15:31:09 +0300
Subject: [PATCH 1/9] fix(helm-generic): parameterize migration job, namespace
 checkpoints, drop dead ingress logic

The helm-generic composite action is used inside the generic-chart-helm
(ingress-nginx) and generic-gateway-helm-template (Gateway API) reusable
workflows. Three fixes, all verified behavior-preserving for existing callers:

#1 Migration Job was hardcoded to one app (`dotnet SW.Shanap.Web.dll --migrate`,
   `connection-string`, `ConnectionStrings__ShanapDb`), so any other app (e.g.
   mealivery) running the "generic" workflow executed the wrong entrypoint.
   Added init_job_command / init_job_secret_key / init_job_connstring_env /
   init_job_environment_env inputs (action + both workflows) that coalesce to
   the historical defaults when empty -> identical Job for existing callers,
   correct entrypoint for new ones.

#2 The action wrote CHECKPOINT_1_STATUS/CHECKPOINT_2_STATUS into $GITHUB_ENV,
   colliding with the same names owned by the calling workflows (different
   meanings). Renamed to HELM_GENERIC_KUBECONFIG_STATUS / _DEPLOY_STATUS so the
   workflows' checkpoint summaries are no longer clobbered.

#3 The action always injected `--set ingress.paths[0]=/`. Removed it: the
   ingress chart (s9genericchart) defaults ingress.paths to ["/"], and the
   gateway chart (s9genericchart-v2) has no ingress template at all, so removal
   renders identical manifests in both workflows (verified via helm template).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .github/actions/helm-generic/action.yml       | 94 +++++++++++--------
 .github/workflows/generic-chart-helm.yml      | 30 +++++-
 .../generic-gateway-helm-template.yml         | 26 +++++
 3 files changed, 108 insertions(+), 42 deletions(-)

diff --git a/.github/actions/helm-generic/action.yml b/.github/actions/helm-generic/action.yml
index cd8f92e..1378933 100644
--- a/.github/actions/helm-generic/action.yml
+++ b/.github/actions/helm-generic/action.yml
@@ -37,16 +37,6 @@ inputs:
     description: "Raw extra args appended to helm upgrade (e.g. --debug)"
     required: false
     default: ''
-  ingress_paths:
-    description: |-
-      YAML list of ingress paths. Provide as a multi-line YAML sequence, e.g.:
-        - /
-        - /api
-        - /docs
-      Defaults to just the root path '/'. Each entry becomes --set ingress.paths[index]=<value>.
-    required: false
-    default: |-
-      - /
   kubeconfig_data:
     description: |-
       Raw kubeconfig YAML or base64-encoded kubeconfig data. This replaces the dynamic secret (<PROFILE>_KUBECONFIG).
@@ -64,8 +54,34 @@ inputs:
     default: ''
   init_job_secret_name:
     description: |-
-      Name of the Kubernetes Secret (in the target namespace) whose 'connection-string' key is
-      injected as ConnectionStrings__ShanapDb into the migration Job. Required when init_job_image is set.
+      Name of the Kubernetes Secret (in the target namespace) whose key (see init_job_secret_key)
+      is injected as the connection-string env var (see init_job_connstring_env) into the migration
+      Job. Required when init_job_image is set.
+    required: false
+    default: ''
+  init_job_command:
+    description: |-
+      Command (and args) the migration Job container runs, as a space-separated string.
+      Converted to a JSON exec array, e.g. "dotnet App.dll --migrate" -> ["dotnet","App.dll","--migrate"].
+      Leave empty to keep the historical default ('dotnet SW.Shanap.Web.dll --migrate').
+    required: false
+    default: ''
+  init_job_secret_key:
+    description: |-
+      Key within init_job_secret_name whose value holds the DB connection string.
+      Leave empty to keep the historical default ('connection-string').
+    required: false
+    default: ''
+  init_job_connstring_env:
+    description: |-
+      Name of the environment variable the connection string is injected into inside the migration Job.
+      Leave empty to keep the historical default ('ConnectionStrings__ShanapDb').
+    required: false
+    default: ''
+  init_job_environment_env:
+    description: |-
+      Name of the environment variable carrying the environment name (the 'environment' input) inside
+      the migration Job. Leave empty to keep the historical default ('ASPNETCORE_ENVIRONMENT').
     required: false
     default: ''
   environment:
@@ -85,8 +101,8 @@ runs:
       run: |
         echo "::notice title=☸️ [HELM] Deploy Chart::App: ${{ inputs.app_name }}, chart: ${{ inputs.chart }}, namespace: ${{ inputs.namespace }}, env: ${{ inputs.environment }}"
         echo "⚙️ [HELM] Chart repo: ${{ inputs.chart_repo != '' && inputs.chart_repo || 'OCI (inline)' }}, timeout: ${{ inputs.helm_timeout }}"
-        echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV"
-        echo "CHECKPOINT_2_STATUS=⏳ Pending" >> "$GITHUB_ENV"
+        echo "HELM_GENERIC_KUBECONFIG_STATUS=⏳ Pending" >> "$GITHUB_ENV"
+        echo "HELM_GENERIC_DEPLOY_STATUS=⏳ Pending" >> "$GITHUB_ENV"
 
     - name: Derive sanitized branch name
       id: branch
@@ -126,7 +142,7 @@ runs:
         fi
         chmod 600 kubeconfig.yaml
         echo "kubeconfig_path=${{ github.workspace }}/kubeconfig.yaml" >> "$GITHUB_OUTPUT"
-        echo "CHECKPOINT_1_STATUS=✅ PASSED" >> "$GITHUB_ENV"
+        echo "HELM_GENERIC_KUBECONFIG_STATUS=✅ PASSED" >> "$GITHUB_ENV"
         echo "::notice title=✅ [HELM] Kubeconfig configured::namespace: ${{ inputs.namespace }}"
         echo "::endgroup::"
 
@@ -152,12 +168,26 @@ runs:
         NAMESPACE: ${{ inputs.namespace }}
         INIT_JOB_IMAGE: ${{ inputs.init_job_image }}
         INIT_JOB_SECRET_NAME: ${{ inputs.init_job_secret_name }}
+        INIT_JOB_COMMAND: ${{ inputs.init_job_command }}
+        INIT_JOB_SECRET_KEY: ${{ inputs.init_job_secret_key }}
+        INIT_JOB_CONNSTRING_ENV: ${{ inputs.init_job_connstring_env }}
+        INIT_JOB_ENVIRONMENT_ENV: ${{ inputs.init_job_environment_env }}
+        ENVIRONMENT: ${{ inputs.environment }}
       run: |
         set -euo pipefail
 
         JOB_NAME="${APP_NAME}-db-migrate-${GITHUB_RUN_NUMBER}"
 
-        echo "Applying migration Job: $JOB_NAME"
+        # Coalesce to the historical defaults so callers that don't override keep identical behavior.
+        CMD_STR="${INIT_JOB_COMMAND:-dotnet SW.Shanap.Web.dll --migrate}"
+        SECRET_KEY="${INIT_JOB_SECRET_KEY:-connection-string}"
+        CONNSTRING_ENV="${INIT_JOB_CONNSTRING_ENV:-ConnectionStrings__ShanapDb}"
+        ENVIRONMENT_ENV="${INIT_JOB_ENVIRONMENT_ENV:-ASPNETCORE_ENVIRONMENT}"
+
+        # Build a JSON exec array from the space-separated command string.
+        CMD_JSON=$(jq -cn --arg s "$CMD_STR" '$s | split(" ") | map(select(length > 0))')
+
+        echo "Applying migration Job: $JOB_NAME (command: ${CMD_JSON})"
         kubectl apply -f - <<EOF
         apiVersion: batch/v1
         kind: Job
@@ -177,15 +207,15 @@ runs:
                 - name: db-migrate
                   image: "${INIT_JOB_IMAGE}"
                   imagePullPolicy: Always
-                  command: ["dotnet", "SW.Shanap.Web.dll", "--migrate"]
+                  command: ${CMD_JSON}
                   env:
-                    - name: ConnectionStrings__ShanapDb
+                    - name: ${CONNSTRING_ENV}
                       valueFrom:
                         secretKeyRef:
                           name: "${INIT_JOB_SECRET_NAME}"
-                          key: connection-string
-                    - name: ASPNETCORE_ENVIRONMENT
-                      value: "${{ inputs.environment }}"
+                          key: ${SECRET_KEY}
+                    - name: ${ENVIRONMENT_ENV}
+                      value: "${ENVIRONMENT}"
         EOF
 
         echo "Waiting for migration Job to complete (timeout 5m)..."
@@ -211,7 +241,6 @@ runs:
         HELM_TIMEOUT: ${{ inputs.helm_timeout }}
         EXTRA_SET_VALUES: ${{ inputs.extra_set_values }}
         EXTRA_ARGS: ${{ inputs.extra_args }}
-        INGRESS_PATHS: ${{ inputs.ingress_paths }}
         BRANCH: ${{ steps.branch.outputs.name }}
       run: |
         echo "::group::☸️ [CHECKPOINT 2/2] Helm Upgrade / Install"
@@ -232,21 +261,6 @@ runs:
           --set app.version="github-${BRANCH}-${GITHUB_RUN_NUMBER}"
         )
 
-        PATH_SET_ARGS=()
-        if [ -n "${INGRESS_PATHS// /}" ]; then
-          INDEX=0
-          CLEAN_PATHS="$(printf '%s' "$INGRESS_PATHS" | tr -d '\r')"
-          while IFS= read -r line; do
-            line="$(printf '%s' "$line" | sed -E 's/#.*$//')"
-            line="$(printf '%s' "$line" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
-            [ -z "$line" ] && continue
-            P="$(printf '%s' "$line" | sed -E 's/^-[[:space:]]+//; s/^"//; s/"$//; s/^'\''//; s/'\''$//')"
-            [ -z "$P" ] && continue
-            PATH_SET_ARGS+=( --set "ingress.paths[$INDEX]=$P" )
-            INDEX=$((INDEX+1))
-          done <<< "$CLEAN_PATHS"
-        fi
-
         EXTRA_SET_ARGS=""
         if [ -n "${EXTRA_SET_VALUES// /}" ]; then
           while IFS= read -r line; do
@@ -262,10 +276,10 @@ runs:
           --install \
           --namespace "${NAMESPACE}" \
           --create-namespace \
-          "${BASE_SET_ARGS[@]}" "${PATH_SET_ARGS[@]}" ${EXTRA_SET_ARGS} \
+          "${BASE_SET_ARGS[@]}" ${EXTRA_SET_ARGS} \
           --timeout "${HELM_TIMEOUT}" \
           ${EXTRA_ARGS}
-        echo "CHECKPOINT_2_STATUS=✅ PASSED" >> "$GITHUB_ENV"
+        echo "HELM_GENERIC_DEPLOY_STATUS=✅ PASSED" >> "$GITHUB_ENV"
         echo "::notice title=✅ [HELM] Helm deploy complete::App: ${APP_NAME}, namespace: ${NAMESPACE}"
         echo "::endgroup::"
 
@@ -285,4 +299,4 @@ runs:
       if: failure()
       shell: bash
       run: |
-        echo "::error title=❌ [HELM] Deploy failed::App '${{ inputs.app_name }}' in namespace '${{ inputs.namespace }}'. Checkpoints — 1) Kubeconfig: ${CHECKPOINT_1_STATUS:-⏭️ Not reached} | 2) Helm deploy: ${CHECKPOINT_2_STATUS:-⏭️ Not reached}."
+        echo "::error title=❌ [HELM] Deploy failed::App '${{ inputs.app_name }}' in namespace '${{ inputs.namespace }}'. Checkpoints — 1) Kubeconfig: ${HELM_GENERIC_KUBECONFIG_STATUS:-⏭️ Not reached} | 2) Helm deploy: ${HELM_GENERIC_DEPLOY_STATUS:-⏭️ Not reached}."
diff --git a/.github/workflows/generic-chart-helm.yml b/.github/workflows/generic-chart-helm.yml
index b6b7d32..1ac9f76 100644
--- a/.github/workflows/generic-chart-helm.yml
+++ b/.github/workflows/generic-chart-helm.yml
@@ -130,12 +130,34 @@ on:
         type: string
       init-job-secret-name:
         description: |-
-          Name of the Kubernetes Secret (in the target namespace) containing a 'connection-string'
-          key with the doadmin credentials. Required when init-job-image is set.
+          Name of the Kubernetes Secret (in the target namespace) containing the connection-string
+          key (see init-job-secret-key) with the doadmin credentials. Required when init-job-image is set.
           Example: mealivery-db-admin-secret
         required: false
         default: ''
         type: string
+      init-job-command:
+        description: |-
+          Command (space-separated) the migration Job runs, e.g. 'dotnet Mealivery.Api.dll --migrate'.
+          Leave empty to keep the historical default ('dotnet SW.Shanap.Web.dll --migrate').
+        required: false
+        default: ''
+        type: string
+      init-job-secret-key:
+        description: Key in init-job-secret-name holding the connection string (default 'connection-string').
+        required: false
+        default: ''
+        type: string
+      init-job-connstring-env:
+        description: Env var name the connection string is injected into (default 'ConnectionStrings__ShanapDb').
+        required: false
+        default: ''
+        type: string
+      init-job-environment-env:
+        description: Env var name carrying the environment name in the migration Job (default 'ASPNETCORE_ENVIRONMENT').
+        required: false
+        default: ''
+        type: string
 
     secrets:
       # Container registry secrets
@@ -492,6 +514,10 @@ jobs:
           kubeconfig_data: ${{ secrets.kubeconfig }}
           init_job_image: ${{ inputs.init-job-image }}
           init_job_secret_name: ${{ inputs.init-job-secret-name }}
+          init_job_command: ${{ inputs.init-job-command }}
+          init_job_secret_key: ${{ inputs.init-job-secret-key }}
+          init_job_connstring_env: ${{ inputs.init-job-connstring-env }}
+          init_job_environment_env: ${{ inputs.init-job-environment-env }}
           environment: ${{ inputs.environment }}
 
       - name: Confirm Helm deployed
diff --git a/.github/workflows/generic-gateway-helm-template.yml b/.github/workflows/generic-gateway-helm-template.yml
index 8078796..e7b0778 100644
--- a/.github/workflows/generic-gateway-helm-template.yml
+++ b/.github/workflows/generic-gateway-helm-template.yml
@@ -255,6 +255,28 @@ on:
         required: false
         default: ''
         type: string
+      init-job-command:
+        description: |-
+          Command (space-separated) the migration Job runs. Leave empty to keep the
+          historical default ('dotnet SW.Shanap.Web.dll --migrate').
+        required: false
+        default: ''
+        type: string
+      init-job-secret-key:
+        description: Key in init-job-secret-name holding the connection string (default 'connection-string').
+        required: false
+        default: ''
+        type: string
+      init-job-connstring-env:
+        description: Env var name the connection string is injected into (default 'ConnectionStrings__ShanapDb').
+        required: false
+        default: ''
+        type: string
+      init-job-environment-env:
+        description: Env var name carrying the environment name in the migration Job (default 'ASPNETCORE_ENVIRONMENT').
+        required: false
+        default: ''
+        type: string
 
     secrets:
       registry-username:
@@ -882,6 +904,10 @@ jobs:
           kubeconfig_data: ${{ secrets.kubeconfig }}
           init_job_image: ${{ inputs.init-job-image }}
           init_job_secret_name: ${{ inputs.init-job-secret-name }}
+          init_job_command: ${{ inputs.init-job-command }}
+          init_job_secret_key: ${{ inputs.init-job-secret-key }}
+          init_job_connstring_env: ${{ inputs.init-job-connstring-env }}
+          init_job_environment_env: ${{ inputs.init-job-environment-env }}
           environment: ${{ inputs.environment }}
 
       - name: Confirm Helm deployed

From c2d4c86ad035540cce19593f8b49658a57d0aa83 Mon Sep 17 00:00:00 2001
From: Musa Misto <64855513+MusaMisto@users.noreply.github.com>
Date: Sun, 14 Jun 2026 15:33:58 +0300
Subject: [PATCH 2/9] fix(helm-generic): correct docs, drop dead inputs, ensure
 namespace before migration

#7 cleanups:
- Remove dead inputs `registry_profile` and `version` (declared but never used;
  neither calling workflow passes them, so removal is contract-safe).
- Remove the redundant `app.name` set from both workflows' extra_set_values; the
  action already sets it canonically from the required app_name input (identical
  value, verified via helm template). app.version is left in place since the
  workflows intentionally override the action's branch-based value with the semver.
- Fix README.md and AGENTS.md, which incorrectly described helm-generic as a
  "checkout + lint + package + push" CI action; it is a deployer.

Namespace ordering fix:
- The migration Job ran before the Helm step's --create-namespace, so a first
  deploy to a fresh namespace failed at the Job apply. Ensure the namespace exists
  first via the idempotent `kubectl create --dry-run=client | apply` pattern already
  used by generic-gateway-helm-template. No-op when the namespace already exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .github/actions/helm-generic/action.yml             | 13 +++++--------
 .github/workflows/generic-chart-helm.yml            |  1 -
 .github/workflows/generic-gateway-helm-template.yml |  1 -
 AGENTS.md                                           |  2 +-
 README.md                                           |  2 +-
 5 files changed, 7 insertions(+), 12 deletions(-)

diff --git a/.github/actions/helm-generic/action.yml b/.github/actions/helm-generic/action.yml
index 1378933..31551a0 100644
--- a/.github/actions/helm-generic/action.yml
+++ b/.github/actions/helm-generic/action.yml
@@ -2,17 +2,9 @@ name: 'Helm Deploy'
 description: 'Deploy a Helm chart to a Kubernetes cluster'
 
 inputs:
-  registry_profile:
-    description: "Profile code for dynamic kubeconfig/registry vars & secrets (e.g., S9, WISEWELL)"
-    required: false
-    default: S9
   app_name:
     description: "Application name (Helm release name)"
     required: true
-  version:
-    description: "Logical version label (may be used in image tag selection)"
-    required: false
-    default: staging
   namespace:
     description: "Kubernetes namespace to deploy into"
     required: true
@@ -178,6 +170,11 @@ runs:
 
         JOB_NAME="${APP_NAME}-db-migrate-${GITHUB_RUN_NUMBER}"
 
+        # Ensure the target namespace exists before applying the Job. The Helm step uses
+        # --create-namespace, but it runs AFTER this Job, so on a first deploy to a fresh
+        # namespace the Job apply would otherwise fail. Idempotent (no-op if it already exists).
+        kubectl create namespace "${NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f -
+
         # Coalesce to the historical defaults so callers that don't override keep identical behavior.
         CMD_STR="${INIT_JOB_COMMAND:-dotnet SW.Shanap.Web.dll --migrate}"
         SECRET_KEY="${INIT_JOB_SECRET_KEY:-connection-string}"
diff --git a/.github/workflows/generic-chart-helm.yml b/.github/workflows/generic-chart-helm.yml
index 1ac9f76..efafb52 100644
--- a/.github/workflows/generic-chart-helm.yml
+++ b/.github/workflows/generic-chart-helm.yml
@@ -498,7 +498,6 @@ jobs:
           # Translate your previous comma-joined --set into line-based entries
           extra_set_values: |
             image.repo=${{ env.CONTAINER_REGISTRY }}
-            app.name=${{ inputs.app-name }}
             app.version=${{ needs.version.outputs.version }}
             image.overrideName=${{ inputs.app-name }}
             image.overrideVersion=${{ needs.version.outputs.version }}
diff --git a/.github/workflows/generic-gateway-helm-template.yml b/.github/workflows/generic-gateway-helm-template.yml
index e7b0778..562c656 100644
--- a/.github/workflows/generic-gateway-helm-template.yml
+++ b/.github/workflows/generic-gateway-helm-template.yml
@@ -890,7 +890,6 @@ jobs:
           extra_args: --wait
           extra_set_values: |
             image.repo=${{ env.CONTAINER_REGISTRY }}
-            app.name=${{ inputs.app-name || github.repository }}
             app.version=${{ needs.version.outputs.version }}
             image.overrideName=${{ inputs.app-name }}
             image.overrideVersion=${{ needs.version.outputs.version }}
diff --git a/AGENTS.md b/AGENTS.md
index 23987f3..9de2ca3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -272,7 +272,7 @@ steps:
 ### Helm
 - `helm-deploy` — Profile-based deploy (`registry_profile` selects dynamic secrets). Supports `init_job_image` for database migration Jobs before deploy.
 - `helm-deploy-s9generic` — Deploy using `s9genericchart` from `https://charts.sf9.io`. Handles `set-values` (`--set`) and `set-string-values` (`--set-string`) separately.
-- `helm-generic` — Checkout + lint + package + push (single composite for chart CI).
+- `helm-generic` — Deploy a Helm chart via `helm upgrade --install`, with an optional pre-deploy DB migration Job (`init_job_image` + `init_job_command`). Internal building block for the `generic-chart-helm` (ingress-nginx) and `generic-gateway-helm-template` (Gateway API) reusable workflows.
 - `helm-package-push` — Package a chart and push to OCI registry.
 
 ### .NET
diff --git a/README.md b/README.md
index cb96008..3d927d4 100644
--- a/README.md
+++ b/README.md
@@ -759,7 +759,7 @@ uses: simplify9/.github/.github/actions/<name>@main
 |---|---|---|
 | `helm-deploy` | Profile-based deploy; supports `init_job_image` for pre-deploy DB migration Jobs | `app_name`, `namespace`, `kubeconfig_data` |
 | `helm-deploy-s9generic` | Deploy `s9genericchart` from `https://charts.sf9.io`; handles `set-values` (`--set`) and `set-string-values` (`--set-string`) separately | `chart-name`, `chart-version`, `kubeconfig` |
-| `helm-generic` | Checkout + Helm lint + package + push in a single composite | `app-name`, `version` |
+| `helm-generic` | Deploy a Helm chart (`helm upgrade --install`) with optional pre-deploy DB migration Job. Used by the `generic-chart-helm` and `generic-gateway-helm-template` reusable workflows | `app_name`, `namespace`, `kubeconfig_data` |
 | `helm-package-push` | Package chart and push to OCI registry | `chart-path`, `chart-name`, `chart-version` |
 
 ### .NET

From 2b272f555cae447648b6ace8f65d303d26030946 Mon Sep 17 00:00:00 2001
From: Musa Misto <64855513+MusaMisto@users.noreply.github.com>
Date: Sun, 14 Jun 2026 15:38:51 +0300
Subject: [PATCH 3/9] fix(helm-generic): harden kubeconfig handling, drop
 wasted checkout, pin kubectl
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

#5 kubeconfig handling:
- Write the decoded kubeconfig to $RUNNER_TEMP instead of the workspace, so the
  credential is no longer left in the checked-out tree, and add an `if: always()`
  cleanup step that removes it (matters on self-hosted/reused runners).
- Adopt the generic-gateway-helm-template workflow's decoder (base64 with raw
  fallback) and add a fail-fast apiVersion validation. Behavior-preserving for the
  two real input forms (base64 secret, raw YAML) — both decode to a semantically
  identical kubeconfig — and now rejects malformed input with a clear message
  instead of letting Helm fail later.

#6:
- Remove `actions/checkout@v6`: the deploy job uses no repo files (chart comes
  from chart_repo, image from the registry); the only workspace reference was the
  old kubeconfig path, now in RUNNER_TEMP. Updated the chart/chart_repo input docs
  to note local chart paths are not supported.
- Pin kubectl to v1.34.0 instead of 'latest' for reproducibility (only basic
  core-resource ops are used, so cluster skew is not a concern; documented as
  bumpable to track the cluster's minor version).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .github/actions/helm-generic/action.yml | 40 +++++++++++++++++++------
 1 file changed, 31 insertions(+), 9 deletions(-)

diff --git a/.github/actions/helm-generic/action.yml b/.github/actions/helm-generic/action.yml
index 31551a0..252350b 100644
--- a/.github/actions/helm-generic/action.yml
+++ b/.github/actions/helm-generic/action.yml
@@ -9,11 +9,11 @@ inputs:
     description: "Kubernetes namespace to deploy into"
     required: true
   chart:
-    description: "Chart name (local or from repo)"
+    description: "Chart name, resolved from chart_repo (this action does not check out the repo, so local chart paths are not supported)"
     required: false
     default: s9genericchart
   chart_repo:
-    description: "Helm repo URL providing the chart (ignored if chart is a path)"
+    description: "Helm repo URL providing the chart"
     required: false
     default: "https://charts.sf9.io"
   helm_timeout:
@@ -86,8 +86,6 @@ inputs:
 runs:
   using: 'composite'
   steps:
-    - uses: actions/checkout@v6
-
     - name: Announce Helm deployment
       shell: bash
       run: |
@@ -127,13 +125,24 @@ runs:
           echo "kubeconfig_data input is required but empty." >&2
           exit 1
         fi
+        # Write to RUNNER_TEMP (not the workspace) so the credential is not left in the
+        # checked-out tree. Accept raw kubeconfig YAML or base64-encoded data, mirroring the
+        # generic-gateway-helm-template workflow's decoder: prefer raw when it already looks
+        # like a kubeconfig, otherwise base64-decode, falling back to raw.
+        KCFG_PATH="${RUNNER_TEMP}/kubeconfig.yaml"
         if echo "$DATA" | grep -q 'apiVersion:'; then
-          printf "%s" "$DATA" > kubeconfig.yaml
+          printf '%s' "$DATA" > "$KCFG_PATH"
+        elif printf '%s' "$DATA" | base64 -d > "$KCFG_PATH" 2>/dev/null; then
+          : # successfully decoded base64
         else
-          echo "$DATA" | base64 -d > kubeconfig.yaml || { echo "Failed to base64 decode kubeconfig_data input" >&2; exit 1; }
+          printf '%s' "$DATA" > "$KCFG_PATH"
+        fi
+        if ! grep -q 'apiVersion:' "$KCFG_PATH"; then
+          echo "kubeconfig_data did not resolve to a valid kubeconfig (no apiVersion found)." >&2
+          exit 1
         fi
-        chmod 600 kubeconfig.yaml
-        echo "kubeconfig_path=${{ github.workspace }}/kubeconfig.yaml" >> "$GITHUB_OUTPUT"
+        chmod 600 "$KCFG_PATH"
+        echo "kubeconfig_path=${KCFG_PATH}" >> "$GITHUB_OUTPUT"
         echo "HELM_GENERIC_KUBECONFIG_STATUS=✅ PASSED" >> "$GITHUB_ENV"
         echo "::notice title=✅ [HELM] Kubeconfig configured::namespace: ${{ inputs.namespace }}"
         echo "::endgroup::"
@@ -150,7 +159,10 @@ runs:
     - name: Install kubectl
       uses: azure/setup-kubectl@v5
       with:
-        version: 'latest'
+        # Pinned for reproducibility (avoids a new kubectl silently changing CI behavior).
+        # Only basic core-resource ops are used here (apply/wait/get/create), so exact skew
+        # with the cluster is not critical; bump this to track your cluster's minor version.
+        version: 'v1.34.0'
 
     - name: Run DB migration Job (init container equivalent)
       if: ${{ inputs.init_job_image != '' }}
@@ -297,3 +309,13 @@ runs:
       shell: bash
       run: |
         echo "::error title=❌ [HELM] Deploy failed::App '${{ inputs.app_name }}' in namespace '${{ inputs.namespace }}'. Checkpoints — 1) Kubeconfig: ${HELM_GENERIC_KUBECONFIG_STATUS:-⏭️ Not reached} | 2) Helm deploy: ${HELM_GENERIC_DEPLOY_STATUS:-⏭️ Not reached}."
+
+    - name: Clean up kubeconfig
+      if: always()
+      shell: bash
+      env:
+        KCFG_PATH: ${{ steps.kube.outputs.kubeconfig_path }}
+      run: |
+        # Remove the decoded kubeconfig so the credential does not linger (matters on
+        # self-hosted/reused runners). No-op if the step never produced a path.
+        [ -n "${KCFG_PATH:-}" ] && rm -f "$KCFG_PATH" || true

From fcba18368541f4b12eeba2abf8d66201b3cf0301 Mon Sep 17 00:00:00 2001
From: Musa Misto <64855513+MusaMisto@users.noreply.github.com>
Date: Sun, 14 Jun 2026 16:01:29 +0300
Subject: [PATCH 4/9] refactor(helm-generic): doc accuracy, env-binding, arg
 arrays, atomic + robustness
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Addresses the cold-review findings M1-M4 and m1-m5 (SHA-pinning intentionally
deferred):

M1 - Rewrite stale input docs: kubeconfig_data no longer references the removed
     <PROFILE> secret concept; init_job_image no longer hardcodes "--migrate"/EF
     Core/doadmin language and now points at init_job_command.
M2 - Reword the leftover "BASE_SET_ARGS now only…/modified input description" comment.
M3 - Bind ${{ inputs.* }} to env: in the Announce, kubeconfig-notice, Set KUBECONFIG
     and Report-failure steps (GitHub injection-hardening guidance); clears shellcheck
     SC2296 noise. Behavior-identical.
M4 - Assemble extra_set_values and extra_args as bash arrays instead of unquoted
     word-split strings, so --set values containing spaces stay intact. Token stream
     is identical for current (space-free) callers.

m1 - Add `atomic` input (default 'true') -> pass --atomic so a failed deploy rolls
     back automatically. (Behavior change: failed upgrades now roll back.)
m2 - Migration wait now polls for Complete/Failed and surfaces a failed Job
     immediately instead of blocking the full 5m timeout.
m3 - Show-status pod selector tries app.kubernetes.io/instance=<release> (gateway
     chart) then falls back to app=<app.name> (ingress chart), so it matches both.
m4 - Rename action to 'Helm Generic Deploy' and add author: Simplify9.
m5 - generic-gateway auto-onboard step now traps EXIT to remove its decoded
     kubeconfig from RUNNER_TEMP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .github/actions/helm-generic/action.yml       | 101 ++++++++++++------
 .../generic-gateway-helm-template.yml         |   3 +
 2 files changed, 74 insertions(+), 30 deletions(-)

diff --git a/.github/actions/helm-generic/action.yml b/.github/actions/helm-generic/action.yml
index 252350b..526649f 100644
--- a/.github/actions/helm-generic/action.yml
+++ b/.github/actions/helm-generic/action.yml
@@ -1,5 +1,6 @@
-name: 'Helm Deploy'
+name: 'Helm Generic Deploy'
 description: 'Deploy a Helm chart to a Kubernetes cluster'
+author: 'Simplify9'
 
 inputs:
   app_name:
@@ -29,19 +30,22 @@ inputs:
     description: "Raw extra args appended to helm upgrade (e.g. --debug)"
     required: false
     default: ''
+  atomic:
+    description: "If 'true', pass --atomic to helm upgrade so a failed deploy is rolled back automatically (implies --wait)."
+    required: false
+    default: 'true'
   kubeconfig_data:
     description: |-
-      Raw kubeconfig YAML or base64-encoded kubeconfig data. This replaces the dynamic secret (<PROFILE>_KUBECONFIG).
-      IMPORTANT: Passing sensitive data as a plain input may expose it in logs. Prefer keeping this workflow reusable only
-      in controlled contexts. Provide either the full YAML (containing apiVersion, clusters, users, etc.) or a base64 string
-      that decodes to the kubeconfig file contents.
+      Kubeconfig used to reach the target cluster, as either raw YAML (containing apiVersion, clusters,
+      users, contexts, ...) or a base64-encoded string of that YAML.
+      Provide it from a secret; do not pass cluster credentials as a plaintext literal.
     required: true
   init_job_image:
     description: |-
-      If set, a Kubernetes Job is created before the Helm deploy that runs this image with the
-      argument '--migrate'. The Job uses the connection string from init_job_secret_name to run
-      EF Core migrations with elevated (doadmin) credentials before the main container starts.
-      Example: registry.digitalocean.com/mealiverycr/mealivery-api:latest
+      If set, a Kubernetes Job runs this image to completion before the Helm deploy (e.g. for database
+      migrations). The command it runs is controlled by init_job_command, and a connection string is
+      injected from init_job_secret_name. Leave empty to skip the pre-deploy Job.
+      Example: registry.digitalocean.com/acme/api:latest
     required: false
     default: ''
   init_job_secret_name:
@@ -88,9 +92,16 @@ runs:
   steps:
     - name: Announce Helm deployment
       shell: bash
+      env:
+        APP_NAME: ${{ inputs.app_name }}
+        CHART: ${{ inputs.chart }}
+        NAMESPACE: ${{ inputs.namespace }}
+        ENVIRONMENT: ${{ inputs.environment }}
+        CHART_REPO: ${{ inputs.chart_repo }}
+        HELM_TIMEOUT: ${{ inputs.helm_timeout }}
       run: |
-        echo "::notice title=☸️ [HELM] Deploy Chart::App: ${{ inputs.app_name }}, chart: ${{ inputs.chart }}, namespace: ${{ inputs.namespace }}, env: ${{ inputs.environment }}"
-        echo "⚙️ [HELM] Chart repo: ${{ inputs.chart_repo != '' && inputs.chart_repo || 'OCI (inline)' }}, timeout: ${{ inputs.helm_timeout }}"
+        echo "::notice title=☸️ [HELM] Deploy Chart::App: ${APP_NAME}, chart: ${CHART}, namespace: ${NAMESPACE}, env: ${ENVIRONMENT}"
+        echo "⚙️ [HELM] Chart repo: ${CHART_REPO:-OCI (inline)}, timeout: ${HELM_TIMEOUT}"
         echo "HELM_GENERIC_KUBECONFIG_STATUS=⏳ Pending" >> "$GITHUB_ENV"
         echo "HELM_GENERIC_DEPLOY_STATUS=⏳ Pending" >> "$GITHUB_ENV"
 
@@ -117,6 +128,7 @@ runs:
       shell: bash
       env:
         KUBECONFIG_DATA: ${{ inputs.kubeconfig_data }}
+        NAMESPACE: ${{ inputs.namespace }}
       run: |
         echo "::group::☸️ [CHECKPOINT 1/2] Process Kubeconfig"
         set -euo pipefail
@@ -144,12 +156,14 @@ runs:
         chmod 600 "$KCFG_PATH"
         echo "kubeconfig_path=${KCFG_PATH}" >> "$GITHUB_OUTPUT"
         echo "HELM_GENERIC_KUBECONFIG_STATUS=✅ PASSED" >> "$GITHUB_ENV"
-        echo "::notice title=✅ [HELM] Kubeconfig configured::namespace: ${{ inputs.namespace }}"
+        echo "::notice title=✅ [HELM] Kubeconfig configured::namespace: ${NAMESPACE}"
         echo "::endgroup::"
 
     - name: Set KUBECONFIG env
       shell: bash
-      run: echo "KUBECONFIG=${{ steps.kube.outputs.kubeconfig_path }}" >> "$GITHUB_ENV"
+      env:
+        KUBECONFIG_PATH: ${{ steps.kube.outputs.kubeconfig_path }}
+      run: echo "KUBECONFIG=${KUBECONFIG_PATH}" >> "$GITHUB_ENV"
 
     - name: Install Helm
       uses: azure/setup-helm@v5
@@ -228,17 +242,24 @@ runs:
         EOF
 
         echo "Waiting for migration Job to complete (timeout 5m)..."
-        kubectl wait job/"${JOB_NAME}" \
-          --for=condition=complete \
-          --namespace="${NAMESPACE}" \
-          --timeout=5m || {
-            echo "Migration Job failed. Fetching logs:"
+        # Poll for Complete or Failed so a failed Job surfaces immediately instead of blocking
+        # the full timeout (kubectl wait --for=condition=complete does not return on failure).
+        DEADLINE=$((SECONDS + 300))
+        while true; do
+          COMPLETE=$(kubectl get job "${JOB_NAME}" -n "${NAMESPACE}" -o jsonpath='{.status.conditions[?(@.type=="Complete")].status}' 2>/dev/null || true)
+          FAILED=$(kubectl get job "${JOB_NAME}" -n "${NAMESPACE}" -o jsonpath='{.status.conditions[?(@.type=="Failed")].status}' 2>/dev/null || true)
+          if [ "$COMPLETE" = "True" ]; then
+            echo "Migration Job completed successfully."
+            break
+          fi
+          if [ "$FAILED" = "True" ] || [ "$SECONDS" -ge "$DEADLINE" ]; then
+            echo "Migration Job failed or timed out. Fetching logs:"
             POD=$(kubectl get pods -n "${NAMESPACE}" -l job-name="${JOB_NAME}" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
             [ -n "$POD" ] && kubectl logs -n "${NAMESPACE}" "$POD" || true
             exit 1
-          }
-
-        echo "Migration Job completed successfully."
+          fi
+          sleep 5
+        done
 
     - name: Helm upgrade / install
       shell: bash
@@ -250,6 +271,7 @@ runs:
         HELM_TIMEOUT: ${{ inputs.helm_timeout }}
         EXTRA_SET_VALUES: ${{ inputs.extra_set_values }}
         EXTRA_ARGS: ${{ inputs.extra_args }}
+        ATOMIC: ${{ inputs.atomic }}
         BRANCH: ${{ steps.branch.outputs.name }}
       run: |
         echo "::group::☸️ [CHECKPOINT 2/2] Helm Upgrade / Install"
@@ -263,31 +285,40 @@ runs:
           CHART_REF="$CHART"
         fi
 
-        # BASE_SET_ARGS now only covers mandatory intrinsic values; all other chart settings must be passed via
-        # extra_set_values (one per line) or extra_args. See modified input description for guidance.
+        # Intrinsic values the action always sets; all other chart settings come from
+        # extra_set_values (one "key=value" per line) or extra_args.
         BASE_SET_ARGS=(
           --set app.name="${APP_NAME}"
           --set app.version="github-${BRANCH}-${GITHUB_RUN_NUMBER}"
         )
 
-        EXTRA_SET_ARGS=""
+        # Build "--set key=value" pairs as an array so values containing spaces stay intact
+        # (an unquoted string would word-split them).
+        EXTRA_SET_ARGS=()
         if [ -n "${EXTRA_SET_VALUES// /}" ]; then
           while IFS= read -r line; do
-            [ -z "${line}" ] && continue
             KV=$(echo "$line" | sed 's/^ *//;s/ *$//')
             [ -z "$KV" ] && continue
-            EXTRA_SET_ARGS+=" --set $KV"
+            EXTRA_SET_ARGS+=( --set "$KV" )
           done <<< "$EXTRA_SET_VALUES"
         fi
 
+        # extra_args is raw CLI text (e.g. "--debug --wait"); split into argv tokens.
+        read -ra EXTRA_ARGS_ARR <<< "${EXTRA_ARGS:-}"
+
+        ATOMIC_ARGS=()
+        if [ "${ATOMIC}" = "true" ]; then
+          ATOMIC_ARGS+=( --atomic )
+        fi
+
         echo "Executing Helm upgrade with base settings plus extras..."
         helm upgrade "${APP_NAME}" "$CHART_REF" \
           --install \
           --namespace "${NAMESPACE}" \
           --create-namespace \
-          "${BASE_SET_ARGS[@]}" ${EXTRA_SET_ARGS} \
+          "${BASE_SET_ARGS[@]}" "${EXTRA_SET_ARGS[@]}" \
           --timeout "${HELM_TIMEOUT}" \
-          ${EXTRA_ARGS}
+          "${ATOMIC_ARGS[@]}" "${EXTRA_ARGS_ARR[@]}"
         echo "HELM_GENERIC_DEPLOY_STATUS=✅ PASSED" >> "$GITHUB_ENV"
         echo "::notice title=✅ [HELM] Helm deploy complete::App: ${APP_NAME}, namespace: ${NAMESPACE}"
         echo "::endgroup::"
@@ -302,13 +333,23 @@ runs:
         set -euo pipefail
         helm status "$APP_NAME" -n "$NAMESPACE" || true
         echo "---"
-        kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/instance="$APP_NAME" || true
+        # Charts in use label pods differently (app.kubernetes.io/instance=<release> on the gateway
+        # chart, app=<app.name> on the ingress chart); show whichever selector matches.
+        SELECTOR="app.kubernetes.io/instance=$APP_NAME"
+        if [ -z "$(kubectl get pods -n "$NAMESPACE" -l "$SELECTOR" --no-headers 2>/dev/null)" ]; then
+          SELECTOR="app=$APP_NAME"
+        fi
+        echo "Pods (selector: ${SELECTOR}):"
+        kubectl get pods -n "$NAMESPACE" -l "$SELECTOR" || true
 
     - name: Report failure
       if: failure()
       shell: bash
+      env:
+        APP_NAME: ${{ inputs.app_name }}
+        NAMESPACE: ${{ inputs.namespace }}
       run: |
-        echo "::error title=❌ [HELM] Deploy failed::App '${{ inputs.app_name }}' in namespace '${{ inputs.namespace }}'. Checkpoints — 1) Kubeconfig: ${HELM_GENERIC_KUBECONFIG_STATUS:-⏭️ Not reached} | 2) Helm deploy: ${HELM_GENERIC_DEPLOY_STATUS:-⏭️ Not reached}."
+        echo "::error title=❌ [HELM] Deploy failed::App '${APP_NAME}' in namespace '${NAMESPACE}'. Checkpoints — 1) Kubeconfig: ${HELM_GENERIC_KUBECONFIG_STATUS:-⏭️ Not reached} | 2) Helm deploy: ${HELM_GENERIC_DEPLOY_STATUS:-⏭️ Not reached}."
 
     - name: Clean up kubeconfig
       if: always()
diff --git a/.github/workflows/generic-gateway-helm-template.yml b/.github/workflows/generic-gateway-helm-template.yml
index 562c656..1657b66 100644
--- a/.github/workflows/generic-gateway-helm-template.yml
+++ b/.github/workflows/generic-gateway-helm-template.yml
@@ -717,6 +717,9 @@ jobs:
           fi
 
           KCFG_PATH="$RUNNER_TEMP/kubeconfig"
+          # Remove the decoded kubeconfig when this step exits so the credential does not linger
+          # (matters on self-hosted/reused runners).
+          trap 'rm -f "$KCFG_PATH"' EXIT
           if echo "$KUBECONFIG_DATA" | grep -q 'apiVersion:'; then
             printf "%s" "$KUBECONFIG_DATA" > "$KCFG_PATH"
           else

From 7bf16b2299b76e02a6c4abd91f10a81b61748bf6 Mon Sep 17 00:00:00 2001
From: Musa Misto <64855513+MusaMisto@users.noreply.github.com>
Date: Sun, 14 Jun 2026 16:14:51 +0300
Subject: [PATCH 5/9] fix(helm-generic): guard empty-array expansion; surface
 atomic as workflow input

F1 - Guard the helm-upgrade array expansions with ${arr[@]+"${arr[@]}"} so an empty
     EXTRA_SET_ARGS / ATOMIC_ARGS / EXTRA_ARGS_ARR no longer triggers "unbound variable"
     under `set -u` on bash < 4.4 (a latent regression from the earlier array refactor).
     Verified on bash 3.2 for both empty and populated arrays; no effect on the two
     consumers (they always pass non-empty arrays on ubuntu-latest / bash 5.2).

F2 - Add an `atomic` input (default 'true') to both reusable workflows and pass it
     through to the action, so workflow callers can opt out of --atomic auto-rollback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .github/actions/helm-generic/action.yml             | 6 ++++--
 .github/workflows/generic-chart-helm.yml            | 6 ++++++
 .github/workflows/generic-gateway-helm-template.yml | 6 ++++++
 3 files changed, 16 insertions(+), 2 deletions(-)

diff --git a/.github/actions/helm-generic/action.yml b/.github/actions/helm-generic/action.yml
index 526649f..13d2a06 100644
--- a/.github/actions/helm-generic/action.yml
+++ b/.github/actions/helm-generic/action.yml
@@ -312,13 +312,15 @@ runs:
         fi
 
         echo "Executing Helm upgrade with base settings plus extras..."
+        # The ${arr[@]+"${arr[@]}"} guard expands to nothing when the array is empty, avoiding
+        # an "unbound variable" error under `set -u` on bash < 4.4 (BASE_SET_ARGS is always set).
         helm upgrade "${APP_NAME}" "$CHART_REF" \
           --install \
           --namespace "${NAMESPACE}" \
           --create-namespace \
-          "${BASE_SET_ARGS[@]}" "${EXTRA_SET_ARGS[@]}" \
+          "${BASE_SET_ARGS[@]}" ${EXTRA_SET_ARGS[@]+"${EXTRA_SET_ARGS[@]}"} \
           --timeout "${HELM_TIMEOUT}" \
-          "${ATOMIC_ARGS[@]}" "${EXTRA_ARGS_ARR[@]}"
+          ${ATOMIC_ARGS[@]+"${ATOMIC_ARGS[@]}"} ${EXTRA_ARGS_ARR[@]+"${EXTRA_ARGS_ARR[@]}"}
         echo "HELM_GENERIC_DEPLOY_STATUS=✅ PASSED" >> "$GITHUB_ENV"
         echo "::notice title=✅ [HELM] Helm deploy complete::App: ${APP_NAME}, namespace: ${NAMESPACE}"
         echo "::endgroup::"
diff --git a/.github/workflows/generic-chart-helm.yml b/.github/workflows/generic-chart-helm.yml
index efafb52..568eac7 100644
--- a/.github/workflows/generic-chart-helm.yml
+++ b/.github/workflows/generic-chart-helm.yml
@@ -85,6 +85,11 @@ on:
         required: false
         default: ''
         type: string
+      atomic:
+        description: "If 'true', pass --atomic to helm upgrade so a failed deploy rolls back automatically (implies --wait)."
+        required: false
+        default: 'true'
+        type: string
       helm-set-values:
         description: 'Development Helm set values (comma-separated: key1=value1,key2=value2)'
         required: false
@@ -494,6 +499,7 @@ jobs:
           # Helm timing/flags
           helm_timeout: ${{ inputs.helm-timeout-minutes != '' && format('{0}m', inputs.helm-timeout-minutes) || '10m' }}
           extra_args: "--wait"
+          atomic: ${{ inputs.atomic }}
 
           # Translate your previous comma-joined --set into line-based entries
           extra_set_values: |
diff --git a/.github/workflows/generic-gateway-helm-template.yml b/.github/workflows/generic-gateway-helm-template.yml
index 1657b66..8de69ae 100644
--- a/.github/workflows/generic-gateway-helm-template.yml
+++ b/.github/workflows/generic-gateway-helm-template.yml
@@ -115,6 +115,11 @@ on:
         required: false
         default: ''
         type: string
+      atomic:
+        description: "If 'true', pass --atomic to helm upgrade so a failed deploy rolls back automatically (implies --wait)."
+        required: false
+        default: 'true'
+        type: string
       helm-set-values:
         description: Additional Helm set values line or comma separated
         required: false
@@ -891,6 +896,7 @@ jobs:
           chart_repo: ${{ inputs.chart-repo }}
           helm_timeout: ${{ inputs.helm-timeout-minutes != '' && format('{0}m', inputs.helm-timeout-minutes) || '10m' }}
           extra_args: --wait
+          atomic: ${{ inputs.atomic }}
           extra_set_values: |
             image.repo=${{ env.CONTAINER_REGISTRY }}
             app.version=${{ needs.version.outputs.version }}

From ce072bdde04dee1ac482f59c513d74159b027007 Mon Sep 17 00:00:00 2001
From: Musa Misto <64855513+MusaMisto@users.noreply.github.com>
Date: Sun, 14 Jun 2026 16:20:16 +0300
Subject: [PATCH 6/9] fix(helm-generic): force-update helm repo alias; surface
 add failures

N1 - `helm repo add s9generic` previously errored when the alias already existed with
     a different URL (reused/self-hosted runner); the `|| true` swallowed it and the
     deploy then pulled from the stale URL. Use --force-update so the alias is repointed
     at the current chart_repo. Also drop `2>/dev/null || true` so a genuine repo-add
     failure (e.g. unreachable repo) fails fast with a clear error instead of proceeding
     to a doomed chart ref, and scope `helm repo update` to the added repo.

Verified with helm: same-alias/new-URL now repoints correctly; scoped update works on
Helm 4. No effect on ephemeral GitHub-hosted runners.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .github/actions/helm-generic/action.yml | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/.github/actions/helm-generic/action.yml b/.github/actions/helm-generic/action.yml
index 13d2a06..6bb73a5 100644
--- a/.github/actions/helm-generic/action.yml
+++ b/.github/actions/helm-generic/action.yml
@@ -278,8 +278,10 @@ runs:
         set -euo pipefail
 
         if [ -n "$CHART_REPO" ]; then
-          helm repo add s9generic "$CHART_REPO" 2>/dev/null || true
-          helm repo update >/dev/null
+          # --force-update so a stale 's9generic' alias from a prior run (reused/self-hosted runner)
+          # is repointed at the current chart_repo instead of silently keeping the old URL.
+          helm repo add --force-update s9generic "$CHART_REPO" >/dev/null
+          helm repo update s9generic >/dev/null
           CHART_REF="s9generic/$CHART"
         else
           CHART_REF="$CHART"

From 3c51192025d9b1d56b3dae6f2976f103b7d52b71 Mon Sep 17 00:00:00 2001
From: Musa Misto <64855513+MusaMisto@users.noreply.github.com>
Date: Sun, 14 Jun 2026 16:28:46 +0300
Subject: [PATCH 7/9] style(sw-cicd): group consecutive GITHUB_ENV writes
 (shellcheck SC2129)

Combine the three CHECKPOINT_*_STATUS echoes in the ci job's announce step into a
single `{ ... } >> "$GITHUB_ENV"` block. Behavior identical; clears the only actionlint
finding in this workflow. Independent of the helm-generic changes (sw-cicd uses
helm-deploy, not helm-generic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .github/workflows/sw-cicd.yml | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/sw-cicd.yml b/.github/workflows/sw-cicd.yml
index c32d6a7..9baeb79 100644
--- a/.github/workflows/sw-cicd.yml
+++ b/.github/workflows/sw-cicd.yml
@@ -272,9 +272,11 @@ jobs:
       - name: Announce ci job
         run: |
           echo "::notice title=🐳 [DOCKER] CI Build — Job Started::Version: ${{ needs.version.outputs.version }} | Registry: ${{ inputs.container-registry }} | Triggered by: ${{ github.actor }}"
-          echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV"
-          echo "CHECKPOINT_2_STATUS=⏳ Pending" >> "$GITHUB_ENV"
-          echo "CHECKPOINT_3_STATUS=⏳ Pending" >> "$GITHUB_ENV"
+          {
+            echo "CHECKPOINT_1_STATUS=⏳ Pending"
+            echo "CHECKPOINT_2_STATUS=⏳ Pending"
+            echo "CHECKPOINT_3_STATUS=⏳ Pending"
+          } >> "$GITHUB_ENV"
 
       - name: Checkout code
         uses: actions/checkout@v6

From 3492690fd6bc42dc86bfdb13f7d1aca931b378d8 Mon Sep 17 00:00:00 2001
From: Musa Misto <64855513+MusaMisto@users.noreply.github.com>
Date: Sun, 14 Jun 2026 16:37:27 +0300
Subject: [PATCH 8/9] kubectl version upgrade

---
 .github/actions/helm-generic/action.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/actions/helm-generic/action.yml b/.github/actions/helm-generic/action.yml
index 6bb73a5..92164b8 100644
--- a/.github/actions/helm-generic/action.yml
+++ b/.github/actions/helm-generic/action.yml
@@ -176,7 +176,7 @@ runs:
         # Pinned for reproducibility (avoids a new kubectl silently changing CI behavior).
         # Only basic core-resource ops are used here (apply/wait/get/create), so exact skew
         # with the cluster is not critical; bump this to track your cluster's minor version.
-        version: 'v1.34.0'
+        version: 'v1.36.0'
 
     - name: Run DB migration Job (init container equivalent)
       if: ${{ inputs.init_job_image != '' }}

From 0026df7510056ff2b55404a3a30e41b98378ff31 Mon Sep 17 00:00:00 2001
From: Musa Misto <64855513+MusaMisto@users.noreply.github.com>
Date: Sun, 14 Jun 2026 16:37:47 +0300
Subject: [PATCH 9/9] Kubectl version in between

---
 .github/actions/helm-generic/action.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/actions/helm-generic/action.yml b/.github/actions/helm-generic/action.yml
index 92164b8..31a70a4 100644
--- a/.github/actions/helm-generic/action.yml
+++ b/.github/actions/helm-generic/action.yml
@@ -176,7 +176,7 @@ runs:
         # Pinned for reproducibility (avoids a new kubectl silently changing CI behavior).
         # Only basic core-resource ops are used here (apply/wait/get/create), so exact skew
         # with the cluster is not critical; bump this to track your cluster's minor version.
-        version: 'v1.36.0'
+        version: 'v1.35.0'
 
     - name: Run DB migration Job (init container equivalent)
       if: ${{ inputs.init_job_image != '' }}
