Compare commits

..

4 Commits

Author SHA1 Message Date
DarkSky
891b4ed7e4 Merge branch 'canary' into darksky/bump-playwright 2026-01-04 17:26:05 +08:00
DarkSky
3b09cc9557 Merge branch 'canary' into darksky/bump-playwright 2025-12-24 13:39:37 +08:00
DarkSky
02782c9097 Merge branch 'canary' into darksky/bump-playwright 2025-12-13 18:12:18 +08:00
DarkSky
2c73413a6a feat: bump playwright 2025-11-15 17:03:45 +08:00
1190 changed files with 23004 additions and 58842 deletions

View File

@@ -222,7 +222,7 @@
}, },
"SMTP.sender": { "SMTP.sender": {
"type": "string", "type": "string",
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted &lt;noreply@example.com&gt;\")\n@default \"AFFiNE Self Hosted <noreply@example.com>\"\n@environment `MAILER_SENDER`", "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>" "default": "AFFiNE Self Hosted <noreply@example.com>"
}, },
"SMTP.ignoreTLS": { "SMTP.ignoreTLS": {
@@ -262,7 +262,7 @@
}, },
"fallbackSMTP.sender": { "fallbackSMTP.sender": {
"type": "string", "type": "string",
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted &lt;noreply@example.com&gt;\")\n@default \"\"", "description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"\"",
"default": "" "default": ""
}, },
"fallbackSMTP.ignoreTLS": { "fallbackSMTP.ignoreTLS": {
@@ -337,42 +337,8 @@
}, },
"config": { "config": {
"type": "object", "type": "object",
"description": "The config for the S3 compatible storage provider.", "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"properties": { "properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
"description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": { "credentials": {
"type": "object", "type": "object",
"description": "The credentials for the s3 compatible storage provider.", "description": "The credentials for the s3 compatible storage provider.",
@@ -382,9 +348,6 @@
}, },
"secretAccessKey": { "secretAccessKey": {
"type": "string" "type": "string"
},
"sessionToken": {
"type": "string"
} }
} }
} }
@@ -406,42 +369,8 @@
}, },
"config": { "config": {
"type": "object", "type": "object",
"description": "The config for the S3 compatible storage provider.", "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"properties": { "properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
"description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": { "credentials": {
"type": "object", "type": "object",
"description": "The credentials for the s3 compatible storage provider.", "description": "The credentials for the s3 compatible storage provider.",
@@ -451,9 +380,6 @@
}, },
"secretAccessKey": { "secretAccessKey": {
"type": "string" "type": "string"
},
"sessionToken": {
"type": "string"
} }
} }
}, },
@@ -532,42 +458,8 @@
}, },
"config": { "config": {
"type": "object", "type": "object",
"description": "The config for the S3 compatible storage provider.", "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"properties": { "properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
"description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": { "credentials": {
"type": "object", "type": "object",
"description": "The credentials for the s3 compatible storage provider.", "description": "The credentials for the s3 compatible storage provider.",
@@ -577,9 +469,6 @@
}, },
"secretAccessKey": { "secretAccessKey": {
"type": "string" "type": "string"
},
"sessionToken": {
"type": "string"
} }
} }
} }
@@ -601,42 +490,8 @@
}, },
"config": { "config": {
"type": "object", "type": "object",
"description": "The config for the S3 compatible storage provider.", "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"properties": { "properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
"description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": { "credentials": {
"type": "object", "type": "object",
"description": "The credentials for the s3 compatible storage provider.", "description": "The credentials for the s3 compatible storage provider.",
@@ -646,9 +501,6 @@
}, },
"secretAccessKey": { "secretAccessKey": {
"type": "string" "type": "string"
},
"sessionToken": {
"type": "string"
} }
} }
}, },
@@ -743,11 +595,6 @@
"description": "Multiple hosts the server will accept requests from.\n@default []", "description": "Multiple hosts the server will accept requests from.\n@default []",
"default": [] "default": []
}, },
"listenAddr": {
"type": "string",
"description": "The address to listen on (e.g., 0.0.0.0 for IPv4, :: for IPv6).\n@default \"0.0.0.0\"\n@environment `LISTEN_ADDR`",
"default": "0.0.0.0"
},
"port": { "port": {
"type": "number", "type": "number",
"description": "Which port the server will listen on.\n@default 3010\n@environment `AFFINE_SERVER_PORT`", "description": "Which port the server will listen on.\n@default 3010\n@environment `AFFINE_SERVER_PORT`",
@@ -782,45 +629,6 @@
} }
} }
}, },
"telemetry": {
"type": "object",
"description": "Configuration for telemetry module",
"properties": {
"allowedOrigin": {
"type": "array",
"description": "Allowed origins for telemetry collection.\n@default [\"localhost\",\"127.0.0.1\"]",
"default": [
"localhost",
"127.0.0.1"
]
},
"ga4.measurementId": {
"type": "string",
"description": "GA4 Measurement ID for Measurement Protocol.\n@default \"\"\n@environment `GA4_MEASUREMENT_ID`",
"default": ""
},
"ga4.apiSecret": {
"type": "string",
"description": "GA4 API secret for Measurement Protocol.\n@default \"\"\n@environment `GA4_API_SECRET`",
"default": ""
},
"dedupe.ttlHours": {
"type": "number",
"description": "Telemetry dedupe TTL in hours.\n@default 24",
"default": 24
},
"dedupe.maxEntries": {
"type": "number",
"description": "Telemetry dedupe max entries.\n@default 100000",
"default": 100000
},
"batch.maxEvents": {
"type": "number",
"description": "Max events per telemetry batch.\n@default 25",
"default": 25
}
}
},
"client": { "client": {
"type": "object", "type": "object",
"description": "Configuration for client module", "description": "Configuration for client module",
@@ -832,108 +640,8 @@
}, },
"versionControl.requiredVersion": { "versionControl.requiredVersion": {
"type": "string", "type": "string",
"description": "Allowed version range of the app that allowed to access the server. Requires 'client/versionControl.enabled' to be true to take effect.\n@default \">=0.25.0\"", "description": "Allowed version range of the app that allowed to access the server. Requires 'client/versionControl.enabled' to be true to take effect.\n@default \">=0.20.0\"",
"default": ">=0.25.0" "default": ">=0.20.0"
}
}
},
"calendar": {
"type": "object",
"description": "Configuration for calendar module",
"properties": {
"google": {
"type": "object",
"description": "Google Calendar integration config\n@default {\"enabled\":false,\"clientId\":\"\",\"clientSecret\":\"\",\"externalWebhookUrl\":\"\",\"webhookVerificationToken\":\"\"}\n@link https://developers.google.com/calendar/api/guides/push",
"properties": {
"enabled": {
"type": "boolean"
},
"clientId": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"externalWebhookUrl": {
"type": "string"
},
"webhookVerificationToken": {
"type": "string"
}
},
"default": {
"enabled": false,
"clientId": "",
"clientSecret": "",
"externalWebhookUrl": "",
"webhookVerificationToken": ""
}
},
"caldav": {
"type": "object",
"description": "CalDAV integration config\n@default {\"enabled\":false,\"allowCustomProvider\":false,\"providers\":[],\"allowInsecureHttp\":false,\"allowedHosts\":[],\"blockPrivateNetwork\":true,\"requestTimeoutMs\":10000,\"maxRedirects\":5}",
"properties": {
"enabled": {
"type": "boolean"
},
"allowCustomProvider": {
"type": "boolean"
},
"providers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"label": {
"type": "string"
},
"serverUrl": {
"type": "string"
},
"authType": {
"type": "string"
},
"requiresAppPassword": {
"type": "boolean"
},
"docsUrl": {
"type": "string"
}
}
}
},
"allowInsecureHttp": {
"type": "boolean"
},
"allowedHosts": {
"type": "array",
"items": {
"type": "string"
}
},
"blockPrivateNetwork": {
"type": "boolean"
},
"requestTimeoutMs": {
"type": "number"
},
"maxRedirects": {
"type": "number"
}
},
"default": {
"enabled": false,
"allowCustomProvider": false,
"providers": [],
"allowInsecureHttp": false,
"allowedHosts": [],
"blockPrivateNetwork": true,
"requestTimeoutMs": 10000,
"maxRedirects": 5
}
} }
} }
}, },
@@ -1155,42 +863,8 @@
}, },
"config": { "config": {
"type": "object", "type": "object",
"description": "The config for the S3 compatible storage provider.", "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"properties": { "properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
"description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": { "credentials": {
"type": "object", "type": "object",
"description": "The credentials for the s3 compatible storage provider.", "description": "The credentials for the s3 compatible storage provider.",
@@ -1200,9 +874,6 @@
}, },
"secretAccessKey": { "secretAccessKey": {
"type": "string" "type": "string"
},
"sessionToken": {
"type": "string"
} }
} }
} }
@@ -1224,42 +895,8 @@
}, },
"config": { "config": {
"type": "object", "type": "object",
"description": "The config for the S3 compatible storage provider.", "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
"properties": { "properties": {
"endpoint": {
"type": "string",
"description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://<account>.r2.cloudflarestorage.com\"."
},
"region": {
"type": "string",
"description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2."
},
"forcePathStyle": {
"type": "boolean",
"description": "Whether to use path-style bucket addressing."
},
"requestTimeoutMs": {
"type": "number",
"description": "Request timeout in milliseconds."
},
"minPartSize": {
"type": "number",
"description": "Minimum multipart part size in bytes."
},
"presign": {
"type": "object",
"description": "Presigned URL behavior configuration.",
"properties": {
"expiresInSeconds": {
"type": "number",
"description": "Expiration time in seconds for presigned URLs."
},
"signContentTypeForPut": {
"type": "boolean",
"description": "Whether to sign Content-Type for presigned PUT."
}
}
},
"credentials": { "credentials": {
"type": "object", "type": "object",
"description": "The credentials for the s3 compatible storage provider.", "description": "The credentials for the s3 compatible storage provider.",
@@ -1269,9 +906,6 @@
}, },
"secretAccessKey": { "secretAccessKey": {
"type": "string" "type": "string"
},
"sessionToken": {
"type": "string"
} }
} }
}, },

View File

@@ -1,26 +0,0 @@
.git
.github/**/*.md
.gitignore
# Local dependency/build artifacts
/node_modules
/target
# Yarn v4 artifacts (not needed for image packaging)
/.yarn/cache
/.yarn/unplugged
/.yarn/install-state.gz
/.pnp.*
# Test artifacts
/test-results
/playwright-report
/coverage
/.coverage
# OS noise
.DS_Store
# Sourcemaps (keep server sourcemap for backend stacktraces)
**/*.map
!packages/backend/server/dist/main.js.map

View File

@@ -1,8 +1,6 @@
# Editor configuration, see http://editorconfig.org # Editor configuration, see http://editorconfig.org
root = true root = true
[*.rs]
max_line_length = 120
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = space

View File

@@ -2,6 +2,7 @@ name: Feature Request
description: Suggest a feature or improvement description: Suggest a feature or improvement
title: '[Feature Request]: ' title: '[Feature Request]: '
labels: ['feat', 'story'] labels: ['feat', 'story']
assignees: ['hwangdev97']
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@@ -25,30 +25,47 @@ const buildType = BUILD_TYPE || 'canary';
const isProduction = buildType === 'stable'; const isProduction = buildType === 'stable';
const isBeta = buildType === 'beta'; const isBeta = buildType === 'beta';
const isCanary = buildType === 'canary';
const isInternal = buildType === 'internal'; const isInternal = buildType === 'internal';
const isSpotEnabled = isBeta || isCanary;
const replicaConfig = { const replicaConfig = {
stable: { stable: {
front: Number(process.env.PRODUCTION_FRONT_REPLICA) || 2, web: 2,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2, graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 2,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 2,
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2,
}, },
beta: { beta: {
front: Number(process.env.BETA_FRONT_REPLICA) || 1, web: 1,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1, graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1,
sync: Number(process.env.BETA_SYNC_REPLICA) || 1,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 1,
doc: Number(process.env.BETA_DOC_REPLICA) || 1,
},
canary: {
web: 1,
graphql: 1,
sync: 1,
renderer: 1,
doc: 1,
}, },
canary: { front: 1, graphql: 1 },
}; };
const cpuConfig = { const cpuConfig = {
beta: { front: '1', graphql: '1' }, beta: {
canary: { front: '500m', graphql: '1' }, web: '300m',
}; graphql: '1',
sync: '1',
const memoryConfig = { doc: '1',
beta: { front: '2Gi', graphql: '1Gi' }, renderer: '300m',
canary: { front: '512Mi', graphql: '512Mi' }, },
canary: {
web: '300m',
graphql: '1',
sync: '1',
doc: '1',
renderer: '300m',
},
}; };
const createHelmCommand = ({ isDryRun }) => { const createHelmCommand = ({ isDryRun }) => {
@@ -72,47 +89,32 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string global.indexer.endpoint="${AFFINE_INDEXER_SEARCH_ENDPOINT}"`, `--set-string global.indexer.endpoint="${AFFINE_INDEXER_SEARCH_ENDPOINT}"`,
`--set-string global.indexer.apiKey="${AFFINE_INDEXER_SEARCH_API_KEY}"`, `--set-string global.indexer.apiKey="${AFFINE_INDEXER_SEARCH_API_KEY}"`,
]; ];
const cloudSqlNodeSelector = isBeta
? `{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\", \\"cloud.google.com/gke-spot\\": \\"true\\" }`
: `{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }`;
const serviceAnnotations = [ const serviceAnnotations = [
`--set-json front.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`, `--set-json web.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json graphql.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`, `--set-json graphql.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json sync.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json doc.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
].concat( ].concat(
isProduction || isBeta || isInternal isProduction || isBeta || isInternal
? [ ? [
`--set-json front.services.web.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`, `--set-json web.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json front.services.sync.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json front.services.renderer.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json graphql.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`, `--set-json graphql.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json sync.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
`--set-json cloud-sql-proxy.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }"`, `--set-json cloud-sql-proxy.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }"`,
`--set-json cloud-sql-proxy.nodeSelector="${cloudSqlNodeSelector}"`, `--set-json cloud-sql-proxy.nodeSelector="{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }"`,
] ]
: [] : []
); );
const spotNodeSelector = `{ \\"cloud.google.com/gke-spot\\": \\"true\\" }`;
const spotScheduling = isSpotEnabled
? [
`--set-json front.nodeSelector="${spotNodeSelector}"`,
`--set-json graphql.nodeSelector="${spotNodeSelector}"`,
]
: [];
const cpu = cpuConfig[buildType]; const cpu = cpuConfig[buildType];
const memory = memoryConfig[buildType]; const resources = cpu
let resources = []; ? [
if (cpu) { `--set web.resources.requests.cpu="${cpu.web}"`,
resources = resources.concat([ `--set graphql.resources.requests.cpu="${cpu.graphql}"`,
`--set front.resources.requests.cpu="${cpu.front}"`, `--set sync.resources.requests.cpu="${cpu.sync}"`,
`--set graphql.resources.requests.cpu="${cpu.graphql}"`, `--set doc.resources.requests.cpu="${cpu.doc}"`,
]); ]
} : [];
if (memory) {
resources = resources.concat([
`--set front.resources.requests.memory="${memory.front}"`,
`--set graphql.resources.requests.memory="${memory.graphql}"`,
]);
}
const replica = replicaConfig[buildType] || replicaConfig.canary; const replica = replicaConfig[buildType] || replicaConfig.canary;
@@ -128,7 +130,6 @@ const createHelmCommand = ({ isDryRun }) => {
.split(',') .split(',')
.map(host => host.trim()) .map(host => host.trim())
.filter(host => host); .filter(host => host);
const primaryHost = hosts[0] || '0.0.0.0';
const deployCommand = [ const deployCommand = [
`helm upgrade --install affine .github/helm/affine`, `helm upgrade --install affine .github/helm/affine`,
`--namespace ${namespace}`, `--namespace ${namespace}`,
@@ -143,14 +144,20 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string global.version="${APP_VERSION}"`, `--set-string global.version="${APP_VERSION}"`,
...redisAndPostgres, ...redisAndPostgres,
...indexerOptions, ...indexerOptions,
`--set front.replicaCount=${replica.front}`, `--set web.replicaCount=${replica.web}`,
`--set-string front.image.tag="${imageTag}"`, `--set-string web.image.tag="${imageTag}"`,
`--set-string front.app.host="${primaryHost}"`,
`--set graphql.replicaCount=${replica.graphql}`, `--set graphql.replicaCount=${replica.graphql}`,
`--set-string graphql.image.tag="${imageTag}"`, `--set-string graphql.image.tag="${imageTag}"`,
`--set-string graphql.app.host="${primaryHost}"`, `--set graphql.app.host=${hosts[0]}`,
`--set sync.replicaCount=${replica.sync}`,
`--set-string sync.image.tag="${imageTag}"`,
`--set-string renderer.image.tag="${imageTag}"`,
`--set renderer.app.host=${hosts[0]}`,
`--set renderer.replicaCount=${replica.renderer}`,
`--set-string doc.image.tag="${imageTag}"`,
`--set doc.app.host=${hosts[0]}`,
`--set doc.replicaCount=${replica.doc}`,
...serviceAnnotations, ...serviceAnnotations,
...spotScheduling,
...resources, ...resources,
`--timeout 10m`, `--timeout 10m`,
flag, flag,

13
.github/deployment/front/Dockerfile vendored Normal file
View File

@@ -0,0 +1,13 @@
FROM openresty/openresty:1.27.1.1-0-buster
WORKDIR /app
COPY ./packages/frontend/apps/web/dist ./dist
COPY ./packages/frontend/admin/dist ./admin
COPY ./packages/frontend/apps/mobile/dist ./mobile
COPY ./.github/deployment/front/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY ./.github/deployment/front/affine.nginx.conf /etc/nginx/conf.d/affine.nginx.conf
RUN mkdir -p /var/log/nginx && \
rm /etc/nginx/conf.d/default.conf
EXPOSE 8080
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]

View File

@@ -0,0 +1,42 @@
server {
listen 8080;
location /admin {
root /app/;
index index.html;
try_files $uri/index.html $uri/ $uri /admin/index.html;
}
set $app_root_path /app/dist/;
set $mobile_root /app/dist/;
set_by_lua $affine_env 'return os.getenv("AFFINE_ENV")';
if ($affine_env = "dev") {
set $mobile_root /app/mobile/;
}
# https://gist.github.com/mariusom/6683dc52b1cad1a1f372e908bdb209d0
if ($http_user_agent ~* "(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino") {
set $app_root_path $mobile_root;
}
if ($http_user_agent ~* "^(1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-)") {
set $app_root_path $mobile_root;
}
location ~ ^/(_plugin|assets|imgs|js|plugins|static)/ {
root $app_root_path;
try_files $uri $uri/ =404;
}
location / {
root $app_root_path;
index index.html;
try_files $uri $uri/ /index.html;
add_header Cache-Control "private, no-cache, no-store, max-age=0, must-revalidate";
}
error_page 404 /404.html;
location = /404.html {
internal;
}
}

15
.github/deployment/front/nginx.conf vendored Normal file
View File

@@ -0,0 +1,15 @@
worker_processes 4;
error_log /var/log/nginx/error.log warn;
pcre_jit on;
env AFFINE_ENV;
events {
worker_connections 1024;
}
http {
include mime.types;
log_format main '$remote_addr [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -1,28 +1,11 @@
# syntax=docker/dockerfile:1.7 FROM node:22-bookworm-slim
FROM node:22-bookworm-slim AS assets
WORKDIR /app
COPY ./packages/backend/server /app COPY ./packages/backend/server /app
COPY ./packages/frontend/apps/web/dist /app/static COPY ./packages/frontend/apps/web/dist /app/static
COPY ./packages/frontend/admin/dist /app/static/admin COPY ./packages/frontend/admin/dist /app/static/admin
COPY ./packages/frontend/apps/mobile/dist /app/static/mobile COPY ./packages/frontend/apps/mobile/dist /app/static/mobile
# Keep server sourcemap for stacktraces, but don't ship frontend/node_modules sourcemaps.
ARG TARGETARCH
ARG TARGETVARIANT
# Needed for Prisma engine resolution (and potential engine download during cleanup).
RUN apt-get update && \
apt-get install -y --no-install-recommends openssl ca-certificates && \
rm -rf /var/lib/apt/lists/*
RUN AFFINE_DOCKER_CLEAN=1 TARGETARCH="${TARGETARCH}" TARGETVARIANT="${TARGETVARIANT}" node ./scripts/docker-clean.mjs
FROM node:22-bookworm-slim
WORKDIR /app WORKDIR /app
COPY --from=assets /app /app
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends openssl libjemalloc2 && \ apt-get install -y --no-install-recommends openssl libjemalloc2 && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
@@ -30,6 +13,4 @@ RUN apt-get update && \
# Enable jemalloc by preloading the library # Enable jemalloc by preloading the library
ENV LD_PRELOAD=libjemalloc.so.2 ENV LD_PRELOAD=libjemalloc.so.2
EXPOSE 3010
CMD ["node", "./dist/main.js"] CMD ["node", "./dist/main.js"]

View File

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

View File

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

View File

@@ -1,15 +1,15 @@
1. Get the application URL by running these commands: 1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.services.sync.type }} {{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ .Values.services.sync.name }}) export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "doc.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.services.sync.type }} {{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available. NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ .Values.services.sync.name }}' You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "doc.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ .Values.services.sync.name }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "doc.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.services.sync.port }} echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.services.sync.type }} {{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "front.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "doc.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application" echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT

View File

@@ -0,0 +1,63 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "doc.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "doc.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "doc.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "doc.labels" -}}
helm.sh/chart: {{ include "doc.chart" . }}
{{ include "doc.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
monitoring: enabled
{{- end }}
{{/*
Selector labels
*/}}
{{- define "doc.selectorLabels" -}}
app.kubernetes.io/name: {{ include "doc.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "doc.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "doc.fullname" .) .Values.global.docService.name }}
{{- else }}
{{- default "default" .Values.global.docService.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,118 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "doc.fullname" . }}
labels:
{{- include "doc.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "doc.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "doc.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "doc.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NODE_OPTIONS
value: "--max-old-space-size=4096"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "{{ .Values.global.deployment.type }}"
- name: DEPLOYMENT_PLATFORM
value: "{{ .Values.global.deployment.platform }}"
- name: SERVER_FLAVOR
value: "doc"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_ENABLED
value: "true"
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT
value: "{{ .Values.global.docService.port }}"
- name: AFFINE_SERVER_SUB_PATH
value: "{{ .Values.app.path }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: AFFINE_SERVER_HTTPS
value: "{{ .Values.app.https }}"
ports:
- name: http
containerPort: {{ .Values.global.docService.port }}
protocol: TCP
livenessProbe:
httpGet:
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 }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -1,19 +1,19 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: {{ .Values.global.docService.name }} name: {{ include "doc.fullname" . }}
labels: labels:
{{- include "front.labels" . | nindent 4 }} {{- include "doc.labels" . | nindent 4 }}
{{- with .Values.services.doc.annotations }} {{- with .Values.service.annotations }}
annotations: annotations:
{{- toYaml . | nindent 4 }} {{- toYaml . | nindent 4 }}
{{- end }} {{- end }}
spec: spec:
type: {{ .Values.services.doc.type }} type: {{ .Values.service.type }}
ports: ports:
- port: {{ .Values.global.docService.port }} - port: {{ .Values.global.docService.port }}
targetPort: http targetPort: http
protocol: TCP protocol: TCP
name: http name: http
selector: selector:
{{- include "front.selectorLabels" . | nindent 4 }} {{- include "doc.selectorLabels" . | nindent 4 }}

View File

@@ -2,9 +2,9 @@
apiVersion: v1 apiVersion: v1
kind: ServiceAccount kind: ServiceAccount
metadata: metadata:
name: {{ include "front.serviceAccountName" . }} name: {{ include "doc.serviceAccountName" . }}
labels: labels:
{{- include "front.labels" . | nindent 4 }} {{- include "doc.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }} {{- with .Values.serviceAccount.annotations }}
annotations: annotations:
{{- toYaml . | nindent 4 }} {{- toYaml . | nindent 4 }}

View File

@@ -1,9 +1,9 @@
apiVersion: v1 apiVersion: v1
kind: Pod kind: Pod
metadata: metadata:
name: "{{ include "front.fullname" . }}-test-connection" name: "{{ include "doc.fullname" . }}-test-connection"
labels: labels:
{{- include "front.labels" . | nindent 4 }} {{- include "doc.labels" . | nindent 4 }}
annotations: annotations:
"helm.sh/hook": test "helm.sh/hook": test
spec: spec:
@@ -11,5 +11,5 @@ spec:
- name: wget - name: wget
image: busybox image: busybox
command: ['wget'] command: ['wget']
args: ['{{ .Values.services.sync.name }}:{{ .Values.services.sync.port }}'] args: ['{{ include "doc.fullname" . }}:{{ .Values.global.docService.port }}']
restartPolicy: Never restartPolicy: Never

View File

@@ -30,12 +30,9 @@ podSecurityContext:
fsGroup: 2000 fsGroup: 2000
resources: resources:
limits:
cpu: '1'
memory: 4Gi
requests: requests:
cpu: '1' cpu: '1'
memory: 2Gi memory: 4Gi
probe: probe:
initialDelaySeconds: 20 initialDelaySeconds: 20

View File

@@ -1,118 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "front.fullname" . }}
labels:
{{- include "front.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "front.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "front.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "front.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NODE_OPTIONS
value: "{{ .Values.nodeOptions }}"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "{{ .Values.global.deployment.type }}"
- name: DEPLOYMENT_PLATFORM
value: "{{ .Values.global.deployment.platform }}"
- name: SERVER_FLAVOR
value: "front"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_ENABLED
value: "true"
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT
value: "{{ .Values.app.port }}"
- name: AFFINE_SERVER_SUB_PATH
value: "{{ .Values.app.path }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: AFFINE_SERVER_HTTPS
value: "{{ .Values.app.https }}"
ports:
- name: http
containerPort: {{ .Values.app.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.services.renderer.name }}
labels:
{{- include "front.labels" . | nindent 4 }}
{{- with .Values.services.renderer.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.services.renderer.type }}
ports:
- port: {{ .Values.services.renderer.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "front.selectorLabels" . | nindent 4 }}

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.services.sync.name }}
labels:
{{- include "front.labels" . | nindent 4 }}
{{- with .Values.services.sync.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.services.sync.type }}
ports:
- port: {{ .Values.services.sync.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "front.selectorLabels" . | nindent 4 }}

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.services.web.name }}
labels:
{{- include "front.labels" . | nindent 4 }}
{{- with .Values.services.web.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.services.web.type }}
ports:
- port: {{ .Values.services.web.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "front.selectorLabels" . | nindent 4 }}

View File

@@ -1,66 +0,0 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
nodeOptions: '--max-old-space-size=3072'
app:
# AFFINE_SERVER_PORT
port: 3010
# AFFINE_SERVER_SUB_PATH
path: ''
# AFFINE_SERVER_HOST
host: '0.0.0.0'
https: true
serviceAccount:
create: true
annotations: {}
name: 'affine-front'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
limits:
cpu: '1'
memory: 2Gi
requests:
cpu: '1'
memory: 2Gi
probe:
initialDelaySeconds: 20
services:
sync:
name: affine-sync
type: ClusterIP
port: 3010
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
renderer:
name: affine-renderer
type: ClusterIP
port: 3000
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
web:
name: affine-web
type: ClusterIP
port: 8080
annotations: {}
doc:
type: ClusterIP
annotations: {}
nodeSelector: {}
tolerations: []
affinity: {}

View File

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

View File

@@ -27,11 +27,8 @@ podSecurityContext:
fsGroup: 2000 fsGroup: 2000
resources: resources:
limits:
cpu: '1'
memory: 4Gi
requests: requests:
cpu: '1' cpu: '2'
memory: 2Gi memory: 2Gi
probe: probe:

View File

@@ -0,0 +1,11 @@
apiVersion: v2
name: renderer
description: AFFiNE renderer server
type: application
version: 0.0.0
appVersion: "0.25.7"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
repository: "file://../gcloud-sql-proxy"
condition: .global.database.gcloud.enabled

View File

@@ -0,0 +1,16 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "renderer.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "renderer.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "renderer.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "renderer.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,63 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "renderer.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "renderer.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "renderer.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "renderer.labels" -}}
helm.sh/chart: {{ include "renderer.chart" . }}
{{ include "renderer.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
monitoring: enabled
{{- end }}
{{/*
Selector labels
*/}}
{{- define "renderer.selectorLabels" -}}
app.kubernetes.io/name: {{ include "renderer.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "renderer.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "renderer.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,118 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "renderer.fullname" . }}
labels:
{{- include "renderer.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "renderer.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "renderer.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "renderer.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NODE_OPTIONS
value: "--max-old-space-size=2048"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "{{ .Values.global.deployment.type }}"
- name: DEPLOYMENT_PLATFORM
value: "{{ .Values.global.deployment.platform }}"
- name: SERVER_FLAVOR
value: "renderer"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_ENABLED
value: "true"
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_SUB_PATH
value: "{{ .Values.app.path }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: AFFINE_SERVER_HTTPS
value: "{{ .Values.app.https }}"
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

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

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "graphql.serviceAccountName" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "renderer.fullname" . }}-test-connection"
labels:
{{- include "renderer.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "renderer.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,38 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
app:
# AFFINE_SERVER_SUB_PATH
path: ''
# AFFINE_SERVER_HOST
host: '0.0.0.0'
https: true
serviceAccount:
create: true
annotations: {}
name: 'affine-renderer'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
requests:
cpu: '1'
memory: 2Gi
probe:
initialDelaySeconds: 20
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -1,9 +1,9 @@
apiVersion: v2 apiVersion: v2
name: front name: sync
description: AFFiNE front server description: AFFiNE Sync Server
type: application type: application
version: 0.0.0 version: 0.0.0
appVersion: "0.26.1" appVersion: "0.25.7"
dependencies: dependencies:
- name: gcloud-sql-proxy - name: gcloud-sql-proxy
version: 0.0.0 version: 0.0.0

View File

@@ -0,0 +1,16 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sync.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sync.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sync.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sync.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -1,7 +1,7 @@
{{/* {{/*
Expand the name of the chart. Expand the name of the chart.
*/}} */}}
{{- define "front.name" -}} {{- define "sync.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }} {{- end }}
@@ -10,7 +10,7 @@ Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name. If release name contains chart name it will be used as a full name.
*/}} */}}
{{- define "front.fullname" -}} {{- define "sync.fullname" -}}
{{- if .Values.fullnameOverride }} {{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }} {{- else }}
@@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name.
{{/* {{/*
Create chart name and version as used by the chart label. Create chart name and version as used by the chart label.
*/}} */}}
{{- define "front.chart" -}} {{- define "sync.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }} {{- end }}
{{/* {{/*
Common labels Common labels
*/}} */}}
{{- define "front.labels" -}} {{- define "sync.labels" -}}
helm.sh/chart: {{ include "front.chart" . }} helm.sh/chart: {{ include "sync.chart" . }}
{{ include "front.selectorLabels" . }} {{ include "sync.selectorLabels" . }}
{{- if .Chart.AppVersion }} {{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }} {{- end }}
@@ -46,17 +46,17 @@ monitoring: enabled
{{/* {{/*
Selector labels Selector labels
*/}} */}}
{{- define "front.selectorLabels" -}} {{- define "sync.selectorLabels" -}}
app.kubernetes.io/name: {{ include "front.name" . }} app.kubernetes.io/name: {{ include "sync.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }} {{- end }}
{{/* {{/*
Create the name of the service account to use Create the name of the service account to use
*/}} */}}
{{- define "front.serviceAccountName" -}} {{- define "sync.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }} {{- if .Values.serviceAccount.create }}
{{- default (include "front.fullname" .) .Values.serviceAccount.name }} {{- default (include "sync.fullname" .) .Values.serviceAccount.name }}
{{- else }} {{- else }}
{{- default "default" .Values.serviceAccount.name }} {{- default "default" .Values.serviceAccount.name }}
{{- end }} {{- end }}

View File

@@ -0,0 +1,112 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sync.fullname" . }}
labels:
{{- include "sync.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "sync.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "sync.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sync.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "{{ .Values.global.deployment.type }}"
- name: DEPLOYMENT_PLATFORM
value: "{{ .Values.global.deployment.platform }}"
- name: SERVER_FLAVOR
value: "sync"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.host }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

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

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "sync.serviceAccountName" . }}
labels:
{{- include "sync.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "sync.fullname" . }}-test-connection"
labels:
{{- include "sync.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "sync.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,38 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
app:
# AFFINE_SERVER_HOST
host: '0.0.0.0'
serviceAccount:
create: true
annotations: {}
name: 'affine-sync'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
limits:
cpu: '2'
memory: 4Gi
requests:
cpu: '1'
memory: 2Gi
probe:
initialDelaySeconds: 20
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,6 @@
apiVersion: v2
name: web
description: A Helm chart for Kubernetes
type: application
version: 0.0.0
appVersion: "0.7.0-canary.18"

View File

@@ -0,0 +1,16 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "web.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "web.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "web.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "web.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,63 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "web.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "web.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "web.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "web.labels" -}}
helm.sh/chart: {{ include "web.chart" . }}
{{ include "web.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
monitoring: enabled
{{- end }}
{{/*
Selector labels
*/}}
{{- define "web.selectorLabels" -}}
app.kubernetes.io/name: {{ include "web.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "web.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "web.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "web.fullname" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "web.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "web.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "web.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

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

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "web.serviceAccountName" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "web.fullname" . }}-test-connection"
labels:
{{- include "web.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "web.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,37 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine-front
pullPolicy: IfNotPresent
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: "affine-web"
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
limits:
cpu: '500m'
memory: 2Gi
requests:
cpu: '500m'
memory: 2Gi
nodeSelector: {}
tolerations: []
affinity: {}
probe:
initialDelaySeconds: 1

View File

@@ -44,9 +44,9 @@ spec:
pathType: Prefix pathType: Prefix
backend: backend:
service: service:
name: {{ $.Values.front.services.sync.name }} name: affine-sync
port: port:
number: {{ $.Values.front.services.sync.port }} number: {{ $.Values.sync.service.port }}
- path: /graphql - path: /graphql
pathType: Prefix pathType: Prefix
backend: backend:
@@ -65,15 +65,15 @@ spec:
pathType: Prefix pathType: Prefix
backend: backend:
service: service:
name: {{ $.Values.front.services.renderer.name }} name: affine-renderer
port: port:
number: {{ $.Values.front.services.renderer.port }} number: {{ $.Values.renderer.service.port }}
- path: / - path: /
pathType: Prefix pathType: Prefix
backend: backend:
service: service:
name: {{ $.Values.front.services.web.name }} name: affine-web
port: port:
number: {{ $.Values.front.services.web.port }} number: {{ $.Values.web.service.port }}
{{- end }} {{- end }}
{{- end }} {{- end }}

View File

@@ -47,25 +47,27 @@ graphql:
annotations: annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
front: sync:
services: service:
sync: type: ClusterIP
name: affine-sync port: 3010
type: ClusterIP annotations:
port: 3010 cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' renderer:
renderer: service:
name: affine-renderer type: ClusterIP
type: ClusterIP port: 3000
port: 3000 annotations:
annotations: cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
web: doc:
name: affine-web service:
type: ClusterIP type: ClusterIP
port: 8080 annotations:
doc: cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
type: ClusterIP
annotations: web:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}' service:
type: ClusterIP
port: 8080

View File

@@ -1,10 +1,6 @@
name: 'Pull Request Labeler' name: 'Pull Request Labeler'
on: on:
pull_request_target: - pull_request_target
types:
- opened
- reopened
- synchronize
jobs: jobs:
triage: triage:

View File

@@ -45,6 +45,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }} PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload web artifact - name: Upload web artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -77,6 +78,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }} PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload admin artifact - name: Upload admin artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -109,6 +111,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }} PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload mobile artifact - name: Upload mobile artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -263,7 +266,18 @@ jobs:
with: with:
app-version: ${{ inputs.app-version }} app-version: ${{ inputs.app-version }}
- name: Build backend Dockerfile - name: Build front Dockerfile
uses: docker/build-push-action@v6
with:
context: .
push: true
pull: true
platforms: linux/amd64,linux/arm64
provenance: true
file: .github/deployment/front/Dockerfile
tags: ghcr.io/toeverything/affine-front:${{inputs.build-type}}-${{ inputs.git-short-hash }}
- name: Build graphql Dockerfile
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .

View File

@@ -182,7 +182,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
shard: [1, 2] shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
@@ -210,13 +210,18 @@ jobs:
e2e-blocksuite-cross-browser-test: e2e-blocksuite-cross-browser-test:
name: E2E BlockSuite Cross Browser Test name: E2E BlockSuite Cross Browser Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2]
browser: ['chromium', 'firefox', 'webkit']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
with: with:
playwright-install: true playwright-install: true
playwright-platform: 'chromium,firefox,webkit' playwright-platform: ${{ matrix.browser }}
electron-install: false electron-install: false
full-cache: true full-cache: true
@@ -224,64 +229,18 @@ jobs:
run: yarn workspace @blocksuite/playground build run: yarn workspace @blocksuite/playground build
- name: Run playwright tests - name: Run playwright tests
run: | env:
yarn workspace @blocksuite/integration-test test:unit BROWSER: ${{ matrix.browser }}
yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only run: yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Upload test results - name: Upload test results
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: test-results-e2e-bs-cross-browser name: test-results-e2e-bs-cross-browser-${{ matrix.browser }}-${{ matrix.shard }}
path: ./test-results path: ./test-results
if-no-files-found: ignore if-no-files-found: ignore
bundler-matrix:
name: Bundler Matrix (${{ matrix.bundler }})
runs-on: ubuntu-24.04-arm
strategy:
fail-fast: false
matrix:
bundler: [webpack, rspack]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: false
electron-install: false
full-cache: true
- name: Run frontend build matrix
env:
AFFINE_BUNDLER: ${{ matrix.bundler }}
run: |
set -euo pipefail
packages=(
"@affine/web"
"@affine/mobile"
"@affine/ios"
"@affine/android"
"@affine/admin"
"@affine/electron-renderer"
)
summary="test-results-bundler-${AFFINE_BUNDLER}.txt"
: > "$summary"
for pkg in "${packages[@]}"; do
start=$(date +%s)
yarn affine "$pkg" build
end=$(date +%s)
echo "${pkg},$((end-start))" >> "$summary"
done
- name: Upload bundler timing
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-bundler-${{ matrix.bundler }}
path: ./test-results-bundler-${{ matrix.bundler }}.txt
if-no-files-found: ignore
e2e-test: e2e-test:
name: E2E Test name: E2E Test
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
@@ -292,7 +251,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
shard: [1, 2, 3, 4, 5] shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
@@ -323,7 +282,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
shard: [1, 2] shard: [1, 2, 3, 4, 5]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
@@ -348,13 +307,13 @@ jobs:
name: Unit Test name: Unit Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build-native-linux - build-native
env: env:
DISTRIBUTION: web DISTRIBUTION: web
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
shard: [1, 2, 3] shard: [1, 2, 3, 4, 5]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
@@ -362,7 +321,6 @@ jobs:
with: with:
electron-install: true electron-install: true
playwright-install: true playwright-install: true
playwright-platform: 'chromium,firefox,webkit'
full-cache: true full-cache: true
- name: Download affine.linux-x64-gnu.node - name: Download affine.linux-x64-gnu.node
@@ -383,39 +341,7 @@ jobs:
name: affine name: affine
fail_ci_if_error: false fail_ci_if_error: false
build-native-linux: build-native:
name: Build AFFiNE native (x86_64-unknown-linux-gnu)
runs-on: ubuntu-latest
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/native
electron-install: false
- name: Setup filename
id: filename
working-directory: ${{ github.workspace }}
shell: bash
run: |
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('x86_64-unknown-linux-gnu').platformArchABI)")
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: x86_64-unknown-linux-gnu
package: '@affine/native'
- name: Upload ${{ steps.filename.outputs.filename }}
uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ steps.filename.outputs.filename }}
path: ${{ github.workspace }}/packages/frontend/native/${{ steps.filename.outputs.filename }}
if-no-files-found: error
build-native-macos:
name: Build AFFiNE native (${{ matrix.spec.target }}) name: Build AFFiNE native (${{ matrix.spec.target }})
runs-on: ${{ matrix.spec.os }} runs-on: ${{ matrix.spec.os }}
env: env:
@@ -424,6 +350,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
spec: spec:
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
- { os: macos-latest, target: x86_64-apple-darwin } - { os: macos-latest, target: x86_64-apple-darwin }
- { os: macos-latest, target: aarch64-apple-darwin } - { os: macos-latest, target: aarch64-apple-darwin }
@@ -456,7 +383,7 @@ jobs:
# Split Windows build because it's too slow # Split Windows build because it's too slow
# and other ci jobs required linux native # and other ci jobs required linux native
build-native-windows: build-windows-native:
name: Build AFFiNE native (${{ matrix.spec.target }}) name: Build AFFiNE native (${{ matrix.spec.target }})
runs-on: ${{ matrix.spec.os }} runs-on: ${{ matrix.spec.os }}
env: env:
@@ -556,7 +483,7 @@ jobs:
name: Native Unit Test name: Native Unit Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build-native-linux - build-native
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
@@ -580,8 +507,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node_index: [0, 1, 2, 3] node_index: [0, 1, 2, 3, 4, 5, 6, 7]
total_nodes: [4] total_nodes: [8]
env: env:
NODE_ENV: test NODE_ENV: test
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
@@ -650,6 +577,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build-server-native - build-server-native
strategy:
fail-fast: false
env: env:
NODE_ENV: test NODE_ENV: test
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
@@ -869,6 +798,49 @@ jobs:
name: fuzz-artifact name: fuzz-artifact
path: packages/common/y-octo/utils/fuzz/artifacts/**/* path: packages/common/y-octo/utils/fuzz/artifacts/**/*
y-octo-binding-test:
name: y-octo binding test on ${{ matrix.settings.target }}
runs-on: ${{ matrix.settings.os }}
strategy:
fail-fast: false
matrix:
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: 'aarch64-apple-darwin', os: 'macos-latest' }
- { target: 'x86_64-pc-windows-msvc', os: 'windows-latest' }
- { target: 'aarch64-pc-windows-msvc', os: 'windows-11-arm' }
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine-tools/cli @affine/monorepo @y-octo/node
electron-install: false
- name: Install rustup (Windows 11 ARM)
if: matrix.settings.os == 'windows-11-arm'
shell: pwsh
run: |
Invoke-WebRequest -Uri "https://static.rust-lang.org/rustup/dist/aarch64-pc-windows-msvc/rustup-init.exe" -OutFile rustup-init.exe
.\rustup-init.exe --default-toolchain none -y
"$env:USERPROFILE\.cargo\bin" | Out-File -Append -Encoding ascii $env:GITHUB_PATH
"CARGO_HOME=$env:USERPROFILE\.cargo" | Out-File -Append -Encoding ascii $env:GITHUB_ENV
- name: Install Rust (Windows 11 ARM)
if: matrix.settings.os == 'windows-11-arm'
shell: pwsh
run: |
rustup install stable
rustup target add ${{ matrix.settings.target }}
cargo --version
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: ${{ matrix.settings.target }}
package: '@y-octo/node'
- name: Run tests
run: yarn affine @y-octo/node test
rust-test: rust-test:
name: Run native tests name: Run native tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -890,51 +862,11 @@ jobs:
- name: Run tests - name: Run tests
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
copilot-test-filter:
name: Copilot test filter
runs-on: ubuntu-latest
outputs:
run-api: ${{ steps.decision.outputs.run_api }}
run-e2e: ${{ steps.decision.outputs.run_e2e }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: copilot-filter
with:
filters: |
api:
- 'packages/backend/server/src/plugins/copilot/**'
- 'packages/backend/server/tests/copilot.*'
e2e:
- '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: Decide test scope
id: decision
run: |
if [[ "${{ steps.copilot-filter.outputs.api }}" == "true" ]]; then
echo "run_api=true" >> "$GITHUB_OUTPUT"
else
echo "run_api=false" >> "$GITHUB_OUTPUT"
fi
if [[ "${{ steps.copilot-filter.outputs.e2e }}" == "true" ]]; then
echo "run_e2e=true" >> "$GITHUB_OUTPUT"
else
echo "run_e2e=false" >> "$GITHUB_OUTPUT"
fi
copilot-api-test: copilot-api-test:
name: Server Copilot Api Test name: Server Copilot Api Test
if: ${{ needs.copilot-test-filter.outputs.run-api == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build-server-native - build-server-native
- copilot-test-filter
env: env:
NODE_ENV: test NODE_ENV: test
DISTRIBUTION: web DISTRIBUTION: web
@@ -968,29 +900,53 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Check blocksuite update
id: check-blocksuite-update
env:
BASE_REF: ${{ github.base_ref }}
run: |
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
echo "skip=false" >> $GITHUB_OUTPUT
else
echo "skip=true" >> $GITHUB_OUTPUT
fi
- uses: dorny/paths-filter@v3
id: apifilter
with:
filters: |
changed:
- 'packages/backend/server/src/plugins/copilot/**'
- 'packages/backend/server/tests/copilot.*'
- name: Setup Node.js - name: Setup Node.js
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
with: with:
electron-install: false electron-install: false
full-cache: true full-cache: true
- name: Download server-native.node - name: Download server-native.node
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: server-native.node name: server-native.node
path: ./packages/backend/native path: ./packages/backend/native
- name: Prepare Server Test Environment - name: Prepare Server Test Environment
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
env: env:
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }} SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
uses: ./.github/actions/server-test-env uses: ./.github/actions/server-test-env
- name: Run server tests - name: Run server tests
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
run: yarn affine @affine/server test:copilot:coverage --forbid-only run: yarn affine @affine/server test:copilot:coverage --forbid-only
env: env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target' CARGO_TARGET_DIR: '${{ github.workspace }}/target'
- name: Upload server test coverage results - name: Upload server test coverage results
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@@ -1001,7 +957,6 @@ jobs:
copilot-e2e-test: copilot-e2e-test:
name: Frontend Copilot E2E Test name: Frontend Copilot E2E Test
if: ${{ needs.copilot-test-filter.outputs.run-e2e == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DISTRIBUTION: web DISTRIBUTION: web
@@ -1012,11 +967,10 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
shardIndex: [1, 2, 3, 4, 5] shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shardTotal: [5] shardTotal: [10]
needs: needs:
- build-server-native - build-server-native
- copilot-test-filter
services: services:
postgres: postgres:
image: pgvector/pgvector:pg16 image: pgvector/pgvector:pg16
@@ -1040,7 +994,30 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Check blocksuite update
id: check-blocksuite-update
env:
BASE_REF: ${{ github.base_ref }}
run: |
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
echo "skip=false" >> $GITHUB_OUTPUT
else
echo "skip=true" >> $GITHUB_OUTPUT
fi
- uses: dorny/paths-filter@v3
id: e2efilter
with:
filters: |
changed:
- '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 - name: Setup Node.js
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
with: with:
playwright-install: true playwright-install: true
@@ -1049,17 +1026,20 @@ jobs:
hard-link-nm: false hard-link-nm: false
- name: Download server-native.node - name: Download server-native.node
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: server-native.node name: server-native.node
path: ./packages/backend/native path: ./packages/backend/native
- name: Prepare Server Test Environment - name: Prepare Server Test Environment
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
env: env:
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }} SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
uses: ./.github/actions/server-test-env uses: ./.github/actions/server-test-env
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
uses: ./.github/actions/copilot-test uses: ./.github/actions/copilot-test
with: with:
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
@@ -1069,7 +1049,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build-server-native - build-server-native
- build-native-linux - build-native
env: env:
DISTRIBUTION: web DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
@@ -1079,12 +1059,36 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
tests: tests:
- name: 'Cloud E2E Test 1/2' - name: 'Cloud E2E Test 1/10'
shard: 1 shard: 1
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/2 script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/10
- name: 'Cloud E2E Test 2/2' - name: 'Cloud E2E Test 2/10'
shard: 2 shard: 2
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/2 script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/10
- name: 'Cloud E2E Test 3/10'
shard: 3
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=3/10
- name: 'Cloud E2E Test 4/10'
shard: 4
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=4/10
- name: 'Cloud E2E Test 5/10'
shard: 5
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=5/10
- name: 'Cloud E2E Test 6/10'
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
- name: 'Cloud Desktop E2E Test' - name: 'Cloud Desktop E2E Test'
shard: desktop shard: desktop
script: | script: |
@@ -1162,9 +1166,7 @@ jobs:
runs-on: ${{ matrix.spec.os }} runs-on: ${{ matrix.spec.os }}
needs: needs:
- build-electron-renderer - build-electron-renderer
- build-native-linux - build-native
- build-native-macos
- build-native-windows
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -1247,6 +1249,84 @@ jobs:
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }} if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn affine @affine-test/affine-desktop e2e run: yarn affine @affine-test/affine-desktop e2e
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
path: ./test-results
if-no-files-found: ignore
desktop-bundle-check:
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:
- build-electron-renderer
- build-native
strategy:
fail-fast: false
matrix:
spec:
- {
os: macos-latest,
platform: macos,
arch: x64,
target: x86_64-apple-darwin,
test: false,
}
- {
os: macos-latest,
platform: macos,
arch: arm64,
target: aarch64-apple-darwin,
test: true,
}
- {
os: ubuntu-latest,
platform: linux,
arch: x64,
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
uses: ./.github/actions/setup-node
timeout-minutes: 10
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop @affine/nbstore @toeverything/infra
playwright-install: true
hard-link-nm: false
enableScripts: false
- name: Setup filename
id: filename
shell: bash
run: |
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
- name: Download ${{ steps.filename.outputs.filename }}
uses: actions/download-artifact@v4
with:
name: ${{ steps.filename.outputs.filename }}
path: ./packages/frontend/native
- name: Download web artifact
uses: ./.github/actions/download-web
with:
path: packages/frontend/apps/electron/resources/web-static
- name: Build Desktop Layers
run: yarn affine @affine/electron build
- name: Make bundle (macOS) - name: Make bundle (macOS)
if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }} if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }}
env: env:
@@ -1286,14 +1366,6 @@ jobs:
run: | run: |
yarn affine @affine/electron node ./scripts/macos-arm64-output-check.ts yarn affine @affine/electron node ./scripts/macos-arm64-output-check.ts
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
path: ./test-results
if-no-files-found: ignore
test-done: test-done:
needs: needs:
- analyze - analyze
@@ -1307,22 +1379,22 @@ jobs:
- e2e-blocksuite-cross-browser-test - e2e-blocksuite-cross-browser-test
- e2e-mobile-test - e2e-mobile-test
- unit-test - unit-test
- build-native-linux - build-native
- build-native-macos - build-windows-native
- build-native-windows
- build-server-native - build-server-native
- build-electron-renderer - build-electron-renderer
- native-unit-test - native-unit-test
- miri - miri
- loom - loom
- fuzzing - fuzzing
- y-octo-binding-test
- server-test - server-test
- server-e2e-test - server-e2e-test
- rust-test - rust-test
- copilot-test-filter
- copilot-api-test - copilot-api-test
- copilot-e2e-test - copilot-e2e-test
- desktop-test - desktop-test
- desktop-bundle-check
- cloud-e2e-test - cloud-e2e-test
if: always() if: always()
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -16,7 +16,6 @@ jobs:
check-pull-request-title: check-pull-request-title:
name: Check pull request title name: Check pull request title
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.action != 'edited' || github.event.changes.title != null }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js

View File

@@ -68,6 +68,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app_version }} SENTRY_RELEASE: ${{ inputs.app_version }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@@ -66,6 +66,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app-version }} SENTRY_RELEASE: ${{ inputs.app-version }}
RELEASE_VERSION: ${{ inputs.app-version }} RELEASE_VERSION: ${{ inputs.app-version }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload web artifact - name: Upload web artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -201,44 +202,13 @@ jobs:
nmHoistingLimits: workspaces nmHoistingLimits: workspaces
env: env:
npm_config_arch: ${{ matrix.spec.arch }} npm_config_arch: ${{ matrix.spec.arch }}
- name: Download packaged artifacts - name: Download and overwrite packaged artifacts
uses: actions/download-artifact@v4
with:
name: packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: packaged-unsigned
- name: unzip packaged artifacts
run: Expand-Archive -Path packaged-unsigned/archive.zip -DestinationPath packages/frontend/apps/electron/out
- name: Download signed packaged file diff
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: signed-packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }} name: signed-packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: signed-packaged-diff path: .
- name: Apply signed packaged file diff - name: unzip file
shell: pwsh run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out
run: |
$DiffRoot = 'signed-packaged-diff/files'
$TargetRoot = 'packages/frontend/apps/electron/out'
if (!(Test-Path -LiteralPath $DiffRoot)) {
throw "Signed diff directory not found: $DiffRoot"
}
Copy-Item -Path (Join-Path $DiffRoot '*') -Destination $TargetRoot -Recurse -Force
$ManifestPath = 'signed-packaged-diff/manifest.json'
if (Test-Path -LiteralPath $ManifestPath) {
$ManifestEntries = @(Get-Content -LiteralPath $ManifestPath | ConvertFrom-Json)
foreach ($Entry in $ManifestEntries) {
$TargetPath = Join-Path $TargetRoot $Entry.path
if (!(Test-Path -LiteralPath $TargetPath -PathType Leaf)) {
throw "Applied signed file not found: $($Entry.path)"
}
$TargetHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $TargetPath).Hash
if ($TargetHash -ne $Entry.sha256) {
throw "Signed file hash mismatch: $($Entry.path)"
}
}
}
- name: Make squirrel.windows installer - name: Make squirrel.windows installer
run: yarn affine @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }} run: yarn affine @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
@@ -298,44 +268,13 @@ jobs:
arch: arm64 arch: arm64
runs-on: ${{ matrix.spec.runner }} runs-on: ${{ matrix.spec.runner }}
steps: steps:
- name: Download installer artifacts - name: Download and overwrite installer artifacts
uses: actions/download-artifact@v4
with:
name: installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: installer-unsigned
- name: unzip installer artifacts
run: Expand-Archive -Path installer-unsigned/archive.zip -DestinationPath packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make
- name: Download signed installer file diff
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: signed-installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }} name: signed-installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: signed-installer-diff path: .
- name: Apply signed installer file diff - name: unzip file
shell: pwsh run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make
run: |
$DiffRoot = 'signed-installer-diff/files'
$TargetRoot = 'packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make'
if (!(Test-Path -LiteralPath $DiffRoot)) {
throw "Signed diff directory not found: $DiffRoot"
}
Copy-Item -Path (Join-Path $DiffRoot '*') -Destination $TargetRoot -Recurse -Force
$ManifestPath = 'signed-installer-diff/manifest.json'
if (Test-Path -LiteralPath $ManifestPath) {
$ManifestEntries = @(Get-Content -LiteralPath $ManifestPath | ConvertFrom-Json)
foreach ($Entry in $ManifestEntries) {
$TargetPath = Join-Path $TargetRoot $Entry.path
if (!(Test-Path -LiteralPath $TargetPath -PathType Leaf)) {
throw "Applied signed file not found: $($Entry.path)"
}
$TargetHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $TargetPath).Hash
if ($TargetHash -ne $Entry.sha256) {
throw "Signed file hash mismatch: $($Entry.path)"
}
}
}
- name: Save artifacts - name: Save artifacts
run: | run: |

View File

@@ -39,6 +39,7 @@ jobs:
run: yarn affine @affine/ios build run: yarn affine @affine/ios build
env: env:
PUBLIC_PATH: '/' PUBLIC_PATH: '/'
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine' SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -67,6 +68,7 @@ jobs:
run: yarn affine @affine/android build run: yarn affine @affine/android build
env: env:
PUBLIC_PATH: '/' PUBLIC_PATH: '/'
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine' SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -108,7 +110,7 @@ jobs:
enableScripts: false enableScripts: false
- uses: maxim-lobanov/setup-xcode@v1 - uses: maxim-lobanov/setup-xcode@v1
with: with:
xcode-version: 26.2 xcode-version: 16.4
- name: Install Swiftformat - name: Install Swiftformat
run: brew install swiftformat run: brew install swiftformat
- name: Cap sync - name: Cap sync

View File

@@ -148,7 +148,7 @@ jobs:
name: Wait for approval name: Wait for approval
with: with:
secret: ${{ secrets.GITHUB_TOKEN }} secret: ${{ secrets.GITHUB_TOKEN }}
approvers: darkskygit approvers: darkskygit,pengx17,L-Sun,EYHN
minimum-approvals: 1 minimum-approvals: 1
fail-on-denial: true fail-on-denial: true
issue-title: Please confirm to release docker image issue-title: Please confirm to release docker image

72
.github/workflows/sync-i18n.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Sync I18n with Crowdin
on:
push:
branches:
- canary
paths:
- 'packages/frontend/i18n/**'
workflow_dispatch:
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Crowdin action
id: crowdin
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: false
download_translations: true
auto_approve_imported: true
import_eq_suggestions: true
export_only_approved: true
skip_untranslated_strings: true
localization_branch_name: l10n_crowdin_translations
create_pull_request: true
pull_request_title: 'chore(i18n): sync translations'
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
pull_request_base_branch_name: 'canary'
config: packages/frontend/i18n/crowdin.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
i18n-codegen:
needs: synchronize-with-crowdin
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: l10n_crowdin_translations
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
full-cache: true
- name: Run i18n codegen
run: yarn affine @affine/i18n build
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "chore(i18n): i18n codegen"
git push origin l10n_crowdin_translations

View File

@@ -30,43 +30,13 @@ jobs:
run: | run: |
cd ${{ env.ARCHIVE_DIR }}/out 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.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
- name: collect signed file diff - name: zip file
shell: powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File {0} shell: cmd
run: | run: |
$OutDir = Join-Path '${{ env.ARCHIVE_DIR }}' 'out' cd ${{ env.ARCHIVE_DIR }}
$DiffDir = Join-Path '${{ env.ARCHIVE_DIR }}' 'signed-diff' 7za a signed.zip .\out\*
$FilesDir = Join-Path $DiffDir 'files'
New-Item -ItemType Directory -Path $FilesDir -Force | Out-Null
$SignedFiles = [regex]::Matches('${{ inputs.files }}', '"([^"]+)"') | ForEach-Object { $_.Groups[1].Value }
if ($SignedFiles.Count -eq 0) {
throw 'No files to sign were provided.'
}
$Manifest = @()
foreach ($RelativePath in $SignedFiles) {
$SourcePath = Join-Path $OutDir $RelativePath
if (!(Test-Path -LiteralPath $SourcePath -PathType Leaf)) {
throw "Signed file not found: $RelativePath"
}
$TargetPath = Join-Path $FilesDir $RelativePath
$TargetDir = Split-Path -Parent $TargetPath
if ($TargetDir) {
New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null
}
Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
$Manifest += [PSCustomObject]@{
path = $RelativePath
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $TargetPath).Hash
}
}
$Manifest | ConvertTo-Json -Depth 4 | Out-File -FilePath (Join-Path $DiffDir 'manifest.json') -Encoding utf8
Write-Host "Collected $($SignedFiles.Count) signed files."
- name: upload - name: upload
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: signed-${{ inputs.artifact-name }} name: signed-${{ inputs.artifact-name }}
path: ${{ env.ARCHIVE_DIR }}/signed-diff path: ${{ env.ARCHIVE_DIR }}/signed.zip

3
.gitignore vendored
View File

@@ -47,7 +47,8 @@ testem.log
.pnpm-debug.log .pnpm-debug.log
/typings /typings
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
.context rfc*.md
todo.md
# System Files # System Files
.DS_Store .DS_Store

2
.nvmrc
View File

@@ -1 +1 @@
22.22.0 22.21.1

View File

@@ -1,8 +1,4 @@
exclude = [ exclude = ["node_modules/**/*.toml", "target/**/*.toml"]
"node_modules/**/*.toml",
"target/**/*.toml",
"packages/frontend/apps/ios/App/Packages/AffineGraphQL/**/*.toml",
]
# https://taplo.tamasfe.dev/configuration/formatter-options.html # https://taplo.tamasfe.dev/configuration/formatter-options.html
[formatting] [formatting]

View File

@@ -1,5 +1,5 @@
{ {
"prisma.pinToPrisma6": true, "eslint.packageManager": "yarn",
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.formatOnSaveMode": "file", "editor.formatOnSaveMode": "file",
@@ -14,13 +14,11 @@
"testid", "testid",
"schemars" "schemars"
], ],
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": { "explorer.fileNesting.patterns": {
"*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map", "*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map",
"package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, eslint.*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, dangerfile*, dlint.json, dprint.json, firebase.json, grunt*, gulp*, histoire.config.*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, package.nls*.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, release-tasks.sh, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, vitest.*, webpack*, workspace.json, xo.config.*, yarn*, babel.*, .babelrc, project.json, .oxlintrc.json, oxlint.json, nyc.config.*", "package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, crowdin*, cypress.*, dangerfile*, dlint.json, dprint.json, firebase.json, grunt*, gulp*, histoire.config.*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, package.nls*.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, release-tasks.sh, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, vitest.config.*, webpack*, workspace.json, xo.config.*, yarn*, babel.*, .babelrc, project.json",
"Cargo.toml": "Cargo.lock, rust-toolchain*, rustfmt.toml, .taplo.toml", "Cargo.toml": "Cargo.lock",
"README.md": "LICENSE*, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, README.*", "README.md": "LICENSE, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md"
".gitignore": ".gitattributes, .dockerignore, .eslintignore, .prettierignore, .stylelintignore, .tslintignore, .yarnignore"
}, },
"[rust]": { "[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer" "editor.defaultFormatter": "rust-lang.rust-analyzer"
@@ -34,6 +32,5 @@
"vitest.include": ["packages/**/*.spec.ts", "packages/**/*.spec.tsx"], "vitest.include": ["packages/**/*.spec.ts", "packages/**/*.spec.tsx"],
"rust-analyzer.check.extraEnv": { "rust-analyzer.check.extraEnv": {
"DATABASE_URL": "sqlite:affine.db" "DATABASE_URL": "sqlite:affine.db"
}, }
"typescript.tsdk": "node_modules/typescript/lib"
} }

1544
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ members = [
"./packages/backend/native", "./packages/backend/native",
"./packages/common/native", "./packages/common/native",
"./packages/common/y-octo/core", "./packages/common/y-octo/core",
"./packages/common/y-octo/node",
"./packages/common/y-octo/utils", "./packages/common/y-octo/utils",
"./packages/frontend/mobile-native", "./packages/frontend/mobile-native",
"./packages/frontend/native", "./packages/frontend/native",
@@ -46,8 +47,7 @@ resolver = "3"
libc = "0.2" libc = "0.2"
log = "0.4" log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] } loom = { version = "0.7", features = ["checkpoint"] }
lru = "0.16" memory-indexer = "0.2.1"
memory-indexer = "0.3.0"
mimalloc = "0.1" mimalloc = "0.1"
mp4parse = "0.17" mp4parse = "0.17"
nanoid = "0.4" nanoid = "0.4"
@@ -72,7 +72,6 @@ resolver = "3"
phf = { version = "0.11", features = ["macros"] } phf = { version = "0.11", features = ["macros"] }
proptest = "1.3" proptest = "1.3"
proptest-derive = "0.5" proptest-derive = "0.5"
pulldown-cmark = "0.13"
rand = "0.9" rand = "0.9"
rand_chacha = "0.9" rand_chacha = "0.9"
rand_distr = "0.5" rand_distr = "0.5"

View File

@@ -21,6 +21,23 @@
<br/> <br/>
<br/> <br/>
<div align="left" valign="middle">
<a href="https://runblaze.dev">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.runblaze.dev/logo_dark.png">
<img align="right" src="https://www.runblaze.dev/logo_light.png" height="102px"/>
</picture>
</a>
<br style="display: none;"/>
_Special thanks to [Blaze](https://runblaze.dev) for their support of this project. They provide high-performance Apple Silicon macOS and Linux (AMD64 & ARM64) runners for GitHub Actions, greatly reducing our automated build times._
</div>
<br/>
<br/>
<div align="center"> <div align="center">
<a href="https://affine.pro">Home Page</a> | <a href="https://affine.pro">Home Page</a> |
<a href="https://affine.pro/redirect/discord">Discord</a> | <a href="https://affine.pro/redirect/discord">Discord</a> |
@@ -90,10 +107,10 @@ Thanks for checking us out, we appreciate your interest and sincerely hope that
## Contributing ## Contributing
| Bug Reports | Feature Requests | Questions/Discussions | AFFiNE Community | | Bug Reports | Feature Requests | Questions/Discussions | AFFiNE Community |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------- | | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------- |
| [Create a bug report](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=bug%2Cproduct-review&template=BUG-REPORT.yml&title=TITLE) | [Submit a feature request](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=feat%2Cproduct-review&template=FEATURE-REQUEST.yml&title=TITLE) | [Check GitHub Discussion](https://github.com/toeverything/AFFiNE/discussions) | [Visit the AFFiNE Community](https://community.affine.pro) | | [Create a bug report](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=bug%2Cproduct-review&template=BUG-REPORT.yml&title=TITLE) | [Submit a feature request](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=feat%2Cproduct-review&template=FEATURE-REQUEST.yml&title=TITLE) | [Check GitHub Discussion](https://github.com/toeverything/AFFiNE/discussions) | [Vist the AFFiNE Community](https://community.affine.pro) |
| Something isn't working as expected | An idea for a new feature, or improvements | Discuss and ask questions | A place to ask, learn and engage with others | | Something isn't working as expected | An idea for a new feature, or improvements | Discuss and ask questions | A place to ask, learn and engage with others |
Calling all developers, testers, tech writers and more! Contributions of all types are more than welcome, you can read more in [docs/types-of-contributions.md](docs/types-of-contributions.md). If you are interested in contributing code, read our [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) and feel free to check out our GitHub issues to get stuck in to show us what youre made of. Calling all developers, testers, tech writers and more! Contributions of all types are more than welcome, you can read more in [docs/types-of-contributions.md](docs/types-of-contributions.md). If you are interested in contributing code, read our [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) and feel free to check out our GitHub issues to get stuck in to show us what youre made of.
@@ -152,10 +169,8 @@ Welcome to the AFFiNE blog section! Here, youll find the latest insights, tip
We would also like to give thanks to open-source projects that make AFFiNE possible: We would also like to give thanks to open-source projects that make AFFiNE possible:
- [Blocksuite](https://github.com/toeverything/BlockSuite) - 💠 BlockSuite is the open-source collaborative editor project behind AFFiNE. - [Blocksuite](https://github.com/toeverything/BlockSuite) - 💠 BlockSuite is the open-source collaborative editor project behind AFFiNE.
- [y-octo](https://github.com/y-crdt/y-octo) - 🐙 y-octo is a native, high-performance, thread-safe YJS CRDT implementation, serving as the core engine enabling the AFFiNE Client/Server to achieve "local-first" functionality.
- [OctoBase](https://github.com/toeverything/OctoBase) - 🐙 OctoBase is the open-source database behind AFFiNE, local-first, yet collaborative. A light-weight, scalable, data engine written in Rust. - [OctoBase](https://github.com/toeverything/OctoBase) - 🐙 OctoBase is the open-source database behind AFFiNE, local-first, yet collaborative. A light-weight, scalable, data engine written in Rust.
- [yjs](https://github.com/yjs/yjs) - Fundamental support of CRDTs for our implementation on state management and data sync.
- [yjs](https://github.com/yjs/yjs) - Fundamental support of CRDTs for our implementation on state management and data sync on web.
- [electron](https://github.com/electron/electron) - Build cross-platform desktop apps with JavaScript, HTML, and CSS. - [electron](https://github.com/electron/electron) - Build cross-platform desktop apps with JavaScript, HTML, and CSS.
- [React](https://github.com/facebook/react) - The library for web and native user interfaces. - [React](https://github.com/facebook/react) - The library for web and native user interfaces.
- [napi-rs](https://github.com/napi-rs/napi-rs) - A framework for building compiled Node.js add-ons in Rust via Node-API. - [napi-rs](https://github.com/napi-rs/napi-rs) - A framework for building compiled Node.js add-ons in Rust via Node-API.
@@ -206,6 +221,12 @@ See [BUILDING.md] for instructions on how to build AFFiNE from source code.
We welcome contributions from everyone. We welcome contributions from everyone.
See [docs/contributing/tutorial.md](./docs/contributing/tutorial.md) for details. See [docs/contributing/tutorial.md](./docs/contributing/tutorial.md) for details.
## Thanks
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" width="153" height="30" alt="Chromatic" /></a>
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
## License ## License
### Editions ### Editions

View File

@@ -6,8 +6,8 @@ We recommend users to always use the latest major version. Security updates will
| Version | Supported | | Version | Supported |
| --------------- | ------------------ | | --------------- | ------------------ |
| 0.26.x (stable) | :white_check_mark: | | 0.25.x (stable) | :white_check_mark: |
| < 0.26.x | :x: | | < 0.25.x | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@@ -296,7 +296,7 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1", "version": "0.25.7",
"devDependencies": { "devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0", "@vanilla-extract/vite-plugin": "^5.0.0",
"msw": "^2.12.4", "msw": "^2.12.4",

View File

@@ -2101,157 +2101,6 @@ describe('html to snapshot', () => {
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot); expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
}); });
test('paragraph with br should split into multiple blocks', async () => {
const html = template(`<p>aaa<br>bbb<br>ccc</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: [{ insert: 'aaa' }],
},
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[2]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [{ insert: 'bbb' }],
},
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[3]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [{ insert: 'ccc' }],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('paragraph with br should keep inline styles in each split line', async () => {
const html = template(
`<p><strong>aaa</strong><br><a href="https://www.google.com/">bbb</a><br><em>ccc</em></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: [
{
insert: 'aaa',
attributes: {
bold: true,
},
},
],
},
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[2]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'bbb',
attributes: {
link: 'https://www.google.com/',
},
},
],
},
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[3]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'ccc',
attributes: {
italic: true,
},
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('nested list', async () => { test('nested list', async () => {
const html = template(`<ul><li>111<ul><li>222</li></ul></li></ul>`); const html = template(`<ul><li>111<ul><li>222</li></ul></li></ul>`);

View File

@@ -1,95 +0,0 @@
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { describe, expect, test } from 'vitest';
import { insertUrlTextSegments } from '../../../../blocks/database/src/properties/paste-url.js';
type InsertCall = {
range: {
index: number;
length: number;
};
text: string;
attributes?: AffineTextAttributes;
};
describe('insertUrlTextSegments', () => {
test('should replace selected text on first insert and append remaining segments', () => {
const insertCalls: InsertCall[] = [];
const selectionCalls: Array<{ index: number; length: number } | null> = [];
const inlineEditor = {
insertText: (
range: { index: number; length: number },
text: string,
attributes?: AffineTextAttributes
) => {
insertCalls.push({ range, text, attributes });
},
setInlineRange: (range: { index: number; length: number } | null) => {
selectionCalls.push(range);
},
};
const inlineRange = { index: 4, length: 6 };
const segments = [
{ text: 'hi - ' },
{ text: 'https://google.com', link: 'https://google.com' },
];
insertUrlTextSegments(inlineEditor, inlineRange, segments);
expect(insertCalls).toEqual([
{
range: { index: 4, length: 6 },
text: 'hi - ',
},
{
range: { index: 9, length: 0 },
text: 'https://google.com',
attributes: {
link: 'https://google.com',
},
},
]);
expect(selectionCalls).toEqual([{ index: 27, length: 0 }]);
});
test('should keep insertion range length zero when there is no selected text', () => {
const insertCalls: InsertCall[] = [];
const selectionCalls: Array<{ index: number; length: number } | null> = [];
const inlineEditor = {
insertText: (
range: { index: number; length: number },
text: string,
attributes?: AffineTextAttributes
) => {
insertCalls.push({ range, text, attributes });
},
setInlineRange: (range: { index: number; length: number } | null) => {
selectionCalls.push(range);
},
};
const inlineRange = { index: 2, length: 0 };
const segments = [
{ text: 'prefix ' },
{ text: 'https://a.com', link: 'https://a.com' },
];
insertUrlTextSegments(inlineEditor, inlineRange, segments);
expect(insertCalls).toEqual([
{
range: { index: 2, length: 0 },
text: 'prefix ',
},
{
range: { index: 9, length: 0 },
text: 'https://a.com',
attributes: {
link: 'https://a.com',
},
},
]);
expect(selectionCalls).toEqual([{ index: 22, length: 0 }]);
});
});

View File

@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"file-type": "^21.0.0", "file-type": "^21.0.0",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
@@ -41,5 +41,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -24,7 +24,7 @@
"@blocksuite/store": "workspace:*", "@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
@@ -45,5 +45,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10", "@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"emoji-mart": "^5.6.0", "emoji-mart": "^5.6.0",
"lit": "^3.2.0", "lit": "^3.2.0",
@@ -45,5 +45,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -28,7 +28,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
@@ -48,5 +48,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
@@ -42,5 +42,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -28,7 +28,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"date-fns": "^4.0.0", "date-fns": "^4.0.0",
"lit": "^3.2.0", "lit": "^3.2.0",
@@ -48,5 +48,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -135,10 +135,14 @@ export class DatabaseBlockDataSource extends DataSourceBase {
override featureFlags$: ReadonlySignal<DatabaseFlags> = computed(() => { override featureFlags$: ReadonlySignal<DatabaseFlags> = computed(() => {
const featureFlagService = this.doc.get(FeatureFlagService); const featureFlagService = this.doc.get(FeatureFlagService);
const enableNumberFormat = featureFlagService.getFlag(
'enable_database_number_formatting'
);
const enableTableVirtualScroll = featureFlagService.getFlag( const enableTableVirtualScroll = featureFlagService.getFlag(
'enable_table_virtual_scroll' 'enable_table_virtual_scroll'
); );
return { return {
enable_number_formatting: enableNumberFormat ?? false,
enable_table_virtual_scroll: enableTableVirtualScroll ?? false, enable_table_virtual_scroll: enableTableVirtualScroll ?? false,
}; };
}); });

View File

@@ -1,56 +0,0 @@
import type {
AffineInlineEditor,
AffineTextAttributes,
} from '@blocksuite/affine-shared/types';
import {
splitTextByUrl,
type UrlTextSegment,
} from '@blocksuite/affine-shared/utils';
import type { InlineRange } from '@blocksuite/std/inline';
type UrlPasteInlineEditor = Pick<
AffineInlineEditor,
'insertText' | 'setInlineRange'
>;
export function analyzeTextForUrlPaste(text: string) {
const segments = splitTextByUrl(text);
const firstSegment = segments[0];
const singleUrl =
segments.length === 1 && firstSegment?.link && firstSegment.text === text
? firstSegment.link
: undefined;
return {
segments,
singleUrl,
};
}
export function insertUrlTextSegments(
inlineEditor: UrlPasteInlineEditor,
inlineRange: InlineRange,
segments: UrlTextSegment[]
) {
let index = inlineRange.index;
let replacedSelection = false;
segments.forEach(segment => {
if (!segment.text) return;
const attributes: AffineTextAttributes | undefined = segment.link
? { link: segment.link }
: undefined;
inlineEditor.insertText(
{
index,
length: replacedSelection ? 0 : inlineRange.length,
},
segment.text,
attributes
);
replacedSelection = true;
index += segment.text.length;
});
inlineEditor.setInlineRange({
index,
length: 0,
});
}

View File

@@ -8,7 +8,10 @@ import type {
AffineInlineEditor, AffineInlineEditor,
AffineTextAttributes, AffineTextAttributes,
} from '@blocksuite/affine-shared/types'; } from '@blocksuite/affine-shared/types';
import { getViewportElement } from '@blocksuite/affine-shared/utils'; import {
getViewportElement,
isValidUrl,
} from '@blocksuite/affine-shared/utils';
import { import {
BaseCellRenderer, BaseCellRenderer,
createFromBaseCellRenderer, createFromBaseCellRenderer,
@@ -23,7 +26,6 @@ import { html } from 'lit/static-html.js';
import { EditorHostKey } from '../../context/host-context.js'; import { EditorHostKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js'; import type { DatabaseBlockComponent } from '../../database-block.js';
import { analyzeTextForUrlPaste, insertUrlTextSegments } from '../paste-url.js';
import { import {
richTextCellStyle, richTextCellStyle,
richTextContainerStyle, richTextContainerStyle,
@@ -269,13 +271,10 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
?.getData('text/plain') ?.getData('text/plain')
?.replace(/\r?\n|\r/g, '\n'); ?.replace(/\r?\n|\r/g, '\n');
if (!text) return; if (!text) return;
const { segments, singleUrl } = analyzeTextForUrlPaste(text);
if (singleUrl) { if (isValidUrl(text)) {
const std = this.std; const std = this.std;
const result = std const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text);
?.getOptional(ParseDocUrlProvider)
?.parseDocUrl(singleUrl);
if (result) { if (result) {
const text = ' '; const text = ' ';
inlineEditor.insertText(inlineRange, text, { inlineEditor.insertText(inlineRange, text, {
@@ -301,10 +300,22 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
segment: 'database', segment: 'database',
parentFlavour: 'affine:database', parentFlavour: 'affine:database',
}); });
return; } else {
inlineEditor.insertText(inlineRange, text, {
link: text,
});
inlineEditor.setInlineRange({
index: inlineRange.index + text.length,
length: 0,
});
} }
} else {
inlineEditor.insertText(inlineRange, text);
inlineEditor.setInlineRange({
index: inlineRange.index + text.length,
length: 0,
});
} }
insertUrlTextSegments(inlineEditor, inlineRange, segments);
}; };
override connectedCallback() { override connectedCallback() {

View File

@@ -4,7 +4,10 @@ import {
ParseDocUrlProvider, ParseDocUrlProvider,
TelemetryProvider, TelemetryProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { getViewportElement } from '@blocksuite/affine-shared/utils'; import {
getViewportElement,
isValidUrl,
} from '@blocksuite/affine-shared/utils';
import { BaseCellRenderer } from '@blocksuite/data-view'; import { BaseCellRenderer } from '@blocksuite/data-view';
import { IS_MAC } from '@blocksuite/global/env'; import { IS_MAC } from '@blocksuite/global/env';
import { LinkedPageIcon } from '@blocksuite/icons/lit'; import { LinkedPageIcon } from '@blocksuite/icons/lit';
@@ -17,7 +20,6 @@ import { html } from 'lit/static-html.js';
import { EditorHostKey } from '../../context/host-context.js'; import { EditorHostKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js'; import type { DatabaseBlockComponent } from '../../database-block.js';
import { getSingleDocIdFromText } from '../../utils/title-doc.js'; import { getSingleDocIdFromText } from '../../utils/title-doc.js';
import { analyzeTextForUrlPaste, insertUrlTextSegments } from '../paste-url.js';
import { import {
headerAreaIconStyle, headerAreaIconStyle,
titleCellStyle, titleCellStyle,
@@ -93,9 +95,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
private readonly _onPaste = (e: ClipboardEvent) => { private readonly _onPaste = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor; const inlineEditor = this.inlineEditor;
const inlineRange = inlineEditor?.getInlineRange(); const inlineRange = inlineEditor?.getInlineRange();
if (!inlineEditor || !inlineRange) return; if (!inlineRange) return;
e.preventDefault();
e.stopPropagation();
if (e.clipboardData) { if (e.clipboardData) {
try { try {
const getDeltas = (snapshot: BlockSnapshot): DeltaInsert[] => { const getDeltas = (snapshot: BlockSnapshot): DeltaInsert[] => {
@@ -121,15 +121,14 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
?.getData('text/plain') ?.getData('text/plain')
?.replace(/\r?\n|\r/g, '\n'); ?.replace(/\r?\n|\r/g, '\n');
if (!text) return; if (!text) return;
const { segments, singleUrl } = analyzeTextForUrlPaste(text); e.preventDefault();
if (singleUrl) { e.stopPropagation();
if (isValidUrl(text)) {
const std = this.std; const std = this.std;
const result = std const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text);
?.getOptional(ParseDocUrlProvider)
?.parseDocUrl(singleUrl);
if (result) { if (result) {
const text = ' '; const text = ' ';
inlineEditor.insertText(inlineRange, text, { inlineEditor?.insertText(inlineRange, text, {
reference: { reference: {
type: 'LinkedPage', type: 'LinkedPage',
pageId: result.docId, pageId: result.docId,
@@ -140,7 +139,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
}, },
}, },
}); });
inlineEditor.setInlineRange({ inlineEditor?.setInlineRange({
index: inlineRange.index + text.length, index: inlineRange.index + text.length,
length: 0, length: 0,
}); });
@@ -152,10 +151,22 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
segment: 'database', segment: 'database',
parentFlavour: 'affine:database', parentFlavour: 'affine:database',
}); });
return; } else {
inlineEditor?.insertText(inlineRange, text, {
link: text,
});
inlineEditor?.setInlineRange({
index: inlineRange.index + text.length,
length: 0,
});
} }
} else {
inlineEditor?.insertText(inlineRange, text);
inlineEditor?.setInlineRange({
index: inlineRange.index + text.length,
length: 0,
});
} }
insertUrlTextSegments(inlineEditor, inlineRange, segments);
}; };
insertDelta = (delta: DeltaInsert) => { insertDelta = (delta: DeltaInsert) => {
@@ -229,8 +240,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
this.disposables.addFromEvent( this.disposables.addFromEvent(
this.richText.value, this.richText.value,
'paste', 'paste',
this._onPaste, this._onPaste
true
); );
const inlineEditor = this.inlineEditor; const inlineEditor = this.inlineEditor;
if (inlineEditor) { if (inlineEditor) {

View File

@@ -21,7 +21,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
@@ -39,5 +39,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
@@ -43,5 +43,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -26,10 +26,10 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"lit": "^3.2.0", "lit": "^3.2.0",
"lodash-es": "^4.17.23", "lodash-es": "^4.17.21",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"yjs": "^13.6.27", "yjs": "^13.6.27",
@@ -49,5 +49,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -26,10 +26,10 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"lit": "^3.2.0", "lit": "^3.2.0",
"lodash-es": "^4.17.23", "lodash-es": "^4.17.21",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"yjs": "^13.6.27", "yjs": "^13.6.27",
@@ -49,5 +49,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -10,6 +10,7 @@ const GENERIC_DEFAULT_HEIGHT_IN_NOTE = 400;
* These are based on the centralized cloud constants and known AFFiNE domains * These are based on the centralized cloud constants and known AFFiNE domains
*/ */
const AFFINE_DOMAINS = [ const AFFINE_DOMAINS = [
'affine.pro', // Main AFFiNE domain
'app.affine.pro', // Stable cloud domain 'app.affine.pro', // Stable cloud domain
'insider.affine.pro', // Beta/internal cloud domain 'insider.affine.pro', // Beta/internal cloud domain
'affine.fail', // Canary cloud domain 'affine.fail', // Canary cloud domain

View File

@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
@@ -44,5 +44,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"file-type": "^21.0.0", "file-type": "^21.0.0",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
@@ -44,5 +44,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -26,11 +26,6 @@ import {
@Peekable() @Peekable()
export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockModel> { export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockModel> {
private static readonly LOD_MIN_IMAGE_BYTES = 1024 * 1024;
private static readonly LOD_MIN_IMAGE_PIXELS = 1920 * 1080;
private static readonly LOD_MAX_ZOOM = 0.4;
private static readonly LOD_THUMBNAIL_MAX_EDGE = 256;
static override styles = css` static override styles = css`
affine-edgeless-image { affine-edgeless-image {
position: relative; position: relative;
@@ -68,11 +63,6 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
affine-edgeless-image .resizable-img {
position: relative;
overflow: hidden;
}
`; `;
resourceController = new ResourceController( resourceController = new ResourceController(
@@ -80,12 +70,6 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
'Image' 'Image'
); );
private _lodThumbnailUrl: string | null = null;
private _lodSourceUrl: string | null = null;
private _lodGeneratingSourceUrl: string | null = null;
private _lodGenerationToken = 0;
private _lastShouldUseLod = false;
get blobUrl() { get blobUrl() {
return this.resourceController.blobUrl$.value; return this.resourceController.blobUrl$.value;
} }
@@ -112,134 +96,6 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
}); });
} }
private _isLargeImage() {
const { width = 0, height = 0, size = 0 } = this.model.props;
const pixels = width * height;
return (
size >= ImageEdgelessBlockComponent.LOD_MIN_IMAGE_BYTES ||
pixels >= ImageEdgelessBlockComponent.LOD_MIN_IMAGE_PIXELS
);
}
private _shouldUseLod(blobUrl: string | null, zoom = this.gfx.viewport.zoom) {
return (
Boolean(blobUrl) &&
this._isLargeImage() &&
zoom <= ImageEdgelessBlockComponent.LOD_MAX_ZOOM
);
}
private _revokeLodThumbnail() {
if (!this._lodThumbnailUrl) {
return;
}
URL.revokeObjectURL(this._lodThumbnailUrl);
this._lodThumbnailUrl = null;
}
private _resetLodSource(blobUrl: string | null) {
if (this._lodSourceUrl === blobUrl) {
return;
}
this._lodGenerationToken += 1;
this._lodGeneratingSourceUrl = null;
this._lodSourceUrl = blobUrl;
this._revokeLodThumbnail();
}
private _createImageElement(src: string) {
return new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image();
image.decoding = 'async';
image.onload = () => resolve(image);
image.onerror = () => reject(new Error('Failed to load image'));
image.src = src;
});
}
private _createThumbnailBlob(image: HTMLImageElement) {
const maxEdge = ImageEdgelessBlockComponent.LOD_THUMBNAIL_MAX_EDGE;
const longestEdge = Math.max(image.naturalWidth, image.naturalHeight);
const scale = longestEdge > maxEdge ? maxEdge / longestEdge : 1;
const targetWidth = Math.max(1, Math.round(image.naturalWidth * scale));
const targetHeight = Math.max(1, Math.round(image.naturalHeight * scale));
const canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext('2d');
if (!ctx) {
return Promise.resolve<Blob | null>(null);
}
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'low';
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
return new Promise<Blob | null>(resolve => {
canvas.toBlob(resolve);
});
}
private _ensureLodThumbnail(blobUrl: string) {
if (
this._lodThumbnailUrl ||
this._lodGeneratingSourceUrl === blobUrl ||
!this._shouldUseLod(blobUrl)
) {
return;
}
const token = ++this._lodGenerationToken;
this._lodGeneratingSourceUrl = blobUrl;
void this._createImageElement(blobUrl)
.then(image => this._createThumbnailBlob(image))
.then(blob => {
if (!blob || token !== this._lodGenerationToken || !this.isConnected) {
return;
}
const thumbnailUrl = URL.createObjectURL(blob);
if (token !== this._lodGenerationToken || !this.isConnected) {
URL.revokeObjectURL(thumbnailUrl);
return;
}
this._revokeLodThumbnail();
this._lodThumbnailUrl = thumbnailUrl;
if (this._shouldUseLod(this.blobUrl)) {
this.requestUpdate();
}
})
.catch(err => {
if (token !== this._lodGenerationToken || !this.isConnected) {
return;
}
console.error(err);
})
.finally(() => {
if (token === this._lodGenerationToken) {
this._lodGeneratingSourceUrl = null;
}
});
}
private _updateLodFromViewport(zoom: number) {
const shouldUseLod = this._shouldUseLod(this.blobUrl, zoom);
if (shouldUseLod === this._lastShouldUseLod) {
return;
}
this._lastShouldUseLod = shouldUseLod;
if (shouldUseLod && this.blobUrl) {
this._ensureLodThumbnail(this.blobUrl);
}
this.requestUpdate();
}
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
@@ -252,32 +108,14 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
this.disposables.add( this.disposables.add(
this.model.props.sourceId$.subscribe(() => { this.model.props.sourceId$.subscribe(() => {
this._resetLodSource(null);
this.refreshData(); this.refreshData();
}) })
); );
this.disposables.add(
this.gfx.viewport.viewportUpdated.subscribe(({ zoom }) => {
this._updateLodFromViewport(zoom);
})
);
this._lastShouldUseLod = this._shouldUseLod(this.blobUrl);
}
override disconnectedCallback() {
this._lodGenerationToken += 1;
this._lodGeneratingSourceUrl = null;
this._lodSourceUrl = null;
this._revokeLodThumbnail();
super.disconnectedCallback();
} }
override renderGfxBlock() { override renderGfxBlock() {
const blobUrl = this.blobUrl; const blobUrl = this.blobUrl;
const { rotate = 0, size = 0, caption = 'Image' } = this.model.props; const { rotate = 0, size = 0, caption = 'Image' } = this.model.props;
this._resetLodSource(blobUrl);
const containerStyleMap = styleMap({ const containerStyleMap = styleMap({
display: 'flex', display: 'flex',
@@ -300,13 +138,6 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
}); });
const { loading, icon, description, error, needUpload } = resovledState; const { loading, icon, description, error, needUpload } = resovledState;
const shouldUseLod = this._shouldUseLod(blobUrl);
if (shouldUseLod && blobUrl) {
this._ensureLodThumbnail(blobUrl);
}
this._lastShouldUseLod = shouldUseLod;
const imageUrl =
shouldUseLod && this._lodThumbnailUrl ? this._lodThumbnailUrl : blobUrl;
return html` return html`
<div class="affine-image-container" style=${containerStyleMap}> <div class="affine-image-container" style=${containerStyleMap}>
@@ -318,7 +149,7 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
class="drag-target" class="drag-target"
draggable="false" draggable="false"
loading="lazy" loading="lazy"
src=${imageUrl ?? ''} src=${blobUrl}
alt=${caption} alt=${caption}
@error=${this._handleError} @error=${this._handleError}
/> />

View File

@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/katex": "^0.16.7", "@types/katex": "^0.16.7",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"katex": "^0.16.27", "katex": "^0.16.27",
@@ -46,5 +46,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
@@ -46,5 +46,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -27,12 +27,12 @@
"@blocksuite/store": "workspace:*", "@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"@vanilla-extract/css": "^1.17.0", "@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0", "lit": "^3.2.0",
"lodash-es": "^4.17.23", "lodash-es": "^4.17.21",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"zod": "^3.25.76" "zod": "^3.25.76"
@@ -49,5 +49,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
@@ -42,5 +42,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -37,126 +37,6 @@ const tagsInAncestor = (o: NodeProps<HtmlAST>, tagNames: Array<string>) => {
return false; return false;
}; };
const splitDeltaByNewline = (delta: DeltaInsert[]) => {
const lines: DeltaInsert[][] = [[]];
const pending = [...delta];
while (pending.length > 0) {
const op = pending.shift();
if (!op) continue;
const insert = op.insert;
if (typeof insert !== 'string') {
lines[lines.length - 1].push(op);
continue;
}
if (!insert.includes('\n')) {
if (insert.length === 0) {
continue;
}
lines[lines.length - 1].push(op);
continue;
}
const splitIndex = insert.indexOf('\n');
const linePart = insert.slice(0, splitIndex);
const remainPart = insert.slice(splitIndex + 1);
if (linePart.length > 0) {
lines[lines.length - 1].push({ ...op, insert: linePart });
}
lines.push([]);
if (remainPart) {
pending.unshift({ ...op, insert: remainPart });
}
}
return lines;
};
const hasBlockElementDescendant = (node: HtmlAST): boolean => {
if (!HastUtils.isElement(node)) {
return false;
}
return node.children.some(child => {
if (!HastUtils.isElement(child)) {
return false;
}
return (
(HastUtils.isTagBlock(child.tagName) && child.tagName !== 'br') ||
hasBlockElementDescendant(child)
);
});
};
const getParagraphDeltas = (
node: HtmlAST,
delta: DeltaInsert[]
): DeltaInsert[][] => {
if (!HastUtils.isElement(node)) return [delta];
if (hasBlockElementDescendant(node)) return [delta];
const hasBr = !!HastUtils.querySelector(node, 'br');
if (!hasBr) return [delta];
const hasNewline = delta.some(
op => typeof op.insert === 'string' && op.insert.includes('\n')
);
if (!hasNewline) return [delta];
return splitDeltaByNewline(delta);
};
const openParagraphBlocks = (
deltas: DeltaInsert[][],
type: string,
// AST walker context from html adapter transform pipeline.
walkerContext: any
) => {
for (const delta of deltas) {
walkerContext
.openNode(
{
type: 'block',
id: nanoid(),
flavour: 'affine:paragraph',
props: { type, text: { '$blocksuite:internal:text$': true, delta } },
children: [],
},
'children'
)
.closeNode();
}
};
const MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY =
'affine:paragraph:multi-emitted-nodes';
const markMultiParagraphEmitted = (walkerContext: any, node: HtmlAST) => {
const emittedNodes =
(walkerContext.getGlobalContext(
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY
) as WeakSet<object> | undefined) ?? new WeakSet<object>();
emittedNodes.add(node as object);
walkerContext.setGlobalContext(
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY,
emittedNodes
);
};
const consumeMultiParagraphEmittedMark = (
walkerContext: any,
node: HtmlAST
) => {
const emittedNodes = walkerContext.getGlobalContext(
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY
) as WeakSet<object> | undefined;
if (!emittedNodes) {
return false;
}
return emittedNodes.delete(node as object);
};
export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = { export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
flavour: ParagraphBlockSchema.model.flavour, flavour: ParagraphBlockSchema.model.flavour,
toMatch: o => toMatch: o =>
@@ -208,37 +88,41 @@ export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
!tagsInAncestor(o, ['p', 'li']) && !tagsInAncestor(o, ['p', 'li']) &&
HastUtils.isParagraphLike(o.node) HastUtils.isParagraphLike(o.node)
) { ) {
const delta = deltaConverter.astToDelta(o.node); walkerContext
const deltas = getParagraphDeltas(o.node, delta); .openNode(
openParagraphBlocks(deltas, 'text', walkerContext); {
type: 'block',
id: nanoid(),
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(o.node),
},
},
children: [],
},
'children'
)
.closeNode();
walkerContext.skipAllChildren(); walkerContext.skipAllChildren();
} }
break; break;
} }
case 'p': { case 'p': {
const type = walkerContext.getGlobalContext('hast:blockquote')
? 'quote'
: 'text';
const delta = deltaConverter.astToDelta(o.node);
const deltas = getParagraphDeltas(o.node, delta);
if (deltas.length > 1) {
openParagraphBlocks(deltas, type, walkerContext);
markMultiParagraphEmitted(walkerContext, o.node);
walkerContext.skipAllChildren();
break;
}
walkerContext.openNode( walkerContext.openNode(
{ {
type: 'block', type: 'block',
id: nanoid(), id: nanoid(),
flavour: 'affine:paragraph', flavour: 'affine:paragraph',
props: { props: {
type, type: walkerContext.getGlobalContext('hast:blockquote')
? 'quote'
: 'text',
text: { text: {
'$blocksuite:internal:text$': true, '$blocksuite:internal:text$': true,
delta, delta: deltaConverter.astToDelta(o.node),
}, },
}, },
children: [], children: [],
@@ -308,9 +192,6 @@ export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
break; break;
} }
case 'p': { case 'p': {
if (consumeMultiParagraphEmittedMark(walkerContext, o.node)) {
break;
}
if ( if (
o.next?.type === 'element' && o.next?.type === 'element' &&
o.next.tagName === 'div' && o.next.tagName === 'div' &&

View File

@@ -44,12 +44,12 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23", "@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"dompurify": "^3.3.0", "dompurify": "^3.3.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"lit": "^3.2.0", "lit": "^3.2.0",
"lodash-es": "^4.17.23", "lodash-es": "^4.17.21",
"minimatch": "^10.1.1", "minimatch": "^10.1.1",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"yjs": "^13.6.27", "yjs": "^13.6.27",
@@ -67,5 +67,5 @@
"!src/__tests__", "!src/__tests__",
"!dist/__tests__" "!dist/__tests__"
], ],
"version": "0.26.1" "version": "0.25.7"
} }

View File

@@ -86,7 +86,6 @@ export class PageClipboard extends ReadOnlyClipboard {
if (this.std.store.readonly) return; if (this.std.store.readonly) return;
this.std.store.captureSync(); this.std.store.captureSync();
let hasPasteTarget = false;
this.std.command this.std.command
.chain() .chain()
.try<{}>(cmd => [ .try<{}>(cmd => [
@@ -145,39 +144,18 @@ export class PageClipboard extends ReadOnlyClipboard {
if (!ctx.parentBlock) { if (!ctx.parentBlock) {
return; return;
} }
hasPasteTarget = true;
this.std.clipboard this.std.clipboard
.paste( .paste(
e, e,
this.std.store, this.std.store,
ctx.parentBlock.model.id, ctx.parentBlock.model.id,
ctx.blockIndex !== undefined ? ctx.blockIndex + 1 : 1 ctx.blockIndex ? ctx.blockIndex + 1 : 1
) )
.catch(console.error); .catch(console.error);
return next(); return next();
}) })
.run(); .run();
if (hasPasteTarget) return;
// If no valid selection target exists (for example, stale block selection
// right after cut), create/focus the default paragraph and paste after it.
const firstParagraphId = document
.querySelector('affine-page-root')
?.focusFirstParagraph?.()?.id;
const parentModel = firstParagraphId
? this.std.store.getParent(firstParagraphId)
: null;
const paragraphIndex =
firstParagraphId && parentModel
? parentModel.children.findIndex(child => child.id === firstParagraphId)
: -1;
const insertIndex = paragraphIndex >= 0 ? paragraphIndex + 1 : undefined;
this.std.clipboard
.paste(e, this.std.store, parentModel?.id, insertIndex)
.catch(console.error);
}; };
override mounted() { override mounted() {

View File

@@ -33,11 +33,7 @@ import {
ReleaseFromGroupIcon, ReleaseFromGroupIcon,
UnlockIcon, UnlockIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { import type { GfxModel } from '@blocksuite/std/gfx';
batchAddChildren,
batchRemoveChildren,
type GfxModel,
} from '@blocksuite/std/gfx';
import { html } from 'lit'; import { html } from 'lit';
import { renderAlignmentMenu } from './alignment'; import { renderAlignmentMenu } from './alignment';
@@ -65,13 +61,14 @@ export const builtinMiscToolbarConfig = {
const group = firstModel.group; const group = firstModel.group;
batchRemoveChildren(group, [firstModel]); // oxlint-disable-next-line unicorn/prefer-dom-node-remove
group.removeChild(firstModel);
firstModel.index = ctx.gfx.layer.generateIndex(); firstModel.index = ctx.gfx.layer.generateIndex();
const parent = group.group; const parent = group.group;
if (parent && parent instanceof GroupElementModel) { if (parent && parent instanceof GroupElementModel) {
batchAddChildren(parent, [firstModel]); parent.addChild(firstModel);
} }
}, },
}, },
@@ -258,12 +255,9 @@ export const builtinMiscToolbarConfig = {
// release other elements from their groups and group with top element // release other elements from their groups and group with top element
otherElements.forEach(element => { otherElements.forEach(element => {
if (element.group) { // oxlint-disable-next-line unicorn/prefer-dom-node-remove
batchRemoveChildren(element.group, [element]); element.group?.removeChild(element);
} topElement.group?.addChild(element);
if (topElement.group) {
batchAddChildren(topElement.group, [element]);
}
}); });
if (otherElements.length === 0) { if (otherElements.length === 0) {

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