From 71ccd028ddc7727561a4c20ced08fd24efa8f180 Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Thu, 5 Feb 2026 08:04:10 +0000 Subject: [PATCH 01/14] feat(ephemeral-clusters): bootstrap --- .github/workflows/ephemeral.yml | 348 ++++++++++++++++++ .../kustomize/ephemeral-base/deployment.yaml | 38 ++ .../kustomize/ephemeral-base/ingress.yaml | 25 ++ .../ephemeral-base/kustomization.yaml | 9 + .../kustomize/ephemeral-base/service.yaml | 13 + .../overlays/ephemeral/kustomization.yaml | 26 ++ .../kustomize/ephemeral-base/deployment.yaml | 45 +++ .../kustomize/ephemeral-base/ingress.yaml | 25 ++ .../ephemeral-base/kustomization.yaml | 9 + .../kustomize/ephemeral-base/service.yaml | 13 + .../overlays/ephemeral/kustomization.yaml | 26 ++ .../kustomize/ephemeral-base/deployment.yaml | 50 +++ .../kustomize/ephemeral-base/ingress.yaml | 25 ++ .../ephemeral-base/kustomization.yaml | 9 + .../kustomize/ephemeral-base/service.yaml | 13 + .../overlays/ephemeral/kustomization.yaml | 26 ++ .../kustomize/ephemeral-base/deployment.yaml | 66 ++++ .../kustomize/ephemeral-base/ingress.yaml | 25 ++ .../ephemeral-base/kustomization.yaml | 9 + .../kustomize/ephemeral-base/service.yaml | 13 + .../overlays/ephemeral/kustomization.yaml | 26 ++ 21 files changed, 839 insertions(+) create mode 100644 .github/workflows/ephemeral.yml create mode 100644 apps/ascii-frontend/kustomize/ephemeral-base/deployment.yaml create mode 100644 apps/ascii-frontend/kustomize/ephemeral-base/ingress.yaml create mode 100644 apps/ascii-frontend/kustomize/ephemeral-base/kustomization.yaml create mode 100644 apps/ascii-frontend/kustomize/ephemeral-base/service.yaml create mode 100644 apps/ascii-frontend/kustomize/overlays/ephemeral/kustomization.yaml create mode 100644 apps/enqueuer/kustomize/ephemeral-base/deployment.yaml create mode 100644 apps/enqueuer/kustomize/ephemeral-base/ingress.yaml create mode 100644 apps/enqueuer/kustomize/ephemeral-base/kustomization.yaml create mode 100644 apps/enqueuer/kustomize/ephemeral-base/service.yaml create mode 100644 apps/enqueuer/kustomize/overlays/ephemeral/kustomization.yaml create mode 100644 apps/memorizer/kustomize/ephemeral-base/deployment.yaml create mode 100644 apps/memorizer/kustomize/ephemeral-base/ingress.yaml create mode 100644 apps/memorizer/kustomize/ephemeral-base/kustomization.yaml create mode 100644 apps/memorizer/kustomize/ephemeral-base/service.yaml create mode 100644 apps/memorizer/kustomize/overlays/ephemeral/kustomization.yaml create mode 100644 apps/writer/kustomize/ephemeral-base/deployment.yaml create mode 100644 apps/writer/kustomize/ephemeral-base/ingress.yaml create mode 100644 apps/writer/kustomize/ephemeral-base/kustomization.yaml create mode 100644 apps/writer/kustomize/ephemeral-base/service.yaml create mode 100644 apps/writer/kustomize/overlays/ephemeral/kustomization.yaml diff --git a/.github/workflows/ephemeral.yml b/.github/workflows/ephemeral.yml new file mode 100644 index 0000000..d4c0d5f --- /dev/null +++ b/.github/workflows/ephemeral.yml @@ -0,0 +1,348 @@ +name: PR Ephemeral Environment + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +env: + REGISTRY: registry.fullstack.pw + VAULT_ADDR: https://vault.fullstack.pw + +jobs: + manage-environment: + runs-on: self-hosted + outputs: + cluster_name: ${{ steps.cluster-info.outputs.cluster_name }} + writer_dns: ${{ steps.cluster-info.outputs.writer_dns }} + enqueuer_dns: ${{ steps.cluster-info.outputs.enqueuer_dns }} + memorizer_dns: ${{ steps.cluster-info.outputs.memorizer_dns }} + frontend_dns: ${{ steps.cluster-info.outputs.frontend_dns }} + deployment_status: ${{ steps.deployment-status.outputs.status }} + steps: + - name: Set cluster info + id: cluster-info + run: | + PR_NUM="${{ github.event.pull_request.number }}" + CLUSTER_NAME="pr-demo-apps-${PR_NUM}" + WRITER_DNS="pr-${PR_NUM}-writer.ephemeral.fullstack.pw" + ENQUEUER_DNS="pr-${PR_NUM}-enqueuer.ephemeral.fullstack.pw" + MEMORIZER_DNS="pr-${PR_NUM}-memorizer.ephemeral.fullstack.pw" + FRONTEND_DNS="pr-${PR_NUM}-ascii-frontend.ephemeral.fullstack.pw" + + echo "cluster_name=$CLUSTER_NAME" >> $GITHUB_OUTPUT + echo "writer_dns=$WRITER_DNS" >> $GITHUB_OUTPUT + echo "enqueuer_dns=$ENQUEUER_DNS" >> $GITHUB_OUTPUT + echo "memorizer_dns=$MEMORIZER_DNS" >> $GITHUB_OUTPUT + echo "frontend_dns=$FRONTEND_DNS" >> $GITHUB_OUTPUT + + - name: Checkout infra repo + uses: actions/checkout@v4 + with: + repository: fullstack-pw/infra + token: ${{ secrets.RUNNER }} + path: infra + + - name: Check if cluster exists + id: check-cluster + run: | + CLUSTER_NAME="${{ steps.cluster-info.outputs.cluster_name }}" + if kubectl get cluster "$CLUSTER_NAME" -n "$CLUSTER_NAME" --context tools --kubeconfig /home/runner/.kube/config &>/dev/null; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Cluster $CLUSTER_NAME already exists" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "Cluster $CLUSTER_NAME does not exist" + fi + + # ==================== CREATE/UPDATE ENVIRONMENT ==================== + - name: Check IP pool capacity + if: github.event.action != 'closed' && steps.check-cluster.outputs.exists == 'false' + run: | + cd infra + AVAILABLE=$(./clusters/scripts/ip_pool_manager.sh check-capacity) + CAPACITY_RESULT=$? + echo "Capacity available: $AVAILABLE slots free" + if [ "$CAPACITY_RESULT" -ne 0 ]; then + echo "::error::IP pool exhausted. All 5 ephemeral cluster slots are in use (10 IPs / 2 per cluster). Please close old PRs." + exit 1 + fi + + - name: Allocate IP from pool + if: github.event.action != 'closed' && steps.check-cluster.outputs.exists == 'false' + id: allocate + run: | + cd infra + CLUSTER_NAME="${{ steps.cluster-info.outputs.cluster_name }}" + CLUSTER_IP=$(./clusters/scripts/ip_pool_manager.sh allocate "$CLUSTER_NAME") + + # Calculate node IP (VIP + 1) - each cluster needs 2 IPs + IP_LAST_OCTET=$(echo "$CLUSTER_IP" | cut -d'.' -f4) + NODE_IP_LAST_OCTET=$((IP_LAST_OCTET + 1)) + NODE_IP="192.168.1.${NODE_IP_LAST_OCTET}" + + echo "ip=$CLUSTER_IP" >> $GITHUB_OUTPUT + echo "node_ip=$NODE_IP" >> $GITHUB_OUTPUT + echo "Allocated control plane VIP: $CLUSTER_IP" + echo "Node IP: $NODE_IP" + + - name: Render Cluster API manifest + if: github.event.action != 'closed' && steps.check-cluster.outputs.exists == 'false' + run: | + cd infra + export CLUSTER_NAME="${{ steps.cluster-info.outputs.cluster_name }}" + export CLUSTER_IP="${{ steps.allocate.outputs.ip }}" + export NODE_IP="${{ steps.allocate.outputs.node_ip }}" + export PR_NUMBER="${{ github.event.pull_request.number }}" + export REPOSITORY="demo-apps" + envsubst < ephemeral-clusters/cluster-api/k3s-cluster.yaml.tpl > /tmp/cluster.yaml + cat /tmp/cluster.yaml + + - name: Create cluster via Cluster API + if: github.event.action != 'closed' && steps.check-cluster.outputs.exists == 'false' + run: | + kubectl apply -f /tmp/cluster.yaml --context tools --kubeconfig /home/runner/.kube/config + + - name: Copy Proxmox credentials to namespace + if: github.event.action != 'closed' && steps.check-cluster.outputs.exists == 'false' + run: | + CLUSTER_NAME="${{ steps.cluster-info.outputs.cluster_name }}" + kubectl --context tools --kubeconfig /home/runner/.kube/config \ + get secret proxmox-credentials -n k3s-test -o json | \ + jq 'del(.metadata.namespace,.metadata.uid,.metadata.resourceVersion,.metadata.creationTimestamp,.metadata.ownerReferences,.metadata.finalizers)' | \ + kubectl --context tools --kubeconfig /home/runner/.kube/config apply -n "$CLUSTER_NAME" -f - || \ + echo "Warning: Could not copy proxmox-credentials (may already exist)" + + - name: Wait for cluster available + if: github.event.action != 'closed' && steps.check-cluster.outputs.exists == 'false' + run: | + CLUSTER_NAME="${{ steps.cluster-info.outputs.cluster_name }}" + echo "Waiting for cluster $CLUSTER_NAME to be available..." + kubectl wait --for=condition=Available \ + cluster/$CLUSTER_NAME \ + -n $CLUSTER_NAME \ + --timeout=5m --context tools --kubeconfig /home/runner/.kube/config + + - name: Extract kubeconfig + if: github.event.action != 'closed' && steps.check-cluster.outputs.exists == 'false' + run: | + CLUSTER_NAME="${{ steps.cluster-info.outputs.cluster_name }}" + kubectl get secret ${CLUSTER_NAME}-kubeconfig \ + -n $CLUSTER_NAME \ + -o jsonpath='{.data.value}' --context tools --kubeconfig /home/runner/.kube/config | base64 -d > /tmp/kubeconfig + + # Rename context to match workspace name for OpenTofu + ORIGINAL_CONTEXT=$(kubectl --kubeconfig /tmp/kubeconfig config current-context) + kubectl --kubeconfig /tmp/kubeconfig config rename-context "$ORIGINAL_CONTEXT" "$CLUSTER_NAME" + kubectl --kubeconfig /tmp/kubeconfig config use-context "$CLUSTER_NAME" + + - name: Apply ephemeral infrastructure with OpenTofu + if: github.event.action != 'closed' && steps.check-cluster.outputs.exists == 'false' + env: + KUBECONFIG: /tmp/kubeconfig + run: | + cd infra + make ephemeral-init + make ephemeral-apply WORKSPACE=${{ steps.cluster-info.outputs.cluster_name }} + + # ==================== BUILD AND DEPLOY (PR opened/updated) ==================== + - name: Checkout app code + if: github.event.action != 'closed' + uses: actions/checkout@v4 + + - name: Build Docker images + if: github.event.action != 'closed' + run: | + PR_TAG="pr-${{ github.event.pull_request.number }}" + + # Build all 4 images + docker build -t ${{ env.REGISTRY }}/library/writer:${PR_TAG} -f apps/writer/Dockerfile apps/writer & + docker build -t ${{ env.REGISTRY }}/library/enqueuer:${PR_TAG} -f apps/enqueuer/Dockerfile apps/enqueuer & + docker build -t ${{ env.REGISTRY }}/library/memorizer:${PR_TAG} -f apps/memorizer/Dockerfile apps/memorizer & + docker build -t ${{ env.REGISTRY }}/library/ascii-frontend:${PR_TAG} -f apps/ascii-frontend/Dockerfile apps/ascii-frontend & + + # Wait for all builds to complete + wait + echo "All images built successfully" + + - name: Push Docker images + if: github.event.action != 'closed' + run: | + PR_TAG="pr-${{ github.event.pull_request.number }}" + + # Login to Harbor + echo "${HARBOR_KEY}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin + + # Push all 4 images + docker push ${{ env.REGISTRY }}/library/writer:${PR_TAG} + docker push ${{ env.REGISTRY }}/library/enqueuer:${PR_TAG} + docker push ${{ env.REGISTRY }}/library/memorizer:${PR_TAG} + docker push ${{ env.REGISTRY }}/library/ascii-frontend:${PR_TAG} + + - name: Extract kubeconfig for deployment + if: github.event.action != 'closed' + run: | + CLUSTER_NAME="${{ steps.cluster-info.outputs.cluster_name }}" + kubectl get secret ${CLUSTER_NAME}-kubeconfig \ + -n $CLUSTER_NAME \ + -o jsonpath='{.data.value}' --context tools --kubeconfig /home/runner/.kube/config | base64 -d > /tmp/kubeconfig + + # Rename context to match workspace name for OpenTofu + ORIGINAL_CONTEXT=$(kubectl --kubeconfig /tmp/kubeconfig config current-context) + kubectl --kubeconfig /tmp/kubeconfig config rename-context "$ORIGINAL_CONTEXT" "$CLUSTER_NAME" + kubectl --kubeconfig /tmp/kubeconfig config use-context "$CLUSTER_NAME" + + - name: Deploy writer to ephemeral cluster + if: github.event.action != 'closed' + env: + KUBECONFIG: /tmp/kubeconfig + run: | + kubectl kustomize apps/writer/kustomize/overlays/ephemeral \ + | sed "s|${{ env.REGISTRY }}/library/writer:pr-will-be-replaced|${{ env.REGISTRY }}/library/writer:pr-${{ github.event.pull_request.number }}|" \ + | sed "s|dev.writer.fullstack.pw|${{ steps.cluster-info.outputs.writer_dns }}|g" \ + | kubectl apply -f - + + kubectl rollout status deployment/writer -n default --timeout=5m + + - name: Deploy enqueuer to ephemeral cluster + if: github.event.action != 'closed' + env: + KUBECONFIG: /tmp/kubeconfig + run: | + kubectl kustomize apps/enqueuer/kustomize/overlays/ephemeral \ + | sed "s|${{ env.REGISTRY }}/library/enqueuer:pr-will-be-replaced|${{ env.REGISTRY }}/library/enqueuer:pr-${{ github.event.pull_request.number }}|" \ + | sed "s|dev.enqueuer.fullstack.pw|${{ steps.cluster-info.outputs.enqueuer_dns }}|g" \ + | kubectl apply -f - + + kubectl rollout status deployment/enqueuer -n default --timeout=5m + + - name: Deploy memorizer to ephemeral cluster + if: github.event.action != 'closed' + env: + KUBECONFIG: /tmp/kubeconfig + run: | + kubectl kustomize apps/memorizer/kustomize/overlays/ephemeral \ + | sed "s|${{ env.REGISTRY }}/library/memorizer:pr-will-be-replaced|${{ env.REGISTRY }}/library/memorizer:pr-${{ github.event.pull_request.number }}|" \ + | sed "s|dev.memorizer.fullstack.pw|${{ steps.cluster-info.outputs.memorizer_dns }}|g" \ + | kubectl apply -f - + + kubectl rollout status deployment/memorizer -n default --timeout=5m + + - name: Deploy ascii-frontend to ephemeral cluster + if: github.event.action != 'closed' + env: + KUBECONFIG: /tmp/kubeconfig + run: | + kubectl kustomize apps/ascii-frontend/kustomize/overlays/ephemeral \ + | sed "s|${{ env.REGISTRY }}/library/ascii-frontend:pr-will-be-replaced|${{ env.REGISTRY }}/library/ascii-frontend:pr-${{ github.event.pull_request.number }}|" \ + | sed "s|dev.ascii-frontend.fullstack.pw|${{ steps.cluster-info.outputs.frontend_dns }}|g" \ + | kubectl apply -f - + + kubectl rollout status deployment/ascii-frontend -n default --timeout=5m + + - name: Set deployment status + id: deployment-status + if: github.event.action != 'closed' + run: echo "status=success" >> $GITHUB_OUTPUT + + - name: Post deployment info + if: github.event.action != 'closed' && steps.check-cluster.outputs.exists == 'false' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const clusterName = '${{ steps.cluster-info.outputs.cluster_name }}'; + const writerDns = '${{ steps.cluster-info.outputs.writer_dns }}'; + const enqueuerDns = '${{ steps.cluster-info.outputs.enqueuer_dns }}'; + const memorizerDns = '${{ steps.cluster-info.outputs.memorizer_dns }}'; + const frontendDns = '${{ steps.cluster-info.outputs.frontend_dns }}'; + const prTag = 'pr-${{ github.event.pull_request.number }}'; + + const body = `## Ephemeral Environment Ready + + Your ephemeral environment has been deployed with all 4 demo apps! + + ### Application URLs + - **Writer**: https://${writerDns} + - **Enqueuer**: https://${enqueuerDns} + - **Memorizer**: https://${memorizerDns} + - **ASCII Frontend**: https://${frontendDns} + + ### Details + **Cluster**: \`${clusterName}\` + **Image Tag**: \`${prTag}\` + + The environment will be automatically destroyed when this PR is closed. + `; + + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + + # ==================== CLEANUP ==================== + - name: Get kubeconfig for destruction + if: github.event.action == 'closed' || (failure() && steps.check-cluster.outputs.exists == 'false') + run: | + CLUSTER_NAME="${{ steps.cluster-info.outputs.cluster_name }}" + kubectl get secret ${CLUSTER_NAME}-kubeconfig \ + -n $CLUSTER_NAME \ + -o jsonpath='{.data.value}' --context tools --kubeconfig /home/runner/.kube/config | base64 -d > /tmp/kubeconfig || true + + # Rename context to match workspace name for OpenTofu + if [ -f /tmp/kubeconfig ]; then + ORIGINAL_CONTEXT=$(kubectl --kubeconfig /tmp/kubeconfig config current-context 2>/dev/null || echo "") + if [ -n "$ORIGINAL_CONTEXT" ]; then + kubectl --kubeconfig /tmp/kubeconfig config rename-context "$ORIGINAL_CONTEXT" "$CLUSTER_NAME" 2>/dev/null || true + kubectl --kubeconfig /tmp/kubeconfig config use-context "$CLUSTER_NAME" 2>/dev/null || true + fi + fi + + - name: Destroy ephemeral infrastructure + if: github.event.action == 'closed' || (failure() && steps.check-cluster.outputs.exists == 'false') + env: + KUBECONFIG: /tmp/kubeconfig + run: | + cd infra + CLUSTER_NAME="${{ steps.cluster-info.outputs.cluster_name }}" + + make ephemeral-init + make ephemeral-destroy WORKSPACE=${{ steps.cluster-info.outputs.cluster_name }} || true + + kubectl delete cluster "$CLUSTER_NAME" -n "$CLUSTER_NAME" --context tools --kubeconfig /home/runner/.kube/config || true + + ./clusters/scripts/ip_pool_manager.sh release "$CLUSTER_NAME" || true + + cypress-tests: + needs: manage-environment + if: github.event.action != 'closed' && needs.manage-environment.outputs.deployment_status == 'success' + runs-on: self-hosted + container: + image: cypress/included:13.6.4 + steps: + - name: Checkout demo-apps repository + uses: actions/checkout@v4 + + - name: Run Cypress tests + env: + WRITER_URL: https://${{ needs.manage-environment.outputs.writer_dns }} + ENQUEUER_URL: https://${{ needs.manage-environment.outputs.enqueuer_dns }} + MEMORIZER_URL: https://${{ needs.manage-environment.outputs.memorizer_dns }} + FRONTEND_URL: https://${{ needs.manage-environment.outputs.frontend_dns }} + TEST_ENV: ephemeral + CYPRESS_CACHE_FOLDER: /tmp/.cache/Cypress + run: | + echo "Running Cypress tests for demo-apps" + + # Install dependencies + npm ci + + # Run Cypress tests (if they exist) + if [ -d "cypress/e2e" ]; then + npx cypress run \ + --env WRITER_URL=${WRITER_URL},ENQUEUER_URL=${ENQUEUER_URL},MEMORIZER_URL=${MEMORIZER_URL},FRONTEND_URL=${FRONTEND_URL},TEST_ENV=${TEST_ENV} \ + --config baseUrl=${FRONTEND_URL} + else + echo "No Cypress tests found, skipping..." + fi diff --git a/apps/ascii-frontend/kustomize/ephemeral-base/deployment.yaml b/apps/ascii-frontend/kustomize/ephemeral-base/deployment.yaml new file mode 100644 index 0000000..1a8f12b --- /dev/null +++ b/apps/ascii-frontend/kustomize/ephemeral-base/deployment.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ascii-frontend +spec: + replicas: 1 + selector: + matchLabels: + app: ascii-frontend + template: + metadata: + labels: + app: ascii-frontend + spec: + containers: + - name: ascii-frontend + image: registry.fullstack.pw/library/ascii-frontend:latest + ports: + - containerPort: 80 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 15 + periodSeconds: 20 diff --git a/apps/ascii-frontend/kustomize/ephemeral-base/ingress.yaml b/apps/ascii-frontend/kustomize/ephemeral-base/ingress.yaml new file mode 100644 index 0000000..f714472 --- /dev/null +++ b/apps/ascii-frontend/kustomize/ephemeral-base/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ascii-frontend + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + external-dns.alpha.kubernetes.io/hostname: "endpoint" + cert-manager.io/cluster-issuer: "letsencrypt-staging" +spec: + ingressClassName: "traefik" + tls: + - hosts: + - "endpoint" + secretName: ascii-frontend-tls-ephemeral + rules: + - host: "endpoint" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ascii-frontend + port: + number: 80 diff --git a/apps/ascii-frontend/kustomize/ephemeral-base/kustomization.yaml b/apps/ascii-frontend/kustomize/ephemeral-base/kustomization.yaml new file mode 100644 index 0000000..df0f21c --- /dev/null +++ b/apps/ascii-frontend/kustomize/ephemeral-base/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - service.yaml + - deployment.yaml + - ingress.yaml + +namespace: default diff --git a/apps/ascii-frontend/kustomize/ephemeral-base/service.yaml b/apps/ascii-frontend/kustomize/ephemeral-base/service.yaml new file mode 100644 index 0000000..463be1f --- /dev/null +++ b/apps/ascii-frontend/kustomize/ephemeral-base/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: ascii-frontend +spec: + selector: + app: ascii-frontend + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + type: ClusterIP diff --git a/apps/ascii-frontend/kustomize/overlays/ephemeral/kustomization.yaml b/apps/ascii-frontend/kustomize/overlays/ephemeral/kustomization.yaml new file mode 100644 index 0000000..5ac92a0 --- /dev/null +++ b/apps/ascii-frontend/kustomize/overlays/ephemeral/kustomization.yaml @@ -0,0 +1,26 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: default + +resources: + - ../../ephemeral-base + +images: + - name: registry.fullstack.pw/library/ascii-frontend + newTag: pr-will-be-replaced + +patches: + - patch: |- + - op: replace + path: /metadata/annotations/external-dns.alpha.kubernetes.io~1hostname + value: "dev.ascii-frontend.fullstack.pw" + - op: replace + path: /spec/tls/0/hosts/0 + value: "dev.ascii-frontend.fullstack.pw" + - op: replace + path: /spec/rules/0/host + value: "dev.ascii-frontend.fullstack.pw" + target: + kind: Ingress + name: ascii-frontend diff --git a/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml b/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml new file mode 100644 index 0000000..874859e --- /dev/null +++ b/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: enqueuer +spec: + replicas: 1 + selector: + matchLabels: + app: enqueuer + template: + metadata: + labels: + app: enqueuer + spec: + containers: + - name: enqueuer + image: registry.fullstack.pw/library/enqueuer:latest + ports: + - containerPort: 8080 + env: + - name: ENV + value: "ephemeral" + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: opentelemetry-collector.observability.svc.cluster.local:4317 + - name: NATS_URL + value: nats.fullstack.pw:4222 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 20 diff --git a/apps/enqueuer/kustomize/ephemeral-base/ingress.yaml b/apps/enqueuer/kustomize/ephemeral-base/ingress.yaml new file mode 100644 index 0000000..0860036 --- /dev/null +++ b/apps/enqueuer/kustomize/ephemeral-base/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: enqueuer + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + external-dns.alpha.kubernetes.io/hostname: "endpoint" + cert-manager.io/cluster-issuer: "letsencrypt-staging" +spec: + ingressClassName: "traefik" + tls: + - hosts: + - "endpoint" + secretName: enqueuer-tls-ephemeral + rules: + - host: "endpoint" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: enqueuer + port: + number: 8080 diff --git a/apps/enqueuer/kustomize/ephemeral-base/kustomization.yaml b/apps/enqueuer/kustomize/ephemeral-base/kustomization.yaml new file mode 100644 index 0000000..df0f21c --- /dev/null +++ b/apps/enqueuer/kustomize/ephemeral-base/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - service.yaml + - deployment.yaml + - ingress.yaml + +namespace: default diff --git a/apps/enqueuer/kustomize/ephemeral-base/service.yaml b/apps/enqueuer/kustomize/ephemeral-base/service.yaml new file mode 100644 index 0000000..a06d818 --- /dev/null +++ b/apps/enqueuer/kustomize/ephemeral-base/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: enqueuer +spec: + selector: + app: enqueuer + ports: + - port: 8080 + targetPort: 8080 + protocol: TCP + name: http + type: ClusterIP diff --git a/apps/enqueuer/kustomize/overlays/ephemeral/kustomization.yaml b/apps/enqueuer/kustomize/overlays/ephemeral/kustomization.yaml new file mode 100644 index 0000000..6f1d580 --- /dev/null +++ b/apps/enqueuer/kustomize/overlays/ephemeral/kustomization.yaml @@ -0,0 +1,26 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: default + +resources: + - ../../ephemeral-base + +images: + - name: registry.fullstack.pw/library/enqueuer + newTag: pr-will-be-replaced + +patches: + - patch: |- + - op: replace + path: /metadata/annotations/external-dns.alpha.kubernetes.io~1hostname + value: "dev.enqueuer.fullstack.pw" + - op: replace + path: /spec/tls/0/hosts/0 + value: "dev.enqueuer.fullstack.pw" + - op: replace + path: /spec/rules/0/host + value: "dev.enqueuer.fullstack.pw" + target: + kind: Ingress + name: enqueuer diff --git a/apps/memorizer/kustomize/ephemeral-base/deployment.yaml b/apps/memorizer/kustomize/ephemeral-base/deployment.yaml new file mode 100644 index 0000000..2222035 --- /dev/null +++ b/apps/memorizer/kustomize/ephemeral-base/deployment.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: memorizer +spec: + replicas: 1 + selector: + matchLabels: + app: memorizer + template: + metadata: + labels: + app: memorizer + spec: + containers: + - name: memorizer + image: registry.fullstack.pw/library/memorizer:latest + ports: + - containerPort: 8080 + env: + - name: ENV + value: "ephemeral" + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: opentelemetry-collector.observability.svc.cluster.local:4317 + - name: QUEUE_NAMES + value: "ephemeral-queue" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: cluster-secrets + key: REDIS_PASSWORD + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 20 diff --git a/apps/memorizer/kustomize/ephemeral-base/ingress.yaml b/apps/memorizer/kustomize/ephemeral-base/ingress.yaml new file mode 100644 index 0000000..dff1862 --- /dev/null +++ b/apps/memorizer/kustomize/ephemeral-base/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: memorizer + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + external-dns.alpha.kubernetes.io/hostname: "endpoint" + cert-manager.io/cluster-issuer: "letsencrypt-staging" +spec: + ingressClassName: "traefik" + tls: + - hosts: + - "endpoint" + secretName: memorizer-tls-ephemeral + rules: + - host: "endpoint" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: memorizer + port: + number: 8080 diff --git a/apps/memorizer/kustomize/ephemeral-base/kustomization.yaml b/apps/memorizer/kustomize/ephemeral-base/kustomization.yaml new file mode 100644 index 0000000..df0f21c --- /dev/null +++ b/apps/memorizer/kustomize/ephemeral-base/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - service.yaml + - deployment.yaml + - ingress.yaml + +namespace: default diff --git a/apps/memorizer/kustomize/ephemeral-base/service.yaml b/apps/memorizer/kustomize/ephemeral-base/service.yaml new file mode 100644 index 0000000..6f157ad --- /dev/null +++ b/apps/memorizer/kustomize/ephemeral-base/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: memorizer +spec: + selector: + app: memorizer + ports: + - port: 8080 + targetPort: 8080 + protocol: TCP + name: http + type: ClusterIP diff --git a/apps/memorizer/kustomize/overlays/ephemeral/kustomization.yaml b/apps/memorizer/kustomize/overlays/ephemeral/kustomization.yaml new file mode 100644 index 0000000..4aa7840 --- /dev/null +++ b/apps/memorizer/kustomize/overlays/ephemeral/kustomization.yaml @@ -0,0 +1,26 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: default + +resources: + - ../../ephemeral-base + +images: + - name: registry.fullstack.pw/library/memorizer + newTag: pr-will-be-replaced + +patches: + - patch: |- + - op: replace + path: /metadata/annotations/external-dns.alpha.kubernetes.io~1hostname + value: "dev.memorizer.fullstack.pw" + - op: replace + path: /spec/tls/0/hosts/0 + value: "dev.memorizer.fullstack.pw" + - op: replace + path: /spec/rules/0/host + value: "dev.memorizer.fullstack.pw" + target: + kind: Ingress + name: memorizer diff --git a/apps/writer/kustomize/ephemeral-base/deployment.yaml b/apps/writer/kustomize/ephemeral-base/deployment.yaml new file mode 100644 index 0000000..8a6586b --- /dev/null +++ b/apps/writer/kustomize/ephemeral-base/deployment.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: writer +spec: + replicas: 1 + selector: + matchLabels: + app: writer + template: + metadata: + labels: + app: writer + spec: + containers: + - name: writer + image: registry.fullstack.pw/library/writer:latest + ports: + - containerPort: 8080 + env: + - name: ENV + value: "ephemeral" + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: opentelemetry-collector.observability.svc.cluster.local:4317 + - name: QUEUE_NAMES + value: "ephemeral-queue" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: cluster-secrets + key: REDIS_PASSWORD + - name: DB_USER + valueFrom: + secretKeyRef: + name: postgres-app + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-app + key: password + - name: DB_HOST + value: "postgres-rw.default.svc.cluster.local" + - name: DB_NAME + value: "app" + - name: DB_SSLMODE + value: "require" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 20 diff --git a/apps/writer/kustomize/ephemeral-base/ingress.yaml b/apps/writer/kustomize/ephemeral-base/ingress.yaml new file mode 100644 index 0000000..21d9a72 --- /dev/null +++ b/apps/writer/kustomize/ephemeral-base/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: writer + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + external-dns.alpha.kubernetes.io/hostname: "endpoint" + cert-manager.io/cluster-issuer: "letsencrypt-staging" +spec: + ingressClassName: "traefik" + tls: + - hosts: + - "endpoint" + secretName: writer-tls-ephemeral + rules: + - host: "endpoint" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: writer + port: + number: 8080 diff --git a/apps/writer/kustomize/ephemeral-base/kustomization.yaml b/apps/writer/kustomize/ephemeral-base/kustomization.yaml new file mode 100644 index 0000000..df0f21c --- /dev/null +++ b/apps/writer/kustomize/ephemeral-base/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - service.yaml + - deployment.yaml + - ingress.yaml + +namespace: default diff --git a/apps/writer/kustomize/ephemeral-base/service.yaml b/apps/writer/kustomize/ephemeral-base/service.yaml new file mode 100644 index 0000000..75b43a5 --- /dev/null +++ b/apps/writer/kustomize/ephemeral-base/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: writer +spec: + selector: + app: writer + ports: + - port: 8080 + targetPort: 8080 + protocol: TCP + name: http + type: ClusterIP diff --git a/apps/writer/kustomize/overlays/ephemeral/kustomization.yaml b/apps/writer/kustomize/overlays/ephemeral/kustomization.yaml new file mode 100644 index 0000000..bd80254 --- /dev/null +++ b/apps/writer/kustomize/overlays/ephemeral/kustomization.yaml @@ -0,0 +1,26 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: default + +resources: + - ../../ephemeral-base + +images: + - name: registry.fullstack.pw/library/writer + newTag: pr-will-be-replaced + +patches: + - patch: |- + - op: replace + path: /metadata/annotations/external-dns.alpha.kubernetes.io~1hostname + value: "dev.writer.fullstack.pw" + - op: replace + path: /spec/tls/0/hosts/0 + value: "dev.writer.fullstack.pw" + - op: replace + path: /spec/rules/0/host + value: "dev.writer.fullstack.pw" + target: + kind: Ingress + name: writer From 8655de238811e98d8e8c036e89add53688224709 Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Thu, 5 Feb 2026 08:13:33 +0000 Subject: [PATCH 02/14] fix(ephemeral-clusters): docker context --- .github/workflows/ephemeral.yml | 10 +- .github/workflows/pipeline.yml | 446 ++++++++++++++++---------------- 2 files changed, 228 insertions(+), 228 deletions(-) diff --git a/.github/workflows/ephemeral.yml b/.github/workflows/ephemeral.yml index d4c0d5f..3aa771c 100644 --- a/.github/workflows/ephemeral.yml +++ b/.github/workflows/ephemeral.yml @@ -154,11 +154,11 @@ jobs: run: | PR_TAG="pr-${{ github.event.pull_request.number }}" - # Build all 4 images - docker build -t ${{ env.REGISTRY }}/library/writer:${PR_TAG} -f apps/writer/Dockerfile apps/writer & - docker build -t ${{ env.REGISTRY }}/library/enqueuer:${PR_TAG} -f apps/enqueuer/Dockerfile apps/enqueuer & - docker build -t ${{ env.REGISTRY }}/library/memorizer:${PR_TAG} -f apps/memorizer/Dockerfile apps/memorizer & - docker build -t ${{ env.REGISTRY }}/library/ascii-frontend:${PR_TAG} -f apps/ascii-frontend/Dockerfile apps/ascii-frontend & + # Build all 4 images with correct context + docker build -t ${{ env.REGISTRY }}/library/writer:${PR_TAG} -f apps/writer/Dockerfile apps & + docker build -t ${{ env.REGISTRY }}/library/enqueuer:${PR_TAG} -f apps/enqueuer/Dockerfile apps & + docker build -t ${{ env.REGISTRY }}/library/memorizer:${PR_TAG} -f apps/memorizer/Dockerfile apps & + docker build -t ${{ env.REGISTRY }}/library/ascii-frontend:${PR_TAG} -f apps/ascii-frontend/Dockerfile apps & # Wait for all builds to complete wait diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 596d73e..e2112e5 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -1,235 +1,235 @@ -name: Multi-App CI/CD Pipeline - -on: - pull_request: - branches: - - main - types: - - opened - - synchronize - - reopened - paths: - - "apps/**" - push: - branches: - - main - paths: - - "apps/**" - -permissions: - contents: write - -jobs: - determine-app: - runs-on: self-hosted - outputs: - apps: ${{ steps.find-apps.outputs.apps }} - go_apps: ${{ steps.find-apps.outputs.go_apps }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Find changed apps - id: find-apps - run: | - if [[ "${{ github.event_name }}" == "push" ]]; then - # For push events, get changed files from the commit - CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }}) - else - # For PR events, get changed files in the PR - CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} HEAD) - fi - - APPS=() - GO_APPS=() - SEEN_APPS=() - - for FILE in $CHANGED_FILES; do - if [[ $FILE == apps/* && $FILE != apps/shared/* ]]; then - APP_PATH=$(echo $FILE | cut -d'/' -f1-2) - APP_NAME=$(echo $FILE | cut -d'/' -f2) - - # Check if this app is already in the list - if [[ ! " ${SEEN_APPS[@]} " =~ " $APP_NAME " ]]; then - SEEN_APPS+=("$APP_NAME") - - APP_ENTRY="{\"app_path\": \"$APP_PATH\", \"app_name\": \"$APP_NAME\"}" - APPS+=("$APP_ENTRY") - - # Add to go_apps if it's a Go project - if [[ -f "$APP_PATH/go.mod" ]]; then - GO_APPS+=("$APP_ENTRY") - fi - fi - fi - done - - # Format as JSON arrays for GitHub Actions - if [[ ${#APPS[@]} -eq 0 ]]; then - APPS_JSON="[]" - else - APPS_JSON=$(printf '%s\n' "${APPS[@]}" | jq -s . | jq -c .) - fi - - if [[ ${#GO_APPS[@]} -eq 0 ]]; then - GO_APPS_JSON="[]" - else - GO_APPS_JSON=$(printf '%s\n' "${GO_APPS[@]}" | jq -s . | jq -c .) - fi - - echo "Apps: $APPS_JSON" - echo "Go Apps: $GO_APPS_JSON" - echo "apps=$APPS_JSON" >> $GITHUB_OUTPUT - echo "go_apps=$GO_APPS_JSON" >> $GITHUB_OUTPUT - - go-tests: - needs: determine-app - if: needs.determine-app.outputs.go_apps != '[]' - strategy: - matrix: - app: ${{ fromJson(needs.determine-app.outputs.go_apps) }} - name: GO Tests for ${{ matrix.app.app_name }} - uses: fullstack-pw/pipelines/.github/workflows/go-tests.yml@main - with: - go-dir: ./${{ matrix.app.app_path }} - - docker-build-and-push: - needs: [determine-app] - if: needs.determine-app.outputs.apps != '[]' - strategy: - matrix: - app: ${{ fromJson(needs.determine-app.outputs.apps) }} - name: Build and Push ${{ matrix.app.app_name }} - uses: fullstack-pw/pipelines/.github/workflows/build-and-push.yml@main - with: - app-context: "./apps" - app-name: "${{ matrix.app.app_name }}" - app-dockerfile: "./apps/${{ matrix.app.app_name }}/Dockerfile" - - update-dev-kustomization: - needs: [determine-app, docker-build-and-push] - if: needs.determine-app.outputs.apps != '[]' && github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: self-hosted - strategy: - matrix: - app: ${{ fromJson(needs.determine-app.outputs.apps) }} - name: Deploy ${{ matrix.app.app_name }} to dev - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - - - name: Update dev kustomization with new image tag - run: | - # Use full SHA to match the Docker tag from build-and-push.yml - VERSION="${{ github.sha }}" - KUSTOMIZATION_FILE="apps/${{ matrix.app.app_name }}/kustomize/overlays/dev/kustomization.yaml" - - if [ ! -f "$KUSTOMIZATION_FILE" ]; then - echo "Warning: $KUSTOMIZATION_FILE not found, skipping" - exit 0 - fi - - echo "Updating ${{ matrix.app.app_name }} dev to version: $VERSION" - sed -i "s/newTag: .*/newTag: $VERSION/" "$KUSTOMIZATION_FILE" - - git config user.name "GitHub Actions Bot" - git config user.email "actions@github.com" - git add "$KUSTOMIZATION_FILE" - git commit -m "chore(${{ matrix.app.app_name }}): deploy $VERSION to dev [skip ci]" || echo "No changes to commit" - git push || echo "Nothing to push" - -# dev-deploy: -# needs: [determine-app, docker-build-and-push] -# if: needs.determine-app.outputs.app_paths != '[]' +# name: Multi-App CI/CD Pipeline + +# on: +# pull_request: +# branches: +# - main +# types: +# - opened +# - synchronize +# - reopened +# paths: +# - "apps/**" +# push: +# branches: +# - main +# paths: +# - "apps/**" + +# permissions: +# contents: write + +# jobs: +# determine-app: +# runs-on: self-hosted +# outputs: +# apps: ${{ steps.find-apps.outputs.apps }} +# go_apps: ${{ steps.find-apps.outputs.go_apps }} +# steps: +# - name: Checkout code +# uses: actions/checkout@v4 +# with: +# fetch-depth: 0 + +# - name: Find changed apps +# id: find-apps +# run: | +# if [[ "${{ github.event_name }}" == "push" ]]; then +# # For push events, get changed files from the commit +# CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }}) +# else +# # For PR events, get changed files in the PR +# CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} HEAD) +# fi + +# APPS=() +# GO_APPS=() +# SEEN_APPS=() + +# for FILE in $CHANGED_FILES; do +# if [[ $FILE == apps/* && $FILE != apps/shared/* ]]; then +# APP_PATH=$(echo $FILE | cut -d'/' -f1-2) +# APP_NAME=$(echo $FILE | cut -d'/' -f2) + +# # Check if this app is already in the list +# if [[ ! " ${SEEN_APPS[@]} " =~ " $APP_NAME " ]]; then +# SEEN_APPS+=("$APP_NAME") + +# APP_ENTRY="{\"app_path\": \"$APP_PATH\", \"app_name\": \"$APP_NAME\"}" +# APPS+=("$APP_ENTRY") + +# # Add to go_apps if it's a Go project +# if [[ -f "$APP_PATH/go.mod" ]]; then +# GO_APPS+=("$APP_ENTRY") +# fi +# fi +# fi +# done + +# # Format as JSON arrays for GitHub Actions +# if [[ ${#APPS[@]} -eq 0 ]]; then +# APPS_JSON="[]" +# else +# APPS_JSON=$(printf '%s\n' "${APPS[@]}" | jq -s . | jq -c .) +# fi + +# if [[ ${#GO_APPS[@]} -eq 0 ]]; then +# GO_APPS_JSON="[]" +# else +# GO_APPS_JSON=$(printf '%s\n' "${GO_APPS[@]}" | jq -s . | jq -c .) +# fi + +# echo "Apps: $APPS_JSON" +# echo "Go Apps: $GO_APPS_JSON" +# echo "apps=$APPS_JSON" >> $GITHUB_OUTPUT +# echo "go_apps=$GO_APPS_JSON" >> $GITHUB_OUTPUT + +# go-tests: +# needs: determine-app +# if: needs.determine-app.outputs.go_apps != '[]' # strategy: # matrix: -# app: ${{ fromJson(needs.determine-app.outputs.apps) }} -# name: DEV deploy ${{ matrix.app.app_name }} -# uses: fullstack-pw/pipelines/.github/workflows/deploy-kustomize.yml@main -# with: -# kustomize-dir: "./${{ matrix.app.app_path }}/kustomize/overlays/dev" -# context: "dev" -# app-name: ${{ matrix.app.app_name }} - -# dev-cypress-tests: -# needs: dev-deploy -# name: DEV cypress -# uses: fullstack-pw/pipelines/.github/workflows/cypress.yml@main +# app: ${{ fromJson(needs.determine-app.outputs.go_apps) }} +# name: GO Tests for ${{ matrix.app.app_name }} +# uses: fullstack-pw/pipelines/.github/workflows/go-tests.yml@main # with: -# start: npm run cypress:dev -# env-vars: | -# { -# "ENQUEUER_URL": "https://dev.enqueuer.fullstack.pw", -# "ENVIRONMENT": "dev", -# "TEST_RETRIES": "2" -# } - -# stg-deploy: -# needs: [determine-app, dev-cypress-tests] -# if: needs.determine-app.outputs.app_paths != '[]' +# go-dir: ./${{ matrix.app.app_path }} + +# docker-build-and-push: +# needs: [determine-app] +# if: needs.determine-app.outputs.apps != '[]' # strategy: # matrix: # app: ${{ fromJson(needs.determine-app.outputs.apps) }} -# name: STG deploy ${{ matrix.app.app_name }} -# uses: fullstack-pw/pipelines/.github/workflows/deploy-kustomize.yml@main -# with: -# kustomize-dir: "./${{ matrix.app.app_path }}/kustomize/overlays/stg" -# context: "stg" -# app-name: ${{ matrix.app.app_name }} - -# stg-cypress-tests: -# needs: stg-deploy -# name: STG cypress -# uses: fullstack-pw/pipelines/.github/workflows/cypress.yml@main +# name: Build and Push ${{ matrix.app.app_name }} +# uses: fullstack-pw/pipelines/.github/workflows/build-and-push.yml@main # with: -# start: npm run cypress:stg -# env-vars: | -# { -# "ENQUEUER_URL": "https://stg.enqueuer.fullstack.pw", -# "WRITER_URL": "https://stg.writer.fullstack.pw", -# "MEMORIZER_URL": "https://stg.memorizer.fullstack.pw", -# "ENVIRONMENT": "stg", -# "TEST_RETRIES": "2" -# } - -# prod-deploy: -# needs: [determine-app, stg-cypress-tests] -# if: needs.determine-app.outputs.app_paths != '[]' && github.event_name == 'push' && github.ref == 'refs/heads/main' +# app-context: "./apps" +# app-name: "${{ matrix.app.app_name }}" +# app-dockerfile: "./apps/${{ matrix.app.app_name }}/Dockerfile" + +# update-dev-kustomization: +# needs: [determine-app, docker-build-and-push] +# if: needs.determine-app.outputs.apps != '[]' && github.event_name == 'push' && github.ref == 'refs/heads/main' +# runs-on: self-hosted # strategy: # matrix: # app: ${{ fromJson(needs.determine-app.outputs.apps) }} -# name: PROD deploy ${{ matrix.app.app_name }} -# uses: fullstack-pw/pipelines/.github/workflows/deploy-kustomize.yml@main -# with: -# kustomize-dir: "./${{ matrix.app.app_path }}/kustomize/overlays/prod" -# context: "prod" -# app-name: ${{ matrix.app.app_name }} - -# versioning: -# permissions: -# contents: write -# runs-on: self-hosted -# if: github.event_name == 'push' && github.ref == 'refs/heads/main' -# name: Versioning +# name: Deploy ${{ matrix.app.app_name }} to dev # steps: -# - name: Get Next Version -# id: semver -# uses: ietf-tools/semver-action@v1 -# with: -# token: ${{ github.token }} -# branch: main - -# - name: Create Release -# uses: ncipollo/release-action@v1.12.0 +# - name: Checkout code +# uses: actions/checkout@v4 # with: -# allowUpdates: true -# draft: false -# makeLatest: true -# tag: ${{ steps.semver.outputs.next }} -# body: Changelog Contents -# token: ${{ github.token }} +# token: ${{ secrets.GITHUB_TOKEN }} +# fetch-depth: 0 + +# - name: Update dev kustomization with new image tag +# run: | +# # Use full SHA to match the Docker tag from build-and-push.yml +# VERSION="${{ github.sha }}" +# KUSTOMIZATION_FILE="apps/${{ matrix.app.app_name }}/kustomize/overlays/dev/kustomization.yaml" + +# if [ ! -f "$KUSTOMIZATION_FILE" ]; then +# echo "Warning: $KUSTOMIZATION_FILE not found, skipping" +# exit 0 +# fi + +# echo "Updating ${{ matrix.app.app_name }} dev to version: $VERSION" +# sed -i "s/newTag: .*/newTag: $VERSION/" "$KUSTOMIZATION_FILE" + +# git config user.name "GitHub Actions Bot" +# git config user.email "actions@github.com" +# git add "$KUSTOMIZATION_FILE" +# git commit -m "chore(${{ matrix.app.app_name }}): deploy $VERSION to dev [skip ci]" || echo "No changes to commit" +# git push || echo "Nothing to push" + +# # dev-deploy: +# # needs: [determine-app, docker-build-and-push] +# # if: needs.determine-app.outputs.app_paths != '[]' +# # strategy: +# # matrix: +# # app: ${{ fromJson(needs.determine-app.outputs.apps) }} +# # name: DEV deploy ${{ matrix.app.app_name }} +# # uses: fullstack-pw/pipelines/.github/workflows/deploy-kustomize.yml@main +# # with: +# # kustomize-dir: "./${{ matrix.app.app_path }}/kustomize/overlays/dev" +# # context: "dev" +# # app-name: ${{ matrix.app.app_name }} + +# # dev-cypress-tests: +# # needs: dev-deploy +# # name: DEV cypress +# # uses: fullstack-pw/pipelines/.github/workflows/cypress.yml@main +# # with: +# # start: npm run cypress:dev +# # env-vars: | +# # { +# # "ENQUEUER_URL": "https://dev.enqueuer.fullstack.pw", +# # "ENVIRONMENT": "dev", +# # "TEST_RETRIES": "2" +# # } + +# # stg-deploy: +# # needs: [determine-app, dev-cypress-tests] +# # if: needs.determine-app.outputs.app_paths != '[]' +# # strategy: +# # matrix: +# # app: ${{ fromJson(needs.determine-app.outputs.apps) }} +# # name: STG deploy ${{ matrix.app.app_name }} +# # uses: fullstack-pw/pipelines/.github/workflows/deploy-kustomize.yml@main +# # with: +# # kustomize-dir: "./${{ matrix.app.app_path }}/kustomize/overlays/stg" +# # context: "stg" +# # app-name: ${{ matrix.app.app_name }} + +# # stg-cypress-tests: +# # needs: stg-deploy +# # name: STG cypress +# # uses: fullstack-pw/pipelines/.github/workflows/cypress.yml@main +# # with: +# # start: npm run cypress:stg +# # env-vars: | +# # { +# # "ENQUEUER_URL": "https://stg.enqueuer.fullstack.pw", +# # "WRITER_URL": "https://stg.writer.fullstack.pw", +# # "MEMORIZER_URL": "https://stg.memorizer.fullstack.pw", +# # "ENVIRONMENT": "stg", +# # "TEST_RETRIES": "2" +# # } + +# # prod-deploy: +# # needs: [determine-app, stg-cypress-tests] +# # if: needs.determine-app.outputs.app_paths != '[]' && github.event_name == 'push' && github.ref == 'refs/heads/main' +# # strategy: +# # matrix: +# # app: ${{ fromJson(needs.determine-app.outputs.apps) }} +# # name: PROD deploy ${{ matrix.app.app_name }} +# # uses: fullstack-pw/pipelines/.github/workflows/deploy-kustomize.yml@main +# # with: +# # kustomize-dir: "./${{ matrix.app.app_path }}/kustomize/overlays/prod" +# # context: "prod" +# # app-name: ${{ matrix.app.app_name }} + +# # versioning: +# # permissions: +# # contents: write +# # runs-on: self-hosted +# # if: github.event_name == 'push' && github.ref == 'refs/heads/main' +# # name: Versioning +# # steps: +# # - name: Get Next Version +# # id: semver +# # uses: ietf-tools/semver-action@v1 +# # with: +# # token: ${{ github.token }} +# # branch: main + +# # - name: Create Release +# # uses: ncipollo/release-action@v1.12.0 +# # with: +# # allowUpdates: true +# # draft: false +# # makeLatest: true +# # tag: ${{ steps.semver.outputs.next }} +# # body: Changelog Contents +# # token: ${{ github.token }} From f5adae76283c1768ae5eba584b1dea5aea0ea621 Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 06:45:49 +0000 Subject: [PATCH 03/14] fix(ephemeral): tests --- apps/ascii-frontend/kustomize/ephemeral-base/ingress.yaml | 2 +- apps/enqueuer/kustomize/ephemeral-base/ingress.yaml | 2 +- apps/memorizer/kustomize/ephemeral-base/ingress.yaml | 2 +- apps/writer/kustomize/ephemeral-base/ingress.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ascii-frontend/kustomize/ephemeral-base/ingress.yaml b/apps/ascii-frontend/kustomize/ephemeral-base/ingress.yaml index f714472..0bff800 100644 --- a/apps/ascii-frontend/kustomize/ephemeral-base/ingress.yaml +++ b/apps/ascii-frontend/kustomize/ephemeral-base/ingress.yaml @@ -5,7 +5,7 @@ metadata: annotations: nginx.ingress.kubernetes.io/ssl-redirect: "true" external-dns.alpha.kubernetes.io/hostname: "endpoint" - cert-manager.io/cluster-issuer: "letsencrypt-staging" + cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: ingressClassName: "traefik" tls: diff --git a/apps/enqueuer/kustomize/ephemeral-base/ingress.yaml b/apps/enqueuer/kustomize/ephemeral-base/ingress.yaml index 0860036..b216b21 100644 --- a/apps/enqueuer/kustomize/ephemeral-base/ingress.yaml +++ b/apps/enqueuer/kustomize/ephemeral-base/ingress.yaml @@ -5,7 +5,7 @@ metadata: annotations: nginx.ingress.kubernetes.io/ssl-redirect: "true" external-dns.alpha.kubernetes.io/hostname: "endpoint" - cert-manager.io/cluster-issuer: "letsencrypt-staging" + cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: ingressClassName: "traefik" tls: diff --git a/apps/memorizer/kustomize/ephemeral-base/ingress.yaml b/apps/memorizer/kustomize/ephemeral-base/ingress.yaml index dff1862..7219366 100644 --- a/apps/memorizer/kustomize/ephemeral-base/ingress.yaml +++ b/apps/memorizer/kustomize/ephemeral-base/ingress.yaml @@ -5,7 +5,7 @@ metadata: annotations: nginx.ingress.kubernetes.io/ssl-redirect: "true" external-dns.alpha.kubernetes.io/hostname: "endpoint" - cert-manager.io/cluster-issuer: "letsencrypt-staging" + cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: ingressClassName: "traefik" tls: diff --git a/apps/writer/kustomize/ephemeral-base/ingress.yaml b/apps/writer/kustomize/ephemeral-base/ingress.yaml index 21d9a72..b6526aa 100644 --- a/apps/writer/kustomize/ephemeral-base/ingress.yaml +++ b/apps/writer/kustomize/ephemeral-base/ingress.yaml @@ -5,7 +5,7 @@ metadata: annotations: nginx.ingress.kubernetes.io/ssl-redirect: "true" external-dns.alpha.kubernetes.io/hostname: "endpoint" - cert-manager.io/cluster-issuer: "letsencrypt-staging" + cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: ingressClassName: "traefik" tls: From c47dd5a0af81a333b081b76624de798293acc271 Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 06:55:40 +0000 Subject: [PATCH 04/14] fix(ephemeral): tests --- .github/workflows/ephemeral.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ephemeral.yml b/.github/workflows/ephemeral.yml index 3aa771c..81967f2 100644 --- a/.github/workflows/ephemeral.yml +++ b/.github/workflows/ephemeral.yml @@ -331,6 +331,7 @@ jobs: MEMORIZER_URL: https://${{ needs.manage-environment.outputs.memorizer_dns }} FRONTEND_URL: https://${{ needs.manage-environment.outputs.frontend_dns }} TEST_ENV: ephemeral + ENVIRONMENT: ephemeral CYPRESS_CACHE_FOLDER: /tmp/.cache/Cypress run: | echo "Running Cypress tests for demo-apps" From 50f0e335e32a711593be0c60fe4cf9cd27774336 Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 08:06:09 +0000 Subject: [PATCH 05/14] fix(ephemeral): tests --- cypress/e2e/enqueuer.cy.js | 6 ++++-- cypress/fixtures/environments.json | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/enqueuer.cy.js b/cypress/e2e/enqueuer.cy.js index d3d7fa4..712b10f 100644 --- a/cypress/e2e/enqueuer.cy.js +++ b/cypress/e2e/enqueuer.cy.js @@ -7,6 +7,8 @@ describe('Enqueuer Service Tests', () => { }); it('should accept and queue valid messages', () => { + const queueName = `queue-${Cypress.env('ENVIRONMENT')}`; + // Test with valid message const validMessage = { content: `Linux Tux`, @@ -15,7 +17,7 @@ describe('Enqueuer Service Tests', () => { cy.request({ method: 'POST', - url: `${Cypress.env('ENQUEUER_URL')}/add?queue=queue-dev`, + url: `${Cypress.env('ENQUEUER_URL')}/add?queue=${queueName}`, body: validMessage, headers: { 'Content-Type': 'application/json' @@ -27,7 +29,7 @@ describe('Enqueuer Service Tests', () => { if (response.status === 201) { expect(response.body).to.have.property('status'); - expect(response.body).to.have.property('queue', 'queue-dev'); + expect(response.body).to.have.property('queue', queueName); expect(response.body).to.have.property('image_url').and.not.be.empty; // ASCII fields are present but may be empty if memorizer didn't respond in time expect(response.body).to.have.property('image_ascii_text'); diff --git a/cypress/fixtures/environments.json b/cypress/fixtures/environments.json index 63c63af..156ae6e 100644 --- a/cypress/fixtures/environments.json +++ b/cypress/fixtures/environments.json @@ -10,5 +10,8 @@ }, "local": { "enqueuer": "http://localhost:8081" + }, + "ephemeral": { + "enqueuer": "PLACEHOLDER_WILL_BE_OVERRIDDEN" } } \ No newline at end of file From 5c35eef59377404f70d1c24be359c20f3e459660 Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 09:47:56 +0000 Subject: [PATCH 06/14] fix(ephemeral): tests --- cypress.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress.config.js b/cypress.config.js index c147164..de8cfe6 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -86,7 +86,7 @@ module.exports = defineConfig({ supportFile: 'cypress/support/e2e.js', defaultCommandTimeout: 10000, requestTimeout: 8000, - responseTimeout: 30000, + responseTimeout: 180000, retries: { runMode: 2, openMode: 0 From 80a28e3869bd2d5b7f03e3ff884a06004b809e72 Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 10:17:48 +0000 Subject: [PATCH 07/14] fix(cypress): ephemeral --- README.md | 27 +++++++++++++++++++ .../kustomize/ephemeral-base/deployment.yaml | 7 ----- .../kustomize/ephemeral-base/deployment.yaml | 2 +- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8d4bafb..f3b41df 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,33 @@ This project demonstrates a modern microservices architecture that processes mes The system consists of multiple microservices communicating via message queues: ![Arch](demo-apps.svg) + +### Complete Message Flow + +1. **User Request**: User accesses `ascii-frontend` and submits a query (e.g., "kubernetes") +2. **Enqueuer Processing**: + - Receives the query via REST API (`POST /add`) + - Searches Google Images using Chromium headless browser + - Picks the first suitable image URL + - Publishes message with image URL to NATS queue (`{ENV}.queue-{QUEUE_NAME}`) + - Subscribes to result queue (`{ENV}.result-queue`) to wait for ASCII art response +3. **Memorizer Processing**: + - Listens to NATS queue for new messages + - Downloads the image from the URL + - Generates ASCII art (terminal, file, and HTML formats) + - Stores the ASCII art output in Redis (with environment prefix: `{ENV}:{MESSAGE_ID}:ascii_*`) + - Publishes the result back to NATS result queue +4. **Enqueuer Response**: + - Receives ASCII art result from NATS result queue + - Returns complete response to the user (including image URL and ASCII art) +5. **Writer Storage**: + - Monitors Redis for new entries + - Retrieves processed messages with ASCII art + - Stores everything in PostgreSQL (environment-specific tables) + - Provides query endpoints for historical data + +### Technology Stack + - **Communication**: NATS for message queuing - **Storage**: Redis for temporary storage, PostgreSQL for persistent storage - **Deployment**: Kubernetes with Kustomize configurations diff --git a/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml b/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml index 874859e..4a810fc 100644 --- a/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml +++ b/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml @@ -24,13 +24,6 @@ spec: value: opentelemetry-collector.observability.svc.cluster.local:4317 - name: NATS_URL value: nats.fullstack.pw:4222 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 200m - memory: 256Mi readinessProbe: httpGet: path: /health diff --git a/apps/memorizer/kustomize/ephemeral-base/deployment.yaml b/apps/memorizer/kustomize/ephemeral-base/deployment.yaml index 2222035..b40cbb7 100644 --- a/apps/memorizer/kustomize/ephemeral-base/deployment.yaml +++ b/apps/memorizer/kustomize/ephemeral-base/deployment.yaml @@ -23,7 +23,7 @@ spec: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: opentelemetry-collector.observability.svc.cluster.local:4317 - name: QUEUE_NAMES - value: "ephemeral-queue" + value: "queue-ephemeral" - name: REDIS_PASSWORD valueFrom: secretKeyRef: From 0da264f068217955c1f730d9ba95b555c956e0dc Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 10:28:09 +0000 Subject: [PATCH 08/14] fix(cypress): ephemeral --- cypress/support/commands.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 8a7fea6..afe298d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,10 +1,14 @@ // This file allows to create custom Cypress commands and overwrite existing ones // -- This is a parent command -- -Cypress.Commands.add('sendMessage', (message, queueName = 'cypress-test') => { +Cypress.Commands.add('sendMessage', (message, queueName = null) => { + // Use environment-specific queue name if not explicitly provided + const defaultQueue = `queue-${Cypress.env('ENVIRONMENT')}`; + const queue = queueName || defaultQueue; + return cy.request({ method: 'POST', - url: `${Cypress.env('ENQUEUER_URL')}/add?queue=${queueName}`, + url: `${Cypress.env('ENQUEUER_URL')}/add?queue=${queue}`, body: message, headers: { 'Content-Type': 'application/json' From 04397e637635cf85acec28eb035a2849c400d3cf Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 10:38:09 +0000 Subject: [PATCH 09/14] fix(cypress): ephemeral --- cypress/e2e/full-pipeline.cy.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cypress/e2e/full-pipeline.cy.js b/cypress/e2e/full-pipeline.cy.js index 8bb3f5e..d1edbb1 100644 --- a/cypress/e2e/full-pipeline.cy.js +++ b/cypress/e2e/full-pipeline.cy.js @@ -29,6 +29,10 @@ describe('Microservices Pipeline Tests', () => { expect(response.status).to.equal(201); cy.log(`Message ${testId} sent to pipeline`); + // Use retry logic to wait for memorizer to process the message + // Add a small delay to avoid race condition where Redis write is still in progress + cy.wait(1000); + // Check if the message was processed by Memorizer via Enqueuer's check endpoint cy.request({ method: 'GET', From 770d459bfc2f4e87e4472e056e3e1342754ff38f Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 10:45:21 +0000 Subject: [PATCH 10/14] fix(cypress): ephemeral --- cypress/e2e/full-pipeline.cy.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/full-pipeline.cy.js b/cypress/e2e/full-pipeline.cy.js index d1edbb1..123c139 100644 --- a/cypress/e2e/full-pipeline.cy.js +++ b/cypress/e2e/full-pipeline.cy.js @@ -30,8 +30,9 @@ describe('Microservices Pipeline Tests', () => { cy.log(`Message ${testId} sent to pipeline`); // Use retry logic to wait for memorizer to process the message - // Add a small delay to avoid race condition where Redis write is still in progress - cy.wait(1000); + // Add a delay to ensure Redis write operation completes + // The memorizer stores in Redis asynchronously after publishing to NATS + cy.wait(30000); // Check if the message was processed by Memorizer via Enqueuer's check endpoint cy.request({ From 2ca0cbf921487d462ee76b754b8a0a10615b7a70 Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 10:53:36 +0000 Subject: [PATCH 11/14] fix(cypress): ephemeral --- apps/memorizer/main.go | 9 ++++++--- cypress/e2e/full-pipeline.cy.js | 6 +----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/memorizer/main.go b/apps/memorizer/main.go index 4747aa5..b9b629e 100644 --- a/apps/memorizer/main.go +++ b/apps/memorizer/main.go @@ -442,12 +442,15 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { span.SetAttributes(attribute.String("message.id", id)) - // Check if the message exists in Redis - exists, err := redisConn.Client().Exists(ctx, id).Result() + // Apply environment prefix to key (same as SetWithTracing does) + prefixedKey := redisConn.PrefixKey(id) + + // Check if the message exists in Redis using the prefixed key + exists, err := redisConn.Client().Exists(ctx, prefixedKey).Result() if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "Redis query failed") - logger.Error(ctx, "Error checking if message exists", "error", err, "id", id) + logger.Error(ctx, "Error checking if message exists", "error", err, "id", id, "prefixed_key", prefixedKey) http.Error(w, "Error checking message status", http.StatusInternalServerError) return } diff --git a/cypress/e2e/full-pipeline.cy.js b/cypress/e2e/full-pipeline.cy.js index 123c139..034319c 100644 --- a/cypress/e2e/full-pipeline.cy.js +++ b/cypress/e2e/full-pipeline.cy.js @@ -29,11 +29,7 @@ describe('Microservices Pipeline Tests', () => { expect(response.status).to.equal(201); cy.log(`Message ${testId} sent to pipeline`); - // Use retry logic to wait for memorizer to process the message - // Add a delay to ensure Redis write operation completes - // The memorizer stores in Redis asynchronously after publishing to NATS - cy.wait(30000); - + // If enqueuer returned 201, memorizer has already processed and stored the message // Check if the message was processed by Memorizer via Enqueuer's check endpoint cy.request({ method: 'GET', From c8f1145dc8c91d5ec265e5abedc275ffaa1e02bf Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 11:13:31 +0000 Subject: [PATCH 12/14] fix(ephemeral): image tag --- .github/workflows/ephemeral.yml | 20 +++++++++++-------- .../kustomize/ephemeral-base/deployment.yaml | 1 + .../kustomize/ephemeral-base/deployment.yaml | 1 + .../kustomize/ephemeral-base/deployment.yaml | 1 + .../kustomize/ephemeral-base/deployment.yaml | 1 + 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ephemeral.yml b/.github/workflows/ephemeral.yml index 81967f2..48d3ba3 100644 --- a/.github/workflows/ephemeral.yml +++ b/.github/workflows/ephemeral.yml @@ -152,7 +152,7 @@ jobs: - name: Build Docker images if: github.event.action != 'closed' run: | - PR_TAG="pr-${{ github.event.pull_request.number }}" + PR_TAG="pr-${{ github.event.pull_request.number }}-${{ github.sha }}" # Build all 4 images with correct context docker build -t ${{ env.REGISTRY }}/library/writer:${PR_TAG} -f apps/writer/Dockerfile apps & @@ -162,12 +162,12 @@ jobs: # Wait for all builds to complete wait - echo "All images built successfully" + echo "All images built successfully with tag: ${PR_TAG}" - name: Push Docker images if: github.event.action != 'closed' run: | - PR_TAG="pr-${{ github.event.pull_request.number }}" + PR_TAG="pr-${{ github.event.pull_request.number }}-${{ github.sha }}" # Login to Harbor echo "${HARBOR_KEY}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin @@ -196,8 +196,9 @@ jobs: env: KUBECONFIG: /tmp/kubeconfig run: | + PR_TAG="pr-${{ github.event.pull_request.number }}-${{ github.sha }}" kubectl kustomize apps/writer/kustomize/overlays/ephemeral \ - | sed "s|${{ env.REGISTRY }}/library/writer:pr-will-be-replaced|${{ env.REGISTRY }}/library/writer:pr-${{ github.event.pull_request.number }}|" \ + | sed "s|${{ env.REGISTRY }}/library/writer:pr-will-be-replaced|${{ env.REGISTRY }}/library/writer:${PR_TAG}|" \ | sed "s|dev.writer.fullstack.pw|${{ steps.cluster-info.outputs.writer_dns }}|g" \ | kubectl apply -f - @@ -208,8 +209,9 @@ jobs: env: KUBECONFIG: /tmp/kubeconfig run: | + PR_TAG="pr-${{ github.event.pull_request.number }}-${{ github.sha }}" kubectl kustomize apps/enqueuer/kustomize/overlays/ephemeral \ - | sed "s|${{ env.REGISTRY }}/library/enqueuer:pr-will-be-replaced|${{ env.REGISTRY }}/library/enqueuer:pr-${{ github.event.pull_request.number }}|" \ + | sed "s|${{ env.REGISTRY }}/library/enqueuer:pr-will-be-replaced|${{ env.REGISTRY }}/library/enqueuer:${PR_TAG}|" \ | sed "s|dev.enqueuer.fullstack.pw|${{ steps.cluster-info.outputs.enqueuer_dns }}|g" \ | kubectl apply -f - @@ -220,8 +222,9 @@ jobs: env: KUBECONFIG: /tmp/kubeconfig run: | + PR_TAG="pr-${{ github.event.pull_request.number }}-${{ github.sha }}" kubectl kustomize apps/memorizer/kustomize/overlays/ephemeral \ - | sed "s|${{ env.REGISTRY }}/library/memorizer:pr-will-be-replaced|${{ env.REGISTRY }}/library/memorizer:pr-${{ github.event.pull_request.number }}|" \ + | sed "s|${{ env.REGISTRY }}/library/memorizer:pr-will-be-replaced|${{ env.REGISTRY }}/library/memorizer:${PR_TAG}|" \ | sed "s|dev.memorizer.fullstack.pw|${{ steps.cluster-info.outputs.memorizer_dns }}|g" \ | kubectl apply -f - @@ -232,8 +235,9 @@ jobs: env: KUBECONFIG: /tmp/kubeconfig run: | + PR_TAG="pr-${{ github.event.pull_request.number }}-${{ github.sha }}" kubectl kustomize apps/ascii-frontend/kustomize/overlays/ephemeral \ - | sed "s|${{ env.REGISTRY }}/library/ascii-frontend:pr-will-be-replaced|${{ env.REGISTRY }}/library/ascii-frontend:pr-${{ github.event.pull_request.number }}|" \ + | sed "s|${{ env.REGISTRY }}/library/ascii-frontend:pr-will-be-replaced|${{ env.REGISTRY }}/library/ascii-frontend:${PR_TAG}|" \ | sed "s|dev.ascii-frontend.fullstack.pw|${{ steps.cluster-info.outputs.frontend_dns }}|g" \ | kubectl apply -f - @@ -255,7 +259,7 @@ jobs: const enqueuerDns = '${{ steps.cluster-info.outputs.enqueuer_dns }}'; const memorizerDns = '${{ steps.cluster-info.outputs.memorizer_dns }}'; const frontendDns = '${{ steps.cluster-info.outputs.frontend_dns }}'; - const prTag = 'pr-${{ github.event.pull_request.number }}'; + const prTag = 'pr-${{ github.event.pull_request.number }}-${{ github.sha }}'; const body = `## Ephemeral Environment Ready diff --git a/apps/ascii-frontend/kustomize/ephemeral-base/deployment.yaml b/apps/ascii-frontend/kustomize/ephemeral-base/deployment.yaml index 1a8f12b..34b034a 100644 --- a/apps/ascii-frontend/kustomize/ephemeral-base/deployment.yaml +++ b/apps/ascii-frontend/kustomize/ephemeral-base/deployment.yaml @@ -15,6 +15,7 @@ spec: containers: - name: ascii-frontend image: registry.fullstack.pw/library/ascii-frontend:latest + imagePullPolicy: Always ports: - containerPort: 80 resources: diff --git a/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml b/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml index 4a810fc..dc155ac 100644 --- a/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml +++ b/apps/enqueuer/kustomize/ephemeral-base/deployment.yaml @@ -15,6 +15,7 @@ spec: containers: - name: enqueuer image: registry.fullstack.pw/library/enqueuer:latest + imagePullPolicy: Always ports: - containerPort: 8080 env: diff --git a/apps/memorizer/kustomize/ephemeral-base/deployment.yaml b/apps/memorizer/kustomize/ephemeral-base/deployment.yaml index b40cbb7..8ca5107 100644 --- a/apps/memorizer/kustomize/ephemeral-base/deployment.yaml +++ b/apps/memorizer/kustomize/ephemeral-base/deployment.yaml @@ -15,6 +15,7 @@ spec: containers: - name: memorizer image: registry.fullstack.pw/library/memorizer:latest + imagePullPolicy: Always ports: - containerPort: 8080 env: diff --git a/apps/writer/kustomize/ephemeral-base/deployment.yaml b/apps/writer/kustomize/ephemeral-base/deployment.yaml index 8a6586b..2cd0457 100644 --- a/apps/writer/kustomize/ephemeral-base/deployment.yaml +++ b/apps/writer/kustomize/ephemeral-base/deployment.yaml @@ -15,6 +15,7 @@ spec: containers: - name: writer image: registry.fullstack.pw/library/writer:latest + imagePullPolicy: Always ports: - containerPort: 8080 env: From 9755e76ef3a744637fcfbfa84461392155983bca Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 11:25:34 +0000 Subject: [PATCH 13/14] fix(ephemeral): tests --- apps/memorizer/main.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/memorizer/main.go b/apps/memorizer/main.go index b9b629e..5f1d0d4 100644 --- a/apps/memorizer/main.go +++ b/apps/memorizer/main.go @@ -442,15 +442,17 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { span.SetAttributes(attribute.String("message.id", id)) - // Apply environment prefix to key (same as SetWithTracing does) - prefixedKey := redisConn.PrefixKey(id) + // Check for ASCII art keys instead of base message key + // The base message key is deleted by writer after processing, but ASCII art keys remain + terminalKey := redisConn.PrefixKey(id + ":ascii:terminal") + htmlKey := redisConn.PrefixKey(id + ":ascii:html") - // Check if the message exists in Redis using the prefixed key - exists, err := redisConn.Client().Exists(ctx, prefixedKey).Result() + // Check if either ASCII art key exists (indicates memorizer processed the message) + exists, err := redisConn.Client().Exists(ctx, terminalKey, htmlKey).Result() if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "Redis query failed") - logger.Error(ctx, "Error checking if message exists", "error", err, "id", id, "prefixed_key", prefixedKey) + logger.Error(ctx, "Error checking if message exists", "error", err, "id", id, "terminal_key", terminalKey, "html_key", htmlKey) http.Error(w, "Error checking message status", http.StatusInternalServerError) return } From 4c9956506a4a8e40fced4e9150c66a9066f14dc6 Mon Sep 17 00:00:00 2001 From: Pedro Pilla Date: Fri, 6 Feb 2026 11:33:41 +0000 Subject: [PATCH 14/14] fix(sec): workflow permissions --- .github/workflows/ephemeral.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ephemeral.yml b/.github/workflows/ephemeral.yml index 48d3ba3..3cd95d9 100644 --- a/.github/workflows/ephemeral.yml +++ b/.github/workflows/ephemeral.yml @@ -4,6 +4,10 @@ on: pull_request: types: [opened, synchronize, reopened, closed] +permissions: + contents: read + pull-requests: write + env: REGISTRY: registry.fullstack.pw VAULT_ADDR: https://vault.fullstack.pw