nulid 0.10.1

Nanosecond-Precision Universally Lexicographically Sortable Identifier
Documentation
name: Release

# This workflow handles publishing crates to crates.io and creating GitHub releases.
#
# Key Features:
# - Retry-safe: Each crate checks if it's already published before attempting to publish
# - Partial failure recovery: If one crate fails, you can re-run and it will skip already-published crates
# - Sequential publishing: nulid_derive → nulid_macros → nulid (respects dependencies)
# - Always runs with 'if: always()' to allow continuation even if earlier steps were skipped
#
# Publishing Order:
# 1. publish-derive: Publishes nulid_derive (has no workspace dependencies)
# 2. publish-macros: Publishes nulid_macros (depends on derive being available)
# 3. publish: Publishes main nulid crate (depends on both derive and macros)
#
# Each job checks crates.io before publishing to avoid "already published" errors on retry.

on:
  push:
    tags:
      - "v*"

env:
  CARGO_TERM_COLOR: always

jobs:
  check-version:
    name: Check Version Consistency
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get-version.outputs.version }}
      tag-version: ${{ steps.get-tag.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - name: Get version from Cargo.toml
        id: get-version
        run: |
          VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Cargo.toml version: $VERSION"
      - name: Get version from tag
        id: get-tag
        run: |
          TAG_VERSION=${GITHUB_REF#refs/tags/v}
          echo "version=$TAG_VERSION" >> $GITHUB_OUTPUT
          echo "Tag version: $TAG_VERSION"
      - name: Verify versions match
        run: |
          if [ "${{ steps.get-version.outputs.version }}" != "${{ steps.get-tag.outputs.version }}" ]; then
            echo "Version mismatch: Cargo.toml has ${{ steps.get-version.outputs.version }}, tag has ${{ steps.get-tag.outputs.version }}"
            exit 1
          fi
          echo "Versions match: ${{ steps.get-version.outputs.version }}"

  test:
    name: Test Before Release
    runs-on: ubuntu-latest
    needs: check-version
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Rust
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: 1.88
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
      - name: Run tests
        run: make test

  publish-derive:
    name: Publish nulid_derive to crates.io
    runs-on: ubuntu-latest
    needs: [check-version, test]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Rust
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: 1.88
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-derive-${{ hashFiles('**/Cargo.lock') }}
      - name: Check if nulid_derive already published
        id: check-derive
        run: |
          VERSION="${{ needs.check-version.outputs.version }}"
          echo "Checking if nulid_derive@$VERSION is already published..."
          HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://crates.io/api/v1/crates/nulid_derive/$VERSION")
          if [ "$HTTP_STATUS" = "200" ]; then
            echo "nulid_derive@$VERSION already published, skipping"
            echo "already-published=true" >> $GITHUB_OUTPUT
          else
            echo "nulid_derive@$VERSION not published yet"
            echo "already-published=false" >> $GITHUB_OUTPUT
          fi
      - name: Publish nulid_derive to crates.io
        if: steps.check-derive.outputs.already-published == 'false'
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          cd nulid_derive && cargo publish
      - name: Wait for crates.io propagation
        if: steps.check-derive.outputs.already-published == 'false'
        run: sleep 30

  publish-macros:
    name: Publish nulid_macros to crates.io
    runs-on: ubuntu-latest
    needs: [check-version, test, publish-derive]
    if: always() && needs.publish-derive.result != 'failure'
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Rust
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: 1.88
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-macros-${{ hashFiles('**/Cargo.lock') }}
      - name: Check if nulid_macros already published
        id: check-macros
        run: |
          VERSION="${{ needs.check-version.outputs.version }}"
          echo "Checking if nulid_macros@$VERSION is already published..."
          HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://crates.io/api/v1/crates/nulid_macros/$VERSION")
          if [ "$HTTP_STATUS" = "200" ]; then
            echo "nulid_macros@$VERSION already published, skipping"
            echo "already-published=true" >> $GITHUB_OUTPUT
          else
            echo "nulid_macros@$VERSION not published yet"
            echo "already-published=false" >> $GITHUB_OUTPUT
          fi
      - name: Publish nulid_macros to crates.io
        if: steps.check-macros.outputs.already-published == 'false'
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          cd nulid_macros && cargo publish
      - name: Wait for crates.io propagation
        if: steps.check-macros.outputs.already-published == 'false'
        run: sleep 30

  publish:
    name: Publish nulid to crates.io
    runs-on: ubuntu-latest
    needs: [check-version, test, publish-derive, publish-macros]
    if: always() && needs.publish-derive.result != 'failure' && needs.publish-macros.result != 'failure'
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Rust
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: 1.88
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
      - name: Check if nulid already published
        id: check-nulid
        run: |
          VERSION="${{ needs.check-version.outputs.version }}"
          echo "Checking if nulid@$VERSION is already published..."
          HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://crates.io/api/v1/crates/nulid/$VERSION")
          if [ "$HTTP_STATUS" = "200" ]; then
            echo "nulid@$VERSION already published, skipping"
            echo "already-published=true" >> $GITHUB_OUTPUT
          else
            echo "nulid@$VERSION not published yet"
            echo "already-published=false" >> $GITHUB_OUTPUT
          fi
      - name: Publish nulid to crates.io
        if: steps.check-nulid.outputs.already-published == 'false'
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish --all-features

  check-release:
    name: Check if GitHub Release Exists
    permissions:
      contents: read
    runs-on: ubuntu-latest
    needs: check-version
    outputs:
      release-exists: ${{ steps.check-release.outputs.release-exists }}
    steps:
      - uses: actions/checkout@v4
      - name: Check if GitHub release exists
        id: check-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG_NAME="v${{ needs.check-version.outputs.version }}"
          echo "Checking if GitHub release for tag $TAG_NAME exists..."

          # Check if release exists
          HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
            -H "Authorization: token $GITHUB_TOKEN" \
            "https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG_NAME")

          if [ "$HTTP_STATUS" = "200" ]; then
            echo "GitHub release for $TAG_NAME already exists"
            echo "release-exists=true" >> $GITHUB_OUTPUT
          else
            echo "GitHub release for $TAG_NAME does not exist"
            echo "release-exists=false" >> $GITHUB_OUTPUT
          fi

  create-release:
    name: Create GitHub Release
    permissions:
      contents: write
    runs-on: ubuntu-latest
    needs:
      [check-version, check-release, publish-derive, publish-macros, publish]
    if: always() && needs.check-release.outputs.release-exists == 'false' && needs.publish-derive.result != 'failure' && needs.publish-macros.result != 'failure' && needs.publish.result != 'failure'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Generate changelog
        id: changelog
        run: |
          # Get the previous tag
          PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${{ github.ref_name }}$" | head -1)

          if [ -z "$PREV_TAG" ]; then
            echo "No previous tag found, using initial commit"
            CHANGELOG=$(git log --pretty=format:"- %s (%h)" ${{ github.ref_name }})
          else
            echo "Previous tag: $PREV_TAG"
            CHANGELOG=$(git log --pretty=format:"- %s (%h)" $PREV_TAG..${{ github.ref_name }})
          fi

          # Save changelog to file
          echo "$CHANGELOG" > changelog.txt
          echo "Generated changelog with $(echo "$CHANGELOG" | wc -l) entries"

      - name: Create Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          PRERELEASE_FLAG=""
          if [[ "${{ needs.check-version.outputs.version }}" == *"-"* ]]; then
            PRERELEASE_FLAG="--prerelease"
          fi

          gh release create "${{ github.ref_name }}" \
            --title "Release ${{ needs.check-version.outputs.version }}" \
            --notes-file changelog.txt \
            $PRERELEASE_FLAG