diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cf6b4c6..31c7bec0 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@v6 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - 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@v6 - 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 - 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@v6 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - global-json-file: global.json - - - name: Restore - run: dotnet restore JD.AI.slnx - - - name: Determine version (NBGV) - id: nbgv - uses: dotnet/nbgv@81f7c98786b56d9fa30c96223e7147d9bb582ff7 # 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@v6 - 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@v6 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - 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@v6 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + 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@v6 + 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 + 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@v6 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + global-json-file: global.json + + - name: Restore + run: dotnet restore JD.AI.slnx + + - name: Determine version (NBGV) + id: nbgv + uses: dotnet/nbgv@81f7c98786b56d9fa30c96223e7147d9bb582ff7 # 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@v6 + 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@v6 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + 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 e39099f3..ba567637 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@v6 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - global-json-file: global.json - - - name: Initialize CodeQL - uses: github/codeql-action/init@v4.36.2 - with: - languages: csharp - - - name: Build - run: dotnet build --configuration Release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.36.2 +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@v6 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + global-json-file: global.json + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4.36.0 + with: + languages: csharp + + - name: Build + run: dotnet build --configuration Release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4.36.0 diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index 84b01066..7d0b9731 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@v6 - - - 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.2.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v6 - 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@v6 - - - 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@v6 + + - 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.2.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v6 + 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@v6 + + - 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 b3e49362..6e48cb62 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@v6 - - - 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@v6 + + - 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 fafb3413..67d3f681 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@v6 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - 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@v6 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - 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@v6 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + 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@v6 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + 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/pr-validation.yml b/.github/workflows/pr-validation.yml index e4dcb564..d031e440 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@v6 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - global-json-file: global.json - - - name: Determine version (NBGV) - id: nbgv - uses: dotnet/nbgv@81f7c98786b56d9fa30c96223e7147d9bb582ff7 # 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 - 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@v6 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + global-json-file: global.json + + - name: Determine version (NBGV) + id: nbgv + uses: dotnet/nbgv@81f7c98786b56d9fa30c96223e7147d9bb582ff7 # 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 + 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/Directory.Packages.props b/Directory.Packages.props index 7425e23c..02619af4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + @@ -15,24 +15,24 @@ - - - - - + + + + + - - - + + + - + @@ -42,7 +42,7 @@ - + @@ -96,7 +96,7 @@ - + diff --git a/src/JD.AI.Core/JD.AI.Core.csproj b/src/JD.AI.Core/JD.AI.Core.csproj index d646a89c..8c2a5ecc 100644 --- a/src/JD.AI.Core/JD.AI.Core.csproj +++ b/src/JD.AI.Core/JD.AI.Core.csproj @@ -9,7 +9,8 @@ - $(NoWarn);NU5118;NU5100;NU5104;CA2227;CA1002;CA1859;MA0016 + + $(NoWarn);NU5118;NU5100;NU5104;CA2227;CA1002;CA1859;CA1724;MA0016