Skip to content

Extensions - Release #34

Extensions - Release

Extensions - Release #34

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:
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@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
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'
- 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('./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
echo "❌ Branch $BRANCH already exists on remote!"
echo "Delete it or bump the version before re-running."
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
run: npm ci
- name: Run linting
run: npm run lint
- name: Run type checking
run: npm run check-types
- name: Compile extension
run: npm run compile
- name: Build production package
run: npm run package
- name: Compile tests
run: npm run compile-tests
- name: Run tests
uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 # v1.0.1
with:
run: npm test
options: -screen 0 1024x768x24
continue-on-error: false # Fail the release if tests fail
- name: Create VSIX package
run: npx vsce package
- name: Get VSIX filename
id: vsix_filename
run: |
VSIX_FILE=$(ls *.vsix | 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: ./${{ 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 \
./${{ steps.vsix_filename.outputs.vsix_file }} 2>&1 | tee /tmp/release_output.txt
- name: Release Summary
if: always()
run: |
if [ "${{ steps.create_release.outcome }}" == "success" ]; then
echo "# ✅ Release Created Successfully" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "🎉 Release ${{ steps.extract_version.outputs.tag_version }} created successfully!" >> $GITHUB_STEP_SUMMARY
echo "📦 VSIX package: ${{ steps.vsix_filename.outputs.vsix_file }}" >> $GITHUB_STEP_SUMMARY
echo "🔗 [Release URL](https://github.com/${{ github.repository }}/releases/tag/${{ steps.tag_name.outputs.tag_name }})" >> $GITHUB_STEP_SUMMARY
else
echo "# ❌ Release Creation Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ steps.extract_version.outputs.tag_version }}" >> $GITHUB_STEP_SUMMARY
echo "**Tag:** ${{ steps.tag_name.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Error Details" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
if [ -f /tmp/release_output.txt ]; then
cat /tmp/release_output.txt >> $GITHUB_STEP_SUMMARY
else
echo "No error details captured" >> $GITHUB_STEP_SUMMARY
fi
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
publish:
needs: release
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@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
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'
- name: Install dependencies
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=$(ls *.vsix | 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" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
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)" >> $GITHUB_STEP_SUMMARY
else
echo "❌ Failed to publish v${{ needs.release.outputs.version }} to marketplace." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Ensure the \`VSCE_PAT\` secret is configured with a valid Azure DevOps PAT" >> $GITHUB_STEP_SUMMARY
echo "with the \`Marketplace (Publish)\` scope for all accessible organizations." >> $GITHUB_STEP_SUMMARY
fi