1#!/usr/bin/env fish
2
3function __release_yatd_usage
4 echo "Usage: release.fish [--from <stage>] [--only <stage>] [--help]
5
6Stages (run in order):
7 tag Bump Cargo.toml, commit, tag, and push
8 build Cross-compile for all release targets
9 pack Compress Linux binaries with UPX
10 upload Upload artifacts via release(1)
11
12Flags:
13 --from <stage> Start at this stage and continue through the rest
14 --only <stage> Run just this one stage
15 --help, -h Show this help
16
17With no flags, all stages run in order (tag → build → pack → upload).
18
19Examples:
20 ./release.fish Full release
21 ./release.fish --from build Rebuild, pack, and upload (skip tagging)
22 ./release.fish --only build Just cross-compile
23 ./release.fish --only upload Re-upload existing dist/ artifacts"
24end
25
26set -l targets \
27 x86_64-unknown-linux-gnu \
28 aarch64-unknown-linux-gnu \
29 x86_64-apple-darwin \
30 aarch64-apple-darwin \
31 x86_64-pc-windows-gnu \
32 x86_64-unknown-freebsd
33
34# UPX 5.x dropped macOS support; Windows and FreeBSD are limited to 32-bit
35# formats. Only Linux targets are packable.
36set -l pack_targets \
37 x86_64-unknown-linux-gnu \
38 aarch64-unknown-linux-gnu
39
40# Global so __should_run can see them. Prefixed to avoid collisions.
41set -g __ry_stages tag build pack upload
42set -g __ry_from ""
43set -g __ry_only ""
44
45# --- Parse flags ---
46
47set -l i 1
48while test $i -le (count $argv)
49 switch $argv[$i]
50 case --from
51 set i (math $i + 1)
52 set -g __ry_from $argv[$i]
53 case --only
54 set i (math $i + 1)
55 set -g __ry_only $argv[$i]
56 case --help -h
57 __release_yatd_usage
58 exit 0
59 case '*'
60 echo "Error: unknown flag '$argv[$i]'" >&2
61 __release_yatd_usage >&2
62 exit 1
63 end
64 set i (math $i + 1)
65end
66
67if test -n "$__ry_from" -a -n "$__ry_only"
68 echo "Error: --from and --only are mutually exclusive" >&2
69 exit 1
70end
71
72if test -n "$__ry_from"; and not contains -- "$__ry_from" $__ry_stages
73 echo "Error: unknown stage '$__ry_from' (valid: $__ry_stages)" >&2
74 exit 1
75end
76
77if test -n "$__ry_only"; and not contains -- "$__ry_only" $__ry_stages
78 echo "Error: unknown stage '$__ry_only' (valid: $__ry_stages)" >&2
79 exit 1
80end
81
82# Returns 0 if the given stage should execute, 1 otherwise.
83function __should_run -a stage
84 if test -n "$__ry_only"
85 test "$stage" = "$__ry_only"
86 return
87 end
88
89 if test -z "$__ry_from"
90 return 0
91 end
92
93 set -l reached 0
94 for s in $__ry_stages
95 if test "$s" = "$__ry_from"
96 set reached 1
97 end
98 if test $reached -eq 1 -a "$s" = "$stage"
99 return 0
100 end
101 end
102 return 1
103end
104
105# --- Resolve tag ---
106#
107# When skipping the tag stage we still need a tag for filenames and
108# ldflags, so fall back to git describe.
109
110set -l tag
111
112if __should_run tag
113 # --- Guards ---
114
115 set -l parent_bookmarks (jj log -r '@-' --no-graph -T 'bookmarks' 2>/dev/null)
116 if not string match -rq 'main|dev' -- "$parent_bookmarks"
117 echo "Error: parent commit is not on main or dev" >&2
118 exit 1
119 end
120
121 if jj diff --summary 2>/dev/null | grep -qE '^[MD] '
122 echo "Error: working copy has modified or deleted files" >&2
123 exit 1
124 end
125
126 # --- Fetch tags ---
127
128 jj git fetch --remote soft
129
130 # --- Compute next version ---
131
132 # jj tag list output is "<tag>: <hash> <desc>"; extract the tag name.
133 set -l current (jj tag list 2>/dev/null | awk '{print $1}' | string replace -r ':$' '' | sort -V | tail -1)
134 if test -z "$current"
135 set current v0.0.0
136 end
137 set -l bump (gum choose major minor patch prerelease)
138
139 # Detect whether the current tag is already a prerelease
140 # (e.g. v1.2.3-rc.4).
141 set -l current_is_prerelease no
142 if string match -rq -- '-[a-zA-Z]+\.[0-9]+$' $current
143 set current_is_prerelease yes
144 end
145
146 # Ask whether to create a prerelease, unless the user already chose
147 # "prerelease".
148 set -l is_prerelease no
149 if test "$bump" = prerelease
150 set is_prerelease yes
151 else
152 if gum confirm "Create pre-release?"
153 set is_prerelease yes
154 end
155 end
156
157 # Determine the prerelease suffix (e.g. "beta", "rc").
158 set -l prerelease_suffix ""
159 if test "$bump" = prerelease -a "$current_is_prerelease" = yes
160 # Reuse the suffix from the current prerelease tag.
161 set prerelease_suffix (string replace -r '.*-([a-zA-Z]+)\.[0-9]+$' '$1' $current)
162 else if test "$is_prerelease" = yes
163 set prerelease_suffix (gum input --placeholder "Enter pre-release suffix (e.g. beta, rc)")
164 if test -z "$prerelease_suffix"
165 echo "Error: pre-release suffix is required" >&2
166 exit 1
167 end
168 end
169
170 # Compute the base version (without prerelease suffix).
171 set -l base_next
172 if test "$bump" = prerelease -a "$current_is_prerelease" = yes
173 # Strip the prerelease suffix to get the base
174 # (e.g. v1.2.3-rc.4 → v1.2.3).
175 set base_next (string replace -r -- '-[a-zA-Z]+\.[0-9]+$' '' $current)
176 else
177 set base_next (svu $bump)
178 end
179
180 # Compute the suffix version number.
181 set -l suffix_ver ""
182 if test "$is_prerelease" = yes -a -n "$prerelease_suffix"
183 if test "$bump" = prerelease -a "$current_is_prerelease" = yes
184 # Increment the current prerelease number.
185 set -l current_num (string replace -r '.*-[a-zA-Z]+\.([0-9]+)$' '$1' $current)
186 set suffix_ver (math $current_num + 1)
187 else
188 # Find existing tags with this suffix and increment past the
189 # highest.
190 set -l highest (jj tag list 2>/dev/null \
191 | awk '{print $1}' \
192 | string replace -r ':$' '' \
193 | string replace -r "^$base_next-$prerelease_suffix\\." '' \
194 | string match -r '^\d+$' \
195 | sort -n | tail -1)
196 if test -n "$highest"
197 set suffix_ver (math $highest + 1)
198 else
199 set suffix_ver 0
200 end
201 end
202 end
203
204 # Assemble the final tag.
205 if test "$is_prerelease" = yes -a -n "$prerelease_suffix"
206 set tag "$base_next-$prerelease_suffix.$suffix_ver"
207 else
208 set tag "$base_next"
209 end
210
211 # --- Confirm ---
212
213 read -P "Release $tag? [y/N] " confirm
214 if test "$confirm" != y -a "$confirm" != Y
215 echo Aborted.
216 exit 0
217 end
218
219 # --- Bump Cargo.toml and commit ---
220
221 set -l cargo_ver (string replace -r '^v' '' $tag)
222 sed -i "s/^version = \".*\"/version = \"$cargo_ver\"/" Cargo.toml
223 jj commit -m "Bump version to $tag"; or begin
224 echo "Error: jj commit failed" >&2
225 exit 1
226 end
227
228 # --- Tag and push ---
229
230 jj tag set $tag -r @-; or begin
231 echo "Error: tagging failed" >&2
232 exit 1
233 end
234
235 git push soft $tag; or begin
236 echo "Error: tag push failed" >&2
237 exit 1
238 end
239
240 jj git push --remote soft -b main; or begin
241 echo "Error: branch push failed" >&2
242 exit 1
243 end
244
245 echo "Tagged and pushed $tag"
246else
247 set tag (git describe --tags --always 2>/dev/null; or echo dev)
248end
249
250# --- Build ---
251
252if __should_run build
253 rm -rf dist
254 mkdir -p dist
255
256 for target in $targets
257 echo "Building $target..."
258 set -l ext ""
259 if string match -q '*windows*' $target
260 set ext .exe
261 end
262 cargo zigbuild --target $target --release; or begin
263 echo "Error: build failed for $target" >&2
264 exit 1
265 end
266 cp "target/$target/release/td$ext" "dist/td-$tag-$target$ext"
267 end
268end
269
270# --- Pack ---
271
272if __should_run pack
273 for target in $pack_targets
274 set -l bin "dist/td-$tag-$target"
275 if test -f "$bin"
276 echo "Packing $target..."
277 upx -q "$bin"
278 end
279 end
280end
281
282# --- Upload ---
283
284if __should_run upload
285 fish -c "release upload yatd $tag --latest dist/*"
286end