From 1d8c518a7b7447421821490436491d8f1a764d0e Mon Sep 17 00:00:00 2001 From: PrinceOliver Date: Thu, 20 Aug 2026 13:49:42 +0530 Subject: [PATCH 01/14] Added CI configuration --- .github/workflows/ci.yml | 94 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c251b01 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: # Allow manual runs from the Actions tab + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + build-and-test: + name: Build & Test + runs-on: ubuntu-latest + timeout-minutes: 25 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.x + 9.x + 10.x + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install npm dependencies + run: npm ci + + - name: Restore + run: dotnet restore ./Syncfusion.Blazor.Toolkit.slnx + + - name: Build + run: dotnet build ./Syncfusion.Blazor.Toolkit.slnx -c Release --no-restore + + - name: Run bUnit tests + run: | + dotnet test tests/Syncfusion.Blazor.Toolkit.BUnitTest/ \ + -c Release \ + --no-build \ + --logger "trx;LogFileName=bunit-results.trx" \ + --logger "html;LogFileName=bunit-results.html" \ + --results-directory TestResults \ + --collect:"XPlat Code Coverage" + + # Uncomment the block below when Playwright tests are ready for CI + # - name: Install Playwright browsers + # run: npx playwright install --with-deps + # + # - name: Run Playwright tests + # run: | + # dotnet test tests/Syncfusion.Blazor.Playwright.Test/ \ + # -c Release \ + # --no-build \ + # --logger "trx;LogFileName=playwright-results.trx" \ + # --results-directory TestResults + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: | + TestResults/ + **/coverage.cobertura.xml + retention-days: 14 + if-no-files-found: ignore + + - name: Publish test results + if: always() + uses: dorny/test-reporter@v1 + with: + name: Test Results + path: 'TestResults/**/*.trx' + reporter: dotnet-trx + fail-on-error: true \ No newline at end of file From d5c1b16f017225842215215b62c7b0f610ac9d20 Mon Sep 17 00:00:00 2001 From: PrinceOliver Date: Thu, 20 Aug 2026 13:54:03 +0530 Subject: [PATCH 02/14] Added CodeQL workflow --- .github/workflows/codeql.yml | 67 ++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..1080c09 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,67 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Runs every Monday at 06:00 UTC + - cron: '0 6 * * 1' + workflow_dispatch: # Allow manual runs + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (csharp) + runs-on: ubuntu-latest + timeout-minutes: 360 # CodeQL can take longer on larger repos + + permissions: + actions: read + contents: read + security-events: write # Required to upload results to GitHub Security tab + + strategy: + fail-fast: false + matrix: + language: ['csharp'] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history improves analysis quality + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # Optional: add custom queries if needed later + # queries: +security-and-quality + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.x + 9.x + 10.x + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + # Autobuild works well for most .NET solutions. + # If it fails, replace this step with a manual build: + + # - name: Manual build + # run: | + # dotnet restore ./Syncfusion.Blazor.Toolkit.slnx + # dotnet build ./Syncfusion.Blazor.Toolkit.slnx -c Release --no-restore + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" \ No newline at end of file From 935bdce2d1b85a9b94b654b4ab3c4d6a7f6ef467 Mon Sep 17 00:00:00 2001 From: PrinceOliver Date: Thu, 20 Aug 2026 14:04:01 +0530 Subject: [PATCH 03/14] add CodeQL security scanning and Dependabot - Add polished CodeQL workflow (csharp + javascript-typescript + actions) - Add Dependabot configuration for NuGet, npm, and GitHub Actions - Enable weekly scheduled scans and dependency updates --- .github/dependabot.yml | 40 +++++++++++++++++++++++++ .github/workflows/codeql.yml | 58 +++++++++++++++++------------------- 2 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f2299a0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,40 @@ +version: 2 +updates: + # NuGet packages (.NET) + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "nuget" + commit-message: + prefix: "deps(nuget)" + + # npm (gulp, Playwright, etc.) + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "npm" + commit-message: + prefix: "deps(npm)" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "deps(actions)" \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1080c09..9657b4b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,14 +1,13 @@ -name: CodeQL +name: "CodeQL" on: push: - branches: [main] + branches: [ "main" ] pull_request: - branches: [main] + branches: [ "main" ] schedule: - # Runs every Monday at 06:00 UTC - - cron: '0 6 * * 1' - workflow_dispatch: # Allow manual runs + - cron: '0 6 * * 1' # Every Monday 06:00 UTC + workflow_dispatch: concurrency: group: codeql-${{ github.ref }} @@ -16,50 +15,47 @@ concurrency: jobs: analyze: - name: Analyze (csharp) + name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest - timeout-minutes: 360 # CodeQL can take longer on larger repos + timeout-minutes: 360 permissions: + security-events: write + packages: read actions: read contents: read - security-events: write # Required to upload results to GitHub Security tab strategy: fail-fast: false matrix: - language: ['csharp'] + include: + - language: csharp + build-mode: autobuild # Best for .NET + - language: javascript-typescript + build-mode: none # For gulp / package.json / Playwright + - language: actions + build-mode: none # For .github/workflows/*.yml steps: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 # Full history improves analysis quality + fetch-depth: 0 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - # Optional: add custom queries if needed later - # queries: +security-and-quality - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.x - 9.x - 10.x - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - # Autobuild works well for most .NET solutions. - # If it fails, replace this step with a manual build: - - # - name: Manual build - # run: | - # dotnet restore ./Syncfusion.Blazor.Toolkit.slnx - # dotnet build ./Syncfusion.Blazor.Toolkit.slnx -c Release --no-restore + build-mode: ${{ matrix.build-mode }} + # Uncomment if you want extra queries later: + # queries: security-extended,security-and-quality + + # Only needed when build-mode is "manual" + - name: Manual build (csharp) + if: matrix.build-mode == 'manual' + run: | + dotnet restore ./Syncfusion.Blazor.Toolkit.slnx + dotnet build ./Syncfusion.Blazor.Toolkit.slnx -c Release --no-restore - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 From e576dc0bd7bbe2e7811e3801e9f63b4c6803d23f Mon Sep 17 00:00:00 2001 From: Prince Oliver <31838171+PrinceOliver@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:15:57 +0530 Subject: [PATCH 04/14] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c251b01..f1fc450 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,9 @@ on: branches: [main] workflow_dispatch: # Allow manual runs from the Actions tab +permissions: + contents: read + concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From 1cf8fc5c4fae0952b1a5de15452b7e2064faec10 Mon Sep 17 00:00:00 2001 From: PrinceOliver Date: Thu, 20 Aug 2026 14:40:04 +0530 Subject: [PATCH 05/14] Update the Setup Node.js + Install dependencies section in .github/workflows/ci.yml --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c251b01..3e87447 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,14 @@ jobs: cache: 'npm' - name: Install npm dependencies - run: npm ci + run: | + if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then + echo "Lock file detected → using npm ci (reproducible)" + npm ci + else + echo "No lock file → using npm install" + npm install --no-fund --no-audit + fi - name: Restore run: dotnet restore ./Syncfusion.Blazor.Toolkit.slnx From fa019349a82c4ec74c4cad67ac88a5577b424d56 Mon Sep 17 00:00:00 2001 From: PrinceOliver Date: Fri, 21 Aug 2026 17:16:47 +0530 Subject: [PATCH 06/14] ci(cd): harden nuget-publish pipeline - Add SLSA build provenance attestation (actions/attest-build-provenance@v2) for nupkg/*.nupkg and nupkg/*.snupkg, scoped to pushed tag ref. - Add workflow-level concurrency guard (cancel-in-progress: false) so re-tags don't race the sign-and-push steps. - Switch vulnerability scan to exit-code-driven --format json capture, upload vuln-report artifact for diagnosis on failure. - Install Node 22 + npm + run gulp themes task on the release runner so the csproj BeforeBuild target completes on a clean image. - Pass -p:ContinuousIntegrationBuild=true to restore/build/pack for re-tag reproducibility. - Replace hard-coded copyright year with UtcNow-derived property. - Update .github/{THREAT-MODEL,SECURITY,DEVELOPMENT}.md to reflect new posture. --- .github/DEVELOPMENT.md | 12 ++ .github/SECURITY.md | 2 +- .github/THREAT-MODEL.md | 6 +- .github/workflows/ci.yml | 223 ++++++++++++++++++++++----- .github/workflows/nuget-publish.yml | 91 ++++++++++- src/Syncfusion.Blazor.Toolkit.csproj | 3 +- 6 files changed, 288 insertions(+), 49 deletions(-) diff --git a/.github/DEVELOPMENT.md b/.github/DEVELOPMENT.md index cc7c023..32da009 100644 --- a/.github/DEVELOPMENT.md +++ b/.github/DEVELOPMENT.md @@ -22,6 +22,18 @@ This page contains the steps to build and run the Syncfusion Toolkit for Blazor dotnet build ./Syncfusion.Blazor.Toolkit.slnx ``` +### Release sanity check (local) + +If you want to mimic what `.github/workflows/nuget-publish.yml` does on a release runner, pass `-p:ContinuousIntegrationBuild=true` so SourceLink and the package hash match what CI produces: + +```dotnetcli +dotnet restore src/Syncfusion.Blazor.Toolkit.csproj -p:ContinuousIntegrationBuild=true +dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true +dotnet pack src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-build -o nupkg -p:ContinuousIntegrationBuild=true +``` + +> **Note**: `dotnet pack` triggers a `BeforeBuild` target that runs `npm install` and `gulp blazor-toolkit-themes` if `src/wwwroot/styles/fluent.min.css` is absent. Make sure Node.js (LTS) is on `PATH`. The release workflow installs Node 22 explicitly to handle this. + ## Running Samples - Open the `samples/Blazor.Toolkit.Samples.slnx` file in Visual Studio. diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 8ef95be..a9426e2 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -58,4 +58,4 @@ On a **monthly cadence** (targeting the second Wednesday of each month), the mai This project maintains a current security reference in the repository's [THREAT-MODEL.md](../THREAT-MODEL.md) document. The project team has reviewed the current architecture, package surface, and release flow and has documented the principal risks and mitigations in good faith. -This attestation reflects the project’s current understanding as of 2026-08-12 and is intended to be updated as the toolkit evolves. +This attestation reflects the project’s current understanding as of 2026-08-21 and is intended to be updated as the toolkit evolves. diff --git a/.github/THREAT-MODEL.md b/.github/THREAT-MODEL.md index d6138ab..e3c8aad 100644 --- a/.github/THREAT-MODEL.md +++ b/.github/THREAT-MODEL.md @@ -129,6 +129,10 @@ This threat model should be reviewed when: ## Self-attestation -This threat model was prepared as a current security reference for the Syncfusion Blazor Toolkit project and reflects the maintainers’ understanding of the project as of 2026-08-12. The project team intends to review and update this document as changes to the component library, assets, or build pipeline occur. +This threat model was prepared as a current security reference for the Syncfusion Blazor Toolkit project and reflects the maintainers’ understanding of the project as of 2026-08-21. The project team intends to review and update this document as changes to the component library, assets, or build pipeline occur. The maintainers attest that the information provided here is a good-faith assessment of the project’s current security risks and mitigations based on the repository structure and package design at the time of publication. + +### Change since last review + +- **2026-08-21 — Hardened CD pipeline for nuget-publish.** Added SLSA build provenance attestation (`actions/attest-build-provenance`), deterministic builds via `ContinuousIntegrationBuild=true`, exit-code-driven vulnerability scan with downloadable `vuln-report` artifact, and concurrency guard for re-tagged same-version pushes. Accepted-risks entries AR-1 and AR-2 were reviewed and remain applicable; no new accepted risk was introduced. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2206931..0d66604 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,10 +5,7 @@ on: branches: [main] pull_request: branches: [main] - workflow_dispatch: # Allow manual runs from the Actions tab - -permissions: - contents: read + workflow_dispatch: concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} @@ -20,38 +17,56 @@ env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true jobs: - build-and-test: - name: Build & Test + # ========================================================= + # Job 1: Build + bUnit (matrix across .NET versions) + # ========================================================= + bunit: + name: bUnit (.NET ${{ matrix.dotnet-version }}) runs-on: ubuntu-latest - timeout-minutes: 25 + timeout-minutes: 20 + + strategy: + fail-fast: false + matrix: + dotnet-version: ['8.0.x', '9.0.x', '10.0.x'] steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup .NET + - name: Setup .NET ${{ matrix.dotnet-version }} uses: actions/setup-dotnet@v4 with: - dotnet-version: | - 8.x - 9.x - 10.x + dotnet-version: ${{ matrix.dotnet-version }} + + # ---------- Caching (best practices) ---------- + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ matrix.dotnet-version }}-${{ hashFiles('**/*.*proj', '**/packages.lock.json') }} + restore-keys: | + nuget-${{ runner.os }}-${{ matrix.dotnet-version }}- + nuget-${{ runner.os }}- - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: '22' + + - name: Cache npm + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('**/package.json') }} + restore-keys: | + npm-${{ runner.os }}- - name: Install npm dependencies run: | if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then - echo "Lock file detected → using npm ci (reproducible)" npm ci else - echo "No lock file → using npm install" npm install --no-fund --no-audit fi @@ -63,42 +78,172 @@ jobs: - name: Run bUnit tests run: | + mkdir -p TestResults dotnet test tests/Syncfusion.Blazor.Toolkit.BUnitTest/ \ -c Release \ --no-build \ - --logger "trx;LogFileName=bunit-results.trx" \ - --logger "html;LogFileName=bunit-results.html" \ + --logger "trx;LogFileName=bunit-${{ matrix.dotnet-version }}.trx" \ + --logger "html;LogFileName=bunit-${{ matrix.dotnet-version }}.html" \ --results-directory TestResults \ - --collect:"XPlat Code Coverage" - - # Uncomment the block below when Playwright tests are ready for CI - # - name: Install Playwright browsers - # run: npx playwright install --with-deps - # - # - name: Run Playwright tests - # run: | - # dotnet test tests/Syncfusion.Blazor.Playwright.Test/ \ - # -c Release \ - # --no-build \ - # --logger "trx;LogFileName=playwright-results.trx" \ - # --results-directory TestResults - - - name: Upload test results + --collect:"XPlat Code Coverage" \ + --verbosity normal + + - name: Upload bUnit results if: always() uses: actions/upload-artifact@v4 with: - name: test-results + name: bunit-results-${{ matrix.dotnet-version }} path: | TestResults/ **/coverage.cobertura.xml retention-days: 14 if-no-files-found: ignore - - name: Publish test results + - name: Publish bUnit test results if: always() uses: dorny/test-reporter@v1 + continue-on-error: true with: - name: Test Results + name: bUnit (.NET ${{ matrix.dotnet-version }}) path: 'TestResults/**/*.trx' reporter: dotnet-trx - fail-on-error: true \ No newline at end of file + fail-on-error: false + + # ========================================================= + # Job 2: Playwright (runs in parallel) + # ========================================================= + playwright: + name: Playwright + runs-on: ubuntu-latest + timeout-minutes: 35 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.x + 9.x + 10.x + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.*proj', '**/packages.lock.json') }} + restore-keys: | + nuget-${{ runner.os }}- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Cache npm + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('**/package.json') }} + restore-keys: | + npm-${{ runner.os }}- + + - name: Install npm dependencies + run: | + if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then + npm ci + else + npm install --no-fund --no-audit + fi + + # Playwright browser cache (best practice) + - name: Cache Playwright browsers + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('**/package.json') }} + + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium + + - name: Install Playwright system deps (when cache hit) + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium + + - name: Restore + run: dotnet restore ./Syncfusion.Blazor.Toolkit.slnx + + - name: Build + run: dotnet build ./Syncfusion.Blazor.Toolkit.slnx -c Release --no-restore + + - name: Run Playwright tests (with coverage) + run: | + npx playwright test \ + --reporter=html,line,json \ + --output=test-results + env: + CI: true + # Enable coverage if your tests support it (Playwright + istanbul/c8 style) + # PLAYWRIGHT_COVERAGE: true + + - name: Upload Playwright artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: | + playwright-report/ + test-results/ + retention-days: 14 + if-no-files-found: ignore + + # ========================================================= + # Job 3: Summary – posts combined status comment + # ========================================================= + summary: + name: CI Summary + runs-on: ubuntu-latest + needs: [bunit, playwright] + if: always() + + steps: + - name: Checkout (for comment context) + uses: actions/checkout@v4 + + - name: Create combined status comment + uses: actions/github-script@v7 + with: + script: | + const bunitJobs = ${{ toJSON(needs.bunit) }}; + const playwright = ${{ toJSON(needs.playwright) }}; + + const bunitSuccess = bunitJobs.result === 'success'; + const playwrightSuccess = playwright.result === 'success'; + + const overall = bunitSuccess && playwrightSuccess ? '✅ All checks passed' : '❌ Some checks failed'; + + const body = `### CI Summary + + | Job | Status | + |-----|--------| + | **bUnit** (.NET 8 / 9 / 10) | ${bunitSuccess ? '✅ Passed' : '❌ Failed'} | + | **Playwright** | ${playwrightSuccess ? '✅ Passed' : '❌ Failed'} | + + **Overall:** ${overall} + `; + + // Only post on pull requests + if (context.eventName === 'pull_request') { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); + } else { + console.log(body); + } \ No newline at end of file diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml index 16ced6f..111faea 100644 --- a/.github/workflows/nuget-publish.yml +++ b/.github/workflows/nuget-publish.yml @@ -7,12 +7,23 @@ on: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-*' +# Serialize runs that target the same ref so two re-tags don't race the sign+push steps. +# Never cancel an in-flight publish — abandoning a half-signed package is worse than queueing. +concurrency: + group: nuget-publish-${{ github.ref }} + cancel-in-progress: false + jobs: publish: runs-on: ubuntu-latest + # Minimal permissions: + # contents: read — fetch sources for SourceLink + # id-token: write — NuGet.org trusted publishing AND Azure Workload Identity (separate token lifetimes) + # attestations: write — actions/attest-build-provenance for build provenance on nuget.org permissions: contents: read - id-token: write # required for both NuGet OIDC trusted publishing and Azure Workload Identity + id-token: write + attestations: write steps: - name: Checkout @@ -28,33 +39,99 @@ jobs: 9.x 10.x + # src/Syncfusion.Blazor.Toolkit.csproj has a BeforeBuild target that runs `npm install` + `gulp + # blazor-toolkit-themes` if wwwroot/styles/fluent.min.css is missing. We must satisfy that on + # a clean runner, otherwise the pack step dies with "npm: not found" instead of a clear failure. + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Cache npm + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + npm-${{ runner.os }}- + + - name: Install npm dependencies + run: | + if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then + npm ci + else + npm install --no-fund --no-audit + fi + + - name: Run gulp themes task + # Mirrors the BeforeBuild target on a fresh runner. Idempotent — a no-op when the CSS already exists. + run: npx gulp blazor-toolkit-themes + # Decode the strong-name key stored as a base64 GitHub Secret. # To create the secret: certutil -encode sf.snk sf.snk.b64 (or base64 sf.snk) - name: Write strong-name key run: echo "${{ secrets.STRONG_NAME_KEY_BASE64 }}" | base64 --decode > src/sf.snk - name: Restore - run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj + run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj -p:ContinuousIntegrationBuild=true # Fail the release if any direct or transitive dependency has a known CVE. + # Gate is exit-code-driven (text changes in future SDKs can't silently flip this to + # always-pass or always-fail). The JSON is uploaded as 'vuln-report' so the run summary + # is diagnosable, and a plain-text version is captured alongside it for humans. - name: Dependency vulnerability scan run: | - dotnet list src/Syncfusion.Blazor.Toolkit.csproj package --vulnerable --include-transitive 2>&1 | tee vuln-report.txt - if grep -q "has the following vulnerable packages" vuln-report.txt; then - echo "::error::Vulnerable packages detected — release blocked. See vuln-report.txt for details." + set -euo pipefail + # Capture both formats. Exit non-zero captures the actual gate. Captures, gate, output. + set +e + dotnet list src/Syncfusion.Blazor.Toolkit.csproj package --vulnerable --include-transitive \ + --format json > vuln-report.json + vuln_exit=$? + dotnet list src/Syncfusion.Blazor.Toolkit.csproj package --vulnerable --include-transitive \ + > vuln-report.txt 2>&1 || true + set -e + + if [ "$vuln_exit" -ne 0 ]; then + echo "::error::Vulnerable packages detected — release blocked. See 'vuln-report' artifact for details." + echo "::group::Vulnerability scan output" + cat vuln-report.txt + echo "::endgroup::" exit 1 fi + - name: Upload vulnerability report + # Always run, even on success, so the run summary confirms the scan ran. + if: always() + uses: actions/upload-artifact@v4 + with: + name: vuln-report + path: | + vuln-report.json + vuln-report.txt + retention-days: 14 + if-no-files-found: ignore + - name: Build - run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore + run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true - name: Pack - run: dotnet pack src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-build -o nupkg + run: dotnet pack src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-build -o nupkg -p:ContinuousIntegrationBuild=true - name: Remove strong-name key if: always() run: rm -f src/sf.snk + # Build provenance attestation — should appear BEFORE signing so the attestation's subjects are + # the un-signed packed archives (.nupkg / .snupkg), not the post-sign bytes that nuget.org sees. + # Scoped to the pushed tag ref so the audit trail matches exactly what consumers will resolve. + - name: Attest build provenance + uses: actions/attest-build-provenance@v2 + with: + subject-path: | + nupkg/*.nupkg + nupkg/*.snupkg + provenance-repository-refs: ${{ github.ref }} + - name: Install NuGetKeyVaultSignTool run: dotnet tool install --global NuGetKeyVaultSignTool diff --git a/src/Syncfusion.Blazor.Toolkit.csproj b/src/Syncfusion.Blazor.Toolkit.csproj index 02f7623..ced1379 100644 --- a/src/Syncfusion.Blazor.Toolkit.csproj +++ b/src/Syncfusion.Blazor.Toolkit.csproj @@ -9,7 +9,8 @@ true true net8.0;net9.0;net10.0 - Copyright 2001 - 2026 Syncfusion® Inc. + + Copyright 2001 - $([System.DateTime]::UtcNow.Year) Syncfusion® Inc. Syncfusion Blazor Toolkit Components The Syncfusion® Toolkit for Blazor is a high-performance, open-source collection of lightweight UI components designed to accelerate Blazor application development (Server and WebAssembly). These controls help developers build modern, responsive, and feature-rich web applications faster, with clean code and excellent performance. syncfusion_logo.png From 4501eb92f18d1d08f98a3d8f1eb73570e26e751a Mon Sep 17 00:00:00 2001 From: Prince Oliver <31838171+PrinceOliver@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:21:35 +0530 Subject: [PATCH 07/14] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d66604..6405bc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,9 @@ on: branches: [main] workflow_dispatch: +permissions: + contents: read + concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From 8e958c8f485f27a85b2ff763b032617f44ea5839 Mon Sep 17 00:00:00 2001 From: Prince Oliver <31838171+PrinceOliver@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:21:56 +0530 Subject: [PATCH 08/14] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6405bc7..778537c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,9 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + env: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: true @@ -212,6 +215,9 @@ jobs: runs-on: ubuntu-latest needs: [bunit, playwright] if: always() + permissions: + contents: read + pull-requests: write steps: - name: Checkout (for comment context) From 6411b3de560ae1bd9beafa3fce4ae18a9559ab75 Mon Sep 17 00:00:00 2001 From: PrinceOliver Date: Mon, 24 Aug 2026 14:12:52 +0530 Subject: [PATCH 09/14] ci(cd): improved ci.yml (resilient bUnit + proper failure propagation) --- .github/workflows/ci.yml | 84 ++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d66604..88abef3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ env: jobs: # ========================================================= - # Job 1: Build + bUnit (matrix across .NET versions) + # Job 1: bUnit (matrix) # ========================================================= bunit: name: bUnit (.NET ${{ matrix.dotnet-version }}) @@ -39,12 +39,11 @@ jobs: with: dotnet-version: ${{ matrix.dotnet-version }} - # ---------- Caching (best practices) ---------- - name: Cache NuGet packages uses: actions/cache@v4 with: path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ matrix.dotnet-version }}-${{ hashFiles('**/*.*proj', '**/packages.lock.json') }} + key: nuget-${{ runner.os }}-${{ matrix.dotnet-version }}-${{ hashFiles('**/*.*proj') }} restore-keys: | nuget-${{ runner.os }}-${{ matrix.dotnet-version }}- nuget-${{ runner.os }}- @@ -59,8 +58,7 @@ jobs: with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package.json') }} - restore-keys: | - npm-${{ runner.os }}- + restore-keys: npm-${{ runner.os }}- - name: Install npm dependencies run: | @@ -79,9 +77,10 @@ jobs: - name: Run bUnit tests run: | mkdir -p TestResults + # Force the correct framework and do NOT use --no-build dotnet test tests/Syncfusion.Blazor.Toolkit.BUnitTest/ \ -c Release \ - --no-build \ + -f net8.0 \ --logger "trx;LogFileName=bunit-${{ matrix.dotnet-version }}.trx" \ --logger "html;LogFileName=bunit-${{ matrix.dotnet-version }}.html" \ --results-directory TestResults \ @@ -110,7 +109,7 @@ jobs: fail-on-error: false # ========================================================= - # Job 2: Playwright (runs in parallel) + # Job 2: Playwright # ========================================================= playwright: name: Playwright @@ -133,9 +132,8 @@ jobs: uses: actions/cache@v4 with: path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.*proj', '**/packages.lock.json') }} - restore-keys: | - nuget-${{ runner.os }}- + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.*proj') }} + restore-keys: nuget-${{ runner.os }}- - name: Setup Node.js uses: actions/setup-node@v4 @@ -147,8 +145,7 @@ jobs: with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package.json') }} - restore-keys: | - npm-${{ runner.os }}- + restore-keys: npm-${{ runner.os }}- - name: Install npm dependencies run: | @@ -158,7 +155,6 @@ jobs: npm install --no-fund --no-audit fi - # Playwright browser cache (best practice) - name: Cache Playwright browsers uses: actions/cache@v4 id: playwright-cache @@ -170,7 +166,7 @@ jobs: if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps chromium - - name: Install Playwright system deps (when cache hit) + - name: Install Playwright system deps if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-deps chromium @@ -180,17 +176,12 @@ jobs: - name: Build run: dotnet build ./Syncfusion.Blazor.Toolkit.slnx -c Release --no-restore - - name: Run Playwright tests (with coverage) - run: | - npx playwright test \ - --reporter=html,line,json \ - --output=test-results + - name: Run Playwright tests + run: npx playwright test --reporter=html,line env: CI: true - # Enable coverage if your tests support it (Playwright + istanbul/c8 style) - # PLAYWRIGHT_COVERAGE: true - - name: Upload Playwright artifacts + - name: Upload Playwright report if: always() uses: actions/upload-artifact@v4 with: @@ -202,7 +193,7 @@ jobs: if-no-files-found: ignore # ========================================================= - # Job 3: Summary – posts combined status comment + # Job 3: Summary # ========================================================= summary: name: CI Summary @@ -211,39 +202,40 @@ jobs: if: always() steps: - - name: Checkout (for comment context) - uses: actions/checkout@v4 + - name: Check results and fail if needed + run: | + echo "bUnit result: ${{ needs.bunit.result }}" + echo "Playwright result: ${{ needs.playwright.result }}" + + if [[ "${{ needs.bunit.result }}" != "success" || "${{ needs.playwright.result }}" != "success" ]]; then + echo "One or more jobs failed → failing the workflow" + exit 1 + fi - - name: Create combined status comment + echo "All jobs succeeded" + + - name: Post summary comment (PRs only) + if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | - const bunitJobs = ${{ toJSON(needs.bunit) }}; - const playwright = ${{ toJSON(needs.playwright) }}; - - const bunitSuccess = bunitJobs.result === 'success'; - const playwrightSuccess = playwright.result === 'success'; - - const overall = bunitSuccess && playwrightSuccess ? '✅ All checks passed' : '❌ Some checks failed'; + const bunitOk = '${{ needs.bunit.result }}' === 'success'; + const pwOk = '${{ needs.playwright.result }}' === 'success'; + const overall = bunitOk && pwOk ? '✅ All checks passed' : '❌ Some checks failed'; const body = `### CI Summary | Job | Status | |-----|--------| - | **bUnit** (.NET 8 / 9 / 10) | ${bunitSuccess ? '✅ Passed' : '❌ Failed'} | - | **Playwright** | ${playwrightSuccess ? '✅ Passed' : '❌ Failed'} | + | **bUnit** (.NET 8 / 9 / 10) | ${bunitOk ? '✅ Passed' : '❌ Failed'} | + | **Playwright** | ${pwOk ? '✅ Passed' : '❌ Failed'} | **Overall:** ${overall} `; - // Only post on pull requests - if (context.eventName === 'pull_request') { - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body - }); - } else { - console.log(body); - } \ No newline at end of file + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body + }); \ No newline at end of file From 5471a785c456064a004f3f755eadab0ca019c023 Mon Sep 17 00:00:00 2001 From: PrinceOliver Date: Mon, 24 Aug 2026 14:42:12 +0530 Subject: [PATCH 10/14] ci(cd): Clean up the duplicate permissions key --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8754e80..0e93420 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,6 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: - contents: read - env: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: true From 2591fb96849ba56c7f577116dde7d63cf8cd105d Mon Sep 17 00:00:00 2001 From: PrinceOliver Date: Mon, 24 Aug 2026 14:51:58 +0530 Subject: [PATCH 11/14] ci(cd): Resolved missing .slnx file during build and restore in ci --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e93420..7f27013 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,15 +72,14 @@ jobs: fi - name: Restore - run: dotnet restore ./Syncfusion.Blazor.Toolkit.slnx + run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj - name: Build - run: dotnet build ./Syncfusion.Blazor.Toolkit.slnx -c Release --no-restore + run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore - name: Run bUnit tests run: | mkdir -p TestResults - # Force the correct framework and do NOT use --no-build dotnet test tests/Syncfusion.Blazor.Toolkit.BUnitTest/ \ -c Release \ -f net8.0 \ @@ -110,6 +109,7 @@ jobs: path: 'TestResults/**/*.trx' reporter: dotnet-trx fail-on-error: false + fail-on-empty: false # ========================================================= # Job 2: Playwright @@ -174,10 +174,10 @@ jobs: run: npx playwright install-deps chromium - name: Restore - run: dotnet restore ./Syncfusion.Blazor.Toolkit.slnx + run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj - name: Build - run: dotnet build ./Syncfusion.Blazor.Toolkit.slnx -c Release --no-restore + run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore - name: Run Playwright tests run: npx playwright test --reporter=html,line From 9328cf656c6e840341e38e768555424e488f83b4 Mon Sep 17 00:00:00 2001 From: PrinceOliver Date: Mon, 24 Aug 2026 16:39:17 +0530 Subject: [PATCH 12/14] Add TZ and LANG at the job level and add a small step before tests that sets the timezone/locale so the runner uses UTC and en_US.UTF-8. --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f27013..955ea67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 + env: + TZ: UTC + LANG: en_US.UTF-8 + strategy: fail-fast: false matrix: @@ -77,6 +81,16 @@ jobs: - name: Build run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore + - name: Set timezone and locale for deterministic tests + run: | + sudo ln -fs /usr/share/zoneinfo/UTC /etc/localtime + sudo apt-get update -y + sudo apt-get install -y locales + sudo locale-gen en_US.UTF-8 + export LANG=en_US.UTF-8 + export TZ=UTC + shell: bash + - name: Run bUnit tests run: | mkdir -p TestResults From a314cd43ff7aaa1c3dff1d1af75865ee5fc8055f Mon Sep 17 00:00:00 2001 From: PrinceOliver Date: Mon, 24 Aug 2026 20:47:32 +0530 Subject: [PATCH 13/14] add automatic versioning from the git tag --- .github/workflows/nuget-publish.yml | 58 ++++++++++++----------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml index 111faea..345cf1c 100644 --- a/.github/workflows/nuget-publish.yml +++ b/.github/workflows/nuget-publish.yml @@ -1,14 +1,11 @@ name: Publish NuGet -# Triggered by version tags: v1.0.0, v1.2.3-preview, etc. on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-*' -# Serialize runs that target the same ref so two re-tags don't race the sign+push steps. -# Never cancel an in-flight publish — abandoning a half-signed package is worse than queueing. concurrency: group: nuget-publish-${{ github.ref }} cancel-in-progress: false @@ -16,10 +13,6 @@ concurrency: jobs: publish: runs-on: ubuntu-latest - # Minimal permissions: - # contents: read — fetch sources for SourceLink - # id-token: write — NuGet.org trusted publishing AND Azure Workload Identity (separate token lifetimes) - # attestations: write — actions/attest-build-provenance for build provenance on nuget.org permissions: contents: read id-token: write @@ -29,7 +22,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 # full history required for SourceLink commit SHA + fetch-depth: 0 - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -39,9 +32,6 @@ jobs: 9.x 10.x - # src/Syncfusion.Blazor.Toolkit.csproj has a BeforeBuild target that runs `npm install` + `gulp - # blazor-toolkit-themes` if wwwroot/styles/fluent.min.css is missing. We must satisfy that on - # a clean runner, otherwise the pack step dies with "npm: not found" instead of a clear failure. - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -51,7 +41,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.npm - key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/package.json') }} restore-keys: | npm-${{ runner.os }}- @@ -64,25 +54,23 @@ jobs: fi - name: Run gulp themes task - # Mirrors the BeforeBuild target on a fresh runner. Idempotent — a no-op when the CSS already exists. run: npx gulp blazor-toolkit-themes - # Decode the strong-name key stored as a base64 GitHub Secret. - # To create the secret: certutil -encode sf.snk sf.snk.b64 (or base64 sf.snk) + - name: Set version from git tag + run: | + VERSION=${GITHUB_REF_NAME#v} + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "Publishing version: $VERSION" + - name: Write strong-name key run: echo "${{ secrets.STRONG_NAME_KEY_BASE64 }}" | base64 --decode > src/sf.snk - name: Restore run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj -p:ContinuousIntegrationBuild=true - # Fail the release if any direct or transitive dependency has a known CVE. - # Gate is exit-code-driven (text changes in future SDKs can't silently flip this to - # always-pass or always-fail). The JSON is uploaded as 'vuln-report' so the run summary - # is diagnosable, and a plain-text version is captured alongside it for humans. - name: Dependency vulnerability scan run: | set -euo pipefail - # Capture both formats. Exit non-zero captures the actual gate. Captures, gate, output. set +e dotnet list src/Syncfusion.Blazor.Toolkit.csproj package --vulnerable --include-transitive \ --format json > vuln-report.json @@ -92,7 +80,7 @@ jobs: set -e if [ "$vuln_exit" -ne 0 ]; then - echo "::error::Vulnerable packages detected — release blocked. See 'vuln-report' artifact for details." + echo "::error::Vulnerable packages detected — release blocked." echo "::group::Vulnerability scan output" cat vuln-report.txt echo "::endgroup::" @@ -100,7 +88,6 @@ jobs: fi - name: Upload vulnerability report - # Always run, even on success, so the run summary confirms the scan ran. if: always() uses: actions/upload-artifact@v4 with: @@ -112,18 +99,26 @@ jobs: if-no-files-found: ignore - name: Build - run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true + run: | + dotnet build src/Syncfusion.Blazor.Toolkit.csproj \ + -c Release \ + --no-restore \ + -p:ContinuousIntegrationBuild=true \ + -p:Version=$VERSION - name: Pack - run: dotnet pack src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-build -o nupkg -p:ContinuousIntegrationBuild=true + run: | + dotnet pack src/Syncfusion.Blazor.Toolkit.csproj \ + -c Release \ + --no-build \ + -o nupkg \ + -p:ContinuousIntegrationBuild=true \ + -p:Version=$VERSION - name: Remove strong-name key if: always() run: rm -f src/sf.snk - # Build provenance attestation — should appear BEFORE signing so the attestation's subjects are - # the un-signed packed archives (.nupkg / .snupkg), not the post-sign bytes that nuget.org sees. - # Scoped to the pushed tag ref so the audit trail matches exactly what consumers will resolve. - name: Attest build provenance uses: actions/attest-build-provenance@v2 with: @@ -135,10 +130,6 @@ jobs: - name: Install NuGetKeyVaultSignTool run: dotnet tool install --global NuGetKeyVaultSignTool - # Log in to Azure using Workload Identity Federation — no client secret needed. - # Prerequisite: add a federated credential for this repo+workflow on the service principal in Azure. - # Non-secret config values (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_KEY_VAULT_URL, - # AZURE_KEY_VAULT_CERT_NAME) are stored as GitHub Actions repository *variables* (vars.*), not secrets. - name: Azure login (OIDC) uses: azure/login@v2 with: @@ -146,7 +137,6 @@ jobs: tenant-id: ${{ vars.AZURE_TENANT_ID }} allow-no-subscriptions: true - # DefaultAzureCredential picks up the ambient Workload Identity token set by azure/login above. - name: Sign NuGet packages run: | NuGetKeyVaultSignTool sign nupkg/*.nupkg \ @@ -156,8 +146,6 @@ jobs: --azure-key-vault-url "${{ vars.AZURE_KEY_VAULT_URL }}" \ --azure-key-vault-certificate "${{ vars.AZURE_KEY_VAULT_CERT_NAME }}" - # Exchange the GitHub OIDC token for a short-lived NuGet.org API token (trusted publishing). - # Prerequisite: configure a trusted publisher on nuget.org for this repo + workflow file. - name: Push to NuGet.org run: | NUGET_TOKEN=$(curl -sS \ @@ -167,4 +155,4 @@ jobs: dotnet nuget push nupkg/*.nupkg \ --api-key "$NUGET_TOKEN" \ --source https://api.nuget.org/v3/index.json \ - --skip-duplicate + --skip-duplicate \ No newline at end of file From ab2b5317de9e04361841b6adff44d14e5aeb6e9d Mon Sep 17 00:00:00 2001 From: Indhumathy-Loganathan Date: Tue, 25 Aug 2026 14:20:22 +0530 Subject: [PATCH 14/14] Nuget package publish with exact version related changes included. --- .github/workflows/nuget-publish.yml | 142 +++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 23 deletions(-) diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml index 345cf1c..029b499 100644 --- a/.github/workflows/nuget-publish.yml +++ b/.github/workflows/nuget-publish.yml @@ -1,11 +1,14 @@ name: Publish NuGet +# Triggered by version tags: v1.0.0, v1.2.3-preview, etc. on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-*' +# Serialize runs that target the same ref so two re-tags don't race the sign+push steps. +# Never cancel an in-flight publish — abandoning a half-signed package is worse than queueing. concurrency: group: nuget-publish-${{ github.ref }} cancel-in-progress: false @@ -13,6 +16,10 @@ concurrency: jobs: publish: runs-on: ubuntu-latest + # Minimal permissions: + # contents: read — fetch sources for SourceLink + # id-token: write — NuGet.org trusted publishing AND Azure Workload Identity (separate token lifetimes) + # attestations: write — actions/attest-build-provenance for build provenance on nuget.org permissions: contents: read id-token: write @@ -22,7 +29,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 0 # full history required for SourceLink commit SHA - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -32,6 +39,9 @@ jobs: 9.x 10.x + # src/Syncfusion.Blazor.Toolkit.csproj has a BeforeBuild target that runs `npm install` + `gulp + # blazor-toolkit-themes` if wwwroot/styles/fluent.min.css is missing. We must satisfy that on + # a clean runner, otherwise the pack step dies with "npm: not found" instead of a clear failure. - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -41,7 +51,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.npm - key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/package.json') }} + key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | npm-${{ runner.os }}- @@ -54,23 +64,25 @@ jobs: fi - name: Run gulp themes task + # Mirrors the BeforeBuild target on a fresh runner. Idempotent — a no-op when the CSS already exists. run: npx gulp blazor-toolkit-themes - - name: Set version from git tag - run: | - VERSION=${GITHUB_REF_NAME#v} - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "Publishing version: $VERSION" - + # Decode the strong-name key stored as a base64 GitHub Secret. + # To create the secret: certutil -encode sf.snk sf.snk.b64 (or base64 sf.snk) - name: Write strong-name key run: echo "${{ secrets.STRONG_NAME_KEY_BASE64 }}" | base64 --decode > src/sf.snk - name: Restore run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj -p:ContinuousIntegrationBuild=true + # Fail the release if any direct or transitive dependency has a known CVE. + # Gate is exit-code-driven (text changes in future SDKs can't silently flip this to + # always-pass or always-fail). The JSON is uploaded as 'vuln-report' so the run summary + # is diagnosable, and a plain-text version is captured alongside it for humans. - name: Dependency vulnerability scan run: | set -euo pipefail + # Capture both formats. Exit non-zero captures the actual gate. Captures, gate, output. set +e dotnet list src/Syncfusion.Blazor.Toolkit.csproj package --vulnerable --include-transitive \ --format json > vuln-report.json @@ -80,7 +92,7 @@ jobs: set -e if [ "$vuln_exit" -ne 0 ]; then - echo "::error::Vulnerable packages detected — release blocked." + echo "::error::Vulnerable packages detected — release blocked. See 'vuln-report' artifact for details." echo "::group::Vulnerability scan output" cat vuln-report.txt echo "::endgroup::" @@ -88,6 +100,7 @@ jobs: fi - name: Upload vulnerability report + # Always run, even on success, so the run summary confirms the scan ran. if: always() uses: actions/upload-artifact@v4 with: @@ -98,27 +111,103 @@ jobs: retention-days: 14 if-no-files-found: ignore - - name: Build + # Determine the package version to publish. + # The trigger tag only acts as a *signal* to publish — it does NOT pin the version. + # We always derive `next_version` by: + # 1. Fetching the latest published version from nuget.org (flat-container API). + # 2. Fetching the latest GitHub release tag for this repo (covers cases where + # a release was tagged but the NuGet publish failed, e.g. transient infra issues). + # 3. Taking the numeric max of (1) and (2) — never ship a smaller version than + # what's already out. + # 4. Incrementing the patch component by 1 (a maintenance/bugfix release). + # The result is fed into `dotnet pack` via `-p:Version=...`, which overrides the + # hardcoded `1.0.0` in src/Syncfusion.Blazor.Toolkit.csproj. + - name: Resolve next version + id: version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - dotnet build src/Syncfusion.Blazor.Toolkit.csproj \ - -c Release \ - --no-restore \ - -p:ContinuousIntegrationBuild=true \ - -p:Version=$VERSION + set -euo pipefail + + NUGET_PKG="Syncfusion.Blazor.Toolkit" + REPO="${GITHUB_REPOSITORY}" + + # --- 1. Latest version on nuget.org (flat-container). --- + # flat-container returns absolute URLs to each version's .nupkg; we only need the + # list itself to compute the floor. A non-fatal warning is emitted if the lookup + # fails (e.g. transient CDN issue) so a stale GitHub-only floor still keeps the + # release moving. + nuget_latest="0.0.0" + if nuget_response=$(curl -fsS "https://api.nuget.org/v3-flatcontainer/${NUGET_PKG}/index.json" 2>/dev/null); then + nuget_latest=$(echo "$nuget_response" \ + | jq -r '.versions[]?' \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V \ + | tail -n 1) || true + nuget_latest="${nuget_latest:-0.0.0}" + else + echo "::warning::Could not query nuget.org for latest version of ${NUGET_PKG} — falling back to '0.0.0'." + fi + + # --- 2. Latest GitHub release tag for this repo. --- + # Fetched via the GitHub REST API. Drafts and prereleases are filtered out so a + # `v1.1.0-rc.1` tag never blocks a `v1.0.x` maintenance bump. Authenticated via + # the workflow's built-in GITHUB_TOKEN to lift the 60-req/hr anonymous rate limit. + releases_latest="0.0.0" + if releases_response=$(curl -fsS \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${REPO}/releases?per_page=20" 2>/dev/null); then + releases_latest=$(echo "$releases_response" \ + | jq -r '.[]? | select(.draft == false and .prerelease == false) | .tag_name' \ + | sed 's/^v//' \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V \ + | tail -n 1) || true + releases_latest="${releases_latest:-0.0.0}" + else + echo "::warning::Could not query GitHub releases for ${REPO} — falling back to '0.0.0'." + fi + + # --- 3. Pick the highest of the two floors. --- + # `sort -V` gives correct numeric ordering; `tail -n 1` picks the max. + floor=$(printf '%s\n%s\n' "${nuget_latest}" "${releases_latest}" | sort -V | tail -n 1) + floor="${floor:-0.0.0}" + + # --- 4. Bump the patch component by 1. --- + # If both lookups returned 0.0.0 the result is 0.0.1 — safe because nuget.org + # would have rejected a republish under the hardcoded "1.0.0" anyway. + IFS='.' read -r f_major f_minor f_patch <<< "${floor}" + next_major="${f_major:-0}" + next_minor="${f_minor:-0}" + next_patch="$((${f_patch:-0} + 1))" + next_version="${next_major}.${next_minor}.${next_patch}" + + echo "nuget_latest = ${nuget_latest}" + echo "releases_latest = ${releases_latest}" + echo "floor = ${floor}" + echo "next_version = ${next_version}" + echo "next_version=${next_version}" >> "$GITHUB_OUTPUT" + + - name: Build + # `PackageVersion` (not `Version`) is what controls the .nupkg filename and the + # manifest's element. The hardcoded 1.0.0 in + # src/Syncfusion.Blazor.Toolkit.csproj would otherwise win over `-p:Version=...`, + # making the override a no-op. See https://learn.microsoft.com/nuget/reference/msbuild-targets + # (pack target inputs → PackageVersion: "Default is the value of $(Version)"). + run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true -p:PackageVersion=${{ steps.version.outputs.next_version }} - name: Pack - run: | - dotnet pack src/Syncfusion.Blazor.Toolkit.csproj \ - -c Release \ - --no-build \ - -o nupkg \ - -p:ContinuousIntegrationBuild=true \ - -p:Version=$VERSION + run: dotnet pack src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-build -o nupkg -p:ContinuousIntegrationBuild=true -p:PackageVersion=${{ steps.version.outputs.next_version }} - name: Remove strong-name key if: always() run: rm -f src/sf.snk + # Build provenance attestation — should appear BEFORE signing so the attestation's subjects are + # the un-signed packed archives (.nupkg / .snupkg), not the post-sign bytes that nuget.org sees. + # Scoped to the pushed tag ref so the audit trail matches exactly what consumers will resolve. - name: Attest build provenance uses: actions/attest-build-provenance@v2 with: @@ -130,6 +219,10 @@ jobs: - name: Install NuGetKeyVaultSignTool run: dotnet tool install --global NuGetKeyVaultSignTool + # Log in to Azure using Workload Identity Federation — no client secret needed. + # Prerequisite: add a federated credential for this repo+workflow on the service principal in Azure. + # Non-secret config values (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_KEY_VAULT_URL, + # AZURE_KEY_VAULT_CERT_NAME) are stored as GitHub Actions repository *variables* (vars.*), not secrets. - name: Azure login (OIDC) uses: azure/login@v2 with: @@ -137,6 +230,7 @@ jobs: tenant-id: ${{ vars.AZURE_TENANT_ID }} allow-no-subscriptions: true + # DefaultAzureCredential picks up the ambient Workload Identity token set by azure/login above. - name: Sign NuGet packages run: | NuGetKeyVaultSignTool sign nupkg/*.nupkg \ @@ -146,6 +240,8 @@ jobs: --azure-key-vault-url "${{ vars.AZURE_KEY_VAULT_URL }}" \ --azure-key-vault-certificate "${{ vars.AZURE_KEY_VAULT_CERT_NAME }}" + # Exchange the GitHub OIDC token for a short-lived NuGet.org API token (trusted publishing). + # Prerequisite: configure a trusted publisher on nuget.org for this repo + workflow file. - name: Push to NuGet.org run: | NUGET_TOKEN=$(curl -sS \ @@ -155,4 +251,4 @@ jobs: dotnet nuget push nupkg/*.nupkg \ --api-key "$NUGET_TOKEN" \ --source https://api.nuget.org/v3/index.json \ - --skip-duplicate \ No newline at end of file + --skip-duplicate