langgenius/dify

22 workflows · maturity 83% · 8 patterns · GitHub ↗

Security 13.64/100

Practices

✓ Matrix✓ Permissions○ Security scan✓ AI review✓ Cache✓ Concurrency✓ Reusable workflows

Detected patterns

Security dimensions

permissions
13.6
security scan
0
supply chain
0
secret handling
0
harden runner
0

Workflows (22)

anti-slop perms .github/workflows/anti-slop.yml
Triggers
pull_request_target
Runs on
ubuntu-latest
Jobs
anti-slop
Actions
peakoss/anti-slop
View raw YAML
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"
api-tests matrix perms .github/workflows/api-tests.yml
Triggers
workflow_call
Runs on
ubuntu-latest
Jobs
test
Matrix
python-version→ 3.12
Actions
astral-sh/setup-uv, hoverkraft-tech/compose-action, codecov/codecov-action
Commands
  • uv lock --project api --check
  • uv sync --project api --dev
  • uv run --project api dev/pytest/pytest_config_tests.py
  • cp docker/.env.example docker/.env cp docker/middleware.env.example docker/middleware.env
  • sh .github/workflows/expose_service_ports.sh
  • cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
  • 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
View raw YAML
name: Run Pytest

on:
  workflow_call:
    secrets:
      CODECOV_TOKEN:
        required: false

permissions:
  contents: read

concurrency:
  group: api-tests-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  test:
    name: API Tests
    runs-on: ubuntu-latest
    env:
      CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
    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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.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: Run dify config tests
        run: uv run --project api dev/pytest/pytest_config_tests.py

      - name: Set up dotenvs
        run: |
          cp docker/.env.example docker/.env
          cp docker/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
        with:
          compose-file: |
            docker/docker-compose.middleware.yaml
          services: |
            db_postgres
            redis
            sandbox
            ssrf_proxy

      - name: setup test config
        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
        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

      - name: Report coverage
        if: ${{ env.CODECOV_TOKEN != '' && matrix.python-version == '3.12' }}
        uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
        with:
          files: ./coverage.xml
          disable_search: true
          flags: api
        env:
          CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
autofix perms .github/workflows/autofix.yml
Triggers
pull_request, merge_group, push
Runs on
ubuntu-latest
Jobs
autofix
Actions
tj-actions/changed-files, tj-actions/changed-files, tj-actions/changed-files, astral-sh/setup-uv, autofix-ci/action
Commands
  • echo "autofix.ci updates pull request branches, not merge group refs."
  • cd docker ./generate_docker_compose
  • cd api uv sync --dev # fmt first to avoid line too long uv run ruff format .. # Fix lint errors uv run ruff check --fix . # Format code uv run ruff format ..
  • cd api ./cnt_base.sh
  • # 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 uvx --from ast-grep-cli ast-grep --pattern 'session.query($WHATEVER).filter($HERE)' --rewrite 'session.query($WHATEVER).where($HERE)' -l py --update-all || true uvx --from ast-grep-cli ast-grep -p '$A = db.Column($$$B)' -r '$A = mapped_column($$$B)' -l py --update-all || true uvx --from ast-grep-cli ast-grep -p '$A : $T = db.Column($$$B)' -r '$A : $T = mapped_column($$$B)' -l py --update-all || true # Convert Optional[T] to T | None (ignoring quoted types) cat > /tmp/optional-rule.yml << 'EOF' id: convert-optional-to-union language: python rule: kind: generic_type all: - has: kind: identifier pattern: Optional - has: kind: type_parameter has: kind: type pattern: $T fix: $T | None EOF uvx --from ast-grep-cli ast-grep scan . --inline-rules "$(cat /tmp/optional-rule.yml)" --update-all # Fix forward references that were incorrectly converted (Python doesn't support "Type" | None syntax) find . -name "*.py" -type f -exec sed -i.bak -E 's/"([^"]+)" \| None/Optional["\1"]/g; s/'"'"'([^'"'"']+)'"'"' \| None/Optional['"'"'\1'"'"']/g' {} \; find . -name "*.py.bak" -type f -delete
  • cd web vp exec eslint --concurrency=2 --prune-suppressions --quiet || true
View raw YAML
name: autofix.ci
on:
  pull_request:
    branches: ["main"]
  merge_group:
    branches: ["main"]
    types: [checks_requested]
  push:
    branches: ["main"]
permissions:
  contents: read

jobs:
  autofix:
    if: github.repository == 'langgenius/dify'
    runs-on: ubuntu-latest
    steps:
      - 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
        with:
          files: |
            docker/generate_docker_compose
            docker/.env.example
            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
        with:
          files: |
            web/**
      - name: Check api inputs
        if: github.event_name != 'merge_group'
        id: api-changes
        uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
        with:
          files: |
            api/**
      - if: github.event_name != 'merge_group'
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.11"

      - if: github.event_name != 'merge_group'
        uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0

      - name: Generate Docker Compose
        if: github.event_name != 'merge_group' && steps.docker-compose-changes.outputs.any_changed == 'true'
        run: |
          cd docker
          ./generate_docker_compose

      - if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
        run: |
          cd api
          uv sync --dev
          # fmt first to avoid line too long
          uv run ruff format ..
          # Fix lint errors
          uv run ruff check --fix .
          # Format code
          uv run ruff format ..

      - name: count migration progress
        if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
        run: |
          cd api
          ./cnt_base.sh

      - name: ast-grep
        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
          uvx --from ast-grep-cli ast-grep --pattern 'session.query($WHATEVER).filter($HERE)' --rewrite 'session.query($WHATEVER).where($HERE)' -l py --update-all || true
          uvx --from ast-grep-cli ast-grep -p '$A = db.Column($$$B)' -r '$A = mapped_column($$$B)' -l py --update-all || true
          uvx --from ast-grep-cli ast-grep -p '$A : $T = db.Column($$$B)' -r '$A : $T = mapped_column($$$B)' -l py --update-all || true
          # Convert Optional[T] to T | None (ignoring quoted types)
          cat > /tmp/optional-rule.yml << 'EOF'
          id: convert-optional-to-union
          language: python
          rule:
            kind: generic_type
            all:
              - has:
                  kind: identifier
                  pattern: Optional
              - has:
                  kind: type_parameter
                  has:
                    kind: type
                    pattern: $T
          fix: $T | None
          EOF
          uvx --from ast-grep-cli ast-grep scan . --inline-rules "$(cat /tmp/optional-rule.yml)" --update-all
          # Fix forward references that were incorrectly converted (Python doesn't support "Type" | None syntax)
          find . -name "*.py" -type f -exec sed -i.bak -E 's/"([^"]+)" \| None/Optional["\1"]/g; s/'"'"'([^'"'"']+)'"'"' \| None/Optional['"'"'\1'"'"']/g' {} \;
          find . -name "*.py.bak" -type f -delete

      - name: Setup web environment
        if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'
        uses: ./.github/actions/setup-web

      - 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

      - if: github.event_name != 'merge_group'
        uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8 # v1.3.3
build-push matrix .github/workflows/build-push.yml
Triggers
push
Runs on
${{ matrix.platform == 'linux/arm64' && 'arm64_runner' || 'ubuntu-latest' }}, ubuntu-latest
Jobs
build, create-manifest
Matrix
include, include.context, include.image_name_env, include.platform, include.service_name→ DIFY_API_IMAGE_NAME, DIFY_WEB_IMAGE_NAME, api, build-api-amd64, build-api-arm64, build-web-amd64, build-web-arm64, linux/amd64, linux/arm64, merge-api-images, merge-web-images, web
Actions
docker/login-action, docker/setup-qemu-action, docker/setup-buildx-action, docker/metadata-action, docker/build-push-action, docker/login-action, docker/metadata-action
Commands
  • platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
  • mkdir -p /tmp/digests sanitized_digest=${DIGEST#sha256:} touch "/tmp/digests/${sanitized_digest}"
  • docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf "$IMAGE_NAME@sha256:%s " *)
  • docker buildx imagetools inspect "$IMAGE_NAME:$IMAGE_VERSION"
View raw YAML
name: Build and Push API & Web

on:
  push:
    branches:
      - "main"
      - "deploy/**"
      - "build/**"
      - "release/e-*"
      - "hotfix/**"
      - "feat/hitl-backend"
    tags:
      - "*"

concurrency:
  group: build-push-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

env:
  DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
  DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
  DIFY_WEB_IMAGE_NAME: ${{ vars.DIFY_WEB_IMAGE_NAME || 'langgenius/dify-web' }}
  DIFY_API_IMAGE_NAME: ${{ vars.DIFY_API_IMAGE_NAME || 'langgenius/dify-api' }}

jobs:
  build:
    runs-on: ${{ matrix.platform == 'linux/arm64' && 'arm64_runner' || 'ubuntu-latest' }}
    if: github.repository == 'langgenius/dify'
    strategy:
      matrix:
        include:
          - service_name: "build-api-amd64"
            image_name_env: "DIFY_API_IMAGE_NAME"
            context: "api"
            platform: linux/amd64
          - service_name: "build-api-arm64"
            image_name_env: "DIFY_API_IMAGE_NAME"
            context: "api"
            platform: linux/arm64
          - service_name: "build-web-amd64"
            image_name_env: "DIFY_WEB_IMAGE_NAME"
            context: "web"
            platform: linux/amd64
          - service_name: "build-web-arm64"
            image_name_env: "DIFY_WEB_IMAGE_NAME"
            context: "web"
            platform: linux/arm64

    steps:
      - name: Prepare
        run: |
          platform=${{ matrix.platform }}
          echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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: Extract metadata for Docker
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
        with:
          images: ${{ env[matrix.image_name_env] }}

      - name: Build Docker image
        id: build
        uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
        with:
          context: "{{defaultContext}}:${{ matrix.context }}"
          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:
          DIGEST: ${{ steps.build.outputs.digest }}
        run: |
          mkdir -p /tmp/digests
          sanitized_digest=${DIGEST#sha256:}
          touch "/tmp/digests/${sanitized_digest}"

      - name: Upload digest
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: digests-${{ matrix.context }}-${{ env.PLATFORM_PAIR }}
          path: /tmp/digests/*
          if-no-files-found: error
          retention-days: 1

  create-manifest:
    needs: build
    runs-on: ubuntu-latest
    if: github.repository == 'langgenius/dify'
    strategy:
      matrix:
        include:
          - service_name: "merge-api-images"
            image_name_env: "DIFY_API_IMAGE_NAME"
            context: "api"
          - service_name: "merge-web-images"
            image_name_env: "DIFY_WEB_IMAGE_NAME"
            context: "web"
    steps:
      - name: Download digests
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          path: /tmp/digests
          pattern: digests-${{ matrix.context }}-*
          merge-multiple: true

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: ${{ env.DOCKERHUB_USER }}
          password: ${{ env.DOCKERHUB_TOKEN }}

      - name: Extract metadata for Docker
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
        with:
          images: ${{ env[matrix.image_name_env] }}
          tags: |
            type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-') }}
            type=ref,event=branch
            type=sha,enable=true,priority=100,prefix=,suffix=,format=long
            type=raw,value=${{ github.ref_name }},enable=${{ startsWith(github.ref, 'refs/tags/') }}

      - name: Create manifest list and push
        working-directory: /tmp/digests
        env:
          IMAGE_NAME: ${{ env[matrix.image_name_env] }}
        run: |
          docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
            $(printf "$IMAGE_NAME@sha256:%s " *)

      - name: Inspect image
        env:
          IMAGE_NAME: ${{ env[matrix.image_name_env] }}
          IMAGE_VERSION: ${{ steps.meta.outputs.version }}
        run: |
          docker buildx imagetools inspect "$IMAGE_NAME:$IMAGE_VERSION"
db-migration-test .github/workflows/db-migration-test.yml
Triggers
workflow_call
Runs on
ubuntu-latest, ubuntu-latest
Jobs
db-migration-test-postgres, db-migration-test-mysql
Actions
astral-sh/setup-uv, hoverkraft-tech/compose-action, astral-sh/setup-uv, hoverkraft-tech/compose-action
Commands
  • uv sync --project api
  • # upgrade uv run --directory api flask db upgrade 'base:head' --sql # downgrade uv run --directory api flask db downgrade 'head:base' --sql
  • cd docker cp middleware.env.example middleware.env
  • cd api cp .env.example .env
  • uv run --directory api flask upgrade-db
  • uv sync --project api
  • # upgrade uv run --directory api flask db upgrade 'base:head' --sql # downgrade uv run --directory api flask db downgrade 'head:base' --sql
  • cd docker cp 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
View raw YAML
name: DB Migration Test

on:
  workflow_call:

concurrency:
  group: db-migration-test-${{ github.ref }}
  cancel-in-progress: true

jobs:
  db-migration-test-postgres:
    runs-on: ubuntu-latest

    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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
        with:
          enable-cache: true
          python-version: "3.12"
          cache-dependency-glob: api/uv.lock

      - name: Install dependencies
        run: uv sync --project api
      - name: Ensure Offline migration are supported
        run: |
          # upgrade
          uv run --directory api flask db upgrade 'base:head' --sql
          # downgrade
          uv run --directory api flask db downgrade 'head:base' --sql

      - name: Prepare middleware env
        run: |
          cd docker
          cp middleware.env.example middleware.env

      - name: Set up Middlewares
        uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
        with:
          compose-file: |
            docker/docker-compose.middleware.yaml
          services: |
            db_postgres
            redis

      - name: Prepare configs
        run: |
          cd api
          cp .env.example .env

      - name: Run DB Migration
        env:
          DEBUG: true
        run: uv run --directory api flask upgrade-db

  db-migration-test-mysql:
    runs-on: ubuntu-latest

    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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
        with:
          enable-cache: true
          python-version: "3.12"
          cache-dependency-glob: api/uv.lock

      - name: Install dependencies
        run: uv sync --project api
      - name: Ensure Offline migration are supported
        run: |
          # upgrade
          uv run --directory api flask db upgrade 'base:head' --sql
          # downgrade
          uv run --directory api flask db downgrade 'head:base' --sql

      - name: Prepare middleware env for MySQL
        run: |
          cd docker
          cp 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
        with:
          compose-file: |
            docker/docker-compose.middleware.yaml
          services: |
            db_mysql
            redis

      - name: Prepare configs for MySQL
        run: |
          cd api
          cp .env.example .env
          sed -i 's/DB_TYPE=postgresql/DB_TYPE=mysql/' .env
          sed -i 's/DB_PORT=5432/DB_PORT=3306/' .env
          sed -i 's/DB_USERNAME=postgres/DB_USERNAME=root/' .env

      - name: Run DB Migration
        env:
          DEBUG: true
        run: uv run --directory api flask upgrade-db
deploy-agent-dev perms .github/workflows/deploy-agent-dev.yml
Triggers
workflow_run
Runs on
ubuntu-latest
Jobs
deploy
Actions
appleboy/ssh-action
View raw YAML
name: Deploy Agent Dev

permissions:
  contents: read

on:
  workflow_run:
    workflows: ["Build and Push API & Web"]
    branches:
      - "deploy/agent-dev"
    types:
      - completed

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: |
      github.event.workflow_run.conclusion == 'success' &&
      github.event.workflow_run.head_branch == 'deploy/agent-dev'
    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
        with:
          host: ${{ secrets.AGENT_DEV_SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            ${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }}
deploy-dev .github/workflows/deploy-dev.yml
Triggers
workflow_run
Runs on
ubuntu-latest
Jobs
deploy
Actions
appleboy/ssh-action
View raw YAML
name: Deploy Dev

on:
  workflow_run:
    workflows: ["Build and Push API & Web"]
    branches:
      - "deploy/dev"
    types:
      - completed

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: |
      github.event.workflow_run.conclusion == 'success' &&
      github.event.workflow_run.head_branch == 'deploy/dev'
    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            ${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }}
deploy-enterprise perms .github/workflows/deploy-enterprise.yml
Triggers
workflow_run
Runs on
ubuntu-latest
Jobs
deploy
Commands
  • IFS=',' read -ra ENDPOINTS <<< "${DEV_ENV_ADDRS:-}" BODY='{"project":"dify-api","tag":"deploy-enterprise"}' for ENDPOINT in "${ENDPOINTS[@]}"; do ENDPOINT="$(echo "$ENDPOINT" | xargs)" [ -z "$ENDPOINT" ] && continue API_SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$DEPLOY_SECRET" | awk '{print "sha256="$2}') curl -sSf -X POST \ -H "Content-Type: application/json" \ -H "X-Hub-Signature-256: $API_SIGNATURE" \ -d "$BODY" \ "$ENDPOINT" done
View raw YAML
name: Deploy Enterprise

permissions:
  contents: read

on:
  workflow_run:
    workflows: ["Build and Push API & Web"]
    branches:
      - "deploy/enterprise"
    types:
      - completed

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: |
      github.event.workflow_run.conclusion == 'success' &&
      github.event.workflow_run.head_branch == 'deploy/enterprise'

    steps:
      - name: trigger deployments
        env:
          DEV_ENV_ADDRS: ${{ vars.DEV_ENV_ADDRS }}
          DEPLOY_SECRET: ${{ secrets.DEPLOY_SECRET }}
        run: |
          IFS=',' read -ra ENDPOINTS <<< "${DEV_ENV_ADDRS:-}"
          BODY='{"project":"dify-api","tag":"deploy-enterprise"}'

          for ENDPOINT in "${ENDPOINTS[@]}"; do
            ENDPOINT="$(echo "$ENDPOINT" | xargs)"
            [ -z "$ENDPOINT" ] && continue

            API_SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$DEPLOY_SECRET" | awk '{print "sha256="$2}')

            curl -sSf -X POST \
              -H "Content-Type: application/json" \
              -H "X-Hub-Signature-256: $API_SIGNATURE" \
              -d "$BODY" \
              "$ENDPOINT"
          done
deploy-hitl .github/workflows/deploy-hitl.yml
Triggers
workflow_run
Runs on
ubuntu-latest
Jobs
deploy
Actions
appleboy/ssh-action
View raw YAML
name: Deploy HITL

on:
  workflow_run:
    workflows: ["Build and Push API & Web"]
    branches:
      - "build/feat/hitl"
    types:
      - completed

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: |
      github.event.workflow_run.conclusion == 'success' &&
      github.event.workflow_run.head_branch == 'build/feat/hitl'
    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
        with:
          host: ${{ secrets.HITL_SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            ${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }}
docker-build matrix .github/workflows/docker-build.yml
Triggers
pull_request
Runs on
ubuntu-latest
Jobs
build-docker
Matrix
include, include.context, include.platform, include.service_name→ api, api-amd64, api-arm64, linux/amd64, linux/arm64, web, web-amd64, web-arm64
Actions
docker/setup-qemu-action, docker/setup-buildx-action, docker/build-push-action
View raw YAML
name: Build docker image

on:
  pull_request:
    branches:
      - "main"
    paths:
      - api/Dockerfile
      - web/Dockerfile

concurrency:
  group: docker-build-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  build-docker:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - service_name: "api-amd64"
            platform: linux/amd64
            context: "api"
          - service_name: "api-arm64"
            platform: linux/arm64
            context: "api"
          - service_name: "web-amd64"
            platform: linux/amd64
            context: "web"
          - service_name: "web-arm64"
            platform: linux/arm64
            context: "web"
    steps:
      - 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: Build Docker Image
        uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
        with:
          push: false
          context: "{{defaultContext}}:${{ matrix.context }}"
          file: "${{ matrix.file }}"
          platforms: ${{ matrix.platform }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
labeler .github/workflows/labeler.yml
Triggers
pull_request_target
Runs on
ubuntu-latest
Jobs
labeler
Actions
actions/labeler
View raw YAML
name: "Pull Request Labeler"
on:
  pull_request_target:

jobs:
  labeler:
    permissions:
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
        with:
          sync-labels: true
main-ci perms .github/workflows/main-ci.yml
Triggers
pull_request, merge_group, push
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
pre_job, check-changes, api-tests-run, api-tests-skip, api-tests, web-tests-run, web-tests-skip, web-tests, style-check, vdb-tests-run, vdb-tests-skip, vdb-tests, db-migration-test-run, db-migration-test-skip, db-migration-test
Actions
fkirc/skip-duplicate-actions, dorny/paths-filter
Commands
  • echo "No API-related changes detected; skipping API tests."
  • 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
  • echo "No web-related changes detected; skipping web tests."
  • 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
  • echo "No VDB-related changes detected; skipping VDB tests."
  • 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
  • echo "No migration-related changes detected; skipping DB migration tests."
  • 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
View raw YAML
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
  statuses: write

concurrency:
  group: main-ci-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  pre_job:
    name: Skip Duplicate Checks
    runs-on: ubuntu-latest
    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
    needs: pre_job
    if: needs.pre_job.outputs.should_skip != 'true'
    runs-on: ubuntu-latest
    outputs:
      api-changed: ${{ steps.changes.outputs.api }}
      web-changed: ${{ steps.changes.outputs.web }}
      vdb-changed: ${{ steps.changes.outputs.vdb }}
      migration-changed: ${{ steps.changes.outputs.migration }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
        id: changes
        with:
          filters: |
            api:
              - 'api/**'
              - 'docker/**'
              - '.github/workflows/api-tests.yml'
            web:
              - 'web/**'
              - '.github/workflows/web-tests.yml'
              - '.github/actions/setup-web/**'
            vdb:
              - 'api/core/rag/datasource/**'
              - 'docker/**'
              - '.github/workflows/vdb-tests.yml'
              - 'api/uv.lock'
              - 'api/pyproject.toml'
            migration:
              - 'api/migrations/**'
              - '.github/workflows/db-migration-test.yml'

  # 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

  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: ubuntu-latest
    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: ubuntu-latest
    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: ubuntu-latest
    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: ubuntu-latest
    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

  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: ubuntu-latest
    steps:
      - name: Report skipped VDB tests
        run: echo "No VDB-related changes detected; skipping VDB tests."

  vdb-tests:
    name: VDB Tests
    if: ${{ always() }}
    needs:
      - pre_job
      - check-changes
      - vdb-tests-run
      - vdb-tests-skip
    runs-on: ubuntu-latest
    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: ubuntu-latest
    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
    if: ${{ always() }}
    needs:
      - pre_job
      - check-changes
      - db-migration-test-run
      - db-migration-test-skip
    runs-on: ubuntu-latest
    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
pyrefly-diff perms .github/workflows/pyrefly-diff.yml
Triggers
pull_request
Runs on
ubuntu-latest
Jobs
pyrefly-diff
Actions
astral-sh/setup-uv
Commands
  • uv sync --project api --dev
  • git show ${{ github.event.pull_request.head.sha }}:api/libs/pyrefly_diagnostics.py > /tmp/pyrefly_diagnostics.py
  • uv run --directory api --dev pyrefly check 2>&1 \ | uv run --directory api python /tmp/pyrefly_diagnostics.py > /tmp/pyrefly_pr.txt || true
  • git checkout ${{ github.base_ref }}
  • uv run --directory api --dev pyrefly check 2>&1 \ | uv run --directory api python /tmp/pyrefly_diagnostics.py > /tmp/pyrefly_base.txt || true
  • diff -u /tmp/pyrefly_base.txt /tmp/pyrefly_pr.txt > pyrefly_diff.txt || true
  • echo ${{ github.event.pull_request.number }} > pr_number.txt
View raw YAML
name: Pyrefly Diff Check

on:
  pull_request:
    paths:
      - 'api/**/*.py'

permissions:
  contents: read

jobs:
  pyrefly-diff:
    runs-on: ubuntu-latest
    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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
        with:
          enable-cache: true

      - name: Install dependencies
        run: uv sync --project api --dev

      - name: Prepare diagnostics extractor
        run: |
          git show ${{ github.event.pull_request.head.sha }}:api/libs/pyrefly_diagnostics.py > /tmp/pyrefly_diagnostics.py

      - name: Run pyrefly on PR branch
        run: |
          uv run --directory api --dev pyrefly check 2>&1 \
            | uv run --directory api python /tmp/pyrefly_diagnostics.py > /tmp/pyrefly_pr.txt || true

      - name: Checkout base branch
        run: git checkout ${{ github.base_ref }}

      - name: Run pyrefly on base branch
        run: |
          uv run --directory api --dev pyrefly check 2>&1 \
            | uv run --directory api python /tmp/pyrefly_diagnostics.py > /tmp/pyrefly_base.txt || true

      - name: Compute diff
        run: |
          diff -u /tmp/pyrefly_base.txt /tmp/pyrefly_pr.txt > pyrefly_diff.txt || true

      - 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
        with:
          name: pyrefly_diff
          path: |
            pyrefly_diff.txt
            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
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const fs = require('fs');
            let diff = fs.readFileSync('pyrefly_diff.txt', { encoding: 'utf8' });
            const prNumber = context.payload.pull_request.number;

            const MAX_CHARS = 65000;
            if (diff.length > MAX_CHARS) {
              diff = diff.slice(0, MAX_CHARS);
              diff = diff.slice(0, diff.lastIndexOf('\n'));
              diff += '\n\n... (truncated) ...';
            }

            const body = diff.trim()
              ? [
                  '### Pyrefly Diff',
                  '<details>',
                  '<summary>base → PR</summary>',
                  '',
                  '```diff',
                  diff,
                  '```',
                  '</details>',
                ].join('\n')
              : '### Pyrefly Diff\nNo changes detected.';

            await github.rest.issues.createComment({
              issue_number: prNumber,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body,
            });
pyrefly-diff-comment perms .github/workflows/pyrefly-diff-comment.yml
Triggers
workflow_run
Runs on
ubuntu-latest
Jobs
comment
Commands
  • unzip -o pyrefly_diff.zip
View raw YAML
name: Comment with Pyrefly Diff

on:
  workflow_run:
    workflows:
      - Pyrefly Diff Check
    types:
      - completed

permissions: {}

jobs:
  comment:
    name: Comment PR with pyrefly diff
    runs-on: ubuntu-latest
    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: Download pyrefly diff artifact
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.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_diff'
            );
            if (!match) {
              throw new Error('pyrefly_diff 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_diff.zip', Buffer.from(download.data));

      - name: Unzip artifact
        run: unzip -o pyrefly_diff.zip

      - name: Post comment
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const fs = require('fs');
            let diff = fs.readFileSync('pyrefly_diff.txt', { encoding: 'utf8' });
            let prNumber = null;
            try {
              prNumber = parseInt(fs.readFileSync('pr_number.txt', { encoding: 'utf8' }), 10);
            } catch (err) {
              // Fallback to workflow_run payload if artifact is missing or incomplete.
              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');
            }

            const MAX_CHARS = 65000;
            if (diff.length > MAX_CHARS) {
              diff = diff.slice(0, MAX_CHARS);
              diff = diff.slice(0, diff.lastIndexOf('\\n'));
              diff += '\\n\\n... (truncated) ...';
            }

            const body = diff.trim()
              ? '### Pyrefly Diff\n<details>\n<summary>base → PR</summary>\n\n```diff\n' + diff + '\n```\n</details>'
              : '### Pyrefly Diff\nNo changes detected.';

            await github.rest.issues.createComment({
              issue_number: prNumber,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body,
            });
semantic-pull-request .github/workflows/semantic-pull-request.yml
Triggers
pull_request, merge_group
Runs on
ubuntu-latest
Jobs
lint
Actions
amannn/action-semantic-pull-request
Commands
  • echo "Semantic PR title validation is handled on pull requests."
View raw YAML
name: Semantic Pull Request

on:
  pull_request:
    types:
      - opened
      - edited
      - reopened
      - synchronize
  merge_group:
    branches: ["main"]
    types: [checks_requested]

jobs:
  lint:
    name: Validate PR title
    permissions:
      pull-requests: read
    runs-on: ubuntu-latest
    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 }}
stale .github/workflows/stale.yml
Triggers
schedule
Runs on
ubuntu-latest
Jobs
stale
Actions
actions/stale
View raw YAML
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests

on:
  schedule:
    - cron: '0 3 * * *'

jobs:
  stale:

    runs-on: ubuntu-latest
    permissions:
      issues: write
      pull-requests: write

    steps:
      - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
        with:
          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-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'
style perms .github/workflows/style.yml
Triggers
workflow_call
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
python-style, web-style, superlinter
Actions
tj-actions/changed-files, astral-sh/setup-uv, tj-actions/changed-files, tj-actions/changed-files, super-linter/super-linter/slim
Commands
  • uv sync --project api --dev
  • uv run --directory api --dev lint-imports
  • make type-check-core
  • uv run --project api dotenv-linter ./api/.env.example ./web/.env.example
  • vp run lint:ci
  • vp run lint:tss
  • vp run type-check
  • vp run knip
View raw YAML
name: Style check

on:
  workflow_call:

concurrency:
  group: style-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

permissions:
  checks: write
  statuses: write
  contents: read

jobs:
  python-style:
    name: Python Style
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Check changed files
        id: changed-files
        uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
        with:
          files: |
            api/**
            .github/workflows/style.yml

      - name: Setup UV and Python
        if: steps.changed-files.outputs.any_changed == 'true'
        uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
        with:
          enable-cache: false
          python-version: "3.12"
          cache-dependency-glob: api/uv.lock

      - name: Install dependencies
        if: steps.changed-files.outputs.any_changed == 'true'
        run: uv sync --project api --dev

      - name: Run Import Linter
        if: steps.changed-files.outputs.any_changed == 'true'
        run: uv run --directory api --dev lint-imports

      - name: Run Type Checks
        if: steps.changed-files.outputs.any_changed == 'true'
        run: make type-check-core

      - name: Dotenv check
        if: steps.changed-files.outputs.any_changed == 'true'
        run: uv run --project api dotenv-linter ./api/.env.example ./web/.env.example

  web-style:
    name: Web Style
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./web
    permissions:
      checks: write
      pull-requests: read

    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        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/style.yml
            .github/actions/setup-web/**

      - name: Setup web environment
        if: steps.changed-files.outputs.any_changed == 'true'
        uses: ./.github/actions/setup-web

      - name: Restore ESLint cache
        if: steps.changed-files.outputs.any_changed == 'true'
        id: eslint-cache-restore
        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        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 }}
          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/**') }}-

      - name: Web style check
        if: steps.changed-files.outputs.any_changed == 'true'
        working-directory: ./web
        run: vp run lint:ci

      - name: Web tsslint
        if: steps.changed-files.outputs.any_changed == 'true'
        working-directory: ./web
        run: vp run lint:tss

      - name: Web type check
        if: steps.changed-files.outputs.any_changed == 'true'
        working-directory: ./web
        run: vp run type-check

      - name: Web dead code check
        if: steps.changed-files.outputs.any_changed == 'true'
        working-directory: ./web
        run: vp run knip

      - 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
        with:
          path: web/.eslintcache
          key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }}

  superlinter:
    name: SuperLinter
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Check changed files
        id: changed-files
        uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
        with:
          files: |
            **.sh
            **.yaml
            **.yml
            **Dockerfile
            dev/**
            .editorconfig

      - name: Super-linter
        uses: super-linter/super-linter/slim@61abc07d755095a68f4987d1c2c3d1d64408f1f9 # v8.5.0
        if: steps.changed-files.outputs.any_changed == 'true'
        env:
          BASH_SEVERITY: warning
          DEFAULT_BRANCH: origin/main
          EDITORCONFIG_FILE_NAME: editorconfig-checker.json
          FILTER_REGEX_INCLUDE: pnpm-lock.yaml
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          IGNORE_GENERATED_FILES: true
          IGNORE_GITIGNORED_FILES: true
          VALIDATE_BASH: true
          VALIDATE_BASH_EXEC: true
          # FIXME: temporarily disabled until api-docker.yaml's run script is fixed for shellcheck
          # VALIDATE_GITHUB_ACTIONS: true
          VALIDATE_DOCKERFILE_HADOLINT: true
          VALIDATE_EDITORCONFIG: true
          VALIDATE_XML: true
          VALIDATE_YAML: true
tool-test-sdks .github/workflows/tool-test-sdks.yaml
Triggers
pull_request
Runs on
ubuntu-latest
Jobs
build
Commands
  • pnpm install --frozen-lockfile
  • pnpm test
View raw YAML
name: Run Unit Test For SDKs

on:
  pull_request:
    branches:
      - main
    paths:
      - sdks/**

concurrency:
  group: sdk-tests-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  build:
    name: unit test for Node.js SDK
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: sdks/nodejs-client

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Use Node.js
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version: 22
          cache: ''
          cache-dependency-path: 'pnpm-lock.yaml'

      - name: Install Dependencies
        run: pnpm install --frozen-lockfile

      - name: Test
        run: pnpm test
translate-i18n-claude perms AI .github/workflows/translate-i18n-claude.yml
Triggers
repository_dispatch, workflow_dispatch
Runs on
ubuntu-latest
Jobs
translate
Actions
anthropics/claude-code-action
Commands
  • git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com"
  • 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 # 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 # 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 else echo "" > /tmp/i18n-diff.txt echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT fi else echo "Unsupported event type: ${{ github.event_name }}" exit 1 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 fi echo "Detected files: $(cat $GITHUB_OUTPUT | grep CHANGED_FILES || echo 'none')"
View raw YAML
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

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.'
        required: false
        type: string
      languages:
        description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported languages.'
        required: false
        type: string
      mode:
        description: 'Sync mode: incremental (only changes) or full (re-check all keys)'
        required: false
        default: 'incremental'
        type: choice
        options:
          - incremental
          - full

permissions:
  contents: write
  pull-requests: write

jobs:
  translate:
    if: github.repository == 'langgenius/dify'
    runs-on: ubuntu-latest
    timeout-minutes: 60

    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Configure Git
        run: |
          git config --global user.name "github-actions[bot]"
          git config --global user.email "github-actions[bot]@users.noreply.github.com"

      - name: Setup web environment
        uses: ./.github/actions/setup-web

      - name: Detect changed files and generate diff
        id: detect_changes
        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

            # 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

            # 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
            else
              echo "" > /tmp/i18n-diff.txt
              echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
            fi
          else
            echo "Unsupported event type: ${{ github.event_name }}"
            exit 1
          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
          fi

          echo "Detected files: $(cat $GITHUB_OUTPUT | grep CHANGED_FILES || echo 'none')"

      - name: Run Claude Code for Translation Sync
        if: steps.detect_changes.outputs.CHANGED_FILES != ''
        uses: anthropics/claude-code-action@ff9acae5886d41a99ed4ec14b7dc147d55834722 # v1.0.77
        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]'
          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).

            ## 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.

            ## WORKING DIRECTORY & ABSOLUTE PATHS
            Claude Code sandbox working directory may vary. Always use absolute paths:
            - For pnpm: `pnpm --dir ${{ github.workspace }}/web <command>`
            - For git: `git -C ${{ github.workspace }} <command>`
            - For gh: `gh --repo ${{ github.repository }} <command>`
            - For file paths: `${{ github.workspace }}/web/i18n/`

            ## 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
              - `<tag>content</tag>` - 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>{{email}}</email>" → Chinese: "<email>{{email}}</email>"
              - English: "<CustomLink>Marketplace</CustomLink>" → Japanese: "<CustomLink>マーケットプレイス</CustomLink>"

              ❌ 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)
              - "<email>" → "<メール>" ❌ (tag name translated)
              - "<CustomLink>" → "<自定义链接>" ❌ (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
            ```

          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"
trigger-i18n-sync perms .github/workflows/trigger-i18n-sync.yml
Triggers
push
Runs on
ubuntu-latest
Jobs
trigger
Actions
peter-evans/repository-dispatch
Commands
  • 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" fi # 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" else echo "has_changes=false" >> $GITHUB_OUTPUT echo "No i18n changes detected" fi
View raw YAML
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]
    paths:
      - 'web/i18n/en-US/*.json'

permissions:
  contents: write

jobs:
  trigger:
    if: github.repository == 'langgenius/dify'
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Detect changed files and generate diff
        id: detect
        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"
          fi

          # 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"
          else
            echo "has_changes=false" >> $GITHUB_OUTPUT
            echo "No i18n changes detected"
          fi

      - name: Trigger i18n sync workflow
        if: steps.detect.outputs.has_changes == 'true'
        uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
        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 }}"}'
vdb-tests matrix .github/workflows/vdb-tests.yml
Triggers
workflow_call
Runs on
ubuntu-latest
Jobs
test
Matrix
python-version→ 3.12
Actions
endersonmenezes/free-disk-space, astral-sh/setup-uv, hoverkraft-tech/compose-action
Commands
  • uv lock --project api --check
  • uv sync --project api --dev
  • cp docker/.env.example docker/.env cp docker/middleware.env.example docker/middleware.env
  • sh .github/workflows/expose_service_ports.sh
  • echo $(pwd) ls -lah . cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
  • uv run --project api bash dev/pytest/pytest_vdb.sh
View raw YAML
name: Run VDB Tests

on:
  workflow_call:

concurrency:
  group: vdb-tests-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  test:
    name: VDB Tests
    runs-on: ubuntu-latest
    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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.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

      - 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 Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase, OceanBase)
        uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.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/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py

      - name: Test Vector Stores
        run: uv run --project api bash dev/pytest/pytest_vdb.sh
web-tests matrix perms .github/workflows/web-tests.yml
Triggers
workflow_call
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
test, merge-reports, web-build
Matrix
shardIndex, shardTotal→ 1, 2, 3, 4
Actions
codecov/codecov-action, tj-actions/changed-files
Commands
  • vp test run --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --coverage
  • vp test --merge-reports --coverage --silent=passed-only
  • vp run build
View raw YAML
name: Web Tests

on:
  workflow_call:
    secrets:
      CODECOV_TOKEN:
        required: false

permissions:
  contents: read

concurrency:
  group: web-tests-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  test:
    name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
    runs-on: ubuntu-latest
    env:
      VITEST_COVERAGE_SCOPE: app-components
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    defaults:
      run:
        shell: bash
        working-directory: ./web

    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Setup web environment
        uses: ./.github/actions/setup-web

      - name: Run tests
        run: vp test run --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --coverage

      - name: Upload blob report
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: blob-report-${{ matrix.shardIndex }}
          path: web/.vitest-reports/*
          include-hidden-files: true
          retention-days: 1

  merge-reports:
    name: Merge Test Reports
    if: ${{ !cancelled() }}
    needs: [test]
    runs-on: ubuntu-latest
    env:
      CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
    defaults:
      run:
        shell: bash
        working-directory: ./web

    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Setup web environment
        uses: ./.github/actions/setup-web

      - name: Download blob reports
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          path: web/.vitest-reports
          pattern: blob-report-*
          merge-multiple: true

      - name: Merge reports
        run: vp test --merge-reports --coverage --silent=passed-only

      - name: Report coverage
        if: ${{ env.CODECOV_TOKEN != '' }}
        uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
        with:
          directory: web/coverage
          flags: web
        env:
          CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}

  web-build:
    name: Web Build
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./web

    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        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