diff --git a/Taskfile.yaml b/Taskfile.yaml index 2b3f2357812d52994b7f10c66d1e56f09a5ded0a..bb682563dc1e5f2fa82feba43538ff0e894d14b7 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -27,29 +27,24 @@ tasks: - task: reuse fmt: - desc: Format all Go source code cmds: - go install mvdan.cc/gofumpt@latest - gofumpt -l -w . lint: - desc: Lint Go source code cmds: - golangci-lint run staticcheck: - desc: Perform static analysis cmds: - go install honnef.co/go/tools/cmd/staticcheck@latest - staticcheck ./... test: - desc: Run tests cmds: - go test -v ./... vuln: - desc: Check for vulnerabilities cmds: - go install golang.org/x/vuln/cmd/govulncheck@latest - govulncheck ./... @@ -60,19 +55,16 @@ tasks: - reuse lint build: - desc: Build git-format cmds: - go build -o git-format -ldflags "-s -w -X main.version={{.VERSION}}" generates: - git-format install: - desc: Install git-format cmds: - go install -ldflags "-s -w -X main.version={{.VERSION}}" run: - desc: Run git-format cmds: - go run -ldflags "-s -w -X main.version={{.VERSION}}" . {{.CLI_ARGS}} @@ -87,7 +79,7 @@ tasks: clean: desc: Remove build artifacts cmds: - - rm -rf git-format + - rm -rf git-format dist/ clean-all: desc: Remove build artifacts and config.toml @@ -95,39 +87,11 @@ tasks: - rm -rf git-format config.toml release: - desc: Interactive release workflow - vars: - BUMP: - sh: | - opts="major minor patch prerelease" - if git describe --tags --abbrev=0 2>/dev/null | grep -qE '\-[a-zA-Z]+\.[0-9]+$'; then - opts="$opts graduate" - fi - gum choose $opts - NEXT: - sh: | - case "{{.BUMP}}" in - graduate) svu next ;; - prerelease) - suffix=$(gum input --placeholder "Suffix (beta, rc, etc.)") - svu prerelease --pre-release "$suffix" ;; - *) svu {{.BUMP}} ;; - esac - LAST_STABLE: - sh: git tag -l | grep -v '\-' | sort -V | tail -n 1 - prompt: "Release {{.NEXT}}?" - preconditions: - - sh: '[ $(git symbolic-ref --short HEAD) = "main" ] || [ $(git symbolic-ref --short HEAD) = "dev" ]' - msg: Not on main or dev branch - - sh: "[ $(git status --porcelain=2 | wc -l) = 0 ]" - msg: "Git is dirty" + desc: Interactive release workflow (tag, build, pack, upload) cmds: - - | - if [ "{{.BUMP}}" = "graduate" ]; then - llm-tag -b {{.LAST_STABLE}} {{.NEXT}} - else - llm-tag {{.NEXT}} - fi - - git push soft {{.NEXT}} - - go list -m git.secluded.site/git-format@{{.NEXT}} > /dev/null - - echo "Released {{.NEXT}} and notified module proxy" + - ./release.fish + + release:build: + desc: Cross-compile and pack for all release targets (no tag/push) + cmds: + - ./release.fish --from build diff --git a/release.fish b/release.fish new file mode 100755 index 0000000000000000000000000000000000000000..b01ebf014c975a0a6f9e2620a64bf365c5b6fe60 --- /dev/null +++ b/release.fish @@ -0,0 +1,282 @@ +#!/usr/bin/env fish + +# SPDX-FileCopyrightText: Amolith +# +# SPDX-License-Identifier: CC0-1.0 + +function __release_git_format_usage + echo "Usage: release.fish [--from ] [--only ] [--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 Start at this stage and continue through the rest + --only 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