diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 537b7ef..2b01c65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -162,9 +162,34 @@ jobs: cp "$dir/linear-release.exe" "./release-files/linear-release-${platform}.exe" fi done + + expected_assets=( + linear-release-linux-x64 + linear-release-linux-arm64 + linear-release-darwin-x64 + linear-release-darwin-arm64 + ) + for asset in "${expected_assets[@]}"; do + if [ ! -f "./release-files/$asset" ]; then + echo "::error::Missing release asset: $asset" + exit 1 + fi + done + + actual_asset_count=$(find ./release-files -maxdepth 1 -type f -name 'linear-release-*' | wc -l | tr -d ' ') + if [ "$actual_asset_count" -ne "${#expected_assets[@]}" ]; then + echo "::error::Expected ${#expected_assets[@]} release assets, found $actual_asset_count" + exit 1 + fi + + ( + cd ./release-files + LC_ALL=C sha256sum linear-release-* > checksums.txt + sha256sum --check --strict checksums.txt + ) ls -la ./release-files/ - - name: Create Release + - name: Create draft release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: tag_name: ${{ steps.tag.outputs.tag_name }} @@ -173,12 +198,60 @@ jobs: ./release-files/* generate_release_notes: true token: ${{ secrets.GITHUB_TOKEN }} - draft: false + draft: true prerelease: false + - name: Publish immutable release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ steps.tag.outputs.tag_name }} + run: gh release edit "$TAG_NAME" --draft=false + + verify-release: + name: Verify published release + needs: release + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Verify immutable metadata and assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ needs.release.outputs.tag_name }} + run: | + release=$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG_NAME}") + if [ "$(jq -r '.immutable' <<<"$release")" != "true" ]; then + echo "::error::Published release is not immutable" + exit 1 + fi + + for asset in \ + linear-release-linux-x64 \ + linear-release-linux-arm64 \ + linear-release-darwin-x64 \ + linear-release-darwin-arm64 \ + checksums.txt; do + count=$(jq --arg asset "$asset" '[.assets[] | select(.name == $asset)] | length' <<<"$release") + if [ "$count" -ne 1 ]; then + echo "::error::Expected one published $asset asset, found $count" + exit 1 + fi + done + + - name: Verify published checksums + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ needs.release.outputs.tag_name }} + run: | + mkdir release-assets + gh release download "$TAG_NAME" --dir release-assets + cd release-assets + sha256sum --check --strict checksums.txt + label-release: name: Label release with version - needs: release + needs: [release, verify-release] permissions: contents: read uses: ./.github/workflows/run-linear-release.yml diff --git a/README.md b/README.md index 0a8140f..4dbf61e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ curl -L https://github.com/linear/linear-release/releases/latest/download/linear chmod +x linear-release ``` +Releases after `v0.16.0` include a `checksums.txt` asset containing the SHA-256 digest of every executable. These releases are immutable, so their tags, executables, and checksum manifests cannot be replaced after publication. CLI releases through `v0.16.0` predate this integrity contract. + ## Quick Start ### GitHub Actions diff --git a/RELEASING.md b/RELEASING.md index f262bc4..1723dbf 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,6 +7,7 @@ This document describes how to create a new release of `linear-release`. - You must be on the `main` branch with a clean working tree, up to date with `origin/main` - The [GitHub CLI](https://cli.github.com) (`gh`) must be installed and authenticated - `pnpm` must be installed +- Immutable releases must be enabled in the repository settings ## Creating a release @@ -57,9 +58,13 @@ When a PR from a `release/*` branch is merged into `main`, the **Auto-tag releas The **Release** workflow (`.github/workflows/release.yml`) is triggered by the new tag and: -1. Builds platform-specific executables (linux-x64, darwin-x64, darwin-arm64) using Bun +1. Builds platform-specific executables (linux-x64, linux-arm64, darwin-x64, darwin-arm64) using Bun 2. Code signs and notarizes the macOS binaries -3. Creates a GitHub Release with the built binaries attached +3. Generates and validates `checksums.txt` for the final executables +4. Creates a draft GitHub Release and attaches all executables and the checksum manifest +5. Publishes the completed draft as an immutable release + +After publication, release assets cannot be changed or deleted and the associated tag cannot be moved. If a published artifact is incorrect, fix the issue and publish a new patch version instead of replacing the existing asset. ### 5. Update the GitHub action