@@ -3,14 +3,19 @@ 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
1212 default : ' true'
1313 type : boolean
14+ publish_marketplace :
15+ description : ' Publish to VS Code Marketplace after creating the release'
16+ required : false
17+ default : ' true'
18+ type : boolean
1419
1520permissions :
1621 contents : read
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)
@@ -123,6 +132,47 @@ jobs:
123132 fi
124133 echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
125134 echo "Tag name: $TAG_NAME"
135+
136+ - name : Generate release notes
137+ env :
138+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
139+ run : |
140+ TAG_NAME="${{ steps.tag_name.outputs.tag_name }}"
141+ VERSION="${{ steps.extract_version.outputs.tag_version }}"
142+ echo "Generating release notes for $TAG_NAME..."
143+
144+ # Use GitHub API to auto-generate notes from merged PRs since last release
145+ if gh api repos/${{ github.repository }}/releases/generate-notes \
146+ -f tag_name="${TAG_NAME}" \
147+ --jq '.body' > /tmp/release_notes.md 2>/dev/null && [ -s /tmp/release_notes.md ]; then
148+ echo "✅ Generated release notes from merged PRs"
149+ else
150+ echo "Release ${VERSION}" > /tmp/release_notes.md
151+ echo "⚠️ Could not auto-generate notes, using fallback"
152+ fi
153+
154+ echo "--- Release notes preview ---"
155+ cat /tmp/release_notes.md
156+ echo "---"
157+
158+ - name : Update CHANGELOG.md for VSIX packaging
159+ run : |
160+ VERSION="${{ steps.extract_version.outputs.tag_version }}"
161+ node -e "
162+ const fs = require('fs');
163+ const version = process.argv[1];
164+ const notes = fs.readFileSync('/tmp/release_notes.md', 'utf8').trim();
165+ let changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
166+ const marker = '## [Unreleased]';
167+ const idx = changelog.indexOf(marker);
168+ if (idx >= 0) {
169+ const insertAt = changelog.indexOf('\n', idx) + 1;
170+ const section = '\n## [' + version + ']\n\n' + notes + '\n';
171+ changelog = changelog.slice(0, insertAt) + section + changelog.slice(insertAt);
172+ }
173+ fs.writeFileSync('CHANGELOG.md', changelog);
174+ console.log('✅ Updated CHANGELOG.md with v' + version + ' notes for VSIX packaging');
175+ " "$VERSION"
126176
127177 - name : Install dependencies
128178 run : npm ci
@@ -158,22 +208,13 @@ jobs:
158208 VSIX_FILE=$(ls *.vsix | head -n 1)
159209 echo "vsix_file=$VSIX_FILE" >> $GITHUB_OUTPUT
160210 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
211+
212+ - name : Upload VSIX as workflow artifact
213+ uses : actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
214+ with :
215+ name : vsix-package
216+ path : ./${{ steps.vsix_filename.outputs.vsix_file }}
217+ retention-days : 90
177218
178219 - name : Create Release
179220 id : create_release
@@ -185,7 +226,7 @@ jobs:
185226 set -o pipefail
186227 echo "Creating release for tag: ${{ steps.tag_name.outputs.tag_name }}"
187228
188- # Create release with notes from file and upload VSIX file
229+ # Create release with auto-generated notes and upload VSIX file
189230 gh release create "${{ steps.tag_name.outputs.tag_name }}" \
190231 --title "Release ${{ steps.extract_version.outputs.tag_version }}" \
191232 --notes-file /tmp/release_notes.md \
@@ -217,3 +258,137 @@ jobs:
217258 echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
218259 exit 1
219260 fi
261+
262+ publish :
263+ needs : release
264+ if : github.event_name == 'workflow_dispatch' && inputs.publish_marketplace
265+ runs-on : ubuntu-latest
266+ permissions :
267+ contents : read
268+
269+ steps :
270+ - name : Harden the runner (Audit all outbound calls)
271+ uses : step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
272+ with :
273+ egress-policy : audit
274+
275+ - name : Checkout code
276+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
277+
278+ - name : Setup Node.js
279+ uses : actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
280+ with :
281+ node-version : ' 20.x'
282+ cache : ' npm'
283+
284+ - name : Install dependencies
285+ run : npm ci
286+
287+ - name : Download VSIX from release
288+ id : download_vsix
289+ env :
290+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
291+ run : |
292+ TAG_NAME="${{ needs.release.outputs.tag_name }}"
293+ echo "Downloading VSIX from release $TAG_NAME..."
294+ gh release download "$TAG_NAME" \
295+ --repo "${{ github.repository }}" \
296+ --pattern "*.vsix" \
297+ --dir .
298+ VSIX_FILE=$(ls *.vsix | head -n 1)
299+ echo "Downloaded: $VSIX_FILE"
300+ echo "vsix_file=$VSIX_FILE" >> $GITHUB_OUTPUT
301+
302+ - name : Publish to VS Code Marketplace
303+ id : publish
304+ env :
305+ VSCE_PAT : ${{ secrets.VSCE_PAT }}
306+ run : |
307+ if [ -z "$VSCE_PAT" ]; then
308+ echo "❌ VSCE_PAT secret is not configured."
309+ echo " Add it at: Settings → Secrets and variables → Actions"
310+ echo " Create a PAT at https://dev.azure.com with 'Marketplace (Publish)' scope"
311+ exit 1
312+ fi
313+
314+ echo "Publishing ${{ steps.download_vsix.outputs.vsix_file }} to VS Code Marketplace..."
315+ npx vsce publish \
316+ --packagePath "${{ steps.download_vsix.outputs.vsix_file }}" \
317+ --pat "$VSCE_PAT"
318+
319+ - name : Publish Summary
320+ if : always()
321+ run : |
322+ echo "# 🚀 VS Code Marketplace" >> $GITHUB_STEP_SUMMARY
323+ echo "" >> $GITHUB_STEP_SUMMARY
324+ if [ "${{ steps.publish.outcome }}" == "success" ]; then
325+ 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
326+ else
327+ echo "❌ Failed to publish v${{ needs.release.outputs.version }} to marketplace." >> $GITHUB_STEP_SUMMARY
328+ echo "" >> $GITHUB_STEP_SUMMARY
329+ echo "Ensure the \`VSCE_PAT\` secret is configured with a valid Azure DevOps PAT" >> $GITHUB_STEP_SUMMARY
330+ echo "with the \`Marketplace (Publish)\` scope for all accessible organizations." >> $GITHUB_STEP_SUMMARY
331+ fi
332+
333+ update-changelog :
334+ needs : release
335+ if : always() && needs.release.result == 'success'
336+ runs-on : ubuntu-latest
337+ permissions :
338+ contents : write
339+ pull-requests : write
340+
341+ steps :
342+ - name : Harden the runner (Audit all outbound calls)
343+ uses : step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
344+ with :
345+ egress-policy : audit
346+
347+ - name : Checkout code
348+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
349+ with :
350+ fetch-depth : 0
351+
352+ - name : Setup Node.js
353+ uses : actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
354+ with :
355+ node-version : ' 20.x'
356+
357+ - name : Sync CHANGELOG.md from GitHub releases
358+ env :
359+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
360+ run : node scripts/sync-changelog.js
361+
362+ - name : Check for changes
363+ id : changes
364+ run : |
365+ if git diff --quiet CHANGELOG.md; then
366+ echo "changed=false" >> $GITHUB_OUTPUT
367+ echo "ℹ️ No changes needed"
368+ else
369+ echo "changed=true" >> $GITHUB_OUTPUT
370+ echo "Changes detected in CHANGELOG.md"
371+ fi
372+
373+ - name : Create Pull Request
374+ if : steps.changes.outputs.changed == 'true'
375+ uses : peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
376+ with :
377+ branch : update-changelog
378+ title : " docs: sync CHANGELOG.md with v${{ needs.release.outputs.version }} release notes"
379+ body : |
380+ Automatically syncs CHANGELOG.md with the latest GitHub release notes.
381+
382+ Triggered by release workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
383+ commit-message : " docs: sync CHANGELOG.md with GitHub release notes"
384+
385+ - name : Changelog Summary
386+ if : always()
387+ run : |
388+ echo "# 📝 Changelog Update" >> $GITHUB_STEP_SUMMARY
389+ echo "" >> $GITHUB_STEP_SUMMARY
390+ if [ "${{ steps.changes.outputs.changed }}" == "true" ]; then
391+ echo "A pull request has been created to update CHANGELOG.md." >> $GITHUB_STEP_SUMMARY
392+ else
393+ echo "CHANGELOG.md is already up to date." >> $GITHUB_STEP_SUMMARY
394+ fi
0 commit comments