enlil 0.1.1

Vendor-neutral open-source control and audit plane for AI agent actions. Sits inline between your agents and any model or tool: enforce what each agent may do, and prove what it did.
Documentation
name: Release

# Tagging v* performs a real release. workflow_dispatch runs the same build
# matrix as a dry run: binaries are compiled and uploaded as workflow artifacts,
# but nothing is published to GHCR and no GitHub Release is created. This lets
# the cross-compilation legs be validated before a tag exists, since a pushed
# tag is public immediately.
on:
  push:
    tags: ["v*"]
  workflow_dispatch:

permissions:
  contents: write
  packages: write

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    name: ${{ matrix.target }}
    runs-on: ${{ matrix.os }}
    timeout-minutes: 45
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: aarch64-apple-darwin
            os: macos-latest

    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.target }}

      # rusqlite builds SQLite from source, so cross-compiling needs both the
      # linker and a matching C cross-compiler.
      - name: Install aarch64 cross-compiler
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
          echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
          echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar" >> $GITHUB_ENV

      - name: Resolve version
        id: v
        run: |
          if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
            echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
          else
            echo "version=dryrun-$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
          fi

      - name: Build
        run: cargo build --release --target ${{ matrix.target }} --bin enlil

      # Confirms the artifact is the architecture we asked for. Deliberately does
      # NOT execute the binary: `enlil` is a long-running server with no --version
      # flag, so invoking it here would start listening and hang the job until the
      # 6-hour timeout. `file` gives us the arch guarantee without running anything.
      - name: Inspect binary
        run: |
          BIN=target/${{ matrix.target }}/release/enlil
          ls -la "$BIN"
          file "$BIN"
          case "${{ matrix.target }}" in
            x86_64-unknown-linux-gnu)  file "$BIN" | grep -q "x86-64"     ;;
            aarch64-unknown-linux-gnu) file "$BIN" | grep -q "aarch64"    ;;
            x86_64-apple-darwin)       file "$BIN" | grep -q "x86_64"     ;;
            aarch64-apple-darwin)      file "$BIN" | grep -q "arm64"      ;;
          esac
          echo "architecture verified for ${{ matrix.target }}"

      - name: Package
        run: |
          mkdir -p dist
          cp target/${{ matrix.target }}/release/enlil dist/
          cp README.md LICENSE dist/
          cd dist
          tar czf ../enlil-${{ steps.v.outputs.version }}-${{ matrix.target }}.tar.gz enlil README.md LICENSE

      - uses: actions/upload-artifact@v4
        with:
          name: enlil-${{ matrix.target }}
          path: enlil-*.tar.gz

  docker:
    name: Image (${{ github.ref_type == 'tag' && 'publish' || 'build only' }})
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        if: github.ref_type == 'tag'
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # On a dry run this still builds both architectures, so a broken arm64
      # image is caught here rather than after a tag is public.
      - uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ github.ref_type == 'tag' }}
          tags: |
            ghcr.io/${{ github.repository_owner }}/enlil:${{ github.ref_name }}
            ghcr.io/${{ github.repository_owner }}/enlil:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

  release:
    name: GitHub Release
    runs-on: ubuntu-latest
    timeout-minutes: 15
    needs: build
    if: github.ref_type == 'tag'
    steps:
      - uses: actions/download-artifact@v4
        with:
          path: artifacts
          merge-multiple: true

      - uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          files: artifacts/*.tar.gz

  summary:
    name: Dry-run summary
    runs-on: ubuntu-latest
    timeout-minutes: 5
    needs: [build, docker]
    if: github.ref_type != 'tag'
    steps:
      - run: |
          echo "### Dry run complete" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "All four target binaries built and both container architectures compiled." >> $GITHUB_STEP_SUMMARY
          echo "Nothing was published. Push a \`v*\` tag to perform the real release." >> $GITHUB_STEP_SUMMARY