detached 9a2054f

fix(release): resolve final review findings

commit
9a2054f2fb88ea10251f48e005248878d16c5e34
author
brice · · 2026-07-17 07:47 UTC
parent
eb936d6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +146 -32
M .github/workflows/release.yml +4 -2
@@ -4,11 +4,13 @@ on:
44 push:
55 tags: ['v*']
66
7-permissions:
8- contents: write
7+# Default to no token permissions; release alone needs GitHub Release writes.
8+permissions: {}
99
1010 jobs:
1111 release:
12+ permissions:
13+ contents: write
1214 # Pinned by SHA deliberately: this workflow receives the AUR key.
1315 # Bump the pin manually when bnema/gh-actions changes.
1416 uses: bnema/gh-actions/.github/workflows/go-release.yml@fcda8139e65ec45af95bc385e318351c86712d48
@@ -4,11 +4,13 @@ on:
4 push:4 push:
5 tags: ['v*']5 tags: ['v*']
6 6
7-permissions:7+# Default to no token permissions; release alone needs GitHub Release writes.
8- contents: write8+permissions: {}
9 9
10 jobs:10 jobs:
11 release:11 release:
12+ permissions:
13+ contents: write
12 # Pinned by SHA deliberately: this workflow receives the AUR key.14 # Pinned by SHA deliberately: this workflow receives the AUR key.
13 # Bump the pin manually when bnema/gh-actions changes.15 # Bump the pin manually when bnema/gh-actions changes.
14 uses: bnema/gh-actions/.github/workflows/go-release.yml@fcda8139e65ec45af95bc385e318351c86712d4816 uses: bnema/gh-actions/.github/workflows/go-release.yml@fcda8139e65ec45af95bc385e318351c86712d48
M .goreleaser.yaml +10 -6
@@ -52,11 +52,11 @@ changelog:
5252 use: github
5353 filters:
5454 exclude:
55- - '^docs:'
56- - '^test:'
57- - '^ci:'
58- - '^chore:'
59- - '^build\(deps\):'
55+ - '^docs(\(.+\))?:'
56+ - '^test(\(.+\))?:'
57+ - '^ci(\(.+\))?:'
58+ - '^chore(\(.+\))?:'
59+ - '^build\(deps.*\):'
6060 - Merge pull request
6161 - Merge branch
6262 groups:
@@ -91,7 +91,7 @@ release:
9191
9292 ### Script (Linux x86_64, macOS arm64)
9393 ```sh
94- curl -fsSL https://raw.githubusercontent.com/bnema/vev/main/install.sh | sh
94+ curl -fsSL https://raw.githubusercontent.com/bnema/vev/{{ .Tag }}/install.sh | sh
9595 ```
9696
9797 ### Arch Linux
@@ -109,7 +109,11 @@ release:
109109 xattr -d com.apple.quarantine ~/.local/bin/vev
110110 ```
111111
112+ {{- if .PreviousTag }}
112113 **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 }}
113117
114118 git:
115119 tag_sort: smartsemver
@@ -52,11 +52,11 @@ changelog:
52 use: github52 use: github
53 filters:53 filters:
54 exclude:54 exclude:
55- - '^docs:'55+ - '^docs(\(.+\))?:'
56- - '^test:'56+ - '^test(\(.+\))?:'
57- - '^ci:'57+ - '^ci(\(.+\))?:'
58- - '^chore:'58+ - '^chore(\(.+\))?:'
59- - '^build\(deps\):'59+ - '^build\(deps.*\):'
60 - Merge pull request60 - Merge pull request
61 - Merge branch61 - Merge branch
62 groups:62 groups:
@@ -91,7 +91,7 @@ release:
91 91
92 ### Script (Linux x86_64, macOS arm64)92 ### Script (Linux x86_64, macOS arm64)
93 ```sh93 ```sh
94- curl -fsSL https://raw.githubusercontent.com/bnema/vev/main/install.sh | sh94+ curl -fsSL https://raw.githubusercontent.com/bnema/vev/{{ .Tag }}/install.sh | sh
95 ```95 ```
96 96
97 ### Arch Linux97 ### Arch Linux
@@ -109,7 +109,11 @@ release:
109 xattr -d com.apple.quarantine ~/.local/bin/vev109 xattr -d com.apple.quarantine ~/.local/bin/vev
110 ```110 ```
111 111
112+ {{- if .PreviousTag }}
112 **Full Changelog**: https://github.com/bnema/vev/compare/{{ .PreviousTag }}...{{ .Tag }}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 }}
113 117
114 git:118 git:
115 tag_sort: smartsemver119 tag_sort: smartsemver
M RELEASING.md +16 -7
@@ -11,8 +11,16 @@ AUR for `-rc.*`/`-alpha.*` prerelease tags).
1111 1. main is green on both CI legs (ubuntu, macos-14).
1212 2. Tag and push. Watch: `gh run watch`.
1313 3. Verify the release page: two archives + `checksums.txt` + grouped changelog.
14-4. `curl -fsSL https://raw.githubusercontent.com/bnema/vev/main/install.sh | sh`
15- on a Linux box; confirm `vev --version` prints the tag.
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 the exact tag.
1624 5. Stable tags only: confirm https://aur.archlinux.org/packages/vev-bin
1725 picked up the new version.
1826
@@ -22,11 +30,12 @@ Do **not** delete or re-push the tag. The GitHub release is the source of truth;
2230 only the PKGBUILD push failed (usually SSH/key). Fix the cause, then either:
2331
2432 - Re-run the failed workflow job (`gh run rerun <run-id> --failed`), which
25- re-executes goreleaser idempotently (`replace_existing_draft`, same tag), or
26-- Push the PKGBUILD by hand: `goreleaser release --clean --skip=validate`
27- locally with `AUR_KEY` exported, or clone
28- `ssh://aur@aur.archlinux.org/vev-bin.git` and commit the PKGBUILD generated
29- under `dist/`.
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.
3039
3140 ## If AUR publishes but GitHub fails
3241
@@ -11,8 +11,16 @@ AUR for `-rc.*`/`-alpha.*` prerelease tags).
11 1. main is green on both CI legs (ubuntu, macos-14).11 1. main is green on both CI legs (ubuntu, macos-14).
12 2. Tag and push. Watch: `gh run watch`.12 2. Tag and push. Watch: `gh run watch`.
13 3. Verify the release page: two archives + `checksums.txt` + grouped changelog.13 3. Verify the release page: two archives + `checksums.txt` + grouped changelog.
14-4. `curl -fsSL https://raw.githubusercontent.com/bnema/vev/main/install.sh | sh`14+4. On Linux amd64, download the installer from the exact tag, inspect it,
15- on a Linux box; confirm `vev --version` prints the tag.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 the exact tag.
16 5. Stable tags only: confirm https://aur.archlinux.org/packages/vev-bin24 5. Stable tags only: confirm https://aur.archlinux.org/packages/vev-bin
17 picked up the new version.25 picked up the new version.
18 26
@@ -22,11 +30,12 @@ Do **not** delete or re-push the tag. The GitHub release is the source of truth;
22 only the PKGBUILD push failed (usually SSH/key). Fix the cause, then either:30 only the PKGBUILD push failed (usually SSH/key). Fix the cause, then either:
23 31
24 - Re-run the failed workflow job (`gh run rerun <run-id> --failed`), which32 - Re-run the failed workflow job (`gh run rerun <run-id> --failed`), which
25- re-executes goreleaser idempotently (`replace_existing_draft`, same tag), or33+ re-executes goreleaser idempotently for the same tag, or
26-- Push the PKGBUILD by hand: `goreleaser release --clean --skip=validate`34+- For manual recovery, clone
27- locally with `AUR_KEY` exported, or clone35+ `ssh://aur@aur.archlinux.org/vev-bin.git`, update its existing PKGBUILD for
28- `ssh://aur@aur.archlinux.org/vev-bin.git` and commit the PKGBUILD generated36+ the exact tag using that GitHub Release's `checksums.txt`, and regenerate
29- under `dist/`.37+ `.SRCINFO` with `makepkg --printsrcinfo > .SRCINFO`. Review the diff, then
38+ commit and push only the AUR repository.
30 39
31 ## If AUR publishes but GitHub fails40 ## If AUR publishes but GitHub fails
32 41
M install.sh +113 -12
@@ -58,24 +58,125 @@ info "checksum verified"
5858 # --- Extract + install -----------------------------------------------------
5959 tar -xzf "$TARBALL"
6060
61-do_install() {
62- # $1 = src, $2 = dst dir
63- if [ -w "$2" ]; then
64- install -m 755 "$1" "$2/"
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"
6584 else
66- info "escalating with sudo to write into $2"
67- sudo install -m 755 "$1" "$2/"
85+ info "escalating with sudo to create $INSTALL_DIR"
86+ sudo mkdir -p "$INSTALL_DIR" || err "cannot create $INSTALL_DIR"
6887 fi
6988 }
7089
71-mkdir -p "$INSTALL_DIR" 2>/dev/null || true
72-do_install vev "$INSTALL_DIR"
73-# Default status-bar segments; vev's default config references them by name.
74-do_install scripts/vev-bar-top-right "$INSTALL_DIR"
75-do_install scripts/vev-bar-bottom-right "$INSTALL_DIR"
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
76177
77178 info "installed: $INSTALL_DIR/vev"
78-case ":$PATH:" in
179+case ":${PATH:-}:" in
79180 *":$INSTALL_DIR:"*) "$INSTALL_DIR/vev" --version ;;
80181 *) info "note: $INSTALL_DIR is not in your PATH" ;;
81182 esac
@@ -58,24 +58,125 @@ info "checksum verified"
58 # --- Extract + install -----------------------------------------------------58 # --- Extract + install -----------------------------------------------------
59 tar -xzf "$TARBALL"59 tar -xzf "$TARBALL"
60 60
61-do_install() {61+# Keep this list in the archive's required installation order. All payloads
62- # $1 = src, $2 = dst dir62+# are checked and staged before a live file is replaced.
63- if [ -w "$2" ]; then63+PAYLOADS="vev scripts/vev-bar-top-right scripts/vev-bar-bottom-right"
64- install -m 755 "$1" "$2/"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"
65 else84 else
66- info "escalating with sudo to write into $2"85+ info "escalating with sudo to create $INSTALL_DIR"
67- sudo install -m 755 "$1" "$2/"86+ sudo mkdir -p "$INSTALL_DIR" || err "cannot create $INSTALL_DIR"
68 fi87 fi
69 }88 }
70 89
71-mkdir -p "$INSTALL_DIR" 2>/dev/null || true90+stage_path() {
72-do_install vev "$INSTALL_DIR"91+ printf '%s/%s-%s.new\n' "$INSTALL_DIR" "$TRANSACTION" "${1##*/}"
73-# Default status-bar segments; vev's default config references them by name.92+}
74-do_install scripts/vev-bar-top-right "$INSTALL_DIR"93+
75-do_install scripts/vev-bar-bottom-right "$INSTALL_DIR"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
76 177
77 info "installed: $INSTALL_DIR/vev"178 info "installed: $INSTALL_DIR/vev"
78-case ":$PATH:" in179+case ":${PATH:-}:" in
79 *":$INSTALL_DIR:"*) "$INSTALL_DIR/vev" --version ;;180 *":$INSTALL_DIR:"*) "$INSTALL_DIR/vev" --version ;;
80 *) info "note: $INSTALL_DIR is not in your PATH" ;;181 *) info "note: $INSTALL_DIR is not in your PATH" ;;
81 esac182 esac
M internal/app/version_test.go +3 -5
@@ -17,10 +17,8 @@ func TestVersionDefaults(t *testing.T) {
1717 }
1818
1919 func TestVersionLine(t *testing.T) {
20- got := versionLine()
21- for _, part := range []string{version, commit, date} {
22- if !strings.Contains(got, part) {
23- t.Fatalf("versionLine() = %q, missing %q", got, part)
24- }
20+ want := "vev " + version + " (commit " + commit + ", built " + date + ")"
21+ if got := versionLine(); got != want {
22+ t.Fatalf("versionLine() = %q, want %q", got, want)
2523 }
2624 }
@@ -17,10 +17,8 @@ func TestVersionDefaults(t *testing.T) {
17 }17 }
18 18
19 func TestVersionLine(t *testing.T) {19 func TestVersionLine(t *testing.T) {
20- got := versionLine()20+ want := "vev " + version + " (commit " + commit + ", built " + date + ")"
21- for _, part := range []string{version, commit, date} {21+ if got := versionLine(); got != want {
22- if !strings.Contains(got, part) {22+ t.Fatalf("versionLine() = %q, want %q", got, want)
23- t.Fatalf("versionLine() = %q, missing %q", got, part)
24- }
25 }23 }
26 }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 12

Tags 11

↑↓ moveenter openesc close