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