This document describes the local development workflow used to test the full release pipeline end-to-end before pushing a real Git tag to GitHub.
The goal is to:
- reuse the same GitHub Actions release workflow used in production
- build container images locally with act
- load them into a local kind cluster
- deploy the service using Helm templating only (no Helm state, no releases)
This step creates a local Kubernetes cluster using kind.
Skip this section if you already have a kind cluster up and running.
The cluster exposes a few container ports to the host to make local development and testing easier.
kind get kubeconfig >/dev/null 2>&1 || \
cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30081
hostPort: 30081
listenAddress: "127.0.0.1"
protocol: TCP
- containerPort: 30082
hostPort: 30082
listenAddress: "127.0.0.1"
protocol: TCP
- containerPort: 30084
hostPort: 30084
listenAddress: "127.0.0.1"
protocol: TCP
- containerPort: 5432
hostPort: 30083
listenAddress: "127.0.0.1"
protocol: TCP
EOF## Running the GitHub release pipeline locally
Before running the release workflow, we compute a few environment variables and persist them into a .env.dev file.
This file acts as a single source of truth for:
- the image tag
- the image repository
- the kind cluster name
It can be reused by subsequent steps and other scripts.
TAG="${1:-0.0.8}"
CLUSTER_NAME="kind"
NAMESPACE="eventstack-system"
REPO="ghcr.io/$(git config --get remote.origin.url | sed -E 's#.*[:/](.*)/(.*)\.git#\1/\2#')"
IMAGE="${REPO}:${TAG}"
cat > .env.dev <<EOF
# Generated by local release test script
# $(date -u)
TAG=${TAG}
CLUSTER_NAME=${CLUSTER_NAME}
NAMESPACE=${NAMESPACE}
REPO=${REPO}
IMAGE=${IMAGE}
EOFBuild the release image via act (no push)
In this step we use act to execute the same GitHub Actions workflow used in production to build the release image.
Key points:
- the workflow is triggered as if a Git tag was pushed
- the image is built locally
- the image is not pushed to GHCR
This allows us to validate the release pipeline without touching any remote registry.
export $(grep -v '^#' .env | xargs)
act push \
-W .github/workflows/release-image.yml \
--env GITHUB_REF=refs/tags/${TAG} \
--env DOCKER_PUSH=false \
--env LOCAL_KIND=trueImages built by act exist only in the local Docker daemon.
Since kind runs Kubernetes nodes inside Docker containers, the images must be explicitly loaded into the kind cluster so that Pods can start without pulling from a remote registry.
Both the versioned tag and the latest tag are loaded to match different deployment configurations.
export $(grep -v '^#' .env | xargs)
kind load docker-image "${IMAGE}" "${REPO}:latest" --name "${CLUSTER_NAME}"This chart expects an existing Kubernetes Secret containing the database credentials.
The secret must exist before installing the chart.
Note: In production, you should avoid using inline secrets directly in commands.
Instead, consider using environment files, external secret management, or Helm overrides.
export $(grep -v '^#' .env | xargs)
# Ensure namespace exists
kubectl get namespace ${NAMESPACE} >/dev/null 2>&1 || kubectl create namespace ${NAMESPACE}
# Create or update DB secret
kubectl create secret generic eventrouter-db \
--from-literal=DB_USER="test" --from-literal=DB_PASS="test" \
--namespace ${NAMESPACE} \
--dry-run=client -o yaml | kubectl apply -f -eventrouter-db→ must match.Values.secret.namein the Helm chart--namespace demo-system→ must match.Values.namespace
- Store credentials in a
.envfile: - Create the secret from the file:
export $(grep -v '^#' .env | xargs)
kubectl create secret generic eventrouter-db \
--from-env-file=dev.env \
--namespace ${NAMESPACE}This avoids exposing secrets directly on the command line.
The service is deployed using the same Helm chart used in production, but only via templating.
Helm is used exclusively to:
- render Kubernetes manifests
- inject environment-specific values
No helm install or helm upgrade is performed.
All resources are applied using kubectl apply, keeping full control over the manifests and avoiding Helm state in the cluster.
export $(grep -v '^#' .env | xargs)
helm template deviser ./chart \
-n ${NAMESPACE} \
--set image.repository=${REPO} \
--set image.tag=${TAG} \
--set image.pullPolicy=Never \
-f "./chart/values.dev.yaml" | kubectl apply -f -