infra: test project setup, Examples project, NuGet metadata, CI/CD pipelines#19
Conversation
…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>
📝 WalkthroughWalkthroughThis 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). ChangesSDK Examples and CI/CD Integration
🎯 3 (Moderate) | ⏱️ ~20 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (4)
.github/workflows/publish-nuget.yml (2)
47-59: ⚖️ Poor tradeoffConsider migrating to NuGet trusted publishing.
NuGet.org supports OIDC-based trusted publishing, which eliminates the need to manage
NUGET_API_KEYsecrets 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 winReview: Version extraction and template expansion.
The workflow extracts the package version from the tag name and passes it to
dotnet packvia template expansion. While static analysis flags potential template injection at Line 44, the risk is mitigated by:
- The tag pattern
v[0-9]+.[0-9]+.[0-9]+*restricts inputs to semantic version format- MSBuild property assignment (
-p:PackageVersion=...) is not shell-injectableGITHUB_REF_NAMEis a GitHub-provided variable, not arbitrary user inputHowever, 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: RestoreAlso 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 valueConsider sanitizing the server-provided filename for defense in depth.
The code writes to
Path.Combine(Path.GetTempPath(), export.FileName)whereexport.FileNamecomes from the server response. WhilePath.Combineprovides 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 betweenTableauSharp.SampleandTableauSharp.Examples
TableauSharp.Samplecontains web-style app code (e.g.,Controllers/HomeController.cs,Models/ErrorViewModel.cs), whilesamples/TableauSharp.Examplesis organized as multipleExamples/*Examples.csmodules with its ownProgram.cs.- No
README*.md/*.mdin 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
📒 Files selected for processing (15)
.github/workflows/ci.yml.github/workflows/publish-nuget.ymlTableauSharp.slnsamples/TableauSharp.Examples/Examples/AuthExamples.cssamples/TableauSharp.Examples/Examples/DataSourceExamples.cssamples/TableauSharp.Examples/Examples/EmbeddingExamples.cssamples/TableauSharp.Examples/Examples/PermissionExamples.cssamples/TableauSharp.Examples/Examples/ProjectExamples.cssamples/TableauSharp.Examples/Examples/UserGroupExamples.cssamples/TableauSharp.Examples/Examples/WorkbookExamples.cssamples/TableauSharp.Examples/Program.cssamples/TableauSharp.Examples/TableauSharp.Examples.csprojsamples/TableauSharp.Examples/appsettings.jsonsrc/TableauSharp/TableauSharp.csprojtest/TableauSharp.Tests/TableauSharp.Tests.csproj
| - name: Checkout | ||
| uses: actions/checkout@v4 |
There was a problem hiding this comment.
GitHub Actions security hardening needed in both workflows.
Both .github/workflows/ci.yml and .github/workflows/publish-nuget.yml share the same security gaps:
- Missing
persist-credentials: falseon checkout actions - Repository credentials could be exposed through artifacts or subsequent steps - 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
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 |
There was a problem hiding this comment.
🧩 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 -nRepository: 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 |
There was a problem hiding this comment.
🧩 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
fiRepository: 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
| 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(); | ||
| } |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
🧩 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=csRepository: 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=csRepository: 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.csRepository: 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.
| // 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(); | ||
| } |
There was a problem hiding this comment.
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.
| // 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.
| 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(); | ||
| } |
There was a problem hiding this comment.
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.
| 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> |
There was a problem hiding this comment.
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.
| <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" /> |
There was a problem hiding this comment.
🧩 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}")
PYRepository: 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; echoRepository: 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
doneRepository: 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
doneRepository: 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
doneRepository: 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
doneRepository: 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(so9.0.7is behind). - OSV reports
0known vulnerabilities affecting9.0.7for these four packages. - Also pinned to
9.0.7elsewhere in the repo (src/TableauSharp/TableauSharp.csproj:Microsoft.Extensions.HttpandMicrosoft.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.
Summary
This PR establishes the full development infrastructure before any bug fixes begin. Every subsequent fix PR will build on top of this.
MoqandRichardSzalay.MockHttptoTableauSharp.Tests, plus a project reference to the SDK so tests can actually import SDK typessamples/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 withdotnet run -- <area>PackageId,Version,Authors,Description, tags, repo URL, README bundle, and.snupkgsymbol package config toTableauSharp.csproj.github/workflows/ci.ymlbuilds and tests on every push/PR tomaster, uploads test results and coverage artifacts.github/workflows/publish-nuget.ymlpacks and publishes to NuGet onv*tags (e.g.git tag v0.1.0 && git push --tags); requiresNUGET_API_KEYsecret in repo settingsPre-release checklist
NUGET_API_KEYto repository secrets (Settings → Secrets → Actions) before the first release tagappsettings.jsonin the Examples project with your server details before running examplesTest plan
dotnet restore— cleandotnet build -c Release— 0 errorsdotnet test -c Release— 1 passed (stub; real tests added per bug-fix PR)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Chores