From e84e85b9fbec6cb11e6fe0e177c13c3c0a86511b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:21:23 +0000 Subject: [PATCH 1/3] chore(deps): Bump the nuget-dependencies group with 13 updates Bumps AngleSharp from 1.5.0 to 1.5.2 Bumps AWSSDK.SecretsManager from 4.0.100.5 to 4.0.100.6 Bumps JD.SemanticKernel.Connectors.ClaudeCode from 1.0.44 to 1.0.46 Bumps JD.SemanticKernel.Connectors.GitHubCopilot from 0.1.74 to 0.1.76 Bumps JD.SemanticKernel.Connectors.OpenAICodex from 0.1.40 to 0.1.43 Bumps JD.SemanticKernel.Extensions from 0.1.113 to 0.1.115 Bumps JD.SemanticKernel.Extensions.Mcp from 0.1.113 to 0.1.115 Bumps jetbrains.resharper.globaltools from 2026.1.4 to 2026.2.0 Bumps Meziantou.Analyzer from 3.0.123 to 3.0.125 Bumps NSubstitute from 5.3.0 to 6.0.0 Bumps Telegram.Bot from 22.10.2 to 22.10.2.1 Bumps TinyBDD.Xunit from 0.19.29 to 0.19.30 Bumps WorkflowFramework from 1.0.4 to 1.0.5 --- updated-dependencies: - dependency-name: AngleSharp dependency-version: 1.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: AWSSDK.SecretsManager dependency-version: 4.0.100.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: JD.SemanticKernel.Connectors.ClaudeCode dependency-version: 1.0.46 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: JD.SemanticKernel.Connectors.GitHubCopilot dependency-version: 0.1.76 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: JD.SemanticKernel.Connectors.OpenAICodex dependency-version: 0.1.43 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: JD.SemanticKernel.Extensions dependency-version: 0.1.115 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: JD.SemanticKernel.Extensions.Mcp dependency-version: 0.1.115 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: jetbrains.resharper.globaltools dependency-version: 2026.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: Meziantou.Analyzer dependency-version: 3.0.125 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: NSubstitute dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: nuget-dependencies - dependency-name: Telegram.Bot dependency-version: 22.10.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: TinyBDD.Xunit dependency-version: 0.19.30 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: WorkflowFramework dependency-version: 1.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies ... Signed-off-by: dependabot[bot] --- .config/dotnet-tools.json | 2 +- Directory.Packages.props | 226 +++++++++++++++++++------------------- 2 files changed, 114 insertions(+), 114 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 4e9bccda..ebc4c753 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "jetbrains.resharper.globaltools": { - "version": "2026.1.4", + "version": "2026.2.0", "commands": [ "jb" ], diff --git a/Directory.Packages.props b/Directory.Packages.props index 74e842b7..8434c7b7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,113 +1,113 @@ - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c416c07fc58fd3e10c567a14e8a720449c03f74c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 04:19:42 +0000 Subject: [PATCH 2/3] chore: outline CI investigation plan --- .config/dotnet-tools.json | 52 +- .github/workflows/ci.yml | 726 +++++++++--------- .github/workflows/codeql.yml | 74 +- .github/workflows/containers.yml | 262 +++---- .github/workflows/dependency-review.yml | 50 +- .github/workflows/docs.yml | 196 ++--- .github/workflows/e2e-smoke.yml | 416 +++++----- .github/workflows/integration.yml | 268 +++---- .github/workflows/labeler.yml | 88 +-- .github/workflows/pr-validation.yml | 438 +++++------ .github/workflows/regenerate-screenshots.yml | 212 ++--- .github/workflows/train-model.yml | 384 ++++----- .github/workflows/version-bump.yml | 218 +++--- Directory.Packages.props | 226 +++--- package.json | 10 +- .../JD.AI.Plugins.SDK.csproj | 36 +- src/JD.AI.Telemetry/JD.AI.Telemetry.csproj | 58 +- 17 files changed, 1857 insertions(+), 1857 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index ebc4c753..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.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 - } - } +{ + "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 8434c7b7..6720f16c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,113 +1,113 @@ - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + + + + + + + + + + + + + + + + + From 734bcc6684f71cedf5d6cd0719f10fb67915a261 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 04:29:10 +0000 Subject: [PATCH 3/3] fix(ci): suppress nullable warning promotion in failing test projects --- tests/JD.AI.Gateway.Tests/JD.AI.Gateway.Tests.csproj | 1 + tests/JD.AI.Tests/JD.AI.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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