fix(ui): tolerate legacy npm run start override + add official Helm chart (#6683)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Shuchang Zheng 2026-06-18 11:24:26 -07:00 committed by GitHub
parent b5ad775942
commit 4d0b721966
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 342 additions and 2 deletions

View file

@ -132,3 +132,6 @@ repos:
rev: v0.17.2
hooks:
- id: yamlfmt
# Helm chart templates contain Go templating ({{ ... }}) and are not
# valid standalone YAML, so yamlfmt cannot parse them.
exclude: ^k8s/charts/.*/templates/

View file

@ -0,0 +1,10 @@
# Patterns to ignore when building packages.
.DS_Store
.git/
.gitignore
*.tmproj
*.tmp
*.bak
*.orig
.idea/
.vscode/

View file

@ -0,0 +1,14 @@
apiVersion: v2
name: skyvern-ui
description: Skyvern UI — prebuilt frontend bundle served with runtime env injection
type: application
# Chart version — bump on chart changes.
version: 0.1.0
# Default Skyvern UI image version this chart targets.
appVersion: "v1.0.42"
home: https://www.skyvern.com
sources:
- https://github.com/Skyvern-AI/skyvern
maintainers:
- name: Skyvern
url: https://www.skyvern.com

View file

@ -0,0 +1,28 @@
Skyvern UI ({{ .Values.image.repository }}:{{ .Values.image.tag }}) has been deployed.
IMPORTANT — do NOT override the container command.
This chart deliberately runs the image's own entrypoint, which injects the
VITE_* runtime config into the prebuilt bundle and serves it. Adding a
`command:`/`args:` such as ["npm","run","start"] forces a runtime build that
the production image cannot perform and will crashloop ("tsc: not found").
Access the UI:
{{- if .Values.ingress.enabled }}
http(s)://{{ (index .Values.ingress.hosts 0).host }}
{{- else if eq .Values.service.type "ClusterIP" }}
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "skyvern-ui.fullname" . }} 8080:{{ .Values.service.port }}
then open http://localhost:8080
{{- else }}
Service type {{ .Values.service.type }} — resolve the external address with:
kubectl --namespace {{ .Release.Namespace }} get svc {{ include "skyvern-ui.fullname" . }}
{{- end }}
Backend wiring (set in values.yaml -> config):
VITE_API_BASE_URL = {{ .Values.config.apiBaseUrl }}
VITE_WSS_BASE_URL = {{ .Values.config.wssBaseUrl }}
VITE_ARTIFACT_API_BASE_URL = {{ .Values.config.artifactApiBaseUrl }}
{{- if and (not .Values.apiKey.value) (not .Values.apiKey.existingSecret) }}
WARNING: no API key configured (apiKey.value / apiKey.existingSecret). The UI
will start but cannot authenticate to the backend until one is provided.
{{- end }}

View file

@ -0,0 +1,48 @@
{{/* Expand the name of the chart. */}}
{{- define "skyvern-ui.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/* Create a default fully qualified app name. */}}
{{- define "skyvern-ui.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/* Chart name and version as used by the chart label. */}}
{{- define "skyvern-ui.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/* Common labels */}}
{{- define "skyvern-ui.labels" -}}
helm.sh/chart: {{ include "skyvern-ui.chart" . }}
{{ include "skyvern-ui.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/* Selector labels */}}
{{- define "skyvern-ui.selectorLabels" -}}
app.kubernetes.io/name: {{ include "skyvern-ui.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/* Name of the Secret holding the API key (chart-managed or external). */}}
{{- define "skyvern-ui.apiKeySecretName" -}}
{{- if .Values.apiKey.existingSecret }}
{{- .Values.apiKey.existingSecret }}
{{- else }}
{{- include "skyvern-ui.fullname" . }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,14 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "skyvern-ui.fullname" . }}
labels:
{{- include "skyvern-ui.labels" . | nindent 4 }}
data:
VITE_API_BASE_URL: {{ .Values.config.apiBaseUrl | quote }}
VITE_WSS_BASE_URL: {{ .Values.config.wssBaseUrl | quote }}
VITE_ARTIFACT_API_BASE_URL: {{ .Values.config.artifactApiBaseUrl | quote }}
VITE_BROWSER_STREAMING_MODE: {{ .Values.config.browserStreamingMode | quote }}
VITE_ENABLE_CODE_BLOCK: {{ .Values.config.enableCodeBlock | quote }}
VITE_ENABLE_LOG_ARTIFACTS: {{ .Values.config.enableLogArtifacts | quote }}
VITE_ENABLE_2FA_NOTIFICATIONS: {{ .Values.config.enable2faNotifications | quote }}

View file

@ -0,0 +1,85 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "skyvern-ui.fullname" . }}
labels:
{{- include "skyvern-ui.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "skyvern-ui.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
# Roll pods when the injected runtime config changes so new env applies.
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "skyvern-ui.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: skyvern-ui
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
# NOTE: intentionally NO `command`/`args`. The image entrypoint injects
# VITE_* runtime config into the prebuilt bundle and serves it. Setting
# a command (e.g. ["npm","run","start"]) forces a runtime build the
# production image cannot do and crashloops with "tsc: not found".
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
envFrom:
- configMapRef:
name: {{ include "skyvern-ui.fullname" . }}
env:
- name: VITE_SKYVERN_API_KEY
valueFrom:
secretKeyRef:
name: {{ include "skyvern-ui.apiKeySecretName" . }}
key: {{ .Values.apiKey.existingSecretKey }}
optional: true
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: artifact
containerPort: 9090
protocol: TCP
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 3
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View file

@ -0,0 +1,37 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "skyvern-ui.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "skyvern-ui.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- with .Values.ingress.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,11 @@
{{- if and .Values.apiKey.value (not .Values.apiKey.existingSecret) }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "skyvern-ui.fullname" . }}
labels:
{{- include "skyvern-ui.labels" . | nindent 4 }}
type: Opaque
stringData:
{{ .Values.apiKey.existingSecretKey }}: {{ .Values.apiKey.value | quote }}
{{- end }}

View file

@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "skyvern-ui.fullname" . }}
labels:
{{- include "skyvern-ui.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "skyvern-ui.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
- name: artifact
port: {{ .Values.service.artifactPort }}
targetPort: artifact
protocol: TCP

View file

@ -0,0 +1,71 @@
# Default values for skyvern-ui.
replicaCount: 1
image:
repository: public.ecr.aws/skyvern/skyvern-ui
# Pin to a specific version. Do NOT use "latest" in production: a floating tag
# means a pod restart can silently pull a newly published image, which is
# exactly how a known-good deployment can drift onto a different build.
tag: "v1.0.42"
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# -----------------------------------------------------------------------------
# IMPORTANT: do not set a container `command`/`args` for the UI.
#
# The image ships a PREBUILT static bundle. Its entrypoint injects the VITE_*
# runtime config below into the already-built assets at container start and then
# serves them. Overriding the command (e.g. `npm run start`) forces a runtime
# `tsc && vite build`, which the lean production image cannot perform — the
# container will crashloop with "tsc: not found". Let the image entrypoint run.
# -----------------------------------------------------------------------------
# Runtime config injected into the prebuilt bundle by the image entrypoint.
config:
apiBaseUrl: "http://skyvern:8000/api/v1"
wssBaseUrl: "ws://skyvern:8000/api/v1"
artifactApiBaseUrl: "http://skyvern:9090"
browserStreamingMode: "vnc"
enableCodeBlock: "false"
# NOTE: enableLogArtifacts is honored at runtime only on images that ship the
# log-artifacts placeholder in the bundle. On older images this flag is fixed
# at build time; setting it here is forward-compatible and harmless.
enableLogArtifacts: "false"
enable2faNotifications: "false"
# API key the UI uses to call the backend. Provide ONE of:
# - apiKey.value: inline (templated into a Secret managed by this chart)
# - apiKey.existingSecret (+ existingSecretKey): reference a pre-existing Secret
# If neither is set, the env is left unset (the UI logs a warning and starts).
apiKey:
value: ""
existingSecret: ""
existingSecretKey: "VITE_SKYVERN_API_KEY"
service:
type: ClusterIP
port: 8080
artifactPort: 9090
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: skyvern-ui.local
paths:
- path: /
pathType: Prefix
tls: []
# Sized for SERVING static assets, not building. A runtime build would need
# ~4Gi (Vite/rollup) — another reason not to override the command.
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
podAnnotations: {}
podSecurityContext: {}
securityContext: {}
nodeSelector: {}
tolerations: []
affinity: {}

View file

@ -14,9 +14,9 @@
"artifact-server": "node artifactServer.js",
"run-artifact-server": "node artifactServer.js",
"start-local": "npm run dev & npm run artifact-server",
"serve": "npm run build && node localServer.js",
"serve": "if [ -d dist.template ]; then echo '[skyvern-ui] prebuilt bundle detected; injecting runtime env and serving without rebuild' && exec bash ./entrypoint-skyvernui.sh; else npm run build && node localServer.js; fi",
"test": "vitest run",
"start": "npm run serve & npm run run-artifact-server"
"start": "if [ -d dist.template ]; then echo '[skyvern-ui] prebuilt bundle detected; injecting runtime env and serving without rebuild' && exec bash ./entrypoint-skyvernui.sh; else npm run serve & npm run run-artifact-server; fi"
},
"dependencies": {
"@clerk/clerk-react": "^5.61.8",