release.fish

  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