Skip to content

Commit 0c53c2c

Browse files
authored
Merge pull request #419 from rajbos/copilot/fix-release-workflow-process
Automate release pipeline: one-trigger workflow with marketplace publish and proper release notes
2 parents 806060f + 32b49f2 commit 0c53c2c

File tree

6 files changed

+257
-151
lines changed

6 files changed

+257
-151
lines changed

.github/workflows/release.yml

Lines changed: 203 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ name: Release
33
on:
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

1520
permissions:
@@ -20,6 +25,10 @@ jobs:
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)
@@ -54,41 +63,6 @@ jobs:
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+
Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
name: Sync Release Notes
22

3+
# Manual-only trigger — automated changelog sync is handled by the Release workflow.
4+
# Use this to re-sync CHANGELOG.md from GitHub releases at any time.
35
on:
4-
workflow_dispatch: # Manual trigger
5-
release:
6-
types: [published, edited] # Automatic trigger when releases are published or edited
6+
workflow_dispatch:
77

88
permissions:
99
contents: read
@@ -12,7 +12,8 @@ jobs:
1212
sync-release-notes:
1313
runs-on: ubuntu-latest
1414
permissions:
15-
contents: write # Need write permission to update CHANGELOG.md
15+
contents: write
16+
pull-requests: write
1617

1718
steps:
1819
- name: Harden the runner (Audit all outbound calls)
@@ -23,7 +24,7 @@ jobs:
2324
- name: Checkout code
2425
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2526
with:
26-
fetch-depth: 0 # Fetch all history so we can work with all releases
27+
fetch-depth: 0
2728

2829
- name: Setup Node.js
2930
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
@@ -33,9 +34,7 @@ jobs:
3334
- name: Sync GitHub release notes to CHANGELOG.md
3435
env:
3536
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36-
run: |
37-
# Run the sync script from the scripts directory
38-
node scripts/sync-changelog.js
37+
run: node scripts/sync-changelog.js
3938

4039
- name: Check for changes
4140
id: changes
@@ -47,24 +46,6 @@ jobs:
4746
echo "changed=true" >> $GITHUB_OUTPUT
4847
echo "Changes detected in CHANGELOG.md"
4948
fi
50-
51-
- name: Ensure on main branch
52-
if: steps.changes.outputs.changed == 'true'
53-
run: |
54-
git checkout -b update-changelog
55-
56-
- name: Commit and push changes
57-
if: steps.changes.outputs.changed == 'true'
58-
run: |
59-
git config --local user.email "action@github.com"
60-
git config --local user.name "GitHub Action"
61-
git add CHANGELOG.md
62-
git commit -m "docs: sync CHANGELOG.md with GitHub release notes
63-
64-
This commit automatically updates the CHANGELOG.md file to match
65-
the release notes from GitHub releases, ensuring consistency
66-
between local documentation and published releases."
67-
git push -u origin update-changelog
6849
6950
- name: Create Pull Request
7051
if: steps.changes.outputs.changed == 'true'
@@ -73,14 +54,15 @@ jobs:
7354
branch: update-changelog
7455
title: "docs: sync CHANGELOG.md with GitHub release notes"
7556
body: |
76-
This pull request updates the CHANGELOG.md file to reflect the latest
77-
GitHub release notes, ensuring that the changelog is always up to date
78-
with the published releases.
57+
Automatically syncs CHANGELOG.md with the latest GitHub release notes.
58+
59+
Triggered by: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
60+
commit-message: "docs: sync CHANGELOG.md with GitHub release notes"
7961

8062
- name: Summary
8163
run: |
8264
if [ "${{ steps.changes.outputs.changed }}" == "true" ]; then
83-
echo "✅ CHANGELOG.md has been successfully updated with GitHub release notes"
65+
echo "✅ A PR has been created to update CHANGELOG.md with GitHub release notes"
8466
else
8567
echo "ℹ️ CHANGELOG.md was already up to date with GitHub release notes"
8668
fi

0 commit comments

Comments
 (0)