diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml
deleted file mode 100644
index b0f0a36bc9..0000000000
--- a/.github/workflows/anti-slop.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-name: Anti-Slop PR Check
-
-on:
- pull_request_target:
- types: [opened, edited, synchronize]
-
-permissions:
- pull-requests: write
- contents: read
-
-jobs:
- anti-slop:
- runs-on: ubuntu-latest
- steps:
- - uses: peakoss/anti-slop@85daca1880e9e1af197fc06ea03349daf08f4202 # v0.2.1
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- close-pr: false
- failure-add-pr-labels: "needs-revision"
diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml
index c1da73b5df..a08e7aacae 100644
--- a/.github/workflows/api-tests.yml
+++ b/.github/workflows/api-tests.yml
@@ -14,11 +14,11 @@ concurrency:
cancel-in-progress: true
jobs:
- test:
- name: API Tests
- runs-on: ubuntu-latest
+ api-unit:
+ name: API Unit Tests
+ runs-on: depot-ubuntu-24.04
env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ COVERAGE_FILE: coverage-unit
defaults:
run:
shell: bash
@@ -35,7 +35,7 @@ jobs:
persist-credentials: false
- name: Setup UV and Python
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
@@ -50,16 +50,62 @@ jobs:
- name: Run dify config tests
run: uv run --project api dev/pytest/pytest_config_tests.py
+ - name: Run Unit Tests
+ run: uv run --project api bash dev/pytest/pytest_unit_tests.sh
+
+ - name: Upload unit coverage data
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: api-coverage-unit
+ path: coverage-unit
+ retention-days: 1
+
+ api-integration:
+ name: API Integration Tests
+ runs-on: depot-ubuntu-24.04
+ env:
+ COVERAGE_FILE: coverage-integration
+ STORAGE_TYPE: opendal
+ OPENDAL_SCHEME: fs
+ OPENDAL_FS_ROOT: /tmp/dify-storage
+ defaults:
+ run:
+ shell: bash
+ strategy:
+ matrix:
+ python-version:
+ - "3.12"
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Setup UV and Python
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ enable-cache: true
+ python-version: ${{ matrix.python-version }}
+ cache-dependency-glob: api/uv.lock
+
+ - name: Check UV lockfile
+ run: uv lock --project api --check
+
+ - name: Install dependencies
+ run: uv sync --project api --dev
+
- name: Set up dotenvs
run: |
cp docker/.env.example docker/.env
- cp docker/middleware.env.example docker/middleware.env
+ cp docker/envs/middleware.env.example docker/middleware.env
- name: Expose Service Ports
run: sh .github/workflows/expose_service_ports.sh
- name: Set up Sandbox
- uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
+ uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
with:
compose-file: |
docker/docker-compose.middleware.yaml
@@ -73,23 +119,91 @@ jobs:
run: |
cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
- - name: Run API Tests
- env:
- STORAGE_TYPE: opendal
- OPENDAL_SCHEME: fs
- OPENDAL_FS_ROOT: /tmp/dify-storage
+ - name: Run Integration Tests
run: |
uv run --project api pytest \
-n auto \
--timeout "${PYTEST_TIMEOUT:-180}" \
api/tests/integration_tests/workflow \
api/tests/integration_tests/tools \
- api/tests/test_containers_integration_tests \
- api/tests/unit_tests
+ api/tests/test_containers_integration_tests
+
+ - name: Upload integration coverage data
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: api-coverage-integration
+ path: coverage-integration
+ retention-days: 1
+
+ api-coverage:
+ name: API Coverage
+ runs-on: depot-ubuntu-24.04
+ needs:
+ - api-unit
+ - api-integration
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ COVERAGE_FILE: .coverage
+ defaults:
+ run:
+ shell: bash
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Setup UV and Python
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ enable-cache: true
+ python-version: "3.12"
+ cache-dependency-glob: api/uv.lock
+
+ - name: Install dependencies
+ run: uv sync --project api --dev
+
+ - name: Download coverage data
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ path: coverage-data
+ pattern: api-coverage-*
+ merge-multiple: true
+
+ - name: Combine coverage
+ run: |
+ set -euo pipefail
+
+ echo "### API Coverage" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Merged backend coverage report generated for Codecov project status." >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+
+ unit_coverage="$(find coverage-data -type f -name coverage-unit -print -quit)"
+ integration_coverage="$(find coverage-data -type f -name coverage-integration -print -quit)"
+ : "${unit_coverage:?coverage-unit artifact not found}"
+ : "${integration_coverage:?coverage-integration artifact not found}"
+
+ report_file="$(mktemp)"
+ uv run --project api coverage combine "$unit_coverage" "$integration_coverage"
+ uv run --project api coverage report --show-missing | tee "$report_file"
+ echo "Summary: \`$(tail -n 1 "$report_file")\`" >> "$GITHUB_STEP_SUMMARY"
+ {
+ echo ""
+ echo "Coverage report
"
+ echo ""
+ echo '```'
+ cat "$report_file"
+ echo '```'
+ echo " "
+ } >> "$GITHUB_STEP_SUMMARY"
+ uv run --project api coverage xml -o coverage.xml
- name: Report coverage
- if: ${{ env.CODECOV_TOKEN != '' && matrix.python-version == '3.12' }}
- uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
+ if: ${{ env.CODECOV_TOKEN != '' }}
+ uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./coverage.xml
disable_search: true
diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml
index be6186980e..b0cd652c43 100644
--- a/.github/workflows/autofix.yml
+++ b/.github/workflows/autofix.yml
@@ -2,6 +2,9 @@ name: autofix.ci
on:
pull_request:
branches: ["main"]
+ merge_group:
+ branches: ["main"]
+ types: [checks_requested]
push:
branches: ["main"]
permissions:
@@ -10,13 +13,19 @@ permissions:
jobs:
autofix:
if: github.repository == 'langgenius/dify'
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - name: Complete merge group check
+ if: github.event_name == 'merge_group'
+ run: echo "autofix.ci updates pull request branches, not merge group refs."
+
+ - if: github.event_name != 'merge_group'
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check Docker Compose inputs
+ if: github.event_name != 'merge_group'
id: docker-compose-changes
- uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
docker/generate_docker_compose
@@ -24,30 +33,39 @@ jobs:
docker/docker-compose-template.yaml
docker/docker-compose.yaml
- name: Check web inputs
+ if: github.event_name != 'merge_group'
id: web-changes
- uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
web/**
+ packages/**
+ package.json
+ pnpm-lock.yaml
+ pnpm-workspace.yaml
+ .nvmrc
- name: Check api inputs
+ if: github.event_name != 'merge_group'
id: api-changes
- uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
api/**
- - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+ - if: github.event_name != 'merge_group'
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
+ - if: github.event_name != 'merge_group'
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Generate Docker Compose
- if: steps.docker-compose-changes.outputs.any_changed == 'true'
+ if: github.event_name != 'merge_group' && steps.docker-compose-changes.outputs.any_changed == 'true'
run: |
cd docker
./generate_docker_compose
- - if: steps.api-changes.outputs.any_changed == 'true'
+ - if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
run: |
cd api
uv sync --dev
@@ -59,13 +77,13 @@ jobs:
uv run ruff format ..
- name: count migration progress
- if: steps.api-changes.outputs.any_changed == 'true'
+ if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
run: |
cd api
./cnt_base.sh
- name: ast-grep
- if: steps.api-changes.outputs.any_changed == 'true'
+ if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
run: |
# ast-grep exits 1 if no matches are found; allow idempotent runs.
uvx --from ast-grep-cli ast-grep --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all || true
@@ -95,13 +113,23 @@ jobs:
find . -name "*.py.bak" -type f -delete
- name: Setup web environment
- if: steps.web-changes.outputs.any_changed == 'true'
+ if: github.event_name != 'merge_group'
uses: ./.github/actions/setup-web
- - name: ESLint autofix
- if: steps.web-changes.outputs.any_changed == 'true'
+ - name: Generate API docs
+ if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
+ run: |
+ cd api
+ uv run dev/generate_swagger_markdown_docs.py --swagger-dir ../packages/contracts/openapi --markdown-dir openapi/markdown --keep-swagger-json
+
+ - name: Generate frontend contracts
+ if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
+ run: pnpm --dir packages/contracts gen-api-contract-from-openapi
+
+ - name: ESLint autofix
+ if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'
run: |
- cd web
vp exec eslint --concurrency=2 --prune-suppressions --quiet || true
- - uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8 # v1.3.3
+ - if: github.event_name != 'merge_group'
+ uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4
diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml
index 1ae8d44482..915ed6cfe8 100644
--- a/.github/workflows/build-push.yml
+++ b/.github/workflows/build-push.yml
@@ -24,27 +24,42 @@ env:
jobs:
build:
- runs-on: ${{ matrix.platform == 'linux/arm64' && 'arm64_runner' || 'ubuntu-latest' }}
+ runs-on: ${{ matrix.runs_on }}
if: github.repository == 'langgenius/dify'
+ permissions:
+ contents: read
+ id-token: write
strategy:
matrix:
include:
- service_name: "build-api-amd64"
image_name_env: "DIFY_API_IMAGE_NAME"
- context: "api"
+ artifact_context: "api"
+ build_context: "{{defaultContext}}:api"
+ file: "Dockerfile"
platform: linux/amd64
+ runs_on: depot-ubuntu-24.04-4
- service_name: "build-api-arm64"
image_name_env: "DIFY_API_IMAGE_NAME"
- context: "api"
+ artifact_context: "api"
+ build_context: "{{defaultContext}}:api"
+ file: "Dockerfile"
platform: linux/arm64
+ runs_on: depot-ubuntu-24.04-4
- service_name: "build-web-amd64"
image_name_env: "DIFY_WEB_IMAGE_NAME"
- context: "web"
+ artifact_context: "web"
+ build_context: "{{defaultContext}}"
+ file: "web/Dockerfile"
platform: linux/amd64
+ runs_on: depot-ubuntu-24.04-4
- service_name: "build-web-arm64"
image_name_env: "DIFY_WEB_IMAGE_NAME"
- context: "web"
+ artifact_context: "web"
+ build_context: "{{defaultContext}}"
+ file: "web/Dockerfile"
platform: linux/arm64
+ runs_on: depot-ubuntu-24.04-4
steps:
- name: Prepare
@@ -53,16 +68,13 @@ jobs:
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Login to Docker Hub
- uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ env.DOCKERHUB_USER }}
password: ${{ env.DOCKERHUB_TOKEN }}
- - name: Set up QEMU
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
+ - name: Set up Depot CLI
+ uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
- name: Extract metadata for Docker
id: meta
@@ -72,15 +84,15 @@ jobs:
- name: Build Docker image
id: build
- uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
+ uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
with:
- context: "{{defaultContext}}:${{ matrix.context }}"
+ project: ${{ vars.DEPOT_PROJECT_ID }}
+ context: ${{ matrix.build_context }}
+ file: ${{ matrix.file }}
platforms: ${{ matrix.platform }}
build-args: COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env[matrix.image_name_env] }},push-by-digest=true,name-canonical=true,push=true
- cache-from: type=gha,scope=${{ matrix.service_name }}
- cache-to: type=gha,mode=max,scope=${{ matrix.service_name }}
- name: Export digest
env:
@@ -91,16 +103,40 @@ jobs:
touch "/tmp/digests/${sanitized_digest}"
- name: Upload digest
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
- name: digests-${{ matrix.context }}-${{ env.PLATFORM_PAIR }}
+ name: digests-${{ matrix.artifact_context }}-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
+ fork-build-validate:
+ if: github.repository != 'langgenius/dify'
+ runs-on: ubuntu-24.04
+ strategy:
+ matrix:
+ include:
+ - service_name: "validate-api-amd64"
+ build_context: "{{defaultContext}}:api"
+ file: "Dockerfile"
+ - service_name: "validate-web-amd64"
+ build_context: "{{defaultContext}}"
+ file: "web/Dockerfile"
+ steps:
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
+
+ - name: Validate Docker image
+ uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
+ with:
+ push: false
+ context: ${{ matrix.build_context }}
+ file: ${{ matrix.file }}
+ platforms: linux/amd64
+
create-manifest:
needs: build
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
if: github.repository == 'langgenius/dify'
strategy:
matrix:
@@ -120,7 +156,7 @@ jobs:
merge-multiple: true
- name: Login to Docker Hub
- uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ env.DOCKERHUB_USER }}
password: ${{ env.DOCKERHUB_TOKEN }}
diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml
index ffb9734e48..9d3ccb34b2 100644
--- a/.github/workflows/db-migration-test.yml
+++ b/.github/workflows/db-migration-test.yml
@@ -9,7 +9,7 @@ concurrency:
jobs:
db-migration-test-postgres:
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
steps:
- name: Checkout code
@@ -19,7 +19,7 @@ jobs:
persist-credentials: false
- name: Setup UV and Python
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
python-version: "3.12"
@@ -37,10 +37,10 @@ jobs:
- name: Prepare middleware env
run: |
cd docker
- cp middleware.env.example middleware.env
+ cp envs/middleware.env.example middleware.env
- name: Set up Middlewares
- uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
+ uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
with:
compose-file: |
docker/docker-compose.middleware.yaml
@@ -59,7 +59,7 @@ jobs:
run: uv run --directory api flask upgrade-db
db-migration-test-mysql:
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
steps:
- name: Checkout code
@@ -69,7 +69,7 @@ jobs:
persist-credentials: false
- name: Setup UV and Python
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
python-version: "3.12"
@@ -87,14 +87,14 @@ jobs:
- name: Prepare middleware env for MySQL
run: |
cd docker
- cp middleware.env.example middleware.env
+ cp envs/middleware.env.example middleware.env
sed -i 's/DB_TYPE=postgresql/DB_TYPE=mysql/' middleware.env
sed -i 's/DB_HOST=db_postgres/DB_HOST=db_mysql/' middleware.env
sed -i 's/DB_PORT=5432/DB_PORT=3306/' middleware.env
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=mysql/' middleware.env
- name: Set up Middlewares
- uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
+ uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
with:
compose-file: |
docker/docker-compose.middleware.yaml
@@ -110,6 +110,28 @@ jobs:
sed -i 's/DB_PORT=5432/DB_PORT=3306/' .env
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=root/' .env
+ # hoverkraft-tech/compose-action@v2.6.0 only waits for `docker compose up -d`
+ # to return (container processes started); it does not wait on healthcheck
+ # status. mysql:8.0's first-time init takes 15-30s, so without an explicit
+ # wait the migration runs while InnoDB is still initialising and gets
+ # killed with "Lost connection during query". Poll a real SELECT until it
+ # succeeds.
+ - name: Wait for MySQL to accept queries
+ run: |
+ set +e
+ for i in $(seq 1 60); do
+ if docker run --rm --network host mysql:8.0 \
+ mysql -h 127.0.0.1 -P 3306 -uroot -pdifyai123456 \
+ -e 'SELECT 1' >/dev/null 2>&1; then
+ echo "MySQL ready after ${i}s"
+ exit 0
+ fi
+ sleep 1
+ done
+ echo "MySQL not ready after 60s; dumping container logs:"
+ docker compose -f docker/docker-compose.middleware.yaml --profile mysql logs --tail=200 db_mysql
+ exit 1
+
- name: Run DB Migration
env:
DEBUG: true
diff --git a/.github/workflows/deploy-agent-dev.yml b/.github/workflows/deploy-agent-dev.yml
index cd5fe9242e..9b9b77e0a2 100644
--- a/.github/workflows/deploy-agent-dev.yml
+++ b/.github/workflows/deploy-agent-dev.yml
@@ -13,7 +13,7 @@ on:
jobs:
deploy:
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'deploy/agent-dev'
diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml
index 954537663a..c2ff8c6332 100644
--- a/.github/workflows/deploy-dev.yml
+++ b/.github/workflows/deploy-dev.yml
@@ -10,7 +10,7 @@ on:
jobs:
deploy:
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'deploy/dev'
diff --git a/.github/workflows/deploy-enterprise.yml b/.github/workflows/deploy-enterprise.yml
index 9cff3a3482..2740541f0f 100644
--- a/.github/workflows/deploy-enterprise.yml
+++ b/.github/workflows/deploy-enterprise.yml
@@ -13,7 +13,7 @@ on:
jobs:
deploy:
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'deploy/enterprise'
diff --git a/.github/workflows/deploy-hitl.yml b/.github/workflows/deploy-hitl.yml
index c6f1cc7e6f..0da241cf95 100644
--- a/.github/workflows/deploy-hitl.yml
+++ b/.github/workflows/deploy-hitl.yml
@@ -10,7 +10,7 @@ on:
jobs:
deploy:
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'build/feat/hitl'
diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
index 340b380dc9..5144510be5 100644
--- a/.github/workflows/docker-build.yml
+++ b/.github/workflows/docker-build.yml
@@ -14,35 +14,69 @@ concurrency:
jobs:
build-docker:
- runs-on: ubuntu-latest
+ if: github.event.pull_request.head.repo.full_name == github.repository
+ runs-on: ${{ matrix.runs_on }}
+ permissions:
+ contents: read
+ id-token: write
strategy:
matrix:
include:
- service_name: "api-amd64"
platform: linux/amd64
- context: "api"
+ runs_on: depot-ubuntu-24.04-4
+ context: "{{defaultContext}}:api"
+ file: "Dockerfile"
- service_name: "api-arm64"
platform: linux/arm64
- context: "api"
+ runs_on: depot-ubuntu-24.04-4
+ context: "{{defaultContext}}:api"
+ file: "Dockerfile"
- service_name: "web-amd64"
platform: linux/amd64
- context: "web"
+ runs_on: depot-ubuntu-24.04-4
+ context: "{{defaultContext}}"
+ file: "web/Dockerfile"
- service_name: "web-arm64"
platform: linux/arm64
- context: "web"
+ runs_on: depot-ubuntu-24.04-4
+ context: "{{defaultContext}}"
+ file: "web/Dockerfile"
steps:
- - name: Set up QEMU
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
+ - name: Set up Depot CLI
+ uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
+ - name: Build Docker Image
+ uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
+ with:
+ project: ${{ vars.DEPOT_PROJECT_ID }}
+ push: false
+ context: ${{ matrix.context }}
+ file: ${{ matrix.file }}
+ platforms: ${{ matrix.platform }}
+
+ build-docker-fork:
+ if: github.event.pull_request.head.repo.full_name != github.repository
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ strategy:
+ matrix:
+ include:
+ - service_name: "api-amd64"
+ context: "{{defaultContext}}:api"
+ file: "Dockerfile"
+ - service_name: "web-amd64"
+ context: "{{defaultContext}}"
+ file: "web/Dockerfile"
+ steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build Docker Image
- uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
+ uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
push: false
- context: "{{defaultContext}}:${{ matrix.context }}"
- file: "${{ matrix.file }}"
- platforms: ${{ matrix.platform }}
- cache-from: type=gha
- cache-to: type=gha,mode=max
+ context: ${{ matrix.context }}
+ file: ${{ matrix.file }}
+ platforms: linux/amd64
diff --git a/.github/workflows/hotfix-cherry-pick.yml b/.github/workflows/hotfix-cherry-pick.yml
new file mode 100644
index 0000000000..594b10c743
--- /dev/null
+++ b/.github/workflows/hotfix-cherry-pick.yml
@@ -0,0 +1,49 @@
+name: Hotfix Cherry-Pick Provenance
+
+on:
+ pull_request:
+ branches:
+ - 'hotfix/**'
+ - 'lts/**'
+ types:
+ - opened
+ - edited
+ - reopened
+ - ready_for_review
+ - synchronize
+
+permissions:
+ contents: read
+
+concurrency:
+ group: hotfix-cherry-pick-${{ github.event.pull_request.number || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ check-cherry-pick-provenance:
+ name: Require cherry-pick provenance
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: Fetch PR base, PR head, and main
+ env:
+ BASE_REF: ${{ github.base_ref }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: |
+ git fetch --no-tags --prune origin \
+ "+refs/heads/main:refs/remotes/origin/main" \
+ "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" \
+ "+refs/pull/${PR_NUMBER}/head:refs/remotes/pull/${PR_NUMBER}/head"
+
+ - name: Load checker from main
+ run: git show origin/main:.github/scripts/check-hotfix-cherry-picks.sh > "$RUNNER_TEMP/check-hotfix-cherry-picks.sh"
+
+ - name: Check PR commits
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ MAIN_REF: origin/main
+ run: bash "$RUNNER_TEMP/check-hotfix-cherry-picks.sh"
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index 278e10bc04..aefcf1b5ac 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -7,8 +7,8 @@ jobs:
permissions:
contents: read
pull-requests: write
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
steps:
- - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
+ - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
sync-labels: true
diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml
index 69023c24cc..f624e8f872 100644
--- a/.github/workflows/main-ci.yml
+++ b/.github/workflows/main-ci.yml
@@ -3,10 +3,14 @@ name: Main CI Pipeline
on:
pull_request:
branches: ["main"]
+ merge_group:
+ branches: ["main"]
+ types: [checks_requested]
push:
branches: ["main"]
permissions:
+ actions: write
contents: write
pull-requests: write
checks: write
@@ -17,12 +21,28 @@ concurrency:
cancel-in-progress: true
jobs:
+ pre_job:
+ name: Skip Duplicate Checks
+ runs-on: depot-ubuntu-24.04
+ outputs:
+ should_skip: ${{ steps.skip_check.outputs.should_skip || 'false' }}
+ steps:
+ - id: skip_check
+ continue-on-error: true
+ uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1
+ with:
+ cancel_others: 'true'
+ concurrent_skipping: same_content_newer
+
# Check which paths were changed to determine which tests to run
check-changes:
name: Check Changed Files
- runs-on: ubuntu-latest
+ needs: pre_job
+ if: needs.pre_job.outputs.should_skip != 'true'
+ runs-on: depot-ubuntu-24.04
outputs:
api-changed: ${{ steps.changes.outputs.api }}
+ e2e-changed: ${{ steps.changes.outputs.e2e }}
web-changed: ${{ steps.changes.outputs.web }}
vdb-changed: ${{ steps.changes.outputs.vdb }}
migration-changed: ${{ steps.changes.outputs.migration }}
@@ -34,49 +54,375 @@ jobs:
filters: |
api:
- 'api/**'
- - 'docker/**'
- '.github/workflows/api-tests.yml'
+ - '.github/workflows/expose_service_ports.sh'
+ - 'docker/.env.example'
+ - 'docker/envs/middleware.env.example'
+ - 'docker/docker-compose.middleware.yaml'
+ - 'docker/docker-compose-template.yaml'
+ - 'docker/generate_docker_compose'
+ - 'docker/ssrf_proxy/**'
+ - 'docker/volumes/sandbox/conf/**'
web:
- 'web/**'
+ - 'packages/**'
+ - 'package.json'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - '.nvmrc'
- '.github/workflows/web-tests.yml'
- '.github/actions/setup-web/**'
+ e2e:
+ - 'api/**'
+ - 'api/pyproject.toml'
+ - 'api/uv.lock'
+ - 'e2e/**'
+ - 'web/**'
+ - 'packages/**'
+ - 'package.json'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - '.nvmrc'
+ - 'docker/docker-compose.middleware.yaml'
+ - 'docker/envs/middleware.env.example'
+ - '.github/workflows/web-e2e.yml'
+ - '.github/actions/setup-web/**'
vdb:
- 'api/core/rag/datasource/**'
- - 'docker/**'
+ - 'api/tests/integration_tests/vdb/**'
+ - 'api/providers/vdb/*/tests/**'
- '.github/workflows/vdb-tests.yml'
+ - '.github/workflows/expose_service_ports.sh'
+ - 'docker/.env.example'
+ - 'docker/envs/middleware.env.example'
+ - 'docker/docker-compose.yaml'
+ - 'docker/docker-compose-template.yaml'
+ - 'docker/generate_docker_compose'
+ - 'docker/certbot/**'
+ - 'docker/couchbase-server/**'
+ - 'docker/elasticsearch/**'
+ - 'docker/iris/**'
+ - 'docker/nginx/**'
+ - 'docker/pgvector/**'
+ - 'docker/ssrf_proxy/**'
+ - 'docker/startupscripts/**'
+ - 'docker/tidb/**'
+ - 'docker/volumes/**'
- 'api/uv.lock'
- 'api/pyproject.toml'
migration:
- 'api/migrations/**'
+ - 'api/.env.example'
- '.github/workflows/db-migration-test.yml'
+ - '.github/workflows/expose_service_ports.sh'
+ - 'docker/.env.example'
+ - 'docker/envs/middleware.env.example'
+ - 'docker/docker-compose.middleware.yaml'
+ - 'docker/docker-compose-template.yaml'
+ - 'docker/generate_docker_compose'
+ - 'docker/ssrf_proxy/**'
+ - 'docker/volumes/sandbox/conf/**'
- # Run tests in parallel
- api-tests:
- name: API Tests
- needs: check-changes
- if: needs.check-changes.outputs.api-changed == 'true'
+ # Run tests in parallel while always emitting stable required checks.
+ api-tests-run:
+ name: Run API Tests
+ needs:
+ - pre_job
+ - check-changes
+ if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.api-changed == 'true'
uses: ./.github/workflows/api-tests.yml
secrets: inherit
- web-tests:
- name: Web Tests
- needs: check-changes
- if: needs.check-changes.outputs.web-changed == 'true'
+ api-tests-skip:
+ name: Skip API Tests
+ needs:
+ - pre_job
+ - check-changes
+ if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.api-changed != 'true'
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - name: Report skipped API tests
+ run: echo "No API-related changes detected; skipping API tests."
+
+ api-tests:
+ name: API Tests
+ if: ${{ always() }}
+ needs:
+ - pre_job
+ - check-changes
+ - api-tests-run
+ - api-tests-skip
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - name: Finalize API Tests status
+ env:
+ SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
+ TESTS_CHANGED: ${{ needs.check-changes.outputs.api-changed }}
+ RUN_RESULT: ${{ needs.api-tests-run.result }}
+ SKIP_RESULT: ${{ needs.api-tests-skip.result }}
+ run: |
+ if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
+ echo "API tests were skipped because this workflow run duplicated a successful or newer run."
+ exit 0
+ fi
+
+ if [[ "$TESTS_CHANGED" == 'true' ]]; then
+ if [[ "$RUN_RESULT" == 'success' ]]; then
+ echo "API tests ran successfully."
+ exit 0
+ fi
+
+ echo "API tests were required but finished with result: $RUN_RESULT" >&2
+ exit 1
+ fi
+
+ if [[ "$SKIP_RESULT" == 'success' ]]; then
+ echo "API tests were skipped because no API-related files changed."
+ exit 0
+ fi
+
+ echo "API tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
+ exit 1
+
+ web-tests-run:
+ name: Run Web Tests
+ needs:
+ - pre_job
+ - check-changes
+ if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.web-changed == 'true'
uses: ./.github/workflows/web-tests.yml
secrets: inherit
+ web-tests-skip:
+ name: Skip Web Tests
+ needs:
+ - pre_job
+ - check-changes
+ if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.web-changed != 'true'
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - name: Report skipped web tests
+ run: echo "No web-related changes detected; skipping web tests."
+
+ web-tests:
+ name: Web Tests
+ if: ${{ always() }}
+ needs:
+ - pre_job
+ - check-changes
+ - web-tests-run
+ - web-tests-skip
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - name: Finalize Web Tests status
+ env:
+ SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
+ TESTS_CHANGED: ${{ needs.check-changes.outputs.web-changed }}
+ RUN_RESULT: ${{ needs.web-tests-run.result }}
+ SKIP_RESULT: ${{ needs.web-tests-skip.result }}
+ run: |
+ if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
+ echo "Web tests were skipped because this workflow run duplicated a successful or newer run."
+ exit 0
+ fi
+
+ if [[ "$TESTS_CHANGED" == 'true' ]]; then
+ if [[ "$RUN_RESULT" == 'success' ]]; then
+ echo "Web tests ran successfully."
+ exit 0
+ fi
+
+ echo "Web tests were required but finished with result: $RUN_RESULT" >&2
+ exit 1
+ fi
+
+ if [[ "$SKIP_RESULT" == 'success' ]]; then
+ echo "Web tests were skipped because no web-related files changed."
+ exit 0
+ fi
+
+ echo "Web tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
+ exit 1
+
+ web-e2e-run:
+ name: Run Web Full-Stack E2E
+ needs:
+ - pre_job
+ - check-changes
+ if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed == 'true'
+ uses: ./.github/workflows/web-e2e.yml
+
+ web-e2e-skip:
+ name: Skip Web Full-Stack E2E
+ needs:
+ - pre_job
+ - check-changes
+ if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed != 'true'
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - name: Report skipped web full-stack e2e
+ run: echo "No E2E-related changes detected; skipping web full-stack E2E."
+
+ web-e2e:
+ name: Web Full-Stack E2E
+ if: ${{ always() }}
+ needs:
+ - pre_job
+ - check-changes
+ - web-e2e-run
+ - web-e2e-skip
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - name: Finalize Web Full-Stack E2E status
+ env:
+ SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
+ TESTS_CHANGED: ${{ needs.check-changes.outputs.e2e-changed }}
+ RUN_RESULT: ${{ needs.web-e2e-run.result }}
+ SKIP_RESULT: ${{ needs.web-e2e-skip.result }}
+ run: |
+ if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
+ echo "Web full-stack E2E was skipped because this workflow run duplicated a successful or newer run."
+ exit 0
+ fi
+
+ if [[ "$TESTS_CHANGED" == 'true' ]]; then
+ if [[ "$RUN_RESULT" == 'success' ]]; then
+ echo "Web full-stack E2E ran successfully."
+ exit 0
+ fi
+
+ echo "Web full-stack E2E was required but finished with result: $RUN_RESULT" >&2
+ exit 1
+ fi
+
+ if [[ "$SKIP_RESULT" == 'success' ]]; then
+ echo "Web full-stack E2E was skipped because no E2E-related files changed."
+ exit 0
+ fi
+
+ echo "Web full-stack E2E was not required, but the skip job finished with result: $SKIP_RESULT" >&2
+ exit 1
+
style-check:
name: Style Check
+ needs: pre_job
+ if: needs.pre_job.outputs.should_skip != 'true'
uses: ./.github/workflows/style.yml
+ vdb-tests-run:
+ name: Run VDB Tests
+ needs:
+ - pre_job
+ - check-changes
+ if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.vdb-changed == 'true'
+ uses: ./.github/workflows/vdb-tests.yml
+
+ vdb-tests-skip:
+ name: Skip VDB Tests
+ needs:
+ - pre_job
+ - check-changes
+ if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.vdb-changed != 'true'
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - name: Report skipped VDB tests
+ run: echo "No VDB-related changes detected; skipping VDB tests."
+
vdb-tests:
name: VDB Tests
- needs: check-changes
- if: needs.check-changes.outputs.vdb-changed == 'true'
- uses: ./.github/workflows/vdb-tests.yml
+ if: ${{ always() }}
+ needs:
+ - pre_job
+ - check-changes
+ - vdb-tests-run
+ - vdb-tests-skip
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - name: Finalize VDB Tests status
+ env:
+ SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
+ TESTS_CHANGED: ${{ needs.check-changes.outputs.vdb-changed }}
+ RUN_RESULT: ${{ needs.vdb-tests-run.result }}
+ SKIP_RESULT: ${{ needs.vdb-tests-skip.result }}
+ run: |
+ if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
+ echo "VDB tests were skipped because this workflow run duplicated a successful or newer run."
+ exit 0
+ fi
+
+ if [[ "$TESTS_CHANGED" == 'true' ]]; then
+ if [[ "$RUN_RESULT" == 'success' ]]; then
+ echo "VDB tests ran successfully."
+ exit 0
+ fi
+
+ echo "VDB tests were required but finished with result: $RUN_RESULT" >&2
+ exit 1
+ fi
+
+ if [[ "$SKIP_RESULT" == 'success' ]]; then
+ echo "VDB tests were skipped because no VDB-related files changed."
+ exit 0
+ fi
+
+ echo "VDB tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
+ exit 1
+
+ db-migration-test-run:
+ name: Run DB Migration Test
+ needs:
+ - pre_job
+ - check-changes
+ if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.migration-changed == 'true'
+ uses: ./.github/workflows/db-migration-test.yml
+
+ db-migration-test-skip:
+ name: Skip DB Migration Test
+ needs:
+ - pre_job
+ - check-changes
+ if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.migration-changed != 'true'
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - name: Report skipped DB migration tests
+ run: echo "No migration-related changes detected; skipping DB migration tests."
db-migration-test:
name: DB Migration Test
- needs: check-changes
- if: needs.check-changes.outputs.migration-changed == 'true'
- uses: ./.github/workflows/db-migration-test.yml
+ if: ${{ always() }}
+ needs:
+ - pre_job
+ - check-changes
+ - db-migration-test-run
+ - db-migration-test-skip
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - name: Finalize DB Migration Test status
+ env:
+ SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
+ TESTS_CHANGED: ${{ needs.check-changes.outputs.migration-changed }}
+ RUN_RESULT: ${{ needs.db-migration-test-run.result }}
+ SKIP_RESULT: ${{ needs.db-migration-test-skip.result }}
+ run: |
+ if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
+ echo "DB migration tests were skipped because this workflow run duplicated a successful or newer run."
+ exit 0
+ fi
+
+ if [[ "$TESTS_CHANGED" == 'true' ]]; then
+ if [[ "$RUN_RESULT" == 'success' ]]; then
+ echo "DB migration tests ran successfully."
+ exit 0
+ fi
+
+ echo "DB migration tests were required but finished with result: $RUN_RESULT" >&2
+ exit 1
+ fi
+
+ if [[ "$SKIP_RESULT" == 'success' ]]; then
+ echo "DB migration tests were skipped because no migration-related files changed."
+ exit 0
+ fi
+
+ echo "DB migration tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
+ exit 1
diff --git a/.github/workflows/pyrefly-diff-comment.yml b/.github/workflows/pyrefly-diff-comment.yml
index 0278e1e0d3..8e16baf933 100644
--- a/.github/workflows/pyrefly-diff-comment.yml
+++ b/.github/workflows/pyrefly-diff-comment.yml
@@ -12,7 +12,7 @@ permissions: {}
jobs:
comment:
name: Comment PR with pyrefly diff
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
permissions:
actions: read
contents: read
@@ -21,7 +21,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
steps:
- name: Download pyrefly diff artifact
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -49,7 +49,7 @@ jobs:
run: unzip -o pyrefly_diff.zip
- name: Post comment
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -76,13 +76,29 @@ jobs:
diff += '\\n\\n... (truncated) ...';
}
- const body = diff.trim()
- ? '### Pyrefly Diff\n\nbase โ PR
\n\n```diff\n' + diff + '\n```\n '
- : '### Pyrefly Diff\nNo changes detected.';
+ if (diff.trim()) {
+ const body = '### Pyrefly Diff\n\nbase โ PR
\n\n```diff\n' + diff + '\n```\n ';
+ const marker = '### Pyrefly Diff';
+ const { data: comments } = await github.rest.issues.listComments({
+ issue_number: prNumber,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ });
+ const existing = comments.find((comment) => comment.body.startsWith(marker));
- await github.rest.issues.createComment({
- issue_number: prNumber,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body,
- });
+ if (existing) {
+ await github.rest.issues.updateComment({
+ comment_id: existing.id,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ issue_number: prNumber,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body,
+ });
+ }
+ }
diff --git a/.github/workflows/pyrefly-diff.yml b/.github/workflows/pyrefly-diff.yml
index a00f469bbe..386bd25751 100644
--- a/.github/workflows/pyrefly-diff.yml
+++ b/.github/workflows/pyrefly-diff.yml
@@ -10,7 +10,7 @@ permissions:
jobs:
pyrefly-diff:
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
permissions:
contents: read
issues: write
@@ -22,7 +22,7 @@ jobs:
fetch-depth: 0
- name: Setup Python & UV
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
@@ -50,12 +50,23 @@ jobs:
run: |
diff -u /tmp/pyrefly_base.txt /tmp/pyrefly_pr.txt > pyrefly_diff.txt || true
+ - name: Check if line counts match
+ id: line_count_check
+ run: |
+ base_lines=$(wc -l < /tmp/pyrefly_base.txt)
+ pr_lines=$(wc -l < /tmp/pyrefly_pr.txt)
+ if [ "$base_lines" -eq "$pr_lines" ]; then
+ echo "same=true" >> $GITHUB_OUTPUT
+ else
+ echo "same=false" >> $GITHUB_OUTPUT
+ fi
+
- name: Save PR number
run: |
echo ${{ github.event.pull_request.number }} > pr_number.txt
- name: Upload pyrefly diff
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pyrefly_diff
path: |
@@ -63,8 +74,8 @@ jobs:
pr_number.txt
- name: Comment PR with pyrefly diff
- if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ if: ${{ github.event.pull_request.head.repo.full_name == github.repository && steps.line_count_check.outputs.same == 'false' }}
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -92,9 +103,26 @@ jobs:
].join('\n')
: '### Pyrefly Diff\nNo changes detected.';
- await github.rest.issues.createComment({
+ const marker = '### Pyrefly Diff';
+ const { data: comments } = await github.rest.issues.listComments({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
- body,
});
+ const existing = comments.find((comment) => comment.body.startsWith(marker));
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ comment_id: existing.id,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ issue_number: prNumber,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body,
+ });
+ }
diff --git a/.github/workflows/pyrefly-type-coverage-comment.yml b/.github/workflows/pyrefly-type-coverage-comment.yml
new file mode 100644
index 0000000000..52c16f3153
--- /dev/null
+++ b/.github/workflows/pyrefly-type-coverage-comment.yml
@@ -0,0 +1,118 @@
+name: Comment with Pyrefly Type Coverage
+
+on:
+ workflow_run:
+ workflows:
+ - Pyrefly Type Coverage
+ types:
+ - completed
+
+permissions: {}
+
+jobs:
+ comment:
+ name: Comment PR with type coverage
+ runs-on: depot-ubuntu-24.04
+ permissions:
+ actions: read
+ contents: read
+ issues: write
+ pull-requests: write
+ if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
+ steps:
+ - name: Checkout default branch (trusted code)
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Setup Python & UV
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ enable-cache: true
+
+ - name: Install dependencies
+ run: uv sync --project api --dev
+
+ - name: Download type coverage artifact
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const fs = require('fs');
+ const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: ${{ github.event.workflow_run.id }},
+ });
+ const match = artifacts.data.artifacts.find((artifact) =>
+ artifact.name === 'pyrefly_type_coverage'
+ );
+ if (!match) {
+ throw new Error('pyrefly_type_coverage artifact not found');
+ }
+ const download = await github.rest.actions.downloadArtifact({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ artifact_id: match.id,
+ archive_format: 'zip',
+ });
+ fs.writeFileSync('pyrefly_type_coverage.zip', Buffer.from(download.data));
+
+ - name: Unzip artifact
+ run: unzip -o pyrefly_type_coverage.zip
+
+ - name: Render coverage markdown from structured data
+ id: render
+ run: |
+ comment_body="$(uv run --directory api python libs/pyrefly_type_coverage.py \
+ --base base_report.json \
+ < pr_report.json)"
+
+ {
+ echo "### Pyrefly Type Coverage"
+ echo ""
+ echo "$comment_body"
+ } > /tmp/type_coverage_comment.md
+
+ - name: Post comment
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const fs = require('fs');
+ const body = fs.readFileSync('/tmp/type_coverage_comment.md', { encoding: 'utf8' });
+ let prNumber = null;
+ try {
+ prNumber = parseInt(fs.readFileSync('pr_number.txt', { encoding: 'utf8' }), 10);
+ } catch (err) {
+ const prs = context.payload.workflow_run.pull_requests || [];
+ if (prs.length > 0 && prs[0].number) {
+ prNumber = prs[0].number;
+ }
+ }
+ if (!prNumber) {
+ throw new Error('PR number not found in artifact or workflow_run payload');
+ }
+
+ // Update existing comment if one exists, otherwise create new
+ const { data: comments } = await github.rest.issues.listComments({
+ issue_number: prNumber,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ });
+ const marker = '### Pyrefly Type Coverage';
+ const existing = comments.find(c => c.body.startsWith(marker));
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ comment_id: existing.id,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ issue_number: prNumber,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body,
+ });
+ }
diff --git a/.github/workflows/pyrefly-type-coverage.yml b/.github/workflows/pyrefly-type-coverage.yml
new file mode 100644
index 0000000000..eae8debf1a
--- /dev/null
+++ b/.github/workflows/pyrefly-type-coverage.yml
@@ -0,0 +1,120 @@
+name: Pyrefly Type Coverage
+
+on:
+ pull_request:
+ paths:
+ - 'api/**/*.py'
+
+permissions:
+ contents: read
+
+jobs:
+ pyrefly-type-coverage:
+ runs-on: depot-ubuntu-24.04
+ permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+ steps:
+ - name: Checkout PR branch
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: Setup Python & UV
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ enable-cache: true
+
+ - name: Install dependencies
+ run: uv sync --project api --dev
+
+ - name: Run pyrefly report on PR branch
+ run: |
+ uv run --directory api --dev pyrefly report 2>/dev/null > /tmp/pyrefly_report_pr.tmp && \
+ mv /tmp/pyrefly_report_pr.tmp /tmp/pyrefly_report_pr.json || \
+ echo '{}' > /tmp/pyrefly_report_pr.json
+
+ - name: Save helper script from base branch
+ run: |
+ git show ${{ github.event.pull_request.base.sha }}:api/libs/pyrefly_type_coverage.py > /tmp/pyrefly_type_coverage.py 2>/dev/null \
+ || cp api/libs/pyrefly_type_coverage.py /tmp/pyrefly_type_coverage.py
+
+ - name: Checkout base branch
+ run: git checkout ${{ github.base_ref }}
+
+ - name: Run pyrefly report on base branch
+ run: |
+ uv run --directory api --dev pyrefly report 2>/dev/null > /tmp/pyrefly_report_base.tmp && \
+ mv /tmp/pyrefly_report_base.tmp /tmp/pyrefly_report_base.json || \
+ echo '{}' > /tmp/pyrefly_report_base.json
+
+ - name: Generate coverage comparison
+ id: coverage
+ run: |
+ comment_body="$(uv run --directory api python /tmp/pyrefly_type_coverage.py \
+ --base /tmp/pyrefly_report_base.json \
+ < /tmp/pyrefly_report_pr.json)"
+
+ {
+ echo "### Pyrefly Type Coverage"
+ echo ""
+ echo "$comment_body"
+ } | tee -a "$GITHUB_STEP_SUMMARY" > /tmp/type_coverage_comment.md
+
+ # Save structured data for the fork-PR comment workflow
+ cp /tmp/pyrefly_report_pr.json pr_report.json
+ cp /tmp/pyrefly_report_base.json base_report.json
+
+ - name: Save PR number
+ run: |
+ echo ${{ github.event.pull_request.number }} > pr_number.txt
+
+ - name: Upload type coverage artifact
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: pyrefly_type_coverage
+ path: |
+ pr_report.json
+ base_report.json
+ pr_number.txt
+
+ - name: Comment PR with type coverage
+ if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const fs = require('fs');
+ const marker = '### Pyrefly Type Coverage';
+ let body;
+ try {
+ body = fs.readFileSync('/tmp/type_coverage_comment.md', { encoding: 'utf8' });
+ } catch {
+ body = `${marker}\n\n_Coverage report unavailable._`;
+ }
+ const prNumber = context.payload.pull_request.number;
+
+ // Update existing comment if one exists, otherwise create new
+ const { data: comments } = await github.rest.issues.listComments({
+ issue_number: prNumber,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ });
+ const existing = comments.find(c => c.body.startsWith(marker));
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ comment_id: existing.id,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ issue_number: prNumber,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body,
+ });
+ }
diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml
index c21331ec0d..6f3193bbf5 100644
--- a/.github/workflows/semantic-pull-request.yml
+++ b/.github/workflows/semantic-pull-request.yml
@@ -7,15 +7,22 @@ on:
- edited
- reopened
- synchronize
+ merge_group:
+ branches: ["main"]
+ types: [checks_requested]
jobs:
lint:
name: Validate PR title
permissions:
pull-requests: read
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
steps:
+ - name: Complete merge group check
+ if: github.event_name == 'merge_group'
+ run: echo "Semantic PR title validation is handled on pull requests."
- name: Check title
+ if: github.event_name == 'pull_request'
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 5cf52daed2..b23648c7c6 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -12,7 +12,7 @@ on:
jobs:
stale:
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
permissions:
issues: write
pull-requests: write
@@ -23,8 +23,8 @@ jobs:
days-before-issue-stale: 15
days-before-issue-close: 3
repo-token: ${{ secrets.GITHUB_TOKEN }}
- stale-issue-message: "Close due to it's no longer active, if you have any questions, you can reopen it."
- stale-pr-message: "Close due to it's no longer active, if you have any questions, you can reopen it."
+ stale-issue-message: "Closed due to inactivity. If you have any questions, you can reopen it."
+ stale-pr-message: "Closed due to inactivity. If you have any questions, you can reopen it."
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'
- any-of-labels: 'duplicate,question,invalid,wontfix,no-issue-activity,no-pr-activity,enhancement,cant-reproduce,help-wanted'
+ any-of-labels: '๐ invalid,๐โโ๏ธ question,wont-fix,no-issue-activity,no-pr-activity,๐ช enhancement,๐ค cant-reproduce,๐ help wanted'
diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml
index 23ae36f7b1..4ce121ba60 100644
--- a/.github/workflows/style.yml
+++ b/.github/workflows/style.yml
@@ -15,7 +15,7 @@ permissions:
jobs:
python-style:
name: Python Style
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
steps:
- name: Checkout code
@@ -25,7 +25,7 @@ jobs:
- name: Check changed files
id: changed-files
- uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
api/**
@@ -33,7 +33,7 @@ jobs:
- name: Setup UV and Python
if: steps.changed-files.outputs.any_changed == 'true'
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: false
python-version: "3.12"
@@ -49,7 +49,7 @@ jobs:
- name: Run Type Checks
if: steps.changed-files.outputs.any_changed == 'true'
- run: make type-check
+ run: make type-check-core
- name: Dotenv check
if: steps.changed-files.outputs.any_changed == 'true'
@@ -57,7 +57,7 @@ jobs:
web-style:
name: Web Style
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
defaults:
run:
working-directory: ./web
@@ -73,10 +73,17 @@ jobs:
- name: Check changed files
id: changed-files
- uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
web/**
+ e2e/**
+ sdks/nodejs-client/**
+ packages/**
+ package.json
+ pnpm-lock.yaml
+ pnpm-workspace.yaml
+ .nvmrc
.github/workflows/style.yml
.github/actions/setup-web/**
@@ -87,26 +94,28 @@ jobs:
- name: Restore ESLint cache
if: steps.changed-files.outputs.any_changed == 'true'
id: eslint-cache-restore
- uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
+ uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
- path: web/.eslintcache
- key: ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'web/pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
+ path: .eslintcache
+ key: ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
restore-keys: |
- ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'web/pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-
+ ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-
- name: Web style check
if: steps.changed-files.outputs.any_changed == 'true'
- working-directory: ./web
+ working-directory: .
run: vp run lint:ci
- name: Web tsslint
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ./web
+ env:
+ NODE_OPTIONS: --max-old-space-size=4096
run: vp run lint:tss
- name: Web type check
if: steps.changed-files.outputs.any_changed == 'true'
- working-directory: ./web
+ working-directory: .
run: vp run type-check
- name: Web dead code check
@@ -116,14 +125,14 @@ jobs:
- name: Save ESLint cache
if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true'
- uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
+ uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
- path: web/.eslintcache
+ path: .eslintcache
key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }}
superlinter:
name: SuperLinter
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
steps:
- name: Checkout code
@@ -134,7 +143,7 @@ jobs:
- name: Check changed files
id: changed-files
- uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
**.sh
@@ -145,7 +154,7 @@ jobs:
.editorconfig
- name: Super-linter
- uses: super-linter/super-linter/slim@61abc07d755095a68f4987d1c2c3d1d64408f1f9 # v8.5.0
+ uses: super-linter/super-linter/slim@9e863354e3ff62e0727d37183162c4a88873df41 # v8.6.0
if: steps.changed-files.outputs.any_changed == 'true'
env:
BASH_SEVERITY: warning
diff --git a/.github/workflows/tool-test-sdks.yaml b/.github/workflows/tool-test-sdks.yaml
index 3fc351c0c2..adaf99f33a 100644
--- a/.github/workflows/tool-test-sdks.yaml
+++ b/.github/workflows/tool-test-sdks.yaml
@@ -6,6 +6,9 @@ on:
- main
paths:
- sdks/**
+ - package.json
+ - pnpm-lock.yaml
+ - pnpm-workspace.yaml
concurrency:
group: sdk-tests-${{ github.head_ref || github.run_id }}
@@ -14,7 +17,7 @@ concurrency:
jobs:
build:
name: unit test for Node.js SDK
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
defaults:
run:
@@ -26,7 +29,7 @@ jobs:
persist-credentials: false
- name: Use Node.js
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: ''
diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml
index 1869254295..4e738df684 100644
--- a/.github/workflows/translate-i18n-claude.yml
+++ b/.github/workflows/translate-i18n-claude.yml
@@ -1,26 +1,24 @@
name: Translate i18n Files with Claude Code
# Note: claude-code-action doesn't support push events directly.
-# Push events are handled by trigger-i18n-sync.yml which sends repository_dispatch.
-# See: https://github.com/langgenius/dify/issues/30743
-
+# Push events are bridged by trigger-i18n-sync.yml via repository_dispatch.
on:
repository_dispatch:
types: [i18n-sync]
workflow_dispatch:
inputs:
files:
- description: 'Specific files to translate (space-separated, e.g., "app common"). Leave empty for all files.'
+ description: 'Specific files to translate (space-separated, e.g., "app common"). Required for full mode; leave empty in incremental mode to use en-US files changed since HEAD~1.'
required: false
type: string
languages:
- description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported languages.'
+ description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported target languages except en-US.'
required: false
type: string
mode:
- description: 'Sync mode: incremental (only changes) or full (re-check all keys)'
+ description: 'Sync mode: incremental (compare with previous en-US revision) or full (sync all keys in scope)'
required: false
- default: 'incremental'
+ default: incremental
type: choice
options:
- incremental
@@ -30,11 +28,15 @@ permissions:
contents: write
pull-requests: write
+concurrency:
+ group: translate-i18n-${{ github.event_name }}-${{ github.ref }}
+ cancel-in-progress: false
+
jobs:
translate:
if: github.repository == 'langgenius/dify'
- runs-on: ubuntu-latest
- timeout-minutes: 60
+ runs-on: depot-ubuntu-24.04
+ timeout-minutes: 120
steps:
- name: Checkout repository
@@ -51,380 +53,293 @@ jobs:
- name: Setup web environment
uses: ./.github/actions/setup-web
- - name: Detect changed files and generate diff
- id: detect_changes
+ - name: Prepare sync context
+ id: context
+ shell: bash
run: |
- if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
- # Manual trigger
- if [ -n "${{ github.event.inputs.files }}" ]; then
- echo "CHANGED_FILES=${{ github.event.inputs.files }}" >> $GITHUB_OUTPUT
- else
- # Get all JSON files in en-US directory
- files=$(ls web/i18n/en-US/*.json 2>/dev/null | xargs -n1 basename | sed 's/.json$//' | tr '\n' ' ')
- echo "CHANGED_FILES=$files" >> $GITHUB_OUTPUT
- fi
- echo "TARGET_LANGS=${{ github.event.inputs.languages }}" >> $GITHUB_OUTPUT
- echo "SYNC_MODE=${{ github.event.inputs.mode || 'incremental' }}" >> $GITHUB_OUTPUT
+ DEFAULT_TARGET_LANGS=$(awk "
+ /value: '/ {
+ value=\$2
+ gsub(/[',]/, \"\", value)
+ }
+ /supported: true/ && value != \"en-US\" {
+ printf \"%s \", value
+ }
+ " web/i18n-config/languages.ts | sed 's/[[:space:]]*$//')
- # For manual trigger with incremental mode, get diff from last commit
- # For full mode, we'll do a complete check anyway
- if [ "${{ github.event.inputs.mode }}" == "full" ]; then
- echo "Full mode: will check all keys" > /tmp/i18n-diff.txt
- echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
- else
- git diff HEAD~1..HEAD -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
- if [ -s /tmp/i18n-diff.txt ]; then
- echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
- else
- echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
- fi
- fi
- elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
- # Triggered by push via trigger-i18n-sync.yml workflow
- # Validate required payload fields
- if [ -z "${{ github.event.client_payload.changed_files }}" ]; then
- echo "Error: repository_dispatch payload missing required 'changed_files' field" >&2
- exit 1
- fi
- echo "CHANGED_FILES=${{ github.event.client_payload.changed_files }}" >> $GITHUB_OUTPUT
- echo "TARGET_LANGS=" >> $GITHUB_OUTPUT
- echo "SYNC_MODE=${{ github.event.client_payload.sync_mode || 'incremental' }}" >> $GITHUB_OUTPUT
+ generate_changes_json() {
+ node .github/scripts/generate-i18n-changes.mjs
+ }
- # Decode the base64-encoded diff from the trigger workflow
- if [ -n "${{ github.event.client_payload.diff_base64 }}" ]; then
- if ! echo "${{ github.event.client_payload.diff_base64 }}" | base64 -d > /tmp/i18n-diff.txt 2>&1; then
- echo "Warning: Failed to decode base64 diff payload" >&2
- echo "" > /tmp/i18n-diff.txt
- echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
- elif [ -s /tmp/i18n-diff.txt ]; then
- echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
- else
- echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
- fi
+ if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
+ BASE_SHA="${{ github.event.client_payload.base_sha }}"
+ HEAD_SHA="${{ github.event.client_payload.head_sha }}"
+ CHANGED_FILES="${{ github.event.client_payload.changed_files }}"
+ TARGET_LANGS="$DEFAULT_TARGET_LANGS"
+ SYNC_MODE="${{ github.event.client_payload.sync_mode || 'incremental' }}"
+
+ if [ -n "${{ github.event.client_payload.changes_base64 }}" ]; then
+ printf '%s' '${{ github.event.client_payload.changes_base64 }}' | base64 -d > /tmp/i18n-changes.json
+ CHANGES_AVAILABLE="true"
+ CHANGES_SOURCE="embedded"
+ elif [ -n "$BASE_SHA" ] && [ -n "$CHANGED_FILES" ]; then
+ export BASE_SHA HEAD_SHA CHANGED_FILES
+ generate_changes_json
+ CHANGES_AVAILABLE="true"
+ CHANGES_SOURCE="recomputed"
else
- echo "" > /tmp/i18n-diff.txt
- echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
+ printf '%s' '{"baseSha":"","headSha":"","files":[],"changes":{}}' > /tmp/i18n-changes.json
+ CHANGES_AVAILABLE="false"
+ CHANGES_SOURCE="unavailable"
fi
else
- echo "Unsupported event type: ${{ github.event_name }}"
- exit 1
+ BASE_SHA=""
+ HEAD_SHA=$(git rev-parse HEAD)
+ if [ -n "${{ github.event.inputs.languages }}" ]; then
+ TARGET_LANGS="${{ github.event.inputs.languages }}"
+ else
+ TARGET_LANGS="$DEFAULT_TARGET_LANGS"
+ fi
+ SYNC_MODE="${{ github.event.inputs.mode || 'incremental' }}"
+ if [ -n "${{ github.event.inputs.files }}" ]; then
+ CHANGED_FILES="${{ github.event.inputs.files }}"
+ elif [ "$SYNC_MODE" = "incremental" ]; then
+ BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || true)
+ if [ -n "$BASE_SHA" ]; then
+ CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
+ else
+ CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//')
+ fi
+ elif [ "$SYNC_MODE" = "full" ]; then
+ echo "workflow_dispatch full mode requires the files input to stay within CI limits." >&2
+ exit 1
+ else
+ CHANGED_FILES=""
+ fi
+
+ if [ "$SYNC_MODE" = "incremental" ] && [ -n "$CHANGED_FILES" ]; then
+ export BASE_SHA HEAD_SHA CHANGED_FILES
+ generate_changes_json
+ CHANGES_AVAILABLE="true"
+ CHANGES_SOURCE="local"
+ else
+ printf '%s' '{"baseSha":"","headSha":"","files":[],"changes":{}}' > /tmp/i18n-changes.json
+ CHANGES_AVAILABLE="false"
+ CHANGES_SOURCE="unavailable"
+ fi
fi
- # Truncate diff if too large (keep first 50KB)
- if [ -f /tmp/i18n-diff.txt ]; then
- head -c 50000 /tmp/i18n-diff.txt > /tmp/i18n-diff-truncated.txt
- mv /tmp/i18n-diff-truncated.txt /tmp/i18n-diff.txt
+ FILE_ARGS=""
+ if [ -n "$CHANGED_FILES" ]; then
+ FILE_ARGS="--file $CHANGED_FILES"
fi
- echo "Detected files: $(cat $GITHUB_OUTPUT | grep CHANGED_FILES || echo 'none')"
+ LANG_ARGS=""
+ if [ -n "$TARGET_LANGS" ]; then
+ LANG_ARGS="--lang $TARGET_LANGS"
+ fi
+
+ {
+ echo "DEFAULT_TARGET_LANGS=$DEFAULT_TARGET_LANGS"
+ echo "BASE_SHA=$BASE_SHA"
+ echo "HEAD_SHA=$HEAD_SHA"
+ echo "CHANGED_FILES=$CHANGED_FILES"
+ echo "TARGET_LANGS=$TARGET_LANGS"
+ echo "SYNC_MODE=$SYNC_MODE"
+ echo "CHANGES_AVAILABLE=$CHANGES_AVAILABLE"
+ echo "CHANGES_SOURCE=$CHANGES_SOURCE"
+ echo "FILE_ARGS=$FILE_ARGS"
+ echo "LANG_ARGS=$LANG_ARGS"
+ } >> "$GITHUB_OUTPUT"
+
+ echo "Files: ${CHANGED_FILES:-}"
+ echo "Languages: ${TARGET_LANGS:-}"
+ echo "Mode: $SYNC_MODE"
- name: Run Claude Code for Translation Sync
- if: steps.detect_changes.outputs.CHANGED_FILES != ''
- uses: anthropics/claude-code-action@ff9acae5886d41a99ed4ec14b7dc147d55834722 # v1.0.77
+ if: steps.context.outputs.CHANGED_FILES != ''
+ uses: anthropics/claude-code-action@476e359e6203e73dad705c8b322e333fabbd7416 # v1.0.119
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
- # Allow github-actions bot to trigger this workflow via repository_dispatch
- # See: https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
allowed_bots: 'github-actions[bot]'
+ show_full_output: ${{ github.event_name == 'workflow_dispatch' }}
prompt: |
- You are a professional i18n synchronization engineer for the Dify project.
- Your task is to keep all language translations in sync with the English source (en-US).
+ You are the i18n sync agent for the Dify repository.
+ Your job is to keep translations synchronized with the English source files under `${{ github.workspace }}/web/i18n/en-US/`.
- ## CRITICAL TOOL RESTRICTIONS
- - Use **Read** tool to read files (NOT cat or bash)
- - Use **Edit** tool to modify JSON files (NOT node, jq, or bash scripts)
- - Use **Bash** ONLY for: git commands, gh commands, pnpm commands
- - Run bash commands ONE BY ONE, never combine with && or ||
- - NEVER use `$()` command substitution - it's not supported. Split into separate commands instead.
+ Use absolute paths at all times:
+ - Repo root: `${{ github.workspace }}`
+ - Web directory: `${{ github.workspace }}/web`
+ - Language config: `${{ github.workspace }}/web/i18n-config/languages.ts`
- ## WORKING DIRECTORY & ABSOLUTE PATHS
- Claude Code sandbox working directory may vary. Always use absolute paths:
- - For pnpm: `pnpm --dir ${{ github.workspace }}/web `
- - For git: `git -C ${{ github.workspace }} `
- - For gh: `gh --repo ${{ github.repository }} `
- - For file paths: `${{ github.workspace }}/web/i18n/`
+ Inputs:
+ - Files in scope: `${{ steps.context.outputs.CHANGED_FILES }}`
+ - Target languages: `${{ steps.context.outputs.TARGET_LANGS }}`
+ - Sync mode: `${{ steps.context.outputs.SYNC_MODE }}`
+ - Base SHA: `${{ steps.context.outputs.BASE_SHA }}`
+ - Head SHA: `${{ steps.context.outputs.HEAD_SHA }}`
+ - Scoped file args: `${{ steps.context.outputs.FILE_ARGS }}`
+ - Scoped language args: `${{ steps.context.outputs.LANG_ARGS }}`
+ - Structured change set available: `${{ steps.context.outputs.CHANGES_AVAILABLE }}`
+ - Structured change set source: `${{ steps.context.outputs.CHANGES_SOURCE }}`
+ - Structured change set file: `/tmp/i18n-changes.json`
- ## EFFICIENCY RULES
- - **ONE Edit per language file** - batch all key additions into a single Edit
- - Insert new keys at the beginning of JSON (after `{`), lint:fix will sort them
- - Translate ALL keys for a language mentally first, then do ONE Edit
-
- ## Context
- - Changed/target files: ${{ steps.detect_changes.outputs.CHANGED_FILES }}
- - Target languages (empty means all supported): ${{ steps.detect_changes.outputs.TARGET_LANGS }}
- - Sync mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}
- - Translation files are located in: ${{ github.workspace }}/web/i18n/{locale}/{filename}.json
- - Language configuration is in: ${{ github.workspace }}/web/i18n-config/languages.ts
- - Git diff is available: ${{ steps.detect_changes.outputs.DIFF_AVAILABLE }}
-
- ## CRITICAL DESIGN: Verify First, Then Sync
-
- You MUST follow this three-phase approach:
-
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- โ PHASE 1: VERIFY - Analyze and Generate Change Report โ
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-
- ### Step 1.1: Analyze Git Diff (for incremental mode)
- Use the Read tool to read `/tmp/i18n-diff.txt` to see the git diff.
-
- Parse the diff to categorize changes:
- - Lines with `+` (not `+++`): Added or modified values
- - Lines with `-` (not `---`): Removed or old values
- - Identify specific keys for each category:
- * ADD: Keys that appear only in `+` lines (new keys)
- * UPDATE: Keys that appear in both `-` and `+` lines (value changed)
- * DELETE: Keys that appear only in `-` lines (removed keys)
-
- ### Step 1.2: Read Language Configuration
- Use the Read tool to read `${{ github.workspace }}/web/i18n-config/languages.ts`.
- Extract all languages with `supported: true`.
-
- ### Step 1.3: Run i18n:check for Each Language
- ```bash
- pnpm --dir ${{ github.workspace }}/web install --frozen-lockfile
- ```
- ```bash
- pnpm --dir ${{ github.workspace }}/web run i18n:check
- ```
-
- This will report:
- - Missing keys (need to ADD)
- - Extra keys (need to DELETE)
-
- ### Step 1.4: Generate Change Report
-
- Create a structured report identifying:
- ```
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- โ I18N SYNC CHANGE REPORT โ
- โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
- โ Files to process: [list] โ
- โ Languages to sync: [list] โ
- โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
- โ ADD (New Keys): โ
- โ - [filename].[key]: "English value" โ
- โ ... โ
- โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
- โ UPDATE (Modified Keys - MUST re-translate): โ
- โ - [filename].[key]: "Old value" โ "New value" โ
- โ ... โ
- โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
- โ DELETE (Extra Keys): โ
- โ - [language]/[filename].[key] โ
- โ ... โ
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- ```
-
- **IMPORTANT**: For UPDATE detection, compare git diff to find keys where
- the English value changed. These MUST be re-translated even if target
- language already has a translation (it's now stale!).
-
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- โ PHASE 2: SYNC - Execute Changes Based on Report โ
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-
- ### Step 2.1: Process ADD Operations (BATCH per language file)
-
- **CRITICAL WORKFLOW for efficiency:**
- 1. First, translate ALL new keys for ALL languages mentally
- 2. Then, for EACH language file, do ONE Edit operation:
- - Read the file once
- - Insert ALL new keys at the beginning (right after the opening `{`)
- - Don't worry about alphabetical order - lint:fix will sort them later
-
- Example Edit (adding 3 keys to zh-Hans/app.json):
- ```
- old_string: '{\n "accessControl"'
- new_string: '{\n "newKey1": "translation1",\n "newKey2": "translation2",\n "newKey3": "translation3",\n "accessControl"'
- ```
-
- **IMPORTANT**:
- - ONE Edit per language file (not one Edit per key!)
- - Always use the Edit tool. NEVER use bash scripts, node, or jq.
-
- ### Step 2.2: Process UPDATE Operations
-
- **IMPORTANT: Special handling for zh-Hans and ja-JP**
- If zh-Hans or ja-JP files were ALSO modified in the same push:
- - Run: `git -C ${{ github.workspace }} diff HEAD~1 --name-only` and check for zh-Hans or ja-JP files
- - If found, it means someone manually translated them. Apply these rules:
-
- 1. **Missing keys**: Still ADD them (completeness required)
- 2. **Existing translations**: Compare with the NEW English value:
- - If translation is **completely wrong** or **unrelated** โ Update it
- - If translation is **roughly correct** (captures the meaning) โ Keep it, respect manual work
- - When in doubt, **keep the manual translation**
-
- Example:
- - English changed: "Save" โ "Save Changes"
- - Manual translation: "ไฟๅญๆดๆน" โ Keep it (correct meaning)
- - Manual translation: "ๅ ้ค" โ Update it (completely wrong)
-
- For other languages:
- Use Edit tool to replace the old value with the new translation.
- You can batch multiple updates in one Edit if they are adjacent.
-
- ### Step 2.3: Process DELETE Operations
- For extra keys reported by i18n:check:
- - Run: `pnpm --dir ${{ github.workspace }}/web run i18n:check --auto-remove`
- - Or manually remove from target language JSON files
-
- ## Translation Guidelines
-
- - PRESERVE all placeholders exactly as-is:
- - `{{variable}}` - Mustache interpolation
- - `${variable}` - Template literal
- - `content` - HTML tags
- - `_one`, `_other` - Pluralization suffixes (these are KEY suffixes, not values)
-
- **CRITICAL: Variable names and tag names MUST stay in English - NEVER translate them**
-
- โ
CORRECT examples:
- - English: "{{count}} items" โ Japanese: "{{count}} ๅใฎใขใคใใ "
- - English: "{{name}} updated" โ Korean: "{{name}} ์
๋ฐ์ดํธ๋จ"
- - English: "{{email}}" โ Chinese: "{{email}}"
- - English: "Marketplace" โ Japanese: "ใใผใฑใใใใฌใคใน"
-
- โ WRONG examples (NEVER do this - will break the application):
- - "{{count}}" โ "{{ใซใฆใณใ}}" โ (variable name translated to Japanese)
- - "{{name}}" โ "{{์ด๋ฆ}}" โ (variable name translated to Korean)
- - "{{email}}" โ "{{้ฎ็ฎฑ}}" โ (variable name translated to Chinese)
- - "" โ "<ใกใผใซ>" โ (tag name translated)
- - "" โ "<่ชๅฎไน้พๆฅ>" โ (component name translated)
-
- - Use appropriate language register (formal/informal) based on existing translations
- - Match existing translation style in each language
- - Technical terms: check existing conventions per language
- - For CJK languages: no spaces between characters unless necessary
- - For RTL languages (ar-TN, fa-IR): ensure proper text handling
-
- ## Output Format Requirements
- - Alphabetical key ordering (if original file uses it)
- - 2-space indentation
- - Trailing newline at end of file
- - Valid JSON (use proper escaping for special characters)
-
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- โ PHASE 3: RE-VERIFY - Confirm All Issues Resolved โ
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-
- ### Step 3.1: Run Lint Fix (IMPORTANT!)
- ```bash
- pnpm --dir ${{ github.workspace }}/web lint:fix --quiet -- 'i18n/**/*.json'
- ```
- This ensures:
- - JSON keys are sorted alphabetically (jsonc/sort-keys rule)
- - Valid i18n keys (dify-i18n/valid-i18n-keys rule)
- - No extra keys (dify-i18n/no-extra-keys rule)
-
- ### Step 3.2: Run Final i18n Check
- ```bash
- pnpm --dir ${{ github.workspace }}/web run i18n:check
- ```
-
- ### Step 3.3: Fix Any Remaining Issues
- If check reports issues:
- - Go back to PHASE 2 for unresolved items
- - Repeat until check passes
-
- ### Step 3.4: Generate Final Summary
- ```
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- โ SYNC COMPLETED SUMMARY โ
- โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
- โ Language โ Added โ Updated โ Deleted โ Status โ
- โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
- โ zh-Hans โ 5 โ 2 โ 1 โ โ Complete โ
- โ ja-JP โ 5 โ 2 โ 1 โ โ Complete โ
- โ ... โ ... โ ... โ ... โ ... โ
- โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
- โ i18n:check โ PASSED - All keys in sync โ
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- ```
-
- ## Mode-Specific Behavior
-
- **SYNC_MODE = "incremental"** (default):
- - Focus on keys identified from git diff
- - Also check i18n:check output for any missing/extra keys
- - Efficient for small changes
-
- **SYNC_MODE = "full"**:
- - Compare ALL keys between en-US and each language
- - Run i18n:check to identify all discrepancies
- - Use for first-time sync or fixing historical issues
-
- ## Important Notes
-
- 1. Always run i18n:check BEFORE and AFTER making changes
- 2. The check script is the source of truth for missing/extra keys
- 3. For UPDATE scenario: git diff is the source of truth for changed values
- 4. Create a single commit with all translation changes
- 5. If any translation fails, continue with others and report failures
-
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- โ PHASE 4: COMMIT AND CREATE PR โ
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-
- After all translations are complete and verified:
-
- ### Step 4.1: Check for changes
- ```bash
- git -C ${{ github.workspace }} status --porcelain
- ```
-
- If there are changes:
-
- ### Step 4.2: Create a new branch and commit
- Run these git commands ONE BY ONE (not combined with &&).
- **IMPORTANT**: Do NOT use `$()` command substitution. Use two separate commands:
-
- 1. First, get the timestamp:
- ```bash
- date +%Y%m%d-%H%M%S
- ```
- (Note the output, e.g., "20260115-143052")
-
- 2. Then create branch using the timestamp value:
- ```bash
- git -C ${{ github.workspace }} checkout -b chore/i18n-sync-20260115-143052
- ```
- (Replace "20260115-143052" with the actual timestamp from step 1)
-
- 3. Stage changes:
- ```bash
- git -C ${{ github.workspace }} add web/i18n/
- ```
-
- 4. Commit:
- ```bash
- git -C ${{ github.workspace }} commit -m "chore(i18n): sync translations with en-US - Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}"
- ```
-
- 5. Push:
- ```bash
- git -C ${{ github.workspace }} push origin HEAD
- ```
-
- ### Step 4.3: Create Pull Request
- ```bash
- gh pr create --repo ${{ github.repository }} --title "chore(i18n): sync translations with en-US" --body "## Summary
-
- This PR was automatically generated to sync i18n translation files.
-
- ### Changes
- - Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}
- - Files processed: ${{ steps.detect_changes.outputs.CHANGED_FILES }}
-
- ### Verification
- - [x] \`i18n:check\` passed
- - [x] \`lint:fix\` applied
-
- ๐ค Generated with Claude Code GitHub Action" --base main
- ```
+ Tool rules:
+ - Use Read for repository files.
+ - Use Edit for JSON updates.
+ - Use Bash only for `vp`.
+ - Do not use Bash for `git`, `gh`, or branch management.
+ Required execution plan:
+ 1. Resolve target languages.
+ - Use the provided `Target languages` value as the source of truth.
+ - If it is unexpectedly empty, read `${{ github.workspace }}/web/i18n-config/languages.ts` and use every language with `supported: true` except `en-US`.
+ 2. Stay strictly in scope.
+ - Only process the files listed in `Files in scope`.
+ - Only process the resolved target languages, never `en-US`.
+ - Do not touch unrelated i18n files.
+ - Do not modify `${{ github.workspace }}/web/i18n/en-US/`.
+ 3. Resolve source changes.
+ - If `Structured change set available` is `true`, read `/tmp/i18n-changes.json` and use it as the source of truth for file-level and key-level changes.
+ - For each file entry:
+ - `added` contains new English keys that need translations.
+ - `updated` contains stale keys whose English source changed; re-translate using the `after` value.
+ - `deleted` contains keys that should be removed from locale files.
+ - `fileDeleted: true` means the English file no longer exists; remove the matching locale file if present.
+ - Read the current English JSON file for any file that still exists so wording, placeholders, and surrounding terminology stay accurate.
+ - If `Structured change set available` is `false`, treat this as a scoped full sync and use the current English files plus scoped checks as the source of truth.
+ 4. Run a scoped pre-check before editing:
+ - `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}`
+ - Use this command as the source of truth for missing and extra keys inside the current scope.
+ 5. Apply translations.
+ - For every target language and scoped file:
+ - If `fileDeleted` is `true`, remove the locale file if it exists and skip the rest of that file.
+ - If the locale file does not exist yet, create it with `Write` and then continue with `Edit` as needed.
+ - ADD missing keys.
+ - UPDATE stale translations when the English value changed.
+ - DELETE removed keys. Prefer `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }} --auto-remove` for extra keys so deletions stay in scope.
+ - Preserve placeholders exactly: `{{variable}}`, `${variable}`, HTML tags, component tags, and variable names.
+ - Match the existing terminology and register used by each locale.
+ - Prefer one Edit per file when stable, but prioritize correctness over batching.
+ 6. Verify only the edited files.
+ - Run `vp run dify-web#lint:fix --quiet -- `
+ - Run `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}`
+ - If verification fails, fix the remaining problems before continuing.
+ 7. Stop after the scoped locale files are updated and verification passes.
+ - Do not create branches, commits, or pull requests.
claude_args: |
- --max-turns 150
- --allowedTools "Read,Write,Edit,Bash(git *),Bash(git:*),Bash(gh *),Bash(gh:*),Bash(pnpm *),Bash(pnpm:*),Bash(date *),Bash(date:*),Glob,Grep"
+ --max-turns 120
+ --allowedTools "Read,Write,Edit,Bash(vp *),Bash(vp:*),Glob,Grep"
+
+ - name: Prepare branch metadata
+ id: pr_meta
+ if: steps.context.outputs.CHANGED_FILES != ''
+ shell: bash
+ run: |
+ if [ -z "$(git -C "${{ github.workspace }}" status --porcelain -- web/i18n/)" ]; then
+ echo "has_changes=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ SCOPE_HASH=$(printf '%s|%s|%s' "${{ steps.context.outputs.CHANGED_FILES }}" "${{ steps.context.outputs.TARGET_LANGS }}" "${{ steps.context.outputs.SYNC_MODE }}" | sha256sum | cut -c1-8)
+ HEAD_SHORT=$(printf '%s' "${{ steps.context.outputs.HEAD_SHA }}" | cut -c1-12)
+ BRANCH_NAME="chore/i18n-sync-${HEAD_SHORT}-${SCOPE_HASH}"
+
+ {
+ echo "has_changes=true"
+ echo "branch_name=$BRANCH_NAME"
+ } >> "$GITHUB_OUTPUT"
+
+ - name: Commit translation changes
+ if: steps.pr_meta.outputs.has_changes == 'true'
+ shell: bash
+ run: |
+ git -C "${{ github.workspace }}" checkout -B "${{ steps.pr_meta.outputs.branch_name }}"
+ git -C "${{ github.workspace }}" add web/i18n/
+ git -C "${{ github.workspace }}" commit -m "chore(i18n): sync translations with en-US"
+
+ - name: Push translation branch
+ if: steps.pr_meta.outputs.has_changes == 'true'
+ shell: bash
+ run: |
+ if git -C "${{ github.workspace }}" ls-remote --exit-code --heads origin "${{ steps.pr_meta.outputs.branch_name }}" >/dev/null 2>&1; then
+ git -C "${{ github.workspace }}" push --force-with-lease origin "${{ steps.pr_meta.outputs.branch_name }}"
+ else
+ git -C "${{ github.workspace }}" push --set-upstream origin "${{ steps.pr_meta.outputs.branch_name }}"
+ fi
+
+ - name: Create or update translation PR
+ if: steps.pr_meta.outputs.has_changes == 'true'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ BRANCH_NAME: ${{ steps.pr_meta.outputs.branch_name }}
+ FILES_IN_SCOPE: ${{ steps.context.outputs.CHANGED_FILES }}
+ TARGET_LANGS: ${{ steps.context.outputs.TARGET_LANGS }}
+ SYNC_MODE: ${{ steps.context.outputs.SYNC_MODE }}
+ CHANGES_SOURCE: ${{ steps.context.outputs.CHANGES_SOURCE }}
+ BASE_SHA: ${{ steps.context.outputs.BASE_SHA }}
+ HEAD_SHA: ${{ steps.context.outputs.HEAD_SHA }}
+ REPO_NAME: ${{ github.repository }}
+ shell: bash
+ run: |
+ PR_BODY_FILE=/tmp/i18n-pr-body.md
+ LANG_COUNT=$(printf '%s\n' "$TARGET_LANGS" | wc -w | tr -d ' ')
+ if [ "$LANG_COUNT" = "0" ]; then
+ LANG_COUNT="0"
+ fi
+ export LANG_COUNT
+
+ node <<'NODE' > "$PR_BODY_FILE"
+ const fs = require('node:fs')
+
+ const changesPath = '/tmp/i18n-changes.json'
+ const changes = fs.existsSync(changesPath)
+ ? JSON.parse(fs.readFileSync(changesPath, 'utf8'))
+ : { changes: {} }
+
+ const filesInScope = (process.env.FILES_IN_SCOPE || '').split(/\s+/).filter(Boolean)
+ const lines = [
+ '## Summary',
+ '',
+ `- **Files synced**: \`${process.env.FILES_IN_SCOPE || ''}\``,
+ `- **Languages updated**: ${process.env.TARGET_LANGS || ''} (${process.env.LANG_COUNT} languages)`,
+ `- **Sync mode**: ${process.env.SYNC_MODE}${process.env.BASE_SHA ? ` (base: \`${process.env.BASE_SHA.slice(0, 10)}\`, head: \`${process.env.HEAD_SHA.slice(0, 10)}\`)` : ` (head: \`${process.env.HEAD_SHA.slice(0, 10)}\`)`}`,
+ '',
+ '### Key changes',
+ ]
+
+ for (const fileName of filesInScope) {
+ const fileChange = changes.changes?.[fileName] || { added: {}, updated: {}, deleted: [], fileDeleted: false }
+ const addedKeys = Object.keys(fileChange.added || {})
+ const updatedKeys = Object.keys(fileChange.updated || {})
+ const deletedKeys = fileChange.deleted || []
+ lines.push(`- \`${fileName}\`: +${addedKeys.length} / ~${updatedKeys.length} / -${deletedKeys.length}${fileChange.fileDeleted ? ' (file deleted in en-US)' : ''}`)
+ }
+
+ lines.push(
+ '',
+ '## Verification',
+ '',
+ `- \`vp run dify-web#i18n:check --file ${process.env.FILES_IN_SCOPE} --lang ${process.env.TARGET_LANGS}\``,
+ `- \`vp run dify-web#lint:fix --quiet -- \``,
+ '',
+ '## Notes',
+ '',
+ '- This PR was generated from structured en-US key changes produced by `trigger-i18n-sync.yml`.',
+ `- Structured change source: ${process.env.CHANGES_SOURCE || 'unknown'}.`,
+ '- Branch name is deterministic for the head SHA and scope, so reruns update the same PR instead of opening duplicates.',
+ '',
+ '๐ค Generated with [Claude Code](https://claude.com/claude-code)'
+ )
+
+ process.stdout.write(lines.join('\n'))
+ NODE
+
+ EXISTING_PR_NUMBER=$(gh pr list --repo "$REPO_NAME" --head "$BRANCH_NAME" --state open --json number --jq '.[0].number')
+
+ if [ -n "$EXISTING_PR_NUMBER" ] && [ "$EXISTING_PR_NUMBER" != "null" ]; then
+ gh pr edit "$EXISTING_PR_NUMBER" --repo "$REPO_NAME" --title "chore(i18n): sync translations with en-US" --body-file "$PR_BODY_FILE"
+ else
+ gh pr create --repo "$REPO_NAME" --head "$BRANCH_NAME" --base main --title "chore(i18n): sync translations with en-US" --body-file "$PR_BODY_FILE"
+ fi
diff --git a/.github/workflows/trigger-i18n-sync.yml b/.github/workflows/trigger-i18n-sync.yml
index 1caaddd47a..87c88e2023 100644
--- a/.github/workflows/trigger-i18n-sync.yml
+++ b/.github/workflows/trigger-i18n-sync.yml
@@ -1,9 +1,5 @@
name: Trigger i18n Sync on Push
-# This workflow bridges the push event to repository_dispatch
-# because claude-code-action doesn't support push events directly.
-# See: https://github.com/langgenius/dify/issues/30743
-
on:
push:
branches: [main]
@@ -13,10 +9,14 @@ on:
permissions:
contents: write
+concurrency:
+ group: trigger-i18n-sync-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
trigger:
if: github.repository == 'langgenius/dify'
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
timeout-minutes: 5
steps:
@@ -25,42 +25,66 @@ jobs:
with:
fetch-depth: 0
- - name: Detect changed files and generate diff
+ - name: Detect changed files and build structured change set
id: detect
+ shell: bash
run: |
- BEFORE_SHA="${{ github.event.before }}"
- # Handle edge case: force push may have null/zero SHA
- if [ -z "$BEFORE_SHA" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
- BEFORE_SHA="HEAD~1"
+ BASE_SHA="${{ github.event.before }}"
+ if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
+ BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || true)
fi
+ HEAD_SHA="${{ github.sha }}"
- # Detect changed i18n files
- changed=$(git diff --name-only "$BEFORE_SHA" "${{ github.sha }}" -- 'web/i18n/en-US/*.json' 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/.json$//' | tr '\n' ' ' || echo "")
- echo "changed_files=$changed" >> $GITHUB_OUTPUT
-
- # Generate diff for context
- git diff "$BEFORE_SHA" "${{ github.sha }}" -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
-
- # Truncate if too large (keep first 50KB to match receiving workflow)
- head -c 50000 /tmp/i18n-diff.txt > /tmp/i18n-diff-truncated.txt
- mv /tmp/i18n-diff-truncated.txt /tmp/i18n-diff.txt
-
- # Base64 encode the diff for safe JSON transport (portable, single-line)
- diff_base64=$(base64 < /tmp/i18n-diff.txt | tr -d '\n')
- echo "diff_base64=$diff_base64" >> $GITHUB_OUTPUT
-
- if [ -n "$changed" ]; then
- echo "has_changes=true" >> $GITHUB_OUTPUT
- echo "Detected changed files: $changed"
+ if [ -n "$BASE_SHA" ]; then
+ CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
else
- echo "has_changes=false" >> $GITHUB_OUTPUT
- echo "No i18n changes detected"
+ CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//')
fi
+ export BASE_SHA HEAD_SHA CHANGED_FILES
+ node .github/scripts/generate-i18n-changes.mjs
+
+ if [ -n "$CHANGED_FILES" ]; then
+ echo "has_changes=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "has_changes=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
+ echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
+ echo "changed_files=$CHANGED_FILES" >> "$GITHUB_OUTPUT"
+
- name: Trigger i18n sync workflow
if: steps.detect.outputs.has_changes == 'true'
- uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ BASE_SHA: ${{ steps.detect.outputs.base_sha }}
+ HEAD_SHA: ${{ steps.detect.outputs.head_sha }}
+ CHANGED_FILES: ${{ steps.detect.outputs.changed_files }}
with:
- token: ${{ secrets.GITHUB_TOKEN }}
- event-type: i18n-sync
- client-payload: '{"changed_files": "${{ steps.detect.outputs.changed_files }}", "diff_base64": "${{ steps.detect.outputs.diff_base64 }}", "sync_mode": "incremental", "trigger_sha": "${{ github.sha }}"}'
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const fs = require('fs')
+
+ const changesJson = fs.readFileSync('/tmp/i18n-changes.json', 'utf8')
+ const changesBase64 = Buffer.from(changesJson).toString('base64')
+ const maxEmbeddedChangesChars = 48000
+ const changesEmbedded = changesBase64.length <= maxEmbeddedChangesChars
+
+ if (!changesEmbedded) {
+ console.log(`Structured change set too large to embed safely (${changesBase64.length} chars). Downstream workflow will regenerate it from git history.`)
+ }
+
+ await github.rest.repos.createDispatchEvent({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ event_type: 'i18n-sync',
+ client_payload: {
+ changed_files: process.env.CHANGED_FILES,
+ changes_base64: changesEmbedded ? changesBase64 : '',
+ changes_embedded: changesEmbedded,
+ sync_mode: 'incremental',
+ base_sha: process.env.BASE_SHA,
+ head_sha: process.env.HEAD_SHA,
+ },
+ })
diff --git a/.github/workflows/vdb-tests-full.yml b/.github/workflows/vdb-tests-full.yml
new file mode 100644
index 0000000000..1405eb4eeb
--- /dev/null
+++ b/.github/workflows/vdb-tests-full.yml
@@ -0,0 +1,95 @@
+name: Run Full VDB Tests
+
+on:
+ schedule:
+ - cron: '0 3 * * 1'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: vdb-tests-full-${{ github.ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ name: Full VDB Tests
+ if: github.repository == 'langgenius/dify'
+ runs-on: depot-ubuntu-24.04
+ strategy:
+ matrix:
+ python-version:
+ - "3.12"
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Free Disk Space
+ uses: endersonmenezes/free-disk-space@7901478139cff6e9d44df5972fd8ab8fcade4db1 # v3.2.2
+ with:
+ remove_dotnet: true
+ remove_haskell: true
+ remove_tool_cache: true
+
+ - name: Setup UV and Python
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ enable-cache: true
+ python-version: ${{ matrix.python-version }}
+ cache-dependency-glob: api/uv.lock
+
+ - name: Check UV lockfile
+ run: uv lock --project api --check
+
+ - name: Install dependencies
+ run: uv sync --project api --dev
+
+ - name: Set up dotenvs
+ run: |
+ cp docker/.env.example docker/.env
+ cp docker/envs/middleware.env.example docker/middleware.env
+
+ - name: Expose Service Ports
+ run: sh .github/workflows/expose_service_ports.sh
+
+# - name: Set up Vector Store (TiDB)
+# uses: hoverkraft-tech/compose-action@v2.0.2
+# with:
+# compose-file: docker/tidb/docker-compose.yaml
+# services: |
+# tidb
+# tiflash
+
+ - name: Set up Full Vector Store Matrix
+ uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
+ with:
+ compose-file: |
+ docker/docker-compose.yaml
+ services: |
+ weaviate
+ qdrant
+ couchbase-server
+ etcd
+ minio
+ milvus-standalone
+ pgvecto-rs
+ pgvector
+ chroma
+ elasticsearch
+ oceanbase
+
+ - name: setup test config
+ run: |
+ echo $(pwd)
+ ls -lah .
+ cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
+
+# - name: Check VDB Ready (TiDB)
+# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
+
+ - name: Test Vector Stores
+ run: uv run --project api bash dev/pytest/pytest_vdb.sh
diff --git a/.github/workflows/vdb-tests.yml b/.github/workflows/vdb-tests.yml
index f45f2137d6..cdcdcb27d7 100644
--- a/.github/workflows/vdb-tests.yml
+++ b/.github/workflows/vdb-tests.yml
@@ -1,20 +1,22 @@
-name: Run VDB Tests
+name: Run VDB Smoke Tests
on:
workflow_call:
+permissions:
+ contents: read
+
concurrency:
group: vdb-tests-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
- name: VDB Tests
- runs-on: ubuntu-latest
+ name: VDB Smoke Tests
+ runs-on: depot-ubuntu-24.04
strategy:
matrix:
python-version:
- - "3.11"
- "3.12"
steps:
@@ -31,7 +33,7 @@ jobs:
remove_tool_cache: true
- name: Setup UV and Python
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
@@ -46,7 +48,7 @@ jobs:
- name: Set up dotenvs
run: |
cp docker/.env.example docker/.env
- cp docker/middleware.env.example docker/middleware.env
+ cp docker/envs/middleware.env.example docker/middleware.env
- name: Expose Service Ports
run: sh .github/workflows/expose_service_ports.sh
@@ -59,23 +61,18 @@ jobs:
# tidb
# tiflash
- - name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase, OceanBase)
- uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
+ - name: Set up Vector Stores for Smoke Coverage
+ uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
with:
compose-file: |
docker/docker-compose.yaml
services: |
+ db_postgres
+ redis
weaviate
qdrant
- couchbase-server
- etcd
- minio
- milvus-standalone
- pgvecto-rs
pgvector
chroma
- elasticsearch
- oceanbase
- name: setup test config
run: |
@@ -84,7 +81,12 @@ jobs:
cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
# - name: Check VDB Ready (TiDB)
-# run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
+# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
- name: Test Vector Stores
- run: uv run --project api bash dev/pytest/pytest_vdb.sh
+ run: |
+ uv run --project api pytest --timeout "${PYTEST_TIMEOUT:-180}" \
+ api/providers/vdb/vdb-chroma/tests/integration_tests \
+ api/providers/vdb/vdb-pgvector/tests/integration_tests \
+ api/providers/vdb/vdb-qdrant/tests/integration_tests \
+ api/providers/vdb/vdb-weaviate/tests/integration_tests
diff --git a/.github/workflows/web-e2e.yml b/.github/workflows/web-e2e.yml
new file mode 100644
index 0000000000..bdc24887db
--- /dev/null
+++ b/.github/workflows/web-e2e.yml
@@ -0,0 +1,68 @@
+name: Web Full-Stack E2E
+
+on:
+ workflow_call:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: web-e2e-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ name: Web Full-Stack E2E
+ runs-on: depot-ubuntu-24.04-4
+ defaults:
+ run:
+ shell: bash
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Setup web dependencies
+ uses: ./.github/actions/setup-web
+
+ - name: Setup UV and Python
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ enable-cache: true
+ python-version: "3.12"
+ cache-dependency-glob: api/uv.lock
+
+ - name: Install API dependencies
+ run: uv sync --project api --dev
+
+ - name: Install Playwright browser
+ working-directory: ./e2e
+ run: vp run e2e:install
+
+ - name: Run isolated source-api and built-web Cucumber E2E tests
+ working-directory: ./e2e
+ env:
+ E2E_ADMIN_EMAIL: e2e-admin@example.com
+ E2E_ADMIN_NAME: E2E Admin
+ E2E_ADMIN_PASSWORD: E2eAdmin12345
+ E2E_FORCE_WEB_BUILD: "1"
+ E2E_INIT_PASSWORD: E2eInit12345
+ run: vp run e2e:full
+
+ - name: Upload Cucumber report
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: cucumber-report
+ path: e2e/cucumber-report
+ retention-days: 7
+
+ - name: Upload E2E logs
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: e2e-logs
+ path: e2e/.logs
+ retention-days: 7
diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml
index d40cd4bfeb..4619f3c104 100644
--- a/.github/workflows/web-tests.yml
+++ b/.github/workflows/web-tests.yml
@@ -16,14 +16,14 @@ concurrency:
jobs:
test:
name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04-4
env:
VITEST_COVERAGE_SCOPE: app-components
strategy:
fail-fast: false
matrix:
- shardIndex: [1, 2, 3, 4, 5, 6]
- shardTotal: [6]
+ shardIndex: [1, 2, 3, 4]
+ shardTotal: [4]
defaults:
run:
shell: bash
@@ -43,7 +43,7 @@ jobs:
- name: Upload blob report
if: ${{ !cancelled() }}
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: blob-report-${{ matrix.shardIndex }}
path: web/.vitest-reports/*
@@ -54,7 +54,7 @@ jobs:
name: Merge Test Reports
if: ${{ !cancelled() }}
needs: [test]
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04-4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
defaults:
@@ -66,7 +66,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- fetch-depth: 0
persist-credentials: false
- name: Setup web environment
@@ -84,19 +83,22 @@ jobs:
- name: Report coverage
if: ${{ env.CODECOV_TOKEN != '' }}
- uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
+ uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
directory: web/coverage
flags: web
env:
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
- web-build:
- name: Web Build
- runs-on: ubuntu-latest
+ dify-ui-test:
+ name: dify-ui Tests
+ runs-on: depot-ubuntu-24.04-4
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
defaults:
run:
- working-directory: ./web
+ shell: bash
+ working-directory: ./packages/dify-ui
steps:
- name: Checkout code
@@ -104,20 +106,20 @@ jobs:
with:
persist-credentials: false
- - name: Check changed files
- id: changed-files
- uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
- with:
- files: |
- web/**
- .github/workflows/web-tests.yml
- .github/actions/setup-web/**
-
- name: Setup web environment
- if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/actions/setup-web
- - name: Web build check
- if: steps.changed-files.outputs.any_changed == 'true'
- working-directory: ./web
- run: vp run build
+ - name: Install Chromium for Browser Mode
+ run: vp exec playwright install --with-deps chromium
+
+ - name: Run dify-ui tests
+ run: vp test run --coverage --silent=passed-only
+
+ - name: Report coverage
+ if: ${{ env.CODECOV_TOKEN != '' }}
+ uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
+ with:
+ directory: packages/dify-ui/coverage
+ flags: dify-ui
+ env:
+ CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}