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