1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# 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