Skip to content

infra: test project setup, Examples project, NuGet metadata, CI/CD pipelines#19

Merged
teesofttech merged 1 commit into
masterfrom
infra/phase-0-test-examples-pipeline
Jun 12, 2026
Merged

infra: test project setup, Examples project, NuGet metadata, CI/CD pipelines#19
teesofttech merged 1 commit into
masterfrom
infra/phase-0-test-examples-pipeline

Conversation

@teesofttech

@teesofttech teesofttech commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

This PR establishes the full development infrastructure before any bug fixes begin. Every subsequent fix PR will build on top of this.

  • Test project — adds Moq and RichardSzalay.MockHttp to TableauSharp.Tests, plus a project reference to the SDK so tests can actually import SDK types
  • Examples project — creates samples/TableauSharp.Examples, a runnable .NET 8 console app with self-contained examples for all 7 service areas (Auth, Users/Groups, Projects, Workbooks, DataSources, Permissions, Embedding); run with dotnet run -- <area>
  • NuGet metadata — adds PackageId, Version, Authors, Description, tags, repo URL, README bundle, and .snupkg symbol package config to TableauSharp.csproj
  • CI workflow.github/workflows/ci.yml builds and tests on every push/PR to master, uploads test results and coverage artifacts
  • Publish workflow.github/workflows/publish-nuget.yml packs and publishes to NuGet on v* tags (e.g. git tag v0.1.0 && git push --tags); requires NUGET_API_KEY secret in repo settings

Pre-release checklist

  • Add NUGET_API_KEY to repository secrets (Settings → Secrets → Actions) before the first release tag
  • Populate appsettings.json in the Examples project with your server details before running examples

Test plan

  • dotnet restore — clean
  • dotnet build -c Release — 0 errors
  • dotnet test -c Release — 1 passed (stub; real tests added per bug-fix PR)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added comprehensive examples demonstrating authentication, data sources, embedding, permissions, project management, user and group management, and workbook operations.
  • Chores

    • Automated CI/CD pipelines for building, testing, and publishing to NuGet.
    • Updated project configuration for NuGet package distribution and metadata.

…CD pipelines

- Tests: add Moq + RichardSzalay.MockHttp and SDK project reference to TableauSharp.Tests
- Examples: create samples/TableauSharp.Examples console project with runnable examples
  covering Auth, Users/Groups, Projects, Workbooks, DataSources, Permissions, and Embedding
- NuGet: add PackageId, Version, Authors, Description, tags, repo URL, README, and
  symbol package configuration to TableauSharp.csproj
- CI: .github/workflows/ci.yml builds and tests on every push/PR to master
- Publish: .github/workflows/publish-nuget.yml packs and publishes to NuGet on v* tags;
  set NUGET_API_KEY in repository secrets before releasing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 12, 2026 05:41
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive examples application for the TableauSharp SDK alongside CI/CD infrastructure. The changes include GitHub Actions workflows for continuous integration and tag-based NuGet publishing, a new .NET 8.0 console examples project with seven runnable sample classes demonstrating SDK features (authentication, data sources, workbooks, embedding, permissions, projects, users/groups), solution file updates, NuGet packaging metadata, and test dependencies (Moq, RichardSzalay.MockHttp).

Changes

SDK Examples and CI/CD Integration

Layer / File(s) Summary
CI and NuGet Publishing Workflows
.github/workflows/ci.yml, .github/workflows/publish-nuget.yml
GitHub Actions workflows added: CI workflow triggers on push/PR to master, builds/tests in Release mode, uploads TRX and Cobertura coverage artifacts. NuGet publish workflow triggers on version tags, extracts package version, builds/tests, packs/publishes nupkg and snupkg to nuget.org with API key authentication.
Solution and Examples Project Setup
TableauSharp.sln, samples/TableauSharp.Examples/TableauSharp.Examples.csproj, samples/TableauSharp.Examples/appsettings.json
Solution file adds TableauSharp.Examples project to samples folder with full build configurations (Debug/Release, Any CPU/x64/x86). Examples.csproj targets .NET 8.0 as an executable, enables nullable/implicit usings, references TableauSharp, and pins Microsoft.Extensions packages to 9.0.7. Configuration file defines Tableau server/auth options (PAT, credentials, JWT, connected app settings).
Example Application Entrypoint
samples/TableauSharp.Examples/Program.cs
Generic host configured with JSON/environment configuration, console logging, and TableauSharp services registered. Dispatcher selects and runs example class by command-line argument; displays help listing all available examples.
Authentication Examples
samples/TableauSharp.Examples/Examples/AuthExamples.cs
AuthExamples demonstrates PAT sign-in (logs token prefix, site/user IDs, expiration) and sign-out (invalidates server session), with exception logging.
Data Operations Examples
samples/TableauSharp.Examples/Examples/DataSourceExamples.cs, samples/TableauSharp.Examples/Examples/WorkbookExamples.cs, samples/TableauSharp.Examples/Examples/EmbeddingExamples.cs
DataSourceExamples lists data sources (with certified indicator) and triggers extract refresh. WorkbookExamples lists workbooks, lists views for first workbook, exports first view as PNG. EmbeddingExamples retrieves trusted ticket (hardcoded username/site), outputs ticket details and embed URL; generates embed URLs for views/workbooks with and without ticket.
Project and Permission Management Examples
samples/TableauSharp.Examples/Examples/ProjectExamples.cs, samples/TableauSharp.Examples/Examples/PermissionExamples.cs
ProjectExamples demonstrates listing projects, CRUD (create/update/delete), and nested project hierarchy via ParentProjectId. PermissionExamples retrieves workbook permissions (iterates grantees/capabilities), grants View capability to first user on first workbook, then revokes that permission.
User/Group Management Examples
samples/TableauSharp.Examples/Examples/UserGroupExamples.cs
UserGroupExamples lists users, creates/updates/deletes user (with site role/active flag changes), creates/adds/removes/deletes groups. All operations log failures via ILogger and print progress to console.
NuGet Packaging and Test Dependencies
src/TableauSharp/TableauSharp.csproj, test/TableauSharp.Tests/TableauSharp.Tests.csproj
TableauSharp.csproj adds package metadata (id, version, authors, description, tags, license, repository, README file inclusion, symbol package format). Test project adds Moq and RichardSzalay.MockHttp NuGet package references.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🐰 CI pipelines now stand tall,
Examples hop through every call,
Auth, projects, views, and more—
The SDK opens every door!
Hopping toward nuget.org 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title comprehensively summarizes all major components of the infrastructure work: test project setup, new Examples project, NuGet metadata configuration, and CI/CD pipelines.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch infra/phase-0-test-examples-pipeline

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (4)
.github/workflows/publish-nuget.yml (2)

47-59: ⚖️ Poor tradeoff

Consider migrating to NuGet trusted publishing.

NuGet.org supports OIDC-based trusted publishing, which eliminates the need to manage NUGET_API_KEY secrets and provides stronger security guarantees. The current API key approach works but is less secure.

📚 Reference

NuGet trusted publishing allows GitHub Actions to authenticate directly using OIDC tokens. See: https://learn.microsoft.com/en-us/nuget/nuget-org/publish-a-package#publishing-with-github-actions

Source: Linters/SAST tools


25-28: ⚡ Quick win

Review: Version extraction and template expansion.

The workflow extracts the package version from the tag name and passes it to dotnet pack via template expansion. While static analysis flags potential template injection at Line 44, the risk is mitigated by:

  1. The tag pattern v[0-9]+.[0-9]+.[0-9]+* restricts inputs to semantic version format
  2. MSBuild property assignment (-p:PackageVersion=...) is not shell-injectable
  3. GITHUB_REF_NAME is a GitHub-provided variable, not arbitrary user input

However, consider adding explicit validation of the extracted version format to provide defense in depth.

🛡️ Optional validation step
       - name: Extract version from tag
         id: version
         # Strip the leading 'v' from the tag name (v0.1.0 → 0.1.0)
         run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
 
+      - name: Validate version format
+        run: |
+          if ! [[ "${{ steps.version.outputs.VERSION }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then
+            echo "Invalid version format: ${{ steps.version.outputs.VERSION }}"
+            exit 1
+          fi
+
       - name: Restore

Also applies to: 44-44

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish-nuget.yml around lines 25 - 28, Add an explicit
semantic-version validation in the "Extract version from tag" step (id: version)
before exporting VERSION from GITHUB_REF_NAME: verify the stripped value
(assigned to VERSION) matches a strict semantic version regex (e.g.,
major.minor.patch with optional prerelease/build) and fail the job with a clear
error if it doesn't; only write to GITHUB_OUTPUT when validation passes so the
downstream dotnet pack invocation (which consumes VERSION via
-p:PackageVersion=...) always receives a validated value.

Source: Linters/SAST tools

samples/TableauSharp.Examples/Examples/WorkbookExamples.cs (1)

92-93: 💤 Low value

Consider sanitizing the server-provided filename for defense in depth.

The code writes to Path.Combine(Path.GetTempPath(), export.FileName) where export.FileName comes from the server response. While Path.Combine provides some protection and the SDK likely validates the filename, explicitly sanitizing it would provide defense in depth against potential path traversal attacks.

♻️ Optional defensive hardening
-var outputPath = Path.Combine(Path.GetTempPath(), export.FileName);
+var sanitizedFileName = Path.GetFileName(export.FileName); // Strip any path components
+var outputPath = Path.Combine(Path.GetTempPath(), sanitizedFileName);
 await File.WriteAllBytesAsync(outputPath, export.FileContent);

This ensures only the filename (not path components) is used, preventing directory traversal even if the SDK validation has a gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/TableauSharp.Examples/Examples/WorkbookExamples.cs` around lines 92 -
93, Sanitize the server-provided export.FileName before using it with
Path.Combine to prevent path traversal: replace using export.FileName directly
in Path.Combine(Path.GetTempPath(), export.FileName) with a sanitized name
(e.g., use Path.GetFileName(export.FileName) or otherwise strip path separators
and validate characters), and fall back to a safe default filename if the result
is empty; then pass that sanitized filename to Path.Combine and continue using
File.WriteAllBytesAsync(outputPath, export.FileContent).
TableauSharp.sln (1)

16-16: Document the difference between TableauSharp.Sample and TableauSharp.Examples

  • TableauSharp.Sample contains web-style app code (e.g., Controllers/HomeController.cs, Models/ErrorViewModel.cs), while samples/TableauSharp.Examples is organized as multiple Examples/*Examples.cs modules with its own Program.cs.
  • No README*.md / *.md in the repo references “Sample” vs “Examples`, so add a short note on what each host is for and how to run them to avoid confusion.
    -->
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@TableauSharp.sln` at line 16, Add documentation clarifying the difference
between the two sample hosts: state that TableauSharp.Sample is a web-style app
(references: Controllers/HomeController.cs, Models/ErrorViewModel.cs) and
TableauSharp.Examples is a set of console/example modules (references:
samples/Examples/*Examples.cs and samples/Program.cs); create or update a README
(e.g., README.md at repo root or README_SAMPLES.md) with a short section
describing each host, how to run them (commands to start the web app vs run the
examples Program.cs), and where to find relevant entry points (HomeController.cs
for the web sample and *Examples.cs/Program.cs for the examples).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 15-16: Update the checkout steps that currently read "uses:
actions/checkout@v4" to include "persist-credentials: false" and replace the
mutable tag with an immutable commit SHA (i.e., pin the action to a specific
commit) in both workflows; locate the checkout step symbol "uses:
actions/checkout@v4" and change it to use the commit SHA reference and add the
"persist-credentials: false" input so credentials are not persisted and the
action is pinned.
- Line 16: Pin the referenced GitHub Actions to immutable commit SHAs instead of
mutable tags: replace each occurrence of "uses: actions/checkout@v4", "uses:
actions/setup-dotnet@v4", and both "uses: actions/upload-artifact@v4" with the
corresponding full commit SHA for the action repo (obtain the desired commit SHA
from the action's GitHub repository releases/commits) so the workflow always
uses that exact commit; update all instances in the file (search for the strings
"actions/checkout@v4", "actions/setup-dotnet@v4", and
"actions/upload-artifact@v4") and commit the replacements.

In @.github/workflows/publish-nuget.yml:
- Line 18: Replace the floating action versions with pinned commit SHAs: find
occurrences of actions/checkout@v4, actions/setup-dotnet@v4 and
actions/upload-artifact@v4 in the workflow files and change each to the
corresponding owner/repo@<commit_sha> form (use the exact commit SHA for the
release you want to pin); ensure every instance is updated (the three
identifiers above) so the workflows use immutable references rather than version
tags.

In `@samples/TableauSharp.Examples/Examples/AuthExamples.cs`:
- Around line 44-59: SignOutAsync is performing a redundant sign-in by calling
authService.SignInWithPATAsync() even though RunAsync already signed in and
tokens are stored in ITableauTokenProvider; remove the SignInWithPATAsync call
and retrieve the existing token from the shared token provider (or accept the
token as a parameter) and pass that token to authService.SignOutAsync(token) in
SignOutAsync (update SignOutAsync signature or body to use
ITableauTokenProvider/GetStoredToken instead of creating a new session);
alternatively, if examples must be standalone, change RunAsync to invoke only
one example at a time or document that each example is independent so they
should not be called sequentially.

In `@samples/TableauSharp.Examples/Examples/PermissionExamples.cs`:
- Line 93: PermissionExamples currently grants a permission using
PermissionCapability.View.ToString() (which is correct) but then only logs
exceptions, so if DeleteWorkbookPermissionAsync fails the granted permission may
remain; update the flow in PermissionExamples to ensure cleanup: after calling
PermissionService.DeleteWorkbookPermissionAsync (and similarly after granting
via the method that uses PermissionCapability.View.ToString()), wrap the
grant+delete sequence in try/finally (or catch+revoke in the catch) and in the
finally attempt a best-effort revoke/remove of the permission (calling the same
PermissionService revoke/delete method) and log any errors from that cleanup so
the example leaves no side-effects even when deletion initially fails.

In `@samples/TableauSharp.Examples/Examples/ProjectExamples.cs`:
- Around line 75-101: CreateNestedProjectAsync leaves the parent project behind
if child creation or deletion fails; after creating parent (variable parent via
projectService.CreateAsync in CreateNestedProjectAsync) ensure the parent is
always deleted by using a try/finally (or nested try) around the child
creation/deletion block: after parent is assigned, run child creation/deletion
inside a try and in the finally attempt projectService.DeleteAsync(parent.Id)
(guard null) and catch/log any deletion errors (use logger.LogError) so parent
cleanup is attempted even when child operations throw.

In `@samples/TableauSharp.Examples/Examples/UserGroupExamples.cs`:
- Around line 77-106: CreateGroupAndManageMembersAsync may leave the created
group behind if AddUserToGroupAsync or RemoveUserFromGroupAsync throws; change
the flow to ensure the group created by groupService.CreateAsync (the local
variable group) is always cleaned up by invoking groupService.DeleteAsync in a
finally block (or equivalent) only when group is non-null, and log any errors
from the delete call (e.g., with logger.LogError) without suppressing the
original exception flow.

In `@samples/TableauSharp.Examples/TableauSharp.Examples.csproj`:
- Line 5: The project targets net8.0 but references Microsoft.Extensions
packages at 9.0.7, causing a framework/package mismatch; fix by either (A)
changing the <TargetFramework> value from net8.0 to net9.0 to match the
Microsoft.Extensions 9.x packages, or (B) downgrade the Microsoft.Extensions
package references (those PackageReference entries for Microsoft.Extensions.*
around lines referencing 9.0.7) to the 8.x versions that align with net8.0 so
the target framework and package major versions match.
- Around line 16-19: Update the Microsoft.Extensions package pins from 9.0.7 to
the current stable 10.0.9: change the Version for
Microsoft.Extensions.Configuration.Json,
Microsoft.Extensions.DependencyInjection, Microsoft.Extensions.Hosting, and
Microsoft.Extensions.Logging.Console in the TableauSharp.Examples project and
also update Microsoft.Extensions.Http and Microsoft.Extensions.Options in the
TableauSharp project to the same 10.0.9 version so all Microsoft.Extensions
packages are consistent across the repo.

---

Nitpick comments:
In @.github/workflows/publish-nuget.yml:
- Around line 25-28: Add an explicit semantic-version validation in the "Extract
version from tag" step (id: version) before exporting VERSION from
GITHUB_REF_NAME: verify the stripped value (assigned to VERSION) matches a
strict semantic version regex (e.g., major.minor.patch with optional
prerelease/build) and fail the job with a clear error if it doesn't; only write
to GITHUB_OUTPUT when validation passes so the downstream dotnet pack invocation
(which consumes VERSION via -p:PackageVersion=...) always receives a validated
value.

In `@samples/TableauSharp.Examples/Examples/WorkbookExamples.cs`:
- Around line 92-93: Sanitize the server-provided export.FileName before using
it with Path.Combine to prevent path traversal: replace using export.FileName
directly in Path.Combine(Path.GetTempPath(), export.FileName) with a sanitized
name (e.g., use Path.GetFileName(export.FileName) or otherwise strip path
separators and validate characters), and fall back to a safe default filename if
the result is empty; then pass that sanitized filename to Path.Combine and
continue using File.WriteAllBytesAsync(outputPath, export.FileContent).

In `@TableauSharp.sln`:
- Line 16: Add documentation clarifying the difference between the two sample
hosts: state that TableauSharp.Sample is a web-style app (references:
Controllers/HomeController.cs, Models/ErrorViewModel.cs) and
TableauSharp.Examples is a set of console/example modules (references:
samples/Examples/*Examples.cs and samples/Program.cs); create or update a README
(e.g., README.md at repo root or README_SAMPLES.md) with a short section
describing each host, how to run them (commands to start the web app vs run the
examples Program.cs), and where to find relevant entry points (HomeController.cs
for the web sample and *Examples.cs/Program.cs for the examples).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d67d8e8-3a7a-485a-a174-ee86b525da01

📥 Commits

Reviewing files that changed from the base of the PR and between 1b5c319 and 83ee144.

📒 Files selected for processing (15)
  • .github/workflows/ci.yml
  • .github/workflows/publish-nuget.yml
  • TableauSharp.sln
  • samples/TableauSharp.Examples/Examples/AuthExamples.cs
  • samples/TableauSharp.Examples/Examples/DataSourceExamples.cs
  • samples/TableauSharp.Examples/Examples/EmbeddingExamples.cs
  • samples/TableauSharp.Examples/Examples/PermissionExamples.cs
  • samples/TableauSharp.Examples/Examples/ProjectExamples.cs
  • samples/TableauSharp.Examples/Examples/UserGroupExamples.cs
  • samples/TableauSharp.Examples/Examples/WorkbookExamples.cs
  • samples/TableauSharp.Examples/Program.cs
  • samples/TableauSharp.Examples/TableauSharp.Examples.csproj
  • samples/TableauSharp.Examples/appsettings.json
  • src/TableauSharp/TableauSharp.csproj
  • test/TableauSharp.Tests/TableauSharp.Tests.csproj

Comment thread .github/workflows/ci.yml
Comment on lines +15 to +16
- name: Checkout
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

GitHub Actions security hardening needed in both workflows.

Both .github/workflows/ci.yml and .github/workflows/publish-nuget.yml share the same security gaps:

  1. Missing persist-credentials: false on checkout actions - Repository credentials could be exposed through artifacts or subsequent steps
  2. Unpinned action references - Actions are pinned to mutable version tags (v4) instead of immutable commit SHAs, creating supply chain attack vectors

These are industry best practices for securing GitHub Actions workflows and should be addressed before production use.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 15-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 15 - 16, Update the checkout steps
that currently read "uses: actions/checkout@v4" to include "persist-credentials:
false" and replace the mutable tag with an immutable commit SHA (i.e., pin the
action to a specific commit) in both workflows; locate the checkout step symbol
"uses: actions/checkout@v4" and change it to use the commit SHA reference and
add the "persist-credentials: false" input so credentials are not persisted and
the action is pinned.

Source: Linters/SAST tools

Comment thread .github/workflows/ci.yml

steps:
- name: Checkout
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch current commit hashes for pinned actions

echo "=== actions/checkout@v4 ==="
gh api repos/actions/checkout/git/matching-refs/tags/v4 --jq '.[0].object.sha'

echo "=== actions/setup-dotnet@v4 ==="
gh api repos/actions/setup-dotnet/git/matching-refs/tags/v4 --jq '.[0].object.sha'

echo "=== actions/upload-artifact@v4 ==="
gh api repos/actions/upload-artifact/git/matching-refs/tags/v4 --jq '.[0].object.sha'

Repository: teesofttech/TableauSharp

Length of output: 3690


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "=== exists? ==="
ls -la "$FILE"

echo "=== uses: actions/ in $FILE ==="
rg -n "uses:\s*actions/" "$FILE" || true

echo "=== show surrounding lines for referenced areas ==="
# Print around line 1-120 (workflow likely small); cap output
sed -n '1,120p' "$FILE" | cat -n

Repository: teesofttech/TableauSharp

Length of output: 1881


Security: Pin GitHub Actions to immutable commit SHAs (not @v4 tags).

In .github/workflows/ci.yml, these steps reference mutable tags: actions/checkout@v4 (line 16), actions/setup-dotnet@v4 (line 19), and actions/upload-artifact@v4 (lines 40, 47). Pin each uses: to the full commit SHA to prevent supply-chain risk from tag updates.

- uses: actions/checkout@<commit-sha>
- uses: actions/setup-dotnet@<commit-sha>
- uses: actions/upload-artifact@<commit-sha>
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 15-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 16, Pin the referenced GitHub Actions to
immutable commit SHAs instead of mutable tags: replace each occurrence of "uses:
actions/checkout@v4", "uses: actions/setup-dotnet@v4", and both "uses:
actions/upload-artifact@v4" with the corresponding full commit SHA for the
action repo (obtain the desired commit SHA from the action's GitHub repository
releases/commits) so the workflow always uses that exact commit; update all
instances in the file (search for the strings "actions/checkout@v4",
"actions/setup-dotnet@v4", and "actions/upload-artifact@v4") and commit the
replacements.

Source: Linters/SAST tools


steps:
- name: Checkout
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la .github/workflows || true

echo "---- publish-nuget.yml ----"
sed -n '1,200p' .github/workflows/publish-nuget.yml

echo "---- ci.yml (if present) ----"
if [ -f .github/workflows/ci.yml ]; then
  sed -n '1,240p' .github/workflows/ci.yml
else
  # Try common variants
  for f in .github/workflows/*ci*.yml .github/workflows/*CI*.yml; do
    [ -f "$f" ] || continue
    echo "---- $f ----"
    sed -n '1,240p' "$f"
  done
fi

echo "---- all workflow action usages in publish-nuget.yml ----"
rg -n "uses:\s*[^#\n]+" .github/workflows/publish-nuget.yml || true

echo "---- all workflow action usages in ci.yml (or matches) ----"
if [ -f .github/workflows/ci.yml ]; then
  rg -n "uses:\s*[^#\n]+" .github/workflows/ci.yml || true
else
  for f in .github/workflows/*ci*.yml .github/workflows/*CI*.yml; do
    [ -f "$f" ] || continue
    echo "## $f"
    rg -n "uses:\s*[^#\n]+" "$f" || true
  done
fi

Repository: teesofttech/TableauSharp

Length of output: 3612


Security: Pin GitHub Actions to immutable commit SHAs (no version tags).

In .github/workflows/publish-nuget.yml, replace actions/checkout@v4 (line 18), actions/setup-dotnet@v4 (line 21), and actions/upload-artifact@v4 (lines 62-65) with pinned owner/repo@<commit_sha> versions. The same applies in .github/workflows/ci.yml (actions/checkout@v4 line 16, actions/setup-dotnet@v4 line 19, actions/upload-artifact@v4 lines 40 and 47).

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 17-18: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 18-18: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish-nuget.yml at line 18, Replace the floating action
versions with pinned commit SHAs: find occurrences of actions/checkout@v4,
actions/setup-dotnet@v4 and actions/upload-artifact@v4 in the workflow files and
change each to the corresponding owner/repo@<commit_sha> form (use the exact
commit SHA for the release you want to pin); ensure every instance is updated
(the three identifiers above) so the workflows use immutable references rather
than version tags.

Source: Linters/SAST tools

Comment on lines +44 to +59
private async Task SignOutAsync()
{
Console.WriteLine("[2] Sign out");
try
{
var token = await authService.SignInWithPATAsync();
await authService.SignOutAsync(token.Token);
Console.WriteLine(" Signed out successfully.");
}
catch (Exception ex)
{
logger.LogError(ex, "Sign-out failed");
}

Console.WriteLine();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Redundant authentication in SignOutAsync wastes resources.

The SignOutAsync method calls authService.SignInWithPATAsync() at line 49 to obtain a token, but RunAsync already signed in at line 17. According to the class comment (lines 7-9), "After sign-in the token is stored automatically in ITableauTokenProvider," so the second sign-in creates an unnecessary server session and contradicts the documented usage pattern.

When RunAsync calls both methods sequentially (lines 17-18), this results in two sign-ins for one sign-out, which is inefficient and demonstrates incorrect API usage.

🔧 Recommended fixes

Option 1: Store and reuse the token from the initial sign-in

 public async Task RunAsync()
 {
     Console.WriteLine("--- Auth Examples ---");
     Console.WriteLine();

-    await SignInWithPATAsync();
-    await SignOutAsync();
+    var token = await SignInWithPATAsync();
+    await SignOutAsync(token);
 }

-private async Task SignInWithPATAsync()
+private async Task<string> SignInWithPATAsync()
 {
     Console.WriteLine("[1] Sign in with Personal Access Token");
     try
     {
         var token = await authService.SignInWithPATAsync();
         Console.WriteLine($"    Token    : {token.Token[..8]}...");
         Console.WriteLine($"    SiteId   : {token.SiteId}");
         Console.WriteLine($"    UserId   : {token.UserId}");
         Console.WriteLine($"    Expires  : {token.Expiration:u}");
+        return token.Token;
     }
     catch (Exception ex)
     {
         logger.LogError(ex, "PAT sign-in failed");
+        return null;
     }

     Console.WriteLine();
 }

-private async Task SignOutAsync()
+private async Task SignOutAsync(string tokenString)
 {
     Console.WriteLine("[2] Sign out");
+    if (string.IsNullOrEmpty(tokenString))
+    {
+        Console.WriteLine("    No token available to sign out.");
+        return;
+    }
+
     try
     {
-        var token = await authService.SignInWithPATAsync();
-        await authService.SignOutAsync(token.Token);
+        await authService.SignOutAsync(tokenString);
         Console.WriteLine("    Signed out successfully.");
     }
     catch (Exception ex)
     {
         logger.LogError(ex, "Sign-out failed");
     }

     Console.WriteLine();
 }

Option 2: Make examples independent (if they're meant to run standalone)

Restructure RunAsync to run only one example at a time based on a parameter, or add a comment clarifying that these are independent workflows not meant to be called sequentially.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/TableauSharp.Examples/Examples/AuthExamples.cs` around lines 44 - 59,
SignOutAsync is performing a redundant sign-in by calling
authService.SignInWithPATAsync() even though RunAsync already signed in and
tokens are stored in ITableauTokenProvider; remove the SignInWithPATAsync call
and retrieve the existing token from the shared token provider (or accept the
token as a parameter) and pass that token to authService.SignOutAsync(token) in
SignOutAsync (update SignOutAsync signature or body to use
ITableauTokenProvider/GetStoredToken instead of creating a new session);
alternatively, if examples must be standalone, change RunAsync to invoke only
one example at a time or document that each example is independent so they
should not be called sequentially.

Console.WriteLine($" Granted View/Allow to user '{user.Name}' on workbook '{workbook.Name}'.");

await permissionService.DeleteWorkbookPermissionAsync(
workbook.Id, user.Id, "User", PermissionCapability.View.ToString());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate DeleteWorkbookPermissionAsync and check its signature and capability parameter usage

# Find the method definition
ast-grep --pattern $'DeleteWorkbookPermissionAsync($$$) {
  $$$
}'

# Also search for any capability format documentation or usage
rg -nP -A5 -B5 'DeleteWorkbookPermissionAsync' --type=cs

Repository: teesofttech/TableauSharp

Length of output: 3606


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate DeletePermissionAsync implementation and print relevant sections
rg -n --context 2 "DeletePermissionAsync" src/TableauSharp/Permissions/Services/PermissionService.cs
echo "----"
# Print a wider window around the implementation (adjust by first match line)
line=$(rg -n "DeletePermissionAsync" src/TableauSharp/Permissions/Services/PermissionService.cs | head -n1 | cut -d: -f1)
start=$((line-30))
end=$((line+80))
sed -n "${start},${end}p" src/TableauSharp/Permissions/Services/PermissionService.cs

echo "----"
# Check how capability is used/serialized elsewhere (e.g., Add/Put permission flows)
rg -n --context 3 "PermissionCapability" src/TableauSharp/Permissions -S --type=cs
rg -n --context 3 "capability" src/TableauSharp/Permissions/Services/PermissionService.cs -S --type=cs

Repository: teesofttech/TableauSharp

Length of output: 9281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Read the PermissionCapability enum definition
enum_file=$(rg -n --files-with-matches "enum PermissionCapability" src/TableauSharp | head -n1 || true)
echo "Enum file: $enum_file"
if [ -n "$enum_file" ]; then
  sed -n '1,160p' "$enum_file"
fi

echo "----"

# 2) Read PermissionExamples.cs around the revoke block (lines ~70-110)
sed -n '70,115p' samples/TableauSharp.Examples/Examples/PermissionExamples.cs

Repository: teesofttech/TableauSharp

Length of output: 1546


Fix the capability-string concern: PermissionCapability.View.ToString() is consistent with the API usage in this SDK.

PermissionService sends the capability name using c.Capability.ToString() when granting, and it parses the API’s returned name with Enum.TryParse(..., true). PermissionCapability.View.ToString() is "View", so using it for the DeleteWorkbookPermissionAsync capability URL segment is aligned with the SDK’s own contract.

The remaining issue is only that PermissionExamples catches exceptions and logs them, so if the DELETE fails for any reason, the “leave no side-effects” expectation isn’t guaranteed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/TableauSharp.Examples/Examples/PermissionExamples.cs` at line 93,
PermissionExamples currently grants a permission using
PermissionCapability.View.ToString() (which is correct) but then only logs
exceptions, so if DeleteWorkbookPermissionAsync fails the granted permission may
remain; update the flow in PermissionExamples to ensure cleanup: after calling
PermissionService.DeleteWorkbookPermissionAsync (and similarly after granting
via the method that uses PermissionCapability.View.ToString()), wrap the
grant+delete sequence in try/finally (or catch+revoke in the catch) and in the
finally attempt a best-effort revoke/remove of the permission (calling the same
PermissionService revoke/delete method) and log any errors from that cleanup so
the example leaves no side-effects even when deletion initially fails.

Comment on lines +75 to +101
// Sub-projects allow hierarchical organisation of content on Tableau Server.
private async Task CreateNestedProjectAsync()
{
Console.WriteLine("[3] Create a nested project (sub-folder)");
try
{
var parent = await projectService.CreateAsync(new ProjectCreateRequest { Name = "SDK-Parent" });
Console.WriteLine($" Parent: {parent.Id} {parent.Name}");

var child = await projectService.CreateAsync(new ProjectCreateRequest
{
Name = "SDK-Child",
ParentProjectId = parent.Id
});
Console.WriteLine($" Child : {child.Id} {child.Name} (parent={child.ParentProjectId})");

await projectService.DeleteAsync(child.Id);
await projectService.DeleteAsync(parent.Id);
Console.WriteLine(" Cleaned up.");
}
catch (Exception ex)
{
logger.LogError(ex, "Nested project example failed");
}

Console.WriteLine();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Incomplete cleanup when nested project operations fail.

If child project creation (line 84) or child deletion (line 91) throws, the catch block at line 95 captures the exception but the parent project created at line 81 remains on the server. This violates the example's intent to be side-effect-free and may accumulate test data across repeated runs.

🧹 Proposed fix to ensure parent cleanup
 private async Task CreateNestedProjectAsync()
 {
     Console.WriteLine("[3] Create a nested project (sub-folder)");
+    string? parentId = null;
+    string? childId = null;
     try
     {
         var parent = await projectService.CreateAsync(new ProjectCreateRequest { Name = "SDK-Parent" });
+        parentId = parent.Id;
         Console.WriteLine($"    Parent: {parent.Id}  {parent.Name}");

         var child = await projectService.CreateAsync(new ProjectCreateRequest
         {
             Name = "SDK-Child",
             ParentProjectId = parent.Id
         });
+        childId = child.Id;
         Console.WriteLine($"    Child : {child.Id}  {child.Name}  (parent={child.ParentProjectId})");
-
-        await projectService.DeleteAsync(child.Id);
-        await projectService.DeleteAsync(parent.Id);
-        Console.WriteLine("    Cleaned up.");
     }
     catch (Exception ex)
     {
         logger.LogError(ex, "Nested project example failed");
     }
+    finally
+    {
+        // Clean up in reverse order, ignore exceptions
+        if (childId is not null)
+            try { await projectService.DeleteAsync(childId); } catch { }
+        if (parentId is not null)
+            try { await projectService.DeleteAsync(parentId); } catch { }
+        if (childId is not null || parentId is not null)
+            Console.WriteLine("    Cleaned up.");
+    }

     Console.WriteLine();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Sub-projects allow hierarchical organisation of content on Tableau Server.
private async Task CreateNestedProjectAsync()
{
Console.WriteLine("[3] Create a nested project (sub-folder)");
try
{
var parent = await projectService.CreateAsync(new ProjectCreateRequest { Name = "SDK-Parent" });
Console.WriteLine($" Parent: {parent.Id} {parent.Name}");
var child = await projectService.CreateAsync(new ProjectCreateRequest
{
Name = "SDK-Child",
ParentProjectId = parent.Id
});
Console.WriteLine($" Child : {child.Id} {child.Name} (parent={child.ParentProjectId})");
await projectService.DeleteAsync(child.Id);
await projectService.DeleteAsync(parent.Id);
Console.WriteLine(" Cleaned up.");
}
catch (Exception ex)
{
logger.LogError(ex, "Nested project example failed");
}
Console.WriteLine();
}
private async Task CreateNestedProjectAsync()
{
Console.WriteLine("[3] Create a nested project (sub-folder)");
string? parentId = null;
string? childId = null;
try
{
var parent = await projectService.CreateAsync(new ProjectCreateRequest { Name = "SDK-Parent" });
parentId = parent.Id;
Console.WriteLine($" Parent: {parent.Id} {parent.Name}");
var child = await projectService.CreateAsync(new ProjectCreateRequest
{
Name = "SDK-Child",
ParentProjectId = parent.Id
});
childId = child.Id;
Console.WriteLine($" Child : {child.Id} {child.Name} (parent={child.ParentProjectId})");
}
catch (Exception ex)
{
logger.LogError(ex, "Nested project example failed");
}
finally
{
// Clean up in reverse order, ignore exceptions
if (childId is not null)
try { await projectService.DeleteAsync(childId); } catch { }
if (parentId is not null)
try { await projectService.DeleteAsync(parentId); } catch { }
if (childId is not null || parentId is not null)
Console.WriteLine(" Cleaned up.");
}
Console.WriteLine();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/TableauSharp.Examples/Examples/ProjectExamples.cs` around lines 75 -
101, CreateNestedProjectAsync leaves the parent project behind if child creation
or deletion fails; after creating parent (variable parent via
projectService.CreateAsync in CreateNestedProjectAsync) ensure the parent is
always deleted by using a try/finally (or nested try) around the child
creation/deletion block: after parent is assigned, run child creation/deletion
inside a try and in the finally attempt projectService.DeleteAsync(parent.Id)
(guard null) and catch/log any deletion errors (use logger.LogError) so parent
cleanup is attempted even when child operations throw.

Comment on lines +77 to +106
private async Task CreateGroupAndManageMembersAsync()
{
Console.WriteLine("[3] Create group and manage members");
try
{
var group = await groupService.CreateAsync(new GroupCreateRequest { Name = "SDK-Example-Group" });
Console.WriteLine($" Group created: {group.Id} {group.Name}");

// Add first available user to the group
var users = await userService.GetAllAsync();
var user = users.FirstOrDefault();
if (user is not null)
{
await groupService.AddUserToGroupAsync(group.Id, user.Id);
Console.WriteLine($" Added user '{user.Name}' to group.");

await groupService.RemoveUserFromGroupAsync(group.Id, user.Id);
Console.WriteLine(" Removed user from group.");
}

await groupService.DeleteAsync(group.Id);
Console.WriteLine(" Group deleted.");
}
catch (Exception ex)
{
logger.LogError(ex, "Group management failed");
}

Console.WriteLine();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Incomplete cleanup when group membership operations fail.

If AddUserToGroupAsync (line 90) or RemoveUserFromGroupAsync (line 93) throws, the catch block at line 100 captures the exception but the group created at line 82 remains on the server. This pattern mirrors the cleanup issue in ProjectExamples.cs and may accumulate test groups across repeated runs.

🧹 Proposed fix to ensure group cleanup
 private async Task CreateGroupAndManageMembersAsync()
 {
     Console.WriteLine("[3] Create group and manage members");
+    string? groupId = null;
     try
     {
         var group = await groupService.CreateAsync(new GroupCreateRequest { Name = "SDK-Example-Group" });
+        groupId = group.Id;
         Console.WriteLine($"    Group created: {group.Id}  {group.Name}");

         // Add first available user to the group
         var users = await userService.GetAllAsync();
         var user = users.FirstOrDefault();
         if (user is not null)
         {
             await groupService.AddUserToGroupAsync(group.Id, user.Id);
             Console.WriteLine($"    Added user '{user.Name}' to group.");

             await groupService.RemoveUserFromGroupAsync(group.Id, user.Id);
             Console.WriteLine("    Removed user from group.");
         }
-
-        await groupService.DeleteAsync(group.Id);
-        Console.WriteLine("    Group deleted.");
     }
     catch (Exception ex)
     {
         logger.LogError(ex, "Group management failed");
     }
+    finally
+    {
+        if (groupId is not null)
+        {
+            try
+            {
+                await groupService.DeleteAsync(groupId);
+                Console.WriteLine("    Group deleted.");
+            }
+            catch { }
+        }
+    }

     Console.WriteLine();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async Task CreateGroupAndManageMembersAsync()
{
Console.WriteLine("[3] Create group and manage members");
try
{
var group = await groupService.CreateAsync(new GroupCreateRequest { Name = "SDK-Example-Group" });
Console.WriteLine($" Group created: {group.Id} {group.Name}");
// Add first available user to the group
var users = await userService.GetAllAsync();
var user = users.FirstOrDefault();
if (user is not null)
{
await groupService.AddUserToGroupAsync(group.Id, user.Id);
Console.WriteLine($" Added user '{user.Name}' to group.");
await groupService.RemoveUserFromGroupAsync(group.Id, user.Id);
Console.WriteLine(" Removed user from group.");
}
await groupService.DeleteAsync(group.Id);
Console.WriteLine(" Group deleted.");
}
catch (Exception ex)
{
logger.LogError(ex, "Group management failed");
}
Console.WriteLine();
}
private async Task CreateGroupAndManageMembersAsync()
{
Console.WriteLine("[3] Create group and manage members");
string? groupId = null;
try
{
var group = await groupService.CreateAsync(new GroupCreateRequest { Name = "SDK-Example-Group" });
groupId = group.Id;
Console.WriteLine($" Group created: {group.Id} {group.Name}");
// Add first available user to the group
var users = await userService.GetAllAsync();
var user = users.FirstOrDefault();
if (user is not null)
{
await groupService.AddUserToGroupAsync(group.Id, user.Id);
Console.WriteLine($" Added user '{user.Name}' to group.");
await groupService.RemoveUserFromGroupAsync(group.Id, user.Id);
Console.WriteLine(" Removed user from group.");
}
}
catch (Exception ex)
{
logger.LogError(ex, "Group management failed");
}
finally
{
if (groupId is not null)
{
try
{
await groupService.DeleteAsync(groupId);
Console.WriteLine(" Group deleted.");
}
catch { }
}
}
Console.WriteLine();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/TableauSharp.Examples/Examples/UserGroupExamples.cs` around lines 77
- 106, CreateGroupAndManageMembersAsync may leave the created group behind if
AddUserToGroupAsync or RemoveUserFromGroupAsync throws; change the flow to
ensure the group created by groupService.CreateAsync (the local variable group)
is always cleaned up by invoking groupService.DeleteAsync in a finally block (or
equivalent) only when group is non-null, and log any errors from the delete call
(e.g., with logger.LogError) without suppressing the original exception flow.


<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Framework and package version mismatch.

The project targets net8.0 (line 5) but references Microsoft.Extensions packages version 9.0.7 (lines 16-19). Microsoft.Extensions 9.x packages are part of the .NET 9 family and are primarily intended for use with .NET 9 applications.

While cross-version compatibility may work due to .NET's backwards compatibility, this configuration is non-standard and could lead to unexpected runtime behavior or compatibility issues.

🔧 Recommended fix

Option 1 (Recommended): Use Microsoft.Extensions 8.x packages with net8.0

   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
-    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
-    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
+    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
   </ItemGroup>

Option 2: Target net9.0 to match the 9.x packages

   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>

Also applies to: 16-19

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/TableauSharp.Examples/TableauSharp.Examples.csproj` at line 5, The
project targets net8.0 but references Microsoft.Extensions packages at 9.0.7,
causing a framework/package mismatch; fix by either (A) changing the
<TargetFramework> value from net8.0 to net9.0 to match the Microsoft.Extensions
9.x packages, or (B) downgrade the Microsoft.Extensions package references
(those PackageReference entries for Microsoft.Extensions.* around lines
referencing 9.0.7) to the 8.x versions that align with net8.0 so the target
framework and package major versions match.

Comment on lines +16 to +19
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for latest versions and security advisories for Microsoft.Extensions packages

echo "=== Checking latest versions on NuGet ==="
for pkg in "Microsoft.Extensions.Configuration.Json" "Microsoft.Extensions.DependencyInjection" "Microsoft.Extensions.Hosting" "Microsoft.Extensions.Logging.Console"; do
  echo "Package: $pkg"
  curl -s "https://api.nuget.org/v3-flatcontainer/$pkg/index.json" | jq -r '.versions[-5:]'
  echo ""
done

echo "=== Checking for security advisories ==="
gh api graphql -f query='
{
  securityVulnerabilities(first: 10, ecosystem: NUGET, package: "Microsoft.Extensions") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      package { name }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}'

Repository: teesofttech/TableauSharp

Length of output: 743


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json, sys, urllib.request

packages = [
  "Microsoft.Extensions.Configuration.Json",
  "Microsoft.Extensions.DependencyInjection",
  "Microsoft.Extensions.Hosting",
  "Microsoft.Extensions.Logging.Console",
]
version = "9.0.7"

def fetch_url(url):
    req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0"})
    with urllib.request.urlopen(req, timeout=30) as resp:
        return resp.read()

def get_versions(pkg):
    # NuGet flat container is case-insensitive in practice, but we'll use the same casing in the URL.
    # If it fails, we fall back to lowercase.
    base = "https://api.nuget.org/v3-flatcontainer"
    for name in (pkg, pkg.lower()):
        url = f"{base}/{name}/index.json"
        try:
            data = fetch_url(url)
            j = json.loads(data)
            return j.get("versions", [])
        except Exception:
            continue
    raise RuntimeError(f"Failed to fetch versions for {pkg}")

def osv_query(pkg, ver):
    url = "https://api.osv.dev/v1/query"
    payload = {"package": {"name": pkg}, "version": ver}
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(url, data=data, headers={"Content-Type":"application/json","User-Agent":"Mozilla/5.0"})
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read())

def parse_semver(v):
    # basic semver parser sufficient for ordering
    # handles forms like 9.0.7, 9.0.7-preview.1, etc.
    # We'll sort by the leading numeric triples only.
    parts = v.split('-')[0].split('.')
    nums = [int(p) for p in parts[:3]] + [0]*(3-len(parts[:3]))
    return tuple(nums[:3])

print("=== NuGet: latest versions (tail) and whether 9.0.7 is present ===")
for pkg in packages:
    versions = get_versions(pkg)
    if not versions:
        print(pkg, "=> no versions returned")
        continue
    # Sort using numeric portion first, but keep original strings for display.
    sorted_versions = sorted(versions, key=parse_semver)
    present = version in versions
    tail = sorted_versions[-10:] if len(sorted_versions) >= 10 else sorted_versions
    print(f"\n{pkg}")
    print(f"  9.0.7 present: {present}")
    print(f"  latest (up to 10): {', '.join(tail)}")

print("\n=== OSV: vulnerabilities affecting version 9.0.7 ===")
for pkg in packages:
    try:
        res = osv_query(pkg, version)
        vulns = res.get("vulns", [])
        print(f"\n{pkg} @ {version}: {len(vulns)} OSV vuln(s)")
        for v in vulns[:5]:
            summary = v.get("summary","")
            sev = v.get("severity","")
            aliases = v.get("aliases",[])
            references = v.get("references",[])
            cves = [a for a in aliases if isinstance(a,str) and (a.upper().startswith("CVE-") or "CVE" in a.upper())]
            print("  -", summary[:140], ("| severity:" + str(sev) if sev else ""), "| refs:", len(references), "aliases:", (", ".join(aliases[:3]) + ("..." if len(aliases)>3 else "")) if aliases else "")
            if cves:
                print("    CVEs:", ", ".join(cves[:3]) + ("..." if len(cves)>3 else ""))
    except Exception as e:
        print(f"\n{pkg} @ {version}: OSV query failed: {e}")
PY

Repository: teesofttech/TableauSharp

Length of output: 353


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the csproj around the referenced lines
echo "=== TableauSharp.Examples.csproj (lines ~1-60) ==="
sed -n '1,80p' samples/TableauSharp.Examples/TableauSharp.Examples.csproj

# Search for Microsoft.Extensions package references/version pins elsewhere
echo
echo "=== All references to Microsoft.Extensions.* in repo ==="
rg -n "Microsoft\.Extensions\.(Configuration\.Json|DependencyInjection|Hosting|Logging\.Console)|Microsoft\.Extensions\." -S .

Repository: teesofttech/TableauSharp

Length of output: 4504


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="https://api.nuget.org/v3-flatcontainer"
pkg="Microsoft.Extensions.Configuration.Json"
url="$base/$pkg/index.json"

echo "=== NuGet HTTP check: $url ==="
# Print HTTP status and first bytes (without dumping full body)
curl -sS -D /tmp/nuget_headers.txt -o /tmp/nuget_body.txt "$url" || true
echo "--- HTTP headers ---"
cat /tmp/nuget_headers.txt
echo "--- Body (first 300 chars) ---"
head -c 300 /tmp/nuget_body.txt; echo
echo

echo "=== OSV quick check (POST) for $pkg @ 9.0.7 ==="
osv_url="https://api.osv.dev/v1/query"
payload='{"package":{"name":"Microsoft.Extensions.Configuration.Json"},"version":"9.0.7"}'

curl -sS -D /tmp/osv_headers.txt -o /tmp/osv_body.txt \
  -H "Content-Type: application/json" \
  --data "$payload" \
  "$osv_url" || true

echo "--- HTTP headers ---"
cat /tmp/osv_headers.txt
echo "--- Body (first 300 chars) ---"
head -c 300 /tmp/osv_body.txt; echo

Repository: teesofttech/TableauSharp

Length of output: 1766


🏁 Script executed:

#!/bin/bash
set -euo pipefail

packages=(
  "Microsoft.Extensions.Configuration.Json"
  "Microsoft.Extensions.DependencyInjection"
  "Microsoft.Extensions.Hosting"
  "Microsoft.Extensions.Logging.Console"
)
target_ver="9.0.7"

base="https://api.nuget.org/v3-flatcontainer"

echo "=== NuGet flat-container: confirm endpoint exists + list tail versions ==="
python3 - <<'PY'
import json, urllib.request

packages = [
  "Microsoft.Extensions.Configuration.Json",
  "Microsoft.Extensions.DependencyInjection",
  "Microsoft.Extensions.Hosting",
  "Microsoft.Extensions.Logging.Console",
]
target_ver = "9.0.7"
base="https://api.nuget.org/v3-flatcontainer"

def fetch(url):
    req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0"})
    with urllib.request.urlopen(req, timeout=30) as resp:
        return resp.read()

def get_index(pkg):
    # try original and lowercase; NuGet flat-container typically uses lowercase ids
    candidates = [pkg, pkg.lower()]
    last_err = None
    for name in candidates:
        url = f"{base}/{name}/index.json"
        try:
            data = fetch(url)
            return json.loads(data), url
        except Exception as e:
            last_err = e
    raise RuntimeError(f"Failed to fetch NuGet index.json for {pkg}: {last_err}")

for pkg in packages:
    idx, url = get_index(pkg)
    versions = idx.get("versions", [])
    versions_sorted = sorted(versions, key=lambda v: tuple(int(x) if x.isdigit() else 0 for x in (v.split('-')[0].split('.')+[0,0,0])[:3]))
    present = target_ver in versions
    tail = versions_sorted[-10:] if len(versions_sorted) > 10 else versions_sorted
    print(f"\n{pkg}")
    print(f"  index url used: {url}")
    print(f"  9.0.7 present: {present}")
    print(f"  latest (tail up to 10): {', '.join(tail)}")
PY

echo
echo "=== OSV: query package/version and package-only to distinguish 'no vulns' vs query issue ==="
for pkg in \
  "Microsoft.Extensions.Configuration.Json" \
  "Microsoft.Extensions.DependencyInjection" \
  "Microsoft.Extensions.Hosting" \
  "Microsoft.Extensions.Logging.Console"
do
  payload_ver="{\"package\":{\"name\":\"$pkg\"},\"version\":\"$target_ver\"}"
  payload_pkg="{\"package\":{\"name\":\"$pkg\"}}"

  echo
  echo "--- OSV $pkg @ $target_ver ---"
  curl -sS -H "Content-Type: application/json" -d "$payload_ver" https://api.osv.dev/v1/query | head -c 200; echo

  echo "--- OSV $pkg (no version) ---"
  curl -sS -H "Content-Type: application/json" -d "$payload_pkg" https://api.osv.dev/v1/query | head -c 200; echo
done

Repository: teesofttech/TableauSharp

Length of output: 501


🏁 Script executed:

#!/bin/bash
set -euo pipefail

pkgs=(
  "Microsoft.Extensions.Configuration.Json"
  "Microsoft.Extensions.DependencyInjection"
  "Microsoft.Extensions.Hosting"
  "Microsoft.Extensions.Logging.Console"
)
ver="9.0.7"
base="https://api.nuget.org/v3-flatcontainer"

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

json_parse_versions() {
  local pkg="$1"
  local body_file="$2"

  python3 - <<'PY' "${pkg}" "${body_file}"
import json, sys, re
pkg = sys.argv[1]
path = sys.argv[2]
data=json.load(open(path,'r',encoding='utf-8'))
versions=data.get('versions',[])
def semver_key(v):
    # stable first: treat prerelease as lower than stable
    # split off prerelease/build
    main=v.split('-',1)[0].split('+',1)[0]
    parts=main.split('.')
    nums=[int(parts[i]) if i < len(parts) and parts[i].isdigit() else 0 for i in range(3)]
    pre = 1 if '-' in v else 0
    return (nums[0], nums[1], nums[2], 0 if '-' in v else 1)
# Normalize key ordering: higher is later; stable should win over prerelease
def key(v):
    main=v.split('-',1)[0]
    parts=main.split('.')
    a=[int(p) if p.isdigit() else 0 for p in (parts+[0,0,0])[:3]]
    is_pr = 1 if '-' in v else 0
    return (a[0],a[1],a[2], -is_pr)  # stable has -0 > -1
# Determine stable latest
stable=[v for v in versions if '-' not in v]
stable_sorted=sorted(stable, key=key)
print(f"  versions_count: {len(versions)}")
print(f"  stable_versions_count: {len(stable)}")
print(f"  has_{ver}: {ver in versions}")
if stable_sorted:
    print(f"  latest_stable: {stable_sorted[-1]}")
    # show last few
    tail=stable_sorted[-10:]
    print("  latest_stable_tail: "+", ".join(tail))
else:
    print("  latest_stable: (none)")
PY
}

echo "=== NuGet flat-container: fetch index.json + check latest stable vs 9.0.7 ==="
for pkg in "${pkgs[@]}"; do
  echo
  echo "Package: $pkg"
  for id in "$pkg" "${pkg,,}"; do
    url="$base/$id/index.json"
    code="$(curl -sS -o /dev/null -w '%{http_code}' "$url" || true)"
    ct="$(curl -sS -o /dev/null -w '%{content_type}' "$url" || true)"
    echo "  try id='$id' => http:$code content_type:$ct"
    if [[ "$code" == "200" ]]; then
      body="$tmpdir/index_${id//[^a-zA-Z0-9]/_}.json"
      curl -sS "$url" -o "$body"
      echo "  using id='$id' for version parsing"
      json_parse_versions "$pkg" "$body"
      break
    fi
  done
done

echo
echo "=== OSV: inspect v1/query responses for `@9.0.7` (and package-only) ==="
for pkg in "${pkgs[@]}"; do
  echo
  echo "OSV package: $pkg"
  for payload in "ver" "nov"; do
    if [[ "$payload" == "ver" ]]; then
      data="{\"package\":{\"name\":\"$pkg\"},\"version\":\"$ver\"}"
      label="@${ver}"
    else
      data="{\"package\":{\"name\":\"$pkg\"}}"
      label="(no version)"
    fi
    resp="$tmpdir/osv_${label//[^a-zA-Z0-9_]/_}.json"
    curl -sS -H "Content-Type: application/json" -d "$data" https://api.osv.dev/v1/query -o "$resp" || true
    python3 - <<'PY' "$label" "$resp"
import json, sys
label=sys.argv[1]
path=sys.argv[2]
try:
    data=json.load(open(path,'r',encoding='utf-8'))
except Exception as e:
    print(f"  {label}: failed to parse JSON: {e}")
    sys.exit(0)
vulns=data.get('vulns', None)
if vulns is None:
    print(f"  {label}: response has no 'vulns' field. top_keys={list(data.keys())}")
else:
    print(f"  {label}: vulns_count={len(vulns)}")
    if vulns:
        for v in vulns[:3]:
            print("    - "+v.get("summary","").strip()[:120])
excepted_keys=list(data.keys())
PY
  done
done

Repository: teesofttech/TableauSharp

Length of output: 633


🏁 Script executed:

#!/bin/bash
set -euo pipefail

pkgs=(
  "Microsoft.Extensions.Configuration.Json"
  "Microsoft.Extensions.DependencyInjection"
  "Microsoft.Extensions.Hosting"
  "Microsoft.Extensions.Logging.Console"
)
ver="9.0.7"
base="https://api.nuget.org/v3-flatcontainer"

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

json_parse_versions() {
  local pkg="$1"
  local body_file="$2"
  local target_ver="$3"
  python3 - <<'PY' "${pkg}" "${body_file}" "${target_ver}"
import json, sys
pkg = sys.argv[1]
path = sys.argv[2]
target_ver = sys.argv[3]
data=json.load(open(path,'r',encoding='utf-8'))
versions=data.get('versions',[])

def key(v):
    main=v.split('-',1)[0].split('+',1)[0]
    parts=main.split('.')
    a=[int(p) if p.isdigit() else 0 for p in (parts+[0,0,0])[:3]]
    is_pr = 1 if '-' in v else 0
    # stable (is_pr=0) should rank higher than prerelease for same numbers
    return (a[0],a[1],a[2], 0 if is_pr else 1)

stable=[v for v in versions if '-' not in v]
stable_sorted=sorted(stable, key=key)
print(f"  versions_count: {len(versions)}")
print(f"  stable_versions_count: {len(stable)}")
print(f"  has_{target_ver}: {target_ver in versions}")
if stable_sorted:
    print(f"  latest_stable: {stable_sorted[-1]}")
    tail=stable_sorted[-10:]
    print("  latest_stable_tail: "+", ".join(tail))
else:
    print("  latest_stable: (none)")
PY
}

echo "=== NuGet flat-container: fetch index.json + latest stable vs 9.0.7 ==="
for pkg in "${pkgs[@]}"; do
  echo
  echo "Package: $pkg"
  for id in "$pkg" "${pkg,,}"; do
    url="$base/$id/index.json"
    code="$(curl -sS -o /dev/null -w '%{http_code}' "$url" || true)"
    ct="$(curl -sS -o /dev/null -w '%{content_type}' "$url" || true)"
    echo "  try id='$id' => http:$code content_type:$ct"
    if [[ "$code" == "200" ]]; then
      body="$tmpdir/index_${id//[^a-zA-Z0-9]/_}.json"
      curl -sS "$url" -o "$body"
      echo "  using id='$id' for version parsing"
      json_parse_versions "$pkg" "$body" "$ver"
      break
    fi
  done
done

echo
echo "=== OSV: inspect v1/query responses for `@9.0.7` (and package-only) ==="
for pkg in "${pkgs[@]}"; do
  echo
  echo "OSV package: $pkg"
  for payload_kind in ver nov; do
    if [[ "$payload_kind" == "ver" ]]; then
      data="{\"package\":{\"name\":\"$pkg\"},\"version\":\"$ver\"}"
      label="@${ver}"
    else
      data="{\"package\":{\"name\":\"$pkg\"}}"
      label="(no version)"
    fi
    resp="$tmpdir/osv_${label//[^a-zA-Z0-9_]/_}.json"
    curl -sS -H "Content-Type: application/json" -d "$data" https://api.osv.dev/v1/query -o "$resp" || true
    python3 - <<'PY' "$label" "$resp"
import json, sys
label=sys.argv[1]
path=sys.argv[2]
data=json.load(open(path,'r',encoding='utf-8'))
vulns=data.get('vulns', None)
if vulns is None:
    print(f"  {label}: response has no 'vulns' field. top_keys={list(data.keys())[:10]}")
    return
print(f"  {label}: vulns_count={len(vulns)}")
if vulns:
    for v in vulns[:3]:
        summary=v.get("summary","").strip()
        sev=v.get("severity","")
        first_patched=(v.get("first_patched_version",{}) or {}).get("identifier") if isinstance(v.get("first_patched_version"),dict) else None
        aliases=v.get("aliases",[])
        print("    - "+summary[:160]+ (f" | severity:{sev}" if sev else ""))
        if aliases:
            cves=[a for a in aliases if isinstance(a,str) and "CVE" in a.upper()]
            if cves:
                print("      CVEs: "+", ".join(cves[:5]))
        if first_patched:
            print("      firstPatchedVersion: "+first_patched)
PY
  done
done

Repository: teesofttech/TableauSharp

Length of output: 2299


🏁 Script executed:

#!/bin/bash
set -euo pipefail

pkgs=(
  "Microsoft.Extensions.Configuration.Json"
  "Microsoft.Extensions.DependencyInjection"
  "Microsoft.Extensions.Hosting"
  "Microsoft.Extensions.Logging.Console"
)
ver="9.0.7"

echo "=== OSV: v1/query for Microsoft.Extensions.* @ 9.0.7 (and package-only) ==="
for pkg in "${pkgs[@]}"; do
  echo
  echo "OSV package: $pkg"

  for kind in ver nov; do
    if [[ "$kind" == "ver" ]]; then
      data="{\"package\":{\"name\":\"$pkg\"},\"version\":\"$ver\"}"
      label="@${ver}"
    else
      data="{\"package\":{\"name\":\"$pkg\"}}"
      label="(no version)"
    fi

    resp="$(curl -sS -H "Content-Type: application/json" -d "$data" https://api.osv.dev/v1/query)"
    python3 - <<'PY' "$label" "$resp"
import json, sys
label=sys.argv[1]
txt=sys.argv[2]
data=json.loads(txt)
vulns=data.get("vulns", [])
print(f"  {label}: vulns_count={len(vulns)}")
for v in (vulns[:3] if vulns else []):
    summary=v.get("summary","").strip()
    sev=v.get("severity","")
    aliases=v.get("aliases",[])
    first=v.get("first_patched_version",{}) or {}
    first_id=first.get("identifier") if isinstance(first,dict) else None
    print("    - "+summary[:160]+(f" | severity:{sev}" if sev else ""))
    if first_id:
      print("      firstPatchedVersion:", first_id)
    cves=[a for a in aliases if isinstance(a,str) and ("CVE" in a.upper())]
    if cves:
      print("      CVEs:", ", ".join(cves[:5]) + ("..." if len(cves)>5 else ""))
PY
  done
done

Repository: teesofttech/TableauSharp

Length of output: 563


Update Microsoft.Extensions package pins (9.0.7 is not the latest stable)

These references are currently pinned to 9.0.7:

<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />
  • NuGet flat-container shows the latest stable is 10.0.9 (so 9.0.7 is behind).
  • OSV reports 0 known vulnerabilities affecting 9.0.7 for these four packages.
  • Also pinned to 9.0.7 elsewhere in the repo (src/TableauSharp/TableauSharp.csproj: Microsoft.Extensions.Http and Microsoft.Extensions.Options)—update them together for consistency.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/TableauSharp.Examples/TableauSharp.Examples.csproj` around lines 16 -
19, Update the Microsoft.Extensions package pins from 9.0.7 to the current
stable 10.0.9: change the Version for Microsoft.Extensions.Configuration.Json,
Microsoft.Extensions.DependencyInjection, Microsoft.Extensions.Hosting, and
Microsoft.Extensions.Logging.Console in the TableauSharp.Examples project and
also update Microsoft.Extensions.Http and Microsoft.Extensions.Options in the
TableauSharp project to the same 10.0.9 version so all Microsoft.Extensions
packages are consistent across the repo.

@teesofttech
teesofttech merged commit 0c571a0 into master Jun 12, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants