refactor: delegate release workflow to fish script
Amolith
created
Replace the inline release logic in Taskfile.yaml with a dedicated
release.fish script following the same staged pattern used by lune,
yatd, and crush. The script handles tag computation, cross-compilation,
UPX packing, and upload via the shared release(1) fish function.
Taskfile changes:
- Release task now delegates to ./release.fish
- Add release:build convenience task for --from build
- Clean task also removes dist/
@@ -0,0 +1,282 @@
+#!/usr/bin/env fish
+
+# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+#
+# SPDX-License-Identifier: CC0-1.0
+
+function __release_git_format_usage
+ echo "Usage: release.fish [--from <stage>] [--only <stage>] [--help]
+
+Stages (run in order):
+ tag Compute next version, create and push annotated tag
+ build Cross-compile for all release targets
+ pack Compress Linux binaries with UPX
+ upload Upload artifacts via release(1)
+
+Flags:
+ --from <stage> Start at this stage and continue through the rest
+ --only <stage> Run just this one stage
+ --help, -h Show this help
+
+With no flags, all stages run in order (tag → build → pack → upload).
+The tag stage also notifies the Go module proxy after pushing.
+
+Examples:
+ ./release.fish Full release
+ ./release.fish --from build Rebuild, pack, and upload (skip tagging)
+ ./release.fish --only build Just cross-compile
+ ./release.fish --only upload Re-upload existing dist/ artifacts"
+end
+
+set -l targets \
+ linux/amd64 \
+ linux/arm64 \
+ darwin/amd64 \
+ darwin/arm64 \
+ windows/amd64 \
+ freebsd/amd64
+
+# UPX 5.x dropped macOS support; windows/amd64 is PE32 only and
+# freebsd/amd64 is ELF32 only, so only Linux targets are packed.
+set -l pack_targets \
+ linux-amd64 \
+ linux-arm64
+
+# Global so __should_run can see them. Prefixed to avoid collisions.
+set -g __rgf_stages tag build pack upload
+set -g __rgf_from ""
+set -g __rgf_only ""
+
+# --- Parse flags ---
+
+set -l i 1
+while test $i -le (count $argv)
+ switch $argv[$i]
+ case --from
+ set i (math $i + 1)
+ set -g __rgf_from $argv[$i]
+ case --only
+ set i (math $i + 1)
+ set -g __rgf_only $argv[$i]
+ case --help -h
+ __release_git_format_usage
+ exit 0
+ case '*'
+ echo "Error: unknown flag '$argv[$i]'" >&2
+ __release_git_format_usage >&2
+ exit 1
+ end
+ set i (math $i + 1)
+end
+
+if test -n "$__rgf_from" -a -n "$__rgf_only"
+ echo "Error: --from and --only are mutually exclusive" >&2
+ exit 1
+end
+
+if test -n "$__rgf_from"; and not contains -- "$__rgf_from" $__rgf_stages
+ echo "Error: unknown stage '$__rgf_from' (valid: $__rgf_stages)" >&2
+ exit 1
+end
+
+if test -n "$__rgf_only"; and not contains -- "$__rgf_only" $__rgf_stages
+ echo "Error: unknown stage '$__rgf_only' (valid: $__rgf_stages)" >&2
+ exit 1
+end
+
+# Returns 0 if the given stage should execute, 1 otherwise.
+function __should_run -a stage
+ if test -n "$__rgf_only"
+ test "$stage" = "$__rgf_only"
+ return
+ end
+
+ if test -z "$__rgf_from"
+ return 0
+ end
+
+ set -l reached 0
+ for s in $__rgf_stages
+ if test "$s" = "$__rgf_from"
+ set reached 1
+ end
+ if test $reached -eq 1 -a "$s" = "$stage"
+ return 0
+ end
+ end
+ return 1
+end
+
+# --- Resolve tag ---
+#
+# When skipping the tag stage we still need a tag for filenames and
+# ldflags, so fall back to git describe.
+
+set -l tag
+
+if __should_run tag
+ # --- Guards ---
+
+ set -l branch (git symbolic-ref --short HEAD)
+ if test "$branch" != main -a "$branch" != dev
+ echo "Error: not on main or dev branch (on $branch)" >&2
+ exit 1
+ end
+
+ if test (git status --porcelain=2 | wc -l) -ne 0
+ echo "Error: git working tree is dirty" >&2
+ exit 1
+ end
+
+ # --- Fetch tags ---
+
+ git fetch soft --tags
+
+ # --- Compute next version ---
+
+ set -l current (git describe --tags --abbrev=0 2>/dev/null; or echo v0.0.0)
+ set -l bump (gum choose major minor patch prerelease)
+
+ # Detect whether the current tag is already a prerelease
+ # (e.g. v1.2.3-rc.4).
+ set -l current_is_prerelease no
+ if string match -rq -- '-[a-zA-Z]+\.[0-9]+$' $current
+ set current_is_prerelease yes
+ end
+
+ # Ask whether to create a prerelease, unless the user already chose
+ # "prerelease".
+ set -l is_prerelease no
+ if test "$bump" = prerelease
+ set is_prerelease yes
+ else
+ if gum confirm "Create pre-release?"
+ set is_prerelease yes
+ end
+ end
+
+ # Determine the prerelease suffix (e.g. "beta", "rc").
+ set -l prerelease_suffix ""
+ if test "$bump" = prerelease -a "$current_is_prerelease" = yes
+ # Reuse the suffix from the current prerelease tag.
+ set prerelease_suffix (string replace -r '.*-([a-zA-Z]+)\.[0-9]+$' '$1' $current)
+ else if test "$is_prerelease" = yes
+ set prerelease_suffix (gum input --placeholder "Enter pre-release suffix (e.g. beta, rc)")
+ if test -z "$prerelease_suffix"
+ echo "Error: pre-release suffix is required" >&2
+ exit 1
+ end
+ end
+
+ # Compute the base version (without prerelease suffix).
+ set -l base_next
+ if test "$bump" = prerelease -a "$current_is_prerelease" = yes
+ # Strip the prerelease suffix to get the base
+ # (e.g. v1.2.3-rc.4 → v1.2.3).
+ set base_next (string replace -r -- '-[a-zA-Z]+\.[0-9]+$' '' $current)
+ else
+ set base_next (svu $bump)
+ end
+
+ # Compute the suffix version number.
+ set -l suffix_ver ""
+ if test "$is_prerelease" = yes -a -n "$prerelease_suffix"
+ if test "$bump" = prerelease -a "$current_is_prerelease" = yes
+ # Increment the current prerelease number.
+ set -l current_num (string replace -r '.*-[a-zA-Z]+\.([0-9]+)$' '$1' $current)
+ set suffix_ver (math $current_num + 1)
+ else
+ # Find existing tags with this suffix and increment past the
+ # highest.
+ set -l highest (git tag -l "$base_next-$prerelease_suffix.*" \
+ | string replace -r ".*-$prerelease_suffix\." '' \
+ | sort -n | tail -1)
+ if test -n "$highest"
+ set suffix_ver (math $highest + 1)
+ else
+ set suffix_ver 0
+ end
+ end
+ end
+
+ # Assemble the final tag.
+ if test "$is_prerelease" = yes -a -n "$prerelease_suffix"
+ set tag "$base_next-$prerelease_suffix.$suffix_ver"
+ else
+ set tag "$base_next"
+ end
+
+ # --- Confirm ---
+
+ read -P "Release $tag? [y/N] " confirm
+ if test "$confirm" != y -a "$confirm" != Y
+ echo Aborted.
+ exit 0
+ end
+
+ # --- Tag and push ---
+
+ git tag -a $tag; or begin
+ echo "Error: tagging failed" >&2
+ exit 1
+ end
+
+ git push soft $tag; or begin
+ echo "Error: tag push failed — deleting local tag" >&2
+ git tag -d $tag 2>/dev/null
+ exit 1
+ end
+
+ echo "Released $tag"
+
+ # --- Notify Go module proxy ---
+
+ go list -m git.secluded.site/git-format@$tag >/dev/null; or begin
+ echo "Warning: module proxy notification failed" >&2
+ end
+else
+ set tag (git describe --tags --always 2>/dev/null; or echo dev)
+end
+
+# --- Build ---
+
+if __should_run build
+ set -l ldflags "-s -w -X main.version=$tag"
+
+ rm -rf dist
+ mkdir -p dist
+
+ for target in $targets
+ set -l os (echo $target | cut -d/ -f1)
+ set -l arch (echo $target | cut -d/ -f2)
+ set -l ext ""
+ if test "$os" = windows
+ set ext .exe
+ end
+
+ echo "Building $os/$arch..."
+ env CGO_ENABLED=0 GOOS=$os GOARCH=$arch \
+ go build -o "dist/git-format-$tag-$os-$arch$ext" -ldflags "$ldflags"; or begin
+ echo "Error: build failed for $os/$arch" >&2
+ exit 1
+ end
+ end
+end
+
+# --- Pack ---
+
+if __should_run pack
+ for suffix in $pack_targets
+ set -l bin "dist/git-format-$tag-$suffix"
+ if test -f "$bin"
+ echo "Packing $suffix..."
+ upx -q "$bin"
+ end
+ end
+end
+
+# --- Upload ---
+
+if __should_run upload
+ fish -c "release upload git-format $tag --latest dist/*"
+end