diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8107bb2..b323e30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,19 +2,71 @@ name: Release on: push: + branches: [main] tags: ["v*"] permissions: contents: write id-token: write + pull-requests: write env: CARGO_TERM_COLOR: always BINARY_NAME: cell jobs: + release-plz-release: + name: Release-plz release + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + pull-requests: read + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: dtolnay/rust-toolchain@stable + + - name: Publish release + uses: release-plz/action@v0.5.128 + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + release-plz-pr: + name: Release-plz PR + if: github.ref == 'refs/heads/main' + needs: release-plz-release + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: dtolnay/rust-toolchain@stable + + - name: Open or update release PR + uses: release-plz/action@v0.5.128 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build: name: Build (${{ matrix.target }}) + if: startsWith(github.ref, 'refs/tags/v') runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -55,29 +107,41 @@ jobs: key: ${{ matrix.target }} - name: Build - run: cargo build --release --target ${{ matrix.target }} -p cell-sheet-tui + run: cargo build --locked --release --target ${{ matrix.target }} -p cell-sheet-tui - name: Package (unix) if: matrix.archive == 'tar.gz' run: | - cd target/${{ matrix.target }}/release - tar czf ../../../${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}.tar.gz ${{ env.BINARY_NAME }} + mkdir -p dist/${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }} + cp target/${{ matrix.target }}/release/${{ env.BINARY_NAME }} dist/${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}/ + cp README.md CHANGELOG.md LICENSE dist/${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}/ + tar czf ${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}.tar.gz -C dist ${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }} + shasum -a 256 ${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}.tar.gz > ${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}.tar.gz.sha256 - name: Package (windows) if: matrix.archive == 'zip' shell: pwsh run: | - Compress-Archive -Path "target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}.exe" -DestinationPath "${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}.zip" + $dist = "dist/${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}" + New-Item -ItemType Directory -Force -Path $dist + Copy-Item "target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}.exe" $dist + Copy-Item README.md,CHANGELOG.md,LICENSE $dist + Compress-Archive -Path $dist -DestinationPath "${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}.zip" + $hash = (Get-FileHash "${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}.zip" -Algorithm SHA256).Hash.ToLower() + "$hash ${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}.zip" | Out-File -Encoding ascii "${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}.zip.sha256" - uses: actions/upload-artifact@v7 with: name: ${{ env.BINARY_NAME }}-${{ matrix.target }} path: ${{ env.BINARY_NAME }}-${{ github.ref_name }}-${{ matrix.target }}.* - release: - name: Create Release + upload-artifacts: + name: Upload release artifacts + if: startsWith(github.ref, 'refs/tags/v') needs: build runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v6 @@ -86,32 +150,22 @@ jobs: path: artifacts merge-multiple: true - - name: Create GitHub Release - run: gh release create ${{ github.ref_name }} artifacts/* --generate-notes + - name: Wait for release-plz draft + run: | + for attempt in {1..30}; do + if gh release view "${{ github.ref_name }}" >/dev/null 2>&1; then + exit 0 + fi + sleep 2 + done + echo "release ${{ github.ref_name }} was not created by release-plz" >&2 + exit 1 env: GH_TOKEN: ${{ github.token }} - publish: - name: Publish to crates.io - needs: release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@stable - - - uses: rust-lang/crates-io-auth-action@v1.0.4 - id: auth - - - name: Publish cell-sheet-core - run: cargo publish -p cell-sheet-core - env: - CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} - - - name: Wait for crates.io index - run: sleep 30 - - - name: Publish cell-sheet-tui - run: cargo publish -p cell-sheet-tui + - name: Upload release artifacts + run: | + gh release upload "${{ github.ref_name }}" artifacts/* --clobber + gh release edit "${{ github.ref_name }}" --draft=false env: - CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + GH_TOKEN: ${{ github.token }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eebecde..83cbe57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -190,10 +190,12 @@ for broader proposals. ## Releasing Releases are cut by maintainers. The process is documented in the -[Releasing section of the README](README.md#releasing): bump the workspace -version in `Cargo.toml`, move `Unreleased` entries into the new version -section in `CHANGELOG.md`, commit, tag `vX.Y.Z`, and push the tag. The -release workflow handles binaries and `crates.io` publication. +[Releasing section of the README](README.md#releasing) and +[`RELEASE.md`](RELEASE.md): `release-plz` opens a release PR with the next +workspace version and changelog updates. Merging that PR publishes the crates +via trusted publishing, creates the `vX.Y.Z` tag, and lets the release workflow +attach binaries and checksums to the GitHub Release. Maintainers should not +push release tags manually during the normal flow. ## License diff --git a/README.md b/README.md index cc27f18..62fd473 100644 --- a/README.md +++ b/README.md @@ -276,20 +276,19 @@ The core library is independent of the terminal UI and can be tested without a t ## Releasing -1. Update the version in `[Cargo.toml](Cargo.toml)` (workspace version) -2. Update `[CHANGELOG.md](CHANGELOG.md)` with the new version's changes -3. Commit: `git commit -am "release: bump to vX.Y.Z"` -4. Tag and push: - ```sh - git tag vX.Y.Z - git push origin main --tags - ``` - -Pushing a `v*` tag triggers the [release workflow](.github/workflows/release.yml), which: - -- Builds binaries for Linux (x86_64, aarch64), macOS (x86_64, aarch64), and Windows (x86_64) -- Creates a GitHub Release with the binaries attached -- Publishes `cell-sheet-core` and `cell-sheet-tui` to [crates.io](https://crates.io) via trusted publishing +Releases are automated with [release-plz](https://release-plz.dev/) from the +[release workflow](.github/workflows/release.yml): + +- Pushes to `main` open or update a release PR with the next version and + changelog updates. +- Merging the release PR publishes `cell-sheet-core` and `cell-sheet-tui` to + [crates.io](https://crates.io) via trusted publishing, creates the `vX.Y.Z` + tag, and creates a draft GitHub Release. +- The `vX.Y.Z` tag then builds binaries for Linux (x86_64, aarch64), macOS + (x86_64, aarch64), and Windows (x86_64), uploads archives and SHA256 + checksums, and publishes the GitHub Release. + +See [RELEASE.md](RELEASE.md) for maintainer instructions and failure handling. ## Contributing diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..6e3318e --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,59 @@ +# Releasing + +Releases are automated with +[release-plz](https://release-plz.dev/) from +[`.github/workflows/release.yml`](.github/workflows/release.yml). + +The `release.yml` workflow is configured as the crates.io trusted-publishing +provenance source for both `cell-sheet-core` and `cell-sheet-tui`. Do not +rename or replace it without updating the trusted-publishing configuration on +crates.io for both crates. + +## Normal Release Flow + +1. Land user-visible changes on `main` with conventional commit messages. +2. The release workflow opens or updates a `release-plz` pull request with: + - the next workspace version in `Cargo.toml` + - the matching `CHANGELOG.md` release section + - any lockfile changes needed for the release +3. Review the release PR and merge it when the changelog and version are right. +4. After the merge, `release-plz` publishes unpublished crates to crates.io via + trusted publishing, creates a single `vX.Y.Z` tag, and creates a draft + GitHub Release. +5. The same workflow runs again for the new `vX.Y.Z` tag, builds release + binaries, uploads archives plus `.sha256` checksum files, and publishes the + draft GitHub Release. + +No manual tag push is needed. + +## Workflow Ownership + +`release-plz` owns: + +- release PR creation +- version and changelog updates +- crates.io publishing +- the `vX.Y.Z` tag +- the draft GitHub Release + +The tag-triggered artifact jobs in `release.yml` own: + +- Linux, macOS, and Windows binary builds +- release archives +- SHA256 checksum files +- publishing the draft GitHub Release after artifacts are attached + +The workspace has two public crates but one product release, so +[`release-plz.toml`](release-plz.toml) is only configuration for the +`release-plz` jobs in `release.yml`: it disables per-crate tags by default and +enables a single `v{{ version }}` tag/release for `cell-sheet-tui`. + +## Failure Handling + +- If crates.io publishing fails, fix the issue and rerun the failed workflow. + Do not create a manual tag for the same version. +- If artifact building fails after crates.io publishing succeeds, the GitHub + Release remains a draft. Fix the workflow or code, rerun the tag workflow, + and let it upload artifacts and publish the draft. +- If the release PR has the wrong changelog or version, edit the source commits + or `release-plz.toml` configuration and let the release PR update. diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 0000000..53ba6a3 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,16 @@ +[workspace] +release_always = false +changelog_update = false +git_release_enable = false +git_tag_enable = false + +[[package]] +name = "cell-sheet-tui" +changelog_update = true +changelog_path = "./CHANGELOG.md" +changelog_include = ["cell-sheet-core"] +git_tag_name = "v{{ version }}" +git_tag_enable = true +git_release_enable = true +git_release_name = "v{{ version }}" +git_release_draft = true