diskr 0.1.15

Lightweight terminal file explorer and disk/storage manager for macOS
name: Release

on:
  push:
    tags:
      - "v*"

concurrency:
  group: release-${{ github.ref_name }}
  cancel-in-progress: false

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always

jobs:
  publish:
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v6.0.2
        with:
          fetch-depth: 0

      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - name: Read crate metadata
        id: meta
        run: |
          python - <<'PY' >> "$GITHUB_OUTPUT"
          import json
          import subprocess

          metadata = json.loads(
              subprocess.check_output(
                  ["cargo", "metadata", "--no-deps", "--format-version", "1"],
                  text=True,
              )
          )
          package = metadata["packages"][0]
          print(f"name={package['name']}")
          print(f"version={package['version']}")
          PY

      - name: Verify tag matches crate version
        env:
          CRATE_VERSION: ${{ steps.meta.outputs.version }}
        run: |
          tag_version="${GITHUB_REF_NAME#v}"
          if [ "$tag_version" != "$CRATE_VERSION" ]; then
            echo "tag $GITHUB_REF_NAME does not match Cargo.toml version $CRATE_VERSION" >&2
            exit 1
          fi

      - name: Verify release tag points to main
        run: |
          git fetch origin main
          if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
            echo "release tags must point to a commit reachable from origin/main" >&2
            exit 1
          fi

      - run: cargo fmt -- --check
      - run: cargo clippy --locked --all-targets --all-features -- -D warnings
      - run: cargo test --locked
      - run: cargo package --locked
      - run: cargo publish --dry-run --locked

      - name: Check crates.io for existing version
        id: crates
        env:
          CRATE_NAME: ${{ steps.meta.outputs.name }}
          CRATE_VERSION: ${{ steps.meta.outputs.version }}
        run: |
          status_code="$(
            curl -s -o /dev/null -w "%{http_code}" \
              -H "User-Agent: diskr-release-workflow" \
              -H "Accept: application/json" \
              "https://crates.io/api/v1/crates/$CRATE_NAME/$CRATE_VERSION"
          )"
          if [ "$status_code" = "200" ]; then
            echo "already_published=true" >> "$GITHUB_OUTPUT"
          elif [ "$status_code" = "404" ]; then
            echo "already_published=false" >> "$GITHUB_OUTPUT"
          else
            echo "unexpected crates.io status: $status_code" >&2
            exit 1
          fi

      - name: Publish to crates.io
        if: steps.crates.outputs.already_published != 'true'
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          if [ -z "$CARGO_REGISTRY_TOKEN" ]; then
            echo "CARGO_REGISTRY_TOKEN secret is not configured" >&2
            exit 1
          fi
          cargo publish --locked

      - name: Download published crate
        id: published
        env:
          CRATE_NAME: ${{ steps.meta.outputs.name }}
          CRATE_VERSION: ${{ steps.meta.outputs.version }}
        run: |
          crate_archive="$RUNNER_TEMP/${CRATE_NAME}-${CRATE_VERSION}.crate"
          for attempt in $(seq 1 12); do
            if curl -fsSL \
              -H "User-Agent: diskr-release-workflow" \
              -H "Accept: application/octet-stream" \
              "https://crates.io/api/v1/crates/$CRATE_NAME/$CRATE_VERSION/download" \
              -o "$crate_archive"; then
              echo "crate_archive=$crate_archive" >> "$GITHUB_OUTPUT"
              exit 0
            fi
            if [ "$attempt" -eq 12 ]; then
              echo "published crate did not become available on crates.io" >&2
              exit 1
            fi
            sleep 5
          done

      - name: Verify published crate provenance
        env:
          EXPECTED_SHA: ${{ github.sha }}
          CRATE_ARCHIVE: ${{ steps.published.outputs.crate_archive }}
        run: |
          python - <<'PY'
          import json
          import os
          import tarfile

          archive = os.environ["CRATE_ARCHIVE"]
          expected_sha = os.environ["EXPECTED_SHA"]

          with tarfile.open(archive, "r:gz") as tf:
              member = next(
                  (item for item in tf.getmembers() if item.name.endswith("/.cargo_vcs_info.json")),
                  None,
              )
              if member is None:
                  raise SystemExit("published crate is missing .cargo_vcs_info.json")
              with tf.extractfile(member) as fh:
                  info = json.load(fh)

          actual_sha = (info.get("git") or {}).get("sha1")
          if actual_sha != expected_sha:
              raise SystemExit(
                  f"published crate was built from {actual_sha}, expected {expected_sha}"
              )
          PY

      - name: Create GitHub release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
            echo "GitHub release $GITHUB_REF_NAME already exists; leaving it unchanged."
            exit 0
          fi

          notes_file="$RUNNER_TEMP/release-notes.md"
          gh api \
            --method POST \
            -H "Accept: application/vnd.github+json" \
            "repos/$GITHUB_REPOSITORY/releases/generate-notes" \
            -f tag_name="$GITHUB_REF_NAME" \
            -f target_commitish="$GITHUB_SHA" \
            --jq .body > "$notes_file"

          gh release create "$GITHUB_REF_NAME" \
            --repo "$GITHUB_REPOSITORY" \
            --title "$GITHUB_REF_NAME" \
            --notes-file "$notes_file"