ilo 26.5.0

ilo - the token-minimal programming language AI agents write
Documentation
name: Release

on:
  push:
    tags: ["v*"]

permissions:
  contents: write

jobs:
  validate-tag:
    runs-on: ubuntu-latest
    outputs:
      semver: ${{ steps.normalize.outputs.semver }}
    steps:
      - name: Validate tag format
        run: |
          TAG="${GITHUB_REF_NAME}"
          # Accept CalVer (vYY.M, vYY.M.P) and legacy semver (vX.Y.Z); each optionally with -dev.N
          if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?(-dev\.[0-9]+)?$ ]]; then
            echo "Tag '$TAG' is valid."
          else
            echo "ERROR: Tag '$TAG' does not match allowed formats: vYY.M, vYY.M.P, vYY.M-dev.N, or vYY.M.P-dev.N"
            exit 1
          fi
      - name: Normalize to semver
        id: normalize
        run: |
          # Strip leading v, then insert .0 patch if absent so cargo/npm get a semver-shaped string.
          TAG="${GITHUB_REF_NAME#v}"
          if [[ "$TAG" =~ ^([0-9]+\.[0-9]+)(-dev\.[0-9]+)?$ ]]; then
            SEMVER="${BASH_REMATCH[1]}.0${BASH_REMATCH[2]}"
          else
            SEMVER="$TAG"
          fi
          echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
          echo "Normalized $GITHUB_REF_NAME -> $SEMVER"

  build:
    needs: validate-tag
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            cross: true
          - target: aarch64-unknown-linux-musl
            os: ubuntu-latest
            cross: true
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: aarch64-apple-darwin
            os: macos-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          sparse-checkout: |
            src
            build.rs
            SPEC.md
            Cargo.toml
            Cargo.lock
            skills

      - name: Install Rust
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          targets: ${{ matrix.target }}

      - name: Install cross
        if: matrix.cross
        run: cargo install cross --git https://github.com/cross-rs/cross

      - name: Build
        run: |
          if [ "${{ matrix.cross }}" = "true" ]; then
            cross build --release --target ${{ matrix.target }}
          else
            cargo build --release --target ${{ matrix.target }}
          fi
        shell: bash

      - name: Rename binary (unix)
        if: runner.os != 'Windows'
        run: mv target/${{ matrix.target }}/release/ilo ilo-${{ matrix.target }}

      - name: Rename binary (windows)
        if: runner.os == 'Windows'
        run: mv target/${{ matrix.target }}/release/ilo.exe ilo-${{ matrix.target }}.exe
        shell: bash

      - name: Upload artifact
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: ilo-${{ matrix.target }}
          path: ilo-${{ matrix.target }}*

  build-wasm:
    needs: validate-tag
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          sparse-checkout: |
            src
            build.rs
            SPEC.md
            Cargo.toml
            Cargo.lock
            skills

      - name: Install Rust
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          targets: wasm32-wasip1

      - name: Build WASM
        run: cargo build --release --target wasm32-wasip1 --no-default-features

      - name: Upload WASM artifact
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: ilo-wasm
          path: target/wasm32-wasip1/release/ilo.wasm

  release:
    needs: [build, build-wasm]
    runs-on: ubuntu-latest

    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          merge-multiple: true

      - name: Generate checksums
        run: sha256sum ilo-* > checksums-sha256.txt

      - name: Create release
        uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
        with:
          generate_release_notes: true
          prerelease: ${{ contains(github.ref_name, '-dev.') }}
          files: |
            ilo-*
            checksums-sha256.txt

  publish-crates:
    needs: [release]
    runs-on: ubuntu-latest
    if: ${{ !contains(github.ref_name, '-dev.') }}

    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

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

      - name: Publish to crates.io
        run: |
          output=$(cargo publish 2>&1) && echo "$output" || {
            echo "$output"
            echo "$output" | grep -q "already exists" || exit 1
          }
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

  publish-npm:
    needs: [build-wasm, validate-tag]
    runs-on: ubuntu-latest
    if: ${{ !contains(github.ref_name, '-dev.') }}

    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          sparse-checkout: |
            npm
            README.md

      - name: Copy README to npm package
        run: cp README.md npm/README.md

      - name: Download WASM artifact
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          name: ilo-wasm
          path: npm/

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

      - name: Set npm version from tag
        working-directory: npm
        run: npm version ${{ needs.validate-tag.outputs.semver }} --no-git-tag-version

      - name: Publish to npm
        working-directory: npm
        run: |
          output=$(npm publish --access public 2>&1) && echo "$output" || {
            echo "$output"
            echo "$output" | grep -q "previously published" || exit 1
          }
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

  publish-pi:
    needs: [release, validate-tag]
    runs-on: ubuntu-latest
    if: ${{ !contains(github.ref_name, '-dev.') }}

    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          sparse-checkout: |
            pi
            skills/ilo

      - name: Sync canonical skill into pi package
        run: |
          mkdir -p pi/skills/ilo
          cp skills/ilo/SKILL.md pi/skills/ilo/SKILL.md

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

      - name: Set npm version from tag
        working-directory: pi
        run: npm version ${{ needs.validate-tag.outputs.semver }} --no-git-tag-version

      - name: Publish to npm
        working-directory: pi
        run: |
          output=$(npm publish --access public 2>&1) && echo "$output" || {
            echo "$output"
            echo "$output" | grep -q "previously published" || exit 1
          }
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}