1#!/usr/bin/env fish
2
3# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
4#
5# SPDX-License-Identifier: CC0-1.0
6
7function __release_sb_mcp_usage
8 echo "Usage: release.fish [--from <stage>] [--only <stage>] [--help]
9
10Stages (run in order):
11 tag Compute next version, create and push lightweight tag
12 build Cross-compile for all release targets
13 pack Compress Linux binaries with UPX
14 upload Upload artifacts via release(1)
15
16Flags:
17 --from <stage> Start at this stage and continue through the rest
18 --only <stage> Run just this one stage
19 --help, -h Show this help
20
21With no flags, all stages run in order (tag → build → pack → upload).
22The tag stage also notifies the Go module proxy after pushing.
23
24Examples:
25 ./release.fish Full release
26 ./release.fish --from build Rebuild, pack, and upload (skip tagging)
27 ./release.fish --only build Just cross-compile
28 ./release.fish --only upload Re-upload existing dist/ artifacts"
29end
30
31set -l binary sb-mcp
32set -l module_path git.secluded.site/sb-mcp
33set -l version_pkg git.secluded.site/sb-mcp/internal/server.Version
34set -l remote soft
35
36set -l targets \
37 linux/amd64 \
38 linux/arm64 \
39 darwin/amd64 \
40 darwin/arm64 \
41 windows/amd64 \
42 freebsd/amd64
43
44# UPX 5.x dropped macOS support; windows/amd64 is PE32 only and
45# freebsd/amd64 is ELF32 only, so only Linux targets are packed.
46set -l pack_targets \
47 linux-amd64 \
48 linux-arm64
49
50# Global so __should_run can see them. Prefixed to avoid collisions.
51set -g __rl_stages tag build pack upload
52set -g __rl_from ""
53set -g __rl_only ""
54
55# --- Parse flags ---
56
57set -l i 1
58while test $i -le (count $argv)
59 switch $argv[$i]
60 case --from
61 set i (math $i + 1)
62 if test $i -gt (count $argv)
63 echo "Error: --from requires a value" >&2
64 __release_sb_mcp_usage >&2
65 exit 1
66 end
67 set -g __rl_from $argv[$i]
68 case --only
69 set i (math $i + 1)
70 if test $i -gt (count $argv)
71 echo "Error: --only requires a value" >&2
72 __release_sb_mcp_usage >&2
73 exit 1
74 end
75 set -g __rl_only $argv[$i]
76 case --help -h
77 __release_sb_mcp_usage
78 exit 0
79 case '*'
80 echo "Error: unknown flag '$argv[$i]'" >&2
81 __release_sb_mcp_usage >&2
82 exit 1
83 end
84 set i (math $i + 1)
85end
86
87if test -n "$__rl_from" -a -n "$__rl_only"
88 echo "Error: --from and --only are mutually exclusive" >&2
89 exit 1
90end
91
92if test -n "$__rl_from"; and not contains -- "$__rl_from" $__rl_stages
93 echo "Error: unknown stage '$__rl_from' (valid: $__rl_stages)" >&2
94 exit 1
95end
96
97if test -n "$__rl_only"; and not contains -- "$__rl_only" $__rl_stages
98 echo "Error: unknown stage '$__rl_only' (valid: $__rl_stages)" >&2
99 exit 1
100end
101
102# Returns 0 if the given stage should execute, 1 otherwise.
103function __should_run -a stage
104 if test -n "$__rl_only"
105 test "$stage" = "$__rl_only"
106 return
107 end
108
109 if test -z "$__rl_from"
110 return 0
111 end
112
113 set -l reached 0
114 for s in $__rl_stages
115 if test "$s" = "$__rl_from"
116 set reached 1
117 end
118 if test $reached -eq 1 -a "$s" = "$stage"
119 return 0
120 end
121 end
122 return 1
123end
124
125# --- Resolve tag ---
126#
127# When skipping the tag stage we still need a tag for filenames and
128# ldflags, so fall back to git describe.
129
130set -l tag
131
132if __should_run tag
133 # --- Guards ---
134
135 # In jj the working copy is always a draft commit; "clean" means
136 # the working-copy change has no file diffs.
137 if test -n "$(jj diff --summary)"
138 echo "Error: working copy has uncommitted changes" >&2
139 exit 1
140 end
141
142 # --- Fetch tags ---
143
144 git fetch $remote --tags
145
146 # --- Compute next version ---
147
148 set -l current (git describe --tags --abbrev=0 2>/dev/null; or echo v0.0.0)
149 set -l bump (gum choose major minor patch prerelease)
150
151 # Detect whether the current tag is already a prerelease
152 # (e.g. v1.2.3-rc.4).
153 set -l current_is_prerelease no
154 if string match -rq -- '-[a-zA-Z]+\.[0-9]+$' $current
155 set current_is_prerelease yes
156 end
157
158 # Ask whether to create a prerelease, unless the user already chose
159 # "prerelease".
160 set -l is_prerelease no
161 if test "$bump" = prerelease
162 set is_prerelease yes
163 else
164 if gum confirm "Create pre-release?"
165 set is_prerelease yes
166 end
167 end
168
169 # Determine the prerelease suffix (e.g. "beta", "rc").
170 set -l prerelease_suffix ""
171 if test "$bump" = prerelease -a "$current_is_prerelease" = yes
172 # Reuse the suffix from the current prerelease tag.
173 set prerelease_suffix (string replace -r '.*-([a-zA-Z]+)\.[0-9]+$' '$1' $current)
174 else if test "$is_prerelease" = yes
175 set prerelease_suffix (gum input --placeholder "Enter pre-release suffix (e.g. beta, rc)")
176 if test -z "$prerelease_suffix"
177 echo "Error: pre-release suffix is required" >&2
178 exit 1
179 end
180 end
181
182 # Compute the base version (without prerelease suffix).
183 set -l base_next
184 if test "$bump" = prerelease -a "$current_is_prerelease" = yes
185 # Strip the prerelease suffix to get the base
186 # (e.g. v1.2.3-rc.4 → v1.2.3).
187 set base_next (string replace -r -- '-[a-zA-Z]+\.[0-9]+$' '' $current)
188 else
189 set base_next (svu $bump)
190 end
191
192 # Compute the suffix version number.
193 set -l suffix_ver ""
194 if test "$is_prerelease" = yes -a -n "$prerelease_suffix"
195 if test "$bump" = prerelease -a "$current_is_prerelease" = yes
196 # Increment the current prerelease number.
197 set -l current_num (string replace -r '.*-[a-zA-Z]+\.([0-9]+)$' '$1' $current)
198 set suffix_ver (math $current_num + 1)
199 else
200 # Find existing tags with this suffix and increment past the
201 # highest.
202 set -l highest (git tag -l "$base_next-$prerelease_suffix.*" \
203 | string replace -r ".*-$prerelease_suffix\." '' \
204 | sort -n | tail -1)
205 if test -n "$highest"
206 set suffix_ver (math $highest + 1)
207 else
208 set suffix_ver 0
209 end
210 end
211 end
212
213 # Assemble the final tag.
214 if test "$is_prerelease" = yes -a -n "$prerelease_suffix"
215 set tag "$base_next-$prerelease_suffix.$suffix_ver"
216 else
217 set tag "$base_next"
218 end
219
220 # --- Confirm ---
221
222 read -P "Release $tag? [y/N] " confirm
223 if test "$confirm" != y -a "$confirm" != Y
224 echo Aborted.
225 exit 0
226 end
227
228 # --- Tag and push ---
229 #
230 # jj tag set creates a lightweight tag pointing at the given
231 # revision. We tag @- (the parent of the working copy) which is
232 # the commit main points to. Then push via git since jj git push
233 # does not push tags.
234
235 jj tag set $tag -r @-; or begin
236 echo "Error: tagging failed" >&2
237 exit 1
238 end
239
240 git push $remote $tag; or begin
241 echo "Error: tag push failed — deleting local tag" >&2
242 jj tag delete $tag 2>/dev/null
243 exit 1
244 end
245
246 echo "Released $tag"
247
248 # --- Notify Go module proxy ---
249
250 go list -m $module_path@$tag >/dev/null; or begin
251 echo "Warning: module proxy notification failed" >&2
252 end
253else
254 set tag (git describe --tags --always 2>/dev/null; or echo dev)
255end
256
257# --- Build ---
258
259if __should_run build
260 set -l ldflags "-s -w -X $version_pkg=$tag"
261
262 rm -rf dist
263 mkdir -p dist
264
265 for target in $targets
266 set -l os (echo $target | cut -d/ -f1)
267 set -l arch (echo $target | cut -d/ -f2)
268 set -l ext ""
269 if test "$os" = windows
270 set ext .exe
271 end
272
273 echo "Building $os/$arch..."
274 env CGO_ENABLED=0 GOOS=$os GOARCH=$arch \
275 go build -o "dist/$binary-$tag-$os-$arch$ext" -ldflags "$ldflags" ./cmd/$binary/; or begin
276 echo "Error: build failed for $os/$arch" >&2
277 exit 1
278 end
279 end
280end
281
282# --- Pack ---
283
284if __should_run pack
285 for suffix in $pack_targets
286 set -l bin "dist/$binary-$tag-$suffix"
287 if test -f "$bin"
288 echo "Packing $suffix..."
289 upx -q "$bin"
290 end
291 end
292end
293
294# --- Upload ---
295
296if __should_run upload
297 fish -c "release upload $binary $tag --latest dist/*"
298end