diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ec7beb..c099ee5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,8 +4,7 @@ on: push: branches: - main - tags: - - "v[0-9]+.*" + workflow_dispatch: {} permissions: contents: write @@ -17,37 +16,48 @@ env: jobs: # ------------------------------------------------------------------ - # 1. Create a draft GitHub release (tags only) + # 0. Detect whether this is a release run # ------------------------------------------------------------------ - create-release: - name: Create release - if: startsWith(github.ref, 'refs/tags/') + check-release: + name: Check release runs-on: ubuntu-latest outputs: - tag: ${{ steps.tag.outputs.tag }} + is_release: ${{ steps.check.outputs.is_release }} + version: ${{ steps.check.outputs.version }} + version_major: ${{ steps.check.outputs.version_major }} + version_minor: ${{ steps.check.outputs.version_minor }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2 + with: + fetch-depth: 1 + fetch-tags: true - - name: Get tag - id: tag - run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - - - name: Verify tag matches Cargo.toml version + - name: Detect release + id: check run: | - TAG="${{ steps.tag.outputs.tag }}" - CARGO_VERSION="v$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')" - if [ "$TAG" != "$CARGO_VERSION" ]; then - echo "::error::Tag $TAG does not match Cargo.toml version $CARGO_VERSION" - exit 1 + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="v$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')" + RAW="${VERSION#v}" + MAJOR=$(echo "$RAW" | cut -d. -f1) + MINOR="${MAJOR}.$(echo "$RAW" | cut -d. -f2)" + echo "is_release=true" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "version_major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "version_minor=$MINOR" >> "$GITHUB_OUTPUT" + + if git tag -l "$VERSION" | grep -q .; then + echo "::error::Tag $VERSION already exists" + exit 1 + fi + echo "Detected release: $VERSION" + else + echo "is_release=false" >> "$GITHUB_OUTPUT" + echo "version=" >> "$GITHUB_OUTPUT" + echo "Not a release run" fi - - name: Create draft release - env: - GH_TOKEN: ${{ github.token }} - run: gh release create "${{ steps.tag.outputs.tag }}" --draft --verify-tag --title "${{ steps.tag.outputs.tag }}" - # ------------------------------------------------------------------ - # 2. Build binaries for each target (always) + # 1. Build binaries for each target (always) # ------------------------------------------------------------------ build-binaries: name: Build ${{ matrix.name }} @@ -148,17 +158,17 @@ jobs: echo "archive=${ARCHIVE}.tar.gz" >> "$GITHUB_OUTPUT" echo "checksum=${ARCHIVE}.tar.gz.sha256" >> "$GITHUB_OUTPUT" - - name: Upload to release - if: startsWith(github.ref, 'refs/tags/') - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release upload "${GITHUB_REF#refs/tags/}" \ - "${{ steps.package.outputs.archive }}" \ - "${{ steps.package.outputs.checksum }}" + - name: Upload build artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # ratchet:actions/upload-artifact@v7.0.0 + with: + name: binary-${{ matrix.name }} + path: | + ${{ steps.package.outputs.archive }} + ${{ steps.package.outputs.checksum }} + retention-days: 1 # ------------------------------------------------------------------ - # 3. Build per-platform Docker images on native runners + # 2. Build per-platform Docker images on native runners # ------------------------------------------------------------------ docker-build: name: Docker ${{ matrix.platform }} @@ -218,10 +228,11 @@ jobs: retention-days: 1 # ------------------------------------------------------------------ - # 3a. Build SIMD-optimized Docker images (single-arch each) + # 2a. Build SIMD-optimized Docker images (single-arch each) # ------------------------------------------------------------------ docker-build-simd: name: Docker ${{ matrix.name }} + needs: [check-release] runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -267,9 +278,9 @@ jobs: with: images: ghcr.io/${{ env.IMAGE_NAME }} tags: | - type=semver,pattern={{version}}-${{ matrix.tag_suffix }},enable=${{ startsWith(github.ref, 'refs/tags/') }} - type=raw,value=latest-${{ matrix.tag_suffix }},enable=${{ startsWith(github.ref, 'refs/tags/') }} - type=raw,value=dev-${{ matrix.tag_suffix }},enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=${{ needs.check-release.outputs.version }}-${{ matrix.tag_suffix }},enable=${{ needs.check-release.outputs.is_release == 'true' }} + type=raw,value=latest-${{ matrix.tag_suffix }},enable=${{ needs.check-release.outputs.is_release == 'true' }} + type=raw,value=dev-${{ matrix.tag_suffix }} - name: Build and push uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # ratchet:docker/build-push-action@v7.0.0 @@ -285,11 +296,11 @@ jobs: cache-to: type=gha,scope=${{ matrix.name }},mode=max # ------------------------------------------------------------------ - # 3b. Merge per-platform images into a multi-arch manifest + # 2b. Merge per-platform images into a multi-arch manifest # ------------------------------------------------------------------ docker-merge: name: Docker merge - needs: docker-build + needs: [check-release, docker-build] runs-on: ubuntu-latest steps: - name: Lowercase image name @@ -308,11 +319,11 @@ jobs: with: images: ghcr.io/${{ env.IMAGE_NAME }} tags: | - type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/') }} - type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/') }} - type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/') }} - type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} - type=raw,value=dev,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=${{ needs.check-release.outputs.version }},enable=${{ needs.check-release.outputs.is_release == 'true' }} + type=raw,value=${{ needs.check-release.outputs.version_minor }},enable=${{ needs.check-release.outputs.is_release == 'true' }} + type=raw,value=${{ needs.check-release.outputs.version_major }},enable=${{ needs.check-release.outputs.is_release == 'true' }} + type=raw,value=latest,enable=${{ needs.check-release.outputs.is_release == 'true' }} + type=raw,value=dev - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # ratchet:docker/setup-buildx-action@v4.0.0 @@ -332,12 +343,69 @@ jobs: $(printf 'ghcr.io/${{ env.IMAGE_NAME }}@sha256:%s ' *) # ------------------------------------------------------------------ - # 4. Publish to crates.io (tags only) + # 3. Create tag and draft release (release only, after all builds) + # ------------------------------------------------------------------ + create-tag-and-release: + name: Create tag and release + if: needs.check-release.outputs.is_release == 'true' + needs: [check-release, build-binaries, docker-merge, docker-build-simd] + runs-on: ubuntu-latest + outputs: + tag: ${{ needs.check-release.outputs.version }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2 + + - name: Create tag + run: | + git tag "${{ needs.check-release.outputs.version }}" + git push origin "${{ needs.check-release.outputs.version }}" + + - name: Extract changelog + run: | + RAW="${{ needs.check-release.outputs.version }}" + RAW="${RAW#v}" + awk '/^## \[Version '"$RAW"'\]/{found=1; next} /^## \[/{if(found) exit} found' CHANGELOG.md > /tmp/release-notes.md + + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ needs.check-release.outputs.version }}" \ + --target "${{ github.sha }}" \ + --title "RustQC ${{ needs.check-release.outputs.version }}" \ + --notes-file /tmp/release-notes.md + + # ------------------------------------------------------------------ + # 4. Upload binaries to the release + # ------------------------------------------------------------------ + upload-binaries: + name: Upload binaries + if: needs.check-release.outputs.is_release == 'true' + needs: [check-release, create-tag-and-release, build-binaries] + runs-on: ubuntu-latest + steps: + - name: Download all binary artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # ratchet:actions/download-artifact@v8.0.1 + with: + pattern: binary-* + path: /tmp/binaries + merge-multiple: true + + - name: Upload to release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "${{ needs.create-tag-and-release.outputs.tag }}" \ + /tmp/binaries/*.tar.gz \ + /tmp/binaries/*.sha256 + + # ------------------------------------------------------------------ + # 5. Publish to crates.io (release only, after all builds) # ------------------------------------------------------------------ publish-crate: name: Publish to crates.io - if: startsWith(github.ref, 'refs/tags/') - needs: [create-release, build-binaries] + if: needs.check-release.outputs.is_release == 'true' + needs: [check-release, create-tag-and-release] runs-on: ubuntu-latest permissions: id-token: write @@ -353,18 +421,3 @@ jobs: run: cargo publish --no-verify env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} - - # ------------------------------------------------------------------ - # 5. Publish the release (tags only) - # ------------------------------------------------------------------ - publish-release: - name: Publish release - if: startsWith(github.ref, 'refs/tags/') - needs: [create-release, build-binaries, docker-merge, docker-build-simd, publish-crate] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2 - - name: Publish release - env: - GH_TOKEN: ${{ github.token }} - run: gh release edit "${{ needs.create-release.outputs.tag }}" --draft=false