motto 0.4.3

Compiler-as-a-Service: Turn Rust schema.rs into multi-platform SDK toolkits
Documentation
name: Release

on:
  push:
    tags:
      - "v*"
  workflow_dispatch:
    inputs:
      tag:
        description: "Release tag to validate (example: v0.3.1)"
        required: false
        type: string
      publish:
        description: "Publish to crates.io and create GitHub release"
        required: false
        default: false
        type: boolean

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

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: -Dwarnings
  CRATE_NAME: motto

jobs:
  verify:
    name: Verify
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

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

      - uses: Swatinem/rust-cache@v2

      - name: Run quality gates
        run: |
          cargo check --all-features
          cargo fmt --all --check
          cargo clippy --all-features -- -D warnings
          cargo test --all-features
          cargo test --all-features -- --ignored

  validate_version:
    name: Validate Tag Version
    runs-on: ubuntu-latest
    needs: verify
    outputs:
      tag: ${{ steps.resolve.outputs.tag }}
      version: ${{ steps.compare.outputs.version }}
    steps:
      - uses: actions/checkout@v4

      - name: Resolve tag
        id: resolve
        run: |
          if [ "${{ github.event_name }}" = "push" ]; then
            tag="${GITHUB_REF_NAME}"
          else
            tag="${{ github.event.inputs.tag }}"
          fi

          if [ -z "${tag}" ]; then
            echo "No release tag available. For manual runs, provide the 'tag' input."
            exit 1
          fi

          echo "tag=${tag}" >> "${GITHUB_OUTPUT}"

      - name: Compare tag and Cargo.toml version
        id: compare
        env:
          TAG: ${{ steps.resolve.outputs.tag }}
        run: |
          if [[ ! "${TAG}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
            echo "Tag must follow vMAJOR.MINOR.PATCH format. Got: ${TAG}"
            exit 1
          fi

          tag_version="${BASH_REMATCH[1]}"

          cargo_version="$(python -c 'import tomllib;print(tomllib.load(open("Cargo.toml","rb"))["package"]["version"])')"

          if [ "${tag_version}" != "${cargo_version}" ]; then
            echo "Tag version (${tag_version}) does not match Cargo.toml version (${cargo_version})."
            exit 1
          fi

          echo "version=${cargo_version}" >> "${GITHUB_OUTPUT}"

  ci_gate:
    name: Gate On CI
    runs-on: ubuntu-latest
    needs: validate_version
    permissions:
      actions: read
      contents: read
    env:
      RELEASE_TAG: ${{ needs.validate_version.outputs.tag }}
    steps:
      - name: Resolve release commit from tag
        id: release_sha
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
        run: |
          python - <<'PY'
          import json
          import os

          repo = os.environ["REPO"]
          tag = os.environ["RELEASE_TAG"]

          def gh_api(path: str):
              import urllib.request

              req = urllib.request.Request(
                  f"https://api.github.com{path}",
                  headers={
                      "Authorization": f"Bearer {os.environ['GH_TOKEN']}",
                      "Accept": "application/vnd.github+json",
                      "X-GitHub-Api-Version": "2022-11-28",
                  },
              )
              with urllib.request.urlopen(req) as resp:
                  return json.loads(resp.read())

          ref = gh_api(f"/repos/{repo}/git/ref/tags/{tag}")
          obj = ref["object"]
          sha = obj["sha"]

          # Annotated tags point to a tag object; peel to commit.
          if obj["type"] == "tag":
              tag_obj = gh_api(f"/repos/{repo}/git/tags/{sha}")
              sha = tag_obj["object"]["sha"]

          with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
              f.write(f"sha={sha}\n")
          print(f"Resolved {tag} -> {sha}")
          PY

      - name: Wait for CI workflow success on release commit
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          RELEASE_SHA: ${{ steps.release_sha.outputs.sha }}
        run: |
          python - <<'PY'
          import json
          import os
          import sys
          import time

          repo = os.environ["REPO"]
          sha = os.environ["RELEASE_SHA"]
          deadline = time.time() + 3600  # 60 minutes

          def gh_api(path: str):
              import urllib.request

              req = urllib.request.Request(
                  f"https://api.github.com{path}",
                  headers={
                      "Authorization": f"Bearer {os.environ['GH_TOKEN']}",
                      "Accept": "application/vnd.github+json",
                      "X-GitHub-Api-Version": "2022-11-28",
                  },
              )
              with urllib.request.urlopen(req) as resp:
                  return json.loads(resp.read())

          while True:
              runs = gh_api(f"/repos/{repo}/actions/runs?head_sha={sha}&per_page=100")["workflow_runs"]
              ci_runs = [r for r in runs if r.get("name") == "CI"]

              if ci_runs:
                  run = max(ci_runs, key=lambda r: r["run_number"])
                  status = run.get("status")
                  conclusion = run.get("conclusion")
                  url = run.get("html_url")
                  print(f"CI run: status={status} conclusion={conclusion} url={url}")

                  if status == "completed":
                      if conclusion == "success":
                          print("CI gate passed.")
                          break
                      print("CI gate failed: CI workflow did not succeed.")
                      sys.exit(1)
              else:
                  print(f"No CI workflow run found yet for {sha}.")

              if time.time() > deadline:
                  print("Timed out waiting for CI workflow completion.")
                  sys.exit(1)

              time.sleep(20)
          PY

  publish:
    name: Publish Crate
    runs-on: ubuntu-latest
    needs: [validate_version, ci_gate]
    if: github.event_name == 'push' || github.event.inputs.publish == 'true'
    env:
      CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2

      - name: Ensure crates.io token exists
        run: |
          if [ -z "${CARGO_REGISTRY_TOKEN}" ]; then
            echo "CARGO_REGISTRY_TOKEN is not set"
            exit 1
          fi

      - name: Check if version already exists
        id: crate_check
        env:
          VERSION: ${{ needs.validate_version.outputs.version }}
        run: |
          status="$(curl -s -o /tmp/crate.json -w "%{http_code}" "https://crates.io/api/v1/crates/${CRATE_NAME}/${VERSION}")"
          if [ "${status}" = "200" ]; then
            echo "Version ${VERSION} is already published."
            echo "already_published=true" >> "${GITHUB_OUTPUT}"
          else
            echo "Version ${VERSION} is not published yet."
            echo "already_published=false" >> "${GITHUB_OUTPUT}"
          fi

      - name: Publish to crates.io
        if: steps.crate_check.outputs.already_published != 'true'
        run: cargo publish --locked

  github_release:
    name: Create GitHub Release
    runs-on: ubuntu-latest
    needs: [validate_version, publish]
    if: github.event_name == 'push' || github.event.inputs.publish == 'true'
    steps:
      - name: Create release with auto notes
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.validate_version.outputs.tag }}
          generate_release_notes: true