diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/.github/workflows/backend-ci.yml b/vercel-preview-signadot-sandoxes-cicd-connection/.github/workflows/backend-ci.yml new file mode 100644 index 0000000..ee4f13b --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/.github/workflows/backend-ci.yml @@ -0,0 +1,101 @@ +# CI/CD Pipeline for building and pushing Docker images +# Builds image on PR and provides image ref for Signadot sandboxes +name: Build and Push Backend Image + +on: + pull_request: + branches: + - main + - master + push: + branches: + - main + - master + +env: + # Docker image configuration + IMAGE_NAME: vercel-signadot-backend + # Docker registry (e.g., docker.io, ghcr.io, gcr.io) + # Must be set as REGISTRY secret in GitHub + REGISTRY: ${{ secrets.REGISTRY }} + IMAGE_TAG: ${{ github.sha }} + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Sanitize branch name for Docker tags + id: branch + run: | + # Sanitize branch name for Docker tag compatibility + # Docker tags cannot start with hyphens, periods, or contain invalid characters + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + + # Remove leading/trailing hyphens and periods, replace invalid chars + SANITIZED_BRANCH=$(echo "${BRANCH_NAME}" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/^[-.]*//' | sed 's/[-.]*$//' | tr '[:upper:]' '[:lower:]') + + # Ensure it doesn't start with hyphen or period + if [[ "${SANITIZED_BRANCH}" =~ ^[-.] ]]; then + SANITIZED_BRANCH="branch-${SANITIZED_BRANCH}" + fi + + # Fallback if empty or too short + if [ -z "${SANITIZED_BRANCH}" ] || [ "${#SANITIZED_BRANCH}" -lt 1 ]; then + SANITIZED_BRANCH="pr" + fi + + # Limit length to avoid exceeding Docker tag limits + SANITIZED_BRANCH="${SANITIZED_BRANCH:0:50}" + + echo "BRANCH=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT + echo "::notice::Original branch: ${BRANCH_NAME}" + echo "::notice::Sanitized branch: ${SANITIZED_BRANCH}" + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,prefix=${{ steps.branch.outputs.BRANCH }}- + type=sha,format=short + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Output image reference + run: | + IMAGE_REF="${{ env.REGISTRY }}/${{ env.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}" + echo "IMAGE_REF=${IMAGE_REF}" >> $GITHUB_OUTPUT + echo "::notice::Docker image built: ${IMAGE_REF}" + echo "::notice::Use this image reference in your Signadot sandbox action" + diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/.github/workflows/vercel-preview.yml b/vercel-preview-signadot-sandoxes-cicd-connection/.github/workflows/vercel-preview.yml new file mode 100644 index 0000000..0c9fc13 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/.github/workflows/vercel-preview.yml @@ -0,0 +1,190 @@ +# Full-Stack Preview Deployment with Vercel + Signadot +# Creates Signadot sandbox for backend and deploys frontend to Vercel with sandbox URL +# Comments both preview URLs on the PR +name: Deploy Full-Stack Preview + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: {} + +jobs: + deploy-preview: + name: Deploy Full-Stack Preview Environment + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + id-token: write + env: + DOCKER_REGISTRY: docker.io + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + IMAGE_NAME: vercel-signadot-backend + + steps: + - name: Checkout Frontend Code + uses: actions/checkout@v4 + + - name: Checkout Backend Code + uses: actions/checkout@v4 + with: + repository: ${{ secrets.BACKEND_REPO }} + path: backend + token: ${{ secrets.GH_PAT }} + ref: main + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Configure kubectl for EKS + run: | + aws eks update-kubeconfig \ + --name ${{ secrets.AWS_EKS_CLUSTER_NAME }} \ + --region ${{ secrets.AWS_REGION }} + kubectl get nodes + + - name: Install Signadot CLI + run: | + curl -sSLf https://raw.githubusercontent.com/signadot/cli/main/scripts/install.sh | sh + echo "$HOME/.signadot/bin" >> "$GITHUB_PATH" + + - name: Configure Signadot credentials + env: + SIGNADOT_ORG: ${{ secrets.SIGNADOT_ORG }} + SIGNADOT_API_KEY: ${{ secrets.SIGNADOT_API_KEY }} + run: | + mkdir -p "$HOME/.signadot" + { + echo "org: ${SIGNADOT_ORG}" + echo "api_key: ${SIGNADOT_API_KEY}" + } > "$HOME/.signadot/config.yaml" + + - name: Verify Signadot Operator + run: | + # Signadot Operator should be pre-installed on the cluster + # This step verifies the operator is running + if ! kubectl get namespace signadot &>/dev/null; then + echo "::error::Signadot namespace not found. Please install the Signadot Operator first." + echo "::notice::Installation guide: https://www.signadot.com/docs/getting-started/installation/install-signadot-operator" + exit 1 + fi + + echo "::notice::Checking Signadot Operator status..." + kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=signadot-operator -n signadot --timeout=30s || { + echo "::warning::Signadot Operator may not be ready, but continuing..." + kubectl get pods -n signadot || true + } + + - name: Get Backend Image Reference + id: backend-image + run: | + DOCKERHUB_USERNAME="${{ env.DOCKERHUB_USERNAME }}" + IMAGE_NAME="${{ env.IMAGE_NAME }}" + IMAGE_TAG="latest" + IMAGE_REF="${{ env.DOCKER_REGISTRY }}/${DOCKERHUB_USERNAME}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "IMAGE_REF=${IMAGE_REF}" >> $GITHUB_OUTPUT + echo "SANDBOX_IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_OUTPUT + + - name: Prepare Sandbox Configuration + run: | + IMAGE_REF="${{ steps.backend-image.outputs.IMAGE_REF }}" + SANDBOX_IMAGE_TAG="${{ steps.backend-image.outputs.SANDBOX_IMAGE_TAG }}" + DOCKERHUB_USERNAME="${{ env.DOCKERHUB_USERNAME }}" + CLUSTER_NAME="${{ secrets.AWS_EKS_CLUSTER_NAME }}" + NAMESPACE="default" + + # Replace image placeholder + sed -i "s|image: docker.io/DOCKERHUB_USERNAME/vercel-signadot-backend:SANDBOX_IMAGE_TAG|image: ${IMAGE_REF}|g" backend/sandbox.yaml + + # Replace cluster name placeholder + sed -i "s|cluster: CLUSTER_NAME|cluster: ${CLUSTER_NAME}|g" backend/sandbox.yaml + + # Replace namespace placeholders + sed -i "s|namespace: NAMESPACE|namespace: ${NAMESPACE}|g" backend/sandbox.yaml + sed -i "s|NAMESPACE|${NAMESPACE}|g" backend/sandbox.yaml + + # Update sandbox name with PR number + if [ -n "${{ github.event.pull_request.number }}" ]; then + PR_NUM="${{ github.event.pull_request.number }}" + SANDBOX_NAME="backend-pr-${PR_NUM}" + # Replace first name: field (sandbox name, not deployment name) + sed -i "0,/^name:/s/^name:.*/name: ${SANDBOX_NAME}/" backend/sandbox.yaml + else + # Replace PR_NUMBER placeholder for non-PR events + sed -i "s|name: backend-pr-PR_NUMBER|name: backend-main-${GITHUB_SHA:0:7}|g" backend/sandbox.yaml + fi + + - name: Create Signadot Sandbox + id: sandbox + run: | + set -e + + if [ -n "${{ github.event.pull_request.number }}" ]; then + PR_NUM="${{ github.event.pull_request.number }}" + SANDBOX_NAME="backend-pr-${PR_NUM}" + else + SANDBOX_NAME="backend-main-${GITHUB_SHA:0:7}" + fi + + SANDBOX_OUTPUT=$(signadot sandbox apply -f backend/sandbox.yaml) + echo "$SANDBOX_OUTPUT" + + # Extract sandbox URL using JSON output + sleep 5 + SANDBOX_JSON=$(signadot sandbox get "${SANDBOX_NAME}" -o json 2>/dev/null || echo "") + if [ -n "$SANDBOX_JSON" ]; then + if command -v jq &> /dev/null; then + SANDBOX_URL=$(echo "$SANDBOX_JSON" | jq -r '.endpoints[]? | select(.name=="backend-api") | .url' | head -n1) + fi + fi + + # Fallback: extract from text output + if [ -z "$SANDBOX_URL" ]; then + SANDBOX_URL=$(echo "$SANDBOX_OUTPUT" | grep -oE 'https://[^ \t]*\.preview\.signadot\.com' | head -n1) + fi + + if [ -z "$SANDBOX_URL" ]; then + echo "::error::Failed to extract sandbox URL" + exit 1 + fi + + echo "sandbox-url=$SANDBOX_URL" >> "$GITHUB_OUTPUT" + echo "sandbox-name=${SANDBOX_NAME}" >> "$GITHUB_OUTPUT" + + - name: Deploy to Vercel with Sandbox URL + id: vercel + uses: amondnet/vercel-action@v25 + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + working-directory: . + vercel-args: '--build-env NEXT_PUBLIC_API_URL=${{ steps.sandbox.outputs.sandbox-url }} --env SIGNADOT_API_KEY=${{ secrets.SIGNADOT_API_KEY }} --force' + env: + NEXT_PUBLIC_API_URL: ${{ steps.sandbox.outputs.sandbox-url }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + - name: Comment Preview URLs on PR + if: github.event.pull_request.number != null + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## 🚀 Full-Stack Preview Deployed! + + Your preview environment is ready for testing: + + - **Frontend Preview:** ${{ steps.vercel.outputs.preview-url }} + - **Backend Sandbox:** ${{ steps.sandbox.outputs.sandbox-url }} + + The frontend is automatically configured to use the backend sandbox URL.` + }); + diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/.gitignore b/vercel-preview-signadot-sandoxes-cicd-connection/.gitignore new file mode 100644 index 0000000..b9f5416 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/.gitignore @@ -0,0 +1,38 @@ +# Dependencies +node_modules/ +bun.lock +package-lock.json +yarn.lock + +# Build outputs +.next/ +out/ +build/ +dist/ + +# Environment files +.env +.env.local +.env*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts + diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/.signadot/sandbox-template.yaml b/vercel-preview-signadot-sandoxes-cicd-connection/.signadot/sandbox-template.yaml new file mode 100644 index 0000000..9f43d0f --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/.signadot/sandbox-template.yaml @@ -0,0 +1,21 @@ +name: backend-pr-PR_NUMBER +spec: + cluster: CLUSTER_NAME + description: Sandbox environment for vercel-signadot-backend + forks: + - forkOf: + kind: Deployment + namespace: default + name: vercel-signadot-backend + customizations: + images: + - image: docker.io/DOCKERHUB_USERNAME/vercel-signadot-backend:SANDBOX_IMAGE_TAG + command: ["node", "server.js"] + env: + - name: PORT + value: "3000" + defaultRouteGroup: + endpoints: + - name: backend-api + target: http://vercel-signadot-backend.default.svc:3000 + diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/README.md b/vercel-preview-signadot-sandoxes-cicd-connection/README.md new file mode 100644 index 0000000..e8a3cbf --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/README.md @@ -0,0 +1,474 @@ +# Tutorial: End-to-End Hot-Reload‑Style Previews with Vercel + Signadot + +**Goal:** Give full‑stack teams a hot‑reload‑style workflow where every backend change is previewable as quickly as frontend changes on Vercel. + +Frontend developers are used to **instant feedback**: save a file, and the browser updates almost immediately. Vercel previews bring a similar experience to pull requests, where every push spins up a fresh frontend URL. But the **backend usually lags behind**: changes wait for a staging deploy, reviewers test against stale APIs, and full‑stack PRs become slow to validate. + +This tutorial shows how to pair **Vercel Preview Deployments** with **Signadot Sandboxes** so that: + +1. Every PR builds a fresh backend image and launches a **PR‑scoped backend sandbox**. +2. The frontend preview is wired to that sandbox via `NEXT_PUBLIC_API_URL` and a secure API proxy. +3. GitHub Actions posts **both URLs** to the PR so reviewers can exercise the feature end‑to‑end. +4. Pushing more commits to the PR updates both sides, giving a **fast-feedback, hot‑reload‑like loop** for backend and frontend together. + +**Time required:** 45–60 minutes +**Repository:** https://github.com/signadot/examples/tree/main/vercel-preview-signadot-sandoxes-cicd-connection + +> **Stack note:** The sample app uses a Next.js frontend and a Node/Express backend, but the workflow applies to any framework that can read build‑time environment variables and expose a stable Kubernetes deployment for Signadot to clone. + +--- + +## 1. Introduction + +### 1.1 Problem: Fast Frontend, Slow Backend + +- Vercel previews give **instant frontend previews per PR**, but they usually point at a **static staging/production backend**. +- When PRs span both frontend and backend, reviewers test UI changes against **outdated APIs**. +- If staging is broken, **every frontend PR looks broken**, even when the frontend code is fine. + +### 1.2 Solution: End‑to‑End Hot Reload with Sandboxes + +- Create a **Signadot sandbox** for each PR that clones the baseline backend deployment and swaps in the PR’s image. +- Inject the sandbox URL into the Next.js build via `NEXT_PUBLIC_API_URL`, and route calls through a server‑side proxy that keeps the Signadot API key private. +- Use GitHub Actions to orchestrate: + - backend image build & push, + - sandbox creation, + - Vercel preview deployment wired to the sandbox URL, + - and a PR comment with both preview links. + +**Result:** a **full‑stack, hot‑reload‑style workflow** where every push to a PR spins up matching frontend and backend changes, with **instant backend previews** tied to the Vercel preview you already use. + +--- + +## 2. Key Concepts + +- **Vercel Preview Deployment (Frontend Hot Reload Experience)** + Every PR and push gets its own frontend URL. Frontend developers already rely on this for a **fast feedback loop** during code review. + +- **Signadot Sandbox (Instant Backend Preview)** + A sandbox is a PR‑scoped backend environment that **clones your baseline Kubernetes deployment** and overrides just the parts you are changing (for example, the backend image tag). This brings a **hot‑reload‑like loop to backend changes**. + +- **Full‑Stack Hot‑Reload‑Style Workflow** + By wiring the Vercel preview to the PR’s sandbox URL: + - Each PR gets a dedicated backend + frontend pair. + - Reviewers see the exact backend behavior that the frontend expects. + - Pushing another commit updates the backend image and redeploys the preview, preserving the **fast‑feedback experience across the whole stack**. + +### 2.1 Architecture Overview + +![Architecture overview showing PR-triggered backend CI, Signadot sandbox creation, Vercel preview deployment, and PR comment with both URLs](./images/architecture_overview.png) + +--- + +## 3. Prerequisites + +| Requirement | Description | +|-------------|-------------| +| GitHub repositories | Separate frontend (`next.js`) and backend (`express`) repos, or a monorepo | +| Vercel account | Project wired to the frontend repo, API token for GitHub Actions | +| Signadot account | Organization name, API key, access to a Kubernetes cluster with the Signadot Operator installed | +| Kubernetes cluster | AWS EKS or GKE Standard (Operator does **not** run on GKE Autopilot) | +| Container registry | Docker Hub / GHCR / GCR for pushing backend images | + +> **Tip:** Ensure the Signadot Operator is installed ahead of time. The frontend workflow only verifies the operator; it does not install it. + +--- + +## 4. Baseline Environment (One‑Time Setup) + +In this section you prepare the **baseline backend environment** that Signadot will clone for each PR. This is a one‑time setup per cluster. + +### 4.1 Install the Signadot Operator + +Install the operator on your target Kubernetes cluster (for example, an EKS cluster used by your backend): + +```bash +kubectl create namespace signadot + +helm repo add signadot https://charts.signadot.com +helm repo update signadot + +kubectl create secret generic cluster-agent \ + --from-literal=token=$SIGNADOT_CLUSTER_TOKEN \ + -n signadot + +helm upgrade --install signadot-operator signadot/operator \ + --namespace signadot \ + --wait +``` + +**Checkpoint: Operator ready** + +- Run: + + ```bash + kubectl get pods -n signadot + ``` + +- **Expected:** At least one `signadot-operator` pod is in `Running` state. + +### 4.2 Deploy the Baseline Backend + +The backend (`backend/`) is a minimal Express server with Kubernetes manifests under `backend/k8s/`. + +1. Update the deployment image to point at your registry: + + `backend/k8s/deployment.yaml` + + ```yaml + containers: + - name: vercel-signadot-backend + image: YOUR_REGISTRY/vercel-signadot-backend:latest + ``` + +2. Apply the manifests to your cluster: + + ```bash + kubectl apply -f backend/k8s/deployment.yaml + kubectl apply -f backend/k8s/service.yaml + kubectl get deployment vercel-signadot-backend -n default + ``` + +**Checkpoint: Baseline backend healthy** + +- Run: + + ```bash + kubectl get deployment vercel-signadot-backend -n default + ``` + +- **Expected:** `AVAILABLE` replicas is at least `1`. + +For a local quick check (optional): + +```bash +cd backend +npm install +npm run dev +curl http://localhost:8080/health +``` + +You should see a JSON response with `status: "healthy"`. + +### 4.3 Sandbox Blueprint (`backend/sandbox.yaml`) + +`backend/sandbox.yaml` defines how Signadot creates a **PR‑scoped backend sandbox** by forking the baseline Deployment: + +```yaml +name: backend-pr-PR_NUMBER +spec: + cluster: CLUSTER_NAME + description: Sandbox environment for vercel-signadot-backend + forks: + - forkOf: + kind: Deployment + namespace: default + name: vercel-signadot-backend + customizations: + images: + - image: docker.io/DOCKERHUB_USERNAME/vercel-signadot-backend:SANDBOX_IMAGE_TAG + command: ["node", "server.js"] + env: + - name: PORT + value: "3000" + defaultRouteGroup: + endpoints: + - name: backend-api + target: http://vercel-signadot-backend.default.svc:3000 +``` + +Key ideas: + +- `name` becomes PR-specific (for example, `backend-pr-42`). +- `forks/forkOf` clones the baseline Deployment and applies minimal overrides (image, command, env). +- The `images` customization injects the PR-specific image tag built by backend CI. +- `defaultRouteGroup` exposes the sandbox endpoint via the service target URL (yielding URLs like `https://backend-api--backend-pr-42.sb.signadot.com`). + +This is the URL we will wire into `NEXT_PUBLIC_API_URL` for a hot‑reload‑style backend experience. + +--- + +## 5. Application Configuration (Frontend + Backend) + +With the baseline environment ready, configure the sample app to take advantage of it. + +### 5.1 Frontend: Hot‑Reload‑Style Backend URL via `NEXT_PUBLIC_API_URL` + +The frontend (Next.js 13+) reads its backend URL from `NEXT_PUBLIC_API_URL` and automatically proxies sandbox calls through a server‑side route. + +**`frontend/src/lib/config/api.ts`** + +```typescript +const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8080'; + +export function isSignadotUrl(url: string = API_URL): boolean { + return url.includes('.preview.signadot.com') || url.includes('.sb.signadot.com'); +} + +export function getApiUrl(endpoint: string): string { + const path = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint; + if (isSignadotUrl()) { + return `/api/proxy/${path}`; + } + const base = API_URL.endsWith('/') ? API_URL.slice(0, -1) : API_URL; + return `${base}/${path}`; +} + +export function getApiHeaders(): Record { + return isSignadotUrl() ? {} : { 'content-type': 'application/json' }; +} +``` + +**`frontend/src/app/api/proxy/[...path]/route.ts`** + +This route keeps the Signadot API key **server‑side** while still giving the frontend an instant backend preview: + +```typescript +export async function GET(request: NextRequest, { params }: Params) { + const url = `${process.env.NEXT_PUBLIC_API_URL}/${params.path.join('/')}`; + + const response = await fetch(url, { + headers: { + 'signadot-api-key': process.env.SIGNADOT_API_KEY ?? '', + accept: 'application/json' + }, + cache: 'no-store' + }); + + return new NextResponse(response.body, { + status: response.status, + headers: response.headers + }); +} +``` + +All sandbox requests go through `/api/proxy/*`, so `SIGNADOT_API_KEY` is never exposed in client‑side bundles. + +**Example component usage** + +```typescript +const res = await fetch(getApiUrl('/health'), { headers: getApiHeaders() }); +const data = await res.json(); +``` + +**Checkpoint: Local full‑stack dev** + +- Start the backend locally on port `8080`. +- Run the frontend with `NEXT_PUBLIC_API_URL=http://localhost:8080`. +- **Expected:** the sample page renders the backend health data successfully. + +--- + +## 6. Integration with Sandboxes (GitHub Workflows) + +Now that the baseline environment and app configuration are in place, wire them together using GitHub Actions. The goal is to automate the **fast‑feedback loop** for every PR. + +### 6.1 Backend CI: Build and Push Images Only + +File: `backend/.github/workflows/ci.yml` + +Purpose: build, tag, and push backend Docker images so Signadot sandboxes can pull **PR‑specific artifacts**. This workflow **does not** deploy to the cluster or install the operator; those are one‑time baseline steps from the previous section. + +Highlights: + +- Logs into `REGISTRY` using `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN`. +- Tags images with a sanitized branch prefix, short SHA, and `latest` on the default branch. +- Outputs a canonical image reference that matches the registry path used in `sandbox.yaml`. + +```yaml +name: Build and Push Backend Image +on: + pull_request: + branches: [main, master] + push: + branches: [main, master] + +jobs: + build-and-push: + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - uses: docker/build-push-action@v5 + with: + push: true + tags: ${{ steps.meta.outputs.tags }} +``` + +Secrets required in the **backend repo**: + +| Secret | Purpose | +|--------|---------| +| `REGISTRY` | Base registry (for example, `docker.io`) | +| `DOCKERHUB_USERNAME` | Registry username | +| `DOCKERHUB_TOKEN` | Registry write token | + +**Checkpoint: Image available for sandboxes** + +- After pushing a commit, check your registry (for example, Docker Hub). +- **Expected:** a new image tag exists for `vercel-signadot-backend`, matching the branch and SHA for your PR. + +### 6.2 Frontend Preview Workflow: Full‑Stack Hot‑Reload‑Style Preview + +File: `frontend/.github/workflows/vercel-preview.yml` + +Triggered on `pull_request`, this workflow creates a **PR‑scoped backend sandbox** and deploys a Vercel preview wired to it: + +1. **Check out** the frontend repo and backend repo (to read `sandbox.yaml`). +2. **Authenticate to AWS** and configure `kubectl` for the cluster where the baseline backend and Signadot operator run. +3. **Verify the Signadot operator** namespace/pods exist (no install). +4. **Rewrite `backend/sandbox.yaml`** with the cluster name and backend image reference. +5. **Create the Signadot sandbox** and extract its `backend-api` URL. +6. **Deploy to Vercel** with `NEXT_PUBLIC_API_URL` set to the sandbox endpoint and `SIGNADOT_API_KEY` as a server‑side secret. +7. **Comment on the PR** with both frontend and backend preview URLs. + +Key excerpt: + +```yaml +- name: Prepare Sandbox Configuration + run: | + IMAGE_REF=${{ steps.backend-image.outputs.IMAGE_REF }} + sed -i "s|cluster:.*|cluster: ${{ secrets.AWS_EKS_CLUSTER_NAME }}|g" backend/sandbox.yaml + sed -i "s|image:.*vercel-signadot-backend.*|image: ${IMAGE_REF}|g" backend/sandbox.yaml + +- name: Create Signadot Sandbox + id: sandbox + run: | + SANDBOX_NAME="backend-pr-${{ github.event.pull_request.number }}" + sed -i "s|backend-pr-PR_NUMBER|${SANDBOX_NAME}|g" backend/sandbox.yaml + signadot sandbox apply -f backend/sandbox.yaml + SANDBOX_URL=$(signadot sandbox get "${SANDBOX_NAME}" -o json | jq -r '.endpoints[] | select(.name=="backend-api").url') + echo "sandbox-url=${SANDBOX_URL}" >> "$GITHUB_OUTPUT" + +- name: Deploy to Vercel + uses: amondnet/vercel-action@v25 + with: + vercel-args: '--build-env NEXT_PUBLIC_API_URL=${{ steps.sandbox.outputs.sandbox-url }} --env SIGNADOT_API_KEY=${{ secrets.SIGNADOT_API_KEY }} --force' +``` + +Secrets required in the **frontend repo**: + +| Category | Secrets | +|----------|---------| +| Vercel | `VERCEL_TOKEN`, `VERCEL_ORG_ID`, `VERCEL_PROJECT_ID` | +| Signadot | `SIGNADOT_API_KEY`, `SIGNADOT_ORG` | +| GitHub | `BACKEND_REPO`, `GH_PAT` (to check out the backend repo) | +| AWS | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_EKS_CLUSTER_NAME` | +| Registry | `DOCKERHUB_USERNAME` (used for image rewrites) | + +> **Note:** This workflow uses the Signadot CLI (`signadot sandbox apply/get`). You can substitute `signadot/sandbox-action` if desired; the logic is equivalent. + +**Checkpoint: Full‑stack preview workflow** + +- Open a PR against the frontend repo. +- In **GitHub Actions**, you should see: + - Backend CI building and pushing an image. + - Frontend preview workflow creating a sandbox and deploying to Vercel. +- **Expected:** the PR gets a comment with a **Frontend Preview URL** and a **Backend Sandbox URL**. + +If you need a visual reference for secrets and API keys, see: + +- ![Locating the Signadot API key](./images/api_key_view_signadot.png) +- ![Vercel secrets configured for the preview workflow](./images/secret_view_frontend.png) + +--- + +## 7. See It Work: Experiencing the Hot‑Reload‑Style Loop + +This section walks through what it feels like to **use** the workflow, not just configure it. + +### 7.1 Create a Test PR + +1. Make a small change in the **frontend** (for example, update the text rendered by the `BackendStatus` component). +2. Optionally, make a small change in the **backend** (for example, add a field to the `/health` response). +3. Push your branch and open a PR against `main` or `master`. + +**Checkpoint: Workflows running** + +- In the frontend repo, you should see the **Deploy Full‑Stack Preview** workflow running. +- In the backend repo, you should see the **Build and Push Backend Image** workflow running. + +### 7.2 Inspect the PR Comment and URLs + +Once the workflows complete: + +1. Scroll to the bottom of the PR to find the comment created by the frontend workflow. +2. It should list: + - **Frontend Preview:** a `vercel.app` URL. + - **Backend Sandbox:** a `*.sb.signadot.com` or `*.preview.signadot.com` URL. + +These two URLs represent your **end‑to‑end instant preview** for that PR. + +### 7.3 Validate the End‑to‑End Flow + +1. Open the **Frontend Preview URL** in your browser. +2. Open DevTools → **Network** tab. +3. Trigger the UI behavior that hits the backend (for example, load the page that reads `/health`). +4. Inspect the network requests: + - Requests from the browser should go to `/api/proxy/…` on the Vercel domain. + - The proxy (on the server side) forwards to a URL like: + - `https://backend-api--backend-pr-.sb.signadot.com/health` + +**Checkpoint: Instant backend preview** + +- **Expected:** the response body reflects the backend version built for this PR, not the shared staging backend. You are now effectively experiencing a **hot‑reload‑style backend workflow** tied to your PR. + +### 7.4 Push Another Change and Feel the Loop + +To feel the **fast‑feedback loop**: + +1. Change something in the backend (for example, modify the `status` message or add a new field to `/health`). +2. Commit and push to the **same PR branch**. +3. Wait for: + - Backend CI to build and push a new image. + - The frontend preview workflow to recreate or update the sandbox with that new image and redeploy the preview. +4. Refresh the **same** Vercel preview (or the new preview URL if it changed). + +**Checkpoint: Hot‑reload‑style full‑stack update** + +- **Expected:** the frontend now shows the updated backend behavior, without manually promoting anything to staging. Every push gives you an **instant backend preview** wired to your frontend preview. + +--- + +## 8. Troubleshooting + +| Issue | Checks | +|-------|--------| +| Sandbox creation fails | `kubectl get deployment vercel-signadot-backend -n default`, `kubectl get pods -n signadot`, confirm image exists in registry | +| API calls from Vercel fail | Ensure `NEXT_PUBLIC_API_URL` appears in build logs, `SIGNADOT_API_KEY` is set (no `NEXT_PUBLIC_`), `/api/proxy/[...path]` exists | +| 401/403 from sandbox | Requests must go through the proxy so the `signadot-api-key` header is added server‑side | +| AWS auth errors | Verify `AWS_EKS_CLUSTER_NAME` and `AWS_REGION` secrets; IAM needs `eks:DescribeCluster` | + +To debug locally: + +```bash +curl https://.vercel.app/api/proxy/health \ + -H "signadot-api-key: $SIGNADOT_API_KEY" +``` + +--- + +## 9. Next Steps + +By pairing Vercel previews with Signadot sandboxes, you: + +- Bring the **hot‑reload experience** to your backend with **instant backend previews** per PR. +- Give reviewers a **fast-feedback, end‑to‑end preview** they can trust for every full‑stack change. +- Automate the whole flow with GitHub Actions, so the loop stays fast without extra manual steps. + +From here you can: + +- Add more services to the same sandbox to preview multi‑service changes. +- Apply the same pattern to other frontend frameworks that support build‑time env vars. +- Extend the workflows with checks (for example, integration tests) that also run against the sandbox. + +### Additional Resources + +- [Signadot Documentation](https://www.signadot.com/docs) +- [Vercel Preview Deployments](https://vercel.com/docs/deployments/preview-deployments) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) + diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/backend/.dockerignore b/vercel-preview-signadot-sandoxes-cicd-connection/backend/.dockerignore new file mode 100644 index 0000000..70e0dae --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/backend/.dockerignore @@ -0,0 +1,56 @@ +# * Dependencies (will be installed in container) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# * Environment files +.env +.env.local +.env.*.local + +# * Logs +*.log +logs/ + +# * Git files +.git/ +.gitignore +.gitattributes + +# * IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# * OS files +.DS_Store +Thumbs.db + +# * Documentation +README.md +*.md + +# * Test files +test/ +tests/ +*.test.js +*.spec.js + +# * Docker files (avoid recursive copying) +Dockerfile* +.dockerignore +docker-compose*.yml + +# * CI/CD files +.github/ +.gitlab-ci.yml +.travis.yml + +# * Build artifacts +dist/ +build/ +*.map + diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/backend/.github/workflows/ci.yml b/vercel-preview-signadot-sandoxes-cicd-connection/backend/.github/workflows/ci.yml new file mode 100644 index 0000000..1c546d9 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/backend/.github/workflows/ci.yml @@ -0,0 +1,167 @@ +name: Build and Push Backend Image + +on: + pull_request: + branches: + - main + - master + push: + branches: + - main + - master + +env: + # Docker image configuration + IMAGE_NAME: vercel-signadot-backend + # Docker registry (e.g., docker.io, ghcr.io, gcr.io) + # Must be set as REGISTRY secret in GitHub + REGISTRY: ${{ secrets.REGISTRY }} + IMAGE_TAG: ${{ github.sha }} + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Sanitize branch name for Docker tags + id: branch + run: | + # Sanitize branch name for Docker tag compatibility + # Docker tags cannot start with hyphens, periods, or contain invalid characters + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + + # Sanitize: remove invalid chars, normalize case, ensure valid start + SANITIZED_BRANCH=$(echo "${BRANCH_NAME}" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/^[-.]*//' | sed 's/[-.]*$//' | tr '[:upper:]' '[:lower:]') + + # Ensure valid format: prefix if starts with invalid char, fallback if empty, limit length + [[ "${SANITIZED_BRANCH}" =~ ^[-.] ]] && SANITIZED_BRANCH="branch-${SANITIZED_BRANCH}" + [ -z "${SANITIZED_BRANCH}" ] && SANITIZED_BRANCH="pr" + SANITIZED_BRANCH="${SANITIZED_BRANCH:0:50}" + + echo "BRANCH=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,prefix=${{ steps.branch.outputs.BRANCH }}- + type=sha,format=short + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + + deploy-to-eks: + name: Deploy to EKS + runs-on: ubuntu-latest + needs: build-and-push + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + permissions: + contents: read + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Configure kubectl for EKS + run: | + aws eks update-kubeconfig \ + --name ${{ secrets.AWS_EKS_CLUSTER_NAME }} \ + --region ${{ secrets.AWS_REGION }} + + - name: Install Signadot Operator (optional) + env: + SIGNADOT_CLUSTER_TOKEN: ${{ secrets.SIGNADOT_CLUSTER_TOKEN }} + run: | + if [ -z "${SIGNADOT_CLUSTER_TOKEN}" ]; then + echo "::notice::SIGNADOT_CLUSTER_TOKEN not provided, skipping operator installation" + echo "::notice::To install Signadot Operator:" + echo "::notice::1. Get cluster token from Signadot dashboard" + echo "::notice::2. Add it as SIGNADOT_CLUSTER_TOKEN secret" + echo "::notice::3. Or install manually: https://www.signadot.com/docs/getting-started/installation/install-signadot-operator" + exit 0 + fi + + echo "::notice::Installing Signadot Operator..." + + if ! command -v helm &> /dev/null; then + echo "::notice::Installing Helm..." + curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + fi + + kubectl create namespace signadot --dry-run=client -o yaml | kubectl apply -f - + + kubectl create secret generic cluster-agent \ + --from-literal=token="${SIGNADOT_CLUSTER_TOKEN}" \ + -n signadot \ + --dry-run=client -o yaml | kubectl apply -f - + + helm repo add signadot https://charts.signadot.com || helm repo update signadot + helm repo update + + helm upgrade --install signadot-operator signadot/operator \ + --namespace signadot \ + --wait \ + --timeout 5m + + echo "::notice::Signadot Operator installation complete" + echo "::notice::Verify cluster connection in Signadot dashboard" + + - name: Update deployment with new image + run: | + IMAGE_REF="${{ env.REGISTRY }}/${{ env.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}" + + sed -i "s|image:.*|image: ${IMAGE_REF}|g" k8s/deployment.yaml + kubectl apply -f k8s/deployment.yaml -n default + kubectl set image deployment/vercel-signadot-backend \ + backend="${IMAGE_REF}" \ + -n default + + - name: Verify deployment + run: | + kubectl rollout status deployment/vercel-signadot-backend -n default --timeout=5m || { + echo "::error::Deployment failed to become ready" + kubectl describe deployment/vercel-signadot-backend -n default + kubectl logs -l app=vercel-signadot-backend -n default --tail=50 + exit 1 + } + diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/backend/Dockerfile b/vercel-preview-signadot-sandoxes-cicd-connection/backend/Dockerfile new file mode 100644 index 0000000..8442867 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/backend/Dockerfile @@ -0,0 +1,43 @@ +# Multi-stage Dockerfile for production optimization +FROM node:20-alpine AS deps +WORKDIR /app + +COPY package.json bun.lock* ./ + +# Using npm for Docker compatibility (project has bun.lock but no package-lock.json) +RUN npm install --omit=dev --ignore-scripts && \ + npm cache clean --force + +FROM node:20-alpine AS builder +WORKDIR /app + +COPY package.json bun.lock* ./ +RUN npm install --ignore-scripts && \ + npm cache clean --force + +COPY server.js ./ +COPY config.js ./ + +FROM node:20-alpine AS runner +WORKDIR /app + +# Create non-root user for security +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nodejs + +COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=nodejs:nodejs /app/server.js ./ +COPY --from=builder --chown=nodejs:nodejs /app/config.js ./ +COPY --from=builder --chown=nodejs:nodejs /app/package.json ./ + +USER nodejs + +EXPOSE 3000 + +ENV NODE_ENV=production +ENV PORT=3000 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +CMD ["node", "server.js"] diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/backend/config.js b/vercel-preview-signadot-sandoxes-cicd-connection/backend/config.js new file mode 100644 index 0000000..47431e0 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/backend/config.js @@ -0,0 +1,9 @@ +/** + * Application configuration loaded from environment variables + * Only PORT is configurable via environment variables + */ +const config = { + port: process.env.PORT || 3000, +}; + +module.exports = config; diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/backend/k8s/deployment.yaml b/vercel-preview-signadot-sandoxes-cicd-connection/backend/k8s/deployment.yaml new file mode 100644 index 0000000..7c40f9a --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/backend/k8s/deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vercel-signadot-backend + namespace: default + labels: + app: vercel-signadot-backend +spec: + replicas: 1 + selector: + matchLabels: + app: vercel-signadot-backend + template: + metadata: + labels: + app: vercel-signadot-backend + spec: + containers: + - name: backend + # Image will be overridden by Signadot sandbox + image: YOUR_REGISTRY/vercel-signadot-backend:latest + imagePullPolicy: Always + # Explicit command required for Signadot sandbox forks + command: ["node", "server.js"] + ports: + - name: http + containerPort: 3000 + protocol: TCP + env: + - name: PORT + value: "3000" + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" + diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/backend/k8s/service.yaml b/vercel-preview-signadot-sandoxes-cicd-connection/backend/k8s/service.yaml new file mode 100644 index 0000000..c0439f8 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/backend/k8s/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: vercel-signadot-backend + namespace: default + labels: + app: vercel-signadot-backend +spec: + type: ClusterIP + ports: + - name: http + port: 3000 + targetPort: http + protocol: TCP + selector: + app: vercel-signadot-backend + diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/backend/package.json b/vercel-preview-signadot-sandoxes-cicd-connection/backend/package.json new file mode 100644 index 0000000..ebc534a --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/backend/package.json @@ -0,0 +1,16 @@ +{ + "name": "vercel-signadot-backend", + "version": "1.0.0", + "description": "Minimal Node.js server with health endpoint", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js" + }, + "dependencies": { + "express": "^4.18.2" + }, + "engines": { + "node": ">=14.0.0" + } +} \ No newline at end of file diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/backend/sandbox.yaml b/vercel-preview-signadot-sandoxes-cicd-connection/backend/sandbox.yaml new file mode 100644 index 0000000..0f51f89 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/backend/sandbox.yaml @@ -0,0 +1,20 @@ +name: backend-pr-PR_NUMBER +spec: + cluster: CLUSTER_NAME + description: Sandbox environment for vercel-signadot-backend + forks: + - forkOf: + kind: Deployment + namespace: default + name: vercel-signadot-backend + customizations: + images: + - image: docker.io/DOCKERHUB_USERNAME/vercel-signadot-backend:SANDBOX_IMAGE_TAG + command: ["node", "server.js"] + env: + - name: PORT + value: "3000" + defaultRouteGroup: + endpoints: + - name: backend-api + target: http://vercel-signadot-backend.default.svc:3000 diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/backend/server.js b/vercel-preview-signadot-sandoxes-cicd-connection/backend/server.js new file mode 100644 index 0000000..d650b12 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/backend/server.js @@ -0,0 +1,16 @@ +const express = require('express'); +const config = require('./config'); + +const app = express(); + +app.get('/health', (req, res) => { + res.status(200).json({ + status: 'healthy', + timestamp: new Date().toISOString(), + port: config.port + }); +}); + +app.listen(config.port, () => { + console.log(`Server is running on port ${config.port}`); +}); diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/.github/workflows/vercel-preview.yml b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/.github/workflows/vercel-preview.yml new file mode 100644 index 0000000..12a4553 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/.github/workflows/vercel-preview.yml @@ -0,0 +1,242 @@ +name: Deploy Full-Stack Preview + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: {} + +jobs: + deploy-preview: + name: Deploy Full-Stack Preview Environment + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + env: + # Use Docker Hub (docker.io) to match backend CI workflow + DOCKER_REGISTRY: docker.io + # Docker Hub username (should match DOCKERHUB_USERNAME in backend CI workflow) + # Must be set as a GitHub secret + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + IMAGE_NAME: vercel-signadot-backend + + steps: + - name: Checkout Frontend Code + uses: actions/checkout@v4 + with: + path: . + fetch-depth: 0 + + - name: Checkout Backend Code + uses: actions/checkout@v4 + with: + repository: ${{ secrets.BACKEND_REPO }} + path: backend + token: ${{ secrets.GH_PAT }} + + # Always checkout from main branch since: + # 1. We're using :latest tag which should be from main + # 2. The sandbox.yaml file should be consistent across branches + # 3. Backend CI workflow handles building images for different branches + ref: main + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Configure kubectl for EKS + run: | + aws eks update-kubeconfig \ + --name ${{ secrets.AWS_EKS_CLUSTER_NAME }} \ + --region ${{ secrets.AWS_REGION }} + + - name: Install Signadot CLI + shell: bash + run: | + set -euo pipefail + curl -sSLf https://raw.githubusercontent.com/signadot/cli/main/scripts/install.sh | sh + echo "$HOME/.signadot/bin" >> "$GITHUB_PATH" + + - name: Configure Signadot credentials + env: + SIGNADOT_ORG: ${{ secrets.SIGNADOT_ORG }} + SIGNADOT_API_KEY: ${{ secrets.SIGNADOT_API_KEY }} + run: | + mkdir -p "$HOME/.signadot" + { + echo "org: ${SIGNADOT_ORG}" + echo "api_key: ${SIGNADOT_API_KEY}" + } > "$HOME/.signadot/config.yaml" + + - name: Check Signadot Operator + run: | + # Verify Signadot operator is installed and ready + # Operator should be installed manually before running this workflow + # See: https://www.signadot.com/docs/getting-started/installation/install-signadot-operator + if ! kubectl get namespace signadot &>/dev/null; then + echo "::error::Signadot namespace not found. Install operator first." + exit 1 + fi + # Check if operator pods exist and wait for them to be ready + POD_COUNT=$(kubectl get pods -l app.kubernetes.io/name=signadot-operator -n signadot --no-headers 2>/dev/null | wc -l) + if [ "$POD_COUNT" -gt 0 ]; then + kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=signadot-operator -n signadot --timeout=300s + else + echo "::warning::Signadot operator pods not found. Operator may need to be installed." + fi + + - name: Get Backend Image Reference + id: backend-image + run: | + # Construct image reference from Docker Hub + # Backend CI workflow pushes to: docker.io/DOCKERHUB_USERNAME/IMAGE_NAME:SHA + # Format: docker.io/USERNAME/IMAGE_NAME:TAG + + DOCKERHUB_USERNAME="${{ env.DOCKERHUB_USERNAME }}" + IMAGE_NAME="${{ env.IMAGE_NAME }}" + # Use :latest tag for PR previews + # Backend CI workflow also tags with SHA, but :latest is updated on main branch + IMAGE_TAG="latest" + + IMAGE_REF="${{ env.DOCKER_REGISTRY }}/${DOCKERHUB_USERNAME}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "IMAGE_REF=${IMAGE_REF}" >> $GITHUB_OUTPUT + + - name: Prepare Sandbox Configuration + run: | + set -e + # Update sandbox.yaml with actual values + # Replace placeholders (both ${} and CAPITALIZED formats) with actual values + IMAGE_REF="${{ steps.backend-image.outputs.IMAGE_REF }}" + CLUSTER_NAME="${{ secrets.AWS_EKS_CLUSTER_NAME }}" + + # Replace cluster name + sed -i "s|cluster:.*|cluster: ${CLUSTER_NAME}|g" backend/sandbox.yaml + + # Replace image - use the full IMAGE_REF to replace the entire image line + # This handles both ${} and CAPITALIZED placeholder formats + # Match any image line containing vercel-signadot-backend and replace with full IMAGE_REF + sed -i "s|image:.*vercel-signadot-backend.*|image: ${IMAGE_REF}|g" backend/sandbox.yaml + + # Verify the image replacement worked + if ! grep -q "image: ${IMAGE_REF}" backend/sandbox.yaml; then + echo "::error::Image replacement failed" + echo "::error::Expected image: ${IMAGE_REF}" + echo "::error::Current sandbox.yaml:" + cat backend/sandbox.yaml + exit 1 + fi + + if grep -qE '\$\{[^}]*\}|CLUSTER_NAME|DOCKERHUB_USERNAME' backend/sandbox.yaml; then + echo "::error::Critical placeholders still found in sandbox.yaml" + exit 1 + fi + + - name: Create Signadot Sandbox + id: sandbox + run: | + set -e + + # Generate PR-specific sandbox name + # Format: backend-pr-{PR_NUMBER} for PRs, or backend-main-{SHA} for main branch + if [ -n "${{ github.event.pull_request.number }}" ]; then + PR_NUM="${{ github.event.pull_request.number }}" + SANDBOX_NAME="backend-pr-${PR_NUM}" + else + # Fallback for non-PR events (e.g., workflow_dispatch) + PR_NUM="main-${GITHUB_SHA:0:7}" + SANDBOX_NAME="backend-${PR_NUM}" + fi + + # Update sandbox.yaml with PR-specific name + sed -i "s|backend-pr-PR_NUMBER|${SANDBOX_NAME}|g" backend/sandbox.yaml + + # Verify deployment name wasn't changed + if ! grep -q "name: vercel-signadot-backend" backend/sandbox.yaml; then + echo "::error::Deployment name verification failed" + exit 1 + fi + + SANDBOX_OUTPUT=$(signadot sandbox apply -f backend/sandbox.yaml) + echo "sandbox-name=${SANDBOX_NAME}" >> "$GITHUB_OUTPUT" + + sleep 5 + + ENDPOINT_NAME="backend-api" + SANDBOX_URL="" + SANDBOX_JSON=$(signadot sandbox get "${SANDBOX_NAME}" -o json 2>/dev/null) + if command -v jq &> /dev/null; then + SANDBOX_URL=$(echo "$SANDBOX_JSON" | jq -r ".endpoints[]? | select(.name == \"${ENDPOINT_NAME}\") | .url" | head -n1) + else + SANDBOX_URL=$(echo "$SANDBOX_JSON" | grep -A 5 "\"name\":\"${ENDPOINT_NAME}\"" | grep -oE '"url":"https://[^"]*"' | head -n1 | sed 's/"url":"\(.*\)"/\1/') + fi + + # Fallback to text output if JSON parsing failed + if [ -z "$SANDBOX_URL" ]; then + SANDBOX_GET_OUTPUT=$(signadot sandbox get "${SANDBOX_NAME}" 2>/dev/null) + SANDBOX_URL=$(echo "$SANDBOX_GET_OUTPUT" | grep -E "^[ \t]*${ENDPOINT_NAME}[ \t]+" | awk '{print $NF}' | head -n1) + fi + + if [ -z "$SANDBOX_URL" ]; then + echo "::error::Failed to extract sandbox URL" + exit 1 + fi + + echo "sandbox-url=$SANDBOX_URL" >> "$GITHUB_OUTPUT" + + - name: Deploy to Vercel with Sandbox URL + id: vercel + uses: amondnet/vercel-action@v25 + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + working-directory: . + # Removed alias-domains to avoid timing issues with alias creation + # The preview URL is already unique per deployment, which is what we want for PR previews + # Use --build-env to pass build-time environment variables + # This is required for NEXT_PUBLIC_* variables in Next.js + # NOTE: SIGNADOT_API_KEY is passed via --env (runtime, server-side only) + # The API proxy route uses SIGNADOT_API_KEY (without NEXT_PUBLIC_ prefix) + # This keeps the API key server-side and never exposes it to the client + vercel-args: '--build-env NEXT_PUBLIC_API_URL=${{ steps.sandbox.outputs.sandbox-url }} --env SIGNADOT_API_KEY=${{ secrets.SIGNADOT_API_KEY }} --force' + env: + # Also pass as environment variable to the action + # This ensures it's available during the deployment process + NEXT_PUBLIC_API_URL: ${{ steps.sandbox.outputs.sandbox-url }} + # The amondnet/vercel-action may set VERCEL_ORG_ID internally from vercel-org-id input + # but doesn't set VERCEL_PROJECT_ID. The Vercel CLI requires both to be set together. + # Setting both explicitly as environment variables ensures the CLI has the required values + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + - name: Comment Preview URLs on PR + if: github.event.pull_request.number != null + uses: actions/github-script@v7 + with: + script: | + const frontendUrl = '${{ steps.vercel.outputs.preview-url }}'; + const backendUrl = '${{ steps.sandbox.outputs.sandbox-url }}'; + + const comment = `## 🚀 Full-Stack Preview Deployed! + + Your preview environment is ready for testing: + + - **Frontend Preview:** ${frontendUrl} + - **Backend Sandbox:** ${backendUrl} + + The frontend is automatically configured to use the backend sandbox URL. + + ### Testing + - Open the frontend preview in your browser + - Check the Network tab (F12) to see requests hitting the sandbox URL + - All API calls will route to your PR-specific backend instance`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/.gitignore b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/.gitignore new file mode 100644 index 0000000..bfe57c0 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/eslint.config.mjs b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/next.config.ts b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/package.json b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/package.json new file mode 100644 index 0000000..0c735f6 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/package.json @@ -0,0 +1,26 @@ +{ + "name": "my-app", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "next": "16.0.3", + "react": "19.2.0", + "react-dom": "19.2.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.0.3", + "tailwindcss": "^4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/postcss.config.mjs b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/public/next.svg b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/public/vercel.svg b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/api/proxy/[...path]/route.ts b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/api/proxy/[...path]/route.ts new file mode 100644 index 0000000..6144e71 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/api/proxy/[...path]/route.ts @@ -0,0 +1,127 @@ +import { NextRequest, NextResponse } from 'next/server'; + +/** + * Backend API base URL + * Set by GitHub Actions workflow from Signadot sandbox URL in preview environments + */ +const BACKEND_API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; + +/** + * Signadot API key for authenticating requests to Signadot preview URLs + * Server-side only - set as SIGNADOT_API_KEY in Vercel (without NEXT_PUBLIC_ prefix) + */ +const SIGNADOT_API_KEY = process.env.SIGNADOT_API_KEY || ''; + +function isSignadotUrl(url: string = BACKEND_API_URL): boolean { + return url.includes('.preview.signadot.com') || url.includes('.sb.signadot.com'); +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + const resolvedParams = await params; + return handleProxyRequest(request, resolvedParams, 'GET'); +} + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + const resolvedParams = await params; + return handleProxyRequest(request, resolvedParams, 'POST'); +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + const resolvedParams = await params; + return handleProxyRequest(request, resolvedParams, 'PUT'); +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + const resolvedParams = await params; + return handleProxyRequest(request, resolvedParams, 'DELETE'); +} + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + const resolvedParams = await params; + return handleProxyRequest(request, resolvedParams, 'PATCH'); +} + +async function handleProxyRequest( + request: NextRequest, + params: { path: string[] }, + method: string +) { + try { + const pathSegments = params.path || []; + const backendPath = pathSegments.length > 0 + ? `/${pathSegments.join('/')}` + : '/'; + + const backendUrl = `${BACKEND_API_URL}${backendPath}`; + const searchParams = request.nextUrl.searchParams.toString(); + const fullBackendUrl = searchParams + ? `${backendUrl}?${searchParams}` + : backendUrl; + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (isSignadotUrl() && SIGNADOT_API_KEY) { + headers['signadot-api-key'] = SIGNADOT_API_KEY; + } + + let body: string | undefined; + if (['POST', 'PUT', 'PATCH'].includes(method)) { + try { + body = await request.text(); + } catch (error) { + console.error('Failed to parse request body:', error); + } + } + + const backendResponse = await fetch(fullBackendUrl, { + method, + headers, + body, + }); + + const responseData = await backendResponse.text(); + + let jsonData; + try { + jsonData = JSON.parse(responseData); + } catch { + jsonData = responseData; + } + + return NextResponse.json(jsonData, { + status: backendResponse.status, + headers: { + 'Content-Type': 'application/json', + ...(backendResponse.headers.get('Access-Control-Allow-Origin') && { + 'Access-Control-Allow-Origin': backendResponse.headers.get('Access-Control-Allow-Origin')!, + }), + }, + }); + } catch (error) { + console.error('Proxy request failed:', error); + return NextResponse.json( + { + error: 'Failed to proxy request to backend', + message: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/favicon.ico b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/favicon.ico differ diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/globals.css b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/globals.css new file mode 100644 index 0000000..a2dc41e --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/layout.tsx b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/page.tsx b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/page.tsx new file mode 100644 index 0000000..487ac3f --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/app/page.tsx @@ -0,0 +1,69 @@ +import Image from "next/image"; +import BackendStatus from "@/components/BackendStatus"; + +export default function Home() { + return ( +
+
+
+ Next.js logo + +
+
+

+ To get started, edit the page.tsx file. +

+

+ Looking for a starting point or more instructions? Head over to{" "} + + Templates + {" "} + or the{" "} + + Learning + {" "} + center. +

+
+ +
+
+ ); +} diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/components/BackendStatus.tsx b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/components/BackendStatus.tsx new file mode 100644 index 0000000..b32070f --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/components/BackendStatus.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { getApiUrl, getApiHeaders } from '@/lib/config/api'; + +/** + * Backend status indicator component + * Polls health endpoint every 30 seconds + */ +export default function BackendStatus() { + const [status, setStatus] = useState<'checking' | 'online' | 'offline'>('checking'); + const [lastChecked, setLastChecked] = useState(null); + const [error, setError] = useState(null); + + const checkBackendStatus = async () => { + try { + setStatus('checking'); + setError(null); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + const response = await fetch(getApiUrl('/health'), { + method: 'GET', + headers: getApiHeaders(), + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (response.ok) { + const data = await response.json(); + if (data.status === 'healthy') { + setStatus('online'); + setLastChecked(new Date()); + } else { + setStatus('offline'); + setError('Backend returned unhealthy status'); + } + } else { + setStatus('offline'); + setError(`HTTP ${response.status}: ${response.statusText}`); + } + } catch (err) { + setStatus('offline'); + if (err instanceof Error) { + if (err.name === 'AbortError') { + setError('Request timeout'); + } else { + setError(err.message); + } + } else { + setError('Failed to connect to backend'); + } + } + }; + + useEffect(() => { + checkBackendStatus(); + const interval = setInterval(() => { + checkBackendStatus(); + }, 30000); + + return () => clearInterval(interval); + }, []); + + const getStatusColor = () => { + switch (status) { + case 'online': + return 'bg-green-500'; + case 'offline': + return 'bg-red-500'; + case 'checking': + return 'bg-yellow-500'; + default: + return 'bg-gray-500'; + } + }; + + const getStatusText = () => { + switch (status) { + case 'online': + return 'Backend Online'; + case 'offline': + return 'Backend Offline'; + case 'checking': + return 'Checking...'; + default: + return 'Unknown'; + } + }; + + const formatLastChecked = () => { + if (!lastChecked) return ''; + const now = new Date(); + const diff = Math.floor((now.getTime() - lastChecked.getTime()) / 1000); + if (diff < 60) return `${diff}s ago`; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + return lastChecked.toLocaleTimeString(); + }; + + return ( +
+
+
+ + {getStatusText()} + +
+ {lastChecked && status === 'online' && ( + + {formatLastChecked()} + + )} + {error && status === 'offline' && ( + + {error.length > 30 ? `${error.substring(0, 30)}...` : error} + + )} + +
+ ); +} diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/lib/config/api.ts b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/lib/config/api.ts new file mode 100644 index 0000000..ad65c52 --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/src/lib/config/api.ts @@ -0,0 +1,41 @@ +/** + * Backend API base URL + * Set by GitHub Actions workflow from Signadot sandbox URL in preview environments + */ +export const API_URL = + process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; + +/** + * Checks if the API URL is a Signadot preview URL + */ +export function isSignadotUrl(url: string = API_URL): boolean { + return url.includes('.preview.signadot.com') || url.includes('.sb.signadot.com'); +} + +/** + * Creates a full API endpoint URL + * Routes through Next.js API proxy for Signadot URLs to keep API key server-side + * Direct requests for non-Signadot URLs (production/local) + */ +export function getApiUrl(endpoint: string): string { + const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint; + + if (isSignadotUrl()) { + // Proxy route adds Signadot API key header server-side + return `/api/proxy/${cleanEndpoint}`; + } + + const baseUrl = API_URL.endsWith('/') ? API_URL.slice(0, -1) : API_URL; + return `${baseUrl}/${cleanEndpoint}`; +} + +/** + * Gets the headers to include in API requests + * Signadot API key is handled server-side by proxy + */ +export function getApiHeaders(additionalHeaders: Record = {}): HeadersInit { + return { + 'Content-Type': 'application/json', + ...additionalHeaders, + }; +} diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/frontend/tsconfig.json b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/tsconfig.json new file mode 100644 index 0000000..cf9c65d --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/frontend/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/images/api_key_view_signadot.png b/vercel-preview-signadot-sandoxes-cicd-connection/images/api_key_view_signadot.png new file mode 100644 index 0000000..72c4f12 Binary files /dev/null and b/vercel-preview-signadot-sandoxes-cicd-connection/images/api_key_view_signadot.png differ diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/images/architecture_overview.png b/vercel-preview-signadot-sandoxes-cicd-connection/images/architecture_overview.png new file mode 100644 index 0000000..9a2e190 Binary files /dev/null and b/vercel-preview-signadot-sandoxes-cicd-connection/images/architecture_overview.png differ diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/images/frontend_page_online_ping.png b/vercel-preview-signadot-sandoxes-cicd-connection/images/frontend_page_online_ping.png new file mode 100644 index 0000000..49b80e4 Binary files /dev/null and b/vercel-preview-signadot-sandoxes-cicd-connection/images/frontend_page_online_ping.png differ diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/images/github_action_comment.png b/vercel-preview-signadot-sandoxes-cicd-connection/images/github_action_comment.png new file mode 100644 index 0000000..5bb948e Binary files /dev/null and b/vercel-preview-signadot-sandoxes-cicd-connection/images/github_action_comment.png differ diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/images/secret_view_frontend.png b/vercel-preview-signadot-sandoxes-cicd-connection/images/secret_view_frontend.png new file mode 100644 index 0000000..644d73f Binary files /dev/null and b/vercel-preview-signadot-sandoxes-cicd-connection/images/secret_view_frontend.png differ diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/images/successful_backend_workflow.png b/vercel-preview-signadot-sandoxes-cicd-connection/images/successful_backend_workflow.png new file mode 100644 index 0000000..6a0209e Binary files /dev/null and b/vercel-preview-signadot-sandoxes-cicd-connection/images/successful_backend_workflow.png differ diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/images/successful_frontend_job.png b/vercel-preview-signadot-sandoxes-cicd-connection/images/successful_frontend_job.png new file mode 100644 index 0000000..3d79b68 Binary files /dev/null and b/vercel-preview-signadot-sandoxes-cicd-connection/images/successful_frontend_job.png differ diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/k8s/deployment.yaml b/vercel-preview-signadot-sandoxes-cicd-connection/k8s/deployment.yaml new file mode 100644 index 0000000..a6fb7fb --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/k8s/deployment.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vercel-signadot-backend + namespace: default + labels: + app: vercel-signadot-backend +spec: + replicas: 1 + selector: + matchLabels: + app: vercel-signadot-backend + template: + metadata: + labels: + app: vercel-signadot-backend + spec: + containers: + - name: backend + # Image will be overridden by Signadot sandbox + image: YOUR_REGISTRY/vercel-signadot-backend:latest + imagePullPolicy: Always + # Explicit command required for Signadot sandbox forks + command: ["node", "server.js"] + ports: + - name: http + containerPort: 3000 + protocol: TCP + env: + - name: PORT + value: "3000" + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" diff --git a/vercel-preview-signadot-sandoxes-cicd-connection/k8s/service.yaml b/vercel-preview-signadot-sandoxes-cicd-connection/k8s/service.yaml new file mode 100644 index 0000000..5bc61bd --- /dev/null +++ b/vercel-preview-signadot-sandoxes-cicd-connection/k8s/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: vercel-signadot-backend + namespace: default + labels: + app: vercel-signadot-backend +spec: + type: ClusterIP + ports: + - name: http + port: 3000 + targetPort: http + protocol: TCP + selector: + app: vercel-signadot-backend