@@ -3,13 +3,18 @@ name: Release
33on :
44 push :
55 tags :
6- - ' v*' # Triggers on version tags like v1.0.0, v1.2.3, etc.
7- workflow_dispatch : # Allows manual trigger from GitHub UI
6+ - ' v*' # Tag push: build + GitHub release only (no marketplace publish)
7+ workflow_dispatch : # Manual trigger: full pipeline including marketplace publish
88 inputs :
99 create_tag :
1010 description : ' Create tag from package.json version'
1111 required : false
12- default : ' true'
12+ default : true
13+ type : boolean
14+ publish_marketplace :
15+ description : ' Publish to VS Code Marketplace after creating the release'
16+ required : false
17+ default : true
1318 type : boolean
1419
1520permissions :
2025 permissions :
2126 contents : write
2227 runs-on : ubuntu-latest
28+ outputs :
29+ version : ${{ steps.extract_version.outputs.tag_version }}
30+ tag_name : ${{ steps.tag_name.outputs.tag_name }}
31+ vsix_file : ${{ steps.vsix_filename.outputs.vsix_file }}
2332
2433 steps :
2534 - name : Harden the runner (Audit all outbound calls)
5463 echo "package_version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
5564 echo "Package version: $PACKAGE_VERSION"
5665
57- - name : Create tag for manual trigger
58- if : steps.trigger_type.outputs.is_manual == 'true' && inputs.create_tag
59- env :
60- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
61- run : |
62- VERSION="v${{ steps.package_version.outputs.package_version }}"
63- echo "Creating tag: $VERSION"
64-
65- # Check if tag already exists on remote using exit code
66- if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
67- echo "❌ Tag $VERSION already exists on remote!"
68- echo "Please update the version in package.json or delete the existing tag."
69- exit 1
70- fi
71-
72- # Create and push the tag
73- git config --local user.name "github-actions[bot]"
74- git config --local user.email "github-actions[bot]@users.noreply.github.com"
75- git tag -a "$VERSION" -m "Release $VERSION"
76- git push origin "$VERSION"
77-
78- # Verify tag was created successfully on remote
79- echo "Verifying tag was created on remote..."
80- for i in {1..5}; do
81- if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
82- echo "✅ Tag $VERSION created and verified on remote"
83- exit 0
84- fi
85- echo "Waiting for tag to propagate (attempt $i/5)..."
86- sleep 2
87- done
88-
89- echo "❌ Failed to verify tag creation on remote"
90- exit 1
91-
9266 - name : Extract version from tag
9367 id : extract_version
9468 run : |
@@ -123,6 +97,116 @@ jobs:
12397 fi
12498 echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
12599 echo "Tag name: $TAG_NAME"
100+
101+ - name : Generate release notes
102+ env :
103+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
104+ run : |
105+ TAG_NAME="${{ steps.tag_name.outputs.tag_name }}"
106+ VERSION="${{ steps.extract_version.outputs.tag_version }}"
107+ echo "Generating release notes for $TAG_NAME..."
108+
109+ # Use GitHub API to auto-generate notes from merged PRs since last release
110+ if gh api repos/${{ github.repository }}/releases/generate-notes \
111+ -f tag_name="${TAG_NAME}" \
112+ --jq '.body' > /tmp/release_notes.md 2>/tmp/generate_notes_err.txt && [ -s /tmp/release_notes.md ]; then
113+ echo "✅ Generated release notes from merged PRs"
114+ else
115+ echo "Release ${VERSION}" > /tmp/release_notes.md
116+ echo "⚠️ Could not auto-generate notes, using fallback"
117+ if [ -s /tmp/generate_notes_err.txt ]; then
118+ echo " Error: $(cat /tmp/generate_notes_err.txt)"
119+ fi
120+ fi
121+
122+ echo "--- Release notes preview ---"
123+ cat /tmp/release_notes.md
124+ echo "---"
125+
126+ - name : Update CHANGELOG.md for VSIX packaging
127+ run : |
128+ VERSION="${{ steps.extract_version.outputs.tag_version }}"
129+ node -e "
130+ const fs = require('fs');
131+ const version = process.argv[1];
132+ const notes = fs.readFileSync('/tmp/release_notes.md', 'utf8').trim();
133+ let changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
134+ const marker = '## [Unreleased]';
135+ const idx = changelog.indexOf(marker);
136+ if (idx >= 0) {
137+ const insertAt = changelog.indexOf('\n', idx) + 1;
138+ const section = '\n## [' + version + ']\n\n' + notes + '\n';
139+ changelog = changelog.slice(0, insertAt) + section + changelog.slice(insertAt);
140+ }
141+ fs.writeFileSync('CHANGELOG.md', changelog);
142+ console.log('✅ Updated CHANGELOG.md with v' + version + ' notes for VSIX packaging');
143+ " "$VERSION"
144+
145+ - name : Create changelog branch and commit
146+ if : steps.trigger_type.outputs.is_manual == 'true'
147+ id : changelog_branch
148+ env :
149+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
150+ run : |
151+ VERSION="${{ steps.extract_version.outputs.tag_version }}"
152+ BRANCH="changelog/v${VERSION}"
153+ echo "branch=$BRANCH" >> $GITHUB_OUTPUT
154+
155+ git config --local user.name "github-actions[bot]"
156+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
157+
158+ # Check if branch already exists on remote
159+ if git ls-remote --exit-code --heads origin "refs/heads/$BRANCH" >/dev/null 2>&1; then
160+ echo "❌ Branch $BRANCH already exists on remote!"
161+ echo "Delete it or bump the version before re-running."
162+ exit 1
163+ fi
164+
165+ if git diff --quiet CHANGELOG.md; then
166+ echo "ℹ️ No changes to CHANGELOG.md, skipping branch + commit"
167+ exit 0
168+ fi
169+
170+ git checkout -b "$BRANCH"
171+ git add CHANGELOG.md
172+ git commit -m "docs: update CHANGELOG.md for v${VERSION}"
173+ git push origin "$BRANCH"
174+ echo "✅ Pushed CHANGELOG.md update to $BRANCH"
175+
176+ - name : Create tag for manual trigger
177+ if : steps.trigger_type.outputs.is_manual == 'true' && inputs.create_tag == true
178+ env :
179+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
180+ run : |
181+ VERSION="v${{ steps.package_version.outputs.package_version }}"
182+ echo "Creating tag: $VERSION"
183+
184+ # Check if tag already exists on remote using exit code
185+ if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
186+ echo "❌ Tag $VERSION already exists on remote!"
187+ echo "Please update the version in package.json or delete the existing tag."
188+ exit 1
189+ fi
190+
191+ # Create and push the tag
192+ git config --local user.name "github-actions[bot]"
193+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
194+ git tag -a "$VERSION" -m "Release $VERSION"
195+ git push origin "$VERSION"
196+
197+ # Verify tag was created successfully on remote
198+ echo "Verifying tag was created on remote..."
199+ for i in {1..5}; do
200+ if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
201+ echo "✅ Tag $VERSION created and verified on remote"
202+ exit 0
203+ fi
204+ echo "Waiting for tag to propagate (attempt $i/5)..."
205+ sleep 2
206+ done
207+
208+ echo "❌ Failed to verify tag creation on remote"
209+ exit 1
126210
127211 - name : Install dependencies
128212 run : npm ci
@@ -158,22 +242,13 @@ jobs:
158242 VSIX_FILE=$(ls *.vsix | head -n 1)
159243 echo "vsix_file=$VSIX_FILE" >> $GITHUB_OUTPUT
160244 echo "VSIX file: $VSIX_FILE"
161-
162- - name : Generate release notes
163- run : |
164- # Extract the latest changes from CHANGELOG.md if it has been updated
165- # Write to a file to avoid shell interpretation issues with special characters
166- if grep -q "## \[.*\]" CHANGELOG.md; then
167- # Try to extract the latest version section from changelog
168- NOTES=$(sed -n '/## \[.*\]/,/## \[.*\]/p' CHANGELOG.md | head -n -1 | tail -n +2)
169- if [ -n "$NOTES" ]; then
170- echo "$NOTES" > /tmp/release_notes.md
171- else
172- echo "Release ${{ steps.extract_version.outputs.tag_version }}" > /tmp/release_notes.md
173- fi
174- else
175- echo "Release ${{ steps.extract_version.outputs.tag_version }}" > /tmp/release_notes.md
176- fi
245+
246+ - name : Upload VSIX as workflow artifact
247+ uses : actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
248+ with :
249+ name : vsix-package
250+ path : ./${{ steps.vsix_filename.outputs.vsix_file }}
251+ retention-days : 90
177252
178253 - name : Create Release
179254 id : create_release
@@ -185,7 +260,7 @@ jobs:
185260 set -o pipefail
186261 echo "Creating release for tag: ${{ steps.tag_name.outputs.tag_name }}"
187262
188- # Create release with notes from file and upload VSIX file
263+ # Create release with auto-generated notes and upload VSIX file
189264 gh release create "${{ steps.tag_name.outputs.tag_name }}" \
190265 --title "Release ${{ steps.extract_version.outputs.tag_version }}" \
191266 --notes-file /tmp/release_notes.md \
@@ -217,3 +292,76 @@ jobs:
217292 echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
218293 exit 1
219294 fi
295+
296+ publish :
297+ needs : release
298+ if : github.event_name == 'workflow_dispatch' && inputs.publish_marketplace
299+ runs-on : ubuntu-latest
300+ permissions :
301+ contents : read
302+
303+ steps :
304+ - name : Harden the runner (Audit all outbound calls)
305+ uses : step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
306+ with :
307+ egress-policy : audit
308+
309+ - name : Checkout code
310+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
311+
312+ - name : Setup Node.js
313+ uses : actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
314+ with :
315+ node-version : ' 20.x'
316+ cache : ' npm'
317+
318+ - name : Install dependencies
319+ run : npm ci
320+
321+ - name : Download VSIX from release
322+ id : download_vsix
323+ env :
324+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
325+ run : |
326+ TAG_NAME="${{ needs.release.outputs.tag_name }}"
327+ echo "Downloading VSIX from release $TAG_NAME..."
328+ gh release download "$TAG_NAME" \
329+ --repo "${{ github.repository }}" \
330+ --pattern "*.vsix" \
331+ --dir .
332+ VSIX_FILE=$(ls *.vsix | head -n 1)
333+ echo "Downloaded: $VSIX_FILE"
334+ echo "vsix_file=$VSIX_FILE" >> $GITHUB_OUTPUT
335+
336+ - name : Publish to VS Code Marketplace
337+ id : publish
338+ env :
339+ VSCE_PAT : ${{ secrets.VSCE_PAT }}
340+ run : |
341+ if [ -z "$VSCE_PAT" ]; then
342+ echo "❌ VSCE_PAT secret is not configured."
343+ echo " Add it at: Settings → Secrets and variables → Actions"
344+ echo " Create a PAT at https://dev.azure.com with 'Marketplace (Publish)' scope"
345+ exit 1
346+ fi
347+
348+ echo "Publishing ${{ steps.download_vsix.outputs.vsix_file }} to VS Code Marketplace..."
349+ npx vsce publish \
350+ --packagePath "${{ steps.download_vsix.outputs.vsix_file }}" \
351+ --pat "$VSCE_PAT"
352+
353+ - name : Publish Summary
354+ if : always()
355+ run : |
356+ echo "# 🚀 VS Code Marketplace" >> $GITHUB_STEP_SUMMARY
357+ echo "" >> $GITHUB_STEP_SUMMARY
358+ if [ "${{ steps.publish.outcome }}" == "success" ]; then
359+ 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
360+ else
361+ echo "❌ Failed to publish v${{ needs.release.outputs.version }} to marketplace." >> $GITHUB_STEP_SUMMARY
362+ echo "" >> $GITHUB_STEP_SUMMARY
363+ echo "Ensure the \`VSCE_PAT\` secret is configured with a valid Azure DevOps PAT" >> $GITHUB_STEP_SUMMARY
364+ echo "with the \`Marketplace (Publish)\` scope for all accessible organizations." >> $GITHUB_STEP_SUMMARY
365+ fi
366+
367+
0 commit comments