Release Process
This document describes the release and package publishing process for vx.
Overview
The vx project uses an automated release pipeline with GitHub Actions that handles:
- Release creation with release-please
- Binary building for multiple platforms
- Publishing to various package managers (WinGet, Chocolatey, Homebrew, Scoop)
Version Format
Git Tags
vx uses the following version tag formats:
- Release-Please Format:
vx-v0.1.0(current format used by release-please) - Standard Format:
v0.1.0(traditional semantic versioning)
Version Normalization
Different package managers require different version formats:
| Package Manager | Expected Format | Example | Notes |
|---|---|---|---|
| WinGet | 0.1.0 | 0.1.0 | Without v prefix, normalized from vx-v0.1.0 |
| Chocolatey | 0.1.0 | 0.1.0 | Without v prefix |
| Homebrew | 0.1.0 | 0.1.0 | Without v prefix |
| Scoop | 0.1.0 | 0.1.0 | Without v prefix |
The workflows automatically handle version normalization to ensure compatibility with each package manager.
GitHub Actions Workflows
Release Workflow (.github/workflows/release.yml)
This workflow runs on pushes to the main branch and handles:
- Release-Please: Creates release PRs and tags
- Binary Building: Builds binaries for all supported platforms
- Asset Upload: Uploads binaries to the GitHub release
Version Extraction:
# Extract version number: vx-v0.1.0 -> 0.1.0, v0.1.0 -> 0.1.0
VERSION=$(echo "${TAG}" | sed -E 's/^(vx-)?v//')Workflow Trigger Logic
The release workflow uses a sophisticated trigger mechanism to handle different scenarios:
| Scenario | Release-Please Job | Build Job | Notes |
|---|---|---|---|
| Regular push (feat, fix, etc.) | Runs | Triggered if release created | Normal development flow |
Release PR merge (chore: release vX.Y.Z) | Skipped | Triggered | Extracts version from commit message |
Dependabot PR (chore(deps): bump...) | Skipped | Not triggered | Prevents duplicate builds |
| Manual workflow dispatch | Skipped | Triggered | Emergency/manual releases |
Key Logic:
# Release-please job skips release commits to prevent recursion
if: |
github.event_name == 'push' &&
github.ref == 'refs/heads/main' &&
!contains(github.event.head_commit.message, 'chore: release') &&
github.event.head_commit.author.name != 'github-actions[bot]'
# Build job triggers on:
# 1. Release created by release-please
# 2. Release PR merge (detected via commit message)
# 3. Manual workflow dispatch
if: |
always() &&
(
(needs.release-please.result == 'success' && needs.release-please.outputs.release_created == 'true') ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && contains(github.event.head_commit.message, 'chore: release'))
)This ensures that when a release PR is merged (e.g., "chore: release v0.6.24"), the build job still runs even though release-please is skipped.
WinGet Publishing in Release Workflow
Starting from v0.7.9, WinGet publishing is integrated directly into the release workflow (release.yml) as the publish-winget job. This ensures that:
- WinGet is published immediately after release assets are uploaded (no
workflow_rundelay) - The release tag is known exactly (from
planjob output), avoiding API version lookup issues - Pre-release filtering is handled consistently with other publish jobs
Installer regex:
# Only match cargo-dist original unversioned zip files to avoid duplicate entries.
# cargo-dist produces: vx-x86_64-pc-windows-msvc.zip (unversioned)
# release.yml also creates: vx-0.7.8-x86_64-pc-windows-msvc.zip (versioned copy)
# We must match only ONE to avoid duplicate installer entries in winget manifest.
installers-regex: 'vx-(x86_64|aarch64)-pc-windows-msvc\.zip$'Package Managers Workflow (.github/workflows/package-managers.yml)
This workflow runs after the Release workflow completes and publishes to package managers. It serves as a backup for WinGet publishing (in case the release.yml publish-winget job fails) and as the primary publisher for Chocolatey and Scoop.
Version Normalization for WinGet:
# Robustly strip all known prefixes: vx-v0.7.8 -> 0.7.8, v0.7.8 -> 0.7.8
normalized_version="${version}"
normalized_version="${normalized_version#vx-v}"
normalized_version="${normalized_version#vx-}"
normalized_version="${normalized_version#v}"This ensures WinGet receives 0.1.0 instead of vx-v0.1.0, which resolves the issue where WinGet was showing version numbers like "x-v0.1.0".
Publishing Steps
- Check Release: Verifies the Release workflow succeeded
- Get Version: Retrieves the latest release version
- Normalize Version: Strips all tag prefixes for package managers
- Verify Release: Confirms the GitHub release exists
- Publish: Publishes to each package manager in parallel
Supported Package Managers
- WinGet (
publish-winget): Usesvedantmgoyal9/winget-releaser(also inrelease.yml) - Chocolatey (
publish-chocolatey): Downloads binary and creates.nupkg - Homebrew: Handled by cargo-dist's
publish-homebrew-formulajob inrelease.yml - Scoop (
publish-scoop): Creates JSON manifest
Testing Release Workflow Logic
The project includes tests to validate the release workflow trigger logic:
Run Workflow Tests
cargo test --test release_workflow_testsThis validates:
- Version extraction from commit messages
- Version normalization
- Release commit detection
- Workflow trigger conditions for different scenarios
Test Version Extraction
The project also includes test scripts to validate version extraction logic:
Test Version Normalization
Run the test script to verify version normalization:
bash scripts/test-winget-version.shThis tests the following transformations:
| Input | Expected Output | Description |
|---|---|---|
vx-v0.1.0 | 0.1.0 | Remove vx- and v prefix |
vx-v1.0.0 | 1.0.0 | Remove vx- and v prefix |
v0.1.0 | 0.1.0 | Remove v prefix |
v1.0.0 | 1.0.0 | Remove v prefix |
Manual Publishing
Trigger Package Publishing Manually
If you need to manually trigger package publishing:
- Go to Actions → Package Managers
- Click Run workflow
- Enter the version tag (e.g.,
vx-v0.1.0orv0.1.0) - Check Force run if needed
Publishing to Specific Package Managers
Each package manager can be published independently by running the respective job.
Troubleshooting
Release Workflow Not Triggering
Problem: After merging a release PR (e.g., "chore: release v0.6.24"), the build job doesn't run.
Cause: The original workflow logic required release-please job to succeed and create a release. However, when a release PR is merged, the release-please job is intentionally skipped to prevent recursive PR creation. This caused the get-tag job (and subsequent build jobs) to be skipped as well.
Solution: The workflow now includes additional conditions to detect release PR merges:
# Build job now triggers on release PR merges
if: |
always() &&
(
(needs.release-please.result == 'success' && needs.release-please.outputs.release_created == 'true') ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && contains(github.event.head_commit.message, 'chore: release')) # <-- Added
)When a commit message contains "chore: release", the workflow extracts the version from the message and proceeds with the build.
WinGet Version Issues
Problem: WinGet shows version like "x-v0.1.0" instead of "0.1.0"
Cause: The release-tag parameter was receiving the full tag name vx-v0.1.0 without proper normalization to remove both the vx- and v prefixes.
Solution: The workflow now includes a robust normalization step that handles all known tag formats:
- name: Normalize version for WinGet
id: normalize
run: |
version="${{ steps.version.outputs.version }}"
# Robustly strip all known prefixes
normalized_version="${version}"
normalized_version="${normalized_version#vx-v}"
normalized_version="${normalized_version#vx-}"
normalized_version="${normalized_version#v}"
echo "normalized_version=$normalized_version" >> $GITHUB_OUTPUTWinGet Duplicate Installer Entries
Problem: WinGet manifest PR contains duplicate installer entries for the same architecture.
Cause: The release contains both versioned (vx-0.7.8-x86_64-pc-windows-msvc.zip) and unversioned (vx-x86_64-pc-windows-msvc.zip) copies of the same artifact. If the installers-regex matches both, komac creates two installer entries.
Solution: Use a precise regex that only matches the cargo-dist original unversioned files:
# Only match "vx-{arch}-pc-windows-msvc.zip" (excludes "vx-0.7.8-{arch}-...")
installers-regex: 'vx-(x86_64|aarch64)-pc-windows-msvc\.zip$'WinGet Automatic Version Deletion
Problem: When publishing a new version to WinGet, an automated PR is created to delete an older version, but it incorrectly identifies the highest version as "old" and attempts to delete it.
Example: See microsoft/winget-pkgs#340410 where version 0.8.1 (the highest version at that time) was incorrectly marked for deletion.
Cause: The max-versions-to-keep parameter in vedantmgoyal9/winget-releaser action triggers automatic deletion of old versions when the version count exceeds the threshold. However, this feature has a bug that can incorrectly identify the current highest version as "old" based on timestamp comparison.
Solution: Removed the max-versions-to-keep parameter from the WinGet publishing configuration:
# REMOVED: max-versions-to-keep: 5
# This parameter causes winget-releaser to delete old versions automatically.
# However, it has a bug that can incorrectly identify the highest version
# as "old" and attempt to delete it.
# Let WinGet maintain version history naturally without automatic deletions.Best Practice: Let WinGet maintain its own version history. Package managers typically have their own policies for version management, and automatic deletion can lead to user disruption and incorrect version removal.
Verifying Release Assets
To verify release assets are available:
# Check release exists
curl -s https://api.github.com/repos/loonghao/vx/releases/tags/vx-v0.1.0
# List assets
curl -s https://api.github.com/repos/loonghao/vx/releases/tags/vx-v0.1.0 | \
jq -r '.assets[] | "\(.name) (\(.size) bytes)"'Release Commits Triggering Unnecessary CI Runs
Problem: When Release Please merges a release PR (e.g., "chore: release v0.7.6"), the resulting commit triggers CI, CodeQL, and Benchmark workflows unnecessarily, wasting significant CI resources (15+ minutes for CI, 12+ minutes for CodeQL).
Cause: The CI, CodeQL, and Benchmark workflows had no filtering mechanism to exclude release commits on push to main. Since release commits modify Cargo.toml and Cargo.lock (version bumps), even path-filtered workflows like Benchmark were triggered.
Solution: Added if conditions at the job level to skip release commits:
# Skip for release commits (applied to CI, CodeQL, and Benchmark)
if: >-
github.event_name != 'push' ||
!startsWith(github.event.head_commit.message, 'chore: release')This condition:
- Allows the job to run normally for PRs, scheduled runs, and manual dispatches
- Only skips when the event is a push AND the commit message starts with
chore: release - When the first job in a workflow is skipped, all downstream dependent jobs are automatically skipped too
Affected workflows:
.github/workflows/ci.yml-detect-changesjob (gates all downstream CI jobs).github/workflows/codeql.yml-analyzejob.github/workflows/benchmark.yml-benchmarkjob
Not affected (intentionally):
.github/workflows/release-please.yml- Must still run on release commits to detectreleases_createdand trigger the Release workflow
Best Practices
- Always use semantic versioning:
MAJOR.MINOR.PATCH - Test version extraction: Run
scripts/test-winget-version.shbefore releasing - Verify release assets: Ensure all platform binaries are uploaded
- Monitor package publishing: Check workflow status for each package manager
- Update documentation: Keep version references up to date in docs
Related Files
.github/workflows/release.yml- Main release workflow.github/workflows/release-please.yml- Release Please workflow (creates release PRs and tags).github/workflows/package-managers.yml- Package publishing workflow.github/workflows/ci.yml- CI workflow (skips release commits).github/workflows/codeql.yml- CodeQL analysis (skips release commits).github/workflows/benchmark.yml- Performance benchmarks (skips release commits)scripts/test-winget-version.sh- Version normalization testsdistribution.toml- Distribution channel configuration