-
Notifications
You must be signed in to change notification settings - Fork 0
infra: test project setup, Examples project, NuGet metadata, CI/CD pipelines #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [master] | ||
| pull_request: | ||
| branches: [master] | ||
|
|
||
| jobs: | ||
| build-and-test: | ||
| name: Build & Test | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 In - 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 AgentsSource: Linters/SAST tools |
||
|
|
||
| - name: Setup .NET | ||
| uses: actions/setup-dotnet@v4 | ||
| with: | ||
| dotnet-version: '8.0.x' | ||
|
|
||
| - name: Restore | ||
| run: dotnet restore TableauSharp.sln | ||
|
|
||
| - name: Build | ||
| run: dotnet build TableauSharp.sln --no-restore -c Release | ||
|
|
||
| - name: Test | ||
| run: > | ||
| dotnet test TableauSharp.sln | ||
| --no-build | ||
| -c Release | ||
| --verbosity normal | ||
| --logger "trx;LogFileName=test-results.trx" | ||
| --collect:"XPlat Code Coverage" | ||
|
|
||
| - name: Upload test results | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: test-results | ||
| path: '**/TestResults/**' | ||
|
|
||
| - name: Upload coverage | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: coverage | ||
| path: '**/TestResults/**/coverage.cobertura.xml' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| name: Publish to NuGet | ||
|
|
||
| # Triggered by a version tag: git tag v0.1.0 && git push --tags | ||
| on: | ||
| push: | ||
| tags: | ||
| - 'v[0-9]+.[0-9]+.[0-9]+*' # v0.1.0, v1.0.0-beta, etc. | ||
|
|
||
| jobs: | ||
| publish: | ||
| name: Pack & Publish | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 🧰 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 AgentsSource: Linters/SAST tools |
||
|
|
||
| - name: Setup .NET | ||
| uses: actions/setup-dotnet@v4 | ||
| with: | ||
| dotnet-version: '8.0.x' | ||
|
|
||
| - 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: Restore | ||
| run: dotnet restore TableauSharp.sln | ||
|
|
||
| - name: Build | ||
| run: dotnet build TableauSharp.sln --no-restore -c Release | ||
|
|
||
| - name: Test | ||
| run: dotnet test TableauSharp.sln --no-build -c Release --verbosity normal | ||
|
|
||
| - name: Pack | ||
| run: > | ||
| dotnet pack src/TableauSharp/TableauSharp.csproj | ||
| --no-build | ||
| -c Release | ||
| -p:PackageVersion=${{ steps.version.outputs.VERSION }} | ||
| -o ./artifacts | ||
|
|
||
| - name: Publish to NuGet | ||
| run: > | ||
| dotnet nuget push ./artifacts/*.nupkg | ||
| --api-key ${{ secrets.NUGET_API_KEY }} | ||
| --source https://api.nuget.org/v3/index.json | ||
| --skip-duplicate | ||
|
|
||
| - name: Publish symbols | ||
| run: > | ||
| dotnet nuget push ./artifacts/*.snupkg | ||
| --api-key ${{ secrets.NUGET_API_KEY }} | ||
| --source https://api.nuget.org/v3/index.json | ||
| --skip-duplicate | ||
|
|
||
| - name: Upload artifacts | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: nuget-packages | ||
| path: ./artifacts/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| using Microsoft.Extensions.Logging; | ||
| using TableauSharp.Auth.Service; | ||
|
|
||
| namespace TableauSharp.Examples.Examples; | ||
|
|
||
| // Demonstrates the three authentication flows supported by TableauSharp. | ||
| // Only one sign-in method should be called per session. After sign-in the | ||
| // token is stored automatically in ITableauTokenProvider and injected into | ||
| // all other services — you do not need to pass it manually. | ||
| public class AuthExamples(IAuthService authService, ILogger<AuthExamples> logger) | ||
| { | ||
| public async Task RunAsync() | ||
| { | ||
| Console.WriteLine("--- Auth Examples ---"); | ||
| Console.WriteLine(); | ||
|
|
||
| await SignInWithPATAsync(); | ||
| await SignOutAsync(); | ||
| } | ||
|
|
||
| // Personal Access Token is the recommended flow for server-side applications. | ||
| // Configure PersonalAccessTokenName and PersonalAccessTokenSecret in appsettings.json. | ||
| private async Task 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}"); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| logger.LogError(ex, "PAT sign-in failed"); | ||
| } | ||
|
|
||
| Console.WriteLine(); | ||
| } | ||
|
|
||
| // Sign out invalidates the session on the Tableau server. | ||
| // Always sign out when done to avoid leaking server sessions. | ||
| 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(); | ||
| } | ||
|
Comment on lines
+44
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant authentication in The When 🔧 Recommended fixesOption 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 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| using Microsoft.Extensions.Logging; | ||
| using TableauSharp.Auth.Service; | ||
| using TableauSharp.DataSources.Services; | ||
|
|
||
| namespace TableauSharp.Examples.Examples; | ||
|
|
||
| // Demonstrates data source operations. | ||
| // RefreshAsync triggers an extract refresh job on Tableau Server. | ||
| // The job runs asynchronously — use Tableau's Jobs API to poll completion. | ||
| public class DataSourceExamples( | ||
| IAuthService authService, | ||
| IDataSourceService dataSourceService, | ||
| ILogger<DataSourceExamples> logger) | ||
| { | ||
| public async Task RunAsync() | ||
| { | ||
| Console.WriteLine("--- Data Source Examples ---"); | ||
| Console.WriteLine(); | ||
|
|
||
| await authService.SignInWithPATAsync(); | ||
|
|
||
| await ListDataSourcesAsync(); | ||
| await RefreshFirstDataSourceAsync(); | ||
| } | ||
|
|
||
| private async Task ListDataSourcesAsync() | ||
| { | ||
| Console.WriteLine("[1] List all data sources"); | ||
| try | ||
| { | ||
| var sources = await dataSourceService.GetAllAsync(); | ||
| foreach (var ds in sources) | ||
| { | ||
| var certified = ds.IsCertified ? " [certified]" : ""; | ||
| Console.WriteLine($" {ds.Id} {ds.Name,-35} type={ds.Type ?? "unknown"}{certified}"); | ||
| } | ||
|
|
||
| if (!sources.Any()) | ||
| Console.WriteLine(" No data sources found."); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| logger.LogError(ex, "List data sources failed"); | ||
| } | ||
|
|
||
| Console.WriteLine(); | ||
| } | ||
|
|
||
| // Triggering a refresh is fire-and-forget from the SDK's perspective. | ||
| // Tableau Server queues the job and runs it asynchronously. | ||
| private async Task RefreshFirstDataSourceAsync() | ||
| { | ||
| Console.WriteLine("[2] Trigger extract refresh on first data source"); | ||
| try | ||
| { | ||
| var sources = await dataSourceService.GetAllAsync(); | ||
| var first = sources.FirstOrDefault(); | ||
| if (first is null) { Console.WriteLine(" No data sources."); return; } | ||
|
|
||
| await dataSourceService.RefreshAsync(first.Id); | ||
| Console.WriteLine($" Refresh triggered for '{first.Name}'."); | ||
| Console.WriteLine(" Check Tableau Server > Jobs to monitor completion."); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| logger.LogError(ex, "Refresh failed"); | ||
| } | ||
|
|
||
| Console.WriteLine(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GitHub Actions security hardening needed in both workflows.
Both
.github/workflows/ci.ymland.github/workflows/publish-nuget.ymlshare the same security gaps:persist-credentials: falseon checkout actions - Repository credentials could be exposed through artifacts or subsequent stepsThese 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
Source: Linters/SAST tools