Release workflow (release/2025.11) #36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| run-name: Release workflow (${{ github.ref_name }}) | |
| on: | |
| push: | |
| branches: | |
| - 'release/**' | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| packages: write | |
| pull-requests: write | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| jobs: | |
| semantic-version: | |
| name: Compute Version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.semver.outputs.version }} | |
| version_tag: ${{ steps.semver.outputs.version_tag }} | |
| changed: ${{ steps.semver.outputs.changed }} | |
| changelog: ${{ steps.semver.outputs.changelog }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - id: semver | |
| uses: PaulHatch/semantic-version@v5.4.0 | |
| with: | |
| tag_prefix: "v" | |
| major_pattern: "BREAKING CHANGE:" | |
| minor_pattern: "feat:" | |
| bump_each_commit: false | |
| search_commit_body: true | |
| user_format_type: "csv" | |
| enable_prerelease_mode: true | |
| - name: Fail if no semantic changes | |
| if: steps.semver.outputs.changed != 'true' | |
| run: | | |
| echo "No semantic changes since last tag. Skipping release." | |
| exit 1 | |
| test: | |
| name: Test & Lint | |
| runs-on: ubuntu-latest | |
| needs: semantic-version | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Format check | |
| run: cargo fmt --all -- --check | |
| - name: Clippy | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| - name: Tests | |
| run: cargo test --all --locked | |
| update-version: | |
| name: Sync Cargo Version | |
| runs-on: ubuntu-latest | |
| needs: [semantic-version, test] | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Update Cargo.toml | |
| run: | | |
| sed -i.bak 's/^version = ".*"/version = "${{ needs.semantic-version.outputs.version }}"/' Cargo.toml && rm Cargo.toml.bak | |
| git config user.email "action@github.com" | |
| git config user.name "GitHub Action" | |
| git add Cargo.toml | |
| git commit -m "chore: sync version ${{ needs.semantic-version.outputs.version }}" || echo "No change" | |
| git push || true | |
| create-release: | |
| name: Create GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: [semantic-version, test, update-version] | |
| outputs: | |
| upload_url: ${{ steps.rel.outputs.upload_url }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Configure git | |
| run: | | |
| git config user.email "action@github.com" | |
| git config user.name "GitHub Action" | |
| - name: Get previous tag | |
| id: previous-tag | |
| run: | | |
| git fetch --tags | |
| PREVIOUS_TAG=$(git tag --sort=-version:refname | head -2 | tail -1) | |
| echo "tag=${PREVIOUS_TAG:-$(git rev-list --max-parents=0 HEAD)}" >> $GITHUB_OUTPUT | |
| - name: Create tag | |
| run: | | |
| git fetch --tags | |
| if git rev-parse "v${{ needs.semantic-version.outputs.version }}" >/dev/null 2>&1; then | |
| echo "Tag already exists. Skipping." | |
| else | |
| git tag -a "v${{ needs.semantic-version.outputs.version }}" -m "Release ${{ needs.semantic-version.outputs.version }}" | |
| git push origin "v${{ needs.semantic-version.outputs.version }}" | |
| fi | |
| - name: Create Release | |
| id: rel | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.semantic-version.outputs.version_tag }} | |
| name: Release ${{ needs.semantic-version.outputs.version }} | |
| generate_release_notes: true | |
| body: | | |
| ## What's Changed | |
| ${{ needs.semantic-version.outputs.changelog }} | |
| **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.previous-tag.outputs.tag }}...v${{ needs.semantic-version.outputs.version }} | |
| build-matrix: | |
| name: Build Targets | |
| needs: [semantic-version, update-version] | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| - os: macos-latest | |
| target: x86_64-apple-darwin | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.ref_name }} | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| key: ${{ matrix.target }} | |
| - name: Build | |
| run: cargo build --release --target ${{ matrix.target }} | |
| - name: Strip (Linux) | |
| if: startsWith(matrix.target, 'x86_64-unknown-linux') | |
| run: strip target/${{ matrix.target }}/release/repos | |
| - name: Strip (macOS) | |
| if: contains(matrix.target, 'apple-darwin') | |
| run: strip -x target/${{ matrix.target }}/release/repos || true | |
| - name: Package | |
| run: | | |
| staging="repos-${{ needs.semantic-version.outputs.version }}-${{ matrix.target }}" | |
| mkdir "$staging" | |
| cp target/${{ matrix.target }}/release/repos "$staging/" | |
| tar czf "$staging.tar.gz" "$staging" | |
| echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV | |
| - name: Upload Asset | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.semantic-version.outputs.version_tag }} | |
| files: ${{ env.ASSET }} | |
| build-universal-macos: | |
| name: Universal macOS | |
| runs-on: macos-latest | |
| needs: [semantic-version, update-version, build-matrix] | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.ref_name }} | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: x86_64-apple-darwin,aarch64-apple-darwin | |
| - uses: Swatinem/rust-cache@v2 | |
| - run: cargo build --release --target x86_64-apple-darwin | |
| - run: cargo build --release --target aarch64-apple-darwin | |
| - run: | | |
| lipo -create \ | |
| target/x86_64-apple-darwin/release/repos \ | |
| target/aarch64-apple-darwin/release/repos \ | |
| -output repos | |
| strip -x repos || true | |
| staging="repos-${{ needs.semantic-version.outputs.version }}-universal-apple-darwin" | |
| mkdir "$staging" | |
| mv repos "$staging/" | |
| tar czf "$staging.tar.gz" "$staging" | |
| echo "UNIVERSAL=$staging.tar.gz" >> $GITHUB_ENV | |
| - uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.semantic-version.outputs.version_tag }} | |
| files: ${{ env.UNIVERSAL }} | |
| sync-main: | |
| name: Sync version back to main | |
| runs-on: ubuntu-latest | |
| needs: [semantic-version, create-release] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Configure Git | |
| run: | | |
| git config user.name "GitHub Action" | |
| git config user.email "action@github.com" | |
| - name: Fast-forward main | |
| id: ff-main | |
| run: | | |
| RELEASE_BRANCH="${GITHUB_REF_NAME}" | |
| git fetch origin main | |
| git fetch origin "$RELEASE_BRANCH" | |
| git checkout main | |
| if git merge --ff-only "origin/$RELEASE_BRANCH"; then | |
| echo "FAST_FORWARD=1" >> $GITHUB_ENV | |
| echo "fast_forward=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "FAST_FORWARD=0" >> $GITHUB_ENV | |
| echo "fast_forward=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Update version for next release | |
| if: steps.ff-main.outputs.fast_forward == 'true' | |
| run: | | |
| # Extract current version from Cargo.toml | |
| CURRENT_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| echo "Current version: $CURRENT_VERSION" | |
| # Parse semantic version components | |
| if [[ $CURRENT_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$ ]]; then | |
| MAJOR="${BASH_REMATCH[1]}" | |
| MINOR="${BASH_REMATCH[2]}" | |
| PATCH="${BASH_REMATCH[3]}" | |
| PRERELEASE="${BASH_REMATCH[4]}" | |
| # Determine next version based on release type | |
| # If this was a minor version bump (e.g., 0.0.10 -> 0.1.0), | |
| # set next as 0.2.0-rc for the next feature | |
| if [[ $PATCH -eq 0 && -z $PRERELEASE ]]; then | |
| # Clean release, bump minor and add -rc | |
| NEXT_MINOR=$((MINOR + 1)) | |
| NEXT_VERSION="${MAJOR}.${NEXT_MINOR}.0-rc" | |
| elif [[ -n $PRERELEASE ]]; then | |
| # Already a prerelease, keep it or update | |
| NEXT_VERSION="$CURRENT_VERSION" | |
| else | |
| # Patch release, prepare next minor with -rc | |
| NEXT_MINOR=$((MINOR + 1)) | |
| NEXT_VERSION="${MAJOR}.${NEXT_MINOR}.0-rc" | |
| fi | |
| echo "Next version: $NEXT_VERSION" | |
| # Update Cargo.toml with next version | |
| sed -i.bak "s/^version = \".*\"/version = \"$NEXT_VERSION\"/" Cargo.toml && rm Cargo.toml.bak | |
| # Check if there are actual changes | |
| if git diff --quiet Cargo.toml; then | |
| echo "No version changes needed" | |
| echo "VERSION_UPDATED=false" >> $GITHUB_ENV | |
| else | |
| echo "Version updated to $NEXT_VERSION" | |
| echo "VERSION_UPDATED=true" >> $GITHUB_ENV | |
| echo "NEXT_VERSION=$NEXT_VERSION" >> $GITHUB_ENV | |
| fi | |
| else | |
| echo "Failed to parse version: $CURRENT_VERSION" | |
| exit 1 | |
| fi | |
| - name: Commit version update | |
| if: env.VERSION_UPDATED == 'true' | |
| run: | | |
| git add Cargo.toml | |
| git commit -m "chore: bump version to ${{ env.NEXT_VERSION }} [skip ci]" | |
| git push origin main | |
| echo "Version ${{ env.NEXT_VERSION }} committed to main branch" | |
| - name: Create PR (non fast-forward) | |
| if: steps.ff-main.outputs.fast_forward == 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh pr create \ | |
| --base main \ | |
| --head "${GITHUB_REF_NAME}" \ | |
| --title "chore: sync version ${{ needs.semantic-version.outputs.version }}" \ | |
| --body "Automatic version sync after release v${{ needs.semantic-version.outputs.version }}." |