moadim 0.27.0

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
name: GitHub Release

# Cut a GitHub Release for a version. Triggered two ways:
#   * `workflow_call` from auto-release.yml, right after it pushes the tag on a
#     version bump (the automated path — see auto-release.yml). That commit
#     already passed lint/test before landing on main (via cut-release.yml's
#     `needs: [version, lint, test]` gate and/or PR branch protection), so
#     re-running here would be redundant — skipped below.
#   * a manual `v*` tag push (escape hatch for hotfixes cut by hand). Unlike
#     the automated path, this can tag *any* commit, verified or not, so this
#     is where the gap was (see #260): a release could be cut without ever
#     checking cargo test/fmt/clippy. Fixed by calling lint.yml/test.yml
#     (already workflow_call-callable) and gating `release` on them via
#     `needs:`.
# Release notes are extracted from the matching version section in CHANGELOG.md
# so the published notes stay in sync with the changelog. Runs independently of
# publish.yml — neither blocks the other.

on:
  push:
    tags:
      - 'v*'
  workflow_call:
    inputs:
      tag:
        description: Tag to release (e.g. v0.14.0)
        required: true
        type: string

permissions:
  contents: write

jobs:
  # Gate for the manual `push: tags` escape hatch only — see #260. Skipped
  # entirely on the `workflow_call` path since that commit is already verified
  # (see header comment above).
  lint:
    name: Lint tagged commit
    if: github.event_name == 'push'
    uses: ./.github/workflows/lint.yml

  test:
    name: Test tagged commit
    if: github.event_name == 'push'
    uses: ./.github/workflows/test.yml

  release:
    name: Create GitHub Release
    needs: [lint, test]
    # Run if lint/test passed (push path) or were skipped (workflow_call path).
    # Do NOT use the default `success()` here: a skipped need would otherwise
    # make this job skip too.
    if: always() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.test.result == 'success' || needs.test.result == 'skipped')
    runs-on: ubuntu-latest
    env:
      TAG: ${{ inputs.tag || github.ref_name }}
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

      - name: Extract changelog section for this tag
        id: notes
        run: |
          VERSION=${TAG#v}
          # Pull lines between '## [VERSION]' and the next '## [' heading.
          awk -v ver="$VERSION" '
            $0 ~ "^## \\[" ver "\\]" { capture=1; next }
            capture && /^## \[/ { exit }
            capture { print }
          ' CHANGELOG.md > release-notes.md
          if [ ! -s release-notes.md ]; then
            echo "No changelog section found for [$VERSION]" >&2
            exit 1
          fi
          echo "Release notes for $VERSION:"
          cat release-notes.md

      - name: Create release
        uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
        with:
          tag_name: ${{ env.TAG }}
          name: ${{ env.TAG }}
          body_path: release-notes.md