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 }}