flow-wm 0.1.1

A scrolling, infinite-horizontal-canvas tiling window manager for Windows
# Builds flow.exe + flowd.exe for Windows and publishes a portable zip.
#
# Triggers:
#   - push of a version tag (v0.1.0, v1.2.3, ...) -> builds AND publishes a GitHub Release.
#   - workflow_dispatch (manual "Run workflow" button / `gh workflow run release.yml`)
#     -> builds and uploads the zip as a workflow ARTIFACT (no Release is created),
#        so the workflow file can be iterated on without cutting a tag.
#
# The maintainer invariant is: the git tag and Cargo.toml `version` MUST agree.
# The build reads version from Cargo.toml (single source of truth) and asserts the
# tag matches on tag-push runs; a mismatch fails the build loudly.
name: release

on:
  push:
    tags:
      - "v*.*.*"
  workflow_dispatch:

# The auto-provided GITHUB_TOKEN needs contents:write to create the Release and upload assets.
permissions:
  contents: write

jobs:
  build:
    runs-on: windows-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v5

      # Cargo.toml is the single source of truth for the version that gets compiled
      # into the binaries and stamped onto the zip filename.
      - name: Resolve version from Cargo.toml
        id: ver
        shell: pwsh
        run: |

          $m = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"').Matches
          if (-not $m) { throw "Could not find version in Cargo.toml" }
          $ver = $m[0].Groups[1].Value
          Write-Host "Cargo.toml version: $ver"
          "version=$ver" >> $env:GITHUB_OUTPUT

      # On a tag push, fail loud if the tag and Cargo.toml disagree rather than
      # ship a zip whose filename and binary --version drift.
      - name: Assert tag matches Cargo.toml
        if: startsWith(github.ref, 'refs/tags/v')
        shell: pwsh
        run: |

          $tag = '${{ github.ref_name }}'.Substring(1)  # 'v0.1.0' -> '0.1.0'
          $cargo = '${{ steps.ver.outputs.version }}'
          if ($tag -ne $cargo) {
            throw "Tag 'v$tag' != Cargo.toml '$cargo' — bump Cargo.toml before tagging."
          }
          Write-Host "Tag 'v$tag' matches Cargo.toml '$cargo'"

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: x86_64-pc-windows-msvc

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2
        with:
          key: x86_64-pc-windows-msvc

      # --locked refuses to update Cargo.lock; fails the build if the committed
      # lock file has drifted from Cargo.toml (catches "forgot to commit Cargo.lock").
      - name: Build (release, locked)
        shell: pwsh
        run: cargo build --release --locked --target x86_64-pc-windows-msvc

      # Stages both binaries + README + LICENSE into a versioned zip with a SHA256
      # sidecar. Exact paths are surfaced via outputs to avoid glob-matching quirks
      # in softprops/action-gh-release (issue #324).
      - name: Stage and zip
        id: stage
        shell: pwsh
        run: |

          $ver = '${{ steps.ver.outputs.version }}'
          $staging = "flow-wm-$ver-x86_64"
          New-Item -ItemType Directory -Force $staging | Out-Null
          Copy-Item target/x86_64-pc-windows-msvc/release/flowd.exe $staging/
          Copy-Item target/x86_64-pc-windows-msvc/release/flow.exe  $staging/
          Copy-Item README.md, LICENSE $staging/
          Compress-Archive -Path "$staging/*" -DestinationPath "$staging.zip"
          $hash = (Get-FileHash "$staging.zip" -Algorithm SHA256).Hash
          "$hash  $staging.zip" | Out-File -Encoding ascii "$staging.zip.sha256"
          Write-Host "::group::Artifact contents"
          Get-ChildItem $staging | Format-Table Name, Length
          Write-Host "::endgroup::"
          "zip=$staging.zip"        >> $env:GITHUB_OUTPUT
          "sha=$staging.zip.sha256" >> $env:GITHUB_OUTPUT

      # Real release: only on a version-tag push. Uses the workflow file at the
      # tagged commit. If a Release already exists for this tag, action-gh-release
      # updates it (re-uploads assets) rather than failing or duplicating.
      - name: Publish GitHub Release
        if: startsWith(github.ref, 'refs/tags/v')
        uses: softprops/action-gh-release@v3
        with:
          tag_name: ${{ github.ref_name }}
          files: |

            ${{ steps.stage.outputs.zip }}
            ${{ steps.stage.outputs.sha }}
          generate_release_notes: true

      # Dispatch / test runs: skip the Release, upload the zip as a workflow
      # artifact so it can be downloaded and inspected. 14-day retention keeps
      # the artifact store from accumulating forever.
      - name: Upload zip as workflow artifact (dispatch/test only)
        if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
        uses: actions/upload-artifact@v7
        with:
          name: flow-wm-${{ steps.ver.outputs.version }}-x86_64
          path: |

            ${{ steps.stage.outputs.zip }}
            ${{ steps.stage.outputs.sha }}
          retention-days: 14

  # Publishes the crate to crates.io. Runs ONLY on a real version-tag push
  # (never on workflow_dispatch), and only after `build` has succeeded — so the
  # release build has already proven the package compiles on the target.
  publish-crates:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: windows-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v5

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

      # `cargo publish` packages the crate and verifies it builds before
      # uploading. The token is passed via env (never logged) and is read by
      # cargo for the default registry (crates.io). --locked keeps Cargo.lock
      # authoritative. The tag==Cargo.toml invariant was already asserted in
      # `build`, so it holds here without re-checking.
      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish --locked