zvidlib 0.4.1

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.
on:
  push:
    tags: ['v*']
  workflow_dispatch:
    inputs:
      tag:
        description: Release tag, for example v1.0.0
        required: true
        type: string

permissions:
  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"
      # `publish_crates.py` uploads every crate `cargo package` just verified,
      # one at a time in dependency order (#610), pacing them to crates.io's
      # rate limits and waiting out a 429 rather than failing (#637). It runs
      # before the GitHub release is created so a re-run of a failed job
      # repeats both: a crate already on crates.io at this version is skipped,
      # so a re-run resumes after a partial publish. A tag pushed before the
      # token secret exists still gets its GitHub release.
      - name: Publish the crates to crates.io
        # A first release of a dozen new crates waits about ten minutes
        # between uploads once crates.io's burst of five is spent.
        timeout-minutes: 240
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          if [ -z "$CARGO_REGISTRY_TOKEN" ]; then
            echo "::notice::CARGO_REGISTRY_TOKEN is not set, so no crate was published to crates.io."
            exit 0
          fi
          python3 .github/scripts/publish_crates.py --version "${TAG#v}"
      # The notes are the tag's `CHANGELOG.md` section, or `## Unreleased` for
      # a tag pushed without the Release workflow promoting it. They are
      # written outside the checkout, which `cargo publish` needs clean.
      - 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