diff --git a/.github/workflows/deploy-llink-web.yml b/.github/workflows/deploy-llink-web.yml new file mode 100644 index 0000000..43087b6 --- /dev/null +++ b/.github/workflows/deploy-llink-web.yml @@ -0,0 +1,72 @@ +name: Deploy llink-web + +on: + workflow_dispatch: + inputs: + environment: + description: "Target environment" + required: true + type: choice + options: [dev, prod] + +concurrency: + group: deploy-llink-web-${{ inputs.environment }} + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + defaults: + run: + working-directory: js/desktop + steps: + - uses: actions/checkout@v4 + + - name: Resolve environment settings + id: env + run: | + case "${{ inputs.environment }}" in + dev) + echo "project=flowy-dev-440017" >> "$GITHUB_OUTPUT" + echo "cluster=cluster" >> "$GITHUB_OUTPUT" + ;; + prod) + echo "project=flowy-prod-440017" >> "$GITHUB_OUTPUT" + echo "cluster=prod-cluster" >> "$GITHUB_OUTPUT" + ;; + *) + echo "Unknown environment: ${{ inputs.environment }}" >&2; exit 1 + ;; + esac + echo "region=us-west2" >> "$GITHUB_OUTPUT" + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ inputs.environment == 'prod' && secrets.PROD_GKE_SERVICE_ACCOUNT_KEY || secrets.DEV_GKE_SERVICE_ACCOUNT_KEY }} + + - uses: google-github-actions/setup-gcloud@v2 + + - name: Install gke-gcloud-auth-plugin + run: gcloud components install gke-gcloud-auth-plugin --quiet + + - name: Configure Docker for Artifact Registry + run: gcloud auth configure-docker us-west2-docker.pkg.dev --quiet + + - name: Get GKE credentials + run: | + gcloud container clusters get-credentials "${{ steps.env.outputs.cluster }}" \ + --region "${{ steps.env.outputs.region }}" \ + --project "${{ steps.env.outputs.project }}" + + - name: Install skaffold + run: | + curl -fsSLo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 + sudo install skaffold /usr/local/bin/ + skaffold version + + - name: Deploy + env: + SKAFFOLD_DEFAULT_REPO: us-west2-docker.pkg.dev/${{ steps.env.outputs.project }}/deployments + run: skaffold run -p ${{ inputs.environment }} diff --git a/js/desktop/.dockerignore b/js/desktop/.dockerignore new file mode 100644 index 0000000..15d123e --- /dev/null +++ b/js/desktop/.dockerignore @@ -0,0 +1,8 @@ +node_modules +dist-web +.vite +out +.git +.gitignore +.DS_Store +*.log diff --git a/js/desktop/Dockerfile b/js/desktop/Dockerfile new file mode 100644 index 0000000..29e90f9 --- /dev/null +++ b/js/desktop/Dockerfile @@ -0,0 +1,17 @@ +# Two-stage build for the llink web SPA. APP_ENV is baked into the bundle +# at build time via vite's `define` (see vite.env.ts). +FROM node:22-alpine AS builder + +WORKDIR /app +ARG APP_ENV=prod + +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile + +COPY . . +RUN APP_ENV=$APP_ENV yarn web:build:ci + +FROM nginx:1.27-alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist-web /usr/share/nginx/html +EXPOSE 80 diff --git a/js/desktop/k8s/dev/llink-web.yaml b/js/desktop/k8s/dev/llink-web.yaml new file mode 100644 index 0000000..1f89a3e --- /dev/null +++ b/js/desktop/k8s/dev/llink-web.yaml @@ -0,0 +1,98 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: llink-web +spec: + selector: + matchLabels: + app: llink-web + replicas: 1 + template: + metadata: + labels: + app: llink-web + spec: + serviceAccountName: default-service-account + nodeSelector: + cloud.google.com/gke-spot: "true" + terminationGracePeriodSeconds: 15 + containers: + - name: llink-web + image: "llink-web" + ports: + - containerPort: 80 + resources: + requests: + memory: "64Mi" + cpu: 20m + limits: + memory: "64Mi" + cpu: 50m + readinessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 2 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 10 + periodSeconds: 30 + +--- + +apiVersion: v1 +kind: Service +metadata: + name: llink-web +spec: + selector: + app: llink-web + ports: + - port: 80 + targetPort: 80 + protocol: TCP + +--- + +kind: HTTPRoute +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: llink-web +spec: + parentRefs: + - kind: Gateway + name: external-gateway + hostnames: + - llink.dev.flowy.live + rules: + - backendRefs: + - name: llink-web + port: 80 + +--- + +apiVersion: networking.gke.io/v1 +kind: HealthCheckPolicy +metadata: + name: llink-web-service-health-check +spec: + default: + checkIntervalSec: 15 + timeoutSec: 15 + healthyThreshold: 1 + unhealthyThreshold: 2 + logConfig: + enabled: true + config: + type: HTTP + httpHealthCheck: + portSpecification: USE_FIXED_PORT + port: 80 + requestPath: /health + targetRef: + group: "" + kind: Service + name: llink-web diff --git a/js/desktop/k8s/prod/llink-web.yaml b/js/desktop/k8s/prod/llink-web.yaml new file mode 100644 index 0000000..d22514e --- /dev/null +++ b/js/desktop/k8s/prod/llink-web.yaml @@ -0,0 +1,95 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: llink-web +spec: + selector: + matchLabels: + app: llink-web + replicas: 2 + template: + metadata: + labels: + app: llink-web + spec: + serviceAccountName: default-service-account + containers: + - name: llink-web + image: "llink-web" + ports: + - containerPort: 80 + resources: + requests: + memory: "64Mi" + cpu: 20m + limits: + memory: "64Mi" + cpu: 50m + readinessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 2 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 10 + periodSeconds: 30 + +--- + +apiVersion: v1 +kind: Service +metadata: + name: llink-web +spec: + selector: + app: llink-web + ports: + - port: 80 + targetPort: 80 + protocol: TCP + +--- + +kind: HTTPRoute +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: llink-web +spec: + parentRefs: + - kind: Gateway + name: external-gateway + hostnames: + - llink.flowy.live + rules: + - backendRefs: + - name: llink-web + port: 80 + +--- + +apiVersion: networking.gke.io/v1 +kind: HealthCheckPolicy +metadata: + name: llink-web-service-health-check +spec: + default: + checkIntervalSec: 15 + timeoutSec: 15 + healthyThreshold: 1 + unhealthyThreshold: 2 + logConfig: + enabled: true + config: + type: HTTP + httpHealthCheck: + portSpecification: USE_FIXED_PORT + port: 80 + requestPath: /health + targetRef: + group: "" + kind: Service + name: llink-web diff --git a/js/desktop/nginx.conf b/js/desktop/nginx.conf new file mode 100644 index 0000000..7be8dfe --- /dev/null +++ b/js/desktop/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location = /health { + access_log off; + add_header Content-Type text/plain; + return 200 "ok\n"; + } + + location /assets/ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache"; + } +} diff --git a/js/desktop/package.json b/js/desktop/package.json index 5099676..8ef87c0 100644 --- a/js/desktop/package.json +++ b/js/desktop/package.json @@ -17,6 +17,7 @@ "compile": "npx tsc --noEmit 2>&1 | grep '^src/'", "web:dev": "cross-env APP_ENV=dev vite --config vite.web.config.mts", "web:build": "cross-env APP_ENV=prod vite build --config vite.web.config.mts", + "web:build:ci": "vite build --config vite.web.config.mts", "web:preview": "vite preview --config vite.web.config.mts" }, "keywords": [], diff --git a/js/desktop/skaffold.yaml b/js/desktop/skaffold.yaml new file mode 100644 index 0000000..992b41e --- /dev/null +++ b/js/desktop/skaffold.yaml @@ -0,0 +1,38 @@ +apiVersion: skaffold/v4beta11 +kind: Config +metadata: + name: llink-web +build: + local: {} + tagPolicy: + gitCommit: + variant: AbbrevCommitSha +profiles: + - name: dev + build: + artifacts: + - image: llink-web + context: . + docker: + dockerfile: Dockerfile + buildArgs: + APP_ENV: dev + manifests: + rawYaml: + - k8s/dev/llink-web.yaml + deploy: + kubectl: {} + - name: prod + build: + artifacts: + - image: llink-web + context: . + docker: + dockerfile: Dockerfile + buildArgs: + APP_ENV: prod + manifests: + rawYaml: + - k8s/prod/llink-web.yaml + deploy: + kubectl: {}