feat: add Homebrew and Scoop publishing scripts, enhance versioning metadata
This commit is contained in:
Executable
+124
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Renders Homebrew Formula + Scoop manifest files for this release and
|
||||||
|
# pushes them into the shared tap repo (see .gitea/workflows/release.yml).
|
||||||
|
# Other tools publishing into the same tap repo can copy this script
|
||||||
|
# verbatim — only SRC_HOMEPAGE/BIN_NAME/DESC below need to change.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${VERSION:?VERSION (e.g. v1.2.3) is required}"
|
||||||
|
: "${TAP_TOKEN:?TAP_TOKEN secret is required}"
|
||||||
|
|
||||||
|
TAP_REPO_URL="https://tap:${TAP_TOKEN}@git.morlana.online/f.weber/homebrew-tap.git"
|
||||||
|
TAP_BRANCH="main"
|
||||||
|
|
||||||
|
BIN_NAME="vault-tui"
|
||||||
|
DESC="Terminal UI and CLI for HashiCorp Vault"
|
||||||
|
SRC_HOMEPAGE="https://git.morlana.online/f.weber/vault-tui"
|
||||||
|
RELEASE_BASE="${SRC_HOMEPAGE}/releases/download/${VERSION}"
|
||||||
|
DIST="dist"
|
||||||
|
VER_NUM="${VERSION#v}" # Formula/manifest "version" fields conventionally omit the leading v
|
||||||
|
|
||||||
|
sha() { # sha <checksums-file> <asset-filename>
|
||||||
|
grep " $2\$" "$1" | awk '{print $1}'
|
||||||
|
}
|
||||||
|
|
||||||
|
CK="${DIST}/checksums-${BIN_NAME}-${VERSION}.txt"
|
||||||
|
CK_CLOUD="${DIST}/checksums-${BIN_NAME}-cloud-${VERSION}.txt"
|
||||||
|
|
||||||
|
SHA_LINUX_AMD64=$(sha "$CK" "${BIN_NAME}-${VERSION}-linux-amd64.tar.gz")
|
||||||
|
SHA_LINUX_ARM64=$(sha "$CK" "${BIN_NAME}-${VERSION}-linux-arm64.tar.gz")
|
||||||
|
SHA_DARWIN_AMD64=$(sha "$CK" "${BIN_NAME}-${VERSION}-darwin-amd64.tar.gz")
|
||||||
|
SHA_DARWIN_ARM64=$(sha "$CK" "${BIN_NAME}-${VERSION}-darwin-arm64.tar.gz")
|
||||||
|
SHA_WINDOWS_AMD64=$(sha "$CK" "${BIN_NAME}-${VERSION}-windows-amd64.zip")
|
||||||
|
|
||||||
|
SHA_CLOUD_LINUX_AMD64=$(sha "$CK_CLOUD" "${BIN_NAME}-cloud-${VERSION}-linux-amd64.tar.gz")
|
||||||
|
SHA_CLOUD_LINUX_ARM64=$(sha "$CK_CLOUD" "${BIN_NAME}-cloud-${VERSION}-linux-arm64.tar.gz")
|
||||||
|
SHA_CLOUD_DARWIN_AMD64=$(sha "$CK_CLOUD" "${BIN_NAME}-cloud-${VERSION}-darwin-amd64.tar.gz")
|
||||||
|
SHA_CLOUD_DARWIN_ARM64=$(sha "$CK_CLOUD" "${BIN_NAME}-cloud-${VERSION}-darwin-arm64.tar.gz")
|
||||||
|
SHA_CLOUD_WINDOWS_AMD64=$(sha "$CK_CLOUD" "${BIN_NAME}-cloud-${VERSION}-windows-amd64.zip")
|
||||||
|
|
||||||
|
WORKDIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$WORKDIR"' EXIT
|
||||||
|
|
||||||
|
git clone --depth 1 --branch "$TAP_BRANCH" "$TAP_REPO_URL" "$WORKDIR"
|
||||||
|
cd "$WORKDIR"
|
||||||
|
mkdir -p Formula bucket
|
||||||
|
|
||||||
|
render_formula() { # render_formula <class> <bin-prefix> <install-name>
|
||||||
|
local class="$1" prefix="$2" install_name="$3"
|
||||||
|
local sha_l_amd sha_l_arm sha_d_amd sha_d_arm
|
||||||
|
if [ "$prefix" = "${BIN_NAME}-cloud" ]; then
|
||||||
|
sha_l_amd="$SHA_CLOUD_LINUX_AMD64"; sha_l_arm="$SHA_CLOUD_LINUX_ARM64"
|
||||||
|
sha_d_amd="$SHA_CLOUD_DARWIN_AMD64"; sha_d_arm="$SHA_CLOUD_DARWIN_ARM64"
|
||||||
|
else
|
||||||
|
sha_l_amd="$SHA_LINUX_AMD64"; sha_l_arm="$SHA_LINUX_ARM64"
|
||||||
|
sha_d_amd="$SHA_DARWIN_AMD64"; sha_d_arm="$SHA_DARWIN_ARM64"
|
||||||
|
fi
|
||||||
|
cat <<EOF
|
||||||
|
class ${class} < Formula
|
||||||
|
desc "${DESC}"
|
||||||
|
homepage "${SRC_HOMEPAGE}"
|
||||||
|
version "${VER_NUM}"
|
||||||
|
|
||||||
|
on_macos do
|
||||||
|
if Hardware::CPU.arm?
|
||||||
|
url "${RELEASE_BASE}/${prefix}-${VERSION}-darwin-arm64.tar.gz"
|
||||||
|
sha256 "${sha_d_arm}"
|
||||||
|
else
|
||||||
|
url "${RELEASE_BASE}/${prefix}-${VERSION}-darwin-amd64.tar.gz"
|
||||||
|
sha256 "${sha_d_amd}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
on_linux do
|
||||||
|
if Hardware::CPU.arm?
|
||||||
|
url "${RELEASE_BASE}/${prefix}-${VERSION}-linux-arm64.tar.gz"
|
||||||
|
sha256 "${sha_l_arm}"
|
||||||
|
else
|
||||||
|
url "${RELEASE_BASE}/${prefix}-${VERSION}-linux-amd64.tar.gz"
|
||||||
|
sha256 "${sha_l_amd}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def install
|
||||||
|
Dir["${prefix}-*"].each { |f| bin.install f => "${install_name}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
test do
|
||||||
|
assert_match version.to_s, shell_output("#{bin}/${install_name} version")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
render_scoop() { # render_scoop <bin-prefix> <exe-name> <sha256>
|
||||||
|
local prefix="$1" exe_name="$2" sha="$3"
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"version": "${VER_NUM}",
|
||||||
|
"description": "${DESC}",
|
||||||
|
"homepage": "${SRC_HOMEPAGE}",
|
||||||
|
"url": "${RELEASE_BASE}/${prefix}-${VERSION}-windows-amd64.zip",
|
||||||
|
"hash": "${sha}",
|
||||||
|
"bin": [["${prefix}-${VERSION}-windows-amd64.exe", "${exe_name}.exe"]]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
render_formula "VaultTui" "${BIN_NAME}" "${BIN_NAME}" >Formula/vault-tui.rb
|
||||||
|
render_formula "VaultTuiCloud" "${BIN_NAME}-cloud" "${BIN_NAME}-cloud" >Formula/vault-tui-cloud.rb
|
||||||
|
render_scoop "${BIN_NAME}" "vault-tui" "${SHA_WINDOWS_AMD64}" >bucket/vault-tui.json
|
||||||
|
render_scoop "${BIN_NAME}-cloud" "vault-tui-cloud" "${SHA_CLOUD_WINDOWS_AMD64}" >bucket/vault-tui-cloud.json
|
||||||
|
|
||||||
|
git config user.name "vault-tui-release"
|
||||||
|
git config user.email "vault-tui-bot@users.noreply.git.morlana.online"
|
||||||
|
git add Formula/vault-tui.rb Formula/vault-tui-cloud.rb bucket/vault-tui.json bucket/vault-tui-cloud.json
|
||||||
|
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "tap already up to date with ${VERSION}, nothing to push"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "vault-tui ${VERSION}"
|
||||||
|
git push origin "HEAD:${TAP_BRANCH}"
|
||||||
|
echo "pushed vault-tui ${VERSION} to the tap"
|
||||||
@@ -26,9 +26,12 @@ jobs:
|
|||||||
run: make test
|
run: make test
|
||||||
|
|
||||||
- name: Build all artifacts (default + cloud, all platforms)
|
- name: Build all artifacts (default + cloud, all platforms)
|
||||||
|
env:
|
||||||
|
VERSION: ${{ gitea.event.release.tag_name }}
|
||||||
|
PRERELEASE: ${{ gitea.event.release.prerelease }}
|
||||||
run: |
|
run: |
|
||||||
make release
|
make release VERSION="$VERSION" PRERELEASE="$PRERELEASE"
|
||||||
make release-cloud
|
make release-cloud VERSION="$VERSION" PRERELEASE="$PRERELEASE"
|
||||||
|
|
||||||
- name: Upload artifacts to release
|
- name: Upload artifacts to release
|
||||||
env:
|
env:
|
||||||
@@ -43,3 +46,13 @@ jobs:
|
|||||||
-F "attachment=@${f};filename=${name}" \
|
-F "attachment=@${f};filename=${name}" \
|
||||||
"${api}?name=${name}"
|
"${api}?name=${name}"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Homebrew/Linuxbrew tap + Scoop bucket: only for stable releases —
|
||||||
|
# a prerelease shouldn't become what `brew install`/`scoop install`
|
||||||
|
# resolve to by default. See .gitea/scripts/publish-tap.sh.
|
||||||
|
- name: Publish Homebrew/Scoop tap
|
||||||
|
if: gitea.event.release.prerelease != true
|
||||||
|
env:
|
||||||
|
VERSION: ${{ gitea.event.release.tag_name }}
|
||||||
|
TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
|
||||||
|
run: .gitea/scripts/publish-tap.sh
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testing:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: make check
|
||||||
@@ -7,10 +7,11 @@ DIST := dist
|
|||||||
PREFIX ?= $(HOME)/.local
|
PREFIX ?= $(HOME)/.local
|
||||||
GOBIN ?= $(PREFIX)/bin
|
GOBIN ?= $(PREFIX)/bin
|
||||||
|
|
||||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||||
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo none)
|
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo none)
|
||||||
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
LDFLAGS := -s -w -X main.version=$(VERSION)
|
PRERELEASE ?= false
|
||||||
|
LDFLAGS := -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE) -X main.prerelease=$(PRERELEASE)
|
||||||
GOFLAGS := -trimpath
|
GOFLAGS := -trimpath
|
||||||
CGO_ENABLED ?= 0
|
CGO_ENABLED ?= 0
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ help:
|
|||||||
@echo ""
|
@echo ""
|
||||||
@awk 'BEGIN {FS = ":.*## "} /^[a-zA-Z0-9_-]+:.*## / {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
@awk 'BEGIN {FS = ":.*## "} /^[a-zA-Z0-9_-]+:.*## / {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Variables: TAGS=cloud (adds cloud auth to any target), PREFIX, GOBIN, PLATFORMS, VERSION"
|
@echo "Variables: TAGS=cloud (adds cloud auth to any target), PREFIX, GOBIN, PLATFORMS, VERSION, PRERELEASE"
|
||||||
|
|
||||||
## build: build the default binary (no cloud auth) into dist/
|
## build: build the default binary (no cloud auth) into dist/
|
||||||
build: ## build the default binary (no cloud auth) into dist/
|
build: ## build the default binary (no cloud auth) into dist/
|
||||||
|
|||||||
+15
-3
@@ -10,11 +10,23 @@ import (
|
|||||||
_ "git.morlana.online/f.weber/vault-tui/internal/ui" // registers the "ui" command's runner
|
_ "git.morlana.online/f.weber/vault-tui/internal/ui" // registers the "ui" command's runner
|
||||||
)
|
)
|
||||||
|
|
||||||
// version is overridden at build time via -ldflags "-X main.version=...".
|
// version, commit, date, and prerelease are overridden at build time via
|
||||||
var version = "dev"
|
// -ldflags (see the Makefile's LDFLAGS and the release Gitea Actions
|
||||||
|
// workflow, which sets PRERELEASE from the triggering release's flag).
|
||||||
|
var (
|
||||||
|
version = "dev"
|
||||||
|
commit = "none"
|
||||||
|
date = "unknown"
|
||||||
|
prerelease = "false"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
root := cli.Root(version)
|
root := cli.Root(cli.BuildInfo{
|
||||||
|
Version: version,
|
||||||
|
Commit: commit,
|
||||||
|
Date: date,
|
||||||
|
Prerelease: prerelease == "true",
|
||||||
|
})
|
||||||
if err := root.Run(context.Background(), os.Args); err != nil {
|
if err := root.Run(context.Background(), os.Args); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "error:", err)
|
fmt.Fprintln(os.Stderr, "error:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
+5
-4
@@ -91,11 +91,12 @@ func bootstrap(cmd *cli.Command) (*App, error) {
|
|||||||
// First-run UX: if there is genuinely no config yet, an interactive
|
// First-run UX: if there is genuinely no config yet, an interactive
|
||||||
// session gets an inline wizard instead of a wall of "no profile
|
// session gets an inline wizard instead of a wall of "no profile
|
||||||
// configured" errors — but never for `config ...` itself (that
|
// configured" errors — but never for `config ...` itself (that
|
||||||
// subtree has its own explicit `init`) and never in a non-interactive
|
// subtree has its own explicit `init`), never for `version` (which
|
||||||
// context (CI, pipes), where the tool must stay driven entirely by
|
// shouldn't need a Vault connection at all), and never in a
|
||||||
// flags/env instead of blocking on stdin.
|
// non-interactive context (CI, pipes), where the tool must stay
|
||||||
|
// driven entirely by flags/env instead of blocking on stdin.
|
||||||
leaf := cmd.Args().First()
|
leaf := cmd.Args().First()
|
||||||
if !config.Exists(path) && IsInteractive() && leaf != "config" {
|
if !config.Exists(path) && IsInteractive() && leaf != "config" && leaf != "version" {
|
||||||
if _, err := runWizard(path); err != nil {
|
if _, err := runWizard(path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import (
|
|||||||
// Root builds the full command tree. DefaultCommand launches the TUI when
|
// Root builds the full command tree. DefaultCommand launches the TUI when
|
||||||
// vault-tui is invoked with no subcommand, matching the plan's "possible
|
// vault-tui is invoked with no subcommand, matching the plan's "possible
|
||||||
// without any subcommand" requirement.
|
// without any subcommand" requirement.
|
||||||
func Root(version string) *cli.Command {
|
func Root(b BuildInfo) *cli.Command {
|
||||||
|
build = b
|
||||||
root := &cli.Command{
|
root := &cli.Command{
|
||||||
Name: "vault-tui",
|
Name: "vault-tui",
|
||||||
Usage: "Terminal UI for HashiCorp Vault",
|
Usage: "Terminal UI for HashiCorp Vault",
|
||||||
Version: version,
|
Version: b.Version,
|
||||||
DefaultCommand: "ui",
|
DefaultCommand: "ui",
|
||||||
// Deliberately no Sources: cli.EnvVars(...) here: internal/config.Resolve
|
// Deliberately no Sources: cli.EnvVars(...) here: internal/config.Resolve
|
||||||
// (and config.ResolvePath/SelectProfile for --config/--profile) already
|
// (and config.ResolvePath/SelectProfile for --config/--profile) already
|
||||||
@@ -49,6 +50,7 @@ func Root(version string) *cli.Command {
|
|||||||
loginCommand(),
|
loginCommand(),
|
||||||
logoutCommand(),
|
logoutCommand(),
|
||||||
statusCommand(),
|
statusCommand(),
|
||||||
|
versionCommand(),
|
||||||
listCommand(),
|
listCommand(),
|
||||||
readCommand(),
|
readCommand(),
|
||||||
writeCommand(),
|
writeCommand(),
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildInfo carries version metadata baked in at build time via -ldflags
|
||||||
|
// (see the Makefile's LDFLAGS/PRERELEASE variables and the release Gitea
|
||||||
|
// Actions workflow, which sets PRERELEASE from the triggering release's
|
||||||
|
// "prerelease" flag). Root stores the value it was given so versionCommand
|
||||||
|
// and internal/ui's footer (via Build) can read it back without threading
|
||||||
|
// it through every call site.
|
||||||
|
type BuildInfo struct {
|
||||||
|
Version string // git tag (or "dev" for untagged/local builds)
|
||||||
|
Commit string
|
||||||
|
Date string
|
||||||
|
Prerelease bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var build BuildInfo
|
||||||
|
|
||||||
|
// Build returns the BuildInfo Root was constructed with.
|
||||||
|
func Build() BuildInfo { return build }
|
||||||
|
|
||||||
|
// String renders "1.2.3 (commit abc1234, built 2026-08-14T12:00:00Z)",
|
||||||
|
// appending " [prerelease]" when the triggering Gitea release was marked
|
||||||
|
// as a prerelease.
|
||||||
|
func (b BuildInfo) String() string {
|
||||||
|
s := fmt.Sprintf("%s (commit %s, built %s)", valueOr(b.Version, "dev"), valueOr(b.Commit, "none"), valueOr(b.Date, "unknown"))
|
||||||
|
if b.Prerelease {
|
||||||
|
s += " [prerelease]"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionCommand prints build metadata. Unlike every other command it does
|
||||||
|
// not touch a.Client/a.Store — see bootstrap's leaf-skip check, which keeps
|
||||||
|
// `vault-tui version` from tripping the first-run config wizard.
|
||||||
|
func versionCommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "version",
|
||||||
|
Usage: "print version, commit, build date, and prerelease status",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{Name: "json"},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
b := Build()
|
||||||
|
if cmd.Bool("json") {
|
||||||
|
return printJSON(os.Stdout, map[string]any{
|
||||||
|
"version": b.Version,
|
||||||
|
"commit": b.Commit,
|
||||||
|
"date": b.Date,
|
||||||
|
"prerelease": b.Prerelease,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fmt.Println(b.String())
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"charm.land/bubbles/v2/help"
|
"charm.land/bubbles/v2/help"
|
||||||
"charm.land/bubbles/v2/key"
|
"charm.land/bubbles/v2/key"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
|
||||||
|
"git.morlana.online/f.weber/vault-tui/internal/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
// screenHelp is the shape every keys.KeyMap.XxxHelp() screen-help value
|
// screenHelp is the shape every keys.KeyMap.XxxHelp() screen-help value
|
||||||
@@ -156,7 +158,12 @@ func (m *Model) viewHeader() string {
|
|||||||
// status line (profile/address, or the current toast) — one line total on
|
// status line (profile/address, or the current toast) — one line total on
|
||||||
// sizeTiny, two otherwise.
|
// sizeTiny, two otherwise.
|
||||||
func (m *Model) viewFooter(sh screenHelp) string {
|
func (m *Model) viewFooter(sh screenHelp) string {
|
||||||
status := fmt.Sprintf("%s @ %s", m.a.Settings.Profile, m.a.Settings.Address)
|
b := cli.Build()
|
||||||
|
ver := b.Version
|
||||||
|
if b.Prerelease {
|
||||||
|
ver += " (prerelease)"
|
||||||
|
}
|
||||||
|
status := fmt.Sprintf("%s @ %s · %s", m.a.Settings.Profile, m.a.Settings.Address, ver)
|
||||||
if toastStr := m.toastView(); toastStr != "" {
|
if toastStr := m.toastView(); toastStr != "" {
|
||||||
status = toastStr
|
status = toastStr
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user