Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/docker-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: 22.18.0
node-version: 22.22.3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [22.18.0]
node-version: [22.22.3]
runtime:
- mode: v1
force-v2-all: ''
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/linting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:

strategy:
matrix:
node-version: [22.18.0]
node-version: [22.22.3]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/manual-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: 22.18.0
node-version: 22.22.3
- name: ⚙️ Install zx
run: npm install -g zx

Expand Down
205 changes: 3 additions & 202 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ on:
description: 'Final English release note in Markdown'
required: true
type: string
media_sources:
description: 'JSON map from original media URL to downloadable source metadata'
required: false
default: '{}'
type: string
publish_latest:
description: 'Whether GitHub should mark this release as latest'
required: false
Expand All @@ -36,20 +31,13 @@ jobs:
ref: develop
fetch-depth: 0

- name: Install FFmpeg when videos are present
if: contains(inputs.release_note, '.mp4') || contains(inputs.release_note, '.webm') || contains(inputs.release_note, '.mov') || contains(inputs.release_note, '.m4v') || contains(inputs.release_note, '.ogg')
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg

- name: Build release body
env:
RELEASE_NOTE: ${{ inputs.release_note }}
run: |
printf '%s\n' "$RELEASE_NOTE" > release-body.md

- name: Create or update GitHub release
id: release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_ID: ${{ inputs.release_id }}
Expand All @@ -73,7 +61,7 @@ jobs:
}

upsert_release() {
local release_body release_payload response_file release_api_id release_url
local release_body release_payload response_file release_api_id
response_file="$(mktemp)"
release_body="$(cat release-body.md)"
release_payload="$(jq -nc \
Expand Down Expand Up @@ -103,200 +91,13 @@ jobs:
--input - <<<"${release_payload}" >"${response_file}"
fi

release_api_id="$(jq -r '.id' "${response_file}")"
release_url="$(jq -r '.html_url // empty' "${response_file}")"
if [ -z "${release_url}" ] || [ -z "${release_api_id}" ]; then
echo "Missing release id/html_url in GitHub release response" >&2
if [ -z "${release_url}" ]; then
echo "Missing html_url in GitHub release response" >&2
cat "${response_file}" >&2
return 1
fi

echo "release_api_id=${release_api_id}" >> "${GITHUB_OUTPUT}"
echo "release_url=${release_url}" >> "${GITHUB_OUTPUT}"
}

ensure_tag
upsert_release

- name: Render media links and upload GIF previews
env:
GH_TOKEN: ${{ github.token }}
RELEASE_ID: ${{ inputs.release_id }}
RELEASE_API_ID: ${{ steps.release.outputs.release_api_id }}
MEDIA_SOURCES: ${{ inputs.media_sources }}
run: |
node <<'NODE'
const { execFileSync } = require('node:child_process');
const crypto = require('node:crypto');
const fs = require('node:fs/promises');
const path = require('node:path');
const os = require('node:os');

const repo = process.env.GITHUB_REPOSITORY;
const token = process.env.GH_TOKEN;
const releaseApiId = process.env.RELEASE_API_ID;
const mediaSources = (() => {
try { return JSON.parse(process.env.MEDIA_SOURCES || '{}') || {}; }
catch { return {}; }
})();

function getMediaType(url) {
const cleanUrl = String(url).split(/[?#]/)[0].toLowerCase();
if (/^https:\/\/github\.com\/user-attachments\/assets\//.test(cleanUrl)) return 'githubAttachment';
if (/^https:\/\/github\.com\/[^/]+\/[^/]+\/assets\/[^/]+\/[^/?#]+/.test(cleanUrl)) return 'githubAttachment';
if (/\.(png|jpe?g|gif|webp|avif|svg)$/.test(cleanUrl)) return 'image';
if (/\.(mp4|webm|ogg|mov|m4v)$/.test(cleanUrl)) return 'video';
return null;
}

function safeAlt(label, fallback) {
const value = String(label || fallback || 'Media').trim();
return value.replace(/[\]\n\r]/g, '').replace(/^https?:\/\//i, '') || fallback || 'Media';
}

function sanitizeAssetName(name, fallbackUrl) {
const fallbackName = (() => {
try {
const pathname = new URL(fallbackUrl).pathname;
return decodeURIComponent(pathname.split('/').filter(Boolean).pop() || 'video.mp4');
} catch {
return 'video.mp4';
}
})();
const rawName = String(name || fallbackName || 'video.mp4').trim();
const ext = path.extname(rawName);
const base = path.basename(rawName, ext) || crypto.createHash('sha1').update(fallbackUrl).digest('hex').slice(0, 10);
const safeBase = base
.replace(/[\\/:*?"<>|#%{}]/g, '-')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '') || 'video';
return `${safeBase}-preview.gif`;
}

function markdownImage(url, label) {
return `![${safeAlt(label, 'Media')}](${url})`;
}

async function githubJson(method, apiPath, body) {
const options = {
method,
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${token}`,
'X-GitHub-Api-Version': '2022-11-28',
},
};
if (body !== undefined) {
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(body);
}
const response = await fetch(`https://api.github.com${apiPath}`, options);
if (!response.ok) throw new Error(`${method} ${apiPath} failed: HTTP ${response.status}: ${await response.text()}`);
if (response.status === 204) return null;
return response.json();
}

async function uploadGifAsset(filePath, assetName) {
const assets = await githubJson('GET', `/repos/${repo}/releases/${releaseApiId}/assets?per_page=100`);
const existing = assets.find((asset) => asset.name === assetName);
if (existing) await githubJson('DELETE', `/repos/${repo}/releases/assets/${existing.id}`);

const buffer = await fs.readFile(filePath);
const response = await fetch(`https://uploads.github.com/repos/${repo}/releases/${releaseApiId}/assets?name=${encodeURIComponent(assetName)}`, {
method: 'POST',
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${token}`,
'Content-Type': 'image/gif',
'Content-Length': String(buffer.byteLength),
'X-GitHub-Api-Version': '2022-11-28',
},
body: buffer,
});
if (!response.ok) throw new Error(`Upload ${assetName} failed: HTTP ${response.status}: ${await response.text()}`);
const asset = await response.json();
return asset.browser_download_url;
}

async function renderVideo(url, label) {
const source = mediaSources[url] || { url, name: label };
const downloadUrl = source.url || url;
const assetName = sanitizeAssetName(source.name || label, url);
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), 'release-video-'));
const inputPath = path.join(workDir, 'input-video');
const outputPath = path.join(workDir, assetName);

const response = await fetch(downloadUrl);
if (!response.ok) throw new Error(`Download video failed for ${url}: HTTP ${response.status}`);
await fs.writeFile(inputPath, Buffer.from(await response.arrayBuffer()));

execFileSync('ffmpeg', [
'-y',
'-i', inputPath,
'-t', '6',
'-vf', 'fps=12,scale=1280:-1:flags=lanczos,format=rgb24,split[s0][s1];[s0]palettegen=max_colors=256:stats_mode=full[p];[s1][p]paletteuse=dither=sierra2_4a',
outputPath,
], { stdio: 'inherit' });

const gifUrl = await uploadGifAsset(outputPath, assetName);
return `\n\n<img src="${gifUrl}" alt="video preview" width="720">\n\n`;
}

async function renderUrl(url, label) {
const type = getMediaType(url);
if (type === 'image') return markdownImage(url, label);
if (type === 'githubAttachment') return `\n\n${url}\n\n`;
if (type === 'video') return renderVideo(url, label);
return null;
}

async function renderMarkdown(markdown) {
let rendered = String(markdown || '');
const markdownLinkPattern = /(!?)\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g;
const linkMatches = [...rendered.matchAll(markdownLinkPattern)];
for (const match of linkMatches) {
const [fullMatch, bang, label, url] = match;
if (bang) continue;
const replacement = await renderUrl(url, label);
if (replacement) rendered = rendered.replace(fullMatch, replacement);
}

const bareUrlPattern = /(^|[\s>])(https?:\/\/[^\s<>)]+)/g;
const bareMatches = [...rendered.matchAll(bareUrlPattern)];
for (const match of bareMatches) {
const [fullMatch, prefix, url] = match;
const offset = match.index;
if (prefix === '(' || rendered[offset - 1] === '(' || rendered[offset - 1] === '"') continue;
const beforeUrl = rendered.slice(0, offset + prefix.length);
const lastOpenParen = beforeUrl.lastIndexOf('(');
const lastCloseParen = beforeUrl.lastIndexOf(')');
if (lastOpenParen > lastCloseParen && /\[[^\]]*\]$/.test(beforeUrl.slice(0, lastOpenParen))) continue;
const replacement = await renderUrl(url);
if (replacement) rendered = rendered.replace(fullMatch, `${prefix}${replacement}`);
}

return rendered.replace(/\n{3,}/g, '\n\n').trim();
}

(async () => {
const raw = await fs.readFile('release-body.md', 'utf8');
const rendered = await renderMarkdown(raw);
await fs.writeFile('release-body.md', `${rendered}\n`);
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE

- name: Update GitHub release body with media previews
env:
GH_TOKEN: ${{ github.token }}
RELEASE_API_ID: ${{ steps.release.outputs.release_api_id }}
run: |
set -euo pipefail
release_body="$(cat release-body.md)"
jq -nc --arg body "${release_body}" '{ body: $body }' |
gh api "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_API_ID}" \
--method PATCH \
--input -
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

strategy:
matrix:
node-version: [22.18.0]
node-version: [22.22.3]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/v2-benchmark-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

strategy:
matrix:
node-version: [22.18.0]
node-version: [22.22.3]

steps:
- uses: actions/checkout@v4
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/v2-core-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Use Node.js 22.18.0
- name: Use Node.js 22.22.3
uses: actions/setup-node@v4
with:
node-version: 22.18.0
node-version: 22.22.3

- name: 📥 Monorepo install
uses: ./.github/actions/pnpm-install
Expand All @@ -63,10 +63,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Use Node.js 22.18.0
- name: Use Node.js 22.22.3
uses: actions/setup-node@v4
with:
node-version: 22.18.0
node-version: 22.22.3

- name: 📥 Monorepo install
uses: ./.github/actions/pnpm-install
Expand Down Expand Up @@ -102,10 +102,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Use Node.js 22.18.0
- name: Use Node.js 22.22.3
uses: actions/setup-node@v4
with:
node-version: 22.18.0
node-version: 22.22.3

- name: 📥 Download all coverage artifacts
uses: actions/download-artifact@v4
Expand Down
2 changes: 1 addition & 1 deletion .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ auto-install-peers=true
lockfile=true
# force use npmjs.org registry
registry=https://registry.npmjs.org/
use-node-version=22.18.0
use-node-version=22.22.3
save-prefix=''
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ SCRATCH ?= /tmp

UNAME_S := $(shell uname -s)

# set param statement_cache_size=1 to avoid query error `ERROR: cached plan must not change result type` after alter column type (modify field type)
POSTGRES_PRISMA_DATABASE_URL ?= postgresql://teable:teable\@127.0.0.1:5432/teable?schema=public\&statement_cache_size=1
# Disable prepared statement caching to avoid `ERROR: cached plan must not change result type`
# after field schema changes in tests.
POSTGRES_PRISMA_DATABASE_URL ?= postgresql://teable:teable\@127.0.0.1:5432/teable?schema=public\&statement_cache_size=0

# If the first make argument is "start", "stop"...
ifeq (docker.start,$(firstword $(MAKECMDGOALS)))
Expand Down Expand Up @@ -201,7 +202,7 @@ postgres.integration.test: docker.create.network
$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) run -p 25432:5432 -d -T --no-deps --rm --name $$TEST_PG_CONTAINER_NAME teable-postgres; \
chmod +x scripts/wait-for; \
scripts/wait-for 127.0.0.1:25432 --timeout=15 -- echo 'pg database started successfully' && \
export PRISMA_DATABASE_URL=postgresql://teable:teable@127.0.0.1:25432/e2e_test_teable?schema=public\&statement_cache_size=1\&connection_limit=20 && \
export PRISMA_DATABASE_URL=postgresql://teable:teable@127.0.0.1:25432/e2e_test_teable?schema=public\&statement_cache_size=0\&connection_limit=20 && \
make postgres.mode && \
pnpm -F "./packages/**" run build && \
pnpm g:test-e2e-cover && \
Expand Down
4 changes: 3 additions & 1 deletion apps/nestjs-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"eslint-config-next": "15.5.9",
"get-tsconfig": "4.7.3",
"istanbul-merge": "2.0.0",
"neverthrow": "8.2.0",
"npm-run-all2": "6.1.2",
"nyc": "15.1.0",
"pg-mem": "3.0.5",
Expand Down Expand Up @@ -167,7 +168,7 @@
"@opentelemetry/instrumentation-ioredis": "0.49.0",
"@opentelemetry/instrumentation-nestjs-core": "0.49.0",
"@opentelemetry/instrumentation-pg": "0.49.0",
"@opentelemetry/instrumentation-pino": "0.49.0",
"@opentelemetry/instrumentation-pino": "0.54.0",
"@opentelemetry/instrumentation-runtime-node": "0.24.0",
"@opentelemetry/resources": "2.0.1",
"@opentelemetry/sdk-node": "0.201.1",
Expand Down Expand Up @@ -228,6 +229,7 @@
"keyv": "4.5.4",
"knex": "3.1.0",
"lodash": "4.17.21",
"markdown-it": "14.1.0",
"mime-types": "2.1.35",
"minio": "7.1.3",
"ms": "2.1.3",
Expand Down
Loading
Loading