Skip to content

Commit a7bb856

Browse files
fix(workflows): harden even/odd versioning against regression and syntax errors (#816)
## Description Hardened the even/odd marketplace versioning convention across all release workflows. Filtered `LAST_TAG` to stable-only (even-minor) tags to prevent version regression when pre-release tags exist. Fixed a corrupted jq `//` operator syntax in the manifest fallback. Added an even-minor validation step after release-please to block odd-minor stable releases. Added plugin generation to the prerelease workflow to fix Plugin Validation CI failures. ## Related Issue(s) Closes #815 ## Type of Change Select all that apply: **Code & Documentation:** * [x] Bug fix (non-breaking change fixing an issue) * [ ] New feature (non-breaking change adding functionality) * [ ] Breaking change (fix or feature causing existing functionality to change) * [ ] Documentation update **Infrastructure & Configuration:** * [x] GitHub Actions workflow * [ ] Linting configuration (markdown, PowerShell, etc.) * [ ] Security configuration * [ ] DevContainer configuration * [ ] Dependency update **AI Artifacts:** * [ ] Reviewed contribution with `prompt-builder` agent and addressed all feedback * [ ] Copilot instructions (`.github/instructions/*.instructions.md`) * [ ] Copilot prompt (`.github/prompts/*.prompt.md`) * [ ] Copilot agent (`.github/agents/*.agent.md`) * [ ] Copilot skill (`.github/skills/*/SKILL.md`) **Other:** * [ ] Script/automation (`.ps1`, `.sh`, `.py`) * [ ] Other (please describe): ## Testing Validated YAML syntax on all 4 workflow files. Reviewed the even/odd logic for correctness: - `LAST_TAG` awk filter correctly selects even-minor tags via `$(NF-1) % 2 == 0` - jq fallback `.["."] // "0.0.0"` is syntactically valid - Even-minor guard uses `steps.release.outputs.minor` from release-please - Pre-release version format `{MAJOR}.{ODD_MINOR}.{COMMIT_COUNT}` avoids SemVer suffixes incompatible with VS Code marketplace ## Checklist ### Required Checks * [x] Documentation is updated (if applicable) * [x] Files follow existing naming conventions * [x] Changes are backwards compatible (if applicable) * [ ] Tests added for new functionality (if applicable) ### AI Artifact Contributions N/A ### Required Automated Checks The following validation commands must pass before merging: * [x] Markdown linting: `npm run lint:md` * [x] Spell checking: `npm run spell-check` * [x] Frontmatter validation: `npm run lint:frontmatter` * [x] Skill structure validation: `npm run validate:skills` * [x] Link validation: `npm run lint:md-links` * [x] PowerShell analysis: `npm run lint:ps` ## Additional Notes - This PR replaces the SemVer `-rc.N` pre-release suffix approach with the VS Code marketplace even/odd minor convention (even = stable, odd = pre-release). - The `LAST_TAG` filter prevents version regression that would occur if `git describe` picked up a pre-release (odd-minor) tag as the baseline for computing the next version. - PR #739 proposes `3.1.0` as a stable release, which conflicts with even/odd convention. It should be closed after this PR merges.
1 parent 18f7545 commit a7bb856

4 files changed

Lines changed: 66 additions & 27 deletions

File tree

.github/workflows/extension-publish.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,11 @@ jobs:
5858
exit 1
5959
fi
6060
61-
# Validate stable version (must not contain pre-release suffix)
62-
if echo "$VERSION" | grep -qE '\-rc\.[0-9]+'; then
63-
echo "::error::Stable channel does not accept pre-release versions. Got: $VERSION"
64-
echo "::error::Use extension-publish-prerelease workflow for -rc.N versions"
61+
# Even/odd convention: even minor = stable, odd minor = pre-release
62+
PUBLISH_MINOR=$(echo "$VERSION" | cut -d. -f2)
63+
if (( PUBLISH_MINOR % 2 == 1 )); then
64+
echo "::error::Stable channel requires even minor version. Got: $VERSION (odd minor = pre-release)"
65+
echo "::error::Use the pre-release publish workflow for odd-minor versions"
6566
exit 1
6667
fi
6768

.github/workflows/main.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@ jobs:
122122
# created in the next step, ensuring subsequent runs find it.
123123
skip-github-pull-request: ${{ github.event_name == 'push' && startsWith(github.event.head_commit.message, 'chore(main)') }}
124124

125+
- name: Validate even-minor convention
126+
if: ${{ steps.release.outputs.release_created == 'true' }}
127+
run: |
128+
MINOR="${{ steps.release.outputs.minor }}"
129+
if (( MINOR % 2 != 0 )); then
130+
echo "::error::Release version ${{ steps.release.outputs.version }} has odd minor ($MINOR). Even/odd convention requires even minor for stable releases."
131+
exit 1
132+
fi
133+
125134
# Workaround: release-please with "draft": true uses lazy tag
126135
# creation. The git tag is not materialized until the release is
127136
# published. Without the tag, release-please cannot find the draft
@@ -192,7 +201,7 @@ jobs:
192201
RELEASED="${{ needs.release-please.outputs.version }}"
193202
MAJOR=$(echo "$RELEASED" | cut -d. -f1)
194203
MINOR=$(echo "$RELEASED" | cut -d. -f2)
195-
PRE_VERSION="${MAJOR}.$((MINOR + 1)).0-rc.0"
204+
PRE_VERSION="${MAJOR}.$((MINOR + 1)).0"
196205
gh pr edit "$PR_NUMBER" \
197206
--title "chore(main): pre-release $PRE_VERSION" \
198207
--body "## Pre-Release $PRE_VERSION

.github/workflows/prerelease-release.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,20 @@ jobs:
4545
REPO: ${{ github.repository }}
4646
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
4747
run: |
48-
VERSION=$(echo "$PR_TITLE" | grep -oE 'pre-release [0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?')
48+
VERSION=$(echo "$PR_TITLE" | grep -oE 'pre-release [0-9]+\.[0-9]+\.[0-9]+' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
4949
if [ -z "$VERSION" ]; then
5050
echo "⚠️ Could not extract version from PR title, falling back to manifest"
51-
VERSION=$(jq -r '.["."]//"0.0.0"' .release-please-manifest.json 2>/dev/null || echo "")
51+
VERSION=$(jq -r '.["."] // "0.0.0"' .release-please-manifest.json 2>/dev/null || echo "")
5252
fi
5353
if [ -z "$VERSION" ]; then
5454
echo "::error::Could not extract version from PR title or manifest: $PR_TITLE"
5555
exit 1
5656
fi
5757
58-
# Validate pre-release version contains SemVer -rc suffix
59-
if ! echo "$VERSION" | grep -qE '\-rc\.[0-9]+$'; then
60-
echo "::error::Pre-release version $VERSION must contain an -rc.N suffix"
58+
# Even/odd convention: pre-release versions must have odd minor
59+
PRE_MINOR=$(echo "$VERSION" | cut -d. -f2)
60+
if (( PRE_MINOR % 2 == 0 )); then
61+
echo "::error::Pre-release version $VERSION must have an odd minor (even/odd convention)"
6162
exit 1
6263
fi
6364

.github/workflows/prerelease.yml

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,45 +45,49 @@ jobs:
4545
CURRENT=$(jq -r '.["."]//"0.0.0"' .release-please-manifest.json)
4646
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
4747
MINOR=$(echo "$CURRENT" | cut -d. -f2)
48-
PATCH=$(echo "$CURRENT" | cut -d. -f3)
4948
50-
# Find the last release tag for commit range analysis
51-
LAST_TAG=$(git describe --tags --abbrev=0 --match 'hve-core-v*' 2>/dev/null || echo "")
49+
# Find the last stable release tag (even minor) for commit range analysis.
50+
# Pre-release tags (odd minor) are excluded to prevent version regression
51+
# when a pre-release tag exists between the last stable tag and HEAD.
52+
LAST_TAG=$(git tag -l 'hve-core-v*' | awk -F'[v.]' '{if ($(NF-1) % 2 == 0) print}' | sort -V | tail -1)
5253
53-
# Check for breaking changes and new features since last release.
54+
# Check for breaking changes since last release.
5455
# Match conventional commit subject with ! (e.g. feat!:) or
5556
# a BREAKING CHANGE / BREAKING-CHANGE footer token at line start.
5657
HAS_BREAKING=0
57-
HAS_FEAT=0
5858
if [ -n "$LAST_TAG" ]; then
5959
HAS_BREAKING=$(git log "$LAST_TAG"..HEAD --format='%s%n%b' | \
6060
grep -cE '^[a-z]+(\(.+\))?!:|^BREAKING[ -]CHANGE:' || true)
61-
HAS_FEAT=$(git log "$LAST_TAG"..HEAD --format='%s' | \
62-
grep -cE '^feat(\(.+\))?[!]?:' || true)
6361
fi
6462
65-
# Compute base version using conventional commit bump rules
63+
# Even/odd convention: odd minor = pre-release, even minor = stable.
64+
# Compute the next odd minor from the current manifest version.
6665
if [ "$HAS_BREAKING" -gt 0 ]; then
67-
BASE_VERSION="$((MAJOR + 1)).0.0"
68-
elif [ "$HAS_FEAT" -gt 0 ]; then
69-
BASE_VERSION="${MAJOR}.$((MINOR + 1)).0"
66+
PRE_MAJOR=$((MAJOR + 1))
67+
PRE_MINOR=1
7068
else
71-
BASE_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
69+
PRE_MAJOR=$MAJOR
70+
if (( MINOR % 2 == 0 )); then
71+
PRE_MINOR=$((MINOR + 1))
72+
else
73+
PRE_MINOR=$MINOR
74+
fi
7275
fi
7376
74-
# Append SemVer pre-release suffix with commit count as rc number
75-
RC_NUMBER=$(git rev-list --count "${LAST_TAG}..HEAD" 2>/dev/null || echo "1")
76-
PRE_VERSION="${BASE_VERSION}-rc.${RC_NUMBER}"
77+
# Use commit count since last tag as patch for unique, monotonic versions
78+
COMMIT_COUNT=$(git rev-list --count "${LAST_TAG}..HEAD" 2>/dev/null || echo "0")
79+
PRE_VERSION="${PRE_MAJOR}.${PRE_MINOR}.${COMMIT_COUNT}"
7780
7881
echo "version=$PRE_VERSION" >> "$GITHUB_OUTPUT"
79-
echo "Computed pre-release version: $PRE_VERSION (base: $BASE_VERSION, rc: $RC_NUMBER, current: $CURRENT)"
82+
echo "last_tag=$LAST_TAG" >> "$GITHUB_OUTPUT"
83+
echo "Computed pre-release version: $PRE_VERSION (current: $CURRENT, last stable: $LAST_TAG)"
8084
8185
- name: Generate changelog
8286
id: changelog
8387
env:
8488
PRE_VERSION: ${{ steps.version.outputs.version }}
89+
LAST_TAG: ${{ steps.version.outputs.last_tag }}
8590
run: |
86-
LAST_TAG=$(git describe --tags --abbrev=0 --match 'hve-core-v*' 2>/dev/null || echo "")
8791
MAX_COUNT_ARG=""
8892
if [ -z "$LAST_TAG" ]; then
8993
RANGE="HEAD"
@@ -149,6 +153,22 @@ jobs:
149153
echo 'CHANGELOG_EOF'
150154
} >> "$GITHUB_OUTPUT"
151155
156+
- name: Setup Node.js
157+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.1.0
158+
with:
159+
node-version: "20"
160+
cache: "npm"
161+
162+
- name: Install dependencies
163+
run: npm ci
164+
165+
- name: Install PowerShell-Yaml
166+
shell: pwsh
167+
run: |
168+
if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) {
169+
Install-Module -Name PowerShell-Yaml -Force -Scope CurrentUser
170+
}
171+
152172
- name: Update prerelease/next branch with version files
153173
env:
154174
GH_TOKEN: ${{ steps.app-token.outputs.token }}
@@ -202,6 +222,14 @@ jobs:
202222
fi
203223
done
204224
225+
# Update .release-please-manifest.json so release-please sees the
226+
# odd-minor version when this PR merges to main
227+
jq --arg v "$PRE_VERSION" '.["."] = $v' .release-please-manifest.json > tmp.json \
228+
&& mv tmp.json .release-please-manifest.json
229+
230+
# Regenerate plugin outputs so plugin-validation passes
231+
npm run plugin:generate
232+
205233
git add -A
206234
git commit -m "chore: bump pre-release version to ${PRE_VERSION}" || echo "No version changes to commit"
207235
git push origin "$BRANCH"

0 commit comments

Comments
 (0)