Skip to content

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 ManagerExpected FormatExampleNotes
WinGet0.1.00.1.0Without v prefix, normalized from vx-v0.1.0
Chocolatey0.1.00.1.0Without v prefix
Homebrew0.1.00.1.0Without v prefix
Scoop0.1.00.1.0Without 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:

  1. Release-Please: Creates release PRs and tags
  2. Binary Building: Builds binaries for all supported platforms
  3. Asset Upload: Uploads binaries to the GitHub release

Version Extraction:

bash
# 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:

ScenarioRelease-Please JobBuild JobNotes
Regular push (feat, fix, etc.)RunsTriggered if release createdNormal development flow
Release PR merge (chore: release vX.Y.Z)SkippedTriggeredExtracts version from commit message
Dependabot PR (chore(deps): bump...)SkippedNot triggeredPrevents duplicate builds
Manual workflow dispatchSkippedTriggeredEmergency/manual releases

Key Logic:

yaml
# 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:

  1. WinGet is published immediately after release assets are uploaded (no workflow_run delay)
  2. The release tag is known exactly (from plan job output), avoiding API version lookup issues
  3. Pre-release filtering is handled consistently with other publish jobs

Installer regex:

yaml
# 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:

bash
# 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

  1. Check Release: Verifies the Release workflow succeeded
  2. Get Version: Retrieves the latest release version
  3. Normalize Version: Strips all tag prefixes for package managers
  4. Verify Release: Confirms the GitHub release exists
  5. Publish: Publishes to each package manager in parallel

Supported Package Managers

  • WinGet (publish-winget): Uses vedantmgoyal9/winget-releaser (also in release.yml)
  • Chocolatey (publish-chocolatey): Downloads binary and creates .nupkg
  • Homebrew: Handled by cargo-dist's publish-homebrew-formula job in release.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

bash
cargo test --test release_workflow_tests

This 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
bash scripts/test-winget-version.sh

This tests the following transformations:

InputExpected OutputDescription
vx-v0.1.00.1.0Remove vx- and v prefix
vx-v1.0.01.0.0Remove vx- and v prefix
v0.1.00.1.0Remove v prefix
v1.0.01.0.0Remove v prefix

Manual Publishing

Trigger Package Publishing Manually

If you need to manually trigger package publishing:

  1. Go to ActionsPackage Managers
  2. Click Run workflow
  3. Enter the version tag (e.g., vx-v0.1.0 or v0.1.0)
  4. 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:

yaml
# 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:

yaml
- 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_OUTPUT

WinGet 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:

yaml
# 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:

yaml
# 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:

bash
# 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:

yaml
# 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-changes job (gates all downstream CI jobs)
  • .github/workflows/codeql.yml - analyze job
  • .github/workflows/benchmark.yml - benchmark job

Not affected (intentionally):

  • .github/workflows/release-please.yml - Must still run on release commits to detect releases_created and trigger the Release workflow

Best Practices

  1. Always use semantic versioning: MAJOR.MINOR.PATCH
  2. Test version extraction: Run scripts/test-winget-version.sh before releasing
  3. Verify release assets: Ensure all platform binaries are uploaded
  4. Monitor package publishing: Check workflow status for each package manager
  5. Update documentation: Keep version references up to date in docs
  • .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 tests
  • distribution.toml - Distribution channel configuration

Released under the MIT License.