From 48aa9714e01e5b16f24985fe105e284320a6081a Mon Sep 17 00:00:00 2001 From: PiEgg Date: Thu, 9 Apr 2026 10:50:58 +0800 Subject: [PATCH] feat(ci): add plugin peer dependencies check and build workflow (#9058) --- .github/workflows/check-deps-and-build.yml | 357 ++++++++++++ .github/workflows/e2e.yml | 615 --------------------- scripts/check-plugin-peer-deps.sh | 211 +++++++ scripts/peer-deps-whitelist.json | 3 + 4 files changed, 571 insertions(+), 615 deletions(-) create mode 100644 .github/workflows/check-deps-and-build.yml delete mode 100644 .github/workflows/e2e.yml create mode 100755 scripts/check-plugin-peer-deps.sh create mode 100644 scripts/peer-deps-whitelist.json diff --git a/.github/workflows/check-deps-and-build.yml b/.github/workflows/check-deps-and-build.yml new file mode 100644 index 00000000000..ed6c1bfb3a0 --- /dev/null +++ b/.github/workflows/check-deps-and-build.yml @@ -0,0 +1,357 @@ +name: Check Dependencies and Build + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - 'main' + - 'develop' + - 'next' + paths: + - '.github/workflows/check-deps-and-build.yml' + - 'scripts/check-plugin-peer-deps.sh' + - 'scripts/peer-deps-whitelist.json' + - 'packages/**' + - '**/yarn.lock' + pull_request: + branches: + - '**' + paths: + - '.github/workflows/check-deps-and-build.yml' + - 'scripts/check-plugin-peer-deps.sh' + - 'scripts/peer-deps-whitelist.json' + - 'packages/**' + - '**/yarn.lock' + +jobs: + get-plugins: + uses: nocobase/nocobase/.github/workflows/get-plugins.yml@main + secrets: inherit + + check-peer-deps: + name: Check peer dependencies + needs: get-plugins + runs-on: ubuntu-latest + outputs: + result: ${{ steps.check.outputs.result }} + status: ${{ steps.check.outputs.status }} + steps: + - uses: actions/create-github-app-token@v1 + continue-on-error: true + id: app-token + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(needs.get-plugins.outputs.alpha-plugins || '[]'), ',') }} + skip-token-revoke: true + + - uses: actions/checkout@v4 + + - name: Checkout pro-plugins + continue-on-error: true + uses: actions/checkout@v4 + with: + repository: nocobase/pro-plugins + ref: ${{ github.base_ref || github.ref_name }} + path: packages/pro-plugins + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + + - name: Switch pro-plugins branch + continue-on-error: true + run: | + cd packages/pro-plugins + if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then + git checkout ${{ github.head_ref || github.ref_name }} + elif git show-ref --quiet refs/remotes/origin/${{ github.base_ref || github.ref_name }}; then + git checkout ${{ github.base_ref || github.ref_name }} + else + git checkout main + fi + + - name: Determine plugin set + id: plugin-set + run: | + TARGET_BRANCH="${{ github.base_ref || github.ref_name }}" + case "$TARGET_BRANCH" in + develop) + echo "plugins=${{ needs.get-plugins.outputs.alpha-plugins || '[]' }}" >> "$GITHUB_OUTPUT" + ;; + next) + echo "plugins=${{ needs.get-plugins.outputs.beta-plugins || '[]' }}" >> "$GITHUB_OUTPUT" + ;; + *) + echo "plugins=${{ needs.get-plugins.outputs.rc-plugins || '[]' }}" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Clone standalone plugin repos + continue-on-error: true + shell: bash + run: | + PLUGINS='${{ steps.plugin-set.outputs.plugins }}' + if [[ "$PLUGINS" == "[]" || -z "$PLUGINS" ]]; then + echo "No standalone plugins to clone" + exit 0 + fi + for repo in $(echo "$PLUGINS" | jq -r '.[]'); do + if [[ ! -d "packages/pro-plugins/@nocobase/$repo" ]]; then + echo "Cloning $repo..." + git clone -b ${{ github.base_ref || github.ref_name }} \ + https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git \ + packages/pro-plugins/@nocobase/$repo 2>/dev/null || true + fi + done + + - name: Try checkout standalone plugin branches + continue-on-error: true + shell: bash + run: | + for plugin_dir in ./packages/pro-plugins/@nocobase/*/; do + if [[ -d "$plugin_dir/.git" ]]; then + cd "$plugin_dir" + git checkout ${{ github.head_ref || github.ref_name }} 2>/dev/null || true + cd - > /dev/null + fi + done + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Run peer dependency checks + id: check + shell: bash + run: | + exit_code=0 + plugins_json="[]" + pro_plugins_json="[]" + + # Check packages/plugins/@nocobase + if [[ -d packages/plugins/@nocobase ]]; then + echo "::group::Checking packages/plugins/@nocobase" + plugins_json=$(bash scripts/check-plugin-peer-deps.sh packages/plugins/@nocobase \ + --whitelist scripts/peer-deps-whitelist.json 2>&1 1>/tmp/plugins-result.json && cat /tmp/plugins-result.json || { cat /tmp/plugins-result.json 2>/dev/null; echo "[]"; }) || true + + # Re-run to get proper exit code and output + if bash scripts/check-plugin-peer-deps.sh packages/plugins/@nocobase \ + --whitelist scripts/peer-deps-whitelist.json > /tmp/plugins-result.json 2>/tmp/plugins-log.txt; then + cat /tmp/plugins-log.txt + plugins_json=$(cat /tmp/plugins-result.json) + else + cat /tmp/plugins-log.txt + plugins_json=$(cat /tmp/plugins-result.json) + exit_code=1 + fi + echo "::endgroup::" + fi + + # Check packages/pro-plugins/@nocobase + if [[ -d packages/pro-plugins/@nocobase ]]; then + echo "::group::Checking packages/pro-plugins/@nocobase" + if bash scripts/check-plugin-peer-deps.sh packages/pro-plugins/@nocobase \ + --whitelist scripts/peer-deps-whitelist.json > /tmp/pro-plugins-result.json 2>/tmp/pro-plugins-log.txt; then + cat /tmp/pro-plugins-log.txt + pro_plugins_json=$(cat /tmp/pro-plugins-result.json) + else + cat /tmp/pro-plugins-log.txt + pro_plugins_json=$(cat /tmp/pro-plugins-result.json) + exit_code=1 + fi + echo "::endgroup::" + fi + + # Merge results + merged=$(jq -s '.[0] + .[1]' /tmp/plugins-result.json /tmp/pro-plugins-result.json 2>/dev/null || echo "[]") + + # Write outputs (handle multiline JSON) + { + echo "result<> "$GITHUB_OUTPUT" + + if [[ $exit_code -eq 0 ]]; then + echo "status=success" >> "$GITHUB_OUTPUT" + else + echo "status=failure" >> "$GITHUB_OUTPUT" + echo "" + echo "::error::Missing plugin-* peerDependencies detected. See details above." + exit 1 + fi + + build: + name: Build + needs: [get-plugins, check-peer-deps] + runs-on: ubuntu-latest + steps: + - uses: actions/create-github-app-token@v1 + continue-on-error: true + id: app-token + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(needs.get-plugins.outputs.alpha-plugins || '[]'), ',') }} + skip-token-revoke: true + + - uses: actions/checkout@v4 + + - name: Checkout pro-plugins + continue-on-error: true + uses: actions/checkout@v4 + with: + repository: nocobase/pro-plugins + ref: ${{ github.base_ref || github.ref_name }} + path: packages/pro-plugins + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + + - name: Switch pro-plugins branch + continue-on-error: true + run: | + cd packages/pro-plugins + if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then + git checkout ${{ github.head_ref || github.ref_name }} + elif git show-ref --quiet refs/remotes/origin/${{ github.base_ref || github.ref_name }}; then + git checkout ${{ github.base_ref || github.ref_name }} + else + git checkout main + fi + + - name: Determine plugin set + id: plugin-set + run: | + TARGET_BRANCH="${{ github.base_ref || github.ref_name }}" + case "$TARGET_BRANCH" in + develop) + echo "plugins=${{ needs.get-plugins.outputs.alpha-plugins || '[]' }}" >> "$GITHUB_OUTPUT" + ;; + next) + echo "plugins=${{ needs.get-plugins.outputs.beta-plugins || '[]' }}" >> "$GITHUB_OUTPUT" + ;; + *) + echo "plugins=${{ needs.get-plugins.outputs.rc-plugins || '[]' }}" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Clone standalone plugin repos + continue-on-error: true + shell: bash + run: | + PLUGINS='${{ steps.plugin-set.outputs.plugins }}' + if [[ "$PLUGINS" == "[]" || -z "$PLUGINS" ]]; then + exit 0 + fi + for repo in $(echo "$PLUGINS" | jq -r '.[]'); do + if [[ ! -d "packages/pro-plugins/@nocobase/$repo" ]]; then + git clone -b ${{ github.base_ref || github.ref_name }} \ + https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git \ + packages/pro-plugins/@nocobase/$repo 2>/dev/null || true + fi + done + + - name: Try checkout standalone plugin branches + continue-on-error: true + shell: bash + run: | + for plugin_dir in ./packages/pro-plugins/@nocobase/*/; do + if [[ -d "$plugin_dir/.git" ]]; then + cd "$plugin_dir" + git checkout ${{ github.head_ref || github.ref_name }} 2>/dev/null || true + cd - > /dev/null + fi + done + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'yarn' + + - name: Install dependencies + run: | + yarn config set registry https://registry.npmjs.org/ + yarn install + + - name: Build + run: yarn build + + comment-on-pr: + name: Comment on PR + needs: [check-peer-deps, build] + runs-on: ubuntu-latest + if: ${{ always() && github.event.pull_request.number }} + permissions: + pull-requests: write + steps: + - uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const marker = ''; + const prNumber = context.payload.pull_request.number; + const checkStatus = '${{ needs.check-peer-deps.outputs.status }}'; + const buildResult = '${{ needs.build.result }}'; + + let resultJson = []; + try { + resultJson = JSON.parse(`${{ needs.check-peer-deps.outputs.result }}`); + } catch (e) { + // If parsing fails, treat as empty + } + + let body = marker + '\n'; + + if (checkStatus === 'failure' && resultJson.length > 0) { + body += '### ❌ Missing Plugin PeerDependencies\n\n'; + body += 'The following plugins are missing required `plugin-*` peerDependencies in their `package.json`:\n\n'; + body += '| Plugin | Missing peerDependencies |\n'; + body += '|--------|-------------------------|\n'; + for (const item of resultJson) { + const deps = item.missing.map(d => '`' + d + '`').join(', '); + body += `| ${item.plugin} | ${deps} |\n`; + } + body += '\nPlease add the missing peerDependencies to resolve this check.\n'; + } else if (buildResult === 'failure') { + body += '### ❌ Build Failed\n\n'; + body += 'The build step failed. Please check the [workflow logs](' + + `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + + ') for details.\n'; + } else { + body += '### ✅ Plugin PeerDependencies Check Passed\n\n'; + body += 'All plugins have their required `plugin-*` peerDependencies properly declared.'; + if (buildResult === 'success') { + body += ' Build completed successfully.'; + } + body += '\n'; + } + + // Find existing comment with marker + const { data: comments } = await github.rest.issues.listComments({ + ...context.repo, + issue_number: prNumber, + }); + + const existing = comments.find(c => + c.user.login === 'github-actions[bot]' && c.body.includes(marker) + ); + + if (existing) { + await github.rest.issues.updateComment({ + ...context.repo, + comment_id: existing.id, + body, + }); + core.info('Updated existing comment: ' + existing.html_url); + } else { + const { data: created } = await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body, + }); + core.info('Created comment: ' + created.html_url); + } diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index f3479b86cb6..00000000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,615 +0,0 @@ -name: E2E - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - push: - branches: - - 'main' - - 'develop' - - 'next' - paths: - - '.github/workflows/e2e.yml' - - 'packages/**' - - '**/yarn.lock' - pull_request: - branches: - - '**' - paths: - - '.github/workflows/e2e.yml' - - 'packages/**' - - '**/yarn.lock' - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/create-github-app-token@v1 - continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - id: app-token - with: - app-id: ${{ vars.NOCOBASE_APP_ID }} - private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} - repositories: nocobase,pro-plugins,plugin-workflow-approval - skip-token-revoke: true - - uses: actions/checkout@v4 - - name: Checkout pro-plugins - continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - uses: actions/checkout@v4 - with: - repository: nocobase/pro-plugins - ref: main - path: packages/pro-plugins - fetch-depth: 0 - token: ${{ steps.app-token.outputs.token }} - - name: Checkout plugin-workflow-approval - continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - uses: actions/checkout@v4 - with: - repository: nocobase/plugin-workflow-approval - ref: main - path: packages/pro-plugins/@nocobase/plugin-workflow-approval - fetch-depth: 0 - token: ${{ steps.app-token.outputs.token }} - - run: | - cd packages/pro-plugins && - if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then - git checkout ${{ github.head_ref || github.ref_name }} - else - if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then - git checkout ${{ github.event.pull_request.base.ref }} - else - git checkout main - fi - fi - cd ../../ - cd packages/pro-plugins/@nocobase/plugin-workflow-approval && - if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then - git checkout ${{ github.head_ref || github.ref_name }} - else - if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then - git checkout ${{ github.event.pull_request.base.ref }} - else - git checkout main - fi - fi - cd ../../../../ - continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - - name: Git logs - run: | - cd packages/pro-plugins && git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit -n 10 - continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'yarn' - - run: yarn - - run: yarn build - env: - __E2E__: true # e2e will be reusing this workflow, so we need to set this flag to true - - uses: actions/upload-artifact@v4 - with: - name: build-artifact - path: | - packages/**/es/ - packages/**/lib/ - packages/**/dist/ - !packages/**/node_modules/** - # timeout-minutes: 20 - - # core-and-plugins: - # name: Core and plugins - # needs: build - # runs-on: ubuntu-latest - # container: node:20 - # services: - # # Label used to access the service container - # postgres: - # # Docker Hub image - # image: postgres:11 - # # Provide the password for postgres - # env: - # POSTGRES_USER: nocobase - # POSTGRES_PASSWORD: password - # # Set health checks to wait until postgres has started - # options: >- - # --health-cmd pg_isready - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 - # steps: - # - uses: actions/checkout@v4 - # - name: Checkout pro-plugins - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # uses: actions/checkout@v4 - # with: - # repository: nocobase/pro-plugins - # ref: main - # path: packages/pro-plugins - # fetch-depth: 0 - # ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} - # - run: | - # cd packages/pro-plugins && - # if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then - # git checkout ${{ github.head_ref || github.ref_name }} - # else - # if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then - # git checkout ${{ github.event.pull_request.base.ref }} - # else - # git checkout main - # fi - # fi - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # - name: Git logs - # run: | - # cd packages/pro-plugins && git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit -n 10 - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # - name: Set variables - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # run: | - # APPEND_PRESET_LOCAL_PLUGINS=$(find ./packages/pro-plugins/@nocobase -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed 's/^plugin-//' | tr '\n' ',' | sed 's/,$//') - # echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT - # id: vars - - # - name: Get yarn cache directory path - # id: yarn-cache-dir-path - # run: echo "::set-output name=dir::$(yarn cache dir)" - # - uses: actions/cache@v4 - # id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - # with: - # path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - # restore-keys: | - # ${{ runner.os }}-yarn- - - # - run: yarn - - # - name: Download build artifact - # uses: actions/download-artifact@v4 - # with: - # name: build-artifact - # path: packages - - # - run: npx playwright install chromium --with-deps - # - name: Test with postgres - # run: yarn e2e p-test --ignore 'packages/**/{plugin-data-source-main,plugin-workflow,plugin-workflow-*}/**/__e2e__/**/*.test.ts' - # env: - # __E2E__: true - # APP_ENV: production - # LOGGER_LEVEL: error - # DB_DIALECT: postgres - # DB_HOST: postgres - # DB_PORT: 5432 - # DB_USER: nocobase - # DB_PASSWORD: password - # DB_DATABASE: nocobase - # APPEND_PRESET_LOCAL_PLUGINS: ${{ steps.vars.outputs.var2 }} - # ENCRYPTION_FIELD_KEY: 1%&glK;- - # --health-cmd pg_isready - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 - # steps: - # - uses: actions/checkout@v4 - # - name: Checkout pro-plugins - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # uses: actions/checkout@v4 - # with: - # repository: nocobase/pro-plugins - # ref: main - # path: packages/pro-plugins - # fetch-depth: 0 - # ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} - # - run: | - # cd packages/pro-plugins && - # if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then - # git checkout ${{ github.head_ref || github.ref_name }} - # else - # if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then - # git checkout ${{ github.event.pull_request.base.ref }} - # else - # git checkout main - # fi - # fi - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # - name: Git logs - # run: | - # cd packages/pro-plugins && git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit -n 10 - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # - name: Set variables - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # run: | - # APPEND_PRESET_LOCAL_PLUGINS=$(find ./packages/pro-plugins/@nocobase -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed 's/^plugin-//' | tr '\n' ',' | sed 's/,$//') - # echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT - # id: vars - - # - name: Get yarn cache directory path - # id: yarn-cache-dir-path - # run: echo "::set-output name=dir::$(yarn cache dir)" - # - uses: actions/cache@v4 - # id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - # with: - # path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - # restore-keys: | - # ${{ runner.os }}-yarn- - - # - run: yarn - - # - name: Download build artifact - # uses: actions/download-artifact@v4 - # with: - # name: build-artifact - # path: packages - - # - run: npx playwright install chromium --with-deps - # - name: Test with postgres - # run: yarn e2e p-test --match 'packages/**/{plugin-workflow,plugin-workflow-*}/**/__e2e__/**/*.test.ts' --ignore 'packages/**/{plugin-workflow-approval,plugin-workflow-manual}/**/__e2e__/**/*.test.ts' - # env: - # __E2E__: true - # APP_ENV: production - # LOGGER_LEVEL: error - # DB_DIALECT: postgres - # DB_HOST: postgres - # DB_PORT: 5432 - # DB_USER: nocobase - # DB_PASSWORD: password - # DB_DATABASE: nocobase - # APPEND_PRESET_LOCAL_PLUGINS: ${{ steps.vars.outputs.var2 }} - # ENCRYPTION_FIELD_KEY: 1%&glK;- - # --health-cmd pg_isready - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 - # steps: - # - uses: actions/create-github-app-token@v1 - # id: app-token - # with: - # app-id: ${{ vars.NOCOBASE_APP_ID }} - # private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} - # repositories: nocobase,pro-plugins,plugin-workflow-approval - # skip-token-revoke: true - # - uses: actions/checkout@v4 - # - name: Checkout pro-plugins - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # uses: actions/checkout@v4 - # with: - # repository: nocobase/pro-plugins - # ref: main - # path: packages/pro-plugins - # fetch-depth: 0 - # token: ${{ steps.app-token.outputs.token }} - # - name: Checkout plugin-workflow-approval - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # uses: actions/checkout@v4 - # with: - # repository: nocobase/plugin-workflow-approval - # ref: main - # path: packages/pro-plugins/@nocobase/plugin-workflow-approval - # fetch-depth: 0 - # token: ${{ steps.app-token.outputs.token }} - # - run: | - # cd packages/pro-plugins && - # if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then - # git checkout ${{ github.head_ref || github.ref_name }} - # else - # if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then - # git checkout ${{ github.event.pull_request.base.ref }} - # else - # git checkout main - # fi - # fi - # cd ../../ - # cd packages/pro-plugins/@nocobase/plugin-workflow-approval && - # if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then - # git checkout ${{ github.head_ref || github.ref_name }} - # else - # if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then - # git checkout ${{ github.event.pull_request.base.ref }} - # else - # git checkout main - # fi - # fi - # cd ../../../../ - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # - name: Git logs - # run: | - # cd packages/pro-plugins/@nocobase/plugin-workflow-approval && git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit -n 10 - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # - name: Set variables - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # run: | - # APPEND_PRESET_LOCAL_PLUGINS=$(find ./packages/pro-plugins/@nocobase -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed 's/^plugin-//' | tr '\n' ',' | sed 's/,$//') - # echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT - # id: vars - - # - name: Get yarn cache directory path - # id: yarn-cache-dir-path - # run: echo "::set-output name=dir::$(yarn cache dir)" - # - uses: actions/cache@v4 - # id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - # with: - # path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - # restore-keys: | - # ${{ runner.os }}-yarn- - - # - run: yarn - - # - name: Download build artifact - # uses: actions/download-artifact@v4 - # with: - # name: build-artifact - # path: packages - - # - run: npx playwright install chromium --with-deps - # - name: Test with postgres - # run: yarn e2e p-test --match 'packages/**/plugin-workflow-approval/**/__e2e__/**/*.test.ts' - # env: - # __E2E__: true - # APP_ENV: production - # LOGGER_LEVEL: error - # DB_DIALECT: postgres - # DB_HOST: postgres - # DB_PORT: 5432 - # DB_USER: nocobase - # DB_PASSWORD: password - # DB_DATABASE: nocobase - # APPEND_PRESET_LOCAL_PLUGINS: ${{ steps.vars.outputs.var2 }} - # ENCRYPTION_FIELD_KEY: 1%&glK;- - # --health-cmd pg_isready - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 - # steps: - # - uses: actions/checkout@v4 - # - name: Checkout pro-plugins - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # uses: actions/checkout@v4 - # with: - # repository: nocobase/pro-plugins - # ref: main - # path: packages/pro-plugins - # fetch-depth: 0 - # ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} - # - run: | - # cd packages/pro-plugins && - # if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then - # git checkout ${{ github.head_ref || github.ref_name }} - # else - # if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then - # git checkout ${{ github.event.pull_request.base.ref }} - # else - # git checkout main - # fi - # fi - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # - name: Git logs - # run: | - # cd packages/pro-plugins && git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit -n 10 - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # - name: Set variables - # continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true - # run: | - # APPEND_PRESET_LOCAL_PLUGINS=$(find ./packages/pro-plugins/@nocobase -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed 's/^plugin-//' | tr '\n' ',' | sed 's/,$//') - # echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT - # id: vars - - # - name: Get yarn cache directory path - # id: yarn-cache-dir-path - # run: echo "::set-output name=dir::$(yarn cache dir)" - # - uses: actions/cache@v4 - # id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - # with: - # path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - # restore-keys: | - # ${{ runner.os }}-yarn- - - # - run: yarn - - # - name: Download build artifact - # uses: actions/download-artifact@v4 - # with: - # name: build-artifact - # path: packages - - # - run: npx playwright install chromium --with-deps - # - name: Test with postgres - # run: yarn e2e p-test --match 'packages/**/plugin-data-source-main/**/__e2e__/**/*.test.ts' - # env: - # __E2E__: true - # APP_ENV: production - # LOGGER_LEVEL: error - # DB_DIALECT: postgres - # DB_HOST: postgres - # DB_PORT: 5432 - # DB_USER: nocobase - # DB_PASSWORD: password - # DB_DATABASE: nocobase - # APPEND_PRESET_LOCAL_PLUGINS: ${{ steps.vars.outputs.var2 }} - # ENCRYPTION_FIELD_KEY: 1%&glK; item.replace(/__e2e__/g, '\_\_e2e\_\_')).join('\n'); - # if (body.length > 65535) - # body = body.substring(0, 65000) + `... ${body.length - 65000} more characters`; - # return body; - # } - # const { data: response } = await github.rest.issues.createComment({ - # ...context.repo, - # issue_number: prNumber, - # body: formatComment([ - # `### Tests results for "${jobName}"`, - # reportMd, - # '', - # `Full [HTML report](${reportUrl}). Merge [workflow run](${mergeWorkflowUrl}).` - # ]), - # }); - # core.info('Posted comment: ' + response.html_url); - - # timeout-minutes: 5 diff --git a/scripts/check-plugin-peer-deps.sh b/scripts/check-plugin-peer-deps.sh new file mode 100755 index 00000000000..dd338a5d649 --- /dev/null +++ b/scripts/check-plugin-peer-deps.sh @@ -0,0 +1,211 @@ +#!/bin/bash +# +# Check that all @nocobase/plugin-* imports used in non-test source files +# are declared as peerDependencies (or dependencies/devDependencies). +# +# Usage: +# bash scripts/check-plugin-peer-deps.sh [--whitelist ] +# +# Exit codes: +# 0 - all plugins pass +# 1 - missing peerDependencies found +# 2 - invalid arguments +# +# stdout: JSON array of {plugin, missing: [deps]} for CI consumption +# stderr: human-readable table summary + +set -euo pipefail + +# --- argument parsing --- +PLUGINS_DIR="" +WHITELIST_FILE="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --whitelist) + WHITELIST_FILE="$2" + shift 2 + ;; + -*) + echo "Unknown option: $1" >&2 + exit 2 + ;; + *) + if [[ -z "$PLUGINS_DIR" ]]; then + PLUGINS_DIR="$1" + else + echo "Unexpected argument: $1" >&2 + exit 2 + fi + shift + ;; + esac +done + +if [[ -z "$PLUGINS_DIR" ]]; then + echo "Usage: $0 [--whitelist ]" >&2 + exit 2 +fi + +if [[ ! -d "$PLUGINS_DIR" ]]; then + echo "Directory not found: $PLUGINS_DIR" >&2 + exit 2 +fi + +# --- whitelist loading --- +is_whitelisted() { + local plugin_name="$1" + local dep="$2" + if [[ -n "$WHITELIST_FILE" && -f "$WHITELIST_FILE" ]]; then + node -e " + const wl = require('$(realpath "$WHITELIST_FILE")'); + const list = wl['$plugin_name'] || []; + process.exit(list.includes('$dep') ? 0 : 1); + " 2>/dev/null + return $? + fi + return 1 +} + +# --- test file detection --- +is_test_only() { + local src_dir="$1" + local dep="$2" + + # check non-test files + local src_hits + src_hits=$(grep -rl --include="*.ts" --include="*.tsx" \ + -E "from ['\"]${dep}(/[^'\"]*)?['\"]|require\(['\"]${dep}(/[^'\"]*)?['\"]\)" \ + "$src_dir/" 2>/dev/null \ + | grep -v '__tests__' | grep -v '__test__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '/test/' \ + | head -1) || true + + if [[ -n "$src_hits" ]]; then + return 1 # found in src → not test-only + fi + + # check test files + local test_hits + test_hits=$(grep -rl --include="*.ts" --include="*.tsx" \ + -E "from ['\"]${dep}(/[^'\"]*)?['\"]|require\(['\"]${dep}(/[^'\"]*)?['\"]\)" \ + "$src_dir/" 2>/dev/null \ + | grep -E '__tests__|__test__|\.test\.|\.spec\.|/test/' \ + | head -1) || true + + if [[ -n "$test_hits" ]]; then + return 0 # only in test files + fi + + return 1 # not found at all (shouldn't happen, but treat as non-test) +} + +# --- main check --- +errors=() # array of "plugin_name|dep1,dep2,..." +json_results="[]" +has_errors=false + +for plugin_dir in "$PLUGINS_DIR"/*/; do + [[ ! -d "$plugin_dir" ]] && continue + + plugin_name=$(basename "$plugin_dir") + pkg_json="$plugin_dir/package.json" + + [[ ! -f "$pkg_json" ]] && continue + [[ ! -d "$plugin_dir/src" ]] && continue + + # get package name for self-reference exclusion + pkg_scope=$(node -e "console.log(require('$(realpath "$pkg_json")').name)" 2>/dev/null) || continue + + # get all declared @nocobase/plugin-* deps (peer + deps + devDeps) + all_declared=$(node -e " + const pkg = require('$(realpath "$pkg_json")'); + const deps = [ + ...Object.keys(pkg.peerDependencies || {}), + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.devDependencies || {}) + ].filter(k => k.startsWith('@nocobase/plugin-')); + deps.forEach(p => console.log(p)); + " 2>/dev/null | sort -u) || true + + # get actual @nocobase/plugin-* imports from src/ + imported=$(grep -rhoE "from ['\"]@nocobase/plugin-[^'\"]+['\"]|require\(['\"]@nocobase/plugin-[^'\"]+['\"]\)" \ + "$plugin_dir/src/" 2>/dev/null \ + | sed -E "s/from ['\"]//; s/require\(['\"]//; s/['\"].*//; s/\/client$//; s/\/server$//" \ + | sort -u \ + | grep -v "^${pkg_scope}$" \ + | grep -v "^${pkg_scope}/") || true + + # find missing deps + missing_deps=() + while IFS= read -r imp; do + [[ -z "$imp" ]] && continue + base_pkg=$(echo "$imp" | sed -E 's|^(@nocobase/plugin-[^/]+).*|\1|') + + # skip if already declared + if echo "$all_declared" | grep -qx "$base_pkg"; then + continue + fi + + # skip if test-only + if is_test_only "$plugin_dir/src" "$base_pkg"; then + continue + fi + + # skip if whitelisted + if is_whitelisted "$plugin_name" "$base_pkg"; then + continue + fi + + # deduplicate + local_dup=false + for existing in "${missing_deps[@]+"${missing_deps[@]}"}"; do + if [[ "$existing" == "$base_pkg" ]]; then + local_dup=true + break + fi + done + if [[ "$local_dup" == "false" ]]; then + missing_deps+=("$base_pkg") + fi + done <<< "$imported" + + if [[ ${#missing_deps[@]} -gt 0 ]]; then + has_errors=true + deps_joined=$(IFS=','; echo "${missing_deps[*]}") + errors+=("${plugin_name}|${deps_joined}") + + # build JSON entry + deps_json=$(printf '%s\n' "${missing_deps[@]}" | jq -R . | jq -s .) + json_results=$(echo "$json_results" | jq --arg p "$plugin_name" --argjson d "$deps_json" '. + [{plugin: $p, missing: $d}]') + fi +done + +# --- output --- + +# stdout: JSON for CI +echo "$json_results" | jq -c . + +# stderr: human-readable summary +if [[ "$has_errors" == "true" ]]; then + echo "" >&2 + echo "=============================================" >&2 + echo " Missing plugin-* peerDependencies detected!" >&2 + echo "=============================================" >&2 + echo "" >&2 + printf "%-50s %s\n" "Plugin" "Missing peerDependencies" >&2 + printf "%-50s %s\n" "------" "------------------------" >&2 + for entry in "${errors[@]}"; do + plugin="${entry%%|*}" + deps="${entry#*|}" + printf "%-50s %s\n" "$plugin" "$deps" >&2 + done + echo "" >&2 + echo "Please add the missing peerDependencies to the respective package.json files." >&2 + echo "" >&2 + exit 1 +else + echo "" >&2 + echo "All plugins in $PLUGINS_DIR have their plugin-* peerDependencies properly declared." >&2 + echo "" >&2 + exit 0 +fi diff --git a/scripts/peer-deps-whitelist.json b/scripts/peer-deps-whitelist.json new file mode 100644 index 00000000000..f1f986498d3 --- /dev/null +++ b/scripts/peer-deps-whitelist.json @@ -0,0 +1,3 @@ +{ + "plugin-commercial": ["@nocobase/plugin-license"] +}