Skip to content

fix: track token usage from sub-agent calls in Copilot agent mode (#5… #140

fix: track token usage from sub-agent calls in Copilot agent mode (#5…

fix: track token usage from sub-agent calls in Copilot agent mode (#5… #140

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@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
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
node dist/cli.js all --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@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
- name: Download VSIX artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 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"