zvidlib 0.5.0

Frame-accurate video and audio I/O for native and WebAssembly applications
Documentation
name: Finalize release

# Builds and publishes a release whose tag already exists. The Release
# workflow (`release.yml`) pushes the tag and then dispatches this one, since
# a tag pushed with `GITHUB_TOKEN` starts no other workflow; a tag pushed by
# hand starts it directly. It creates the GitHub release and then starts
# Publish crates (`publish-crates.yml`) to upload the crates to crates.io,
# without waiting for crates.io's rate limits (#649).
on:
  push:
    tags: ['v*']
  workflow_dispatch:
    inputs:
      tag:
        description: Release tag, for example v1.0.0
        required: true
        type: string

permissions:
  actions: write
  contents: write

jobs:
  package:
    name: Package release assets
    runs-on: ubuntu-latest
    steps:
      - name: Select the release tag
        env:
          INPUT_TAG: ${{ inputs.tag }}
        run: |
          set -euo pipefail
          if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
            tag="$INPUT_TAG"
          else
            tag="$GITHUB_REF_NAME"
          fi
          [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "invalid release tag: $tag" >&2; exit 1; }
          echo "TAG=$tag" >> "$GITHUB_ENV"
      - uses: actions/checkout@v4
        with:
          ref: refs/tags/${{ env.TAG }}
      - name: Install Rust toolchain
        run: rustup show
      - name: Cache cargo registry and build artifacts
        uses: Swatinem/rust-cache@v2
        with:
          cache-on-failure: true
      - name: Install wasm32 target
        run: rustup target add wasm32-unknown-unknown
      - name: Install native audio dependencies
        timeout-minutes: 15
        run: python3 .github/scripts/apt_install.py libasound2-dev
      - name: Install wasm-pack
        env:
          WASM_PACK_VERSION: '0.15.0'
        run: |
          target="wasm-pack-v${WASM_PACK_VERSION}-x86_64-unknown-linux-musl"
          mkdir -p "$RUNNER_TEMP/wasm-pack"
          curl -sSfL "https://github.com/rustwasm/wasm-pack/releases/download/v${WASM_PACK_VERSION}/${target}.tar.gz" \
            | tar -xz --strip-components=1 -C "$RUNNER_TEMP/wasm-pack" "${target}/wasm-pack"
          echo "$RUNNER_TEMP/wasm-pack" >> "$GITHUB_PATH"
      # Every workspace package is versioned in lockstep with `zvidlib` (#604),
      # so each one has to match the tag, not just the first one listed.
      - name: Verify every package version matches the release tag
        run: |
          mismatched="$(cargo metadata --no-deps --format-version 1 \
            | jq -r --arg version "${TAG#v}" '.packages[] | select(.version != $version) | .name')"
          if [ -n "$mismatched" ]; then
            echo "these packages are not at ${TAG#v}: $mismatched"
            exit 1
          fi
      - name: Build release assets
        run: |
          mkdir release
          # `zvidlib` and every crate it is built from, verified together so
          # each one builds against the others as they will be published.
          # The benchmark helpers are development-only and never published.
          cargo package --workspace --exclude zvidlib-bench-support --locked
          cp target/package/*.crate release/
          wasm-pack build --release --target web --out-dir pkg --features web
          (cd pkg && npm pack --pack-destination ../release)
          mv release/zvidlib-*.tgz "release/zvidlib-web-${TAG}.tgz"
      # The notes are the tag's `CHANGELOG.md` section, or `## Unreleased` for
      # a tag pushed without the Release workflow promoting it.
      - name: Extract the release notes
        run: |
          set -euo pipefail
          extract_section() {
            awk -v heading="$1" '
              index($0, heading) == 1 { in_section = 1; next }
              in_section && /^## / { exit }
              in_section { print }
            ' CHANGELOG.md | sed -e '/./,$!d' | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}'
          }
          notes="$RUNNER_TEMP/release-notes.md"
          extract_section "## ${TAG#v} - " > "$notes"
          if [ ! -s "$notes" ]; then
            extract_section "## Unreleased" > "$notes"
          fi
          [ -s "$notes" ] || { echo "CHANGELOG.md has no entry for $TAG" >&2; exit 1; }
          echo "NOTES_FILE=$notes" >> "$GITHUB_ENV"
      # A re-run after the release was created replaces its assets rather
      # than failing on the release that already exists.
      - name: Create the GitHub release with its assets
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          if gh release view "$TAG" > /dev/null 2>&1; then
            gh release upload "$TAG" release/* --clobber
          else
            gh release create "$TAG" release/* --verify-tag --title "zvidlib ${TAG}" --notes-file "$NOTES_FILE"
          fi
      # Publish crates starts only after `cargo package` has verified every
      # crate, since it uploads them with `--no-verify`, and after the GitHub
      # release exists, so a release that fails here never reaches crates.io.
      # It is not awaited: crates.io's limit on new crates can hold it for
      # hours. A re-run dispatches it again, which skips the crates already
      # uploaded and queues behind a publish still running for the tag.
      - name: Start Publish crates
        env:
          GH_TOKEN: ${{ github.token }}
        run: gh workflow run publish-crates.yml --repo "$GITHUB_REPOSITORY" --ref "$TAG" -f tag="$TAG"