Compare commits

..

1 Commits

Author SHA1 Message Date
EYHN
afa108d517 feat(core): edit and delete pinned collections in all docs 2025-05-15 13:28:35 +09:00
3119 changed files with 48159 additions and 201466 deletions

View File

@@ -5,14 +5,7 @@ rustflags = ["-C", "target-feature=+crt-static"]
[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-args=-Wl,--warn-unresolved-symbols"]
[target.'cfg(target_os = "macos")']
rustflags = [
"-C",
"link-args=-Wl,-undefined,dynamic_lookup,-no_fixup_chains",
"-C",
"link-args=-all_load",
"-C",
"link-args=-weak_framework ScreenCaptureKit",
]
rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup,-no_fixup_chains", "-C", "link-args=-all_load", "-C", "link-args=-weak_framework ScreenCaptureKit"]
# https://sourceware.org/bugzilla/show_bug.cgi?id=21032
# https://sourceware.org/bugzilla/show_bug.cgi?id=21031
# https://github.com/rust-lang/rust/issues/134820

View File

@@ -2,8 +2,6 @@ version: '3.8'
services:
app:
security_opt:
- no-new-privileges:true
image: mcr.microsoft.com/devcontainers/base:bookworm
volumes:
- ../..:/workspaces:cached
@@ -12,7 +10,6 @@ services:
environment:
DATABASE_URL: postgresql://affine:affine@db:5432/affine
REDIS_SERVER_HOST: redis
AFFINE_INDEXER_SEARCH_ENDPOINT: http://indexer:9308
db:
image: pgvector/pgvector:pg16
@@ -26,19 +23,5 @@ services:
redis:
image: redis
indexer:
image: manticoresearch/manticore:${MANTICORE_VERSION:-10.1.0}
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
memlock:
soft: -1
hard: -1
volumes:
- manticoresearch_data:/var/lib/manticore
volumes:
postgres-data:
manticoresearch_data:

View File

@@ -12,4 +12,4 @@ DB_DATABASE_NAME=affine
# ELASTIC_PLATFORM=linux/arm64
# manticoresearch
MANTICORE_VERSION=10.1.0
MANTICORE_VERSION=9.2.14

View File

@@ -1,6 +1,3 @@
postgres
.env
compose.yml
certs/*
!certs/.gitkeep
nginx/conf.d/*
compose.yml

View File

@@ -1,21 +0,0 @@
# Dev containers
## Develop with domain
> MacOs only, OrbStack only
### 1. Generate and install Root CA
```bash
# the root ca file will be located at `./.docker/dev/certs/ca`
yarn affine cert --install
```
### 2. Generate domain certs
```bash
# certificates will be located at `./.docker/dev/certs/${domain}`
yarn affine cert --domain affine.localhost
```
### 3. Enable nginx service in compose.yml

View File

@@ -0,0 +1,65 @@
name: affine_dev_services
services:
postgres:
env_file:
- .env
image: pgvector/pgvector:pg${DB_VERSION:-16}
ports:
- 5432:5432
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:latest
ports:
- 6379:6379
mailhog:
image: mailhog/mailhog:latest
ports:
- 1025:1025
- 8025:8025
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-9.0.1}${ELASTIC_VERSION_ARM64}
platform: ${ELASTIC_PLATFORM}
labels:
co.elastic.logs/module: elasticsearch
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
ports:
- ${ES_PORT:-9200}:9200
environment:
- node.name=es01
- cluster.name=affine-dev
- discovery.type=single-node
- bootstrap.memory_lock=true
- xpack.security.enabled=false
- xpack.security.http.ssl.enabled=false
- xpack.security.transport.ssl.enabled=false
- xpack.license.self_generated.type=basic
mem_limit: ${ES_MEM_LIMIT:-1073741824}
ulimits:
memlock:
soft: -1
hard: -1
healthcheck:
test:
[
"CMD-SHELL",
"curl -s http://localhost:9200 | grep -q 'affine-dev'",
]
interval: 10s
timeout: 10s
retries: 120
networks:
dev:
volumes:
postgres_data:
elasticsearch_data:

View File

@@ -18,23 +18,16 @@ services:
ports:
- 6379:6379
# https://mailpit.axllent.org/docs/install/docker/
mailpit:
image: axllent/mailpit:latest
mailhog:
image: mailhog/mailhog:latest
ports:
- 1025:1025
- 8025:8025
environment:
MP_MAX_MESSAGES: 5000
MP_DATABASE: /data/mailpit.db
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
volumes:
- mailpit_data:/data
# https://manual.manticoresearch.com/Starting_the_server/Docker
manticoresearch:
image: manticoresearch/manticore:${MANTICORE_VERSION:-10.1.0}
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.2.14}
restart: always
ports:
- 9308:9308
ulimits:
@@ -47,47 +40,6 @@ services:
hard: -1
volumes:
- manticoresearch_data:/var/lib/manticore
# elasticsearch:
# image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-9.0.1}${ELASTIC_VERSION_ARM64}
# platform: ${ELASTIC_PLATFORM}
# labels:
# co.elastic.logs/module: elasticsearch
# volumes:
# - elasticsearch_data:/usr/share/elasticsearch/data
# ports:
# - ${ES_PORT:-9200}:9200
# environment:
# - node.name=es01
# - cluster.name=affine-dev
# - discovery.type=single-node
# - bootstrap.memory_lock=true
# - xpack.security.enabled=false
# - xpack.security.http.ssl.enabled=false
# - xpack.security.transport.ssl.enabled=false
# - xpack.license.self_generated.type=basic
# mem_limit: ${ES_MEM_LIMIT:-1073741824}
# ulimits:
# memlock:
# soft: -1
# hard: -1
# healthcheck:
# test:
# [
# "CMD-SHELL",
# "curl -s http://localhost:9200 | grep -q 'affine-dev'",
# ]
# interval: 10s
# timeout: 10s
# retries: 120
# nginx:
# image: nginx:alpine
# volumes:
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
# - ./nginx/conf.d:/etc/nginx/conf.d:ro
# - ./certs:/etc/nginx/certs:ro
# network_mode: host
networks:
dev:
@@ -95,5 +47,3 @@ networks:
volumes:
postgres_data:
manticoresearch_data:
mailpit_data:
elasticsearch_data:

View File

@@ -1,28 +0,0 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 512M;
server_names_hash_bucket_size 128;
ssi on;
gzip on;
include "/etc/nginx/conf.d/*";
}

View File

@@ -1,27 +0,0 @@
server {
listen 80;
server_name DEV_DOMAIN;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/nginx/certs/$host/crt;
ssl_certificate_key /etc/nginx/certs/$host/key;
server_name DEV_DOMAIN;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
resolver 127.0.0.1;
}
}

View File

@@ -1,25 +0,0 @@
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
countryName = Country Name (2 letter code)
countryName_default = US
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = MN
localityName = Locality Name (eg, city)
localityName_default = Minneapolis
organizationalUnitName = Organizational Unit Name (eg, section)
organizationalUnitName_default = Domain Control Validated
commonName = Internet Widgits Ltd
commonName_max = 64
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = DEV_DOMAIN
DNS.2 = *.DEV_DOMAIN

View File

@@ -21,3 +21,8 @@ CONFIG_LOCATION=~/.affine/self-host/config
DB_USERNAME=affine
DB_PASSWORD=
DB_DATABASE=affine
# indexer search provider manticoresearch version
MANTICORE_VERSION=9.2.14
# position of the manticoresearch data to persist
MANTICORE_DATA_LOCATION=~/.affine/self-host/manticore

View File

@@ -1 +0,0 @@
.env

View File

@@ -1,7 +1,7 @@
name: affine
services:
affine:
image: ghcr.io/toeverything/affine:${AFFINE_REVISION:-stable}
image: ghcr.io/toeverything/affine-graphql:${AFFINE_REVISION:-stable}
container_name: affine_server
ports:
- '${PORT:-3010}:3010'
@@ -10,6 +10,8 @@ services:
condition: service_healthy
postgres:
condition: service_healthy
indexer:
condition: service_healthy
affine_migration:
condition: service_completed_successfully
volumes:
@@ -21,11 +23,10 @@ services:
environment:
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
- AFFINE_INDEXER_ENABLED=false
restart: unless-stopped
affine_migration:
image: ghcr.io/toeverything/affine:${AFFINE_REVISION:-stable}
image: ghcr.io/toeverything/affine-graphql:${AFFINE_REVISION:-stable}
container_name: affine_migration_job
volumes:
# custom configurations
@@ -37,12 +38,13 @@ services:
environment:
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
- AFFINE_INDEXER_ENABLED=false
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
indexer:
condition: service_healthy
redis:
image: redis
@@ -55,7 +57,7 @@ services:
restart: unless-stopped
postgres:
image: pgvector/pgvector:pg16
image: postgres:16
container_name: affine_postgres
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
@@ -74,3 +76,24 @@ services:
timeout: 5s
retries: 5
restart: unless-stopped
indexer:
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.2.14}
container_name: affine_indexer
volumes:
- ${MANTICORE_DATA_LOCATION}:/var/lib/manticore
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
memlock:
soft: -1
hard: -1
healthcheck:
test:
['CMD', 'wget', '-O-', 'http://127.0.0.1:9308']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped

View File

@@ -31,13 +31,9 @@
"properties": {
"queue": {
"type": "object",
"description": "The config for job queues\n@default {\"attempts\":5,\"backoff\":{\"type\":\"exponential\",\"delay\":1000},\"removeOnComplete\":true,\"removeOnFail\":{\"age\":86400,\"count\":500}}\n@link https://api.docs.bullmq.io/interfaces/v5.QueueOptions.html",
"description": "The config for job queues\n@default {\"attempts\":5,\"removeOnComplete\":true,\"removeOnFail\":{\"age\":86400,\"count\":500}}\n@link https://api.docs.bullmq.io/interfaces/v5.QueueOptions.html",
"default": {
"attempts": 5,
"backoff": {
"type": "exponential",
"delay": 1000
},
"removeOnComplete": true,
"removeOnFail": {
"age": 86400,
@@ -52,14 +48,14 @@
},
"queues.copilot": {
"type": "object",
"description": "The config for copilot job queue\n@default {\"concurrency\":10}",
"description": "The config for copilot job queue\n@default {\"concurrency\":1}",
"properties": {
"concurrency": {
"type": "number"
}
},
"default": {
"concurrency": 10
"concurrency": 1
}
},
"queues.doc": {
@@ -148,11 +144,6 @@
"description": "Whether allow new registrations.\n@default true",
"default": true
},
"allowSignupForOauth": {
"type": "boolean",
"description": "Whether allow new registrations via configured oauth.\n@default true",
"default": true
},
"requireEmailDomainVerification": {
"type": "boolean",
"description": "Whether require email domain record verification before accessing restricted resources.\n@default false",
@@ -195,11 +186,6 @@
"type": "object",
"description": "Configuration for mailer module",
"properties": {
"SMTP.name": {
"type": "string",
"description": "Name of the email server (e.g. your domain name)\n@default \"AFFiNE Server\"\n@environment `MAILER_SERVERNAME`",
"default": "AFFiNE Server"
},
"SMTP.host": {
"type": "string",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"\n@environment `MAILER_HOST`",
@@ -222,52 +208,12 @@
},
"SMTP.sender": {
"type": "string",
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"AFFiNE Self Hosted <noreply@example.com>\"\n@environment `MAILER_SENDER`",
"default": "AFFiNE Self Hosted <noreply@example.com>"
"description": "Sender of all the emails (e.g. \"AFFiNE Team <noreply@affine.pro>\")\n@default \"\"\n@environment `MAILER_SENDER`",
"default": ""
},
"SMTP.ignoreTLS": {
"type": "boolean",
"description": "Whether ignore email server's TLS certificate verification. Enable it for self-signed certificates.\n@default false\n@environment `MAILER_IGNORE_TLS`",
"default": false
},
"fallbackDomains": {
"type": "array",
"description": "The emails from these domains are always sent using the fallback SMTP server.\n@default []",
"default": []
},
"fallbackSMTP.name": {
"type": "string",
"description": "Name of the fallback email server (e.g. your domain name)\n@default \"AFFiNE Server\"",
"default": "AFFiNE Server"
},
"fallbackSMTP.host": {
"type": "string",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"",
"default": ""
},
"fallbackSMTP.port": {
"type": "number",
"description": "Port of the email server (they commonly are 25, 465 or 587)\n@default 465",
"default": 465
},
"fallbackSMTP.username": {
"type": "string",
"description": "Username used to authenticate the email server\n@default \"\"",
"default": ""
},
"fallbackSMTP.password": {
"type": "string",
"description": "Password used to authenticate the email server\n@default \"\"",
"default": ""
},
"fallbackSMTP.sender": {
"type": "string",
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"\"",
"default": ""
},
"fallbackSMTP.ignoreTLS": {
"type": "boolean",
"description": "Whether ignore email server's TLS certificate verification. Enable it for self-signed certificates.\n@default false",
"description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false\n@environment `MAILER_IGNORE_TLS`",
"default": false
}
}
@@ -397,7 +343,7 @@
},
"urlPrefix": {
"type": "string",
"description": "The custom domain URL prefix for the cloudflare r2 storage provider.\nWhen `enabled=true` and `urlPrefix` + `signKey` are provided, the server will:\n- Redirect GET requests to this custom domain with an HMAC token.\n- Return upload URLs under `/api/storage/*` for uploads.\nPresigned/upload proxy TTL is 1 hour.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
"description": "The presigned url prefix for the cloudflare r2 storage provider.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
},
"signKey": {
"type": "string",
@@ -518,7 +464,7 @@
},
"urlPrefix": {
"type": "string",
"description": "The custom domain URL prefix for the cloudflare r2 storage provider.\nWhen `enabled=true` and `urlPrefix` + `signKey` are provided, the server will:\n- Redirect GET requests to this custom domain with an HMAC token.\n- Return upload URLs under `/api/storage/*` for uploads.\nPresigned/upload proxy TTL is 1 hour.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
"description": "The presigned url prefix for the cloudflare r2 storage provider.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
},
"signKey": {
"type": "string",
@@ -590,11 +536,6 @@
"description": "Where the server get deployed(FQDN).\n@default \"localhost\"\n@environment `AFFINE_SERVER_HOST`",
"default": "localhost"
},
"hosts": {
"type": "array",
"description": "Multiple hosts the server will accept requests from.\n@default []",
"default": []
},
"port": {
"type": "number",
"description": "Which port the server will listen on.\n@default 3010\n@environment `AFFINE_SERVER_PORT`",
@@ -611,10 +552,10 @@
"type": "object",
"description": "Configuration for flags module",
"properties": {
"allowGuestDemoWorkspace": {
"earlyAccessControl": {
"type": "boolean",
"description": "Whether allow guest users to create demo workspaces.\n@default true",
"default": true
"description": "Only allow users with early access features to access the app\n@default false",
"default": false
}
}
},
@@ -674,34 +615,14 @@
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether to enable the copilot plugin. <br> Document: <a href=\"https://docs.affine.pro/self-host-affine/administer/ai\" target=\"_blank\">https://docs.affine.pro/self-host-affine/administer/ai</a>\n@default false",
"description": "Whether to enable the copilot plugin.\n@default false",
"default": false
},
"scenarios": {
"type": "object",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4-5@20250929\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"default": {
"override_enabled": false,
"scenarios": {
"audio_transcribing": "gemini-2.5-flash",
"chat": "gemini-2.5-flash",
"embedding": "gemini-embedding-001",
"image": "gpt-image-1",
"rerank": "gpt-4.1",
"coding": "claude-sonnet-4-5@20250929",
"complex_text_generation": "gpt-4o-2024-08-06",
"quick_decision_making": "gpt-5-mini",
"quick_text_generation": "gemini-2.5-flash",
"polish_and_summarize": "gemini-2.5-flash"
}
}
},
"providers.openai": {
"type": "object",
"description": "The config for the openai provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.openai.com/v1\"}\n@link https://github.com/openai/openai-node",
"description": "The config for the openai provider.\n@default {\"apiKey\":\"\"}\n@link https://github.com/openai/openai-node",
"default": {
"apiKey": "",
"baseURL": "https://api.openai.com/v1"
"apiKey": ""
}
},
"providers.fal": {
@@ -713,47 +634,11 @@
},
"providers.gemini": {
"type": "object",
"description": "The config for the gemini provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://generativelanguage.googleapis.com/v1beta\"}",
"description": "The config for the gemini provider.\n@default {\"apiKey\":\"\"}",
"default": {
"apiKey": "",
"baseURL": "https://generativelanguage.googleapis.com/v1beta"
"apiKey": ""
}
},
"providers.geminiVertex": {
"type": "object",
"description": "The config for the google vertex provider.\n@default {}",
"properties": {
"location": {
"type": "string",
"description": "The location of the google vertex provider."
},
"project": {
"type": "string",
"description": "The project name of the google vertex provider."
},
"googleAuthOptions": {
"type": "object",
"description": "The google auth options for the google vertex provider.",
"properties": {
"credentials": {
"type": "object",
"description": "The credentials for the google vertex provider.",
"properties": {
"client_email": {
"type": "string",
"description": "The client email for the google vertex provider."
},
"private_key": {
"type": "string",
"description": "The private key for the google vertex provider."
}
}
}
}
}
},
"default": {}
},
"providers.perplexity": {
"type": "object",
"description": "The config for the perplexity provider.\n@default {\"apiKey\":\"\"}",
@@ -763,52 +648,11 @@
},
"providers.anthropic": {
"type": "object",
"description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.anthropic.com/v1\"}",
"description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\"}",
"default": {
"apiKey": "",
"baseURL": "https://api.anthropic.com/v1"
"apiKey": ""
}
},
"providers.anthropicVertex": {
"type": "object",
"description": "The config for the google vertex provider.\n@default {}",
"properties": {
"location": {
"type": "string",
"description": "The location of the google vertex provider."
},
"project": {
"type": "string",
"description": "The project name of the google vertex provider."
},
"googleAuthOptions": {
"type": "object",
"description": "The google auth options for the google vertex provider.",
"properties": {
"credentials": {
"type": "object",
"description": "The credentials for the google vertex provider.",
"properties": {
"client_email": {
"type": "string",
"description": "The client email for the google vertex provider."
},
"private_key": {
"type": "string",
"description": "The private key for the google vertex provider."
}
}
}
}
}
},
"default": {}
},
"providers.morph": {
"type": "object",
"description": "The config for the morph provider.\n@default {}",
"default": {}
},
"unsplash": {
"type": "object",
"description": "The config for the unsplash key.\n@default {\"key\":\"\"}",
@@ -923,7 +767,7 @@
},
"urlPrefix": {
"type": "string",
"description": "The custom domain URL prefix for the cloudflare r2 storage provider.\nWhen `enabled=true` and `urlPrefix` + `signKey` are provided, the server will:\n- Redirect GET requests to this custom domain with an HMAC token.\n- Return upload URLs under `/api/storage/*` for uploads.\nPresigned/upload proxy TTL is 1 hour.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
"description": "The presigned url prefix for the cloudflare r2 storage provider.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
},
"signKey": {
"type": "string",
@@ -946,47 +790,6 @@
}
}
},
"indexer": {
"type": "object",
"description": "Configuration for indexer module",
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable indexer plugin\n@default false\n@environment `AFFINE_INDEXER_ENABLED`",
"default": false
},
"provider.type": {
"type": "string",
"description": "Indexer search service provider name\n@default \"manticoresearch\"\n@environment `AFFINE_INDEXER_SEARCH_PROVIDER`",
"default": "manticoresearch"
},
"provider.endpoint": {
"type": "string",
"description": "Indexer search service endpoint\n@default \"http://localhost:9308\"\n@environment `AFFINE_INDEXER_SEARCH_ENDPOINT`",
"default": "http://localhost:9308"
},
"provider.apiKey": {
"type": "string",
"description": "Indexer search service api key. Optional for elasticsearch\n@default \"\"\n@environment `AFFINE_INDEXER_SEARCH_API_KEY`\n@link https://www.elastic.co/guide/server/current/api-key.html",
"default": ""
},
"provider.username": {
"type": "string",
"description": "Indexer search service auth username, if not set, basic auth will be disabled. Optional for elasticsearch\n@default \"\"\n@environment `AFFINE_INDEXER_SEARCH_USERNAME`\n@link https://www.elastic.co/guide/en/elasticsearch/reference/current/http-clients.html",
"default": ""
},
"provider.password": {
"type": "string",
"description": "Indexer search service auth password, if not set, basic auth will be disabled. Optional for elasticsearch\n@default \"\"\n@environment `AFFINE_INDEXER_SEARCH_PASSWORD`",
"default": ""
},
"autoIndex.batchSize": {
"type": "number",
"description": "Number of workspaces automatically indexed per batch\n@default 10",
"default": 10
}
}
},
"customerIo": {
"type": "object",
"description": "Configuration for customerIo module",
@@ -1003,6 +806,37 @@
}
}
},
"indexer": {
"type": "object",
"description": "Configuration for indexer module",
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable indexer plugin\n@default true",
"default": true
},
"provider.type": {
"type": "string",
"description": "Indexer search service provider name\n@default \"manticoresearch\"\n@environment `AFFINE_INDEXER_SEARCH_PROVIDER`",
"default": "manticoresearch"
},
"provider.endpoint": {
"type": "string",
"description": "Indexer search service endpoint\n@default \"http://localhost:9308\"\n@environment `AFFINE_INDEXER_SEARCH_ENDPOINT`",
"default": "http://localhost:9308"
},
"provider.username": {
"type": "string",
"description": "Indexer search service auth username, if not set, basic auth will be disabled. Optional for elasticsearch\n@default \"\"\n@environment `AFFINE_INDEXER_SEARCH_USERNAME`\n@link https://www.elastic.co/guide/en/elasticsearch/reference/current/http-clients.html",
"default": ""
},
"provider.password": {
"type": "string",
"description": "Indexer search service auth password, if not set, basic auth will be disabled. Optional for elasticsearch\n@default \"\"\n@environment `AFFINE_INDEXER_SEARCH_PASSWORD`",
"default": ""
}
}
},
"oauth": {
"type": "object",
"description": "Configuration for oauth module",
@@ -1047,43 +881,13 @@
},
"providers.oidc": {
"type": "object",
"description": "OIDC OAuth provider config\n@default {\"clientId\":\"\",\"clientSecret\":\"\",\"issuer\":\"\",\"args\":{}}\n@link https://openid.net/specs/openid-connect-core-1_0.html",
"properties": {
"clientId": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"args": {
"type": "object"
}
},
"description": "OIDC OAuth provider config\n@default {\"clientId\":\"\",\"clientSecret\":\"\",\"issuer\":\"\",\"args\":{}}",
"default": {
"clientId": "",
"clientSecret": "",
"issuer": "",
"args": {}
}
},
"providers.apple": {
"type": "object",
"description": "Apple OAuth provider config\n@default {\"clientId\":\"\",\"clientSecret\":\"\"}\n@link https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/implementing_sign_in_with_apple_in_your_app",
"properties": {
"clientId": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"args": {
"type": "object"
}
},
"default": {
"clientId": "",
"clientSecret": ""
}
}
}
},
@@ -1103,33 +907,18 @@
},
"apiKey": {
"type": "string",
"description": "[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.\n@default \"\"\n@environment `STRIPE_API_KEY`",
"description": "Stripe API key to enable payment service.\n@default \"\"\n@environment `STRIPE_API_KEY`",
"default": ""
},
"webhookKey": {
"type": "string",
"description": "[Deprecated] Stripe webhook key. Use payment.stripe.webhookKey instead.\n@default \"\"\n@environment `STRIPE_WEBHOOK_KEY`",
"description": "Stripe webhook key to enable payment service.\n@default \"\"\n@environment `STRIPE_WEBHOOK_KEY`",
"default": ""
},
"stripe": {
"type": "object",
"description": "Stripe sdk options and credentials\n@default {\"apiKey\":\"\",\"webhookKey\":\"\"}\n@link https://docs.stripe.com/api",
"default": {
"apiKey": "",
"webhookKey": ""
}
},
"revenuecat": {
"type": "object",
"description": "RevenueCat integration configs\n@default {\"enabled\":false,\"apiKey\":\"\",\"projectId\":\"\",\"webhookAuth\":\"\",\"environment\":\"production\",\"productMap\":{}}\n@link https://www.revenuecat.com/docs/",
"default": {
"enabled": false,
"apiKey": "",
"projectId": "",
"webhookAuth": "",
"environment": "production",
"productMap": {}
}
"description": "Stripe sdk options\n@default {}\n@link https://docs.stripe.com/api",
"default": {}
}
}
},

View File

@@ -74,11 +74,3 @@ body:
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images here
- type: checkboxes
attributes:
label: Is your content generated by AI?
description: >
(Required) Please confirm that the content you submit was not generated by AI or only minimally edited by AI.
If an administrator believes the post contains a large amount of AI-generated content, they may directly close the question.
options:
- label: I confirm that the content I submitted was **not** generated by AI / **merely contained minimal** AI edits.

View File

@@ -35,11 +35,3 @@ body:
See the AFFiNE [Contributing Guide](https://github.com/toeverything/affine/blob/canary/CONTRIBUTING.md) to get started.
options:
- label: Yes I'd like to help by submitting a PR!
- type: checkboxes
attributes:
label: Is your content generated by AI?
description: >
(Required) Please confirm that the content you submit was not generated by AI or only minimally edited by AI.
If an administrator believes the post contains a large amount of AI-generated content, they may directly close the question.
options:
- label: I confirm that the content I submitted was **not** generated by AI / **merely contained minimal** AI edits.

View File

@@ -75,11 +75,7 @@ runs:
shell: bash
if: ${{ runner.os != 'Windows' && inputs.no-build != 'true' }}
run: |
if [[ "${{ inputs.target }}" == "x86_64-unknown-linux-gnu" ]]; then
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }}
else
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }} --use-napi-cross
fi
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }} --use-napi-cross
env:
DEBUG: 'napi:*'

View File

@@ -1,6 +1,9 @@
name: 'Deploy to Cluster'
description: 'Deploy AFFiNE Cloud to cluster'
inputs:
build-type:
description: 'Align with App build type, canary|beta|stable|internal'
default: 'canary'
gcp-project-number:
description: 'GCP project number'
required: true
@@ -33,3 +36,5 @@ runs:
- name: Deploy
shell: bash
run: node ./.github/actions/deploy/deploy.mjs
env:
BUILD_TYPE: '${{ inputs.build-type }}'

View File

@@ -18,7 +18,8 @@ const {
STATIC_IP_NAME,
AFFINE_INDEXER_SEARCH_PROVIDER,
AFFINE_INDEXER_SEARCH_ENDPOINT,
AFFINE_INDEXER_SEARCH_API_KEY,
AFFINE_INDEXER_SEARCH_USERNAME,
AFFINE_INDEXER_SEARCH_PASSWORD,
} = process.env;
const buildType = BUILD_TYPE || 'canary';
@@ -29,25 +30,25 @@ const isInternal = buildType === 'internal';
const replicaConfig = {
stable: {
web: 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,
web: 3,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 3,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 3,
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 3,
},
beta: {
web: 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,
web: 2,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.BETA_SYNC_REPLICA) || 2,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 2,
doc: Number(process.env.BETA_DOC_REPLICA) || 2,
},
canary: {
web: 1,
graphql: 1,
sync: 1,
renderer: 1,
doc: 1,
web: 2,
graphql: 2,
sync: 2,
renderer: 2,
doc: 2,
},
};
@@ -87,7 +88,8 @@ const createHelmCommand = ({ isDryRun }) => {
const indexerOptions = [
`--set-string global.indexer.provider="${AFFINE_INDEXER_SEARCH_PROVIDER}"`,
`--set-string global.indexer.endpoint="${AFFINE_INDEXER_SEARCH_ENDPOINT}"`,
`--set-string global.indexer.apiKey="${AFFINE_INDEXER_SEARCH_API_KEY}"`,
`--set-string global.indexer.username="${AFFINE_INDEXER_SEARCH_USERNAME}"`,
`--set-string global.indexer.password="${AFFINE_INDEXER_SEARCH_PASSWORD}"`,
];
const serviceAnnotations = [
`--set-json web.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
@@ -126,10 +128,7 @@ const createHelmCommand = ({ isDryRun }) => {
? 'internal'
: 'dev';
const hosts = (DEPLOY_HOST || CANARY_DEPLOY_HOST)
.split(',')
.map(host => host.trim())
.filter(host => host);
const host = DEPLOY_HOST || CANARY_DEPLOY_HOST;
const deployCommand = [
`helm upgrade --install affine .github/helm/affine`,
`--namespace ${namespace}`,
@@ -138,9 +137,7 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string global.app.buildType="${buildType}"`,
`--set global.ingress.enabled=true`,
`--set-json global.ingress.annotations="{ \\"kubernetes.io/ingress.class\\": \\"gce\\", \\"kubernetes.io/ingress.allow-http\\": \\"true\\", \\"kubernetes.io/ingress.global-static-ip-name\\": \\"${STATIC_IP_NAME}\\" }"`,
...hosts.map(
(host, index) => `--set global.ingress.hosts[${index}]=${host}`
),
`--set-string global.ingress.host="${host}"`,
`--set-string global.version="${APP_VERSION}"`,
...redisAndPostgres,
...indexerOptions,
@@ -148,14 +145,14 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string web.image.tag="${imageTag}"`,
`--set graphql.replicaCount=${replica.graphql}`,
`--set-string graphql.image.tag="${imageTag}"`,
`--set graphql.app.host=${hosts[0]}`,
`--set graphql.app.host=${host}`,
`--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.app.host=${host}`,
`--set renderer.replicaCount=${replica.renderer}`,
`--set-string doc.image.tag="${imageTag}"`,
`--set doc.app.host=${hosts[0]}`,
`--set doc.app.host=${host}`,
`--set doc.replicaCount=${replica.doc}`,
...serviceAnnotations,
...resources,

View File

@@ -1,41 +0,0 @@
name: Prepare Release
description: 'Prepare Release'
outputs:
APP_VERSION:
description: 'App Version'
value: ${{ steps.get-version.outputs.APP_VERSION }}
GIT_SHORT_HASH:
description: 'Git Short Hash'
value: ${{ steps.get-version.outputs.GIT_SHORT_HASH }}
BUILD_TYPE:
description: 'Build Type'
value: ${{ steps.get-version.outputs.BUILD_TYPE }}
runs:
using: 'composite'
steps:
- name: Get Version
id: get-version
shell: bash
run: |
GIT_SHORT_HASH=$(git rev-parse --short HEAD)
if [ "${{ github.ref_type }}" == "tag" ]; then
APP_VERSION=$(echo "${{ github.ref_name }}" | sed 's/^v//')
else
APP_VERSION=$(date '+%Y.%-m.%-d-canary.%-H%M')
fi
if [[ "$APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
BUILD_TYPE=stable
elif [[ "$APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]]; then
BUILD_TYPE=beta
elif [[ "$APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-canary\.[0-9a-f]+$ ]]; then
BUILD_TYPE=canary
else
echo "Error: unsupported version string: $APP_VERSION" >&2
exit 1
fi
echo $APP_VERSION
echo $GIT_SHORT_HASH
echo $BUILD_TYPE
echo "APP_VERSION=$APP_VERSION" >> "$GITHUB_OUTPUT"
echo "GIT_SHORT_HASH=$GIT_SHORT_HASH" >> "$GITHUB_OUTPUT"
echo "BUILD_TYPE=$BUILD_TYPE" >> "$GITHUB_OUTPUT"

View File

@@ -4,6 +4,11 @@ description: 'Prepare Server Test Environment'
runs:
using: 'composite'
steps:
- name: Bundle @affine/reader
shell: bash
run: |
yarn affine @affine/reader build
- name: Initialize database
shell: bash
run: |
@@ -24,7 +29,11 @@ runs:
- name: Import config
shell: bash
env:
DEFAULT_CONFIG: '{}'
run: |
printf '%s\n' "${SERVER_CONFIG:-$DEFAULT_CONFIG}" > ./packages/backend/server/config.json
printf '{"copilot":{"enabled":true,"providers.fal":{"apiKey":"%s"},"providers.gemini":{"apiKey":"%s"},"providers.openai":{"apiKey":"%s"},"providers.perplexity":{"apiKey":"%s"},"providers.anthropic":{"apiKey":"%s"},"exa":{"key":"%s"}}}' \
"$COPILOT_FAL_API_KEY" \
"$COPILOT_GOOGLE_API_KEY" \
"$COPILOT_OPENAI_API_KEY" \
"$COPILOT_PERPLEXITY_API_KEY" \
"$COPILOT_ANTHROPIC_API_KEY" \
"$COPILOT_EXA_API_KEY" > ./packages/backend/server/config.json

View File

@@ -1,18 +1,24 @@
name: Setup Version
description: 'Setup Version'
inputs:
app-version:
outputs:
APP_VERSION:
description: 'App Version'
required: true
ios-app-version:
description: 'iOS App Store Version (Optional, use App version if empty)'
required: false
type: string
value: ${{ steps.version.outputs.APP_VERSION }}
runs:
using: 'composite'
steps:
- name: 'Write Version'
id: version
shell: bash
env:
IOS_APP_VERSION: ${{ inputs.ios-app-version }}
run: ./scripts/set-version.sh ${{ inputs.app-version }}
run: |
if [ "${{ github.ref_type }}" == "tag" ]; then
APP_VERSION=$(echo "${{ github.ref_name }}" | sed 's/^v//')
else
PACKAGE_VERSION=$(node -p "require('./package.json').version")
TIME_VERSION=$(date +%Y%m%d%H%M)
GIT_SHORT_HASH=$(git rev-parse --short HEAD)
APP_VERSION=$PACKAGE_VERSION-nightly-$GIT_SHORT_HASH
fi
echo $APP_VERSION
echo "APP_VERSION=$APP_VERSION" >> "$GITHUB_OUTPUT"
./scripts/set-version.sh $APP_VERSION

View File

@@ -7,10 +7,7 @@ COPY ./packages/frontend/apps/mobile/dist /app/static/mobile
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends openssl libjemalloc2 && \
apt-get install -y --no-install-recommends openssl && \
rm -rf /var/lib/apt/lists/*
# Enable jemalloc by preloading the library
ENV LD_PRELOAD=libjemalloc.so.2
CMD ["node", "./dist/main.js"]

View File

@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: "0.25.7"
appVersion: "0.21.0"

View File

@@ -3,7 +3,7 @@ name: doc
description: AFFiNE doc server
type: application
version: 0.0.0
appVersion: "0.25.7"
appVersion: "0.20.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -73,11 +73,13 @@ spec:
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
- name: AFFINE_INDEXER_SEARCH_USERNAME
value: "{{ .Values.global.indexer.username }}"
- name: AFFINE_INDEXER_SEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
key: indexer-password
- name: AFFINE_SERVER_PORT
value: "{{ .Values.global.docService.port }}"
- name: AFFINE_SERVER_SUB_PATH
@@ -95,13 +97,11 @@ spec:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
readinessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}

View File

@@ -1,6 +1,6 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
repository: ghcr.io/toeverything/affine-graphql
pullPolicy: IfNotPresent
tag: ''
@@ -36,7 +36,6 @@ resources:
probe:
initialDelaySeconds: 20
timeoutSeconds: 5
nodeSelector: {}
tolerations: []

View File

@@ -1,4 +1,4 @@
replicaCount: 2
replicaCount: 3
enabled: false
database:
connectionName: ""
@@ -33,11 +33,8 @@ service:
resources:
limits:
memory: "1Gi"
cpu: "1"
requests:
memory: "512Mi"
cpu: "100m"
memory: "4Gi"
cpu: "2"
volumes: []
volumeMounts: []

View File

@@ -3,7 +3,7 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: "0.25.7"
appVersion: "0.21.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -71,11 +71,13 @@ spec:
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
- name: AFFINE_INDEXER_SEARCH_USERNAME
value: "{{ .Values.global.indexer.username }}"
- name: AFFINE_INDEXER_SEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
key: indexer-password
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_SUB_PATH

View File

@@ -48,11 +48,13 @@ spec:
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
- name: AFFINE_INDEXER_SEARCH_USERNAME
value: "{{ .Values.global.indexer.username }}"
- name: AFFINE_INDEXER_SEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
key: indexer-password
resources:
requests:
cpu: '100m'

View File

@@ -1,6 +1,6 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
repository: ghcr.io/toeverything/affine-graphql
pullPolicy: IfNotPresent
tag: ''

View File

@@ -3,7 +3,7 @@ name: renderer
description: AFFiNE renderer server
type: application
version: 0.0.0
appVersion: "0.25.7"
appVersion: "0.16.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -73,11 +73,13 @@ spec:
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
- name: AFFINE_INDEXER_SEARCH_USERNAME
value: "{{ .Values.global.indexer.username }}"
- name: AFFINE_INDEXER_SEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
key: indexer-password
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_SUB_PATH

View File

@@ -1,6 +1,6 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
repository: ghcr.io/toeverything/affine-graphql
pullPolicy: IfNotPresent
tag: ''

View File

@@ -3,7 +3,7 @@ name: sync
description: AFFiNE Sync Server
type: application
version: 0.0.0
appVersion: "0.25.7"
appVersion: "0.21.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -73,11 +73,13 @@ spec:
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
- name: AFFINE_INDEXER_SEARCH_USERNAME
value: "{{ .Values.global.indexer.username }}"
- name: AFFINE_INDEXER_SEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
key: indexer-password
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_HOST

View File

@@ -1,6 +1,6 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
repository: ghcr.io/toeverything/affine-graphql
pullPolicy: IfNotPresent
tag: ''

View File

@@ -1,4 +1,4 @@
{{- if .Values.global.indexer.apiKey -}}
{{- if .Values.global.indexer.password -}}
apiVersion: v1
kind: Secret
metadata:
@@ -9,5 +9,5 @@ metadata:
"helm.sh/hook-delete-policy": before-hook-creation
type: Opaque
data:
indexer-apiKey: {{ .Values.global.indexer.apiKey | b64enc }}
indexer-password: {{ .Values.global.indexer.password | b64enc }}
{{- end }}

View File

@@ -36,8 +36,7 @@ spec:
{{- end }}
{{- end }}
rules:
{{- range .Values.global.ingress.hosts }}
- host: {{ . | quote }}
- host: "{{ .Values.global.ingress.host }}"
http:
paths:
- path: /socket.io
@@ -46,34 +45,33 @@ spec:
service:
name: affine-sync
port:
number: {{ $.Values.sync.service.port }}
number: {{ .Values.sync.service.port }}
- path: /graphql
pathType: Prefix
backend:
service:
name: affine-graphql
port:
number: {{ $.Values.graphql.service.port }}
number: {{ .Values.graphql.service.port }}
- path: /api
pathType: Prefix
backend:
service:
name: affine-graphql
port:
number: {{ $.Values.graphql.service.port }}
number: {{ .Values.graphql.service.port }}
- path: /workspace
pathType: Prefix
backend:
service:
name: affine-renderer
port:
number: {{ $.Values.renderer.service.port }}
number: {{ .Values.renderer.service.port }}
- path: /
pathType: Prefix
backend:
service:
name: affine-web
port:
number: {{ $.Values.web.service.port }}
{{- end }}
number: {{ .Values.web.service.port }}
{{- end }}

View File

@@ -1,13 +0,0 @@
{{- if eq .Values.global.deployment.platform "gcp" -}}
apiVersion: monitoring.googleapis.com/v1
kind: PodMonitoring
metadata:
name: "{{ .Release.Name }}-monitoring"
spec:
selector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
endpoints:
- port: 9464
interval: 30s
{{- end }}

View File

@@ -4,13 +4,7 @@ global:
ingress:
enabled: false
className: ''
# hosts for ingress rules
# e.g.
# hosts:
# - affine.pro
# - www.affine.pro
hosts:
- affine.pro
host: affine.pro
tls: []
secret:
secretName: 'server-private-key'

View File

@@ -3,13 +3,7 @@ name: Build Images
on:
workflow_call:
inputs:
build-type:
type: string
required: true
app-version:
type: string
required: true
git-short-hash:
flavor:
type: string
required: true
@@ -22,13 +16,12 @@ jobs:
build-web:
name: Build @affine/web
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Core
@@ -37,11 +30,11 @@ jobs:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
BUILD_TYPE: ${{ inputs.build-type }}
BUILD_TYPE: ${{ github.event.inputs.flavor }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine-web'
SENTRY_RELEASE: ${{ inputs.app-version }}
SENTRY_RELEASE: ${{ steps.version.outputs.APP_VERSION }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }}
@@ -56,13 +49,12 @@ jobs:
build-admin:
name: Build @affine/admin
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Admin
@@ -71,7 +63,7 @@ jobs:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
BUILD_TYPE: ${{ inputs.build-type }}
BUILD_TYPE: ${{ github.event.inputs.flavor }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine-admin'
@@ -89,13 +81,12 @@ jobs:
build-mobile:
name: Build @affine/mobile
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Mobile
@@ -104,7 +95,7 @@ jobs:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
BUILD_TYPE: ${{ inputs.build-type }}
BUILD_TYPE: ${{ github.event.inputs.flavor }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine-mobile'
@@ -122,7 +113,6 @@ jobs:
build-server-native:
name: Build Server native - ${{ matrix.targets.name }}
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
strategy:
fail-fast: false
matrix:
@@ -137,9 +127,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -149,7 +138,6 @@ jobs:
uses: ./.github/actions/build-rust
env:
AFFINE_PRO_PUBLIC_KEY: ${{ secrets.AFFINE_PRO_PUBLIC_KEY }}
AFFINE_PRO_LICENSE_AES_KEY: ${{ secrets.AFFINE_PRO_LICENSE_AES_KEY }}
with:
target: ${{ matrix.targets.name }}
package: '@affine/server-native'
@@ -171,9 +159,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -187,6 +174,8 @@ jobs:
path: ./packages/backend/native
- name: List server-native files
run: ls -alh ./packages/backend/native
- name: Build @affine/reader
run: yarn workspace @affine/reader build
- name: Build Server
run: yarn workspace @affine/server build
- name: Upload server dist
@@ -211,6 +200,16 @@ jobs:
with:
name: server-dist
path: ./packages/backend/server/dist
- name: Setup env
run: |
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
if [ -z "${{ inputs.flavor }}" ]
then
echo "RELEASE_FLAVOR=canary" >> "$GITHUB_ENV"
else
echo "RELEASE_FLAVOR=${{ inputs.flavor }}" >> "$GITHUB_ENV"
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -262,9 +261,8 @@ jobs:
run: mv ./node_modules ./packages/backend/server
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Build front Dockerfile
uses: docker/build-push-action@v6
@@ -275,7 +273,7 @@ jobs:
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 }}
tags: ghcr.io/toeverything/affine-front:${{env.RELEASE_FLAVOR}}-${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:${{env.RELEASE_FLAVOR}}
- name: Build graphql Dockerfile
uses: docker/build-push-action@v6
@@ -286,4 +284,4 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7
provenance: true
file: .github/deployment/node/Dockerfile
tags: ghcr.io/toeverything/affine:${{inputs.build-type}}-${{ inputs.git-short-hash }}
tags: ghcr.io/toeverything/affine-graphql:${{env.RELEASE_FLAVOR}}-${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-graphql:${{env.RELEASE_FLAVOR}}

View File

@@ -0,0 +1,25 @@
name: Build Selfhost Image
on:
workflow_dispatch:
inputs:
flavor:
description: 'Select distribution to build'
type: choice
default: canary
options:
- canary
- beta
- stable
permissions:
contents: 'write'
id-token: 'write'
packages: 'write'
jobs:
build-image:
name: Build Image
uses: ./.github/workflows/build-images.yml
with:
flavor: ${{ github.event.inputs.flavor }}

View File

@@ -11,7 +11,6 @@ on:
paths-ignore:
- README.md
pull_request:
merge_group:
env:
DEBUG: napi:*
@@ -19,20 +18,34 @@ env:
APP_NAME: affine
AFFINE_ENV: dev
COVERAGE: true
MACOSX_DEPLOYMENT_TARGET: '11.6'
MACOSX_DEPLOYMENT_TARGET: '10.13'
DEPLOYMENT_TYPE: affine
AFFINE_INDEXER_ENABLED: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
optimize_ci:
name: Optimize CI
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.check_skip.outputs.skip }}
steps:
- uses: actions/checkout@v4
- name: Graphite CI Optimizer
uses: withgraphite/graphite-ci-action@main
id: check_skip
with:
graphite_token: ${{ secrets.GRAPHITE_CI_OPTIMIZER_TOKEN }}
analyze:
name: Analyze
runs-on: ubuntu-latest
env:
NODE_OPTIONS: --max-old-space-size=14384
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
permissions:
actions: read
contents: read
@@ -66,6 +79,9 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-24.04-arm
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
- name: Run oxlint
@@ -91,6 +107,8 @@ jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-24.04-arm
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
NODE_OPTIONS: --max-old-space-size=14384
steps:
@@ -107,7 +125,6 @@ jobs:
- name: Run BS Docs Build
run: |
yarn affine bs-docs build
git checkout packages/frontend/i18n/src/i18n-completenesses.json
git status --porcelain | grep . && {
echo "Run 'yarn typecheck && yarn affine bs-docs build' and make sure all changes are submitted"
exit 1
@@ -118,6 +135,8 @@ jobs:
lint-rust:
name: Lint Rust
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/build-rust
@@ -131,14 +150,15 @@ jobs:
- name: Clippy
run: |
rustup component add clippy
cargo clippy --workspace --exclude affine_server_native --all-targets --all-features -- -D warnings
cargo clippy -p affine_server_native --all-targets --all-features -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
check-git-status:
name: Check Git Status
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -152,13 +172,17 @@ jobs:
name: server-native.node
path: ./packages/backend/native
- name: Bundle @affine/reader
shell: bash
run: |
yarn workspace @affine/reader build
- name: Run Check
run: |
yarn affine init
yarn affine gql build
yarn affine i18n build
yarn affine server genconfig
git checkout packages/frontend/i18n/src/i18n-completenesses.json
git status --porcelain | grep . && {
echo "Run 'yarn affine init && yarn affine gql build && yarn affine i18n build && yarn affine server genconfig' and make sure all changes are submitted"
exit 1
@@ -169,6 +193,8 @@ jobs:
check-yarn-binary:
name: Check yarn binary
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
- name: Run check
@@ -179,6 +205,8 @@ jobs:
e2e-blocksuite-test:
name: E2E BlockSuite Test
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
matrix:
@@ -210,6 +238,8 @@ jobs:
e2e-blocksuite-cross-browser-test:
name: E2E BlockSuite Cross Browser Test
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
matrix:
@@ -244,6 +274,8 @@ jobs:
e2e-test:
name: E2E Test
runs-on: ubuntu-24.04-arm
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
DISTRIBUTION: web
IN_CI_TEST: true
@@ -276,6 +308,8 @@ jobs:
e2e-mobile-test:
name: E2E Mobile Test
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
DISTRIBUTION: mobile
IN_CI_TEST: true
@@ -307,7 +341,9 @@ jobs:
name: Unit Test
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-native
if: needs.optimize_ci.outputs.skip == 'false'
env:
DISTRIBUTION: web
strategy:
@@ -344,6 +380,8 @@ jobs:
build-native:
name: Build AFFiNE native (${{ matrix.spec.target }})
runs-on: ${{ matrix.spec.os }}
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
strategy:
@@ -386,6 +424,8 @@ jobs:
build-windows-native:
name: Build AFFiNE native (${{ matrix.spec.target }})
runs-on: ${{ matrix.spec.os }}
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
strategy:
@@ -433,6 +473,8 @@ jobs:
build-server-native:
name: Build Server native
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
@@ -458,6 +500,8 @@ jobs:
build-electron-renderer:
name: Build @affine/electron renderer
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -483,7 +527,9 @@ jobs:
name: Native Unit Test
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-native
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -503,7 +549,9 @@ jobs:
name: Server Test
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
matrix:
@@ -534,11 +582,25 @@ jobs:
ports:
- 1025:1025
- 8025:8025
indexer:
image: manticoresearch/manticore:10.1.0
manticoresearch:
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
steps:
# https://github.com/elastic/elastic-github-actions/blob/master/elasticsearch/README.md
- name: Configure sysctl limits for Elasticsearch
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
- name: Runs Elasticsearch
uses: elastic/elastic-github-actions/elasticsearch@master
with:
stack-version: 9.0.1
security-enabled: false
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -572,94 +634,14 @@ jobs:
name: affine
fail_ci_if_error: false
server-test-elasticsearch:
name: Server Test with Elasticsearch
runs-on: ubuntu-latest
needs:
- build-server-native
strategy:
fail-fast: false
env:
NODE_ENV: test
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
AFFINE_INDEXER_SEARCH_PROVIDER: elasticsearch
AFFINE_INDEXER_SEARCH_ENDPOINT: http://localhost:9200
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
# https://github.com/elastic/elastic-github-actions/blob/master/elasticsearch/README.md
- name: Configure sysctl limits for Elasticsearch
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
- name: Runs Elasticsearch
uses: elastic/elastic-github-actions/elasticsearch@master
with:
stack-version: 9.0.1
security-enabled: false
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
full-cache: true
- name: Download server-native.node
uses: actions/download-artifact@v4
with:
name: server-native.node
path: ./packages/backend/native
- name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env
- name: Run server tests with elasticsearch only
run: yarn affine @affine/server test:coverage "**/*/*elasticsearch.spec.ts" --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
CI_NODE_INDEX: ${{ matrix.node_index }}
CI_NODE_TOTAL: ${{ matrix.total_nodes }}
- name: Upload server test coverage results
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: false
server-e2e-test:
# the new version of server e2e test should be super fast, so sharding testing is not needed
name: Server E2E Test
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false'
env:
NODE_ENV: test
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
@@ -681,7 +663,7 @@ jobs:
ports:
- 6379:6379
indexer:
image: manticoresearch/manticore:10.1.0
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
steps:
@@ -717,6 +699,9 @@ jobs:
miri:
name: miri code check
runs-on: ubuntu-latest
needs:
- optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
RUST_BACKTRACE: full
CARGO_TERM_COLOR: always
@@ -730,9 +715,7 @@ jobs:
toolchain: nightly
components: miri
- name: Install latest nextest release
uses: taiki-e/install-action@v2
with:
tool: nextest@0.9.98
uses: taiki-e/install-action@nextest
- name: Miri Code Check
continue-on-error: true
@@ -742,6 +725,9 @@ jobs:
loom:
name: loom thread test
runs-on: ubuntu-latest
needs:
- optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
RUSTFLAGS: --cfg loom
RUST_BACKTRACE: full
@@ -754,9 +740,7 @@ jobs:
with:
toolchain: stable
- name: Install latest nextest release
uses: taiki-e/install-action@v2
with:
tool: nextest@0.9.98
uses: taiki-e/install-action@nextest
- name: Loom Thread Test
run: |
@@ -765,7 +749,11 @@ jobs:
fuzzing:
name: fuzzing
runs-on: ubuntu-latest
needs:
- optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
RUSTFLAGS: -D warnings
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
@@ -807,10 +795,13 @@ jobs:
settings:
- { target: 'x86_64-unknown-linux-gnu', os: 'ubuntu-latest' }
- { target: 'aarch64-unknown-linux-gnu', os: 'ubuntu-24.04-arm' }
- { target: 'x86_64-apple-darwin', os: 'macos-15-intel' }
- { target: 'x86_64-apple-darwin', os: 'macos-13' }
- { target: 'aarch64-apple-darwin', os: 'macos-latest' }
- { target: 'x86_64-pc-windows-msvc', os: 'windows-latest' }
- { target: 'aarch64-pc-windows-msvc', os: 'windows-11-arm' }
needs:
- optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -844,6 +835,8 @@ jobs:
rust-test:
name: Run native tests
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
CARGO_TERM_COLOR: always
steps:
@@ -855,18 +848,18 @@ jobs:
no-build: 'true'
- name: Install latest nextest release
uses: taiki-e/install-action@v2
with:
tool: nextest@0.9.98
uses: taiki-e/install-action@nextest
- name: Run tests
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
run: cargo nextest run --release --no-fail-fast
copilot-api-test:
name: Server Copilot Api Test
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false'
env:
NODE_ENV: test
DISTRIBUTION: web
@@ -893,10 +886,6 @@ jobs:
ports:
- 1025:1025
- 8025:8025
indexer:
image: manticoresearch/manticore:10.1.0
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
@@ -936,7 +925,12 @@ jobs:
- name: Prepare Server Test Environment
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
env:
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env
- name: Run server tests
@@ -967,8 +961,8 @@ jobs:
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shardTotal: [10]
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
needs:
- build-server-native
services:
@@ -987,10 +981,6 @@ jobs:
image: redis
ports:
- 6379:6379
indexer:
image: manticoresearch/manticore:10.1.0
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
@@ -1013,7 +1003,6 @@ jobs:
- 'packages/backend/server/src/plugins/copilot/**'
- 'packages/backend/server/tests/copilot.*'
- 'packages/frontend/core/src/blocksuite/ai/**'
- 'packages/frontend/core/src/modules/workspace-indexer-embedding/**'
- 'tests/affine-cloud-copilot/**'
- name: Setup Node.js
@@ -1035,7 +1024,12 @@ jobs:
- name: Prepare Server Test Environment
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
env:
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
@@ -1048,8 +1042,10 @@ jobs:
name: ${{ matrix.tests.name }}
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
- build-native
if: needs.optimize_ci.outputs.skip == 'false'
env:
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
@@ -1059,36 +1055,24 @@ jobs:
fail-fast: false
matrix:
tests:
- name: 'Cloud E2E Test 1/10'
- name: 'Cloud E2E Test 1/6'
shard: 1
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/10
- name: 'Cloud E2E Test 2/10'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/6
- name: 'Cloud E2E Test 2/6'
shard: 2
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/10
- name: 'Cloud E2E Test 3/10'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/6
- name: 'Cloud E2E Test 3/6'
shard: 3
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=3/10
- name: 'Cloud E2E Test 4/10'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=3/6
- name: 'Cloud E2E Test 4/6'
shard: 4
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=4/10
- name: 'Cloud E2E Test 5/10'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=4/6
- name: 'Cloud E2E Test 5/6'
shard: 5
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=5/10
- name: 'Cloud E2E Test 6/10'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=5/6
- name: 'Cloud E2E Test 6/6'
shard: 6
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=6/10
- name: 'Cloud E2E Test 7/10'
shard: 7
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=7/10
- name: 'Cloud E2E Test 8/10'
shard: 8
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=8/10
- name: 'Cloud E2E Test 9/10'
shard: 9
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=9/10
- name: 'Cloud E2E Test 10/10'
shard: 10
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=10/10
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=6/6
- name: 'Cloud Desktop E2E Test'
shard: desktop
script: |
@@ -1120,7 +1104,7 @@ jobs:
- 1025:1025
- 8025:8025
indexer:
image: manticoresearch/manticore:10.1.0
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
steps:
@@ -1165,8 +1149,10 @@ jobs:
name: Desktop Test (${{ matrix.spec.os }}, ${{ matrix.spec.platform }}, ${{ matrix.spec.arch }}, ${{ matrix.spec.target }}, ${{ matrix.spec.test }})
runs-on: ${{ matrix.spec.os }}
needs:
- optimize_ci
- build-electron-renderer
- build-native
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
matrix:
@@ -1261,8 +1247,10 @@ jobs:
name: Desktop bundle check (${{ matrix.spec.os }}, ${{ matrix.spec.platform }}, ${{ matrix.spec.arch }}, ${{ matrix.spec.target }}, ${{ matrix.spec.test }})
runs-on: ${{ matrix.spec.os }}
needs:
- optimize_ci
- build-electron-renderer
- build-native
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
matrix:
@@ -1288,13 +1276,6 @@ jobs:
target: x86_64-unknown-linux-gnu,
test: true,
}
- {
os: windows-latest,
platform: windows,
arch: x64,
target: x86_64-pc-windows-msvc,
test: true,
}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -1335,23 +1316,11 @@ jobs:
HOIST_NODE_MODULES: 1
run: yarn affine @affine/electron package --platform=darwin --arch=arm64
- name: Make Bundle (Windows)
if: ${{ matrix.spec.target == 'x86_64-pc-windows-msvc' }}
shell: bash
env:
SKIP_BUNDLE: true
SKIP_WEB_BUILD: true
HOIST_NODE_MODULES: 1
run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite/affine/node_modules
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
yarn affine @affine/electron package --platform=win32 --arch=x64
- name: Make Bundle (Linux)
run: |
sudo add-apt-repository universe
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak update
# some flatpak deps need git protocol.file.allow
git config --global protocol.file.allow always
@@ -1366,6 +1335,17 @@ jobs:
run: |
yarn affine @affine/electron node ./scripts/macos-arm64-output-check.ts
test-build-mobile-app:
uses: ./.github/workflows/release-mobile.yml
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
with:
build-type: canary
build-target: development
secrets: inherit
permissions:
id-token: 'write'
test-done:
needs:
- analyze
@@ -1396,6 +1376,7 @@ jobs:
- desktop-test
- desktop-bundle-check
- cloud-e2e-test
- test-build-mobile-app
if: always()
runs-on: ubuntu-latest
name: 3, 2, 1 Launch

View File

@@ -59,10 +59,6 @@ jobs:
ports:
- 1025:1025
- 8025:8025
indexer:
image: manticoresearch/manticore:10.1.0
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
@@ -81,7 +77,12 @@ jobs:
- name: Prepare Server Test Environment
env:
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env
- name: Run server tests
@@ -109,8 +110,8 @@ jobs:
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shardTotal: [10]
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
needs:
- build-server-native
services:
@@ -129,10 +130,6 @@ jobs:
image: redis
ports:
- 6379:6379
indexer:
image: manticoresearch/manticore:10.1.0
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
@@ -151,7 +148,12 @@ jobs:
- name: Prepare Server Test Environment
env:
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

View File

@@ -0,0 +1,32 @@
name: Deploy Automatically
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-canary.[0-9]+'
schedule:
- cron: '0 9 * * *'
permissions:
contents: write
pull-requests: write
actions: write
jobs:
dispatch-deploy:
runs-on: ubuntu-latest
name: Setup Deploy
steps:
- name: dispatch deploy by tag
if: ${{ github.event_name == 'push' }}
uses: benc-uk/workflow-dispatch@v1
with:
workflow: deploy.yml
inputs: '{ "flavor": "canary" }'
- name: dispatch deploy by schedule
if: ${{ github.event_name == 'schedule' }}
uses: benc-uk/workflow-dispatch@v1
with:
workflow: deploy.yml
inputs: '{ "flavor": "canary" }'
ref: canary

190
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,190 @@
name: Deploy
on:
workflow_dispatch:
inputs:
flavor:
description: 'Select what enverionment to deploy to'
type: choice
default: canary
options:
- canary
- beta
- stable
- internal
permissions:
contents: 'write'
id-token: 'write'
packages: 'write'
jobs:
output-prev-version:
name: Output previous version
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
outputs:
prev: ${{ steps.print.outputs.version }}
namespace: ${{ steps.print.outputs.namespace }}
steps:
- uses: actions/checkout@v4
- name: Auth to Cluster
uses: './.github/actions/cluster-auth'
with:
gcp-project-number: ${{ secrets.GCP_PROJECT_NUMBER }}
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
service-account: ${{ secrets.GCP_HELM_DEPLOY_SERVICE_ACCOUNT }}
cluster-name: ${{ secrets.GCP_CLUSTER_NAME }}
cluster-location: ${{ secrets.GCP_CLUSTER_LOCATION }}
- name: Output previous version
id: print
run: |
namespace=""
if [ "${{ github.event.inputs.flavor }}" = "canary" ]; then
namespace="dev"
elif [ "${{ github.event.inputs.flavor }}" = "beta" ]; then
namespace="beta"
elif [ "${{ github.event.inputs.flavor }}" = "stable" ]; then
namespace="production"
else
echo "Invalid flavor: ${{ github.event.inputs.flavor }}"
exit 1
fi
echo "Namespace set to: $namespace"
# Get the previous version from the deployment
prev_version=$(kubectl get deployment -n $namespace affine-graphql -o=jsonpath='{.spec.template.spec.containers[0].image}' | awk -F '-' '{print $3}')
echo "Previous version: $prev_version"
echo "version=$prev_version" >> $GITHUB_OUTPUT
echo "namesapce=$namespace" >> $GITHUB_OUTPUT
build-images:
name: Build Images
uses: ./.github/workflows/build-images.yml
secrets: inherit
with:
flavor: ${{ github.event.inputs.flavor }}
deploy:
name: Deploy to cluster
if: ${{ github.event_name == 'workflow_dispatch' }}
environment: ${{ github.event.inputs.flavor }}
needs:
- build-images
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Deploy to ${{ github.event.inputs.flavor }}
uses: ./.github/actions/deploy
with:
build-type: ${{ github.event.inputs.flavor }}
gcp-project-number: ${{ secrets.GCP_PROJECT_NUMBER }}
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
service-account: ${{ secrets.GCP_HELM_DEPLOY_SERVICE_ACCOUNT }}
cluster-name: ${{ secrets.GCP_CLUSTER_NAME }}
cluster-location: ${{ secrets.GCP_CLUSTER_LOCATION }}
env:
APP_VERSION: ${{ steps.version.outputs.APP_VERSION }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
CANARY_DEPLOY_HOST: ${{ secrets.CANARY_DEPLOY_HOST }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
DATABASE_NAME: ${{ secrets.DATABASE_NAME }}
GCLOUD_CONNECTION_NAME: ${{ secrets.GCLOUD_CONNECTION_NAME }}
REDIS_SERVER_HOST: ${{ secrets.REDIS_SERVER_HOST }}
REDIS_SERVER_PASSWORD: ${{ secrets.REDIS_SERVER_PASSWORD }}
CLOUD_SQL_IAM_ACCOUNT: ${{ secrets.CLOUD_SQL_IAM_ACCOUNT }}
APP_IAM_ACCOUNT: ${{ secrets.APP_IAM_ACCOUNT }}
STATIC_IP_NAME: ${{ secrets.STATIC_IP_NAME }}
AFFINE_INDEXER_SEARCH_PROVIDER: ${{ secrets.AFFINE_INDEXER_SEARCH_PROVIDER }}
AFFINE_INDEXER_SEARCH_ENDPOINT: ${{ secrets.AFFINE_INDEXER_SEARCH_ENDPOINT }}
AFFINE_INDEXER_SEARCH_USERNAME: ${{ secrets.AFFINE_INDEXER_SEARCH_USERNAME }}
AFFINE_INDEXER_SEARCH_PASSWORD: ${{ secrets.AFFINE_INDEXER_SEARCH_PASSWORD }}
deploy-done:
needs:
- output-prev-version
- build-images
- deploy
if: always()
runs-on: ubuntu-latest
name: Post deploy message
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
repository: toeverything/blocksuite
path: blocksuite
fetch-depth: 0
fetch-tags: true
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: 'workspaces focus @affine/changelog'
electron-install: false
- name: Output deployed info
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
id: set_info
run: |
if [ "${{ github.event.inputs.flavor }}" = "canary" ]; then
echo "deployed_url=https://affine.fail" >> $GITHUB_OUTPUT
elif [ "${{ github.event.inputs.flavor }}" = "beta" ]; then
echo "deployed_url=https://insider.affine.pro" >> $GITHUB_OUTPUT
elif [ "${{ github.event.inputs.flavor }}" = "stable" ]; then
echo "deployed_url=https://app.affine.pro" >> $GITHUB_OUTPUT
else
exit 1
fi
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Post Success event to a Slack channel
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
run: node ./tools/changelog/index.js
env:
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
DEPLOYED_URL: ${{ steps.set_info.outputs.deployed_url }}
PREV_VERSION: ${{ needs.output-prev-version.outputs.prev }}
NAMESPACE: ${{ needs.output-prev-version.outputs.namespace }}
DEPLOYMENT: 'SERVER'
FLAVOR: ${{ github.event.inputs.flavor }}
BLOCKSUITE_REPO_PATH: ${{ github.workspace }}/blocksuite
- name: Post Failed event to a Slack channel
id: failed-slack
uses: slackapi/slack-github-action@v2.1.0
if: ${{ always() && contains(needs.*.result, 'failure') }}
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>"
blocks:
- type: section
text:
type: mrkdwn
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>"
- name: Post Cancel event to a Slack channel
id: cancel-slack
uses: slackapi/slack-github-action@v2.1.0
if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
with:
token: ${{ secrets.SLACK_BOT_TOKEN }}
method: chat.postMessage
payload: |
channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>"
blocks:
- type: section
text:
type: mrkdwn
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>"

66
.github/workflows/helm-releaser.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: Release Charts
on:
push:
branches: [canary]
paths:
- '.github/helm/**/Chart.yml'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout Helm chart repo
uses: actions/checkout@v4
with:
repository: toeverything/helm-charts
path: .helm-chart-repo
ref: gh-pages
token: ${{ secrets.HELM_RELEASER_TOKEN }}
- name: Install Helm
uses: azure/setup-helm@v4
- name: Install chart releaser
run: |
set -e
arch="$(dpkg --print-architecture)"
curl -s https://api.github.com/repos/helm/chart-releaser/releases/latest \
| yq --indent 0 --no-colors --input-format json --unwrapScalar \
".assets[] | select(.name | test("\""^chart-releaser_.+_linux_${arch}\.tar\.gz$"\"")) | .browser_download_url" \
| xargs curl -SsL \
| tar zxf - -C /usr/local/bin
- name: Package charts
working-directory: .helm-chart-repo
run: |
mkdir -p .cr-index
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm dependencies build ../.github/helm/affine
helm dependencies build ../.github/helm/affine-cloud
cr package ../.github/helm/affine
cr package ../.github/helm/affine-cloud
- name: Publish charts
working-directory: .helm-chart-repo
run: |
set -ex
git config --local user.name "$GITHUB_ACTOR"
git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com"
owner=$(cut -d '/' -f 1 <<< '${{ github.repository }}')
repo=helm-charts
git_hash=$(git rev-parse HEAD)
cr upload --commit "$git_hash" \
--git-repo "$repo" --owner "$owner" \
--token '${{ secrets.HELM_RELEASER_TOKEN }}' \
--skip-existing
cr index --git-repo "$repo" --owner "$owner" \
--token '${{ secrets.HELM_RELEASER_TOKEN }}' \
--index-path .cr-index --push

19
.github/workflows/label-checker.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Label Checker
on:
pull_request:
types:
- opened
- labeled
- unlabeled
branches:
- canary
jobs:
check_labels:
name: PR should not have a blocked label
runs-on: ubuntu-latest
steps:
- uses: docker://agilepathway/pull-request-label-checker:latest
with:
none_of: blocked
repo_token: ${{ secrets.GITHUB_TOKEN }}

12
.github/workflows/pr-auto-assign.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Pull request auto assign
# on: pull_request
on:
pull_request:
types: [opened, ready_for_review]
jobs:
add-reviews:
runs-on: ubuntu-latest
steps:
- uses: kentaro-m/auto-assign-action@v2.0.0

View File

@@ -0,0 +1,38 @@
name: Release Desktop/Mobile Automatically
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-canary.[0-9]+'
schedule:
- cron: '0 9 * * *'
permissions:
contents: write
pull-requests: write
actions: write
jobs:
dispatch-release-desktop:
runs-on: ubuntu-latest
name: Setup Release Desktop
steps:
- name: dispatch desktop release by tag
if: ${{ github.event_name == 'push' }}
uses: benc-uk/workflow-dispatch@v1
with:
workflow: release-desktop.yml
inputs: '{ "build-type": "canary", "is-draft": false, "is-pre-release": true }'
- name: dispatch desktop release by schedule
if: ${{ github.event_name == 'schedule' }}
uses: benc-uk/workflow-dispatch@v1
with:
workflow: release-desktop.yml
inputs: '{ "build-type": "canary", "is-draft": false, "is-pre-release": true }'
ref: canary
- name: dispatch desktop release by tag
uses: benc-uk/workflow-dispatch@v1
with:
workflow: release-mobile.yml
inputs: '{ "build-type": "canary", "build-target": "distribution" }'

View File

@@ -1,66 +0,0 @@
name: Release Cloud
on:
workflow_call:
inputs:
build-type:
required: true
type: string
app-version:
required: true
type: string
git-short-hash:
required: true
type: string
permissions:
contents: 'write'
id-token: 'write'
packages: 'write'
jobs:
build-images:
name: Build Images
uses: ./.github/workflows/build-images.yml
secrets: inherit
with:
build-type: ${{ inputs.build-type }}
app-version: ${{ inputs.app-version }}
git-short-hash: ${{ inputs.git-short-hash }}
deploy:
name: Deploy to cluster
environment: ${{ inputs.build-type }}
needs:
- build-images
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to ${{ inputs.build-type }}
uses: ./.github/actions/deploy
with:
gcp-project-number: ${{ secrets.GCP_PROJECT_NUMBER }}
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
service-account: ${{ secrets.GCP_HELM_DEPLOY_SERVICE_ACCOUNT }}
cluster-name: ${{ secrets.GCP_CLUSTER_NAME }}
cluster-location: ${{ secrets.GCP_CLUSTER_LOCATION }}
env:
BUILD_TYPE: ${{ inputs.build-type }}
APP_VERSION: ${{ inputs.app-version }}
GIT_SHORT_HASH: ${{ inputs.git-short-hash }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
CANARY_DEPLOY_HOST: ${{ secrets.CANARY_DEPLOY_HOST }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
DATABASE_NAME: ${{ secrets.DATABASE_NAME }}
GCLOUD_CONNECTION_NAME: ${{ secrets.GCLOUD_CONNECTION_NAME }}
REDIS_SERVER_HOST: ${{ secrets.REDIS_SERVER_HOST }}
REDIS_SERVER_PASSWORD: ${{ secrets.REDIS_SERVER_PASSWORD }}
CLOUD_SQL_IAM_ACCOUNT: ${{ secrets.CLOUD_SQL_IAM_ACCOUNT }}
APP_IAM_ACCOUNT: ${{ secrets.APP_IAM_ACCOUNT }}
STATIC_IP_NAME: ${{ secrets.STATIC_IP_NAME }}
AFFINE_INDEXER_SEARCH_PROVIDER: ${{ secrets.AFFINE_INDEXER_SEARCH_PROVIDER }}
AFFINE_INDEXER_SEARCH_ENDPOINT: ${{ secrets.AFFINE_INDEXER_SEARCH_ENDPOINT }}
AFFINE_INDEXER_SEARCH_API_KEY: ${{ secrets.AFFINE_INDEXER_SEARCH_API_KEY }}

View File

@@ -1,226 +0,0 @@
name: Release Desktop Platform
on:
workflow_call:
inputs:
build_type:
required: true
type: string
app_version:
required: true
type: string
git_short_hash:
required: true
type: string
runner:
required: true
type: string
platform:
required: true
type: string
arch:
required: true
type: string
target:
required: true
type: string
apple_codesign:
required: false
default: false
type: boolean
install_linux_deps:
required: false
default: false
type: boolean
enable_scripts:
required: false
default: false
type: boolean
outputs:
files_to_be_signed:
description: Files to be signed (Windows only)
value: ${{ jobs.build.outputs.files_to_be_signed }}
permissions:
actions: write
contents: write
security-events: write
id-token: write
attestations: write
jobs:
build:
runs-on: ${{ inputs.runner }}
outputs:
files_to_be_signed: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
env:
BUILD_TYPE: ${{ inputs.build_type }}
RELEASE_VERSION: ${{ inputs.app_version }}
DEBUG: 'affine:*,napi:*'
APP_NAME: affine
MACOSX_DEPLOYMENT_TARGET: '12.0'
SKIP_GENERATE_ASSETS: 1
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app_version }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app_version }}
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
hard-link-nm: false
nmHoistingLimits: workspaces
enableScripts: ${{ inputs.enable_scripts }}
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ inputs.target }}
package: '@affine/native'
- uses: actions/download-artifact@v4
with:
name: desktop-web
path: packages/frontend/apps/electron/resources/web-static
- name: Build Desktop Layers
run: yarn affine @affine/electron build
- name: Signing By Apple Developer ID
if: ${{ inputs.platform == 'darwin' && inputs.apple_codesign }}
uses: apple-actions/import-codesign-certs@v5
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: Install additional dependencies on Linux
if: ${{ inputs.platform == 'linux' && inputs.install_linux_deps }}
run: |
df -h
sudo add-apt-repository universe
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak update
# some flatpak deps need git protocol.file.allow
git config --global protocol.file.allow always
# clean up apt cache to save disk space
sudo -E apt-get -y purge azure-cli* zulu* hhvm* llvm* firefox* google* dotnet* aspnetcore* powershell* adoptopenjdk* mysql* php* mongodb* moby* snap* || true
sudo -E apt-get -qq autoremove --purge
sudo rm -rf /usr/share/dotnet /opt/ghc /opt/hostedtoolcache/CodeQL /usr/local/lib/android
sudo apt-get clean
rm -rf ~/.cache/yarn ~/.npm
df -h
- name: Remove nbstore node_modules (darwin/linux)
if: ${{ inputs.platform != 'win32' }}
shell: bash
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
run: |
cargo clean
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: Remove nbstore node_modules (windows)
if: ${{ inputs.platform == 'win32' }}
shell: bash
run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite/affine/node_modules
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: make
if: ${{ inputs.platform != 'win32' }}
run: yarn affine @affine/electron make --platform=${{ inputs.platform }} --arch=${{ inputs.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
NODE_OPTIONS: --max-old-space-size=14384
- name: package
if: ${{ inputs.platform == 'win32' }}
run: |
yarn affine @affine/electron package --platform=${{ inputs.platform }} --arch=${{ inputs.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
NODE_OPTIONS: --max-old-space-size=14384
- name: signing DMG
if: ${{ inputs.platform == 'darwin' && inputs.apple_codesign }}
run: |
codesign --force --sign "Developer ID Application: TOEVERYTHING PTE. LTD." packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make/AFFiNE.dmg
- name: Save artifacts (mac)
if: ${{ inputs.platform == 'darwin' }}
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.dmg
mv packages/frontend/apps/electron/out/*/make/zip/darwin/${{ inputs.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.zip
- name: Save artifacts (linux)
if: ${{ inputs.platform == 'linux' }}
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ inputs.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.zip
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.appimage
mv packages/frontend/apps/electron/out/*/make/deb/${{ inputs.arch }}/*.deb ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.deb
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.flatpak
- uses: actions/attest-build-provenance@v2
if: ${{ inputs.platform == 'darwin' }}
with:
subject-path: |
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.zip
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.dmg
- uses: actions/attest-build-provenance@v2
if: ${{ inputs.platform == 'linux' }}
with:
subject-path: |
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.zip
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.appimage
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.deb
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.flatpak
- name: Upload Artifact
if: ${{ inputs.platform == 'darwin' || inputs.platform == 'linux' }}
uses: actions/upload-artifact@v4
with:
name: affine-${{ inputs.platform }}-${{ inputs.arch }}-builds
path: builds
- name: get all files to be signed
id: get_files_to_be_signed
if: ${{ inputs.platform == 'win32' }}
shell: pwsh
run: |
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\', '') + '"' }) -join ' ')
"FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
echo $FILES_TO_BE_SIGNED
- name: Zip artifacts for faster upload
if: ${{ inputs.platform == 'win32' }}
shell: pwsh
run: Compress-Archive -CompressionLevel Fastest -Path packages/frontend/apps/electron/out/* -DestinationPath archive.zip
- name: Save packaged artifacts for signing
if: ${{ inputs.platform == 'win32' }}
uses: actions/upload-artifact@v4
with:
name: packaged-${{ inputs.platform }}-${{ inputs.arch }}
path: |
archive.zip
!**/*.map

View File

@@ -1,32 +1,27 @@
name: Release Desktop
name: Release Desktop App
on:
workflow_call:
workflow_dispatch:
inputs:
build-type:
description: 'Build Type'
type: choice
required: true
type: string
app-version:
default: canary
options:
- canary
- beta
- stable
is-draft:
description: 'Draft Release?'
type: boolean
required: true
type: string
git-short-hash:
default: true
is-pre-release:
description: 'Pre Release? (labeled as "PreRelease")'
type: boolean
required: true
type: string
desktop_macos:
description: 'Desktop - macOS'
required: false
default: true
type: boolean
desktop_windows:
description: 'Desktop - Windows'
required: false
default: true
type: boolean
desktop_linux:
description: 'Desktop - Linux'
required: false
default: true
type: boolean
permissions:
actions: write
@@ -36,23 +31,22 @@ permissions:
attestations: write
env:
BUILD_TYPE: ${{ inputs.build-type }}
RELEASE_VERSION: ${{ inputs.app-version }}
BUILD_TYPE: ${{ github.event.inputs.build-type }}
DEBUG: 'affine:*,napi:*'
APP_NAME: affine
MACOSX_DEPLOYMENT_TARGET: '11.6'
MACOSX_DEPLOYMENT_TARGET: '10.13'
jobs:
before-make:
if: ${{ inputs.desktop_macos || inputs.desktop_windows || inputs.desktop_linux }}
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
environment: ${{ github.event.inputs.build-type }}
outputs:
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
@@ -64,18 +58,17 @@ jobs:
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app-version }}
RELEASE_VERSION: ${{ inputs.app-version }}
SENTRY_RELEASE: ${{ steps.version.outputs.APP_VERSION }}
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload web artifact
uses: actions/upload-artifact@v4
with:
name: desktop-web
name: web
path: packages/frontend/apps/electron/resources/web-static
make-distribution-macos:
if: ${{ inputs.desktop_macos }}
make-distribution:
strategy:
fail-fast: false
matrix:
@@ -88,90 +81,221 @@ jobs:
platform: darwin
arch: arm64
target: aarch64-apple-darwin
needs: before-make
uses: ./.github/workflows/release-desktop-platform.yml
secrets: inherit
with:
build_type: ${{ inputs.build-type }}
app_version: ${{ inputs.app-version }}
git_short_hash: ${{ inputs.git-short-hash }}
runner: ${{ matrix.spec.runner }}
platform: ${{ matrix.spec.platform }}
arch: ${{ matrix.spec.arch }}
target: ${{ matrix.spec.target }}
apple_codesign: true
make-distribution-linux:
if: ${{ inputs.desktop_linux }}
strategy:
fail-fast: false
matrix:
spec:
- runner: ubuntu-latest
platform: linux
arch: x64
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.spec.runner }}
needs: before-make
uses: ./.github/workflows/release-desktop-platform.yml
secrets: inherit
with:
build_type: ${{ inputs.build-type }}
app_version: ${{ inputs.app-version }}
git_short_hash: ${{ inputs.git-short-hash }}
runner: ${{ matrix.spec.runner }}
platform: ${{ matrix.spec.platform }}
arch: ${{ matrix.spec.arch }}
target: ${{ matrix.spec.target }}
install_linux_deps: true
environment: ${{ github.event.inputs.build-type }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SKIP_GENERATE_ASSETS: 1
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ needs.before-make.outputs.RELEASE_VERSION }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
hard-link-nm: false
nmHoistingLimits: workspaces
enableScripts: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
package: '@affine/native'
- uses: actions/download-artifact@v4
with:
name: web
path: packages/frontend/apps/electron/resources/web-static
package-distribution-windows-x64:
if: ${{ inputs.desktop_windows }}
needs: before-make
uses: ./.github/workflows/release-desktop-platform.yml
secrets: inherit
with:
build_type: ${{ inputs.build-type }}
app_version: ${{ inputs.app-version }}
git_short_hash: ${{ inputs.git-short-hash }}
runner: windows-latest
platform: win32
arch: x64
target: x86_64-pc-windows-msvc
enable_scripts: true
- name: Build Desktop Layers
run: yarn affine @affine/electron build
package-distribution-windows-arm64:
if: ${{ inputs.desktop_windows }}
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'darwin' }}
uses: apple-actions/import-codesign-certs@v5
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: Install additional dependencies on Linux
if: ${{ matrix.spec.platform == 'linux' }}
run: |
sudo add-apt-repository universe
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak update
# some flatpak deps need git protocol.file.allow
git config --global protocol.file.allow always
- name: Remove nbstore node_modules
shell: bash
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: make
run: yarn affine @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
NODE_OPTIONS: --max-old-space-size=14384
- name: signing DMG
if: ${{ matrix.spec.platform == 'darwin' }}
run: |
codesign --force --sign "Developer ID Application: TOEVERYTHING PTE. LTD." packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make/AFFiNE.dmg
- name: Save artifacts (mac)
if: ${{ matrix.spec.platform == 'darwin' }}
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/*.dmg ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
mv packages/frontend/apps/electron/out/*/make/zip/darwin/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
- name: Save artifacts (linux)
if: ${{ matrix.spec.platform == 'linux' }}
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.zip
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.appimage
mv packages/frontend/apps/electron/out/*/make/deb/${{ matrix.spec.arch }}/*.deb ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.deb
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.flatpak
- uses: actions/attest-build-provenance@v2
if: ${{ matrix.spec.platform == 'darwin' }}
with:
subject-path: |
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
- uses: actions/attest-build-provenance@v2
if: ${{ matrix.spec.platform == 'linux' }}
with:
subject-path: |
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.zip
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.appimage
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.deb
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
path: builds
package-distribution-windows:
environment: ${{ github.event.inputs.build-type }}
strategy:
fail-fast: false
matrix:
spec:
- runner: windows-latest
platform: win32
arch: x64
target: x86_64-pc-windows-msvc
- runner: windows-latest
platform: win32
arch: arm64
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.spec.runner }}
needs: before-make
uses: ./.github/workflows/release-desktop-platform.yml
secrets: inherit
with:
build_type: ${{ inputs.build-type }}
app_version: ${{ inputs.app-version }}
git_short_hash: ${{ inputs.git-short-hash }}
runner: windows-latest
platform: win32
arch: arm64
target: aarch64-pc-windows-msvc
enable_scripts: true
outputs:
FILES_TO_BE_SIGNED_x64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_x64 }}
FILES_TO_BE_SIGNED_arm64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_arm64 }}
env:
SKIP_GENERATE_ASSETS: 1
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ needs.before-make.outputs.RELEASE_VERSION }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
hard-link-nm: false
nmHoistingLimits: workspaces
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.spec.target }}
package: '@affine/native'
- uses: actions/download-artifact@v4
with:
name: web
path: packages/frontend/apps/electron/resources/web-static
- name: Build Desktop Layers
run: yarn affine @affine/electron build
- name: Remove nbstore node_modules
shell: bash
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: package
run: |
yarn affine @affine/electron package --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
NODE_OPTIONS: --max-old-space-size=14384
- name: get all files to be signed
id: get_files_to_be_signed
run: |
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\', '') + '"' }) -join ' ')
"FILES_TO_BE_SIGNED_${{ matrix.spec.arch }}=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
echo $FILES_TO_BE_SIGNED
- name: Zip artifacts for faster upload
run: Compress-Archive -CompressionLevel Fastest -Path packages/frontend/apps/electron/out/* -DestinationPath archive.zip
- name: Save packaged artifacts for signing
uses: actions/upload-artifact@v4
with:
name: packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: |
archive.zip
!**/*.map
sign-packaged-artifacts-windows_x64:
if: ${{ inputs.desktop_windows }}
needs: package-distribution-windows-x64
needs: package-distribution-windows
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.package-distribution-windows-x64.outputs.files_to_be_signed }}
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED_x64 }}
artifact-name: packaged-win32-x64
sign-packaged-artifacts-windows_arm64:
if: ${{ inputs.desktop_windows }}
needs: package-distribution-windows-arm64
needs: package-distribution-windows
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.package-distribution-windows-arm64.outputs.files_to_be_signed }}
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED_arm64 }}
artifact-name: packaged-win32-arm64
make-windows-installer:
if: ${{ inputs.desktop_windows }}
needs:
- sign-packaged-artifacts-windows_x64
- sign-packaged-artifacts-windows_arm64
@@ -190,9 +314,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
@@ -233,7 +356,6 @@ jobs:
path: archive.zip
sign-installer-artifacts-windows-x64:
if: ${{ inputs.desktop_windows }}
needs: make-windows-installer
uses: ./.github/workflows/windows-signer.yml
with:
@@ -241,7 +363,6 @@ jobs:
artifact-name: installer-win32-x64
sign-installer-artifacts-windows-arm64:
if: ${{ inputs.desktop_windows }}
needs: make-windows-installer
uses: ./.github/workflows/windows-signer.yml
with:
@@ -249,7 +370,6 @@ jobs:
artifact-name: installer-win32-arm64
finalize-installer-windows:
if: ${{ inputs.desktop_windows }}
needs:
[
sign-installer-artifacts-windows-x64,
@@ -279,16 +399,16 @@ jobs:
- name: Save artifacts
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/zip/win32/${{ matrix.spec.arch }}/AFFiNE*-win32-${{ matrix.spec.arch }}-*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
mv packages/frontend/apps/electron/out/*/make/squirrel.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
mv packages/frontend/apps/electron/out/*/make/nsis.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
mv packages/frontend/apps/electron/out/*/make/zip/win32/${{ matrix.spec.arch }}/AFFiNE*-win32-${{ matrix.spec.arch }}-*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
mv packages/frontend/apps/electron/out/*/make/squirrel.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
mv packages/frontend/apps/electron/out/*/make/nsis.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
- uses: actions/attest-build-provenance@v2
with:
subject-path: |
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
- name: Upload Artifact
uses: actions/upload-artifact@v4
@@ -297,18 +417,17 @@ jobs:
path: builds
release:
if: ${{ inputs.desktop_macos && inputs.desktop_linux && inputs.desktop_windows }}
needs:
[
before-make,
make-distribution-macos,
make-distribution-linux,
finalize-installer-windows,
]
needs: [before-make, make-distribution, finalize-installer-windows]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: web
path: web-static
- name: Zip web-static
run: zip -r web-static.zip web-static
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v4
with:
@@ -346,12 +465,32 @@ jobs:
run: |
node ./scripts/generate-release-yml.mjs
env:
RELEASE_VERSION: ${{ env.RELEASE_VERSION }}
- name: Create GitHub Release
RELEASE_VERSION: ${{ needs.before-make.outputs.RELEASE_VERSION }}
- name: Create Release Draft
if: ${{ github.ref_type == 'tag' }}
uses: softprops/action-gh-release@v2
with:
name: ${{ env.RELEASE_VERSION }}
draft: ${{ inputs.build-type == 'stable' }}
prerelease: ${{ inputs.build-type != 'stable' }}
tag_name: v${{ env.RELEASE_VERSION}}
files: ./release/*
name: ${{ needs.before-make.outputs.RELEASE_VERSION }}
body: ''
draft: ${{ github.event.inputs.is-draft }}
prerelease: ${{ github.event.inputs.is-pre-release }}
files: |
./release/*
./release/.env.example
- name: Create Nightly Release Draft
if: ${{ github.ref_type == 'branch' }}
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
# Temporarily, treat release from branch as nightly release, artifact saved to AFFiNE-Releases.
# Need to improve internal build and nightly release logic.
repository: 'toeverything/AFFiNE-Releases'
name: ${{ needs.before-make.outputs.RELEASE_VERSION }}
tag_name: ${{ needs.before-make.outputs.RELEASE_VERSION }}
body: ''
draft: false
prerelease: true
files: |
./release/*
./release/.env.example

View File

@@ -1,36 +1,68 @@
name: Release Mobile
name: Release Mobile App
on:
workflow_call:
inputs:
app-version:
type: string
required: true
git-short-hash:
build-target:
description: 'Build Target'
type: string
required: true
build-type:
description: 'Build Type'
type: string
required: true
ios-app-version:
type: string
required: false
workflow_dispatch:
inputs:
build-target:
description: 'Build Target'
type: choice
required: true
default: distribution
options:
- development
- distribution
build-type:
description: 'Build Type'
type: choice
required: true
default: canary
options:
- canary
- beta
- stable
env:
BUILD_TYPE: ${{ inputs.build-type }}
BUILD_TYPE: ${{ inputs.build-type || github.event.inputs.build-type }}
BUILD_TARGET: ${{ inputs.build-target || github.event.inputs.build-target }}
DEBUG: napi:*
KEYCHAIN_NAME: ${{ github.workspace }}/signing_temp
jobs:
build-ios-web:
output-env:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
outputs:
ENVIRONMENT: ${{ steps.env.outputs.ENVIRONMENT }}
steps:
- name: Output Environment
id: env
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "ENVIRONMENT=${{ github.event.inputs.build-type }}" >> $GITHUB_OUTPUT
else
echo "ENVIRONMENT=" >> $GITHUB_OUTPUT
fi
build-ios-web:
needs:
- output-env
runs-on: ubuntu-24.04-arm
environment: ${{ needs.output-env.outputs.ENVIRONMENT }}
outputs:
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
@@ -44,8 +76,8 @@ jobs:
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app-version }}
RELEASE_VERSION: ${{ inputs.app-version }}
SENTRY_RELEASE: ${{ steps.version.outputs.APP_VERSION }}
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
- name: Upload ios artifact
uses: actions/upload-artifact@v4
with:
@@ -53,13 +85,17 @@ jobs:
path: packages/frontend/apps/ios/dist
build-android-web:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
needs:
- output-env
environment: ${{ needs.output-env.outputs.ENVIRONMENT }}
outputs:
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
@@ -73,27 +109,41 @@ jobs:
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app-version }}
SENTRY_RELEASE: ${{ steps.version.outputs.APP_VERSION }}
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
- name: Upload android artifact
uses: actions/upload-artifact@v4
with:
name: android
path: packages/frontend/apps/android/dist
ios:
runs-on: 'macos-15'
determine-ios-runner:
runs-on: ubuntu-latest
needs:
- build-ios-web
outputs:
RUNNER: ${{ steps.runner.outputs.RUNNER }}
steps:
- name: Determine Runner
id: runner
# Randomly pick runner with 80% chance for blaze/macos-14 and 20% chance for namespace-profile-macos
# blaze/macos-14 is free but has limited concurrency
run: |
RANDOM_NUMBER=$(( $RANDOM % 100 + 1 ))
if [ $RANDOM_NUMBER -le 20 ]; then
echo "Selected namespace-profile-macos (20% probability)"
echo "RUNNER=namespace-profile-macos" >> $GITHUB_OUTPUT
else
echo "Selected blaze/macos-14 (80% probability)"
echo "RUNNER=blaze/macos-14" >> $GITHUB_OUTPUT
fi
ios:
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || needs.determine-ios-runner.outputs.RUNNER }}
needs:
- determine-ios-runner
steps:
- uses: actions/checkout@v4
- name: Setup Version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
ios-app-version: ${{ inputs.ios-app-version }}
- name: 'Update Code Sign Identity'
shell: bash
run: ./packages/frontend/apps/ios/update_code_sign_identity.sh
- name: Download mobile artifact
uses: actions/download-artifact@v4
with:
@@ -110,7 +160,7 @@ jobs:
enableScripts: false
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16.4
xcode-version: 16.2
- name: Install Swiftformat
run: brew install swiftformat
- name: Cap sync
@@ -128,6 +178,7 @@ jobs:
package: 'affine_mobile_native'
no-build: 'true'
- name: Testflight
if: ${{ env.BUILD_TYPE != 'stable' }}
working-directory: packages/frontend/apps/ios/App
run: |
echo -n "${{ env.BUILD_PROVISION_PROFILE }}" | base64 --decode -o $PP_PATH
@@ -135,7 +186,6 @@ jobs:
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
fastlane beta
env:
BUILD_TARGET: distribution
BUILD_PROVISION_PROFILE: ${{ secrets.BUILD_PROVISION_PROFILE }}
PP_PATH: ${{ runner.temp }}/build_pp.mobileprovision
APPLE_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_STORE_CONNECT_API_KEY_ID }}
@@ -151,9 +201,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
- name: Download mobile artifact
uses: actions/download-artifact@v4
with:
@@ -186,6 +235,7 @@ jobs:
- name: Auth gcloud
id: auth
uses: google-github-actions/auth@v2
if: ${{ env.BUILD_TARGET == 'distribution' }}
with:
workload_identity_provider: 'projects/${{ secrets.GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions-helm-deploy'
service_account: '${{ secrets.GCP_HELM_DEPLOY_SERVICE_ACCOUNT }}'
@@ -199,6 +249,7 @@ jobs:
cache: 'gradle'
- name: Auto increment version code
id: bump
if: ${{ env.BUILD_TARGET == 'distribution' }}
run: yarn affine @affine/playstore-auto-bump bump
env:
GOOGLE_APPLICATION_CREDENTIALS: ${{ steps.auth.outputs.credentials_file_path }}
@@ -210,13 +261,14 @@ jobs:
AFFINE_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.AFFINE_ANDROID_KEYSTORE_PASSWORD }}
AFFINE_ANDROID_KEYSTORE_ALIAS_PASSWORD: ${{ secrets.AFFINE_ANDROID_KEYSTORE_ALIAS_PASSWORD }}
AFFINE_ANDROID_SIGN_KEYSTORE: ${{ secrets.AFFINE_ANDROID_SIGN_KEYSTORE }}
VERSION_NAME: ${{ inputs.app-version }}
VERSION_NAME: ${{ steps.version.outputs.APP_VERSION }}
- name: Upload to Google Play
uses: r0adkll/upload-google-play@v1
if: ${{ env.BUILD_TARGET == 'distribution' }}
with:
serviceAccountJson: ${{ steps.auth.outputs.credentials_file_path }}
packageName: app.affine.pro
releaseName: ${{ inputs.app-version }}
releaseName: ${{ steps.version.outputs.APP_VERSION }}
releaseFiles: packages/frontend/apps/android/App/app/build/outputs/bundle/${{ env.BUILD_TYPE }}Release/app-${{ env.BUILD_TYPE }}-release-signed.aab
track: internal
status: draft

View File

@@ -1,210 +0,0 @@
name: Release
on:
schedule:
- cron: '0 9 * * *'
workflow_dispatch:
inputs:
web:
description: 'Release Web?'
required: true
type: boolean
default: false
desktop_macos:
description: 'Desktop - macOS'
required: true
type: boolean
default: false
desktop_windows:
description: 'Desktop - Windows'
required: true
type: boolean
default: false
desktop_linux:
description: 'Desktop - Linux'
required: true
type: boolean
default: false
mobile:
description: 'Release Mobile?'
required: true
type: boolean
default: false
ios-app-version:
description: 'iOS App Store Version (Optional, use tag version if empty)'
required: false
type: string
permissions:
contents: write
pull-requests: write
actions: write
id-token: write
packages: write
security-events: write
attestations: write
issues: write
jobs:
prepare:
name: Prepare
runs-on: ubuntu-latest
outputs:
APP_VERSION: ${{ steps.prepare.outputs.APP_VERSION }}
GIT_SHORT_HASH: ${{ steps.prepare.outputs.GIT_SHORT_HASH }}
BUILD_TYPE: ${{ steps.prepare.outputs.BUILD_TYPE }}
steps:
- uses: actions/checkout@v4
- name: Prepare Release
id: prepare
uses: ./.github/actions/prepare-release
canary-gate:
name: Canary Gate
runs-on: ubuntu-latest
needs:
- prepare
outputs:
SHOULD_RELEASE: ${{ steps.decide.outputs.SHOULD_RELEASE }}
LAST_CANARY_TAG: ${{ steps.decide.outputs.LAST_CANARY_TAG }}
LAST_CANARY_SHA: ${{ steps.decide.outputs.LAST_CANARY_SHA }}
steps:
- name: Decide whether to release
id: decide
uses: actions/github-script@v7
with:
script: |
const buildType = '${{ needs.prepare.outputs.BUILD_TYPE }}'
if (buildType !== 'canary') {
core.setOutput('SHOULD_RELEASE', 'true')
return
}
const owner = context.repo.owner
const repo = context.repo.repo
const currentSha = context.sha
const canaryTagRe = /^v\d+\.\d+\.\d+-canary\.[0-9a-f]+$/i
let page = 1
const perPage = 100
let lastCanary = null
while (!lastCanary && page <= 10) {
const { data } = await github.rest.repos.listTags({
owner,
repo,
per_page: perPage,
page,
})
for (const tag of data) {
if (canaryTagRe.test(tag.name)) {
lastCanary = tag
break
}
}
if (data.length < perPage) break
page++
}
if (!lastCanary) {
core.warning('No canary tags found; proceeding with canary release.')
core.setOutput('SHOULD_RELEASE', 'true')
return
}
core.setOutput('LAST_CANARY_TAG', lastCanary.name)
core.setOutput('LAST_CANARY_SHA', lastCanary.commit.sha)
const shouldRelease = lastCanary.commit.sha !== currentSha
core.info(`Latest canary tag ${lastCanary.name} -> ${lastCanary.commit.sha}; current ${currentSha}; should_release=${shouldRelease}`)
core.setOutput('SHOULD_RELEASE', shouldRelease ? 'true' : 'false')
cloud:
name: Release Cloud
if: ${{ inputs.web || github.event_name != 'workflow_dispatch' }}
needs:
- prepare
uses: ./.github/workflows/release-cloud.yml
secrets: inherit
with:
build-type: ${{ needs.prepare.outputs.BUILD_TYPE }}
app-version: ${{ needs.prepare.outputs.APP_VERSION }}
git-short-hash: ${{ needs.prepare.outputs.GIT_SHORT_HASH }}
image:
name: Release Docker Image
if: ${{ needs.canary-gate.outputs.SHOULD_RELEASE == 'true' }}
runs-on: ubuntu-latest
needs:
- prepare
- canary-gate
- cloud
steps:
- uses: trstringer/manual-approval@v1
if: ${{ needs.prepare.outputs.BUILD_TYPE == 'stable' }}
name: Wait for approval
with:
secret: ${{ secrets.GITHUB_TOKEN }}
approvers: darkskygit,pengx17,L-Sun,EYHN
minimum-approvals: 1
fail-on-denial: true
issue-title: Please confirm to release docker image
issue-body: |
Env: ${{ needs.prepare.outputs.BUILD_TYPE }}
Candidate: ghcr.io/toeverything/affine:${{ needs.prepare.outputs.BUILD_TYPE }}-${{ needs.prepare.outputs.GIT_SHORT_HASH }}
Tag: ghcr.io/toeverything/affine:${{ needs.prepare.outputs.BUILD_TYPE }}
> comment with "approve", "approved", "lgtm", "yes" to approve
> comment with "deny", "denied", "no" to deny
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
logout: false
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Tag Image
run: |
docker buildx imagetools create --tag ghcr.io/toeverything/affine:${{needs.prepare.outputs.BUILD_TYPE}} ghcr.io/toeverything/affine:${{needs.prepare.outputs.BUILD_TYPE}}-${{needs.prepare.outputs.GIT_SHORT_HASH}}
docker buildx imagetools create --tag ghcr.io/toeverything/affine:${{needs.prepare.outputs.APP_VERSION}} ghcr.io/toeverything/affine:${{needs.prepare.outputs.BUILD_TYPE}}-${{needs.prepare.outputs.GIT_SHORT_HASH}}
desktop:
name: Release Desktop
if: >-
${{
(github.event_name != 'workflow_dispatch' && needs.canary-gate.outputs.SHOULD_RELEASE == 'true') ||
inputs.desktop_macos ||
inputs.desktop_windows ||
inputs.desktop_linux
}}
needs:
- prepare
- canary-gate
uses: ./.github/workflows/release-desktop.yml
secrets: inherit
with:
build-type: ${{ needs.prepare.outputs.BUILD_TYPE }}
app-version: ${{ needs.prepare.outputs.APP_VERSION }}
git-short-hash: ${{ needs.prepare.outputs.GIT_SHORT_HASH }}
desktop_macos: ${{ github.event_name != 'workflow_dispatch' || inputs.desktop_macos }}
desktop_windows: ${{ github.event_name != 'workflow_dispatch' || inputs.desktop_windows }}
desktop_linux: ${{ github.event_name != 'workflow_dispatch' || inputs.desktop_linux }}
mobile:
name: Release Mobile
if: ${{ inputs.mobile }}
needs:
- prepare
uses: ./.github/workflows/release-mobile.yml
secrets: inherit
with:
build-type: ${{ needs.prepare.outputs.BUILD_TYPE }}
app-version: ${{ needs.prepare.outputs.APP_VERSION }}
git-short-hash: ${{ needs.prepare.outputs.GIT_SHORT_HASH }}
ios-app-version: ${{ inputs.ios-app-version }}

View File

@@ -29,7 +29,7 @@ jobs:
shell: cmd
run: |
cd ${{ env.ARCHIVE_DIR }}/out
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
signtool sign /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a ${{ inputs.files }}
- name: zip file
shell: cmd
run: |

5
.gitignore vendored
View File

@@ -33,9 +33,6 @@ node_modules
!.vscode/launch.template.json
!.vscode/extensions.json
# Kiro
.kiro
# misc
/.sass-cache
/connect.lock
@@ -47,8 +44,6 @@ testem.log
.pnpm-debug.log
/typings
tsconfig.tsbuildinfo
rfc*.md
todo.md
# System Files
.DS_Store

2
.nvmrc
View File

@@ -1 +1 @@
22.21.1
22.15.0

View File

@@ -2,7 +2,6 @@
**/node_modules
.yarn
.github/helm
.git
.vscode
.yarnrc.yml
.docker

View File

@@ -1,7 +1,7 @@
exclude = ["node_modules/**/*.toml", "target/**/*.toml"]
include = ["./*.toml", "./packages/**/*.toml"]
# https://taplo.tamasfe.dev/configuration/formatter-options.html
[formatting]
align_entries = true
indent_tables = true
reorder_keys = true
align_entries = true
column_width = 180
reorder_arrays = true
reorder_keys = true

1581
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,123 +13,93 @@ members = [
]
resolver = "3"
[workspace.package]
edition = "2024"
[workspace.package]
edition = "2024"
[workspace.dependencies]
affine_common = { path = "./packages/common/native" }
affine_nbstore = { path = "./packages/frontend/native/nbstore" }
ahash = "0.8"
anyhow = "1"
arbitrary = { version = "1.3", features = ["derive"] }
assert-json-diff = "2.0"
async-lock = { version = "3.4.0", features = ["loom"] }
base64-simd = "0.8"
bitvec = "1.0"
block2 = "0.6"
byteorder = "1.5"
chrono = "0.4"
clap = { version = "4.4", features = ["derive"] }
core-foundation = "0.10"
coreaudio-rs = "0.12"
cpal = "0.15"
criterion = { version = "0.5", features = ["html_reports"] }
criterion2 = { version = "3", default-features = false }
crossbeam-channel = "0.5"
dispatch2 = "0.3"
docx-parser = { git = "https://github.com/toeverything/docx-parser" }
dotenvy = "0.15"
file-format = { version = "0.28", features = ["reader"] }
homedir = "0.3"
infer = { version = "0.19.0" }
lasso = { version = "0.7", features = ["multi-threaded"] }
lib0 = { version = "0.16", features = ["lib0-serde"] }
libc = "0.2"
log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] }
memory-indexer = "0.2.1"
mimalloc = "0.1"
mp4parse = "0.17"
nanoid = "0.4"
napi = { version = "3.7.0", features = [
"async",
"chrono_date",
"error_anyhow",
"napi9",
"serde",
] }
napi-build = { version = "2" }
napi-derive = { version = "3.4" }
nom = "8"
notify = { version = "8", features = ["serde"] }
objc2 = "0.6"
objc2-foundation = "0.3"
once_cell = "1"
ordered-float = "5"
parking_lot = "0.12"
path-ext = "0.1.2"
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
phf = { version = "0.11", features = ["macros"] }
proptest = "1.3"
proptest-derive = "0.5"
rand = "0.9"
rand_chacha = "0.9"
rand_distr = "0.5"
rayon = "1.10"
readability = { version = "0.3.0", default-features = false }
regex = "1.10"
rubato = "0.16"
screencapturekit = "0.3"
serde = "1"
serde_json = "1"
sha3 = "0.10"
smol_str = "0.3"
sqlx = { version = "0.8", default-features = false, features = [
"chrono",
"macros",
"migrate",
"runtime-tokio",
"sqlite",
"tls-rustls",
] }
strum_macros = "0.27.0"
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
text-splitter = "0.27"
thiserror = "2"
tiktoken-rs = "0.7"
tokio = "1.45"
tree-sitter = { version = "0.25" }
tree-sitter-c = { version = "0.24" }
tree-sitter-c-sharp = { version = "0.23" }
tree-sitter-cpp = { version = "0.23" }
tree-sitter-go = { version = "0.23" }
tree-sitter-java = { version = "0.23" }
tree-sitter-javascript = { version = "0.23" }
tree-sitter-kotlin-ng = { version = "1.1" }
tree-sitter-python = { version = "0.23" }
tree-sitter-rust = { version = "0.24" }
tree-sitter-scala = { version = "0.24" }
tree-sitter-typescript = { version = "0.23" }
uniffi = "0.29"
url = { version = "2.5" }
uuid = "1.8"
v_htmlescape = "0.15"
windows = { version = "0.61", features = [
"Win32_Devices_FunctionDiscovery",
"Win32_Foundation",
"Win32_Media_Audio",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_ProcessStatus",
"Win32_System_Threading",
"Win32_System_Variant",
"Win32_UI_Shell_PropertiesSystem",
] }
windows-core = { version = "0.61" }
y-octo = { path = "./packages/common/y-octo/core" }
y-sync = { version = "0.4" }
yrs = "0.23.0"
[workspace.dependencies]
affine_common = { path = "./packages/common/native" }
affine_nbstore = { path = "./packages/frontend/native/nbstore" }
ahash = "0.8"
anyhow = "1"
arbitrary = { version = "1.3", features = ["derive"] }
assert-json-diff = "2.0"
async-lock = { version = "3.4.0", features = ["loom"] }
base64-simd = "0.8"
bitvec = "1.0"
block2 = "0.6"
byteorder = "1.5"
chrono = "0.4"
clap = { version = "4.4", features = ["derive"] }
core-foundation = "0.10"
coreaudio-rs = "0.12"
criterion = { version = "0.5", features = ["html_reports"] }
criterion2 = { version = "3", default-features = false }
dispatch2 = "0.3"
docx-parser = { git = "https://github.com/toeverything/docx-parser" }
dotenvy = "0.15"
file-format = { version = "0.26", features = ["reader"] }
homedir = "0.3"
infer = { version = "0.19.0" }
lasso = { version = "0.7", features = ["multi-threaded"] }
lib0 = { version = "0.16", features = ["lib0-serde"] }
libc = "0.2"
log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] }
mimalloc = "0.1"
nanoid = "0.4"
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }
napi-derive = { version = "3.0.0-alpha.28" }
nom = "8"
notify = { version = "8", features = ["serde"] }
objc2 = "0.6"
objc2-foundation = "0.3"
once_cell = "1"
ordered-float = "5"
parking_lot = "0.12"
path-ext = "0.1.1"
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
phf = { version = "0.11", features = ["macros"] }
proptest = "1.3"
proptest-derive = "0.5"
rand = "0.9"
rand_chacha = "0.9"
rand_distr = "0.5"
rayon = "1.10"
readability = { version = "0.3.0", default-features = false }
regex = "1.10"
rubato = "0.16"
screencapturekit = "0.3"
serde = "1"
serde_json = "1"
sha3 = "0.10"
smol_str = "0.3"
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
strum_macros = "0.27.0"
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
text-splitter = "0.25"
thiserror = "2"
tiktoken-rs = "0.6"
tokio = "1.37"
tree-sitter = { version = "0.25" }
tree-sitter-c = { version = "0.23" }
tree-sitter-c-sharp = { version = "0.23" }
tree-sitter-cpp = { version = "0.23" }
tree-sitter-go = { version = "0.23" }
tree-sitter-java = { version = "0.23" }
tree-sitter-javascript = { version = "0.23" }
tree-sitter-kotlin-ng = { version = "1.1" }
tree-sitter-python = { version = "0.23" }
tree-sitter-rust = { version = "0.24" }
tree-sitter-scala = { version = "0.23" }
tree-sitter-typescript = { version = "0.23" }
uniffi = "0.29"
url = { version = "2.5" }
uuid = "1.8"
v_htmlescape = "0.15"
y-octo = { path = "./packages/common/y-octo/core" }
y-sync = { version = "0.4" }
yrs = "0.23.0"
[profile.dev.package.sqlx-macros]
opt-level = 3
@@ -140,6 +110,6 @@ lto = true
opt-level = 3
strip = "symbols"
# android uniffi bindgen requires symbols
[profile.release.package.affine_mobile_native]
strip = "none"
# android uniffi bindgen requires symbols
[profile.release.package.affine_mobile_native]
strip = "none"

View File

@@ -2,7 +2,7 @@ Copyright (c) 2022-present TOEVERYTHING PTE. LTD. and its affiliates.
Portions of this software are licensed as follows:
- All content that resides under the "packages/backend" and "packages/common/native" directory of this repository, if that directory exists, is licensed under the license defined in "packages/backend/server/LICENSE".
- All content that resides under the "packages/backend/server" directory of this repository, if that directory exists, is licensed under the license defined in "packages/backend/server/LICENSE".
- All third party components incorporated into the AFFiNE Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "MIT" license as defined in "LICENSE-MIT".

View File

@@ -6,7 +6,7 @@
<br>
</h1>
<a href="https://affine.pro/download">
<img alt="affine logo" src="https://cdn.affine.pro/Github_hero_image2.png" style="width: 100%">
<img alt="affine logo" src="https://cdn.affine.pro/Github_hero_image1.png" style="width: 100%">
</a>
<br/>
<p align="center">
@@ -81,7 +81,7 @@ Star us, and you will receive all release notifications from GitHub without any
**Multimodal AI partner ready to kick in any work**
- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination, just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming.
- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination,just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming.
**Local-first & Real-time collaborative**
@@ -193,8 +193,6 @@ We would like to express our gratitude to all the individuals who have already c
Begin with Docker to deploy your own feature-rich, unrestricted version of AFFiNE. Our team is diligently updating to the latest version. For more information on how to self-host AFFiNE, please refer to our [documentation](https://docs.affine.pro/self-host-affine).
[![Run on Sealos](https://sealos.io/Deploy-on-Sealos.svg)](https://sealos.io/products/app-store/affine)
[![Run on ClawCloud](https://raw.githubusercontent.com/ClawCloud/Run-Template/refs/heads/main/Run-on-ClawCloud.svg)](https://template.run.claw.cloud/?openapp=system-fastdeploy%3FtemplateName%3Daffine)
## Hiring

View File

@@ -6,14 +6,15 @@ We recommend users to always use the latest major version. Security updates will
| Version | Supported |
| --------------- | ------------------ |
| 0.25.x (stable) | :white_check_mark: |
| < 0.25.x | :x: |
| 0.17.x (stable) | :white_check_mark: |
| < 0.17.x | :x: |
## Reporting a Vulnerability
We welcome you to provide us with bug reports via and email at [security@toeverything.info](mailto:security@toeverything.info) or submit directly on [GitHub](https://github.com/toeverything/AFFiNE/security), **we encourage you to submit the relevant information directly via GitHub**. We expect your report to contain at least the following for us to evaluate and reproduce:
We welcome you to provide us with bug reports via and email at [security@toeverything.info](mailto:security@toeverything.info). We expect your report to contain at least the following for us to evaluate and reproduce:
1. Using platform and version, for example:
- macos arm64 0.12.0-canary-202402220729-0868ac6
- app.affine.pro 0.12.0-canary-202402220729-0868ac6
@@ -21,6 +22,8 @@ We welcome you to provide us with bug reports via and email at [security@toevery
3. Your classification or analysis of the vulnerability (optional)
Since we are an open source project, we also welcome you to provide corresponding fix PRs, we will determine specific rewards based on the evaluation results.
Since we are an open source project, we also welcome you to provide corresponding fix PRs.
We will provide bounties for vulnerabilities involving user information leakage, permission leakage, and unauthorized code execution. For other types of vulnerabilities, we will determine specific rewards based on the evaluation results.
If the vulnerability is caused by a library we depend on, we encourage you to submit a security report to the corresponding dependent library at the same time to benefit more users.

View File

@@ -33,7 +33,6 @@
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-foundation": "workspace:*",
"@blocksuite/affine-fragment-adapter-panel": "workspace:*",
"@blocksuite/affine-fragment-doc-title": "workspace:*",
"@blocksuite/affine-fragment-frame-panel": "workspace:*",
"@blocksuite/affine-fragment-outline": "workspace:*",
@@ -48,7 +47,6 @@
"@blocksuite/affine-gfx-template": "workspace:*",
"@blocksuite/affine-gfx-text": "workspace:*",
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
"@blocksuite/affine-inline-comment": "workspace:*",
"@blocksuite/affine-inline-footnote": "workspace:*",
"@blocksuite/affine-inline-latex": "workspace:*",
"@blocksuite/affine-inline-link": "workspace:*",
@@ -60,14 +58,11 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-drag-handle": "workspace:*",
"@blocksuite/affine-widget-edgeless-auto-connect": "workspace:*",
"@blocksuite/affine-widget-edgeless-dragging-area": "workspace:*",
"@blocksuite/affine-widget-edgeless-selected-rect": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/affine-widget-edgeless-zoom-toolbar": "workspace:*",
"@blocksuite/affine-widget-frame-title": "workspace:*",
"@blocksuite/affine-widget-keyboard-toolbar": "workspace:*",
"@blocksuite/affine-widget-linked-doc": "workspace:*",
"@blocksuite/affine-widget-note-slicer": "workspace:*",
"@blocksuite/affine-widget-page-dragging-area": "workspace:*",
"@blocksuite/affine-widget-remote-selection": "workspace:*",
"@blocksuite/affine-widget-scroll-anchoring": "workspace:*",
@@ -79,7 +74,7 @@
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@blocksuite/sync": "workspace:*",
"rxjs": "^7.8.2"
"rxjs": "^7.8.1"
},
"exports": {
".": "./src/index.ts",
@@ -174,7 +169,6 @@
"./inlines/footnote": "./src/inlines/footnote/index.ts",
"./inlines/footnote/view": "./src/inlines/footnote/view.ts",
"./inlines/footnote/store": "./src/inlines/footnote/store.ts",
"./inlines/comment": "./src/inlines/comment/index.ts",
"./inlines/latex": "./src/inlines/latex/index.ts",
"./inlines/latex/store": "./src/inlines/latex/store.ts",
"./inlines/latex/view": "./src/inlines/latex/view.ts",
@@ -184,8 +178,6 @@
"./widgets/drag-handle/view": "./src/widgets/drag-handle/view.ts",
"./widgets/edgeless-auto-connect": "./src/widgets/edgeless-auto-connect/index.ts",
"./widgets/edgeless-auto-connect/view": "./src/widgets/edgeless-auto-connect/view.ts",
"./widgets/edgeless-dragging-area": "./src/widgets/edgeless-dragging-area/index.ts",
"./widgets/edgeless-dragging-area/view": "./src/widgets/edgeless-dragging-area/view.ts",
"./widgets/edgeless-toolbar": "./src/widgets/edgeless-toolbar/index.ts",
"./widgets/edgeless-toolbar/view": "./src/widgets/edgeless-toolbar/view.ts",
"./widgets/frame-title": "./src/widgets/frame-title/index.ts",
@@ -212,8 +204,6 @@
"./fragments/frame-panel/view": "./src/fragments/frame-panel/view.ts",
"./fragments/outline": "./src/fragments/outline/index.ts",
"./fragments/outline/view": "./src/fragments/outline/view.ts",
"./fragments/adapter-panel": "./src/fragments/adapter-panel/index.ts",
"./fragments/adapter-panel/view": "./src/fragments/adapter-panel/view.ts",
"./gfx/text": "./src/gfx/text/index.ts",
"./gfx/text/store": "./src/gfx/text/store.ts",
"./gfx/text/view": "./src/gfx/text/view.ts",
@@ -266,7 +256,6 @@
"./components/toggle-button": "./src/components/toggle-button.ts",
"./components/toggle-switch": "./src/components/toggle-switch.ts",
"./components/toolbar": "./src/components/toolbar.ts",
"./components/tooltip": "./src/components/tooltip.ts",
"./components/view-dropdown-menu": "./src/components/view-dropdown-menu.ts",
"./components/tooltip-content-with-shortcut": "./src/components/tooltip-content-with-shortcut.ts",
"./components/resource": "./src/components/resource.ts",
@@ -286,7 +275,6 @@
"./sync": "./src/sync/index.ts",
"./extensions/store": "./src/extensions/store.ts",
"./extensions/view": "./src/extensions/view.ts",
"./foundation/clipboard": "./src/foundation/clipboard.ts",
"./foundation/store": "./src/foundation/store.ts",
"./foundation/view": "./src/foundation/view.ts"
},
@@ -296,10 +284,9 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.7",
"version": "0.21.0",
"devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0",
"msw": "^2.12.4",
"vitest": "^3.2.4"
"vitest": "3.1.3"
}
}

View File

@@ -2214,7 +2214,7 @@ describe('html to snapshot', () => {
test('iframe', async () => {
const html = template(
`<iframe width="560" height="315" src="https://www.youtube.com/embed/QDsd0nyzwz0?start=&amp;end=" title="YouTube video player" frameborder="0" allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>`
`<iframe width="560" height="315" src="https://www.youtube.com/embed/QDsd0nyzwz0?start=&amp;end=" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>`
);
const blockSnapshot: BlockSnapshot = {
@@ -2697,335 +2697,4 @@ describe('html to snapshot', () => {
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('block level element in b should not be treated as inline', async () => {
const html = template(`<b><p><span>aaa</span></p></b>`);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'aaa',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
describe('strong element', () => {
test('should not be bold when font-weight is normal', async () => {
const html = template(`<span style="font-weight: normal;">aaa</span>`);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'aaa',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('should be bold when font-weight is bold or 500-900 ', async () => {
const html = template(
`<p><span style="font-weight: bold;">aaa</span><span style="font-weight: 100;">aaa</span><span style="font-weight: 500;">bbb</span><span style="font-weight: 200;">bbb</span><span style="font-weight: 600;">ccc</span><span style="font-weight: 300;">ccc</span><span style="font-weight: 700;">ddd</span></p>`
);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
attributes: {
bold: true,
},
insert: 'aaa',
},
{
insert: 'aaa',
},
{
attributes: {
bold: true,
},
insert: 'bbb',
},
{
insert: 'bbb',
},
{
attributes: {
bold: true,
},
insert: 'ccc',
},
{
insert: 'ccc',
},
{
attributes: {
bold: true,
},
insert: 'ddd',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
});
test('should be italic when tag is i or em or span with style font-style: italic', async () => {
const html = template(
`<p><i>aaa</i><span>aaa</span><em>bbb</em><span>bbb</span><span style="font-style: italic;">ccc</span></p>`
);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
attributes: {
italic: true,
},
insert: 'aaa',
},
{
insert: 'aaa',
},
{
attributes: {
italic: true,
},
insert: 'bbb',
},
{
insert: 'bbb',
},
{
attributes: {
italic: true,
},
insert: 'ccc',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('should be underline when tag is u or span with style text-decoration: underline', async () => {
const html = template(
`<p><u>aaa</u><span>aaa</span><span style="text-decoration: underline;">bbb</span></p>`
);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
attributes: {
underline: true,
},
insert: 'aaa',
},
{
insert: 'aaa',
},
{
attributes: {
underline: true,
},
insert: 'bbb',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('should be strike when tag is del or span with style text-decoration: line-through', async () => {
const html = template(
`<p><del>aaa</del><span>aaa</span><span style="text-decoration: line-through;">bbb</span></p>`
);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
attributes: {
strike: true,
},
insert: 'aaa',
},
{
insert: 'aaa',
},
{
attributes: {
strike: true,
},
insert: 'bbb',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
});

View File

@@ -4393,61 +4393,6 @@ hhh
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[2]',
flavour: 'affine:paragraph',
props: {
type: 'h6',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Sources',
},
],
},
collapsed: true,
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[3]',
flavour: 'affine:bookmark',
props: {
style: 'citation',
url,
title,
description,
icon: favicon,
footnoteIdentifier: '1',
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[4]',
flavour: 'affine:embed-linked-doc',
props: {
style: 'citation',
pageId: 'deadbeef',
footnoteIdentifier: '2',
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[5]',
flavour: 'affine:attachment',
props: {
name: 'test.txt',
sourceId: 'abcdefg',
footnoteIdentifier: '3',
style: 'citation',
},
children: [],
},
],
};
@@ -4472,101 +4417,6 @@ hhh
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('should handle footnote reference with url prefix', async () => {
const blockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'https://example.com',
attributes: {
link: 'https://example.com',
},
},
{
insert: ' ',
},
{
insert: ' ',
attributes: {
footnote: {
label: '1',
reference: {
type: 'url',
url,
favicon,
title,
description,
},
},
},
},
],
},
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[2]',
flavour: 'affine:paragraph',
props: {
type: 'h6',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Sources',
},
],
},
collapsed: true,
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[3]',
flavour: 'affine:bookmark',
props: {
style: 'citation',
url,
title,
description,
icon: favicon,
footnoteIdentifier: '1',
},
children: [],
},
],
};
const markdown = `https://example.com[^1]\n\n[^1]: {"type":"url","url":"${url}","favicon":"${favicon}","title":"${title}","description":"${description}"}\n`;
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
});
test('should not wrap url with angle brackets if it is not a url', async () => {

View File

@@ -1,14 +1,11 @@
import { DefaultTheme, NoteDisplayMode } from '@blocksuite/affine-model';
import { NotionHtmlAdapter } from '@blocksuite/affine-shared/adapters';
import { DEFAULT_IMAGE_PROXY_ENDPOINT } from '@blocksuite/affine-shared/consts';
import {
AssetsManager,
type BlockSnapshot,
MemoryBlobCRUD,
} from '@blocksuite/store';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';
import { describe, expect, test } from 'vitest';
import { createJob } from '../utils/create-job.js';
import { getProvider } from '../utils/get-provider.js';
@@ -1198,71 +1195,43 @@ describe('notion html to snapshot', () => {
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
describe('image', () => {
const originalUrl =
'https://raw.githubusercontent.com/toeverything/blocksuite/master/assets/logo.svg';
test('image', async () => {
const html = `<div class="page-body">
<figure id="ed3d2ae9-62f5-433a-9049-9ddbd1c81ac5" class="image"><a
href="https://raw.githubusercontent.com/toeverything/blocksuite/master/assets/logo.svg"><img src="https://raw.githubusercontent.com/toeverything/blocksuite/master/assets/logo.svg" /></a>
</figure>
</div>`;
const imageProxy = DEFAULT_IMAGE_PROXY_ENDPOINT;
const imageUrl = `${imageProxy}?url=${encodeURIComponent(originalUrl)}`;
// Mock the image request
const imageRequestHandlers = [
http.get(imageUrl.toString(), async () => {
// Return a mock image blob
const mockImageBlob = new Blob(['mock image data'], {
type: 'image/svg+xml',
});
return new HttpResponse(mockImageBlob, {
headers: {
'Content-Type': 'image/svg+xml',
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:image',
props: {
sourceId: 'matchesReplaceMap[2]',
},
});
}),
];
const server = setupServer(...imageRequestHandlers);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
test('network image resource', async () => {
const html = `<div class="page-body">
<figure id="ed3d2ae9-62f5-433a-9049-9ddbd1c81ac5" class="image"><a
href="${originalUrl}"><img src="${originalUrl}" /></a>
</figure>
</div>`;
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
children: [],
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:image',
props: {
sourceId: 'matchesReplaceMap[2]',
},
children: [],
},
],
};
],
};
const adapter = new NotionHtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await adapter.toBlockSnapshot({
file: html,
assets: new AssetsManager({ blob: new MemoryBlobCRUD() }),
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
const adapter = new NotionHtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await adapter.toBlockSnapshot({
file: html,
assets: new AssetsManager({ blob: new MemoryBlobCRUD() }),
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('bookmark', async () => {

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-components/tooltip';

View File

@@ -19,7 +19,6 @@ import { SurfaceViewExtension } from '@blocksuite/affine-block-surface/view';
import { SurfaceRefViewExtension } from '@blocksuite/affine-block-surface-ref/view';
import { TableViewExtension } from '@blocksuite/affine-block-table/view';
import { FoundationViewExtension } from '@blocksuite/affine-foundation/view';
import { AdapterPanelViewExtension } from '@blocksuite/affine-fragment-adapter-panel/view';
import { DocTitleViewExtension } from '@blocksuite/affine-fragment-doc-title/view';
import { FramePanelViewExtension } from '@blocksuite/affine-fragment-frame-panel/view';
import { OutlineViewExtension } from '@blocksuite/affine-fragment-outline/view';
@@ -33,7 +32,6 @@ import { PointerViewExtension } from '@blocksuite/affine-gfx-pointer/view';
import { ShapeViewExtension } from '@blocksuite/affine-gfx-shape/view';
import { TemplateViewExtension } from '@blocksuite/affine-gfx-template/view';
import { TextViewExtension } from '@blocksuite/affine-gfx-text/view';
import { InlineCommentViewExtension } from '@blocksuite/affine-inline-comment/view';
import { FootnoteViewExtension } from '@blocksuite/affine-inline-footnote/view';
import { LatexViewExtension as InlineLatexViewExtension } from '@blocksuite/affine-inline-latex/view';
import { LinkViewExtension } from '@blocksuite/affine-inline-link/view';
@@ -42,14 +40,11 @@ import { InlinePresetViewExtension } from '@blocksuite/affine-inline-preset/view
import { ReferenceViewExtension } from '@blocksuite/affine-inline-reference/view';
import { DragHandleViewExtension } from '@blocksuite/affine-widget-drag-handle/view';
import { EdgelessAutoConnectViewExtension } from '@blocksuite/affine-widget-edgeless-auto-connect/view';
import { EdgelessDraggingAreaViewExtension } from '@blocksuite/affine-widget-edgeless-dragging-area/view';
import { EdgelessSelectedRectViewExtension } from '@blocksuite/affine-widget-edgeless-selected-rect/view';
import { EdgelessToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-toolbar/view';
import { EdgelessZoomToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-zoom-toolbar/view';
import { FrameTitleViewExtension } from '@blocksuite/affine-widget-frame-title/view';
import { KeyboardToolbarViewExtension } from '@blocksuite/affine-widget-keyboard-toolbar/view';
import { LinkedDocViewExtension } from '@blocksuite/affine-widget-linked-doc/view';
import { NoteSlicerViewExtension } from '@blocksuite/affine-widget-note-slicer/view';
import { PageDraggingAreaViewExtension } from '@blocksuite/affine-widget-page-dragging-area/view';
import { RemoteSelectionViewExtension } from '@blocksuite/affine-widget-remote-selection/view';
import { ScrollAnchoringViewExtension } from '@blocksuite/affine-widget-scroll-anchoring/view';
@@ -96,7 +91,6 @@ export function getInternalViewExtensions() {
RootViewExtension,
// Inline
InlineCommentViewExtension,
FootnoteViewExtension,
LinkViewExtension,
ReferenceViewExtension,
@@ -105,9 +99,9 @@ export function getInternalViewExtensions() {
InlinePresetViewExtension,
// Widget
// order will affect the z-index of the widget
DragHandleViewExtension,
EdgelessAutoConnectViewExtension,
EdgelessToolbarViewExtension,
FrameTitleViewExtension,
KeyboardToolbarViewExtension,
LinkedDocViewExtension,
@@ -118,15 +112,10 @@ export function getInternalViewExtensions() {
ViewportOverlayViewExtension,
EdgelessZoomToolbarViewExtension,
PageDraggingAreaViewExtension,
EdgelessSelectedRectViewExtension,
EdgelessDraggingAreaViewExtension,
NoteSlicerViewExtension,
EdgelessToolbarViewExtension,
// Fragment
DocTitleViewExtension,
FramePanelViewExtension,
OutlineViewExtension,
AdapterPanelViewExtension,
];
}

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-foundation/clipboard';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-fragment-adapter-panel';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-fragment-adapter-panel/view';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-inline-comment';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-edgeless-dragging-area';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-edgeless-dragging-area/view';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-edgeless-selected-rect';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-edgeless-selected-rect/view';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-note-slicer';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-note-slicer/view';

View File

@@ -30,7 +30,6 @@
{ "path": "../components" },
{ "path": "../ext-loader" },
{ "path": "../foundation" },
{ "path": "../fragments/adapter-panel" },
{ "path": "../fragments/doc-title" },
{ "path": "../fragments/frame-panel" },
{ "path": "../fragments/outline" },
@@ -45,7 +44,6 @@
{ "path": "../gfx/template" },
{ "path": "../gfx/text" },
{ "path": "../gfx/turbo-renderer" },
{ "path": "../inlines/comment" },
{ "path": "../inlines/footnote" },
{ "path": "../inlines/latex" },
{ "path": "../inlines/link" },
@@ -57,14 +55,11 @@
{ "path": "../shared" },
{ "path": "../widgets/drag-handle" },
{ "path": "../widgets/edgeless-auto-connect" },
{ "path": "../widgets/edgeless-dragging-area" },
{ "path": "../widgets/edgeless-selected-rect" },
{ "path": "../widgets/edgeless-toolbar" },
{ "path": "../widgets/edgeless-zoom-toolbar" },
{ "path": "../widgets/frame-title" },
{ "path": "../widgets/keyboard-toolbar" },
{ "path": "../widgets/linked-doc" },
{ "path": "../widgets/note-slicer" },
{ "path": "../widgets/page-dragging-area" },
{ "path": "../widgets/remote-selection" },
{ "path": "../widgets/scroll-anchoring" },

View File

@@ -17,21 +17,22 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.16",
"file-type": "^21.0.0",
"@toeverything/theme": "^1.1.14",
"file-type": "^20.0.0",
"lit": "^3.2.0",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"zod": "^3.25.76"
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./store": "./src/store.ts",
"./view": "./src/view.ts"
},
@@ -41,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.7"
"version": "0.21.0"
}

View File

@@ -10,6 +10,7 @@ import {
isFootnoteDefinitionNode,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { nanoid } from '@blocksuite/store';
const isAttachmentFootnoteDefinitionNode = (node: MarkdownAST) => {
@@ -35,7 +36,15 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
fromMatch: o => o.node.flavour === AttachmentBlockSchema.model.flavour,
toBlockSnapshot: {
enter: (o, context) => {
if (!isFootnoteDefinitionNode(o.node)) {
const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
return;
}
@@ -64,7 +73,6 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
name: fileName,
sourceId: blobId,
footnoteIdentifier,
style: 'citation',
},
children: [],
},

View File

@@ -4,7 +4,7 @@ import {
} from '@blocksuite/affine-components/caption';
import {
getAttachmentFileIcon,
LoadingIcon,
getLoadingIconWith,
} from '@blocksuite/affine-components/icons';
import { Peekable } from '@blocksuite/affine-components/peek';
import {
@@ -17,16 +17,10 @@ import {
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import {
BlockElementCommentManager,
CitationProvider,
DocModeProvider,
FileSizeLimitProvider,
TelemetryProvider,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import {
formatSize,
openSingleFileWith,
} from '@blocksuite/affine-shared/utils';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
import {
AttachmentIcon,
ResetIcon,
@@ -35,18 +29,17 @@ import {
} from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import { nanoid, Slice } from '@blocksuite/store';
import { batch, computed, signal } from '@preact/signals-core';
import { computed, signal } from '@preact/signals-core';
import { html, type TemplateResult } from 'lit';
import { choose } from 'lit/directives/choose.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { guard } from 'lit/directives/guard.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import { filter } from 'rxjs/operators';
import { AttachmentEmbedProvider } from './embed';
import { styles } from './styles';
import { downloadAttachmentBlob, getFileType, refreshData } from './utils';
import { downloadAttachmentBlob, refreshData } from './utils';
type AttachmentResolvedStateInfo = ResolvedStateInfo & {
kind?: TemplateResult;
@@ -70,11 +63,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
return this.resourceController.blobUrl$.value;
}
get filetype() {
const name = this.model.props.name$.value;
return name.split('.').pop() ?? '';
}
protected containerStyleMap = styleMap({
position: 'relative',
width: '100%',
@@ -85,20 +73,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
return this.std.get(FileSizeLimitProvider).maxFileSize;
}
get citationService() {
return this.std.get(CitationProvider);
}
get isCitation() {
return this.citationService.isCitationModel(this.model);
}
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockElementCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
return !!this.model.props.footnoteIdentifier;
}
convertTo = () => {
@@ -141,50 +117,12 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
// Refreshes the embed component.
reload = () => {
batch(() => {
if (this.model.props.embed$.value) {
this._refreshKey$.value = nanoid();
return;
}
if (this.model.props.embed) {
this._refreshKey$.value = nanoid();
return;
}
this.refreshData();
});
};
// Replaces the current attachment.
replace = async () => {
const state = this.resourceController.state$.peek();
if (state.uploading) return;
const file = await openSingleFileWith();
if (!file) return;
const sourceId = await this.std.store.blobSync.set(file);
const type = await getFileType(file);
const { name, size } = file;
let embed = this.model.props.embed$.value ?? false;
this.std.store.captureSync();
this.std.store.transact(() => {
this.std.store.updateBlock(this.blockId, {
name,
size,
type,
sourceId,
embed: false,
});
const provider = this.std.get(AttachmentEmbedProvider);
embed &&= provider.embedded(this.model);
if (embed) {
provider.convertTo(this.model);
}
// Reloads
this.reload();
});
this.refreshData();
};
private _selectBlock() {
@@ -195,34 +133,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
selectionManager.setGroup('note', [blockSelection]);
}
private readonly _trackCitationDeleteEvent = () => {
// Check citation delete event
this._disposables.add(
this.std.store.slots.blockUpdated
.pipe(
filter(payload => {
if (!payload.isLocal) return false;
const { flavour, id, type } = payload;
if (
type !== 'delete' ||
flavour !== this.model.flavour ||
id !== this.model.id
)
return false;
const { model } = payload;
if (!this.citationService.isCitationModel(model)) return false;
return true;
})
)
.subscribe(() => {
this.citationService.trackEvent('Delete');
})
);
};
override connectedCallback() {
super.connectedCallback();
@@ -233,11 +143,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
this.disposables.add(this.resourceController.subscribe());
this.disposables.add(this.resourceController);
this.disposables.add(
this.model.props.sourceId$.subscribe(() => {
this.refreshData();
})
);
this.refreshData();
if (!this.model.props.style && !this.store.readonly) {
this.store.withoutTransact(() => {
@@ -246,8 +152,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
});
});
}
this._trackCitationDeleteEvent();
}
override firstUpdated() {
@@ -279,22 +183,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
@click=${(event: MouseEvent) => {
event.stopPropagation();
onOverFileSize?.();
{
const mode =
this.std.get(DocModeProvider).getEditorMode() ?? 'page';
const segment = mode === 'page' ? 'doc' : 'whiteboard';
this.std
.getOptional(TelemetryProvider)
?.track('AttachmentUpgradedEvent', {
segment,
page: `${segment} editor`,
module: 'attachment',
control: 'upgrade',
category: 'card',
type: this.model.props.name.split('.').pop() ?? '',
});
}
}}
>
${UpgradeIcon()} Upgrade
@@ -303,56 +191,23 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
);
};
protected renderNormalButton = (needUpload: boolean) => {
const label = needUpload ? 'retry' : 'reload';
const run = async () => {
if (needUpload) {
await this.resourceController.upload();
return;
}
this.refreshData();
};
protected renderReloadButton = () => {
return html`
<button
class="affine-attachment-content-button"
@click=${(event: MouseEvent) => {
event.stopPropagation();
run().catch(console.error);
{
const mode =
this.std.get(DocModeProvider).getEditorMode() ?? 'page';
const segment = mode === 'page' ? 'doc' : 'whiteboard';
this.std
.getOptional(TelemetryProvider)
?.track('AttachmentReloadedEvent', {
segment,
page: `${segment} editor`,
module: 'attachment',
control: label,
category: 'card',
type: this.filetype,
});
}
this.refreshData();
}}
>
${ResetIcon()} ${label}
${ResetIcon()} Reload
</button>
`;
};
protected renderWithHorizontal(
classInfo: ClassInfo,
{
icon,
title,
description,
kind,
state,
needUpload,
}: AttachmentResolvedStateInfo
{ icon, title, description, kind, state }: AttachmentResolvedStateInfo
) {
return html`
<div class=${classMap(classInfo)}>
@@ -369,7 +224,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
${description}
</div>
${choose(state, [
['error', () => this.renderNormalButton(needUpload)],
['error', this.renderReloadButton],
['error:oversize', this.renderUpgradeButton],
])}
</div>
@@ -382,14 +237,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
protected renderWithVertical(
classInfo: ClassInfo,
{
icon,
title,
description,
kind,
state,
needUpload,
}: AttachmentResolvedStateInfo
{ icon, title, description, kind, state }: AttachmentResolvedStateInfo
) {
return html`
<div class=${classMap(classInfo)}>
@@ -409,7 +257,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
<div class="affine-attachment-banner">
${kind}
${choose(state, [
['error', () => this.renderNormalButton(needUpload)],
['error', this.renderReloadButton],
['error:oversize', this.renderUpgradeButton],
])}
</div>
@@ -418,16 +266,19 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
}
protected resolvedState$ = computed<AttachmentResolvedStateInfo>(() => {
const theme = this.std.get(ThemeProvider).theme$.value;
const loadingIcon = getLoadingIconWith(theme);
const size = this.model.props.size;
const name = this.model.props.name$.value;
const kind = getAttachmentFileIcon(this.filetype);
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
const resolvedState = this.resourceController.resolveStateWith({
loadingIcon: LoadingIcon(),
loadingIcon,
errorIcon: WarningIcon(),
icon: AttachmentIcon(),
title: name,
description: formatSize(size),
description: humanFileSize(size),
});
return { ...resolvedState, kind };
@@ -453,7 +304,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
protected renderEmbedView = () => {
const { model, blobUrl } = this;
if (!model.props.embed$.value || !blobUrl) return null;
if (!model.props.embed || !blobUrl) return null;
const { std, _maxFileSize } = this;
const provider = std.get(AttachmentEmbedProvider);
@@ -474,16 +325,11 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
const message = resolvedState.description;
if (!message) return null;
const needUpload = resolvedState.needUpload;
const action = () =>
needUpload ? this.resourceController.upload() : this.reload();
return html`
<affine-resource-status
class="affine-attachment-embed-status"
.message=${message}
.needUpload=${needUpload}
.action=${action}
.reload=${() => this.reload()}
></affine-resource-status>
`;
})}
@@ -492,10 +338,10 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
private readonly _renderCitation = () => {
const { name, footnoteIdentifier } = this.model.props;
const icon = getAttachmentFileIcon(this.filetype);
const fileType = name.split('.').pop() ?? '';
const fileTypeIcon = getAttachmentFileIcon(fileType);
return html`<affine-citation-card
.icon=${icon}
.icon=${fileTypeIcon}
.citationTitle=${name}
.citationIdentifier=${footnoteIdentifier}
.active=${this.selected$.value}
@@ -508,7 +354,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
class=${classMap({
'affine-attachment-container': true,
focused: this.selected$.value,
'comment-highlighted': this.isCommentHighlighted,
})}
style=${this.containerStyleMap}
>

View File

@@ -1,7 +1,6 @@
import { ConfirmIcon } from '@blocksuite/affine-components/icons';
import { toast } from '@blocksuite/affine-components/toast';
import type { AttachmentBlockModel } from '@blocksuite/affine-model';
import { CitationProvider } from '@blocksuite/affine-shared/services';
import type { EditorHost } from '@blocksuite/std';
import { html } from 'lit';
import { createRef, ref } from 'lit/directives/ref.js';
@@ -34,7 +33,6 @@ export const RenameModal = ({
let fileName = includeExtension ? nameWithoutExtension : originalName;
const extension = includeExtension ? originalExtension : '';
const citationService = editorHost.std.get(CitationProvider);
const abort = () => abortController.abort();
const onConfirm = () => {
@@ -46,9 +44,6 @@ export const RenameModal = ({
model.store.updateBlock(model, {
name: newFileName,
});
if (citationService.isCitationModel(model)) {
citationService.trackEvent('Edit');
}
abort();
};
const onInput = (e: InputEvent) => {

View File

@@ -1,4 +1,4 @@
import { openSingleFileWith } from '@blocksuite/affine-shared/utils';
import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit';
@@ -21,7 +21,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
model.store.schema.flavourSchemaMap.has('affine:attachment'),
action: ({ std, model }) => {
(async () => {
const file = await openSingleFileWith();
const file = await openFileOrFiles();
if (!file) return;
await addSiblingAttachmentBlocks(std, [file], model);
@@ -44,7 +44,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
model.store.schema.flavourSchemaMap.has('affine:attachment'),
action: ({ std, model }) => {
(async () => {
const file = await openSingleFileWith();
const file = await openFileOrFiles();
if (!file) return;
await addSiblingAttachmentBlocks(std, [file], model);

View File

@@ -24,7 +24,6 @@ import {
DownloadIcon,
DuplicateIcon,
EditIcon,
ReplaceIcon,
ResetIcon,
} from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier } from '@blocksuite/std';
@@ -140,42 +139,27 @@ export const attachmentViewDropdownMenu = {
});
};
return html`<affine-view-dropdown-menu
@toggle=${onToggle}
.actions=${actions.value}
.context=${ctx}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`;
return html`${keyed(
model,
html`<affine-view-dropdown-menu
@toggle=${onToggle}
.actions=${actions.value}
.context=${ctx}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
} as const satisfies ToolbarActionGroup<ToolbarAction>;
const replaceAction = {
id: 'c.replace',
tooltip: 'Replace attachment',
icon: ReplaceIcon(),
disabled(ctx) {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
if (!block) return true;
const { downloading = false, uploading = false } =
block.resourceController.state$.value;
return downloading || uploading;
},
run(ctx) {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
block?.replace().catch(console.error);
},
} as const satisfies ToolbarAction;
const downloadAction = {
id: 'd.download',
id: 'c.download',
tooltip: 'Download',
icon: DownloadIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
block?.download();
},
when(ctx) {
when: ctx => {
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
if (!model) return false;
// Current citation attachment block does not support download
@@ -184,7 +168,7 @@ const downloadAction = {
} as const satisfies ToolbarAction;
const captionAction = {
id: 'e.caption',
id: 'd.caption',
tooltip: 'Caption',
icon: CaptionIcon(),
run(ctx) {
@@ -237,7 +221,6 @@ const builtinToolbarConfig = {
},
},
attachmentViewDropdownMenu,
replaceAction,
downloadAction,
captionAction,
{
@@ -281,12 +264,6 @@ const builtinToolbarConfig = {
run(ctx) {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
block?.reload();
ctx.track('AttachmentReloadedEvent', {
...trackBaseProps,
control: 'reload',
type: block?.model.props.name.split('.').pop() ?? '',
});
},
},
{
@@ -371,17 +348,13 @@ const builtinSurfaceToolbarConfig = {
)}`;
},
} satisfies ToolbarActionGroup<ToolbarAction>,
{
...replaceAction,
id: 'd.replace',
},
{
...downloadAction,
id: 'e.download',
id: 'd.download',
},
{
...captionAction,
id: 'f.caption',
id: 'e.caption',
},
],
when: ctx => ctx.getSurfaceModelsByType(AttachmentBlockModel).length === 1,

View File

@@ -15,10 +15,6 @@ export const styles = css`
}
}
.affine-attachment-container.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
.affine-attachment-card {
display: flex;
gap: 12px;
@@ -51,10 +47,11 @@ export const styles = css`
.affine-attachment-content-title-icon {
display: flex;
width: 16px;
height: 16px;
align-items: center;
justify-content: center;
color: var(--affine-text-primary-color);
font-size: 16px;
}
.affine-attachment-content-title-text {
@@ -95,7 +92,6 @@ export const styles = css`
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 500;
text-transform: capitalize;
line-height: 20px;
svg {
@@ -111,7 +107,7 @@ export const styles = css`
.affine-attachment-card.loading {
.affine-attachment-content-title-text {
color: ${unsafeCSSVarV2('text/placeholder')};
color: var(--affine-placeholder-color);
}
}

View File

@@ -13,7 +13,7 @@ import {
FileSizeLimitProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { formatSize } from '@blocksuite/affine-shared/utils';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
import { Bound, type IVec, Vec } from '@blocksuite/global/gfx';
import type { BlockStdScope } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
@@ -93,7 +93,7 @@ function hasExceeded(
const exceeded = files.some(file => file.size > maxFileSize);
if (exceeded) {
const size = formatSize(maxFileSize);
const size = humanFileSize(maxFileSize, true, 0);
toast(std.host, `You can only upload files less than ${size}`);
}
@@ -130,7 +130,7 @@ async function buildPropsWith(
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
page: `${mode} editor`,
module: 'attachment',
segment: mode,
segment: 'attachment',
control: 'uploader',
type,
category,

View File

@@ -19,23 +19,24 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.16",
"@toeverything/theme": "^1.1.14",
"lit": "^3.2.0",
"minimatch": "^10.1.1",
"rxjs": "^7.8.2",
"yjs": "^13.6.27",
"zod": "^3.25.76"
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"yjs": "^13.6.23",
"zod": "^3.23.8"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "3.1.3"
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./store": "./src/store.ts",
"./view": "./src/view.ts"
},
@@ -45,5 +46,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.25.7"
"version": "0.21.0"
}

View File

@@ -10,6 +10,7 @@ import {
isFootnoteDefinitionNode,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { nanoid } from '@blocksuite/store';
const isUrlFootnoteDefinitionNode = (node: MarkdownAST) => {
@@ -32,7 +33,15 @@ export const bookmarkBlockMarkdownAdapterMatcher =
toMatch: o => isUrlFootnoteDefinitionNode(o.node),
toBlockSnapshot: {
enter: (o, context) => {
if (!isFootnoteDefinitionNode(o.node)) {
const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
return;
}

View File

@@ -8,19 +8,15 @@ import type {
} from '@blocksuite/affine-model';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import {
BlockElementCommentManager,
CitationProvider,
DocModeProvider,
LinkPreviewServiceIdentifier,
} from '@blocksuite/affine-shared/services';
import { normalizeUrl } from '@blocksuite/affine-shared/utils';
import { BlockSelection } from '@blocksuite/std';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import { html } from 'lit';
import { property, query } from 'lit/decorators.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { filter } from 'rxjs/operators';
import { refreshBookmarkUrlData } from './utils.js';
@@ -103,12 +99,12 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
selectionManager.setGroup('note', [blockSelection]);
};
get link() {
return normalizeUrl(this.model.props.url);
}
open = () => {
window.open(this.link, '_blank');
let link = this.model.props.url;
if (!link.match(/^[a-zA-Z]+:\/\//)) {
link = 'https://' + link;
}
window.open(link, '_blank');
};
refreshData = () => {
@@ -117,26 +113,17 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
);
};
get citationService() {
return this.std.get(CitationProvider);
}
get isCitation() {
return this.citationService.isCitationModel(this.model);
return (
!!this.model.props.footnoteIdentifier &&
this.model.props.style === 'citation'
);
}
get imageProxyService() {
return this.std.get(ImageProxyService);
}
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockElementCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
handleClick = (event: MouseEvent) => {
event.stopPropagation();
@@ -178,31 +165,6 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
></bookmark-card>`;
};
private readonly _trackCitationDeleteEvent = () => {
// Check citation delete event
this._disposables.add(
this.std.store.slots.blockUpdated
.pipe(
filter(payload => {
if (!payload.isLocal) return false;
const { flavour, id, type } = payload;
if (
type !== 'delete' ||
flavour !== this.model.flavour ||
id !== this.model.id
)
return false;
const { model } = payload;
if (!this.citationService.isCitationModel(model)) return false;
return true;
})
)
.subscribe(() => {
this.citationService.trackEvent('Delete');
})
);
};
override connectedCallback() {
super.connectedCallback();
@@ -240,8 +202,6 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
}
})
);
this._trackCitationDeleteEvent();
}
override disconnectedCallback(): void {

View File

@@ -29,15 +29,6 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
};
}
override connectedCallback(): void {
super.connectedCallback();
this.disposables.add(
this.gfx.selection.slots.updated.subscribe(() => {
this.requestUpdate();
})
);
}
override renderGfxBlock() {
const style = this.model.props.style$.value;
const width = EMBED_CARD_WIDTH[style];
@@ -45,14 +36,12 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
const bound = this.model.elementBound;
const scaleX = bound.w / width;
const scaleY = bound.h / height;
const isSelected = this.gfx.selection.has(this.model.id);
this.containerStyleMap = styleMap({
width: `100%`,
height: `100%`,
transform: `scale(${scaleX}, ${scaleY})`,
transformOrigin: '0 0',
pointerEvents: isSelected ? 'auto' : 'none',
});
return this.renderPageContent();

View File

@@ -1,3 +1,5 @@
import '@blocksuite/affine-block-embed/effects';
import { insertEmbedCard } from '@blocksuite/affine-block-embed';
import type { EmbedCardStyle } from '@blocksuite/affine-model';
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';

View File

@@ -1,5 +1,5 @@
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
import { LoadingIcon, WebIcon16 } from '@blocksuite/affine-components/icons';
import { WebIcon16 } from '@blocksuite/affine-components/icons';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { getHostName } from '@blocksuite/affine-shared/utils';
@@ -45,7 +45,6 @@ export class BookmarkCard extends SignalWatcher(
[style]: true,
selected: this.bookmark.selected$.value,
edgeless: isGfxBlockComponent(this.bookmark),
'comment-highlighted': this.bookmark.isCommentHighlighted,
});
const domainName = url.match(
@@ -61,11 +60,11 @@ export class BookmarkCard extends SignalWatcher(
: title;
const theme = this.bookmark.std.get(ThemeProvider).theme;
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const imageProxyService = this.bookmark.store.get(ImageProxyService);
const titleIcon = this.loading
? LoadingIcon()
? LoadingIcon
: icon
? html`<img src=${imageProxyService.buildUrl(icon)} alt="icon" />`
: WebIcon16;

Some files were not shown because too many files have changed in this diff Show More