Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Visual Studio Extension - Build & Package | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| tags: | |
| - 'vs/v*' # Tag push: build + GitHub release (and optionally publish to marketplace) | |
| paths: | |
| - 'visualstudio-extension/**' | |
| - 'cli/**' | |
| - 'vscode-extension/src/sessionDiscovery.ts' | |
| - 'vscode-extension/src/sessionParser.ts' | |
| - 'vscode-extension/src/tokenEstimation.ts' | |
| - 'vscode-extension/src/maturityScoring.ts' | |
| - 'vscode-extension/src/usageAnalysis.ts' | |
| - 'vscode-extension/src/opencode.ts' | |
| - 'vscode-extension/src/visualstudio.ts' | |
| - 'vscode-extension/src/types.ts' | |
| - 'vscode-extension/src/tokenEstimators.json' | |
| - 'vscode-extension/src/modelPricing.json' | |
| - 'vscode-extension/src/toolNames.json' | |
| - '.github/workflows/visualstudio-build.yml' | |
| pull_request: | |
| branches: [ main, develop ] | |
| paths: | |
| - 'visualstudio-extension/**' | |
| - 'cli/**' | |
| - 'vscode-extension/src/sessionDiscovery.ts' | |
| - 'vscode-extension/src/sessionParser.ts' | |
| - 'vscode-extension/src/tokenEstimation.ts' | |
| - 'vscode-extension/src/maturityScoring.ts' | |
| - 'vscode-extension/src/usageAnalysis.ts' | |
| - 'vscode-extension/src/opencode.ts' | |
| - 'vscode-extension/src/visualstudio.ts' | |
| - 'vscode-extension/src/types.ts' | |
| - 'vscode-extension/src/tokenEstimators.json' | |
| - 'vscode-extension/src/modelPricing.json' | |
| - 'vscode-extension/src/toolNames.json' | |
| - '.github/workflows/visualstudio-build.yml' | |
| workflow_dispatch: | |
| inputs: | |
| publish_marketplace: | |
| description: 'Publish to VS Marketplace after packaging' | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: read | |
| jobs: | |
| build: | |
| name: Build & Package | |
| # Windows required: Node.js SEA (bundle-exe.ps1) and MSBuild/VSSDK are Windows-only | |
| runs-on: windows-latest | |
| permissions: | |
| contents: write # needed to create GitHub releases on tag triggers | |
| outputs: | |
| vsix_path: ${{ steps.vsix.outputs.vsix_path }} | |
| vsix_name: ${{ steps.vsix.outputs.vsix_name }} | |
| release_version: ${{ steps.release_version.outputs.version }} | |
| is_tag: ${{ steps.release_version.outputs.is_tag }} | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Extract release version | |
| id: release_version | |
| shell: pwsh | |
| run: | | |
| if ($env:GITHUB_REF -like 'refs/tags/vs/v*') { | |
| $version = $env:GITHUB_REF -replace 'refs/tags/vs/v', '' | |
| Write-Host "Tag trigger detected: vs/v$version" | |
| echo "is_tag=true" >> $env:GITHUB_OUTPUT | |
| echo "version=$version" >> $env:GITHUB_OUTPUT | |
| } else { | |
| # Read version from vsixmanifest for non-tag builds | |
| [xml]$manifest = Get-Content 'visualstudio-extension/src/CopilotTokenTracker/source.extension.vsixmanifest' | |
| $version = $manifest.PackageManifest.Metadata.Identity.Version | |
| Write-Host "Non-tag build, manifest version: $version" | |
| echo "is_tag=false" >> $env:GITHUB_OUTPUT | |
| echo "version=$version" >> $env:GITHUB_OUTPUT | |
| } | |
| - name: Update vsixmanifest version (tag trigger only) | |
| if: steps.release_version.outputs.is_tag == 'true' | |
| shell: pwsh | |
| run: | | |
| $version = "${{ steps.release_version.outputs.version }}" | |
| $manifestPath = 'visualstudio-extension/src/CopilotTokenTracker/source.extension.vsixmanifest' | |
| [xml]$manifest = Get-Content $manifestPath | |
| $oldVersion = $manifest.PackageManifest.Metadata.Identity.Version | |
| $manifest.PackageManifest.Metadata.Identity.Version = $version | |
| $manifest.Save((Resolve-Path $manifestPath)) | |
| Write-Host "✅ Updated vsixmanifest version: $oldVersion → $version" | |
| - name: Setup Node.js | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: '22.x' | |
| # ── Install dependencies ──────────────────────────────────────────────── | |
| - name: Install vscode-extension dependencies | |
| run: npm ci | |
| working-directory: vscode-extension | |
| - name: Install CLI dependencies | |
| run: npm ci | |
| working-directory: cli | |
| # ── Build & validate CLI (js bundle) ─────────────────────────────────── | |
| - name: Build CLI (production bundle) | |
| working-directory: cli | |
| run: npm run build:production | |
| - name: Validate CLI --help | |
| working-directory: cli | |
| run: node dist/cli.js --help | |
| - name: Validate CLI new commands | |
| working-directory: cli | |
| run: | | |
| node dist/cli.js chart --json | |
| node dist/cli.js usage-analysis --json | |
| # ── Bundle CLI as Windows .exe (Node.js SEA) ─────────────────────────── | |
| - name: Bundle CLI as single executable | |
| working-directory: cli | |
| shell: pwsh | |
| run: | | |
| & pwsh -NoProfile -File bundle-exe.ps1 -SkipBuild | |
| if ($LASTEXITCODE -ne 0) { throw "bundle-exe.ps1 failed" } | |
| - name: Verify CLI exe was produced | |
| working-directory: cli | |
| shell: pwsh | |
| run: | | |
| $exe = "dist\copilot-token-tracker.exe" | |
| if (-not (Test-Path $exe)) { throw "CLI exe not found at $exe" } | |
| $sizeMB = [math]::Round((Get-Item $exe).Length / 1MB, 1) | |
| Write-Host "✅ CLI exe: $exe ($sizeMB MB)" | |
| # ── Copy CLI bundle into VS extension project ────────────────────────── | |
| - name: Copy CLI exe + wasm to cli-bundle/ | |
| shell: pwsh | |
| run: | | |
| $vsCliDir = "visualstudio-extension\src\CopilotTokenTracker\cli-bundle" | |
| New-Item -ItemType Directory -Path $vsCliDir -Force | Out-Null | |
| Copy-Item "cli\dist\copilot-token-tracker.exe" "$vsCliDir\copilot-token-tracker.exe" -Force | |
| Copy-Item "cli\dist\sql-wasm.wasm" "$vsCliDir\sql-wasm.wasm" -Force | |
| Write-Host "✅ Copied cli-bundle assets" | |
| # ── Build webview bundles (chart.js, usage.js, etc.) ────────────────── | |
| - name: Build VS Code extension webview bundles | |
| working-directory: vscode-extension | |
| run: npm run package | |
| - name: Copy webview bundles to VS extension project | |
| shell: pwsh | |
| run: | | |
| $src = "vscode-extension\dist\webview" | |
| $dst = "visualstudio-extension\src\CopilotTokenTracker\webview" | |
| if (Test-Path $dst) { Remove-Item $dst -Recurse -Force } | |
| Copy-Item $src $dst -Recurse -Force | |
| Write-Host "✅ Copied webview bundles:" | |
| Get-ChildItem $dst -Filter "*.js" | ForEach-Object { Write-Host " $($_.Name)" } | |
| # ── Build Visual Studio extension (MSBuild / VSSDK) ─────────────────── | |
| - name: Add MSBuild to PATH | |
| uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3 | |
| - name: Restore NuGet packages | |
| working-directory: visualstudio-extension | |
| run: nuget restore CopilotTokenTracker.sln | |
| - name: Restore test project (SDK-style) | |
| working-directory: visualstudio-extension | |
| run: dotnet restore src/CopilotTokenTracker.Tests/CopilotTokenTracker.Tests.csproj | |
| - name: Build solution (Release) | |
| working-directory: visualstudio-extension | |
| run: msbuild CopilotTokenTracker.sln /p:Configuration=Release /t:Build /v:minimal | |
| # ── Run unit tests with coverage ────────────────────────────────────── | |
| - name: Run unit tests with coverage | |
| working-directory: visualstudio-extension | |
| run: >- | |
| dotnet test src/CopilotTokenTracker.Tests/CopilotTokenTracker.Tests.csproj | |
| --no-build | |
| --configuration Release | |
| --logger "trx;LogFileName=test-results.trx" | |
| --collect:"XPlat Code Coverage" | |
| --results-directory TestResults | |
| - name: Generate coverage summary | |
| if: always() | |
| shell: pwsh | |
| working-directory: visualstudio-extension | |
| run: | | |
| $coverageFile = Get-ChildItem -Path TestResults -Filter "coverage.cobertura.xml" -Recurse | | |
| Select-Object -First 1 | |
| if (-not $coverageFile) { | |
| Write-Host "⚠️ No coverage file found" | |
| "## ⚠️ Unit Test Coverage`nNo coverage data collected." >> $env:GITHUB_STEP_SUMMARY | |
| exit 0 | |
| } | |
| Write-Host "Coverage file: $($coverageFile.FullName)" | |
| [xml]$xml = Get-Content $coverageFile.FullName | |
| $lineRate = [double]$xml.coverage.'line-rate' * 100 | |
| $branchRate = [double]$xml.coverage.'branch-rate' * 100 | |
| $linePct = [math]::Round($lineRate, 1) | |
| $branchPct = [math]::Round($branchRate, 1) | |
| # Build per-package (namespace) breakdown | |
| $packageRows = "" | |
| foreach ($pkg in $xml.coverage.packages.package) { | |
| $pkgName = $pkg.name | |
| $pkgLine = [math]::Round([double]$pkg.'line-rate' * 100, 1) | |
| $pkgBranch = [math]::Round([double]$pkg.'branch-rate' * 100, 1) | |
| $packageRows += "| ``$pkgName`` | $pkgLine% | $pkgBranch% |`n" | |
| } | |
| # Build per-class breakdown for classes with notable coverage data | |
| $classRows = "" | |
| foreach ($pkg in $xml.coverage.packages.package) { | |
| foreach ($cls in $pkg.classes.class) { | |
| $clsName = $cls.name | |
| $clsLine = [math]::Round([double]$cls.'line-rate' * 100, 1) | |
| $clsBranch = [math]::Round([double]$cls.'branch-rate' * 100, 1) | |
| $classRows += "| ``$clsName`` | $clsLine% | $clsBranch% |`n" | |
| } | |
| } | |
| # Write step summary | |
| $summary = @" | |
| ## 🧪 Visual Studio Extension — Unit Test Coverage | |
| | Metric | Coverage | | |
| |--------|----------| | |
| | **Line Coverage** | $linePct% | | |
| | **Branch Coverage** | $branchPct% | | |
| ### By Namespace | |
| | Namespace | Line | Branch | | |
| |-----------|------|--------| | |
| $packageRows | |
| <details><summary>By Class</summary> | |
| | Class | Line | Branch | | |
| |-------|------|--------| | |
| $classRows | |
| </details> | |
| "@ | |
| $summary >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "✅ Coverage: $linePct% line, $branchPct% branch" | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: vs-test-results-${{ github.sha }} | |
| path: visualstudio-extension/TestResults/ | |
| retention-days: 30 | |
| # ── Collect the produced .vsix ───────────────────────────────────────── | |
| - name: Find .vsix artifact | |
| id: vsix | |
| shell: pwsh | |
| run: | | |
| $vsix = Get-ChildItem -Path "visualstudio-extension" -Filter "*.vsix" -Recurse | | |
| Sort-Object LastWriteTime -Descending | | |
| Select-Object -First 1 | |
| if (-not $vsix) { throw "No .vsix file produced" } | |
| $sizeMB = [math]::Round($vsix.Length / 1MB, 1) | |
| Write-Host "✅ VSIX: $($vsix.FullName) ($sizeMB MB)" | |
| echo "vsix_path=$($vsix.FullName)" >> $env:GITHUB_OUTPUT | |
| echo "vsix_name=$($vsix.Name)" >> $env:GITHUB_OUTPUT | |
| - name: Upload .vsix as artifact | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: copilot-token-tracker-vs-${{ github.sha }} | |
| path: ${{ steps.vsix.outputs.vsix_path }} | |
| retention-days: 30 | |
| # ── (Optional) Publish to VS Marketplace ────────────────────────────── | |
| - name: Publish to Visual Studio Marketplace | |
| if: github.event_name == 'workflow_dispatch' && inputs.publish_marketplace == true | |
| shell: pwsh | |
| env: | |
| VS_MARKETPLACE_PAT: ${{ secrets.VS_MARKETPLACE_PAT }} | |
| run: | | |
| if (-not $env:VS_MARKETPLACE_PAT) { | |
| Write-Error "❌ VS_MARKETPLACE_PAT secret is not configured." | |
| exit 1 | |
| } | |
| # VsixPublisher.exe is the correct tool for Visual Studio IDE extensions | |
| # (not @vscode/vsce, which is for VS Code extensions only) | |
| $vsixPublisher = Get-ChildItem "C:\Program Files\Microsoft Visual Studio" ` | |
| -Recurse -Filter "VsixPublisher.exe" -ErrorAction SilentlyContinue | | |
| Select-Object -First 1 | |
| if (-not $vsixPublisher) { | |
| Write-Error "❌ VsixPublisher.exe not found on this runner." | |
| exit 1 | |
| } | |
| Write-Host "Found: $($vsixPublisher.FullName)" | |
| $vsix = "${{ steps.vsix.outputs.vsix_path }}" | |
| $manifest = "visualstudio-extension/publish-manifest.json" | |
| Write-Host "Publishing $vsix to Visual Studio Marketplace..." | |
| & $vsixPublisher.FullName publish -payload $vsix -publishManifest $manifest -personalAccessToken $env:VS_MARKETPLACE_PAT | |
| if ($LASTEXITCODE -ne 0) { throw "Marketplace publish failed" } | |
| Write-Host "✅ Published to Visual Studio Marketplace" | |
| # ── Summary ──────────────────────────────────────────────────────────── | |
| - name: Build summary | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $vsixName = "${{ steps.vsix.outputs.vsix_name }}" | |
| if ($vsixName) { | |
| @" | |
| ## ✅ Visual Studio Extension Built Successfully | |
| | Item | Value | | |
| |------|-------| | |
| | VSIX | ``$vsixName`` | | |
| | Version | ``${{ steps.release_version.outputs.version }}`` | | |
| | Trigger | ``${{ github.event_name }}`` | | |
| | Commit | ``${{ github.sha }}`` | | |
| "@ >> $env:GITHUB_STEP_SUMMARY | |
| } else { | |
| "## ❌ Build failed — no .vsix produced" >> $env:GITHUB_STEP_SUMMARY | |
| } | |
| github-release: | |
| name: Create GitHub Release | |
| needs: build | |
| if: startsWith(github.ref, 'refs/tags/vs/v') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 | |
| with: | |
| egress-policy: audit | |
| - name: Download VSIX artifact | |
| uses: actions/download-artifact@cbed621e49e4c01b044d60f6c80ea4ed6328b281 # v4.2.0 | |
| with: | |
| name: copilot-token-tracker-vs-${{ github.sha }} | |
| path: dist | |
| - name: Generate release notes | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG_NAME="vs/v${{ needs.build.outputs.release_version }}" | |
| echo "Generating release notes for $TAG_NAME..." | |
| if gh api repos/${{ github.repository }}/releases/generate-notes \ | |
| -f tag_name="${TAG_NAME}" \ | |
| --jq '.body' > /tmp/release_notes.md 2>/dev/null && [ -s /tmp/release_notes.md ]; then | |
| echo "✅ Generated release notes from merged PRs" | |
| else | |
| echo "Visual Studio Extension v${{ needs.build.outputs.release_version }}" > /tmp/release_notes.md | |
| fi | |
| - name: Create GitHub Release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG_NAME="vs/v${{ needs.build.outputs.release_version }}" | |
| VSIX_FILE=$(find dist -name '*.vsix' | head -n 1) | |
| echo "Creating release $TAG_NAME with $VSIX_FILE" | |
| gh release create "$TAG_NAME" \ | |
| --title "Visual Studio Extension v${{ needs.build.outputs.release_version }}" \ | |
| --notes-file /tmp/release_notes.md \ | |
| "$VSIX_FILE" | |
| echo "✅ GitHub release created: $TAG_NAME" |