detached 834b4af

Merge pull request #103 from bnema/feat/release-ci

ci: goreleaser release pipeline (linux/amd64 + darwin/arm64)
commit
834b4af983f2c1c6c9df84227d0a17ae41b5a48c
author
bnema · · 2026-07-17 10:33 UTC
committer
GitHub · · 2026-07-17 10:33 UTC
merge of
4ab3b08 + af7817b · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
6 files · +433 -4
A .github/workflows/release.yml +21 -0
@@ -0,0 +1,21 @@
1+name: release
2+
3+on:
4+ push:
5+ tags: ['v*']
6+
7+# Default to no token permissions; release alone needs GitHub Release writes.
8+permissions: {}
9+
10+jobs:
11+ release:
12+ permissions:
13+ contents: write
14+ # Pinned by SHA deliberately: this workflow receives the AUR key.
15+ # Bump the pin manually when bnema/gh-actions changes.
16+ uses: bnema/gh-actions/.github/workflows/go-release.yml@fcda8139e65ec45af95bc385e318351c86712d48
17+ with:
18+ go-version-file: go.mod
19+ goreleaser-version: '~> v2'
20+ secrets:
21+ AUR_KEY: ${{ secrets.AUR_KEY }}
@@ -0,0 +1,21 @@
1+name: release
2+
3+on:
4+ push:
5+ tags: ['v*']
6+
7+# Default to no token permissions; release alone needs GitHub Release writes.
8+permissions: {}
9+
10+jobs:
11+ release:
12+ permissions:
13+ contents: write
14+ # Pinned by SHA deliberately: this workflow receives the AUR key.
15+ # Bump the pin manually when bnema/gh-actions changes.
16+ uses: bnema/gh-actions/.github/workflows/go-release.yml@fcda8139e65ec45af95bc385e318351c86712d48
17+ with:
18+ go-version-file: go.mod
19+ goreleaser-version: '~> v2'
20+ secrets:
21+ AUR_KEY: ${{ secrets.AUR_KEY }}
A .goreleaser.yaml +142 -0
@@ -0,0 +1,142 @@
1+version: 2
2+
3+project_name: vev
4+
5+before:
6+ hooks:
7+ - go mod tidy
8+
9+builds:
10+ - id: vev
11+ main: .
12+ binary: vev
13+ env:
14+ - CGO_ENABLED=0
15+ goos:
16+ - linux
17+ - darwin
18+ goarch:
19+ - amd64
20+ - arm64
21+ ignore:
22+ - goos: linux
23+ goarch: arm64
24+ - goos: darwin
25+ goarch: amd64
26+ ldflags:
27+ - -s -w
28+ - -X github.com/bnema/vev/internal/app.version={{.Version}}
29+ - -X github.com/bnema/vev/internal/app.commit={{.ShortCommit}}
30+ - -X github.com/bnema/vev/internal/app.date={{.Date}}
31+
32+archives:
33+ - id: default
34+ # Contract consumed by install.sh and the AUR package block:
35+ # vev_linux_x86_64.tar.gz / vev_darwin_arm64.tar.gz
36+ name_template: >-
37+ {{ .ProjectName }}_{{ .Os }}_{{ if eq .Arch "amd64" }}x86_64{{ else }}{{ .Arch }}{{ end }}
38+ formats: ['tar.gz']
39+ files:
40+ - LICENSE
41+ - README.md
42+ - scripts/vev-bar-top-right
43+ - scripts/vev-bar-bottom-right
44+
45+checksum:
46+ # Fixed name: install.sh downloads .../download/<tag>/checksums.txt
47+ name_template: checksums.txt
48+ algorithm: sha256
49+
50+changelog:
51+ sort: asc
52+ use: github
53+ filters:
54+ exclude:
55+ - '^docs(\(.+\))?:'
56+ - '^test(\(.+\))?:'
57+ - '^ci(\(.+\))?:'
58+ - '^chore(\(.+\))?:'
59+ - '^build\(deps.*\):'
60+ - Merge pull request
61+ - Merge branch
62+ groups:
63+ - title: Features
64+ regexp: '^feat(\(.+\))?:'
65+ order: 0
66+ - title: Bug Fixes
67+ regexp: '^fix(\(.+\))?:'
68+ order: 1
69+ - title: Performance
70+ regexp: '^perf(\(.+\))?:'
71+ order: 2
72+ - title: Refactoring
73+ regexp: '^refactor(\(.+\))?:'
74+ order: 3
75+ - title: Other Changes
76+ order: 999
77+
78+release:
79+ github:
80+ owner: bnema
81+ name: vev
82+ draft: false
83+ replace_existing_draft: true
84+ replace_existing_artifacts: true
85+ prerelease: auto
86+ make_latest: true
87+ footer: |
88+ ---
89+
90+ ## Install
91+
92+ ### Script (Linux x86_64, macOS arm64)
93+ ```sh
94+ curl -fsSL https://raw.githubusercontent.com/bnema/vev/{{ .Tag }}/install.sh | VEV_VERSION={{ .Tag }} sh
95+ ```
96+
97+ ### Arch Linux
98+ ```sh
99+ yay -S vev-bin
100+ ```
101+
102+ ### macOS note
103+ The darwin binary is ad-hoc signed by the Go toolchain, not notarized.
104+ Installing via the script or `curl` avoids the quarantine attribute;
105+ if you downloaded the archive with a browser, Gatekeeper will block it.
106+ Removing the attribute bypasses a Gatekeeper security control — only do
107+ it for the vev binary itself, after verifying its checksum:
108+ ```sh
109+ xattr -d com.apple.quarantine ~/.local/bin/vev
110+ ```
111+
112+ {{- if .PreviousTag }}
113+ **Full Changelog**: https://github.com/bnema/vev/compare/{{ .PreviousTag }}...{{ .Tag }}
114+ {{- else }}
115+ **Full Changelog**: https://github.com/bnema/vev/releases/tag/{{ .Tag }}
116+ {{- end }}
117+
118+git:
119+ tag_sort: smartsemver
120+
121+aurs:
122+ - name: vev-bin
123+ ids: ['default']
124+ homepage: https://github.com/bnema/vev
125+ description: Terminal multiplexer with a per-user daemon, server-side rendering, and minimal socket/SSH diffs
126+ maintainers:
127+ - bnema <b at bnema dot dev>
128+ license: MIT
129+ private_key: '{{ .Env.AUR_KEY }}'
130+ git_url: ssh://aur@aur.archlinux.org/vev-bin.git
131+ # auto: prerelease tags (v1.2.3-rc.1) are not pushed to AUR.
132+ skip_upload: auto
133+ provides:
134+ - vev
135+ conflicts:
136+ - vev
137+ package: |-
138+ install -Dm755 "./vev" "${pkgdir}/usr/bin/vev"
139+ install -Dm755 "./scripts/vev-bar-top-right" "${pkgdir}/usr/bin/vev-bar-top-right"
140+ install -Dm755 "./scripts/vev-bar-bottom-right" "${pkgdir}/usr/bin/vev-bar-bottom-right"
141+ install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/vev-bin/LICENSE"
142+ commit_msg_template: 'Update to {{ .Tag }}'
@@ -0,0 +1,142 @@
1+version: 2
2+
3+project_name: vev
4+
5+before:
6+ hooks:
7+ - go mod tidy
8+
9+builds:
10+ - id: vev
11+ main: .
12+ binary: vev
13+ env:
14+ - CGO_ENABLED=0
15+ goos:
16+ - linux
17+ - darwin
18+ goarch:
19+ - amd64
20+ - arm64
21+ ignore:
22+ - goos: linux
23+ goarch: arm64
24+ - goos: darwin
25+ goarch: amd64
26+ ldflags:
27+ - -s -w
28+ - -X github.com/bnema/vev/internal/app.version={{.Version}}
29+ - -X github.com/bnema/vev/internal/app.commit={{.ShortCommit}}
30+ - -X github.com/bnema/vev/internal/app.date={{.Date}}
31+
32+archives:
33+ - id: default
34+ # Contract consumed by install.sh and the AUR package block:
35+ # vev_linux_x86_64.tar.gz / vev_darwin_arm64.tar.gz
36+ name_template: >-
37+ {{ .ProjectName }}_{{ .Os }}_{{ if eq .Arch "amd64" }}x86_64{{ else }}{{ .Arch }}{{ end }}
38+ formats: ['tar.gz']
39+ files:
40+ - LICENSE
41+ - README.md
42+ - scripts/vev-bar-top-right
43+ - scripts/vev-bar-bottom-right
44+
45+checksum:
46+ # Fixed name: install.sh downloads .../download/<tag>/checksums.txt
47+ name_template: checksums.txt
48+ algorithm: sha256
49+
50+changelog:
51+ sort: asc
52+ use: github
53+ filters:
54+ exclude:
55+ - '^docs(\(.+\))?:'
56+ - '^test(\(.+\))?:'
57+ - '^ci(\(.+\))?:'
58+ - '^chore(\(.+\))?:'
59+ - '^build\(deps.*\):'
60+ - Merge pull request
61+ - Merge branch
62+ groups:
63+ - title: Features
64+ regexp: '^feat(\(.+\))?:'
65+ order: 0
66+ - title: Bug Fixes
67+ regexp: '^fix(\(.+\))?:'
68+ order: 1
69+ - title: Performance
70+ regexp: '^perf(\(.+\))?:'
71+ order: 2
72+ - title: Refactoring
73+ regexp: '^refactor(\(.+\))?:'
74+ order: 3
75+ - title: Other Changes
76+ order: 999
77+
78+release:
79+ github:
80+ owner: bnema
81+ name: vev
82+ draft: false
83+ replace_existing_draft: true
84+ replace_existing_artifacts: true
85+ prerelease: auto
86+ make_latest: true
87+ footer: |
88+ ---
89+
90+ ## Install
91+
92+ ### Script (Linux x86_64, macOS arm64)
93+ ```sh
94+ curl -fsSL https://raw.githubusercontent.com/bnema/vev/{{ .Tag }}/install.sh | VEV_VERSION={{ .Tag }} sh
95+ ```
96+
97+ ### Arch Linux
98+ ```sh
99+ yay -S vev-bin
100+ ```
101+
102+ ### macOS note
103+ The darwin binary is ad-hoc signed by the Go toolchain, not notarized.
104+ Installing via the script or `curl` avoids the quarantine attribute;
105+ if you downloaded the archive with a browser, Gatekeeper will block it.
106+ Removing the attribute bypasses a Gatekeeper security control — only do
107+ it for the vev binary itself, after verifying its checksum:
108+ ```sh
109+ xattr -d com.apple.quarantine ~/.local/bin/vev
110+ ```
111+
112+ {{- if .PreviousTag }}
113+ **Full Changelog**: https://github.com/bnema/vev/compare/{{ .PreviousTag }}...{{ .Tag }}
114+ {{- else }}
115+ **Full Changelog**: https://github.com/bnema/vev/releases/tag/{{ .Tag }}
116+ {{- end }}
117+
118+git:
119+ tag_sort: smartsemver
120+
121+aurs:
122+ - name: vev-bin
123+ ids: ['default']
124+ homepage: https://github.com/bnema/vev
125+ description: Terminal multiplexer with a per-user daemon, server-side rendering, and minimal socket/SSH diffs
126+ maintainers:
127+ - bnema <b at bnema dot dev>
128+ license: MIT
129+ private_key: '{{ .Env.AUR_KEY }}'
130+ git_url: ssh://aur@aur.archlinux.org/vev-bin.git
131+ # auto: prerelease tags (v1.2.3-rc.1) are not pushed to AUR.
132+ skip_upload: auto
133+ provides:
134+ - vev
135+ conflicts:
136+ - vev
137+ package: |-
138+ install -Dm755 "./vev" "${pkgdir}/usr/bin/vev"
139+ install -Dm755 "./scripts/vev-bar-top-right" "${pkgdir}/usr/bin/vev-bar-top-right"
140+ install -Dm755 "./scripts/vev-bar-bottom-right" "${pkgdir}/usr/bin/vev-bar-bottom-right"
141+ install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/vev-bin/LICENSE"
142+ commit_msg_template: 'Update to {{ .Tag }}'
A RELEASING.md +50 -0
@@ -0,0 +1,50 @@
1+# Releasing vev
2+
3+Releases are cut by tagging: `git tag v0.X.Y && git push origin v0.X.Y`.
4+The `release` workflow cross-compiles linux/amd64 + darwin/arm64
5+(`CGO_ENABLED=0`), publishes a GitHub Release with `checksums.txt`, and pushes
6+the `vev-bin` PKGBUILD to AUR (stable tags only — `skip_upload: auto` skips
7+AUR for `-rc.*`/`-alpha.*` prerelease tags).
8+
9+## Checklist
10+
11+1. main is green on both CI legs (ubuntu, macos-14).
12+2. Tag and push. Watch: `gh run watch`.
13+3. Verify the release page: two archives + `checksums.txt` + grouped changelog.
14+4. On Linux amd64, download the installer from the exact tag, inspect it,
15+ then run it for that same version:
16+ ```sh
17+ installer=$(mktemp)
18+ curl -fsSL https://raw.githubusercontent.com/bnema/vev/v0.X.Y/install.sh -o "$installer"
19+ less "$installer"
20+ VEV_VERSION=v0.X.Y sh "$installer"
21+ rm -f "$installer"
22+ ```
23+ Confirm `vev --version` prints `vev 0.X.Y (commit ..., built ...)`.
24+5. Stable tags only: confirm https://aur.archlinux.org/packages/vev-bin
25+ picked up the new version.
26+
27+## If GitHub publishes but AUR fails
28+
29+Do **not** delete or re-push the tag. The GitHub release is the source of truth;
30+only the PKGBUILD push failed (usually SSH/key). Fix the cause, then either:
31+
32+- Re-run the failed workflow job (`gh run rerun <run-id> --failed`), which
33+ re-executes goreleaser idempotently for the same tag, or
34+- For manual recovery, clone
35+ `ssh://aur@aur.archlinux.org/vev-bin.git`, update its existing PKGBUILD for
36+ the exact tag using that GitHub Release's `checksums.txt`, and regenerate
37+ `.SRCINFO` with `makepkg --printsrcinfo > .SRCINFO`. Review the diff, then
38+ commit and push only the AUR repository.
39+
40+## If AUR publishes but GitHub fails
41+
42+Do **not** delete or re-push the tag. Fix the GitHub workflow failure and rerun
43+it (`gh run rerun <run-id> --failed`); if necessary, repair the GitHub Release
44+manually with the same tag's archives and `checksums.txt`, then verify both
45+publication targets before proceeding.
46+
47+## Version stamping
48+
49+`vev --version` reads `internal/app.{version,commit,date}`, stamped via
50+ldflags in `.goreleaser.yaml`. Local builds report `0.1.0-dev`.
@@ -0,0 +1,50 @@
1+# Releasing vev
2+
3+Releases are cut by tagging: `git tag v0.X.Y && git push origin v0.X.Y`.
4+The `release` workflow cross-compiles linux/amd64 + darwin/arm64
5+(`CGO_ENABLED=0`), publishes a GitHub Release with `checksums.txt`, and pushes
6+the `vev-bin` PKGBUILD to AUR (stable tags only — `skip_upload: auto` skips
7+AUR for `-rc.*`/`-alpha.*` prerelease tags).
8+
9+## Checklist
10+
11+1. main is green on both CI legs (ubuntu, macos-14).
12+2. Tag and push. Watch: `gh run watch`.
13+3. Verify the release page: two archives + `checksums.txt` + grouped changelog.
14+4. On Linux amd64, download the installer from the exact tag, inspect it,
15+ then run it for that same version:
16+ ```sh
17+ installer=$(mktemp)
18+ curl -fsSL https://raw.githubusercontent.com/bnema/vev/v0.X.Y/install.sh -o "$installer"
19+ less "$installer"
20+ VEV_VERSION=v0.X.Y sh "$installer"
21+ rm -f "$installer"
22+ ```
23+ Confirm `vev --version` prints `vev 0.X.Y (commit ..., built ...)`.
24+5. Stable tags only: confirm https://aur.archlinux.org/packages/vev-bin
25+ picked up the new version.
26+
27+## If GitHub publishes but AUR fails
28+
29+Do **not** delete or re-push the tag. The GitHub release is the source of truth;
30+only the PKGBUILD push failed (usually SSH/key). Fix the cause, then either:
31+
32+- Re-run the failed workflow job (`gh run rerun <run-id> --failed`), which
33+ re-executes goreleaser idempotently for the same tag, or
34+- For manual recovery, clone
35+ `ssh://aur@aur.archlinux.org/vev-bin.git`, update its existing PKGBUILD for
36+ the exact tag using that GitHub Release's `checksums.txt`, and regenerate
37+ `.SRCINFO` with `makepkg --printsrcinfo > .SRCINFO`. Review the diff, then
38+ commit and push only the AUR repository.
39+
40+## If AUR publishes but GitHub fails
41+
42+Do **not** delete or re-push the tag. Fix the GitHub workflow failure and rerun
43+it (`gh run rerun <run-id> --failed`); if necessary, repair the GitHub Release
44+manually with the same tag's archives and `checksums.txt`, then verify both
45+publication targets before proceeding.
46+
47+## Version stamping
48+
49+`vev --version` reads `internal/app.{version,commit,date}`, stamped via
50+ldflags in `.goreleaser.yaml`. Local builds report `0.1.0-dev`.
A install.sh +182 -0
@@ -0,0 +1,182 @@
1+#!/bin/sh
2+# vev installer — Linux x86_64 and macOS arm64.
3+# Usage: curl -fsSL https://raw.githubusercontent.com/bnema/vev/main/install.sh | sh
4+# Env: VEV_VERSION=vX.Y.Z to pin a version (default: latest release).
5+set -eu
6+
7+REPO="bnema/vev"
8+VERSION="${VEV_VERSION:-latest}"
9+
10+info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
11+err() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
12+
13+# --- Detect platform -------------------------------------------------------
14+OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
15+ARCH="$(uname -m)"
16+case "$OS/$ARCH" in
17+ linux/x86_64|linux/amd64) TARBALL="vev_linux_x86_64.tar.gz" ;;
18+ darwin/arm64) TARBALL="vev_darwin_arm64.tar.gz" ;;
19+ *) err "unsupported platform: $OS/$ARCH (supported: linux/x86_64, darwin/arm64)" ;;
20+esac
21+
22+# --- Pick install dir ------------------------------------------------------
23+if [ -d "$HOME/.local/bin" ]; then
24+ INSTALL_DIR="$HOME/.local/bin"
25+else
26+ INSTALL_DIR="/usr/local/bin"
27+fi
28+
29+# --- Resolve version -------------------------------------------------------
30+if [ "$VERSION" = "latest" ]; then
31+ VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
32+ | grep '"tag_name"' | head -1 | cut -d '"' -f 4)"
33+ [ -n "$VERSION" ] || err "could not resolve the latest release tag"
34+fi
35+info "installing vev ${VERSION} (${OS}/${ARCH}) to ${INSTALL_DIR}"
36+
37+# --- Download + verify -----------------------------------------------------
38+TMP_DIR="$(mktemp -d)"
39+trap 'rm -rf "$TMP_DIR"' EXIT
40+
41+BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
42+curl -fsSL -o "$TMP_DIR/$TARBALL" "$BASE_URL/$TARBALL" \
43+ || err "download failed: $BASE_URL/$TARBALL"
44+curl -fsSL -o "$TMP_DIR/checksums.txt" "$BASE_URL/checksums.txt" \
45+ || err "download failed: $BASE_URL/checksums.txt"
46+
47+cd "$TMP_DIR"
48+EXPECTED="$(grep " $TARBALL\$" checksums.txt | cut -d ' ' -f 1)"
49+[ -n "$EXPECTED" ] || err "$TARBALL not found in checksums.txt"
50+if command -v sha256sum >/dev/null 2>&1; then
51+ ACTUAL="$(sha256sum "$TARBALL" | cut -d ' ' -f 1)"
52+else
53+ ACTUAL="$(shasum -a 256 "$TARBALL" | cut -d ' ' -f 1)"
54+fi
55+[ "$EXPECTED" = "$ACTUAL" ] || err "checksum mismatch for $TARBALL (expected $EXPECTED, got $ACTUAL)"
56+info "checksum verified"
57+
58+# --- Extract + install -----------------------------------------------------
59+tar -xzf "$TARBALL"
60+
61+# Keep this list in the archive's required installation order. All payloads
62+# are checked and staged before a live file is replaced.
63+PAYLOADS="vev scripts/vev-bar-top-right scripts/vev-bar-bottom-right"
64+TRANSACTION=".vev-install-$$"
65+
66+path_exists() {
67+ [ -e "$1" ] || [ -L "$1" ]
68+}
69+
70+install_path() {
71+ # Run every destination mutation through the same privilege path.
72+ if [ -w "$INSTALL_DIR" ]; then
73+ "$@"
74+ else
75+ sudo "$@"
76+ fi
77+}
78+
79+ensure_install_dir() {
80+ [ -d "$INSTALL_DIR" ] && return 0
81+
82+ if [ -w "$(dirname "$INSTALL_DIR")" ]; then
83+ mkdir -p "$INSTALL_DIR" || err "cannot create $INSTALL_DIR"
84+ else
85+ info "escalating with sudo to create $INSTALL_DIR"
86+ sudo mkdir -p "$INSTALL_DIR" || err "cannot create $INSTALL_DIR"
87+ fi
88+}
89+
90+stage_path() {
91+ printf '%s/%s-%s.new\n' "$INSTALL_DIR" "$TRANSACTION" "${1##*/}"
92+}
93+
94+backup_path() {
95+ printf '%s/%s-%s.bak\n' "$INSTALL_DIR" "$TRANSACTION" "${1##*/}"
96+}
97+
98+remove_destination_file() {
99+ install_path rm -f "$1"
100+}
101+
102+cleanup_staging() {
103+ for payload in $PAYLOADS; do
104+ stage="$(stage_path "$payload")"
105+ remove_destination_file "$stage" || return 1
106+ done
107+}
108+
109+rollback_install() {
110+ rollback_ok=true
111+
112+ for payload in $PAYLOADS; do
113+ name="${payload##*/}"
114+ target="$INSTALL_DIR/$name"
115+ stage="$(stage_path "$payload")"
116+ backup="$(backup_path "$payload")"
117+
118+ if path_exists "$backup"; then
119+ # A prior file was moved aside: remove the new one and put it back.
120+ remove_destination_file "$target" || rollback_ok=false
121+ install_path mv "$backup" "$target" || rollback_ok=false
122+ elif ! path_exists "$stage"; then
123+ # No backup means the original was absent. A missing stage was moved
124+ # into place, so restore that original absence.
125+ remove_destination_file "$target" || rollback_ok=false
126+ fi
127+ remove_destination_file "$stage" || rollback_ok=false
128+ done
129+
130+ [ "$rollback_ok" = true ]
131+}
132+
133+preflight_payloads() {
134+ for payload in $PAYLOADS; do
135+ [ -f "$payload" ] && [ -r "$payload" ] \
136+ || err "archive is missing readable payload: $payload"
137+ done
138+}
139+
140+# Do not create or alter the destination until every archive payload exists.
141+preflight_payloads
142+ensure_install_dir
143+
144+# Stage every payload in the destination first, so each later mv is a
145+# same-filesystem replacement. A staging failure leaves live files untouched.
146+for source in $PAYLOADS; do
147+ stage="$(stage_path "$source")"
148+ if ! install_path install -m 755 "$source" "$stage"; then
149+ cleanup_staging || err "failed to stage payloads and clean staging files"
150+ err "failed to stage payload: $source"
151+ fi
152+done
153+
154+# Replace each live file only after all three staged copies are ready. Existing
155+# files are retained as backups until the full transaction succeeds.
156+for source in $PAYLOADS; do
157+ name="${source##*/}"
158+ target="$INSTALL_DIR/$name"
159+ stage="$(stage_path "$source")"
160+ backup="$(backup_path "$source")"
161+
162+ if path_exists "$target" && ! install_path mv "$target" "$backup"; then
163+ rollback_install || err "failed to preserve $target and roll back installation"
164+ err "failed to preserve existing file: $target"
165+ fi
166+ if ! install_path mv "$stage" "$target"; then
167+ rollback_install || err "failed to replace $target and roll back installation"
168+ err "failed to install payload: $source"
169+ fi
170+done
171+
172+for source in $PAYLOADS; do
173+ backup="$(backup_path "$source")"
174+ remove_destination_file "$backup" \
175+ || err "installed payloads but failed to clean backup: $backup"
176+done
177+
178+info "installed: $INSTALL_DIR/vev"
179+case ":${PATH:-}:" in
180+ *":$INSTALL_DIR:"*) "$INSTALL_DIR/vev" --version ;;
181+ *) info "note: $INSTALL_DIR is not in your PATH" ;;
182+esac
@@ -0,0 +1,182 @@
1+#!/bin/sh
2+# vev installer — Linux x86_64 and macOS arm64.
3+# Usage: curl -fsSL https://raw.githubusercontent.com/bnema/vev/main/install.sh | sh
4+# Env: VEV_VERSION=vX.Y.Z to pin a version (default: latest release).
5+set -eu
6+
7+REPO="bnema/vev"
8+VERSION="${VEV_VERSION:-latest}"
9+
10+info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
11+err() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
12+
13+# --- Detect platform -------------------------------------------------------
14+OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
15+ARCH="$(uname -m)"
16+case "$OS/$ARCH" in
17+ linux/x86_64|linux/amd64) TARBALL="vev_linux_x86_64.tar.gz" ;;
18+ darwin/arm64) TARBALL="vev_darwin_arm64.tar.gz" ;;
19+ *) err "unsupported platform: $OS/$ARCH (supported: linux/x86_64, darwin/arm64)" ;;
20+esac
21+
22+# --- Pick install dir ------------------------------------------------------
23+if [ -d "$HOME/.local/bin" ]; then
24+ INSTALL_DIR="$HOME/.local/bin"
25+else
26+ INSTALL_DIR="/usr/local/bin"
27+fi
28+
29+# --- Resolve version -------------------------------------------------------
30+if [ "$VERSION" = "latest" ]; then
31+ VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
32+ | grep '"tag_name"' | head -1 | cut -d '"' -f 4)"
33+ [ -n "$VERSION" ] || err "could not resolve the latest release tag"
34+fi
35+info "installing vev ${VERSION} (${OS}/${ARCH}) to ${INSTALL_DIR}"
36+
37+# --- Download + verify -----------------------------------------------------
38+TMP_DIR="$(mktemp -d)"
39+trap 'rm -rf "$TMP_DIR"' EXIT
40+
41+BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
42+curl -fsSL -o "$TMP_DIR/$TARBALL" "$BASE_URL/$TARBALL" \
43+ || err "download failed: $BASE_URL/$TARBALL"
44+curl -fsSL -o "$TMP_DIR/checksums.txt" "$BASE_URL/checksums.txt" \
45+ || err "download failed: $BASE_URL/checksums.txt"
46+
47+cd "$TMP_DIR"
48+EXPECTED="$(grep " $TARBALL\$" checksums.txt | cut -d ' ' -f 1)"
49+[ -n "$EXPECTED" ] || err "$TARBALL not found in checksums.txt"
50+if command -v sha256sum >/dev/null 2>&1; then
51+ ACTUAL="$(sha256sum "$TARBALL" | cut -d ' ' -f 1)"
52+else
53+ ACTUAL="$(shasum -a 256 "$TARBALL" | cut -d ' ' -f 1)"
54+fi
55+[ "$EXPECTED" = "$ACTUAL" ] || err "checksum mismatch for $TARBALL (expected $EXPECTED, got $ACTUAL)"
56+info "checksum verified"
57+
58+# --- Extract + install -----------------------------------------------------
59+tar -xzf "$TARBALL"
60+
61+# Keep this list in the archive's required installation order. All payloads
62+# are checked and staged before a live file is replaced.
63+PAYLOADS="vev scripts/vev-bar-top-right scripts/vev-bar-bottom-right"
64+TRANSACTION=".vev-install-$$"
65+
66+path_exists() {
67+ [ -e "$1" ] || [ -L "$1" ]
68+}
69+
70+install_path() {
71+ # Run every destination mutation through the same privilege path.
72+ if [ -w "$INSTALL_DIR" ]; then
73+ "$@"
74+ else
75+ sudo "$@"
76+ fi
77+}
78+
79+ensure_install_dir() {
80+ [ -d "$INSTALL_DIR" ] && return 0
81+
82+ if [ -w "$(dirname "$INSTALL_DIR")" ]; then
83+ mkdir -p "$INSTALL_DIR" || err "cannot create $INSTALL_DIR"
84+ else
85+ info "escalating with sudo to create $INSTALL_DIR"
86+ sudo mkdir -p "$INSTALL_DIR" || err "cannot create $INSTALL_DIR"
87+ fi
88+}
89+
90+stage_path() {
91+ printf '%s/%s-%s.new\n' "$INSTALL_DIR" "$TRANSACTION" "${1##*/}"
92+}
93+
94+backup_path() {
95+ printf '%s/%s-%s.bak\n' "$INSTALL_DIR" "$TRANSACTION" "${1##*/}"
96+}
97+
98+remove_destination_file() {
99+ install_path rm -f "$1"
100+}
101+
102+cleanup_staging() {
103+ for payload in $PAYLOADS; do
104+ stage="$(stage_path "$payload")"
105+ remove_destination_file "$stage" || return 1
106+ done
107+}
108+
109+rollback_install() {
110+ rollback_ok=true
111+
112+ for payload in $PAYLOADS; do
113+ name="${payload##*/}"
114+ target="$INSTALL_DIR/$name"
115+ stage="$(stage_path "$payload")"
116+ backup="$(backup_path "$payload")"
117+
118+ if path_exists "$backup"; then
119+ # A prior file was moved aside: remove the new one and put it back.
120+ remove_destination_file "$target" || rollback_ok=false
121+ install_path mv "$backup" "$target" || rollback_ok=false
122+ elif ! path_exists "$stage"; then
123+ # No backup means the original was absent. A missing stage was moved
124+ # into place, so restore that original absence.
125+ remove_destination_file "$target" || rollback_ok=false
126+ fi
127+ remove_destination_file "$stage" || rollback_ok=false
128+ done
129+
130+ [ "$rollback_ok" = true ]
131+}
132+
133+preflight_payloads() {
134+ for payload in $PAYLOADS; do
135+ [ -f "$payload" ] && [ -r "$payload" ] \
136+ || err "archive is missing readable payload: $payload"
137+ done
138+}
139+
140+# Do not create or alter the destination until every archive payload exists.
141+preflight_payloads
142+ensure_install_dir
143+
144+# Stage every payload in the destination first, so each later mv is a
145+# same-filesystem replacement. A staging failure leaves live files untouched.
146+for source in $PAYLOADS; do
147+ stage="$(stage_path "$source")"
148+ if ! install_path install -m 755 "$source" "$stage"; then
149+ cleanup_staging || err "failed to stage payloads and clean staging files"
150+ err "failed to stage payload: $source"
151+ fi
152+done
153+
154+# Replace each live file only after all three staged copies are ready. Existing
155+# files are retained as backups until the full transaction succeeds.
156+for source in $PAYLOADS; do
157+ name="${source##*/}"
158+ target="$INSTALL_DIR/$name"
159+ stage="$(stage_path "$source")"
160+ backup="$(backup_path "$source")"
161+
162+ if path_exists "$target" && ! install_path mv "$target" "$backup"; then
163+ rollback_install || err "failed to preserve $target and roll back installation"
164+ err "failed to preserve existing file: $target"
165+ fi
166+ if ! install_path mv "$stage" "$target"; then
167+ rollback_install || err "failed to replace $target and roll back installation"
168+ err "failed to install payload: $source"
169+ fi
170+done
171+
172+for source in $PAYLOADS; do
173+ backup="$(backup_path "$source")"
174+ remove_destination_file "$backup" \
175+ || err "installed payloads but failed to clean backup: $backup"
176+done
177+
178+info "installed: $INSTALL_DIR/vev"
179+case ":${PATH:-}:" in
180+ *":$INSTALL_DIR:"*) "$INSTALL_DIR/vev" --version ;;
181+ *) info "note: $INSTALL_DIR is not in your PATH" ;;
182+esac
M internal/app/run.go +14 -4
@@ -101,9 +101,19 @@ usage:
101101 vev --help show this help
102102 vev --version show version`
103103
104-// version is vev's reported build version. Wired here for the MVP; a build
105-// stamp can replace it later.
106-const version = "0.1.0-dev"
104+// Build metadata. Defaults describe a plain `go build`; releases overwrite
105+// them via -ldflags "-X github.com/bnema/vev/internal/app.version=..." (and
106+// .commit / .date) — see .goreleaser.yaml.
107+var (
108+ version = "0.1.0-dev"
109+ commit = "none"
110+ date = "unknown"
111+)
112+
113+// versionLine renders the --version output.
114+func versionLine() string {
115+ return fmt.Sprintf("vev %s (commit %s, built %s)", version, commit, date)
116+}
107117
108118 // Run is the entry point invoked by main. It parses args into a command and
109119 // dispatches it.
@@ -219,7 +229,7 @@ func dispatch(ctx context.Context, cmd command) error {
219229 fmt.Println(usageText)
220230 return nil
221231 case kindVersion:
222- fmt.Println("vev", version)
232+ fmt.Println(versionLine())
223233 return nil
224234 case kindDaemon:
225235 return runDaemon()
@@ -101,9 +101,19 @@ usage:
101 vev --help show this help101 vev --help show this help
102 vev --version show version`102 vev --version show version`
103 103
104-// version is vev's reported build version. Wired here for the MVP; a build104+// Build metadata. Defaults describe a plain `go build`; releases overwrite
105-// stamp can replace it later.105+// them via -ldflags "-X github.com/bnema/vev/internal/app.version=..." (and
106-const version = "0.1.0-dev"106+// .commit / .date) — see .goreleaser.yaml.
107+var (
108+ version = "0.1.0-dev"
109+ commit = "none"
110+ date = "unknown"
111+)
112+
113+// versionLine renders the --version output.
114+func versionLine() string {
115+ return fmt.Sprintf("vev %s (commit %s, built %s)", version, commit, date)
116+}
107 117
108 // Run is the entry point invoked by main. It parses args into a command and118 // Run is the entry point invoked by main. It parses args into a command and
109 // dispatches it.119 // dispatches it.
@@ -219,7 +229,7 @@ func dispatch(ctx context.Context, cmd command) error {
219 fmt.Println(usageText)229 fmt.Println(usageText)
220 return nil230 return nil
221 case kindVersion:231 case kindVersion:
222- fmt.Println("vev", version)232+ fmt.Println(versionLine())
223 return nil233 return nil
224 case kindDaemon:234 case kindDaemon:
225 return runDaemon()235 return runDaemon()
A internal/app/version_test.go +24 -0
@@ -0,0 +1,24 @@
1+package app
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+// The release pipeline stamps these vars via -ldflags -X; the defaults must
9+// stay meaningful for plain `go build`.
10+func TestVersionDefaults(t *testing.T) {
11+ if version == "" || commit == "" || date == "" {
12+ t.Fatalf("version metadata must have non-empty defaults: version=%q commit=%q date=%q", version, commit, date)
13+ }
14+ if !strings.Contains(version, "dev") {
15+ t.Fatalf("unstamped version %q should carry a -dev marker", version)
16+ }
17+}
18+
19+func TestVersionLine(t *testing.T) {
20+ want := "vev " + version + " (commit " + commit + ", built " + date + ")"
21+ if got := versionLine(); got != want {
22+ t.Fatalf("versionLine() = %q, want %q", got, want)
23+ }
24+}
@@ -0,0 +1,24 @@
1+package app
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+// The release pipeline stamps these vars via -ldflags -X; the defaults must
9+// stay meaningful for plain `go build`.
10+func TestVersionDefaults(t *testing.T) {
11+ if version == "" || commit == "" || date == "" {
12+ t.Fatalf("version metadata must have non-empty defaults: version=%q commit=%q date=%q", version, commit, date)
13+ }
14+ if !strings.Contains(version, "dev") {
15+ t.Fatalf("unstamped version %q should carry a -dev marker", version)
16+ }
17+}
18+
19+func TestVersionLine(t *testing.T) {
20+ want := "vev " + version + " (commit " + commit + ", built " + date + ")"
21+ if got := versionLine(); got != want {
22+ t.Fatalf("versionLine() = %q, want %q", got, want)
23+ }
24+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close