Skip to content

Extensions - Release #46

Extensions - Release

Extensions - Release #46

Workflow file for this run

name: Release
on:
push:
tags:
- 'v*' # Tag push: build + GitHub release only (no marketplace publish)
workflow_dispatch: # Manual trigger: full pipeline including marketplace publish
inputs:
create_tag:
description: 'Create tag from package.json version'
required: false
default: true
type: boolean
publish_marketplace:
description: 'Publish to VS Code Marketplace after creating the release'
required: false
default: true
type: boolean
permissions:
contents: read
jobs:
release:
name: GitHub Release
permissions:
contents: write
runs-on: ubuntu-latest
outputs:
version: ${{ steps.extract_version.outputs.tag_version }}
tag_name: ${{ steps.tag_name.outputs.tag_name }}
vsix_file: ${{ steps.vsix_filename.outputs.vsix_file }}
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: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '20.x'
cache: 'npm'
cache-dependency-path: vscode-extension/package-lock.json
- name: Determine trigger type
id: trigger_type
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "is_manual=true" >> "$GITHUB_OUTPUT"
echo "Triggered manually via workflow_dispatch"
else
echo "is_manual=false" >> "$GITHUB_OUTPUT"
echo "Triggered by tag push"
fi
- name: Extract version from package.json
id: package_version
run: |
PACKAGE_VERSION=$(node -p "require('./vscode-extension/package.json').version")
echo "package_version=$PACKAGE_VERSION" >> "$GITHUB_OUTPUT"
echo "Package version: $PACKAGE_VERSION"
- name: Extract version from tag
id: extract_version
run: |
if [ "${{ steps.trigger_type.outputs.is_manual }}" == "true" ]; then
# For manual triggers, use package.json version
TAG_VERSION="${{ steps.package_version.outputs.package_version }}"
else
# For tag triggers, extract from the tag
TAG_VERSION=${GITHUB_REF#refs/tags/v}
fi
echo "tag_version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
echo "Release version: $TAG_VERSION"
- name: Verify version consistency
run: |
if [ "${{ steps.extract_version.outputs.tag_version }}" != "${{ steps.package_version.outputs.package_version }}" ]; then
echo "❌ Version mismatch!"
echo "Tag version: ${{ steps.extract_version.outputs.tag_version }}"
echo "Package.json version: ${{ steps.package_version.outputs.package_version }}"
echo "Please ensure the tag version matches the version in package.json"
exit 1
fi
echo "✅ Version check passed: ${{ steps.extract_version.outputs.tag_version }}"
- name: Determine tag name
id: tag_name
run: |
if [ "${{ steps.trigger_type.outputs.is_manual }}" == "true" ]; then
TAG_NAME="v${{ steps.extract_version.outputs.tag_version }}"
else
TAG_NAME="${{ github.ref_name }}"
fi
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "Tag name: $TAG_NAME"
- name: Generate release notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG_NAME="${{ steps.tag_name.outputs.tag_name }}"
VERSION="${{ steps.extract_version.outputs.tag_version }}"
echo "Generating release notes for $TAG_NAME..."
# Use GitHub API to auto-generate notes from merged PRs since last release
if gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="${TAG_NAME}" \
--jq '.body' > /tmp/release_notes.md 2>/tmp/generate_notes_err.txt && [ -s /tmp/release_notes.md ]; then
echo "✅ Generated release notes from merged PRs"
else
echo "Release ${VERSION}" > /tmp/release_notes.md
echo "⚠️ Could not auto-generate notes, using fallback"
if [ -s /tmp/generate_notes_err.txt ]; then
echo " Error: $(cat /tmp/generate_notes_err.txt)"
fi
fi
echo "--- Release notes preview ---"
cat /tmp/release_notes.md
echo "---"
- name: Update CHANGELOG.md for VSIX packaging
run: |
VERSION="${{ steps.extract_version.outputs.tag_version }}"
node -e "
const fs = require('fs');
const version = process.argv[1];
const notes = fs.readFileSync('/tmp/release_notes.md', 'utf8').trim();
let changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
const marker = '## [Unreleased]';
const idx = changelog.indexOf(marker);
if (idx >= 0) {
const insertAt = changelog.indexOf('\n', idx) + 1;
const section = '\n## [' + version + ']\n\n' + notes + '\n';
changelog = changelog.slice(0, insertAt) + section + changelog.slice(insertAt);
}
fs.writeFileSync('CHANGELOG.md', changelog);
console.log('✅ Updated CHANGELOG.md with v' + version + ' notes for VSIX packaging');
" "$VERSION"
- name: Create changelog branch and commit
if: steps.trigger_type.outputs.is_manual == 'true'
id: changelog_branch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.extract_version.outputs.tag_version }}"
BRANCH="changelog/v${VERSION}"
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
# Check if branch already exists on remote
if git ls-remote --exit-code --heads origin "refs/heads/$BRANCH" >/dev/null 2>&1; then
MSG="❌ Branch $BRANCH already exists on remote! Delete it or bump the version before re-running."
echo "$MSG"
echo "$MSG" > /tmp/changelog_error.txt
exit 1
fi
if git diff --quiet CHANGELOG.md; then
echo "ℹ️ No changes to CHANGELOG.md, skipping branch + commit"
exit 0
fi
git checkout -b "$BRANCH"
git add CHANGELOG.md
git commit -m "docs: update CHANGELOG.md for v${VERSION}"
git push origin "$BRANCH"
echo "✅ Pushed CHANGELOG.md update to $BRANCH"
- name: Create tag for manual trigger
if: steps.trigger_type.outputs.is_manual == 'true' && inputs.create_tag == true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="v${{ steps.package_version.outputs.package_version }}"
echo "Creating tag: $VERSION"
# Check if tag already exists on remote using exit code
if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo "❌ Tag $VERSION already exists on remote!"
echo "Please update the version in package.json or delete the existing tag."
exit 1
fi
# Create and push the tag
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$VERSION" -m "Release $VERSION"
git push origin "$VERSION"
# Verify tag was created successfully on remote
echo "Verifying tag was created on remote..."
for i in {1..5}; do
if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo "✅ Tag $VERSION created and verified on remote"
exit 0
fi
echo "Waiting for tag to propagate (attempt $i/5)..."
sleep 2
done
echo "❌ Failed to verify tag creation on remote"
exit 1
- name: Install dependencies
working-directory: vscode-extension
run: npm ci
- name: Run linting
working-directory: vscode-extension
run: npm run lint
- name: Run type checking
working-directory: vscode-extension
run: npm run check-types
- name: Compile extension
working-directory: vscode-extension
run: npm run compile
- name: Build production package
working-directory: vscode-extension
run: npm run package
- name: Compile tests
working-directory: vscode-extension
run: npm run compile-tests
- name: Run tests
uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 # v1.0.1
with:
run: npm test
options: -screen 0 1024x768x24
working-directory: vscode-extension
continue-on-error: false # Fail the release if tests fail
- name: Create VSIX package
working-directory: vscode-extension
run: npx vsce package
- name: Get VSIX filename
id: vsix_filename
working-directory: vscode-extension
run: |
VSIX_FILE=$(find . -maxdepth 1 -name "*.vsix" -exec basename {} \; | head -n 1)
echo "vsix_file=$VSIX_FILE" >> "$GITHUB_OUTPUT"
echo "VSIX file: $VSIX_FILE"
- name: Upload VSIX as workflow artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: vsix-package
path: vscode-extension/${{ steps.vsix_filename.outputs.vsix_file }}
retention-days: 90
- name: Create Release
id: create_release
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
run: |
set -o pipefail
echo "Creating release for tag: ${{ steps.tag_name.outputs.tag_name }}"
# Create release with auto-generated notes and upload VSIX file
gh release create "${{ steps.tag_name.outputs.tag_name }}" \
--title "Release ${{ steps.extract_version.outputs.tag_version }}" \
--notes-file /tmp/release_notes.md \
vscode-extension/${{ steps.vsix_filename.outputs.vsix_file }} 2>&1 | tee /tmp/release_output.txt
- name: Release Summary
if: always()
shell: bash
run: |
if [ "${{ steps.create_release.outcome }}" == "success" ]; then
{
echo "# ✅ Release Created Successfully"
echo ""
echo "🎉 Release ${{ steps.extract_version.outputs.tag_version }} created successfully!"
echo "📦 VSIX package: ${{ steps.vsix_filename.outputs.vsix_file }}"
echo "🔗 [Release URL](https://github.com/${{ github.repository }}/releases/tag/${{ steps.tag_name.outputs.tag_name }})"
} >> "$GITHUB_STEP_SUMMARY"
else
{
echo "# ❌ Release Failed"
echo ""
echo "**Version:** ${{ steps.extract_version.outputs.tag_version }}"
echo "**Tag:** ${{ steps.tag_name.outputs.tag_name }}"
echo ""
echo "## Error Details"
echo ""
echo "\`\`\`"
if [ -f /tmp/changelog_error.txt ]; then
cat /tmp/changelog_error.txt
elif [ -f /tmp/release_output.txt ]; then
cat /tmp/release_output.txt
else
echo "No error details captured — check the job logs above for the failed step."
fi
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
publish:
needs: release
name: Publish VS Code Extension
if: github.event_name == 'workflow_dispatch' && inputs.publish_marketplace
runs-on: ubuntu-latest
permissions:
contents: read
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: Check marketplace version
id: version_check
run: |
LOCAL_VERSION="${{ needs.release.outputs.version }}"
echo "Local version: $LOCAL_VERSION"
MARKETPLACE_VERSION=$(curl -s -X POST \
"https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery?api-version=3.0-preview.1" \
-H "Content-Type: application/json" \
-H "Accept: application/json;api-version=3.0-preview.1" \
-d '{"filters":[{"criteria":[{"filterType":7,"value":"RobBos.copilot-token-tracker"}]}],"flags":512}' \
| jq -r '.results[0].extensions[0].versions[0].version // empty')
if [ -z "$MARKETPLACE_VERSION" ]; then
echo "⚠️ Could not retrieve marketplace version — proceeding with publish."
exit 0
fi
echo "Marketplace version: $MARKETPLACE_VERSION"
if [ "$LOCAL_VERSION" == "$MARKETPLACE_VERSION" ]; then
echo "❌ Version $LOCAL_VERSION is already published on the VS Code Marketplace."
echo " Bump the version in vscode-extension/package.json before publishing."
exit 1
fi
echo "✅ Version check passed: $LOCAL_VERSION is newer than marketplace $MARKETPLACE_VERSION"
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '20.x'
cache: 'npm'
cache-dependency-path: vscode-extension/package-lock.json
- name: Install dependencies
working-directory: vscode-extension
run: npm ci
- name: Download VSIX from release
id: download_vsix
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG_NAME="${{ needs.release.outputs.tag_name }}"
echo "Downloading VSIX from release $TAG_NAME..."
gh release download "$TAG_NAME" \
--repo "${{ github.repository }}" \
--pattern "*.vsix" \
--dir .
VSIX_FILE=$(find . -maxdepth 1 -name "*.vsix" -exec basename {} \; | head -n 1)
echo "Downloaded: $VSIX_FILE"
echo "vsix_file=$VSIX_FILE" >> "$GITHUB_OUTPUT"
- name: Publish to VS Code Marketplace
id: publish
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
run: |
if [ -z "$VSCE_PAT" ]; then
echo "❌ VSCE_PAT secret is not configured."
echo " Add it at: Settings → Secrets and variables → Actions"
echo " Create a PAT at https://dev.azure.com with 'Marketplace (Publish)' scope"
exit 1
fi
echo "Publishing ${{ steps.download_vsix.outputs.vsix_file }} to VS Code Marketplace..."
npx vsce publish \
--packagePath "${{ steps.download_vsix.outputs.vsix_file }}" \
--pat "$VSCE_PAT"
- name: Publish Summary
if: always()
run: |
{
echo "# 🚀 VS Code Marketplace"
echo ""
if [ "${{ steps.publish.outcome }}" == "success" ]; then
echo "✅ Extension v${{ needs.release.outputs.version }} published to the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=RobBos.copilot-token-tracker)"
elif [ "${{ steps.version_check.outcome }}" == "failure" ]; then
echo "⏭️ Publish skipped — v${{ needs.release.outputs.version }} is already live on the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=RobBos.copilot-token-tracker)."
echo ""
echo "Bump the version in \`vscode-extension/package.json\` before publishing again."
else
echo "❌ Failed to publish v${{ needs.release.outputs.version }} to marketplace."
echo ""
echo "Ensure the \`VSCE_PAT\` secret is configured with a valid Azure DevOps PAT"
echo "with the \`Marketplace (Publish)\` scope for all accessible organizations."
fi
} >> "$GITHUB_STEP_SUMMARY"
build-visualstudio:
name: Publish Visual Studio Extension
needs: release
# Windows required: Node.js SEA (bundle-exe.ps1) and MSBuild/VSSDK are Windows-only
runs-on: windows-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: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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 CLI (production bundle + Windows .exe) ───────────────────────
- name: Build CLI (production bundle)
working-directory: cli
run: npm run build:production
- 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: 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 ──────────────────────────────────────────────
- 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"
# ── Build Visual Studio extension (MSBuild / VSSDK) ───────────────────
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0
- name: Restore NuGet packages
working-directory: visualstudio-extension
run: nuget restore CopilotTokenTracker.sln
- name: Build solution (Release)
working-directory: visualstudio-extension
run: msbuild CopilotTokenTracker.sln /p:Configuration=Release /t:Build /v:minimal
# ── 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
# ── Upload .vsix to GitHub release ────────────────────────────────────
- name: Upload .vsix to GitHub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: pwsh
run: |
$tag = "${{ needs.release.outputs.tag_name }}"
$vsixPath = "${{ steps.vsix.outputs.vsix_path }}"
Write-Host "Uploading $vsixPath to release $tag ..."
gh release upload $tag $vsixPath --clobber
if ($LASTEXITCODE -ne 0) { throw "Failed to upload .vsix to release" }
Write-Host "✅ Uploaded to GitHub release"
# ── (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.VSCE_PAT }} # same one for the VSCode Marketplace works for Visual Studio Marketplace as well
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`` |
| Release | ``${{ needs.release.outputs.tag_name }}`` |
| Commit | ``${{ github.sha }}`` |
"@ >> $env:GITHUB_STEP_SUMMARY
} else {
"## ❌ Build failed — no .vsix produced" >> $env:GITHUB_STEP_SUMMARY
}