Skip to content

Commit 19c500a

Browse files
authored
feat: add automatic package.json version bump based on GitHub releases (#19)
This pull request significantly enhances the release automation workflow for the VS Code extension by introducing a robust, multi-stage process for validating release versions, updating the `package.json` version, and ensuring that version bumps are properly committed and merged before publishing. The workflow now automatically creates pull requests for version updates, waits for merges, and updates tags accordingly, improving reliability and reducing manual intervention. Key improvements and additions: **Automated Version Validation and Update:** - Adds a `validate-release-version` job to parse and validate the release tag against semver, ensuring only valid versions proceed. - Introduces an `update-version` job that updates `package.json` and `package-lock.json` with the new version, creates a dedicated release branch, commits changes, opens a pull request, and waits for it to be merged before proceeding. **Branch and Pull Request Management:** - Automatically creates a new branch for the version bump, checks for branch conflicts, and handles errors if the branch already exists. - Uses GitHub CLI (`gh`) to open a pull request, apply labels, enable auto-merge, and wait for the merge to complete before continuing. **Tag Management:** - After the version bump PR is merged, moves the release tag to the new commit to ensure the tag points to the correct version, maintaining release integrity. **Deployment Improvements:** - Ensures the deployment (`deploy` job) checks out the correct branch (`TARGET_BRANCH`) and uses `npm ci` for consistent dependency installation. - Adds a success report step after publishing to both VS Code Marketplace and Open VSX Registry. **Workflow Permissions and Environment:** - Sets explicit permissions for contents, pull-requests, and actions, and defines environment variables for tag and branch names to streamline job steps.
2 parents 465339e + 501d793 commit 19c500a

1 file changed

Lines changed: 274 additions & 1 deletion

File tree

.github/workflows/publish-extension.yml

Lines changed: 274 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,286 @@
1+
# The auto package.json version update code is adapted from: https://github.com/valeryan/vscode-phpsab/blob/v0.0.21/.github/workflows/publish.yml
2+
13
on:
24
release:
35
types: [prereleased, released]
46

7+
env:
8+
TAG_NAME: ${{ github.ref_name }}
9+
TARGET_BRANCH: ${{ github.event.release.target_commitish }}
10+
11+
permissions:
12+
contents: write
13+
pull-requests: write
14+
actions: read
15+
516
name: Publish Extension to VS Code Marketplace and Open VSX Registry
617
jobs:
18+
validate-release-version:
19+
name: Validate Release Version
20+
runs-on: ubuntu-latest
21+
if: github.event_name == 'release'
22+
outputs:
23+
version: ${{ steps.parse-version.outputs.version }}
24+
is-valid: ${{ steps.parse-version.outputs.is-valid }}
25+
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@v4
29+
with:
30+
token: ${{ secrets.GITHUB_TOKEN }}
31+
32+
- name: Parse and Validate Version
33+
id: parse-version
34+
run: |
35+
TAG_NAME="${{ env.TAG_NAME }}"
36+
37+
# Remove 'v' prefix if present and validate semver format
38+
if [[ $TAG_NAME =~ ^v?([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.-]+)?(\+[a-zA-Z0-9\.-]+)?)$ ]]; then
39+
VERSION=${BASH_REMATCH[1]}
40+
echo "version=$VERSION" >> $GITHUB_OUTPUT
41+
echo "is-valid=true" >> $GITHUB_OUTPUT
42+
echo "✅ Valid version format: $VERSION"
43+
else
44+
echo "is-valid=false" >> $GITHUB_OUTPUT
45+
echo "❌ Invalid version format: $TAG_NAME"
46+
echo "Version must follow semver format (e.g., v1.0.0, 1.0.0, v1.0.0-beta.1)"
47+
exit 1
48+
fi
49+
50+
update-version:
51+
name: Update Version
52+
needs: validate-release-version
53+
runs-on: ubuntu-latest
54+
if: needs.validate-release-version.outputs.is-valid == 'true'
55+
steps:
56+
- name: Checkout
57+
uses: actions/checkout@v4
58+
with:
59+
token: ${{ secrets.GITHUB_TOKEN }}
60+
fetch-depth: 0
61+
62+
- name: Setup Git
63+
run: |
64+
git config user.name "github-actions[bot]"
65+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
66+
67+
- name: Setup Node
68+
uses: actions/setup-node@v4
69+
with:
70+
node-version: 20.x
71+
cache: "npm"
72+
73+
- name: Install Dependencies
74+
run: npm ci
75+
76+
- name: Update Package.json Version
77+
run: |
78+
VERSION="${{ needs.validate-release-version.outputs.version }}"
79+
80+
# If this is a prerelease, remove any prerelease tags to get base version
81+
if [ "${{ github.event.action }}" = "prereleased" ]; then
82+
BASE_VERSION=$(echo "$VERSION" | sed 's/-.*$//')
83+
echo "🔄 Prerelease detected: using base version $BASE_VERSION instead of $VERSION"
84+
VERSION="$BASE_VERSION"
85+
fi
86+
87+
CURRENT_VERSION=$(node -p "require('./package.json').version")
88+
89+
if [ "$VERSION" = "$CURRENT_VERSION" ]; then
90+
echo "ℹ️ Package.json already has version $VERSION, skipping update"
91+
else
92+
echo "📦 Updating package.json from $CURRENT_VERSION to $VERSION"
93+
94+
# Update version via "npm version" command without creating a git tag.
95+
# This ensures package-lock.json is also updated as well as package.json.
96+
npm version $VERSION --no-git-tag-version
97+
98+
echo "✅ Successfully updated package version to $VERSION"
99+
fi
100+
101+
- name: Check for Changes
102+
id: git-check
103+
run: |
104+
# Check for any modified or untracked files
105+
if [ -n "$(git status --porcelain)" ]; then
106+
echo "changes=true" >> $GITHUB_OUTPUT
107+
echo "📝 Changes detected:"
108+
git status --porcelain
109+
else
110+
echo "changes=false" >> $GITHUB_OUTPUT
111+
echo "ℹ️ No changes to commit"
112+
fi
113+
114+
- name: Create New Branch
115+
if: steps.git-check.outputs.changes == 'true'
116+
id: create-branch
117+
env:
118+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
119+
run: |
120+
set -e # Exit on any error
121+
BRANCH_NAME="release/${{ env.TAG_NAME }}"
122+
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
123+
124+
# Check if branch already exists remotely
125+
if git ls-remote --exit-code --heads origin "$BRANCH_NAME" >/dev/null 2>&1; then
126+
echo "❌ Cannot create $BRANCH_NAME branch as it already exists. This may indicate:"
127+
echo " - A previous workflow run failed partway through"
128+
echo " - The branch was manually created"
129+
echo "Please manually delete the branch or check for an existing PR and resolve manually."
130+
exit 1
131+
fi
132+
133+
# Create and checkout new branch
134+
git checkout -b "$BRANCH_NAME" || {
135+
echo "❌ Failed to create branch $BRANCH_NAME"
136+
exit 1
137+
}
138+
echo "✅ Created $BRANCH_NAME branch"
139+
140+
- name: Commit Changes
141+
if: steps.git-check.outputs.changes == 'true'
142+
id: commit-changes
143+
env:
144+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
145+
run: |
146+
set -e # Exit on any error
147+
148+
# Stage and commit changes
149+
git add package.json package-lock.json || {
150+
echo "❌ Failed to stage files"
151+
exit 1
152+
}
153+
154+
# Create commit message
155+
COMMIT_MESSAGE="chore: version bump
156+
157+
git commit -m "$COMMIT_MESSAGE" || {
158+
echo "❌ Failed to commit changes"
159+
exit 1
160+
}
161+
echo "✅ Committed changes"
162+
163+
# Push to remote
164+
BRANCH_NAME="${{ steps.create-branch.outputs.branch-name }}"
165+
git push origin "$BRANCH_NAME" || {
166+
echo "❌ Failed to push changes to remote"
167+
exit 1
168+
}
169+
echo "✅ Pushed changes to remote on $BRANCH_NAME branch"
170+
171+
- name: Create Pull Request
172+
if: steps.git-check.outputs.changes == 'true'
173+
id: create-pr
174+
env:
175+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
176+
run: |
177+
set -e # Exit on any error
178+
BRANCH_NAME="${{ steps.create-branch.outputs.branch-name }}"
179+
180+
echo "Creating pull request on $BRANCH_NAME branch into ${{ env.TARGET_BRANCH }}"
181+
182+
# Set PR title and body based on release type
183+
RELEASE_TYPE="release"
184+
if [ "${{ github.event.action }}" = "prereleased" ]; then
185+
RELEASE_TYPE="pre-release"
186+
fi
187+
188+
# Define PR details
189+
# ^ capitalizes the first letter
190+
PR_TITLE="${RELEASE_TYPE^} ${{ env.TAG_NAME }}"
191+
PR_BODY="Automated version bump for $RELEASE_TYPE ${{ env.TAG_NAME }}."
192+
PR_LABELS="auto version bump,release"
193+
194+
# Create pull request
195+
PR_URL=$(gh pr create \
196+
--base "${{ env.TARGET_BRANCH }}" \
197+
--head "$BRANCH_NAME" \
198+
--title "$PR_TITLE" \
199+
--body "$PR_BODY" \
200+
--label "$PR_LABELS") || {
201+
echo "❌ Failed to create pull request"
202+
exit 1
203+
}
204+
echo "✅ Pull request created successfully: $PR_URL"
205+
206+
# Extract PR number from URL and store as output
207+
PR_NUMBER=$(echo "$PR_URL" | grep -oP '\d+$')
208+
echo "pr-number=$PR_NUMBER" >> $GITHUB_OUTPUT
209+
echo "pr-url=$PR_URL" >> $GITHUB_OUTPUT
210+
211+
# Enable GitHub PR auto-merge
212+
gh pr merge "$PR_URL" --auto --squash || {
213+
echo "❌ Failed to enable auto-merge"
214+
echo "This may be due to:"
215+
echo " - Auto-merge not being enabled in repository settings"
216+
echo " - Required status checks not configured"
217+
echo " - Insufficient permissions"
218+
exit 1
219+
}
220+
echo "✅ Enabled auto-merge for pull request"
221+
222+
- name: Wait for PR Merge
223+
if: steps.git-check.outputs.changes == 'true'
224+
env:
225+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
226+
run: |
227+
PR_NUMBER="${{ steps.create-pr.outputs.pr-number }}"
228+
PR_URL="${{ steps.create-pr.outputs.pr-url }}"
229+
230+
echo "⏳ Waiting for pull request #$PR_NUMBER to be merged..."
231+
232+
# Wait for PR to be merged (timeout after 5 minutes)
233+
TIMEOUT=300
234+
ELAPSED=0
235+
INTERVAL=5
236+
237+
while [ $ELAPSED -lt $TIMEOUT ]; do
238+
# Get PR state and merge status in one call
239+
PR_DATA=$(gh pr view "$PR_NUMBER" --json state,mergedAt 2>/dev/null || echo '{"state":"UNKNOWN","mergedAt":null}')
240+
PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
241+
PR_MERGED_AT=$(echo "$PR_DATA" | jq -r '.mergedAt')
242+
243+
if [ "$PR_STATE" = "MERGED" ] || [ "$PR_MERGED_AT" != "null" ]; then
244+
echo "✅ Pull request successfully merged"
245+
break
246+
fi
247+
248+
sleep $INTERVAL
249+
ELAPSED=$((ELAPSED + INTERVAL))
250+
echo "⏳ Still waiting... (${ELAPSED}s elapsed)"
251+
done
252+
253+
if [ $ELAPSED -ge $TIMEOUT ]; then
254+
echo "❌ Timeout waiting for pull request to merge"
255+
exit 1
256+
fi
257+
258+
- name: Update Tag Reference
259+
if: steps.git-check.outputs.changes == 'true'
260+
run: |
261+
# Fetch the latest changes from target branch
262+
git fetch origin ${{ env.TARGET_BRANCH }}
263+
git checkout ${{ env.TARGET_BRANCH }}
264+
git pull origin ${{ env.TARGET_BRANCH }}
265+
266+
# Move the existing tag to point to the new commit (preserves GitHub release relationship)
267+
git tag ${{ env.TAG_NAME }} -f
268+
git push origin ${{ env.TAG_NAME }} -f
269+
echo "✅ Updated tag ${{ env.TAG_NAME }} to point to release commit"
270+
7271
deploy:
272+
name: Publish Extension
273+
needs: [validate-release-version, update-version]
8274
runs-on: ubuntu-latest
9275
steps:
10276
- uses: actions/checkout@v4
277+
with:
278+
ref: ${{ env.TARGET_BRANCH }}
279+
token: ${{ secrets.GITHUB_TOKEN }}
11280
- uses: actions/setup-node@v4
12281
with:
13282
node-version: 20
14-
- run: npm install
283+
- run: npm ci
15284

16285
- name: Publish to VS Code Marketplace
17286
uses: HaaLeo/publish-vscode-extension@v2
@@ -31,3 +300,7 @@ jobs:
31300
# Open VSX Registry.
32301
preRelease: ${{ github.event.action == 'prereleased' }}
33302
extensionFile: ${{ steps.publishToVsCodeMarketplace.outputs.vsixPath }}
303+
304+
- name: Report Success
305+
run: |
306+
echo "✅ Successfully published to both marketplaces"

0 commit comments

Comments
 (0)