diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 4e9bccda..e6b55204 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -1,27 +1,27 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "jetbrains.resharper.globaltools": { - "version": "2026.1.4", - "commands": [ - "jb" - ], - "rollForward": false - }, - "dotnet-reportgenerator-globaltool": { - "version": "5.5.10", - "commands": [ - "reportgenerator" - ], - "rollForward": false - }, - "husky": { - "version": "0.9.1", - "commands": [ - "husky" - ], - "rollForward": false - } - } +{ + "version": 1, + "isRoot": true, + "tools": { + "jetbrains.resharper.globaltools": { + "version": "2026.2.0", + "commands": [ + "jb" + ], + "rollForward": false + }, + "dotnet-reportgenerator-globaltool": { + "version": "5.5.10", + "commands": [ + "reportgenerator" + ], + "rollForward": false + }, + "husky": { + "version": "0.9.1", + "commands": [ + "husky" + ], + "rollForward": false + } + } } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dedb0887..53db6e6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,363 +1,363 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -env: - DOTNET_NOLOGO: true - TEST_RESULTS_DIR: artifacts/test-results - -jobs: - pr-checks: - if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Restore - run: dotnet restore JD.AI.slnx - - - name: Build (Release) - run: > - dotnet build JD.AI.slnx - --configuration Release - --no-restore - /p:ContinuousIntegrationBuild=true - - - name: Verify formatting - run: > - dotnet format JD.AI.slnx - --severity warn - --verify-no-changes - - - name: Test with coverage - timeout-minutes: 15 - run: > - dotnet test JD.AI.slnx - --configuration Release - --no-build - --results-directory ${{ env.TEST_RESULTS_DIR }} - --filter "Category!=Integration&Category!=MlModel&Category!=FlakyEnvironment&Category!=E2E" - --collect:"XPlat Code Coverage" - --blame-hang-timeout 5m - -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura - DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Include="[JD.AI*]*" - DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Exclude="[*Tests]*" - - - name: Install ReportGenerator - if: always() - run: dotnet tool update -g dotnet-reportgenerator-globaltool --version 5.5.4 - - - name: Combine coverage reports - if: always() - shell: bash - run: | - REPORTS=$(find "${TEST_RESULTS_DIR}" -type f -name "coverage.cobertura.xml" 2>/dev/null | tr '\n' ';') - if [ -z "$REPORTS" ] || [ "$REPORTS" = ";" ]; then - echo "No coverage files found — skipping report generation." - exit 0 - fi - reportgenerator \ - -reports:"$REPORTS" \ - -targetdir:"coverage-report" \ - -reporttypes:"HtmlInline;Cobertura;TextSummary;Badges" \ - -assemblyfilters:"+JD.AI*;-*Tests*" \ - -filefilters:"-**/*.Tests/*;-**/*Tests*/**" - echo "COVERAGE_SUMMARY<> $GITHUB_ENV - awk '/^[^ ]/ && NR>1 {exit} {print}' coverage-report/Summary.txt >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - - - name: Upload coverage report - if: always() - uses: actions/upload-artifact@v7 - with: - name: coverage-report - path: coverage-report - - - name: Upload to Codecov - if: always() - continue-on-error: true - uses: codecov/codecov-action@v7 - with: - files: coverage-report/Cobertura.xml - disable_search: true - handle_no_reports_found: true - fail_ci_if_error: false - - - name: Enforce coverage floor - if: always() - shell: bash - env: - MIN_LINE_COVERAGE: "60" - run: | - if [ ! -f coverage-report/Cobertura.xml ]; then - echo "Coverage report missing." - exit 1 - fi - python - <<'PY' - import os - import xml.etree.ElementTree as ET - - min_cov = float(os.environ["MIN_LINE_COVERAGE"]) - rate = float(ET.parse("coverage-report/Cobertura.xml").getroot().attrib["line-rate"]) * 100 - if rate < min_cov: - raise SystemExit(f"Coverage {rate:.2f}% is below {min_cov:.2f}%") - print(f"Coverage {rate:.2f}% meets threshold {min_cov:.2f}%") - PY - - - name: Add coverage summary to PR - if: always() && github.event_name == 'pull_request' - uses: marocchino/sticky-pull-request-comment@v3.0.5 - with: - recreate: true - message: | - ## Code Coverage - ``` - ${{ env.COVERAGE_SUMMARY }} - ``` - - release: - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - permissions: - contents: write - packages: write - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Restore - run: dotnet restore JD.AI.slnx - - - name: Determine version (NBGV) - id: nbgv - uses: dotnet/nbgv@293b36ef77470c667752f376b0515d02a92ba66c # node24 (unreleased past v0.5.1; pin SHA until v0.5.2 ships) - with: - setAllVars: true - - - name: Build (Release) - run: > - dotnet build JD.AI.slnx - --configuration Release - --no-restore - /p:ContinuousIntegrationBuild=true - - - name: Test (Release) - timeout-minutes: 15 - run: > - dotnet test JD.AI.slnx - --configuration Release - --no-build - --results-directory ${{ env.TEST_RESULTS_DIR }} - --filter "Category!=Integration&Category!=MlModel&Category!=FlakyEnvironment&Category!=E2E" - --collect:"XPlat Code Coverage" - --blame-hang-timeout 5m - -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura - DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Include="[JD.AI*]*" - DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Exclude="[*Tests]*" - - - name: Install ReportGenerator - if: always() - run: dotnet tool update -g dotnet-reportgenerator-globaltool --version 5.5.4 - - - name: Combine coverage reports - if: always() - shell: bash - run: | - REPORTS=$(find "${TEST_RESULTS_DIR}" -type f -name "coverage.cobertura.xml" 2>/dev/null | tr '\n' ';') - if [ -z "$REPORTS" ] || [ "$REPORTS" = ";" ]; then - echo "No coverage files found — skipping report generation." - exit 0 - fi - reportgenerator \ - -reports:"$REPORTS" \ - -targetdir:"coverage-report" \ - -reporttypes:"HtmlInline;Cobertura;TextSummary;Badges" \ - -assemblyfilters:"+JD.AI*;-*Tests*" \ - -filefilters:"-**/*.Tests/*;-**/*Tests*/**" - - - name: Upload coverage report - if: always() - uses: actions/upload-artifact@v7 - with: - name: coverage-report - path: coverage-report - - - name: Upload to Codecov - if: always() - uses: codecov/codecov-action@v7 - with: - files: coverage-report/Cobertura.xml - token: ${{ secrets.CODECOV_TOKEN }} - disable_search: true - handle_no_reports_found: true - fail_ci_if_error: false - - - name: Enforce coverage floor - if: always() - shell: bash - env: - MIN_LINE_COVERAGE: "60" - run: | - if [ ! -f coverage-report/Cobertura.xml ]; then - echo "Coverage report missing." - exit 1 - fi - python - <<'PY' - import os - import xml.etree.ElementTree as ET - - min_cov = float(os.environ["MIN_LINE_COVERAGE"]) - rate = float(ET.parse("coverage-report/Cobertura.xml").getroot().attrib["line-rate"]) * 100 - if rate < min_cov: - raise SystemExit(f"Coverage {rate:.2f}% is below {min_cov:.2f}%") - print(f"Coverage {rate:.2f}% meets threshold {min_cov:.2f}%") - PY - - - name: Pack - run: > - dotnet pack JD.AI.slnx - --configuration Release - --no-build - --output ./artifacts - /p:ContinuousIntegrationBuild=true - - - name: Upload packages - uses: actions/upload-artifact@v7 - with: - name: nuget-packages - path: ./artifacts/*.nupkg - - - name: Push to NuGet.org - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - run: | - if [ -n "$NUGET_API_KEY" ]; then - dotnet nuget push ./artifacts/*.nupkg \ - --api-key "$NUGET_API_KEY" \ - --source https://api.nuget.org/v3/index.json \ - --skip-duplicate - else - echo "Skipping NuGet.org push: API key not set." - fi - - - name: Push to GitHub Packages - run: | - dotnet nuget push "./artifacts/*.nupkg" \ - --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ - --api-key "${{ secrets.GITHUB_TOKEN }}" \ - --skip-duplicate - - - name: Create git tag - shell: bash - run: | - set -euo pipefail - TAG="v${NBGV_NuGetPackageVersion}" - if git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then - echo "Tag $TAG already exists — skipping." - else - git tag "$TAG" - git push origin "$TAG" - echo "Created tag $TAG" - fi - - - name: Create GitHub Release - uses: softprops/action-gh-release@v3 - with: - tag_name: v${{ env.NBGV_NuGetPackageVersion }} - name: Release v${{ env.NBGV_NuGetPackageVersion }} - files: | - ./artifacts/*.nupkg - generate_release_notes: true - outputs: - version: ${{ env.NBGV_NuGetPackageVersion }} - - publish-binaries: - needs: release - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - strategy: - fail-fast: false - matrix: - include: - - rid: win-x64 - os: windows-latest - archive: zip - - rid: win-arm64 - os: windows-latest - archive: zip - - rid: linux-x64 - os: ubuntu-latest - archive: tar.gz - - rid: linux-arm64 - os: ubuntu-latest - archive: tar.gz - - rid: osx-x64 - os: macos-latest - archive: tar.gz - - rid: osx-arm64 - os: macos-latest - archive: tar.gz - runs-on: ${{ matrix.os }} - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Publish self-contained binary - env: - GITHUB_ACTIONS: "false" - run: > - dotnet publish src/JD.AI/JD.AI.csproj - --configuration Release - --runtime ${{ matrix.rid }} - --self-contained - -p:PublishSingleFile=true - -p:IncludeNativeLibrariesForSelfExtract=true - -p:ContinuousIntegrationBuild=true - --output ./publish - - - name: Archive (zip) - if: matrix.archive == 'zip' - shell: pwsh - run: Compress-Archive -Path ./publish/* -DestinationPath ./jdai-${{ matrix.rid }}.zip - - - name: Archive (tar.gz) - if: matrix.archive == 'tar.gz' - run: tar -czf ./jdai-${{ matrix.rid }}.tar.gz -C ./publish . - - - name: Upload to GitHub Release - uses: softprops/action-gh-release@v3 - with: - tag_name: v${{ needs.release.outputs.version }} - files: | - ./jdai-${{ matrix.rid }}.${{ matrix.archive }} +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +env: + DOTNET_NOLOGO: true + TEST_RESULTS_DIR: artifacts/test-results + +jobs: + pr-checks: + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Restore + run: dotnet restore JD.AI.slnx + + - name: Build (Release) + run: > + dotnet build JD.AI.slnx + --configuration Release + --no-restore + /p:ContinuousIntegrationBuild=true + + - name: Verify formatting + run: > + dotnet format JD.AI.slnx + --severity warn + --verify-no-changes + + - name: Test with coverage + timeout-minutes: 15 + run: > + dotnet test JD.AI.slnx + --configuration Release + --no-build + --results-directory ${{ env.TEST_RESULTS_DIR }} + --filter "Category!=Integration&Category!=MlModel&Category!=FlakyEnvironment&Category!=E2E" + --collect:"XPlat Code Coverage" + --blame-hang-timeout 5m + -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura + DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Include="[JD.AI*]*" + DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Exclude="[*Tests]*" + + - name: Install ReportGenerator + if: always() + run: dotnet tool update -g dotnet-reportgenerator-globaltool --version 5.5.4 + + - name: Combine coverage reports + if: always() + shell: bash + run: | + REPORTS=$(find "${TEST_RESULTS_DIR}" -type f -name "coverage.cobertura.xml" 2>/dev/null | tr '\n' ';') + if [ -z "$REPORTS" ] || [ "$REPORTS" = ";" ]; then + echo "No coverage files found — skipping report generation." + exit 0 + fi + reportgenerator \ + -reports:"$REPORTS" \ + -targetdir:"coverage-report" \ + -reporttypes:"HtmlInline;Cobertura;TextSummary;Badges" \ + -assemblyfilters:"+JD.AI*;-*Tests*" \ + -filefilters:"-**/*.Tests/*;-**/*Tests*/**" + echo "COVERAGE_SUMMARY<> $GITHUB_ENV + awk '/^[^ ]/ && NR>1 {exit} {print}' coverage-report/Summary.txt >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v7 + with: + name: coverage-report + path: coverage-report + + - name: Upload to Codecov + if: always() + continue-on-error: true + uses: codecov/codecov-action@v7 + with: + files: coverage-report/Cobertura.xml + disable_search: true + handle_no_reports_found: true + fail_ci_if_error: false + + - name: Enforce coverage floor + if: always() + shell: bash + env: + MIN_LINE_COVERAGE: "60" + run: | + if [ ! -f coverage-report/Cobertura.xml ]; then + echo "Coverage report missing." + exit 1 + fi + python - <<'PY' + import os + import xml.etree.ElementTree as ET + + min_cov = float(os.environ["MIN_LINE_COVERAGE"]) + rate = float(ET.parse("coverage-report/Cobertura.xml").getroot().attrib["line-rate"]) * 100 + if rate < min_cov: + raise SystemExit(f"Coverage {rate:.2f}% is below {min_cov:.2f}%") + print(f"Coverage {rate:.2f}% meets threshold {min_cov:.2f}%") + PY + + - name: Add coverage summary to PR + if: always() && github.event_name == 'pull_request' + uses: marocchino/sticky-pull-request-comment@v3.0.5 + with: + recreate: true + message: | + ## Code Coverage + ``` + ${{ env.COVERAGE_SUMMARY }} + ``` + + release: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Restore + run: dotnet restore JD.AI.slnx + + - name: Determine version (NBGV) + id: nbgv + uses: dotnet/nbgv@293b36ef77470c667752f376b0515d02a92ba66c # node24 (unreleased past v0.5.1; pin SHA until v0.5.2 ships) + with: + setAllVars: true + + - name: Build (Release) + run: > + dotnet build JD.AI.slnx + --configuration Release + --no-restore + /p:ContinuousIntegrationBuild=true + + - name: Test (Release) + timeout-minutes: 15 + run: > + dotnet test JD.AI.slnx + --configuration Release + --no-build + --results-directory ${{ env.TEST_RESULTS_DIR }} + --filter "Category!=Integration&Category!=MlModel&Category!=FlakyEnvironment&Category!=E2E" + --collect:"XPlat Code Coverage" + --blame-hang-timeout 5m + -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura + DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Include="[JD.AI*]*" + DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Exclude="[*Tests]*" + + - name: Install ReportGenerator + if: always() + run: dotnet tool update -g dotnet-reportgenerator-globaltool --version 5.5.4 + + - name: Combine coverage reports + if: always() + shell: bash + run: | + REPORTS=$(find "${TEST_RESULTS_DIR}" -type f -name "coverage.cobertura.xml" 2>/dev/null | tr '\n' ';') + if [ -z "$REPORTS" ] || [ "$REPORTS" = ";" ]; then + echo "No coverage files found — skipping report generation." + exit 0 + fi + reportgenerator \ + -reports:"$REPORTS" \ + -targetdir:"coverage-report" \ + -reporttypes:"HtmlInline;Cobertura;TextSummary;Badges" \ + -assemblyfilters:"+JD.AI*;-*Tests*" \ + -filefilters:"-**/*.Tests/*;-**/*Tests*/**" + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v7 + with: + name: coverage-report + path: coverage-report + + - name: Upload to Codecov + if: always() + uses: codecov/codecov-action@v7 + with: + files: coverage-report/Cobertura.xml + token: ${{ secrets.CODECOV_TOKEN }} + disable_search: true + handle_no_reports_found: true + fail_ci_if_error: false + + - name: Enforce coverage floor + if: always() + shell: bash + env: + MIN_LINE_COVERAGE: "60" + run: | + if [ ! -f coverage-report/Cobertura.xml ]; then + echo "Coverage report missing." + exit 1 + fi + python - <<'PY' + import os + import xml.etree.ElementTree as ET + + min_cov = float(os.environ["MIN_LINE_COVERAGE"]) + rate = float(ET.parse("coverage-report/Cobertura.xml").getroot().attrib["line-rate"]) * 100 + if rate < min_cov: + raise SystemExit(f"Coverage {rate:.2f}% is below {min_cov:.2f}%") + print(f"Coverage {rate:.2f}% meets threshold {min_cov:.2f}%") + PY + + - name: Pack + run: > + dotnet pack JD.AI.slnx + --configuration Release + --no-build + --output ./artifacts + /p:ContinuousIntegrationBuild=true + + - name: Upload packages + uses: actions/upload-artifact@v7 + with: + name: nuget-packages + path: ./artifacts/*.nupkg + + - name: Push to NuGet.org + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + if [ -n "$NUGET_API_KEY" ]; then + dotnet nuget push ./artifacts/*.nupkg \ + --api-key "$NUGET_API_KEY" \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate + else + echo "Skipping NuGet.org push: API key not set." + fi + + - name: Push to GitHub Packages + run: | + dotnet nuget push "./artifacts/*.nupkg" \ + --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ + --api-key "${{ secrets.GITHUB_TOKEN }}" \ + --skip-duplicate + + - name: Create git tag + shell: bash + run: | + set -euo pipefail + TAG="v${NBGV_NuGetPackageVersion}" + if git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists — skipping." + else + git tag "$TAG" + git push origin "$TAG" + echo "Created tag $TAG" + fi + + - name: Create GitHub Release + uses: softprops/action-gh-release@v3 + with: + tag_name: v${{ env.NBGV_NuGetPackageVersion }} + name: Release v${{ env.NBGV_NuGetPackageVersion }} + files: | + ./artifacts/*.nupkg + generate_release_notes: true + outputs: + version: ${{ env.NBGV_NuGetPackageVersion }} + + publish-binaries: + needs: release + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + strategy: + fail-fast: false + matrix: + include: + - rid: win-x64 + os: windows-latest + archive: zip + - rid: win-arm64 + os: windows-latest + archive: zip + - rid: linux-x64 + os: ubuntu-latest + archive: tar.gz + - rid: linux-arm64 + os: ubuntu-latest + archive: tar.gz + - rid: osx-x64 + os: macos-latest + archive: tar.gz + - rid: osx-arm64 + os: macos-latest + archive: tar.gz + runs-on: ${{ matrix.os }} + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Publish self-contained binary + env: + GITHUB_ACTIONS: "false" + run: > + dotnet publish src/JD.AI/JD.AI.csproj + --configuration Release + --runtime ${{ matrix.rid }} + --self-contained + -p:PublishSingleFile=true + -p:IncludeNativeLibrariesForSelfExtract=true + -p:ContinuousIntegrationBuild=true + --output ./publish + + - name: Archive (zip) + if: matrix.archive == 'zip' + shell: pwsh + run: Compress-Archive -Path ./publish/* -DestinationPath ./jdai-${{ matrix.rid }}.zip + + - name: Archive (tar.gz) + if: matrix.archive == 'tar.gz' + run: tar -czf ./jdai-${{ matrix.rid }}.tar.gz -C ./publish . + + - name: Upload to GitHub Release + uses: softprops/action-gh-release@v3 + with: + tag_name: v${{ needs.release.outputs.version }} + files: | + ./jdai-${{ matrix.rid }}.${{ matrix.archive }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1f5ed4fb..d082cf45 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,37 +1,37 @@ -name: CodeQL - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - - cron: '0 6 * * 1' - -permissions: - security-events: write - contents: read - -jobs: - analyze: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Initialize CodeQL - uses: github/codeql-action/init@v4.37.1 - with: - languages: csharp - - - name: Build - run: dotnet build --configuration Release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.37.1 +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '0 6 * * 1' + +permissions: + security-events: write + contents: read + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4.37.1 + with: + languages: csharp + + - name: Build + run: dotnet build --configuration Release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4.37.1 diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index 2496542c..d4d3f6a0 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -1,131 +1,131 @@ -name: Container Images - -on: - push: - branches: [main] - tags: - - "v*" - workflow_dispatch: - -env: - REGISTRY: ghcr.io - -jobs: - publish: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write - attestations: write - strategy: - fail-fast: false - matrix: - include: - - component: gateway - dockerfile: deploy/docker/Dockerfile.gateway - - component: daemon - dockerfile: deploy/docker/Dockerfile.daemon - - component: tui - dockerfile: deploy/docker/Dockerfile.tui - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Set up Docker Buildx - shell: bash - run: | - docker buildx version - docker buildx create --name jdai-builder --use || docker buildx use jdai-builder - docker buildx inspect --bootstrap - - - name: Normalize image name - id: image - shell: bash - run: | - owner="${GITHUB_REPOSITORY_OWNER,,}" - echo "name=${{ env.REGISTRY }}/${owner}/jd.ai-${{ matrix.component }}" >> "$GITHUB_OUTPUT" - - - name: Login to GHCR - uses: docker/login-action@v4.4.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v6.2.0 - with: - images: ${{ steps.image.outputs.name }} - tags: | - type=sha - type=raw,value=latest,enable={{is_default_branch}} - type=ref,event=tag - - - name: Build and push - id: build - uses: docker/build-push-action@v7 - with: - context: . - file: ${{ matrix.dockerfile }} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Generate SBOM - uses: anchore/sbom-action@v0 - with: - image: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }} - format: spdx-json - output-file: sbom-${{ matrix.component }}.spdx.json - - - name: Upload SBOM artifact - uses: actions/upload-artifact@v7 - with: - name: sbom-${{ matrix.component }} - path: sbom-${{ matrix.component }}.spdx.json - - - name: Attest SBOM to image - uses: actions/attest-sbom@v4 - continue-on-error: true - with: - subject-name: ${{ steps.image.outputs.name }} - subject-digest: ${{ steps.build.outputs.digest }} - sbom-path: sbom-${{ matrix.component }}.spdx.json - - helm-lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Install Helm - shell: bash - run: | - set -euo pipefail - HELM_VERSION="v3.19.0" - ARCHIVE="helm-${HELM_VERSION}-linux-amd64.tar.gz" - URL="https://get.helm.sh/${ARCHIVE}" - for attempt in 1 2 3; do - if curl -fsSL --retry 3 --retry-delay 2 "$URL" -o "$ARCHIVE"; then - break - fi - if [ "$attempt" -eq 3 ]; then - echo "Failed to download Helm from $URL after retries." - exit 1 - fi - sleep $((attempt * 5)) - done - tar -xzf "$ARCHIVE" - sudo mv linux-amd64/helm /usr/local/bin/helm - helm version --short - - - name: Helm lint - run: helm lint deploy/helm/jdai - - - name: Helm template (dry-run) - run: helm template jdai deploy/helm/jdai --debug > /dev/null - +name: Container Images + +on: + push: + branches: [main] + tags: + - "v*" + workflow_dispatch: + +env: + REGISTRY: ghcr.io + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + attestations: write + strategy: + fail-fast: false + matrix: + include: + - component: gateway + dockerfile: deploy/docker/Dockerfile.gateway + - component: daemon + dockerfile: deploy/docker/Dockerfile.daemon + - component: tui + dockerfile: deploy/docker/Dockerfile.tui + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Docker Buildx + shell: bash + run: | + docker buildx version + docker buildx create --name jdai-builder --use || docker buildx use jdai-builder + docker buildx inspect --bootstrap + + - name: Normalize image name + id: image + shell: bash + run: | + owner="${GITHUB_REPOSITORY_OWNER,,}" + echo "name=${{ env.REGISTRY }}/${owner}/jd.ai-${{ matrix.component }}" >> "$GITHUB_OUTPUT" + + - name: Login to GHCR + uses: docker/login-action@v4.4.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v6.2.0 + with: + images: ${{ steps.image.outputs.name }} + tags: | + type=sha + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=tag + + - name: Build and push + id: build + uses: docker/build-push-action@v7 + with: + context: . + file: ${{ matrix.dockerfile }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Generate SBOM + uses: anchore/sbom-action@v0 + with: + image: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }} + format: spdx-json + output-file: sbom-${{ matrix.component }}.spdx.json + + - name: Upload SBOM artifact + uses: actions/upload-artifact@v7 + with: + name: sbom-${{ matrix.component }} + path: sbom-${{ matrix.component }}.spdx.json + + - name: Attest SBOM to image + uses: actions/attest-sbom@v4 + continue-on-error: true + with: + subject-name: ${{ steps.image.outputs.name }} + subject-digest: ${{ steps.build.outputs.digest }} + sbom-path: sbom-${{ matrix.component }}.spdx.json + + helm-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Install Helm + shell: bash + run: | + set -euo pipefail + HELM_VERSION="v3.19.0" + ARCHIVE="helm-${HELM_VERSION}-linux-amd64.tar.gz" + URL="https://get.helm.sh/${ARCHIVE}" + for attempt in 1 2 3; do + if curl -fsSL --retry 3 --retry-delay 2 "$URL" -o "$ARCHIVE"; then + break + fi + if [ "$attempt" -eq 3 ]; then + echo "Failed to download Helm from $URL after retries." + exit 1 + fi + sleep $((attempt * 5)) + done + tar -xzf "$ARCHIVE" + sudo mv linux-amd64/helm /usr/local/bin/helm + helm version --short + + - name: Helm lint + run: helm lint deploy/helm/jdai + + - name: Helm template (dry-run) + run: helm template jdai deploy/helm/jdai --debug > /dev/null + diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 0f14ccfa..a1920327 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,25 +1,25 @@ -name: Dependency Review - -on: - pull_request: - branches: [main] - -permissions: - contents: read - pull-requests: write - -jobs: - dependency-review: - name: Dependency Review - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Dependency Review - uses: actions/dependency-review-action@v5 - with: - fail-on-severity: moderate - deny-licenses: GPL-2.0, GPL-3.0, AGPL-3.0 - comment-summary-in-pr: always +name: Dependency Review + +on: + pull_request: + branches: [main] + +permissions: + contents: read + pull-requests: write + +jobs: + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Dependency Review + uses: actions/dependency-review-action@v5 + with: + fail-on-severity: moderate + deny-licenses: GPL-2.0, GPL-3.0, AGPL-3.0 + comment-summary-in-pr: always diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ab64b8a6..b8f37bf9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,98 +1,98 @@ -name: Publish docs - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - actions: read - pages: write - id-token: write - pull-requests: write - -concurrency: - group: pages - cancel-in-progress: false - -jobs: - validate-docs: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Restore - run: dotnet restore JD.AI.slnx - - - name: Install docfx - run: dotnet tool update -g docfx - - - name: Build documentation - run: docfx docs/docfx.json - - - name: Validate output - run: | - if [ ! -d "docs/_site" ]; then - echo "::error::Documentation build did not create _site directory" - exit 1 - fi - HTML_COUNT=$(find docs/_site -name "*.html" | wc -l) - echo "Generated $HTML_COUNT HTML files" - if [ "$HTML_COUNT" -lt 1 ]; then - echo "::error::No HTML files were generated" - exit 1 - fi - echo "::notice::Documentation validation passed! Generated $HTML_COUNT pages." - - - name: Upload preview - uses: actions/upload-artifact@v7 - with: - name: documentation-preview - path: docs/_site - retention-days: 7 - - publish-docs: - if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Restore - run: dotnet restore JD.AI.slnx - - - name: Install docfx - run: dotnet tool update -g docfx - - - name: Build documentation - run: docfx docs/docfx.json - - - name: Upload artifact - uses: actions/upload-pages-artifact@v5 - with: - path: docs/_site - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 +name: Publish docs + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + actions: read + pages: write + id-token: write + pull-requests: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + validate-docs: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Restore + run: dotnet restore JD.AI.slnx + + - name: Install docfx + run: dotnet tool update -g docfx + + - name: Build documentation + run: docfx docs/docfx.json + + - name: Validate output + run: | + if [ ! -d "docs/_site" ]; then + echo "::error::Documentation build did not create _site directory" + exit 1 + fi + HTML_COUNT=$(find docs/_site -name "*.html" | wc -l) + echo "Generated $HTML_COUNT HTML files" + if [ "$HTML_COUNT" -lt 1 ]; then + echo "::error::No HTML files were generated" + exit 1 + fi + echo "::notice::Documentation validation passed! Generated $HTML_COUNT pages." + + - name: Upload preview + uses: actions/upload-artifact@v7 + with: + name: documentation-preview + path: docs/_site + retention-days: 7 + + publish-docs: + if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Restore + run: dotnet restore JD.AI.slnx + + - name: Install docfx + run: dotnet tool update -g docfx + + - name: Build documentation + run: docfx docs/docfx.json + + - name: Upload artifact + uses: actions/upload-pages-artifact@v5 + with: + path: docs/_site + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/e2e-smoke.yml b/.github/workflows/e2e-smoke.yml index 559556af..4f43e2a7 100644 --- a/.github/workflows/e2e-smoke.yml +++ b/.github/workflows/e2e-smoke.yml @@ -1,208 +1,208 @@ -name: E2E Smoke - -on: - pull_request: - branches: [main] - push: - branches: [main] - workflow_dispatch: - -env: - DOTNET_NOLOGO: true - -permissions: - contents: read - checks: write - -jobs: - ui-smoke: - name: UI Smoke (Reqnroll + Playwright) - runs-on: ubuntu-latest - timeout-minutes: 35 - - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Restore UI specs - run: dotnet restore tests/JD.AI.Specs.UI/JD.AI.Specs.UI.csproj - - - name: Build UI specs - run: > - dotnet build tests/JD.AI.Specs.UI/JD.AI.Specs.UI.csproj - --configuration Release - --no-restore - -p:IsTestProject=true - /p:ContinuousIntegrationBuild=true - - - name: Install Playwright browsers - shell: pwsh - run: ./tests/JD.AI.Specs.UI/bin/Release/net10.0/playwright.ps1 install --with-deps - - - name: Start dashboard host - run: | - nohup dotnet run --project src/JD.AI.Dashboard.Wasm --configuration Release --urls http://127.0.0.1:5189 > dashboard.log 2>&1 & - echo $! > dashboard.pid - for i in $(seq 1 60); do - if curl -sf http://127.0.0.1:5189 >/dev/null 2>&1; then - echo "Dashboard is ready (attempt $i)" - exit 0 - fi - sleep 1 - done - echo "Dashboard failed to start" - cat dashboard.log || true - exit 1 - - - name: Run UI smoke tests - env: - DASHBOARD_BASE_URL: http://127.0.0.1:5189 - run: > - dotnet test tests/JD.AI.Specs.UI/JD.AI.Specs.UI.csproj - --configuration Release - --no-build - -p:IsTestProject=true - --filter "Category=smoke" - --logger "trx;LogFileName=ui-smoke-results.trx" - - - name: Publish UI test results - uses: EnricoMi/publish-unit-test-result-action@v2.24.0 - if: always() - with: - files: | - tests/JD.AI.Specs.UI/TestResults/**/*.trx - check_name: UI Smoke Test Results - comment_mode: off - - - name: Upload UI artifacts - uses: actions/upload-artifact@v7 - if: always() - with: - name: ui-smoke-artifacts - path: | - dashboard.log - tests/JD.AI.Specs.UI/TestResults/**/*.trx - - - name: Stop dashboard host - if: always() - run: | - if [ -f dashboard.pid ]; then - kill "$(cat dashboard.pid)" || true - fi - - vhs-smoke: - name: CLI Smoke (VHS) - runs-on: ubuntu-latest - timeout-minutes: 25 - - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Publish JD.AI CLI (self-contained) - run: > - dotnet publish src/JD.AI/JD.AI.csproj - --configuration Release - --runtime linux-x64 - --self-contained true - -p:PublishSingleFile=true - --output artifacts/vhs-smoke - - - name: Make CLI executable - run: chmod +x artifacts/vhs-smoke/JD.AI - - - name: Install VHS, ttyd, and ffmpeg - run: | - sudo apt-get update - sudo apt-get install -y ffmpeg - sudo mkdir -p /etc/apt/keyrings - curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg - echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list - sudo apt-get update - sudo apt-get install -y vhs - TTYD_VERSION="1.7.7" - curl -fsSL "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.x86_64" -o /usr/local/bin/ttyd - sudo chmod +x /usr/local/bin/ttyd - - - name: Render VHS smoke tape - run: vhs docs/tapes/ci-cli-smoke.tape - - - name: Render plugin help fallback screenshot - run: | - if [ -f docs/images/ci-plugin-help.png ]; then - echo "ci-plugin-help.png already exists." - exit 0 - fi - - echo "ci-plugin-help.png missing after primary tape; rendering fallback screenshot." - cat > /tmp/ci-plugin-help-fallback.tape <<'EOF' - Output "/tmp/ci-plugin-help-fallback.gif" - Set Shell "bash" - Set Width 1200 - Set Height 700 - Set FontSize 14 - Set TypingSpeed 0ms - Set Padding 20 - Set Theme "Dracula" - Type "./artifacts/vhs-smoke/JD.AI plugin --help" - Enter - Sleep 5s - Screenshot docs/images/ci-plugin-help.png - EOF - sed -i 's/^ //' /tmp/ci-plugin-help-fallback.tape - vhs /tmp/ci-plugin-help-fallback.tape - - - name: Verify generated VHS outputs - run: | - expected=( - "docs/images/ci-cli-smoke.gif" - "docs/images/ci-mcp-help.png" - "docs/images/ci-plugin-help.png" - ) - - for attempt in $(seq 1 10); do - missing=() - for file in "${expected[@]}"; do - if [ ! -f "$file" ]; then - missing+=("$file") - fi - done - - if [ ${#missing[@]} -eq 0 ]; then - echo "All VHS outputs found." - exit 0 - fi - - echo "Waiting for VHS outputs (attempt ${attempt}/10): ${missing[*]}" - sleep 1 - done - - echo "Missing VHS outputs after retries:" - for file in "${missing[@]}"; do - echo " - $file" - done - ls -la docs/images || true - exit 1 - - - name: Upload VHS artifacts - uses: actions/upload-artifact@v7 - with: - name: vhs-smoke-artifacts - path: | - docs/images/ci-cli-smoke.gif - docs/images/ci-mcp-help.png - docs/images/ci-plugin-help.png +name: E2E Smoke + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +env: + DOTNET_NOLOGO: true + +permissions: + contents: read + checks: write + +jobs: + ui-smoke: + name: UI Smoke (Reqnroll + Playwright) + runs-on: ubuntu-latest + timeout-minutes: 35 + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Restore UI specs + run: dotnet restore tests/JD.AI.Specs.UI/JD.AI.Specs.UI.csproj + + - name: Build UI specs + run: > + dotnet build tests/JD.AI.Specs.UI/JD.AI.Specs.UI.csproj + --configuration Release + --no-restore + -p:IsTestProject=true + /p:ContinuousIntegrationBuild=true + + - name: Install Playwright browsers + shell: pwsh + run: ./tests/JD.AI.Specs.UI/bin/Release/net10.0/playwright.ps1 install --with-deps + + - name: Start dashboard host + run: | + nohup dotnet run --project src/JD.AI.Dashboard.Wasm --configuration Release --urls http://127.0.0.1:5189 > dashboard.log 2>&1 & + echo $! > dashboard.pid + for i in $(seq 1 60); do + if curl -sf http://127.0.0.1:5189 >/dev/null 2>&1; then + echo "Dashboard is ready (attempt $i)" + exit 0 + fi + sleep 1 + done + echo "Dashboard failed to start" + cat dashboard.log || true + exit 1 + + - name: Run UI smoke tests + env: + DASHBOARD_BASE_URL: http://127.0.0.1:5189 + run: > + dotnet test tests/JD.AI.Specs.UI/JD.AI.Specs.UI.csproj + --configuration Release + --no-build + -p:IsTestProject=true + --filter "Category=smoke" + --logger "trx;LogFileName=ui-smoke-results.trx" + + - name: Publish UI test results + uses: EnricoMi/publish-unit-test-result-action@v2.24.0 + if: always() + with: + files: | + tests/JD.AI.Specs.UI/TestResults/**/*.trx + check_name: UI Smoke Test Results + comment_mode: off + + - name: Upload UI artifacts + uses: actions/upload-artifact@v7 + if: always() + with: + name: ui-smoke-artifacts + path: | + dashboard.log + tests/JD.AI.Specs.UI/TestResults/**/*.trx + + - name: Stop dashboard host + if: always() + run: | + if [ -f dashboard.pid ]; then + kill "$(cat dashboard.pid)" || true + fi + + vhs-smoke: + name: CLI Smoke (VHS) + runs-on: ubuntu-latest + timeout-minutes: 25 + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Publish JD.AI CLI (self-contained) + run: > + dotnet publish src/JD.AI/JD.AI.csproj + --configuration Release + --runtime linux-x64 + --self-contained true + -p:PublishSingleFile=true + --output artifacts/vhs-smoke + + - name: Make CLI executable + run: chmod +x artifacts/vhs-smoke/JD.AI + + - name: Install VHS, ttyd, and ffmpeg + run: | + sudo apt-get update + sudo apt-get install -y ffmpeg + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg + echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list + sudo apt-get update + sudo apt-get install -y vhs + TTYD_VERSION="1.7.7" + curl -fsSL "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.x86_64" -o /usr/local/bin/ttyd + sudo chmod +x /usr/local/bin/ttyd + + - name: Render VHS smoke tape + run: vhs docs/tapes/ci-cli-smoke.tape + + - name: Render plugin help fallback screenshot + run: | + if [ -f docs/images/ci-plugin-help.png ]; then + echo "ci-plugin-help.png already exists." + exit 0 + fi + + echo "ci-plugin-help.png missing after primary tape; rendering fallback screenshot." + cat > /tmp/ci-plugin-help-fallback.tape <<'EOF' + Output "/tmp/ci-plugin-help-fallback.gif" + Set Shell "bash" + Set Width 1200 + Set Height 700 + Set FontSize 14 + Set TypingSpeed 0ms + Set Padding 20 + Set Theme "Dracula" + Type "./artifacts/vhs-smoke/JD.AI plugin --help" + Enter + Sleep 5s + Screenshot docs/images/ci-plugin-help.png + EOF + sed -i 's/^ //' /tmp/ci-plugin-help-fallback.tape + vhs /tmp/ci-plugin-help-fallback.tape + + - name: Verify generated VHS outputs + run: | + expected=( + "docs/images/ci-cli-smoke.gif" + "docs/images/ci-mcp-help.png" + "docs/images/ci-plugin-help.png" + ) + + for attempt in $(seq 1 10); do + missing=() + for file in "${expected[@]}"; do + if [ ! -f "$file" ]; then + missing+=("$file") + fi + done + + if [ ${#missing[@]} -eq 0 ]; then + echo "All VHS outputs found." + exit 0 + fi + + echo "Waiting for VHS outputs (attempt ${attempt}/10): ${missing[*]}" + sleep 1 + done + + echo "Missing VHS outputs after retries:" + for file in "${missing[@]}"; do + echo " - $file" + done + ls -la docs/images || true + exit 1 + + - name: Upload VHS artifacts + uses: actions/upload-artifact@v7 + with: + name: vhs-smoke-artifacts + path: | + docs/images/ci-cli-smoke.gif + docs/images/ci-mcp-help.png + docs/images/ci-plugin-help.png diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 86db5a6d..f0d7765e 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,134 +1,134 @@ -name: Integration Tests - -on: - push: - branches: [main] - schedule: - - cron: '0 6 * * *' - workflow_dispatch: - inputs: - chat_model: - description: 'Ollama chat model to use' - required: false - default: 'llama3.2:1b' - embedding_model: - description: 'Ollama embedding model to use' - required: false - default: 'all-minilm:22m' - -env: - DOTNET_NOLOGO: true - OLLAMA_CHAT_MODEL: ${{ inputs.chat_model || 'llama3.2:1b' }} - OLLAMA_EMBEDDING_MODEL: ${{ inputs.embedding_model || 'all-minilm:22m' }} - EXTENSIONS_INTEGRATION_TESTS: 'true' - TUI_INTEGRATION_TESTS: 'true' - -jobs: - integration: - name: Integration Tests (Ollama) - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: read - checks: write - services: - ollama: - image: ollama/ollama:latest - ports: - - 11434:11434 - - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Wait for Ollama - run: | - echo "⏳ Waiting for Ollama to be ready..." - for i in $(seq 1 60); do - if curl -sf http://localhost:11434/api/version > /dev/null 2>&1; then - echo "✅ Ollama is ready (attempt $i)" - curl -sf http://localhost:11434/api/version - exit 0 - fi - echo " Attempt $i/60 — waiting..." - sleep 3 - done - echo "❌ Ollama failed to start" - exit 1 - - - name: Pull models - run: | - echo "📥 Pulling chat model: $OLLAMA_CHAT_MODEL" - curl -sf http://localhost:11434/api/pull \ - -d "{\"name\": \"$OLLAMA_CHAT_MODEL\"}" | \ - while IFS= read -r line; do - status=$(echo "$line" | jq -r '.status // empty' 2>/dev/null) - [ -n "$status" ] && echo " $status" - done - echo "✅ Chat model ready" - - echo "📥 Pulling embedding model: $OLLAMA_EMBEDDING_MODEL" - curl -sf http://localhost:11434/api/pull \ - -d "{\"name\": \"$OLLAMA_EMBEDDING_MODEL\"}" | \ - while IFS= read -r line; do - status=$(echo "$line" | jq -r '.status // empty' 2>/dev/null) - [ -n "$status" ] && echo " $status" - done - echo "✅ Embedding model ready" - - - name: Verify models - run: | - echo "🔍 Available models:" - curl -sf http://localhost:11434/api/tags | jq -r '.models[].name' - - - name: Restore - run: dotnet restore JD.AI.slnx - - - name: Build - run: > - dotnet build JD.AI.slnx - --configuration Release - --no-restore - /p:ContinuousIntegrationBuild=true - - - name: Run integration tests - run: > - dotnet test JD.AI.slnx - --configuration Release - --no-build - --filter "Category=Integration" - --verbosity normal - --logger "trx;LogFileName=integration-results.trx" - -- - RunConfiguration.TestSessionTimeout=600000 - - - name: Publish test results - uses: EnricoMi/publish-unit-test-result-action@v2.24.0 - if: always() - with: - files: | - **/TestResults/**/*.trx - check_name: Integration Test Results - comment_title: Integration Test Results - comment_mode: always - # Do not block on failures — live-model integration tests are environment- - # dependent and may flake. Results are always posted to the PR for review. - thresholds: '{}' - - - name: Summary - if: always() - run: | - echo "## 🧪 Integration Test Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY - echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Chat Model | \`$OLLAMA_CHAT_MODEL\` |" >> $GITHUB_STEP_SUMMARY - echo "| Embedding Model | \`$OLLAMA_EMBEDDING_MODEL\` |" >> $GITHUB_STEP_SUMMARY - echo "| Trigger | \`${{ github.event_name }}\` |" >> $GITHUB_STEP_SUMMARY +name: Integration Tests + +on: + push: + branches: [main] + schedule: + - cron: '0 6 * * *' + workflow_dispatch: + inputs: + chat_model: + description: 'Ollama chat model to use' + required: false + default: 'llama3.2:1b' + embedding_model: + description: 'Ollama embedding model to use' + required: false + default: 'all-minilm:22m' + +env: + DOTNET_NOLOGO: true + OLLAMA_CHAT_MODEL: ${{ inputs.chat_model || 'llama3.2:1b' }} + OLLAMA_EMBEDDING_MODEL: ${{ inputs.embedding_model || 'all-minilm:22m' }} + EXTENSIONS_INTEGRATION_TESTS: 'true' + TUI_INTEGRATION_TESTS: 'true' + +jobs: + integration: + name: Integration Tests (Ollama) + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + checks: write + services: + ollama: + image: ollama/ollama:latest + ports: + - 11434:11434 + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Wait for Ollama + run: | + echo "⏳ Waiting for Ollama to be ready..." + for i in $(seq 1 60); do + if curl -sf http://localhost:11434/api/version > /dev/null 2>&1; then + echo "✅ Ollama is ready (attempt $i)" + curl -sf http://localhost:11434/api/version + exit 0 + fi + echo " Attempt $i/60 — waiting..." + sleep 3 + done + echo "❌ Ollama failed to start" + exit 1 + + - name: Pull models + run: | + echo "📥 Pulling chat model: $OLLAMA_CHAT_MODEL" + curl -sf http://localhost:11434/api/pull \ + -d "{\"name\": \"$OLLAMA_CHAT_MODEL\"}" | \ + while IFS= read -r line; do + status=$(echo "$line" | jq -r '.status // empty' 2>/dev/null) + [ -n "$status" ] && echo " $status" + done + echo "✅ Chat model ready" + + echo "📥 Pulling embedding model: $OLLAMA_EMBEDDING_MODEL" + curl -sf http://localhost:11434/api/pull \ + -d "{\"name\": \"$OLLAMA_EMBEDDING_MODEL\"}" | \ + while IFS= read -r line; do + status=$(echo "$line" | jq -r '.status // empty' 2>/dev/null) + [ -n "$status" ] && echo " $status" + done + echo "✅ Embedding model ready" + + - name: Verify models + run: | + echo "🔍 Available models:" + curl -sf http://localhost:11434/api/tags | jq -r '.models[].name' + + - name: Restore + run: dotnet restore JD.AI.slnx + + - name: Build + run: > + dotnet build JD.AI.slnx + --configuration Release + --no-restore + /p:ContinuousIntegrationBuild=true + + - name: Run integration tests + run: > + dotnet test JD.AI.slnx + --configuration Release + --no-build + --filter "Category=Integration" + --verbosity normal + --logger "trx;LogFileName=integration-results.trx" + -- + RunConfiguration.TestSessionTimeout=600000 + + - name: Publish test results + uses: EnricoMi/publish-unit-test-result-action@v2.24.0 + if: always() + with: + files: | + **/TestResults/**/*.trx + check_name: Integration Test Results + comment_title: Integration Test Results + comment_mode: always + # Do not block on failures — live-model integration tests are environment- + # dependent and may flake. Results are always posted to the PR for review. + thresholds: '{}' + + - name: Summary + if: always() + run: | + echo "## 🧪 Integration Test Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY + echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Chat Model | \`$OLLAMA_CHAT_MODEL\` |" >> $GITHUB_STEP_SUMMARY + echo "| Embedding Model | \`$OLLAMA_EMBEDDING_MODEL\` |" >> $GITHUB_STEP_SUMMARY + echo "| Trigger | \`${{ github.event_name }}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 255e11cd..bcde2091 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,44 +1,44 @@ -name: Auto Label - -on: - pull_request: - types: [opened, synchronize, reopened] - issues: - types: [opened, reopened] - -permissions: - contents: read - issues: write - pull-requests: write - -jobs: - label-pr: - name: Label Pull Request - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Label based on files - uses: actions/labeler@v6 - with: - configuration-path: .github/labeler.yml - repo-token: ${{ secrets.GITHUB_TOKEN }} - - label-size: - name: Label PR Size - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Label by size - uses: codelytv/pr-size-labeler@v1 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - xs_max_size: '10' - s_max_size: '100' - m_max_size: '500' - l_max_size: '1000' - fail_if_xl: 'false' +name: Auto Label + +on: + pull_request: + types: [opened, synchronize, reopened] + issues: + types: [opened, reopened] + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + label-pr: + name: Label Pull Request + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Label based on files + uses: actions/labeler@v6 + with: + configuration-path: .github/labeler.yml + repo-token: ${{ secrets.GITHUB_TOKEN }} + + label-size: + name: Label PR Size + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Label by size + uses: codelytv/pr-size-labeler@v1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + xs_max_size: '10' + s_max_size: '100' + m_max_size: '500' + l_max_size: '1000' + fail_if_xl: 'false' diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index fe28d1ae..61b4f4a4 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -1,219 +1,219 @@ -name: PR Validation - -on: - pull_request: - branches: [main] - paths-ignore: - - '**.md' - - 'docs/**' - - '.vscode/**' - - '.editorconfig' - workflow_dispatch: - -env: - DOTNET_NOLOGO: true - TEST_RESULTS_DIR: artifacts/test-results - -permissions: - contents: read - pull-requests: write - checks: write - -jobs: - validate-pr: - name: Validate PR - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Determine version (NBGV) - id: nbgv - uses: dotnet/nbgv@293b36ef77470c667752f376b0515d02a92ba66c # node24 (unreleased past v0.5.1; pin SHA until v0.5.2 ships) - with: - setAllVars: true - - - name: Restore - run: dotnet restore JD.AI.slnx - - - name: Build - run: > - dotnet build JD.AI.slnx - --configuration Release - --no-restore - /p:ContinuousIntegrationBuild=true - /p:Deterministic=true - - - name: Verify formatting - run: > - dotnet format JD.AI.slnx - --severity warn - --verify-no-changes - - - name: Run tests with coverage - run: > - dotnet test JD.AI.slnx - --configuration Release - --no-build - --results-directory ${{ env.TEST_RESULTS_DIR }} - --filter "Category!=Integration&Category!=MlModel&Category!=FlakyEnvironment&Category!=E2E" - --verbosity normal - --logger trx - --collect:"XPlat Code Coverage" - --blame-hang-timeout 5m - -- - DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura - DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Include="[JD.AI*]*" - DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Exclude="[*Tests]*" - - - name: Install ReportGenerator - if: always() - run: dotnet tool update -g dotnet-reportgenerator-globaltool --version 5.5.4 - - - name: Generate coverage report - if: always() - shell: bash - run: | - REPORTS=$(find "${TEST_RESULTS_DIR}" -type f -name "coverage.cobertura.xml" 2>/dev/null | tr '\n' ';') - if [ -z "$REPORTS" ] || [ "$REPORTS" = ";" ]; then - echo "No coverage files found — skipping report generation." - exit 0 - fi - reportgenerator \ - -reports:"$REPORTS" \ - -targetdir:"coverage-report" \ - -reporttypes:"HtmlInline;Cobertura;TextSummary;Badges" \ - -assemblyfilters:"+JD.AI*;-*Tests*" \ - -filefilters:"-**/*.Tests/*;-**/*Tests*/**" - - - name: Upload coverage report - if: always() - uses: actions/upload-artifact@v7 - with: - name: coverage-report - path: coverage-report - - - name: Enforce coverage floor - if: always() - shell: bash - env: - MIN_LINE_COVERAGE: "60" - run: | - if [ ! -f coverage-report/Cobertura.xml ]; then - echo "Coverage report missing." - exit 1 - fi - python - <<'PY' - import os - import xml.etree.ElementTree as ET - - min_cov = float(os.environ["MIN_LINE_COVERAGE"]) - rate = float(ET.parse("coverage-report/Cobertura.xml").getroot().attrib["line-rate"]) * 100 - if rate < min_cov: - raise SystemExit(f"Coverage {rate:.2f}% is below {min_cov:.2f}%") - print(f"Coverage {rate:.2f}% meets threshold {min_cov:.2f}%") - PY - - - name: Coverage gate — check changed source files - if: always() && github.event_name == 'pull_request' - shell: bash - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - # Determine if this PR has a coverage-override label - LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "") - if echo "$LABELS" | grep -q "coverage-override"; then - echo "ℹ️ coverage-override label found — skipping coverage gate." - exit 0 - fi - - # Get changed runtime source files (not tests) - CHANGED_SRC=$(git diff --name-only origin/main...HEAD -- 'src/**/*.cs' 2>/dev/null || \ - git diff --name-only HEAD~1...HEAD -- 'src/**/*.cs' 2>/dev/null || echo "") - - if [ -z "$CHANGED_SRC" ]; then - echo "✅ No runtime source files changed — coverage gate passed." - exit 0 - fi - - echo "📁 Changed source files:" - echo "$CHANGED_SRC" - - # Check if test files were also changed - CHANGED_TESTS=$(git diff --name-only origin/main...HEAD -- 'tests/**/*.cs' 2>/dev/null || \ - git diff --name-only HEAD~1...HEAD -- 'tests/**/*.cs' 2>/dev/null || echo "") - - if [ -z "$CHANGED_TESTS" ]; then - echo "" - echo "⚠️ Runtime source files were modified but no test files changed." - echo " Please add or update tests for the changed code, or add the" - echo " 'coverage-override' label to this PR to bypass this check." - echo "" - echo "Changed files requiring tests:" - echo "$CHANGED_SRC" - exit 1 - fi - - echo "✅ Test files updated alongside source changes — coverage gate passed." - echo "Changed test files:" - echo "$CHANGED_TESTS" - - - name: Publish test results - uses: EnricoMi/publish-unit-test-result-action@v2.24.0 - if: always() - with: - files: | - ${{ env.TEST_RESULTS_DIR }}/**/*.trx - check_name: Test Results - - - name: Dry-run NuGet packaging - run: | - echo "📦 Performing dry-run of NuGet packaging..." - mkdir -p ./dry-run-packages - dotnet pack JD.AI.slnx \ - --configuration Release \ - --no-build \ - --output ./dry-run-packages \ - /p:ContinuousIntegrationBuild=true - PACKAGE_COUNT=$(ls -1 ./dry-run-packages/*.nupkg 2>/dev/null | wc -l) - if [ "$PACKAGE_COUNT" -eq 0 ]; then - echo "❌ No packages were created" - exit 1 - fi - echo "📦 Packaged files:" - ls -lh ./dry-run-packages/ - - - name: Upload dry-run packages - uses: actions/upload-artifact@v7 - with: - name: dry-run-packages - path: ./dry-run-packages/*.nupkg - retention-days: 7 - - - name: PR Summary - if: always() - run: | - echo "## 🎯 PR Validation Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ **Version**: $NBGV_NuGetPackageVersion" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 📦 Packages" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if ls ./dry-run-packages/*.nupkg 1> /dev/null 2>&1; then - echo "The following packages will be created on merge:" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - ls -1 ./dry-run-packages/*.nupkg | xargs -n1 basename >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ No packages were created during the dry-run." >> $GITHUB_STEP_SUMMARY - fi +name: PR Validation + +on: + pull_request: + branches: [main] + paths-ignore: + - '**.md' + - 'docs/**' + - '.vscode/**' + - '.editorconfig' + workflow_dispatch: + +env: + DOTNET_NOLOGO: true + TEST_RESULTS_DIR: artifacts/test-results + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + validate-pr: + name: Validate PR + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Determine version (NBGV) + id: nbgv + uses: dotnet/nbgv@293b36ef77470c667752f376b0515d02a92ba66c # node24 (unreleased past v0.5.1; pin SHA until v0.5.2 ships) + with: + setAllVars: true + + - name: Restore + run: dotnet restore JD.AI.slnx + + - name: Build + run: > + dotnet build JD.AI.slnx + --configuration Release + --no-restore + /p:ContinuousIntegrationBuild=true + /p:Deterministic=true + + - name: Verify formatting + run: > + dotnet format JD.AI.slnx + --severity warn + --verify-no-changes + + - name: Run tests with coverage + run: > + dotnet test JD.AI.slnx + --configuration Release + --no-build + --results-directory ${{ env.TEST_RESULTS_DIR }} + --filter "Category!=Integration&Category!=MlModel&Category!=FlakyEnvironment&Category!=E2E" + --verbosity normal + --logger trx + --collect:"XPlat Code Coverage" + --blame-hang-timeout 5m + -- + DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura + DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Include="[JD.AI*]*" + DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Exclude="[*Tests]*" + + - name: Install ReportGenerator + if: always() + run: dotnet tool update -g dotnet-reportgenerator-globaltool --version 5.5.4 + + - name: Generate coverage report + if: always() + shell: bash + run: | + REPORTS=$(find "${TEST_RESULTS_DIR}" -type f -name "coverage.cobertura.xml" 2>/dev/null | tr '\n' ';') + if [ -z "$REPORTS" ] || [ "$REPORTS" = ";" ]; then + echo "No coverage files found — skipping report generation." + exit 0 + fi + reportgenerator \ + -reports:"$REPORTS" \ + -targetdir:"coverage-report" \ + -reporttypes:"HtmlInline;Cobertura;TextSummary;Badges" \ + -assemblyfilters:"+JD.AI*;-*Tests*" \ + -filefilters:"-**/*.Tests/*;-**/*Tests*/**" + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v7 + with: + name: coverage-report + path: coverage-report + + - name: Enforce coverage floor + if: always() + shell: bash + env: + MIN_LINE_COVERAGE: "60" + run: | + if [ ! -f coverage-report/Cobertura.xml ]; then + echo "Coverage report missing." + exit 1 + fi + python - <<'PY' + import os + import xml.etree.ElementTree as ET + + min_cov = float(os.environ["MIN_LINE_COVERAGE"]) + rate = float(ET.parse("coverage-report/Cobertura.xml").getroot().attrib["line-rate"]) * 100 + if rate < min_cov: + raise SystemExit(f"Coverage {rate:.2f}% is below {min_cov:.2f}%") + print(f"Coverage {rate:.2f}% meets threshold {min_cov:.2f}%") + PY + + - name: Coverage gate — check changed source files + if: always() && github.event_name == 'pull_request' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # Determine if this PR has a coverage-override label + LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "") + if echo "$LABELS" | grep -q "coverage-override"; then + echo "ℹ️ coverage-override label found — skipping coverage gate." + exit 0 + fi + + # Get changed runtime source files (not tests) + CHANGED_SRC=$(git diff --name-only origin/main...HEAD -- 'src/**/*.cs' 2>/dev/null || \ + git diff --name-only HEAD~1...HEAD -- 'src/**/*.cs' 2>/dev/null || echo "") + + if [ -z "$CHANGED_SRC" ]; then + echo "✅ No runtime source files changed — coverage gate passed." + exit 0 + fi + + echo "📁 Changed source files:" + echo "$CHANGED_SRC" + + # Check if test files were also changed + CHANGED_TESTS=$(git diff --name-only origin/main...HEAD -- 'tests/**/*.cs' 2>/dev/null || \ + git diff --name-only HEAD~1...HEAD -- 'tests/**/*.cs' 2>/dev/null || echo "") + + if [ -z "$CHANGED_TESTS" ]; then + echo "" + echo "⚠️ Runtime source files were modified but no test files changed." + echo " Please add or update tests for the changed code, or add the" + echo " 'coverage-override' label to this PR to bypass this check." + echo "" + echo "Changed files requiring tests:" + echo "$CHANGED_SRC" + exit 1 + fi + + echo "✅ Test files updated alongside source changes — coverage gate passed." + echo "Changed test files:" + echo "$CHANGED_TESTS" + + - name: Publish test results + uses: EnricoMi/publish-unit-test-result-action@v2.24.0 + if: always() + with: + files: | + ${{ env.TEST_RESULTS_DIR }}/**/*.trx + check_name: Test Results + + - name: Dry-run NuGet packaging + run: | + echo "📦 Performing dry-run of NuGet packaging..." + mkdir -p ./dry-run-packages + dotnet pack JD.AI.slnx \ + --configuration Release \ + --no-build \ + --output ./dry-run-packages \ + /p:ContinuousIntegrationBuild=true + PACKAGE_COUNT=$(ls -1 ./dry-run-packages/*.nupkg 2>/dev/null | wc -l) + if [ "$PACKAGE_COUNT" -eq 0 ]; then + echo "❌ No packages were created" + exit 1 + fi + echo "📦 Packaged files:" + ls -lh ./dry-run-packages/ + + - name: Upload dry-run packages + uses: actions/upload-artifact@v7 + with: + name: dry-run-packages + path: ./dry-run-packages/*.nupkg + retention-days: 7 + + - name: PR Summary + if: always() + run: | + echo "## 🎯 PR Validation Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ **Version**: $NBGV_NuGetPackageVersion" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📦 Packages" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if ls ./dry-run-packages/*.nupkg 1> /dev/null 2>&1; then + echo "The following packages will be created on merge:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + ls -1 ./dry-run-packages/*.nupkg | xargs -n1 basename >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ No packages were created during the dry-run." >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/regenerate-screenshots.yml b/.github/workflows/regenerate-screenshots.yml index a2dd615e..f5bc0cbb 100644 --- a/.github/workflows/regenerate-screenshots.yml +++ b/.github/workflows/regenerate-screenshots.yml @@ -1,106 +1,106 @@ -name: Regenerate Screenshots - -on: - push: - branches: [main] - paths: - - 'docs/tapes/**' - - 'src/JD.AI/Rendering/**' - - 'src/JD.AI/Commands/**' - - 'src/JD.AI/Startup/**' - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: regenerate-screenshots - cancel-in-progress: true - -jobs: - regenerate: - name: Render VHS tapes - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Publish JD.AI CLI (self-contained) - run: > - dotnet publish src/JD.AI/JD.AI.csproj - --configuration Release - --runtime linux-x64 - --self-contained true - -p:PublishSingleFile=true - --output artifacts/cli - - - name: Make CLI executable and add to PATH - run: | - chmod +x artifacts/cli/JD.AI - # Demo tapes use "jdai", CI tape uses full path — support both - sudo ln -sf "$PWD/artifacts/cli/JD.AI" /usr/local/bin/jdai - - - name: Install VHS, ttyd, and ffmpeg - run: | - sudo apt-get update - sudo apt-get install -y ffmpeg - sudo mkdir -p /etc/apt/keyrings - curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg - echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list - sudo apt-get update - sudo apt-get install -y vhs - TTYD_VERSION="1.7.7" - curl -fsSL "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.x86_64" -o /usr/local/bin/ttyd - sudo chmod +x /usr/local/bin/ttyd - - - name: Render all VHS tapes - run: | - failed=0 - for tape in docs/tapes/*.tape; do - echo "=== Rendering: $tape ===" - if vhs "$tape"; then - echo " OK" - else - echo " FAILED (non-fatal)" - failed=$((failed + 1)) - fi - done - echo "Rendered all tapes ($failed failures)" - - - name: Check for changed screenshots - id: changes - run: | - git add docs/images/ - if git diff --cached --quiet; then - echo "has_changes=false" >> $GITHUB_OUTPUT - echo "No screenshot changes detected." - else - echo "has_changes=true" >> $GITHUB_OUTPUT - echo "Changed screenshots:" - git diff --cached --name-only - fi - - - name: Commit updated screenshots - if: steps.changes.outputs.has_changes == 'true' - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "docs: auto-regenerate VHS screenshots [skip ci]" - git push - - - name: Upload screenshots as artifact - uses: actions/upload-artifact@v7 - if: always() - with: - name: screenshots - path: docs/images/ - retention-days: 7 +name: Regenerate Screenshots + +on: + push: + branches: [main] + paths: + - 'docs/tapes/**' + - 'src/JD.AI/Rendering/**' + - 'src/JD.AI/Commands/**' + - 'src/JD.AI/Startup/**' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: regenerate-screenshots + cancel-in-progress: true + +jobs: + regenerate: + name: Render VHS tapes + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Publish JD.AI CLI (self-contained) + run: > + dotnet publish src/JD.AI/JD.AI.csproj + --configuration Release + --runtime linux-x64 + --self-contained true + -p:PublishSingleFile=true + --output artifacts/cli + + - name: Make CLI executable and add to PATH + run: | + chmod +x artifacts/cli/JD.AI + # Demo tapes use "jdai", CI tape uses full path — support both + sudo ln -sf "$PWD/artifacts/cli/JD.AI" /usr/local/bin/jdai + + - name: Install VHS, ttyd, and ffmpeg + run: | + sudo apt-get update + sudo apt-get install -y ffmpeg + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg + echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list + sudo apt-get update + sudo apt-get install -y vhs + TTYD_VERSION="1.7.7" + curl -fsSL "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.x86_64" -o /usr/local/bin/ttyd + sudo chmod +x /usr/local/bin/ttyd + + - name: Render all VHS tapes + run: | + failed=0 + for tape in docs/tapes/*.tape; do + echo "=== Rendering: $tape ===" + if vhs "$tape"; then + echo " OK" + else + echo " FAILED (non-fatal)" + failed=$((failed + 1)) + fi + done + echo "Rendered all tapes ($failed failures)" + + - name: Check for changed screenshots + id: changes + run: | + git add docs/images/ + if git diff --cached --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "No screenshot changes detected." + else + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "Changed screenshots:" + git diff --cached --name-only + fi + + - name: Commit updated screenshots + if: steps.changes.outputs.has_changes == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "docs: auto-regenerate VHS screenshots [skip ci]" + git push + + - name: Upload screenshots as artifact + uses: actions/upload-artifact@v7 + if: always() + with: + name: screenshots + path: docs/images/ + retention-days: 7 diff --git a/.github/workflows/train-model.yml b/.github/workflows/train-model.yml index de25de2b..77156594 100644 --- a/.github/workflows/train-model.yml +++ b/.github/workflows/train-model.yml @@ -1,192 +1,192 @@ -name: Train Intent Classifier Model - -# ── Triggers ───────────────────────────────────────────────────────── -on: - # Re-train on-demand: go to Actions → "Train Intent Classifier Model" → Run workflow - workflow_dispatch: - inputs: - examples: - description: "Number of AI-synthesized examples to generate (total after merge)" - type: number - default: 50 - required: false - commit_model: - description: "Commit the trained model back to the repository" - type: boolean - default: true - required: false - - # Re-train weekly — Sunday at midnight UTC - schedule: - - cron: "0 0 * * 0" - - # Re-train when training data or tooling changes (paths) - push: - paths: - - "tools/JD.AI.Workflows.Training/**" - - "src/JD.AI.Workflows/MlNetIntentClassifier.cs" - - "src/JD.AI.Workflows/Interfaces.cs" - - "src/JD.AI.Workflows/IntentClassifierHotSwapService.cs" - - ".github/workflows/train-model.yml" - -# ── Permissions ──────────────────────────────────────────────────── -permissions: - contents: write # needed to commit the trained model back - pull-requests: write # open PR if direct push is blocked - -# ── Jobs ─────────────────────────────────────────────────────────── -jobs: - train: - name: Synthesize + Train + Validate - runs-on: ubuntu-latest - timeout-minutes: 60 - - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Install .NET 10 - uses: actions/setup-dotnet@v6 - with: - dotnet-version: "10.0.x" - - # ── Ollama setup ──────────────────────────────────────────────── - - name: Install Ollama - run: | - curl -fsSL https://ollama.com/install.sh | sh - ollama --version - - - name: Start Ollama server - run: | - # Start Ollama as a background service (required for pull and training) - nohup ollama serve > /tmp/ollama.log 2>&1 & - # Give the server a moment to initialize - sleep 5 - # Verify it's running - curl -s http://localhost:11434/api/tags && echo "Ollama server is up" - - - name: Pull llama3.2:1b model - run: ollama pull llama3.2:1b - - # ── Build training tool ───────────────────────────────────────── - - name: Build training tool - run: dotnet build tools/JD.AI.Workflows.Training/JD.AI.Workflows.Training.csproj --configuration Release - - # ── Generate training data via Ollama ────────────────────────── - - name: Synthesize training examples - id: generate - env: - OLLAMA_HOST: "http://localhost:11434" - run: | - mkdir -p src/JD.AI.Workflows/Models - - dotnet run --project tools/JD.AI.Workflows.Training/JD.AI.Workflows.Training.csproj \ - --no-build --configuration Release -- \ - --ollama-generate ${{ github.event.inputs.examples || 50 }} \ - --model llama3.2:1b \ - --output src/JD.AI.Workflows/Models/intent_classifier.zip - - echo "examples=$(cat src/JD.AI.Workflows/Models/ollama_training_data.jsonl | wc -l)" >> $GITHUB_OUTPUT - - # ── Train model ─────────────────────────────────────────────── - - name: Train ML.NET model - env: - OLLAMA_HOST: "http://localhost:11434" - run: | - dotnet run --project tools/JD.AI.Workflows.Training/JD.AI.Workflows.Training.csproj \ - --no-build --configuration Release -- \ - --data src/JD.AI.Workflows/Models/ollama_training_data.jsonl \ - --output src/JD.AI.Workflows/Models/intent_classifier.zip - - # ── Benchmark vs TF-IDF baseline ───────────────────────────── - - name: Benchmark ML.NET vs TF-IDF - env: - OLLAMA_HOST: "http://localhost:11434" - run: | - dotnet run --project tools/JD.AI.Workflows.Training/JD.AI.Workflows.Training.csproj \ - --no-build --configuration Release -- \ - --benchmark - - # ── Validate model quality ──────────────────────────────────── - - name: Validate against Ollama - id: validate - env: - OLLAMA_HOST: "http://localhost:11434" - run: | - dotnet run --project tools/JD.AI.Workflows.Training/JD.AI.Workflows.Training.csproj \ - --no-build --configuration Release -- \ - --ollama-validate \ - --model llama3.2:1b \ - --data src/JD.AI.Workflows/Models/ollama_training_data.jsonl - - # Fail if more than 15% disagreement with Ollama - DISAGREEMENTS=$(cat validate_summary.txt 2>/dev/null || echo "0") - echo "Validation disagreements: $DISAGREEMENTS" - - # ── Commit model (optional) ──────────────────────────────────── - - name: Configure git - if: github.event.inputs.commit_model != 'false' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # Store model SHA for PR description - MODEL_SHA=$(sha256sum src/JD.AI.Workflows/Models/intent_classifier.zip | cut -d' ' -f1) - echo "model_sha=$MODEL_SHA" >> $GITHUB_OUTPUT - echo "model_size=$(du -h src/JD.AI.Workflows/Models/intent_classifier.zip | cut -f1)" >> $GITHUB_OUTPUT - - - name: Commit model - if: github.event.inputs.commit_model != 'false' - id: commit - run: | - git add src/JD.AI.Workflows/Models/ - # Only commit if there are changes - if git diff --cached --quiet; then - echo "no_changes=true" >> $GITHUB_OUTPUT - echo "::notice::No changes to model — skipping commit" - else - git commit -m "ci: retrain intent classifier model - - Generated ${{ steps.generate.outputs.examples }} examples via llama3.2:1b - Triggered by: ${{ github.event_name }} - Workflow run: ${{ github.run_id }}" - echo "no_changes=false" >> $GITHUB_OUTPUT - fi - - - name: Push model commit - if: github.event.inputs.commit_model != 'false' - run: | - if [[ "${{ steps.commit.outputs.no_changes }}" != "true" ]]; then - git push - echo "::notice::Model committed and pushed" - fi - - # ── Upload model as artifact (always) ───────────────────────── - - name: Upload model artifact - uses: actions/upload-artifact@v7 - with: - name: intent-classifier-model - path: src/JD.AI.Workflows/Models/intent_classifier.zip - retention-days: 30 - compression-level: 6 - - - name: Upload training data artifact - uses: actions/upload-artifact@v7 - with: - name: intent-classifier-training-data - path: src/JD.AI.Workflows/Models/ollama_training_data.jsonl - retention-days: 30 - - # ── Summary ──────────────────────────────────────────────────── - - name: Summary - run: | - echo "## Intent Classifier Training Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY - echo "|------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Examples generated | ${{ steps.generate.outputs.examples }} |" >> $GITHUB_STEP_SUMMARY - echo "| Trigger | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY - echo "| Runner | ubuntu-latest |" >> $GITHUB_STEP_SUMMARY - echo "| Model | [intent_classifier.zip](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifact-${{ steps.commit.outputs.model_sha }}) |" >> $GITHUB_STEP_SUMMARY +name: Train Intent Classifier Model + +# ── Triggers ───────────────────────────────────────────────────────── +on: + # Re-train on-demand: go to Actions → "Train Intent Classifier Model" → Run workflow + workflow_dispatch: + inputs: + examples: + description: "Number of AI-synthesized examples to generate (total after merge)" + type: number + default: 50 + required: false + commit_model: + description: "Commit the trained model back to the repository" + type: boolean + default: true + required: false + + # Re-train weekly — Sunday at midnight UTC + schedule: + - cron: "0 0 * * 0" + + # Re-train when training data or tooling changes (paths) + push: + paths: + - "tools/JD.AI.Workflows.Training/**" + - "src/JD.AI.Workflows/MlNetIntentClassifier.cs" + - "src/JD.AI.Workflows/Interfaces.cs" + - "src/JD.AI.Workflows/IntentClassifierHotSwapService.cs" + - ".github/workflows/train-model.yml" + +# ── Permissions ──────────────────────────────────────────────────── +permissions: + contents: write # needed to commit the trained model back + pull-requests: write # open PR if direct push is blocked + +# ── Jobs ─────────────────────────────────────────────────────────── +jobs: + train: + name: Synthesize + Train + Validate + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Install .NET 10 + uses: actions/setup-dotnet@v6 + with: + dotnet-version: "10.0.x" + + # ── Ollama setup ──────────────────────────────────────────────── + - name: Install Ollama + run: | + curl -fsSL https://ollama.com/install.sh | sh + ollama --version + + - name: Start Ollama server + run: | + # Start Ollama as a background service (required for pull and training) + nohup ollama serve > /tmp/ollama.log 2>&1 & + # Give the server a moment to initialize + sleep 5 + # Verify it's running + curl -s http://localhost:11434/api/tags && echo "Ollama server is up" + + - name: Pull llama3.2:1b model + run: ollama pull llama3.2:1b + + # ── Build training tool ───────────────────────────────────────── + - name: Build training tool + run: dotnet build tools/JD.AI.Workflows.Training/JD.AI.Workflows.Training.csproj --configuration Release + + # ── Generate training data via Ollama ────────────────────────── + - name: Synthesize training examples + id: generate + env: + OLLAMA_HOST: "http://localhost:11434" + run: | + mkdir -p src/JD.AI.Workflows/Models + + dotnet run --project tools/JD.AI.Workflows.Training/JD.AI.Workflows.Training.csproj \ + --no-build --configuration Release -- \ + --ollama-generate ${{ github.event.inputs.examples || 50 }} \ + --model llama3.2:1b \ + --output src/JD.AI.Workflows/Models/intent_classifier.zip + + echo "examples=$(cat src/JD.AI.Workflows/Models/ollama_training_data.jsonl | wc -l)" >> $GITHUB_OUTPUT + + # ── Train model ─────────────────────────────────────────────── + - name: Train ML.NET model + env: + OLLAMA_HOST: "http://localhost:11434" + run: | + dotnet run --project tools/JD.AI.Workflows.Training/JD.AI.Workflows.Training.csproj \ + --no-build --configuration Release -- \ + --data src/JD.AI.Workflows/Models/ollama_training_data.jsonl \ + --output src/JD.AI.Workflows/Models/intent_classifier.zip + + # ── Benchmark vs TF-IDF baseline ───────────────────────────── + - name: Benchmark ML.NET vs TF-IDF + env: + OLLAMA_HOST: "http://localhost:11434" + run: | + dotnet run --project tools/JD.AI.Workflows.Training/JD.AI.Workflows.Training.csproj \ + --no-build --configuration Release -- \ + --benchmark + + # ── Validate model quality ──────────────────────────────────── + - name: Validate against Ollama + id: validate + env: + OLLAMA_HOST: "http://localhost:11434" + run: | + dotnet run --project tools/JD.AI.Workflows.Training/JD.AI.Workflows.Training.csproj \ + --no-build --configuration Release -- \ + --ollama-validate \ + --model llama3.2:1b \ + --data src/JD.AI.Workflows/Models/ollama_training_data.jsonl + + # Fail if more than 15% disagreement with Ollama + DISAGREEMENTS=$(cat validate_summary.txt 2>/dev/null || echo "0") + echo "Validation disagreements: $DISAGREEMENTS" + + # ── Commit model (optional) ──────────────────────────────────── + - name: Configure git + if: github.event.inputs.commit_model != 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Store model SHA for PR description + MODEL_SHA=$(sha256sum src/JD.AI.Workflows/Models/intent_classifier.zip | cut -d' ' -f1) + echo "model_sha=$MODEL_SHA" >> $GITHUB_OUTPUT + echo "model_size=$(du -h src/JD.AI.Workflows/Models/intent_classifier.zip | cut -f1)" >> $GITHUB_OUTPUT + + - name: Commit model + if: github.event.inputs.commit_model != 'false' + id: commit + run: | + git add src/JD.AI.Workflows/Models/ + # Only commit if there are changes + if git diff --cached --quiet; then + echo "no_changes=true" >> $GITHUB_OUTPUT + echo "::notice::No changes to model — skipping commit" + else + git commit -m "ci: retrain intent classifier model + + Generated ${{ steps.generate.outputs.examples }} examples via llama3.2:1b + Triggered by: ${{ github.event_name }} + Workflow run: ${{ github.run_id }}" + echo "no_changes=false" >> $GITHUB_OUTPUT + fi + + - name: Push model commit + if: github.event.inputs.commit_model != 'false' + run: | + if [[ "${{ steps.commit.outputs.no_changes }}" != "true" ]]; then + git push + echo "::notice::Model committed and pushed" + fi + + # ── Upload model as artifact (always) ───────────────────────── + - name: Upload model artifact + uses: actions/upload-artifact@v7 + with: + name: intent-classifier-model + path: src/JD.AI.Workflows/Models/intent_classifier.zip + retention-days: 30 + compression-level: 6 + + - name: Upload training data artifact + uses: actions/upload-artifact@v7 + with: + name: intent-classifier-training-data + path: src/JD.AI.Workflows/Models/ollama_training_data.jsonl + retention-days: 30 + + # ── Summary ──────────────────────────────────────────────────── + - name: Summary + run: | + echo "## Intent Classifier Training Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY + echo "|------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Examples generated | ${{ steps.generate.outputs.examples }} |" >> $GITHUB_STEP_SUMMARY + echo "| Trigger | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY + echo "| Runner | ubuntu-latest |" >> $GITHUB_STEP_SUMMARY + echo "| Model | [intent_classifier.zip](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifact-${{ steps.commit.outputs.model_sha }}) |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 45e68774..31ba9387 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -1,109 +1,109 @@ -name: Version Bump - -# Runs after every push to main and auto-bumps version.json based on -# conventional commit types: -# -# feat: → minor bump (1.1.x → 1.2.0) -# BREAKING CHANGE / feat!: / fix!: etc. → major bump (1.x → 2.0) -# fix:, perf: → patch (NBGV commit height handles automatically) -# docs:, ci:, chore:, style:, test:, refactor:, build: → no bump -# -# The workflow skips its own version-bump commits to prevent loops. - -on: - push: - branches: [main] - -permissions: - contents: write - -jobs: - bump: - name: Conventional Commits → version.json - runs-on: ubuntu-latest - # Skip commits made by this workflow to avoid infinite loops - if: "!contains(github.event.head_commit.message, 'chore(version): bump')" - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Install NBGV - run: dotnet tool install -g nbgv - - - name: Determine bump from conventional commits - id: semver - shell: bash - run: | - # Find the commit that last touched version.json - LAST_VERSION_COMMIT=$(git log --follow -1 --format="%H" -- version.json) - - if [ -z "$LAST_VERSION_COMMIT" ]; then - # No prior version.json change — analyze all commits - COMMITS=$(git log --format="%s%n%b" HEAD) - else - # Analyze commits since version.json was last changed - COMMITS=$(git log --format="%s%n%b" "${LAST_VERSION_COMMIT}..HEAD") - fi - - echo "Commits since last version change:" - echo "$COMMITS" - - # Detect breaking changes (BREAKING CHANGE in body OR ! after type) - if echo "$COMMITS" | grep -qE "(BREAKING[[:space:]]CHANGE|^[a-z]+(\([^)]*\))?!:)"; then - echo "bump=major" >> "$GITHUB_OUTPUT" - # Detect features - elif echo "$COMMITS" | grep -qE "^feat(\([^)]*\))?:"; then - echo "bump=minor" >> "$GITHUB_OUTPUT" - else - echo "bump=none" >> "$GITHUB_OUTPUT" - fi - - - name: Apply version bump - if: steps.semver.outputs.bump != 'none' - shell: bash - run: | - BUMP="${{ steps.semver.outputs.bump }}" - CURRENT=$(nbgv get-version -v MajorMinorVersion) - MAJOR=$(echo "$CURRENT" | cut -d. -f1) - MINOR=$(echo "$CURRENT" | cut -d. -f2) - - if [ "$BUMP" = "major" ]; then - MAJOR=$((MAJOR + 1)) - MINOR=0 - elif [ "$BUMP" = "minor" ]; then - MINOR=$((MINOR + 1)) - fi - - NEW_VERSION="${MAJOR}.${MINOR}" - echo "Bumping $CURRENT → $NEW_VERSION ($BUMP)" - nbgv set-version "$NEW_VERSION" - - - name: Commit version bump - if: steps.semver.outputs.bump != 'none' - shell: bash - run: | - BUMP="${{ steps.semver.outputs.bump }}" - NEW_VERSION=$(nbgv get-version -v MajorMinorVersion) - - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - if git diff --quiet -- version.json; then - echo "No version.json changes detected; skipping commit." - exit 0 - fi - - git add version.json - git commit -m "chore(version): bump to ${NEW_VERSION} [${BUMP}] - - Auto-bumped by conventional commits analysis. - [skip ci]" - git push +name: Version Bump + +# Runs after every push to main and auto-bumps version.json based on +# conventional commit types: +# +# feat: → minor bump (1.1.x → 1.2.0) +# BREAKING CHANGE / feat!: / fix!: etc. → major bump (1.x → 2.0) +# fix:, perf: → patch (NBGV commit height handles automatically) +# docs:, ci:, chore:, style:, test:, refactor:, build: → no bump +# +# The workflow skips its own version-bump commits to prevent loops. + +on: + push: + branches: [main] + +permissions: + contents: write + +jobs: + bump: + name: Conventional Commits → version.json + runs-on: ubuntu-latest + # Skip commits made by this workflow to avoid infinite loops + if: "!contains(github.event.head_commit.message, 'chore(version): bump')" + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Install NBGV + run: dotnet tool install -g nbgv + + - name: Determine bump from conventional commits + id: semver + shell: bash + run: | + # Find the commit that last touched version.json + LAST_VERSION_COMMIT=$(git log --follow -1 --format="%H" -- version.json) + + if [ -z "$LAST_VERSION_COMMIT" ]; then + # No prior version.json change — analyze all commits + COMMITS=$(git log --format="%s%n%b" HEAD) + else + # Analyze commits since version.json was last changed + COMMITS=$(git log --format="%s%n%b" "${LAST_VERSION_COMMIT}..HEAD") + fi + + echo "Commits since last version change:" + echo "$COMMITS" + + # Detect breaking changes (BREAKING CHANGE in body OR ! after type) + if echo "$COMMITS" | grep -qE "(BREAKING[[:space:]]CHANGE|^[a-z]+(\([^)]*\))?!:)"; then + echo "bump=major" >> "$GITHUB_OUTPUT" + # Detect features + elif echo "$COMMITS" | grep -qE "^feat(\([^)]*\))?:"; then + echo "bump=minor" >> "$GITHUB_OUTPUT" + else + echo "bump=none" >> "$GITHUB_OUTPUT" + fi + + - name: Apply version bump + if: steps.semver.outputs.bump != 'none' + shell: bash + run: | + BUMP="${{ steps.semver.outputs.bump }}" + CURRENT=$(nbgv get-version -v MajorMinorVersion) + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + + if [ "$BUMP" = "major" ]; then + MAJOR=$((MAJOR + 1)) + MINOR=0 + elif [ "$BUMP" = "minor" ]; then + MINOR=$((MINOR + 1)) + fi + + NEW_VERSION="${MAJOR}.${MINOR}" + echo "Bumping $CURRENT → $NEW_VERSION ($BUMP)" + nbgv set-version "$NEW_VERSION" + + - name: Commit version bump + if: steps.semver.outputs.bump != 'none' + shell: bash + run: | + BUMP="${{ steps.semver.outputs.bump }}" + NEW_VERSION=$(nbgv get-version -v MajorMinorVersion) + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + if git diff --quiet -- version.json; then + echo "No version.json changes detected; skipping commit." + exit 0 + fi + + git add version.json + git commit -m "chore(version): bump to ${NEW_VERSION} [${BUMP}] + + Auto-bumped by conventional commits analysis. + [skip ci]" + git push diff --git a/Directory.Packages.props b/Directory.Packages.props index 74e842b7..6720f16c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,9 +6,9 @@ - + - + @@ -18,11 +18,11 @@ - - - - - + + + + + @@ -36,7 +36,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -84,7 +84,7 @@ - + @@ -100,8 +100,8 @@ - - + + diff --git a/package.json b/package.json index 6279506f..89a1a6a8 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ -{ - "devDependencies": { - "playwright": "^1.61.1" - } -} +{ + "devDependencies": { + "playwright": "^1.61.1" + } +} diff --git a/src/JD.AI.Plugins.SDK/JD.AI.Plugins.SDK.csproj b/src/JD.AI.Plugins.SDK/JD.AI.Plugins.SDK.csproj index f7f48126..8f17f044 100644 --- a/src/JD.AI.Plugins.SDK/JD.AI.Plugins.SDK.csproj +++ b/src/JD.AI.Plugins.SDK/JD.AI.Plugins.SDK.csproj @@ -1,18 +1,18 @@ - - - - net10.0 - JD.AI.Plugins.SDK - Plugin development SDK for JD.AI — interfaces, attributes, and base classes for building JD.AI plugins. - ai;plugin;sdk;semantic-kernel;extensibility - - - - - - - - - - - + + + + net10.0 + JD.AI.Plugins.SDK + Plugin development SDK for JD.AI — interfaces, attributes, and base classes for building JD.AI plugins. + ai;plugin;sdk;semantic-kernel;extensibility + + + + + + + + + + + diff --git a/src/JD.AI.Telemetry/JD.AI.Telemetry.csproj b/src/JD.AI.Telemetry/JD.AI.Telemetry.csproj index 8bb03a61..9ea44236 100644 --- a/src/JD.AI.Telemetry/JD.AI.Telemetry.csproj +++ b/src/JD.AI.Telemetry/JD.AI.Telemetry.csproj @@ -1,29 +1,29 @@ - - - - net10.0 - JD.AI.Telemetry - OpenTelemetry tracing, metrics, and health checks for JD.AI — ActivitySource definitions, Meter definitions, and health check implementations. - ai;observability;opentelemetry;health-checks - - - - - - - - - - - - - - - - - - - - - - + + + + net10.0 + JD.AI.Telemetry + OpenTelemetry tracing, metrics, and health checks for JD.AI — ActivitySource definitions, Meter definitions, and health check implementations. + ai;observability;opentelemetry;health-checks + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/JD.AI.Gateway.Tests/JD.AI.Gateway.Tests.csproj b/tests/JD.AI.Gateway.Tests/JD.AI.Gateway.Tests.csproj index 06daec79..e294fea8 100644 --- a/tests/JD.AI.Gateway.Tests/JD.AI.Gateway.Tests.csproj +++ b/tests/JD.AI.Gateway.Tests/JD.AI.Gateway.Tests.csproj @@ -3,6 +3,7 @@ net10.0 false + $(NoWarn);CS8602;CS8604 diff --git a/tests/JD.AI.Tests/JD.AI.Tests.csproj b/tests/JD.AI.Tests/JD.AI.Tests.csproj index a18da14d..07b2ac60 100644 --- a/tests/JD.AI.Tests/JD.AI.Tests.csproj +++ b/tests/JD.AI.Tests/JD.AI.Tests.csproj @@ -3,7 +3,7 @@ net10.0 false - $(NoWarn);TBDD010 + $(NoWarn);TBDD010;CS8602;CS8604