a3s-acl 0.3.0

Parser and generator for the A3S Agent Configuration Language
Documentation
name: Release

on:
  push:
    tags:
      - "v*"
  workflow_dispatch:
    inputs:
      tag_name:
        description: "Tag name to release (e.g. v0.2.0)"
        required: false
        type: string

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always

jobs:
  # ───────────────────────────────────────────────
  # Gate: CI checks must pass before publishing
  # ───────────────────────────────────────────────
  ci:
    name: CI Checks
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Format check
        run: cargo fmt --all -- --check

      - name: Clippy
        run: cargo clippy --workspace --lib --bins -- -D warnings

      - name: Tests
        run: cargo test --workspace --lib

  # ───────────────────────────────────────────────
  # Publish acl to crates.io
  # ───────────────────────────────────────────────
  publish-crate:
    name: Publish to crates.io
    needs: [ci]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}
        run: |
          VERSION=$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')
          if curl -fsSL "https://crates.io/api/v1/crates/a3s-acl/${VERSION}" >/dev/null 2>&1; then
            echo "a3s-acl ${VERSION} already exists on crates.io, skipping publish."
            exit 0
          fi
          set +e
          OUTPUT=$(cargo publish --allow-dirty 2>&1)
          STATUS=$?
          set -e
          echo "$OUTPUT"
          if [ "$STATUS" -ne 0 ]; then
            if echo "$OUTPUT" | grep -Eqi "already (uploaded|exists)"; then
              echo "a3s-acl ${VERSION} was already published, treating as success."
              exit 0
            fi
            exit "$STATUS"
          fi

  # ───────────────────────────────────────────────
  # Publish Node SDK to npm
  # ───────────────────────────────────────────────
  publish-node:
    name: Publish Node SDK to npm
    needs: [ci]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          registry-url: https://registry.npmjs.org

      - name: Install dependencies
        working-directory: sdk/node
        run: npm install

      - name: TypeScript check
        working-directory: sdk/node
        run: npx tsc --noEmit

      - name: Publish to npm
        working-directory: sdk/node
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          set -euo pipefail
          NAME="$(node -p "require('./package.json').name")"
          VERSION="$(node -p "require('./package.json').version")"
          if npm view "${NAME}@${VERSION}" version >/dev/null 2>&1; then
            echo "${NAME}@${VERSION} already exists on npm, skipping."
            exit 0
          fi

          set +e
          OUTPUT="$(npm publish --access public 2>&1)"
          STATUS=$?
          set -e
          echo "$OUTPUT"

          if [ "$STATUS" -ne 0 ]; then
            if echo "$OUTPUT" | grep -Eqi "previously published|cannot publish over|already exists"; then
              echo "${NAME}@${VERSION} was already published, treating as success."
              exit 0
            fi
            exit "$STATUS"
          fi

  # ───────────────────────────────────────────────
  # Create GitHub Release
  # ───────────────────────────────────────────────
  github-release:
    name: GitHub Release
    needs: [publish-crate, publish-node]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Generate release notes
        run: |
          PREV_TAG=$(git tag --sort=-v:refname | grep '^v' | head -2 | tail -1)
          if [ -z "$PREV_TAG" ]; then
            echo "Initial release" > /tmp/release-notes.md
          else
            git log "${PREV_TAG}..HEAD" --oneline --no-merges --pretty=format:"- %s" | head -50 > /tmp/release-notes.md
          fi

      - name: Create release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          RELEASE_TAG: ${{ github.event.inputs.tag_name || github.ref_name }}
        run: |
          if gh release view "$RELEASE_TAG" &>/dev/null; then
            gh release edit "$RELEASE_TAG" \
              --title "$RELEASE_TAG" \
              --notes-file /tmp/release-notes.md
          else
            gh release create "$RELEASE_TAG" \
              --title "$RELEASE_TAG" \
              --notes-file /tmp/release-notes.md
          fi