feat: merge service (#14384)

This commit is contained in:
DarkSky
2026-02-07 04:52:25 +08:00
committed by GitHub
parent a0cf5681c4
commit 1a2410f541
49 changed files with 966 additions and 924 deletions
+36 -48
View File
@@ -29,43 +29,26 @@ const isInternal = buildType === 'internal';
const replicaConfig = {
stable: {
web: 2,
front: Number(process.env.PRODUCTION_FRONT_REPLICA) || 2,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 2,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 2,
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2,
},
beta: {
web: 1,
front: Number(process.env.BETA_FRONT_REPLICA) || 1,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1,
sync: Number(process.env.BETA_SYNC_REPLICA) || 1,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 1,
doc: Number(process.env.BETA_DOC_REPLICA) || 1,
},
canary: {
web: 1,
graphql: 1,
sync: 1,
renderer: 1,
doc: 1,
},
canary: { front: 1, graphql: 1, doc: 1 },
};
const cpuConfig = {
beta: {
web: '300m',
graphql: '1',
sync: '1',
doc: '1',
renderer: '300m',
},
canary: {
web: '300m',
graphql: '1',
sync: '1',
doc: '1',
renderer: '300m',
},
beta: { front: '2', graphql: '1', doc: '1' },
canary: { front: '500m', graphql: '1', doc: '500m' },
};
const memoryConfig = {
beta: { front: '1Gi', graphql: '1Gi', doc: '1Gi' },
canary: { front: '512Mi', graphql: '512Mi', doc: '512Mi' },
};
const createHelmCommand = ({ isDryRun }) => {
@@ -90,16 +73,16 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string global.indexer.apiKey="${AFFINE_INDEXER_SEARCH_API_KEY}"`,
];
const serviceAnnotations = [
`--set-json web.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json front.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json graphql.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json sync.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json doc.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
].concat(
isProduction || isBeta || isInternal
? [
`--set-json web.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json front.services.web.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json front.services.sync.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json front.services.renderer.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json graphql.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json sync.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json cloud-sql-proxy.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }"`,
`--set-json cloud-sql-proxy.nodeSelector="{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }"`,
]
@@ -107,14 +90,22 @@ const createHelmCommand = ({ isDryRun }) => {
);
const cpu = cpuConfig[buildType];
const resources = cpu
? [
`--set web.resources.requests.cpu="${cpu.web}"`,
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
`--set sync.resources.requests.cpu="${cpu.sync}"`,
`--set doc.resources.requests.cpu="${cpu.doc}"`,
]
: [];
const memory = memoryConfig[buildType];
let resources = [];
if (cpu) {
resources = resources.concat([
`--set front.resources.requests.cpu="${cpu.front}"`,
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
`--set doc.resources.requests.cpu="${cpu.doc}"`,
]);
}
if (memory) {
resources = resources.concat([
`--set front.resources.requests.memory="${memory.front}"`,
`--set graphql.resources.requests.memory="${memory.graphql}"`,
`--set doc.resources.requests.memory="${memory.doc}"`,
]);
}
const replica = replicaConfig[buildType] || replicaConfig.canary;
@@ -130,6 +121,7 @@ const createHelmCommand = ({ isDryRun }) => {
.split(',')
.map(host => host.trim())
.filter(host => host);
const primaryHost = hosts[0] || '0.0.0.0';
const deployCommand = [
`helm upgrade --install affine .github/helm/affine`,
`--namespace ${namespace}`,
@@ -144,18 +136,14 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string global.version="${APP_VERSION}"`,
...redisAndPostgres,
...indexerOptions,
`--set web.replicaCount=${replica.web}`,
`--set-string web.image.tag="${imageTag}"`,
`--set front.replicaCount=${replica.front}`,
`--set-string front.image.tag="${imageTag}"`,
`--set-string front.app.host="${primaryHost}"`,
`--set graphql.replicaCount=${replica.graphql}`,
`--set-string graphql.image.tag="${imageTag}"`,
`--set graphql.app.host=${hosts[0]}`,
`--set sync.replicaCount=${replica.sync}`,
`--set-string sync.image.tag="${imageTag}"`,
`--set-string renderer.image.tag="${imageTag}"`,
`--set renderer.app.host=${hosts[0]}`,
`--set renderer.replicaCount=${replica.renderer}`,
`--set-string graphql.app.host="${primaryHost}"`,
`--set-string doc.image.tag="${imageTag}"`,
`--set doc.app.host=${hosts[0]}`,
`--set-string doc.app.host="${primaryHost}"`,
`--set doc.replicaCount=${replica.doc}`,
...serviceAnnotations,
...resources,
-13
View File
@@ -1,13 +0,0 @@
FROM openresty/openresty:1.27.1.1-0-buster
WORKDIR /app
COPY ./packages/frontend/apps/web/dist ./dist
COPY ./packages/frontend/admin/dist ./admin
COPY ./packages/frontend/apps/mobile/dist ./mobile
COPY ./.github/deployment/front/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY ./.github/deployment/front/affine.nginx.conf /etc/nginx/conf.d/affine.nginx.conf
RUN mkdir -p /var/log/nginx && \
rm /etc/nginx/conf.d/default.conf
EXPOSE 8080
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]
@@ -1,42 +0,0 @@
server {
listen 8080;
location /admin {
root /app/;
index index.html;
try_files $uri/index.html $uri/ $uri /admin/index.html;
}
set $app_root_path /app/dist/;
set $mobile_root /app/dist/;
set_by_lua $affine_env 'return os.getenv("AFFINE_ENV")';
if ($affine_env = "dev") {
set $mobile_root /app/mobile/;
}
# https://gist.github.com/mariusom/6683dc52b1cad1a1f372e908bdb209d0
if ($http_user_agent ~* "(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino") {
set $app_root_path $mobile_root;
}
if ($http_user_agent ~* "^(1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-)") {
set $app_root_path $mobile_root;
}
location ~ ^/(_plugin|assets|imgs|js|plugins|static)/ {
root $app_root_path;
try_files $uri $uri/ =404;
}
location / {
root $app_root_path;
index index.html;
try_files $uri $uri/ /index.html;
add_header Cache-Control "private, no-cache, no-store, max-age=0, must-revalidate";
}
error_page 404 /404.html;
location = /404.html {
internal;
}
}
-15
View File
@@ -1,15 +0,0 @@
worker_processes 4;
error_log /var/log/nginx/error.log warn;
pcre_jit on;
env AFFINE_ENV;
events {
worker_connections 1024;
}
http {
include mime.types;
log_format main '$remote_addr [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
}
@@ -1,6 +1,6 @@
apiVersion: v2
name: sync
description: AFFiNE Sync Server
name: front
description: AFFiNE front server
type: application
version: 0.0.0
appVersion: "0.26.0"
@@ -1,15 +1,15 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "renderer.fullname" . }})
{{- if contains "NodePort" .Values.services.sync.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ .Values.services.sync.name }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
{{- else if contains "LoadBalancer" .Values.services.sync.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "renderer.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "renderer.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "renderer.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ .Values.services.sync.name }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ .Values.services.sync.name }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.services.sync.port }}
{{- else if contains "ClusterIP" .Values.services.sync.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "front.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
@@ -1,7 +1,7 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "sync.name" -}}
{{- define "front.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
@@ -10,7 +10,7 @@ Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "sync.fullname" -}}
{{- define "front.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
@@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name.
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "sync.chart" -}}
{{- define "front.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "sync.labels" -}}
helm.sh/chart: {{ include "sync.chart" . }}
{{ include "sync.selectorLabels" . }}
{{- define "front.labels" -}}
helm.sh/chart: {{ include "front.chart" . }}
{{ include "front.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
@@ -46,17 +46,17 @@ monitoring: enabled
{{/*
Selector labels
*/}}
{{- define "sync.selectorLabels" -}}
app.kubernetes.io/name: {{ include "sync.name" . }}
{{- define "front.selectorLabels" -}}
app.kubernetes.io/name: {{ include "front.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "sync.serviceAccountName" -}}
{{- define "front.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "sync.fullname" .) .Values.serviceAccount.name }}
{{- default (include "front.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
@@ -0,0 +1,120 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "front.fullname" . }}
labels:
{{- include "front.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "front.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "front.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "front.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NODE_OPTIONS
value: "{{ .Values.nodeOptions }}"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "{{ .Values.global.deployment.type }}"
- name: DEPLOYMENT_PLATFORM
value: "{{ .Values.global.deployment.platform }}"
- name: SERVER_FLAVOR
value: "front"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_ENABLED
value: "true"
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT
value: "{{ .Values.app.port }}"
- name: AFFINE_SERVER_SUB_PATH
value: "{{ .Values.app.path }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: AFFINE_SERVER_HTTPS
value: "{{ .Values.app.https }}"
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
ports:
- name: http
containerPort: {{ .Values.app.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
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 }}
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.services.renderer.name }}
labels:
{{- include "front.labels" . | nindent 4 }}
{{- with .Values.services.renderer.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.services.renderer.type }}
ports:
- port: {{ .Values.services.renderer.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "front.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.services.sync.name }}
labels:
{{- include "front.labels" . | nindent 4 }}
{{- with .Values.services.sync.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.services.sync.type }}
ports:
- port: {{ .Values.services.sync.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "front.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.services.web.name }}
labels:
{{- include "front.labels" . | nindent 4 }}
{{- with .Values.services.web.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.services.web.type }}
ports:
- port: {{ .Values.services.web.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "front.selectorLabels" . | nindent 4 }}
@@ -2,9 +2,9 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "web.serviceAccountName" . }}
name: {{ include "front.serviceAccountName" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
{{- include "front.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
@@ -1,9 +1,9 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "web.fullname" . }}-test-connection"
name: "{{ include "front.fullname" . }}-test-connection"
labels:
{{- include "web.labels" . | nindent 4 }}
{{- include "front.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
@@ -11,5 +11,5 @@ spec:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "web.fullname" . }}:{{ .Values.service.port }}']
args: ['{{ .Values.services.sync.name }}:{{ .Values.services.sync.port }}']
restartPolicy: Never
@@ -0,0 +1,60 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
nodeOptions: '--max-old-space-size=3072'
app:
# AFFINE_SERVER_PORT
port: 3010
# AFFINE_SERVER_SUB_PATH
path: ''
# AFFINE_SERVER_HOST
host: '0.0.0.0'
https: true
serviceAccount:
create: true
annotations: {}
name: 'affine-front'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
requests:
cpu: '2'
memory: 4Gi
probe:
initialDelaySeconds: 20
services:
sync:
name: affine-sync
type: ClusterIP
port: 3010
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
renderer:
name: affine-renderer
type: ClusterIP
port: 3000
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
web:
name: affine-web
type: ClusterIP
port: 8080
annotations: {}
nodeSelector: {}
tolerations: []
affinity: {}
@@ -1,11 +0,0 @@
apiVersion: v2
name: renderer
description: AFFiNE renderer server
type: application
version: 0.0.0
appVersion: "0.26.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
repository: "file://../gcloud-sql-proxy"
condition: .global.database.gcloud.enabled
@@ -1,63 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "renderer.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "renderer.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 }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "renderer.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "renderer.labels" -}}
helm.sh/chart: {{ include "renderer.chart" . }}
{{ include "renderer.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
monitoring: enabled
{{- end }}
{{/*
Selector labels
*/}}
{{- define "renderer.selectorLabels" -}}
app.kubernetes.io/name: {{ include "renderer.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "renderer.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "renderer.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
@@ -1,118 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "renderer.fullname" . }}
labels:
{{- include "renderer.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "renderer.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "renderer.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "renderer.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NODE_OPTIONS
value: "--max-old-space-size=2048"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "{{ .Values.global.deployment.type }}"
- name: DEPLOYMENT_PLATFORM
value: "{{ .Values.global.deployment.platform }}"
- name: SERVER_FLAVOR
value: "renderer"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_ENABLED
value: "true"
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_SUB_PATH
value: "{{ .Values.app.path }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: AFFINE_SERVER_HTTPS
value: "{{ .Values.app.https }}"
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
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 }}
@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "graphql.fullname" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "graphql.selectorLabels" . | nindent 4 }}
@@ -1,12 +0,0 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "graphql.serviceAccountName" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -1,15 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "renderer.fullname" . }}-test-connection"
labels:
{{- include "renderer.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "renderer.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never
@@ -1,38 +0,0 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
app:
# AFFINE_SERVER_SUB_PATH
path: ''
# AFFINE_SERVER_HOST
host: '0.0.0.0'
https: true
serviceAccount:
create: true
annotations: {}
name: 'affine-renderer'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
requests:
cpu: '1'
memory: 2Gi
probe:
initialDelaySeconds: 20
nodeSelector: {}
tolerations: []
affinity: {}
@@ -1,23 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
@@ -1,16 +0,0 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sync.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sync.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sync.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sync.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
@@ -1,112 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sync.fullname" . }}
labels:
{{- include "sync.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "sync.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "sync.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sync.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "{{ .Values.global.deployment.type }}"
- name: DEPLOYMENT_PLATFORM
value: "{{ .Values.global.deployment.platform }}"
- name: SERVER_FLAVOR
value: "sync"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
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 }}
@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "sync.fullname" . }}
labels:
{{- include "sync.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "sync.selectorLabels" . | nindent 4 }}
@@ -1,12 +0,0 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "sync.serviceAccountName" . }}
labels:
{{- include "sync.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -1,15 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "sync.fullname" . }}-test-connection"
labels:
{{- include "sync.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "sync.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never
@@ -1,38 +0,0 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
app:
# AFFINE_SERVER_HOST
host: '0.0.0.0'
serviceAccount:
create: true
annotations: {}
name: 'affine-sync'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
limits:
cpu: '2'
memory: 4Gi
requests:
cpu: '1'
memory: 2Gi
probe:
initialDelaySeconds: 20
nodeSelector: {}
tolerations: []
affinity: {}
@@ -1,23 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
@@ -1,6 +0,0 @@
apiVersion: v2
name: web
description: A Helm chart for Kubernetes
type: application
version: 0.0.0
appVersion: "0.7.0-canary.18"
@@ -1,16 +0,0 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "web.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "web.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "web.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "web.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
@@ -1,63 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "web.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "web.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 }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "web.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "web.labels" -}}
helm.sh/chart: {{ include "web.chart" . }}
{{ include "web.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
monitoring: enabled
{{- end }}
{{/*
Selector labels
*/}}
{{- define "web.selectorLabels" -}}
app.kubernetes.io/name: {{ include "web.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "web.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "web.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
@@ -1,60 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "web.fullname" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "web.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "web.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "web.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
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 }}
@@ -1,15 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "web.fullname" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "web.selectorLabels" . | nindent 4 }}
@@ -1,37 +0,0 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine-front
pullPolicy: IfNotPresent
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: "affine-web"
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
limits:
cpu: '500m'
memory: 2Gi
requests:
cpu: '500m'
memory: 2Gi
nodeSelector: {}
tolerations: []
affinity: {}
probe:
initialDelaySeconds: 1
+6 -6
View File
@@ -44,9 +44,9 @@ spec:
pathType: Prefix
backend:
service:
name: affine-sync
name: {{ $.Values.front.services.sync.name }}
port:
number: {{ $.Values.sync.service.port }}
number: {{ $.Values.front.services.sync.port }}
- path: /graphql
pathType: Prefix
backend:
@@ -65,15 +65,15 @@ spec:
pathType: Prefix
backend:
service:
name: affine-renderer
name: {{ $.Values.front.services.renderer.name }}
port:
number: {{ $.Values.renderer.service.port }}
number: {{ $.Values.front.services.renderer.port }}
- path: /
pathType: Prefix
backend:
service:
name: affine-web
name: {{ $.Values.front.services.web.name }}
port:
number: {{ $.Values.web.service.port }}
number: {{ $.Values.front.services.web.port }}
{{- end }}
{{- end }}
+18 -18
View File
@@ -47,27 +47,27 @@ graphql:
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
sync:
service:
type: ClusterIP
port: 3010
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
renderer:
service:
type: ClusterIP
port: 3000
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
doc:
service:
type: ClusterIP
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
web:
service:
type: ClusterIP
port: 8080
front:
services:
sync:
name: affine-sync
type: ClusterIP
port: 3010
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
renderer:
name: affine-renderer
type: ClusterIP
port: 3000
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
web:
name: affine-web
type: ClusterIP
port: 8080
+1 -12
View File
@@ -263,18 +263,7 @@ jobs:
with:
app-version: ${{ inputs.app-version }}
- name: Build front Dockerfile
uses: docker/build-push-action@v6
with:
context: .
push: true
pull: true
platforms: linux/amd64,linux/arm64
provenance: true
file: .github/deployment/front/Dockerfile
tags: ghcr.io/toeverything/affine-front:${{inputs.build-type}}-${{ inputs.git-short-hash }}
- name: Build graphql Dockerfile
- name: Build backend Dockerfile
uses: docker/build-push-action@v6
with:
context: .
@@ -44,3 +44,12 @@ e2e('should init renderer service', async t => {
const res = await app.GET('/info').expect(200);
t.is(res.body.flavor, 'renderer');
});
e2e('should init front service', async t => {
// @ts-expect-error override
globalThis.env.FLAVOR = 'front';
await using app = await createApp();
const res = await app.GET('/info').expect(200);
t.is(res.body.flavor, 'front');
});
@@ -68,12 +68,14 @@ test('should read DEPLOYMENT_TYPE', t => {
test('should read FLAVOR', t => {
t.deepEqual(
['allinone', 'graphql', 'sync', 'renderer', 'doc', 'script'].map(envVal => {
process.env.SERVER_FLAVOR = envVal;
const env = new Env();
return env.FLAVOR;
}),
['allinone', 'graphql', 'sync', 'renderer', 'doc', 'script']
['allinone', 'graphql', 'sync', 'renderer', 'front', 'doc', 'script'].map(
envVal => {
process.env.SERVER_FLAVOR = envVal;
const env = new Env();
return env.FLAVOR;
}
),
['allinone', 'graphql', 'sync', 'renderer', 'front', 'doc', 'script']
);
t.throws(
@@ -83,7 +85,7 @@ test('should read FLAVOR', t => {
},
{
message:
'Invalid value "unknown" for environment variable SERVER_FLAVOR, expected one of ["allinone","graphql","sync","renderer","doc","script"]',
'Invalid value "unknown" for environment variable SERVER_FLAVOR, expected one of ["allinone","graphql","sync","renderer","front","doc","script"]',
}
);
});
@@ -110,6 +112,7 @@ test('should tell flavors correctly', t => {
graphql: true,
sync: true,
renderer: true,
front: false,
doc: true,
script: false,
});
@@ -119,6 +122,17 @@ test('should tell flavors correctly', t => {
graphql: true,
sync: false,
renderer: false,
front: false,
doc: false,
script: false,
});
process.env.SERVER_FLAVOR = 'front';
t.deepEqual(new Env().flavors, {
graphql: false,
sync: false,
renderer: false,
front: true,
doc: false,
script: false,
});
@@ -128,6 +142,7 @@ test('should tell flavors correctly', t => {
graphql: false,
sync: false,
renderer: false,
front: false,
doc: false,
script: true,
});
+12 -5
View File
@@ -43,6 +43,7 @@ import { PermissionModule } from './core/permission';
import { QueueDashboardModule } from './core/queue-dashboard';
import { QuotaModule } from './core/quota';
import { SelfhostModule } from './core/selfhost';
import { StaticFileModule } from './core/static-files';
import { StorageModule } from './core/storage';
import { SyncModule } from './core/sync';
import { TelemetryModule } from './core/telemetry';
@@ -173,10 +174,14 @@ export function buildAppModule(env: Env) {
NotificationModule,
MailModule
)
// renderer server only
.useIf(() => env.flavors.renderer, DocRendererModule)
// sync server only
.useIf(() => env.flavors.sync, SyncModule, TelemetryModule)
// renderer server and front server
.useIf(() => env.flavors.renderer || env.flavors.front, DocRendererModule)
// sync server and front server
.useIf(
() => env.flavors.sync || env.flavors.front,
SyncModule,
TelemetryModule
)
// graphql server only
.useIf(
() => env.flavors.graphql,
@@ -199,8 +204,10 @@ export function buildAppModule(env: Env) {
)
// doc service only
.useIf(() => env.flavors.doc, DocServiceModule)
// self hosted server only
// worker for and self-hosted API only for self-host and local development only
.useIf(() => env.dev || env.selfhosted, WorkerModule, SelfhostModule)
// static frontend routes for front flavor
.useIf(() => env.flavors.front, StaticFileModule)
// gcloud
.useIf(() => env.gcp, GCloudModule);
@@ -8,6 +8,11 @@ export class SetupMiddleware implements NestMiddleware {
constructor(private readonly server: ServerService) {}
use = (req: Request, res: Response, next: (error?: Error | any) => void) => {
if (!env.selfhosted) {
next();
return;
}
// never throw
this.server
.initialized()
@@ -0,0 +1,193 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import test from 'ava';
import express from 'express';
import request from 'supertest';
import { Namespace } from '../../../env';
import { StaticFilesResolver } from '../static';
const mobileUA =
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36';
function initStaticFixture(root: string) {
const staticRoot = join(root, 'static');
const files: Array<[string, string]> = [
['index.html', 'web-index'],
['admin/index.html', 'admin-index'],
['assets/main.js', 'web-asset'],
['mobile/index.html', 'mobile-index'],
['mobile/assets/main.js', 'mobile-asset'],
];
for (const [file, content] of files) {
const fullPath = join(staticRoot, file);
mkdirSync(dirname(fullPath), { recursive: true });
writeFileSync(fullPath, content);
}
}
async function createApp(basePath = '') {
const app = express();
const resolver = new StaticFilesResolver(
{ server: { path: basePath } } as any,
{
httpAdapter: {
getInstance: () => app,
},
} as any
);
resolver.onModuleInit();
return app;
}
test.serial('serves admin files and admin route fallback', async t => {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'affine-static-files-'));
initStaticFixture(fixtureRoot);
const prevProjectRoot = env.projectRoot;
const prevNamespace = env.NAMESPACE;
try {
// @ts-expect-error test override
env.projectRoot = fixtureRoot;
// @ts-expect-error test override
env.NAMESPACE = Namespace.Production;
const app = await createApp();
const indexRes = await request(app).get('/admin/index.html').expect(200);
t.is(indexRes.text, 'admin-index');
const fallbackRes = await request(app).get('/admin/settings').expect(200);
t.is(fallbackRes.text, 'admin-index');
} finally {
// @ts-expect-error test override
env.projectRoot = prevProjectRoot;
// @ts-expect-error test override
env.NAMESPACE = prevNamespace;
rmSync(fixtureRoot, { recursive: true, force: true });
}
});
test.serial(
'serves static assets from prefixed paths and returns 404 on missing',
async t => {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'affine-static-files-'));
initStaticFixture(fixtureRoot);
const prevProjectRoot = env.projectRoot;
const prevNamespace = env.NAMESPACE;
try {
// @ts-expect-error test override
env.projectRoot = fixtureRoot;
// @ts-expect-error test override
env.NAMESPACE = Namespace.Production;
const app = await createApp();
const assetRes = await request(app).get('/assets/main.js').expect(200);
t.is(assetRes.text, 'web-asset');
await request(app).get('/assets/missing.js').expect(404);
} finally {
// @ts-expect-error test override
env.projectRoot = prevProjectRoot;
// @ts-expect-error test override
env.NAMESPACE = prevNamespace;
rmSync(fixtureRoot, { recursive: true, force: true });
}
}
);
test.serial(
'matches front container index behavior and cache header',
async t => {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'affine-static-files-'));
initStaticFixture(fixtureRoot);
const prevProjectRoot = env.projectRoot;
const prevNamespace = env.NAMESPACE;
try {
// @ts-expect-error test override
env.projectRoot = fixtureRoot;
// @ts-expect-error test override
env.NAMESPACE = Namespace.Production;
const app = await createApp();
const indexRes = await request(app).get('/index.html').expect(200);
t.is(indexRes.text, 'web-index');
t.is(
indexRes.headers['cache-control'],
'private, no-cache, no-store, max-age=0, must-revalidate'
);
const fallbackRes = await request(app).get('/workspace/path').expect(200);
t.is(fallbackRes.text, 'web-index');
} finally {
// @ts-expect-error test override
env.projectRoot = prevProjectRoot;
// @ts-expect-error test override
env.NAMESPACE = prevNamespace;
rmSync(fixtureRoot, { recursive: true, force: true });
}
}
);
test.serial('uses mobile root only in dev namespace for mobile UA', async t => {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'affine-static-files-'));
initStaticFixture(fixtureRoot);
const prevProjectRoot = env.projectRoot;
const prevNamespace = env.NAMESPACE;
try {
// @ts-expect-error test override
env.projectRoot = fixtureRoot;
// @ts-expect-error test override
env.NAMESPACE = Namespace.Dev;
const app = await createApp();
const mobileAssetRes = await request(app)
.get('/assets/main.js')
.set('user-agent', mobileUA)
.expect(200);
t.is(mobileAssetRes.text, 'mobile-asset');
const webAssetRes = await request(app).get('/assets/main.js').expect(200);
t.is(webAssetRes.text, 'web-asset');
const mobileFromHint = await request(app)
.get('/assets/main.js')
.set('user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
.set('sec-ch-ua-mobile', '?1')
.expect(200);
t.is(mobileFromHint.text, 'mobile-asset');
const desktopFromHint = await request(app)
.get('/assets/main.js')
.set('user-agent', mobileUA)
.set('sec-ch-ua-mobile', '?0')
.expect(200);
t.is(desktopFromHint.text, 'web-asset');
const mobileFromPlatformHint = await request(app)
.get('/assets/main.js')
.set('sec-ch-ua-platform', '"Android"')
.expect(200);
t.is(mobileFromPlatformHint.text, 'mobile-asset');
} finally {
// @ts-expect-error test override
env.projectRoot = prevProjectRoot;
// @ts-expect-error test override
env.NAMESPACE = prevNamespace;
rmSync(fixtureRoot, { recursive: true, force: true });
}
});
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { StaticFilesResolver } from './static';
@Module({
providers: [StaticFilesResolver],
})
export class StaticFileModule {}
@@ -0,0 +1,119 @@
import { join } from 'node:path';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import type { Application, Request, Response } from 'express';
import { static as serveStatic } from 'express';
import { Config } from '../../base';
import { isMobileRequest } from '../utils/user-agent';
const staticPathRegex = /^\/(_plugin|assets|imgs|js|plugins|static)\//;
@Injectable()
export class StaticFilesResolver implements OnModuleInit {
constructor(
private readonly config: Config,
private readonly adapterHost: HttpAdapterHost
) {}
onModuleInit() {
if (!this.adapterHost.httpAdapter) {
return;
}
const app = this.adapterHost.httpAdapter.getInstance<Application>();
const basePath = this.config.server.path;
const rootPath = basePath || '/';
const staticPath = join(env.projectRoot, 'static');
const adminPath = join(staticPath, 'admin');
const mobilePath = env.namespaces.canary
? join(staticPath, 'mobile')
: staticPath;
const staticAsset = serveStatic(staticPath, {
redirect: false,
index: false,
fallthrough: true,
});
const mobileAsset = serveStatic(mobilePath, {
redirect: false,
index: false,
fallthrough: true,
});
const staticAssetStrict = serveStatic(staticPath, {
redirect: false,
index: false,
fallthrough: false,
});
const mobileAssetStrict = serveStatic(mobilePath, {
redirect: false,
index: false,
fallthrough: false,
});
const adminAsset = serveStatic(adminPath, {
redirect: false,
index: false,
fallthrough: true,
});
const routeByUA = (
req: Request,
res: Response,
next: (err?: unknown) => void,
strict = false
) => {
const isMobile = isMobileRequest(req.headers);
if (strict) {
return isMobile
? mobileAssetStrict(req, res, next)
: staticAssetStrict(req, res, next);
}
return isMobile
? mobileAsset(req, res, next)
: staticAsset(req, res, next);
};
// /admin
app.use(basePath + '/admin', adminAsset);
app.get([basePath + '/admin', basePath + '/admin/*path'], (_req, res) => {
res.sendFile(join(adminPath, 'index.html'));
});
// /_plugin|/assets|/imgs|/js|/plugins|/static
app.use(rootPath, (req, res, next) => {
if (!staticPathRegex.test(req.path)) {
next();
return;
}
routeByUA(req, res, next, true);
});
// /
app.use(rootPath, (req, res, next) => {
if (req.path.startsWith('/admin')) {
next();
return;
}
res.setHeader(
'Cache-Control',
'private, no-cache, no-store, max-age=0, must-revalidate'
);
routeByUA(req, res, next, false);
});
app.get(
[basePath || '/', basePath + '/*path'],
(req: Request, res: Response) => {
if (req.path.startsWith('/admin')) {
res.status(404).end();
return;
}
const root = isMobileRequest(req.headers) ? mobilePath : staticPath;
res.sendFile(join(root, 'index.html'));
}
);
}
}
@@ -0,0 +1,65 @@
import test from 'ava';
import {
isMobileRequest,
isMobileUserAgent,
parseRequestUserAgent,
parseUserAgent,
} from '../user-agent';
const mobileUserAgent =
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36';
const desktopUserAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
test('returns desktop for empty user agent values', t => {
t.false(isMobileUserAgent(undefined));
t.deepEqual(parseUserAgent(undefined), {
ua: '',
deviceType: 'desktop',
isMobile: false,
});
});
test('detects mobile and desktop user agents', t => {
const mobile = parseUserAgent(mobileUserAgent);
t.true(mobile.isMobile);
t.is(mobile.deviceType, 'mobile');
const desktop = parseUserAgent(desktopUserAgent);
t.false(desktop.isMobile);
t.is(desktop.deviceType, 'desktop');
});
test('prefers sec-ch-ua-mobile over user-agent when available', t => {
const mobileFromHint = parseRequestUserAgent({
'user-agent': desktopUserAgent,
'sec-ch-ua-mobile': '?1',
});
t.true(mobileFromHint.isMobile);
t.is(mobileFromHint.deviceType, 'mobile');
t.true(isMobileRequest({ 'sec-ch-ua-mobile': '?1' }));
const desktopFromHint = parseRequestUserAgent({
'user-agent': mobileUserAgent,
'sec-ch-ua-mobile': '?0',
});
t.false(desktopFromHint.isMobile);
t.is(desktopFromHint.deviceType, 'desktop');
});
test('uses sec-ch-ua-platform as fallback hint', t => {
const parsed = parseRequestUserAgent({
'sec-ch-ua-platform': '"Android"',
});
t.true(parsed.isMobile);
t.is(parsed.deviceType, 'mobile');
const desktop = parseRequestUserAgent({
'sec-ch-ua-platform': '"Windows"',
});
t.false(desktop.isMobile);
t.is(desktop.deviceType, 'desktop');
t.false(isMobileUserAgent(undefined));
});
@@ -0,0 +1,207 @@
import type { IncomingHttpHeaders } from 'node:http';
import isMobile from 'is-mobile';
export type UserAgentHeader = IncomingHttpHeaders['user-agent'];
export type UserAgentDeviceType = 'desktop' | 'mobile';
export interface ParsedUserAgent {
readonly ua: string;
readonly deviceType: UserAgentDeviceType;
readonly isMobile: boolean;
}
type HeaderValue = string | string[] | undefined;
const USER_AGENT_MAX_LENGTH = 512;
const USER_AGENT_CACHE_MAX_SIZE = 1024;
const CLIENT_HINT_MOBILE_TRUE = new Set(['?1', '1', 'true']);
const MOBILE_PLATFORM_HINTS = new Set(['android', 'ios', 'ipados']);
const EMPTY_USER_AGENT: ParsedUserAgent = {
ua: '',
deviceType: 'desktop',
isMobile: false,
};
const parsedUserAgentCache = new Map<string, ParsedUserAgent>();
interface UserAgentSignals {
ua: string;
clientHintMobile?: boolean;
clientHintPlatform?: string;
}
function pickHeaderValue(value: HeaderValue): string {
if (typeof value === 'string') {
return value;
}
if (!Array.isArray(value)) {
return '';
}
return value.find(item => typeof item === 'string' && item.length > 0) ?? '';
}
function normalizeHeaderValue(value: HeaderValue): string {
const header = pickHeaderValue(value);
if (!header) return '';
return header.trim().toLowerCase();
}
function unquoteHeaderValue(value: string): string {
if (!value) return value;
if (value.startsWith('"') && value.endsWith('"')) {
return value.slice(1, -1);
}
return value;
}
function normalizeUserAgentHeader(value: UserAgentHeader): string {
const normalized = normalizeHeaderValue(value);
if (!normalized) return '';
if (normalized.length <= USER_AGENT_MAX_LENGTH) return normalized;
return normalized.slice(0, USER_AGENT_MAX_LENGTH);
}
function parseClientHintMobile(value: HeaderValue): boolean | undefined {
const normalized = unquoteHeaderValue(normalizeHeaderValue(value));
if (!normalized) return;
if (CLIENT_HINT_MOBILE_TRUE.has(normalized)) return true;
return false;
}
function parseClientHintPlatform(value: HeaderValue): string | undefined {
const normalized = unquoteHeaderValue(normalizeHeaderValue(value));
if (!normalized) {
return;
}
return normalized;
}
function parseUserAgentSignals(headers: IncomingHttpHeaders): UserAgentSignals {
return {
ua: normalizeUserAgentHeader(headers['user-agent']),
clientHintMobile: parseClientHintMobile(headers['sec-ch-ua-mobile']),
clientHintPlatform: parseClientHintPlatform(headers['sec-ch-ua-platform']),
};
}
function getDeviceType(signals: UserAgentSignals): UserAgentDeviceType {
if (signals.clientHintMobile !== undefined) {
return signals.clientHintMobile ? 'mobile' : 'desktop';
}
if (
signals.clientHintPlatform &&
MOBILE_PLATFORM_HINTS.has(signals.clientHintPlatform)
) {
return 'mobile';
}
if (!signals.ua) {
return 'desktop';
}
const mobile = isMobile({
ua: signals.ua,
tablet: true,
featureDetect: false,
});
return mobile ? 'mobile' : 'desktop';
}
function getCacheKey(signals: UserAgentSignals): string {
const hintMobile =
signals.clientHintMobile === undefined
? ''
: signals.clientHintMobile
? '1'
: '0';
const hintPlatform = signals.clientHintPlatform ?? '';
return `${signals.ua}|${hintMobile}|${hintPlatform}`;
}
function getCachedParsedUserAgent(
cacheKey: string
): ParsedUserAgent | undefined {
const cached = parsedUserAgentCache.get(cacheKey);
if (!cached) return;
// Keep recently-used entries hot in the bounded cache.
parsedUserAgentCache.delete(cacheKey);
parsedUserAgentCache.set(cacheKey, cached);
return cached;
}
function cacheParsedUserAgent(
cacheKey: string,
parsedUserAgent: ParsedUserAgent
) {
if (parsedUserAgentCache.has(cacheKey)) {
parsedUserAgentCache.delete(cacheKey);
} else if (parsedUserAgentCache.size >= USER_AGENT_CACHE_MAX_SIZE) {
const oldestUserAgent = parsedUserAgentCache.keys().next();
if (!oldestUserAgent.done) {
parsedUserAgentCache.delete(oldestUserAgent.value);
}
}
parsedUserAgentCache.set(cacheKey, parsedUserAgent);
}
function parseUserAgentWithSignals(signals: UserAgentSignals): ParsedUserAgent {
const cacheKey = getCacheKey(signals);
const cached = getCachedParsedUserAgent(cacheKey);
if (cached) return cached;
const deviceType = getDeviceType(signals);
const parsed: ParsedUserAgent = {
ua: signals.ua,
deviceType,
isMobile: deviceType === 'mobile',
};
cacheParsedUserAgent(cacheKey, parsed);
return parsed;
}
export function parseUserAgent(
userAgentHeader: UserAgentHeader
): ParsedUserAgent {
const ua = normalizeUserAgentHeader(userAgentHeader);
if (!ua) {
return EMPTY_USER_AGENT;
}
return parseUserAgentWithSignals({ ua });
}
export function isMobileUserAgent(userAgentHeader: UserAgentHeader): boolean {
return parseUserAgent(userAgentHeader).isMobile;
}
export function parseRequestUserAgent(
headers: IncomingHttpHeaders
): ParsedUserAgent {
const signals = parseUserAgentSignals(headers);
if (
!signals.ua &&
signals.clientHintMobile === undefined &&
!signals.clientHintPlatform
) {
return EMPTY_USER_AGENT;
}
return parseUserAgentWithSignals(signals);
}
export function isMobileRequest(headers: IncomingHttpHeaders): boolean {
return parseRequestUserAgent(headers).isMobile;
}
+2
View File
@@ -22,6 +22,7 @@ export enum Flavor {
Graphql = 'graphql',
Sync = 'sync',
Renderer = 'renderer',
Front = 'front',
Doc = 'doc',
Script = 'script',
}
@@ -108,6 +109,7 @@ export class Env implements AppEnv {
graphql: this.isFlavor(Flavor.Graphql),
sync: this.isFlavor(Flavor.Sync),
renderer: this.isFlavor(Flavor.Renderer),
front: this.FLAVOR === Flavor.Front,
doc: this.isFlavor(Flavor.Doc),
// Script in a special flavor, return true only when it is set explicitly
script: this.FLAVOR === Flavor.Script,
+1 -2
View File
@@ -103,8 +103,7 @@ ios_new_version=${IOS_APP_VERSION:-$new_version}
update_app_version_in_helm_charts ".github/helm/affine/Chart.yaml" "$new_version"
update_app_version_in_helm_charts ".github/helm/affine/charts/graphql/Chart.yaml" "$new_version"
update_app_version_in_helm_charts ".github/helm/affine/charts/sync/Chart.yaml" "$new_version"
update_app_version_in_helm_charts ".github/helm/affine/charts/renderer/Chart.yaml" "$new_version"
update_app_version_in_helm_charts ".github/helm/affine/charts/front/Chart.yaml" "$new_version"
update_app_version_in_helm_charts ".github/helm/affine/charts/doc/Chart.yaml" "$new_version"
update_app_stream_version "packages/frontend/apps/electron/resources/affine.metainfo.xml" "$new_version"