release.fish

  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