fix(release): resolve final review findings
- commit
- 9a2054f2fb88ea10251f48e005248878d16c5e34
- author
- brice · · 2026-07-17 07:47 UTC
- parent
- eb936d6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +146 -32
M .github/workflows/release.yml +4 -2
| @@ -4,11 +4,13 @@ on: | |||
| 4 | 4 | push: | |
| 5 | 5 | tags: ['v*'] | |
| 6 | 6 | ||
| 7 | - | permissions: | |
| 8 | - | contents: write | |
| 7 | + | # Default to no token permissions; release alone needs GitHub Release writes. | |
| 8 | + | permissions: {} | |
| 9 | 9 | ||
| 10 | 10 | jobs: | |
| 11 | 11 | release: | |
| 12 | + | permissions: | |
| 13 | + | contents: write | |
| 12 | 14 | # Pinned by SHA deliberately: this workflow receives the AUR key. | |
| 13 | 15 | # Bump the pin manually when bnema/gh-actions changes. | |
| 14 | 16 | 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: write | 8 | + | 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@fcda8139e65ec45af95bc385e318351c86712d48 | 16 | uses: bnema/gh-actions/.github/workflows/go-release.yml@fcda8139e65ec45af95bc385e318351c86712d48 | ||
M .goreleaser.yaml +10 -6
| @@ -52,11 +52,11 @@ changelog: | |||
| 52 | 52 | use: github | |
| 53 | 53 | filters: | |
| 54 | 54 | exclude: | |
| 55 | - | - '^docs:' | |
| 56 | - | - '^test:' | |
| 57 | - | - '^ci:' | |
| 58 | - | - '^chore:' | |
| 59 | - | - '^build\(deps\):' | |
| 55 | + | - '^docs(\(.+\))?:' | |
| 56 | + | - '^test(\(.+\))?:' | |
| 57 | + | - '^ci(\(.+\))?:' | |
| 58 | + | - '^chore(\(.+\))?:' | |
| 59 | + | - '^build\(deps.*\):' | |
| 60 | 60 | - Merge pull request | |
| 61 | 61 | - Merge branch | |
| 62 | 62 | groups: | |
| @@ -91,7 +91,7 @@ release: | |||
| 91 | 91 | ||
| 92 | 92 | ### Script (Linux x86_64, macOS arm64) | |
| 93 | 93 | ```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 | |
| 95 | 95 | ``` | |
| 96 | 96 | ||
| 97 | 97 | ### Arch Linux | |
| @@ -109,7 +109,11 @@ release: | |||
| 109 | 109 | xattr -d com.apple.quarantine ~/.local/bin/vev | |
| 110 | 110 | ``` | |
| 111 | 111 | ||
| 112 | + | {{- if .PreviousTag }} | |
| 112 | 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 | 118 | git: | |
| 115 | 119 | tag_sort: smartsemver | |
| @@ -52,11 +52,11 @@ changelog: | |||||
| 52 | use: github | 52 | 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 request | 60 | - Merge pull request | ||
| 61 | - Merge branch | 61 | - 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 | ```sh | 93 | ```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 |
| 95 | ``` | 95 | ``` | ||
| 96 | 96 | ||||
| 97 | ### Arch Linux | 97 | ### Arch Linux | ||
| @@ -109,7 +109,11 @@ release: | |||||
| 109 | xattr -d com.apple.quarantine ~/.local/bin/vev | 109 | 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: smartsemver | 119 | tag_sort: smartsemver | ||
M RELEASING.md +16 -7
| @@ -11,8 +11,16 @@ AUR for `-rc.*`/`-alpha.*` prerelease tags). | |||
| 11 | 11 | 1. main is green on both CI legs (ubuntu, macos-14). | |
| 12 | 12 | 2. Tag and push. Watch: `gh run watch`. | |
| 13 | 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` | |
| 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. | |
| 16 | 24 | 5. Stable tags only: confirm https://aur.archlinux.org/packages/vev-bin | |
| 17 | 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 | 30 | only the PKGBUILD push failed (usually SSH/key). Fix the cause, then either: | |
| 23 | 31 | ||
| 24 | 32 | - 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. | |
| 30 | 39 | ||
| 31 | 40 | ## If AUR publishes but GitHub fails | |
| 32 | 41 | ||
| @@ -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-bin | 24 | 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`), which | 32 | - Re-run the failed workflow job (`gh run rerun <run-id> --failed`), which | ||
| 25 | - | re-executes goreleaser idempotently (`replace_existing_draft`, same tag), or | 33 | + | 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 clone | 35 | + | `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 generated | 36 | + | 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 fails | 40 | ## If AUR publishes but GitHub fails | ||
| 32 | 41 | ||||
M install.sh +113 -12
| @@ -58,24 +58,125 @@ info "checksum verified" | |||
| 58 | 58 | # --- Extract + install ----------------------------------------------------- | |
| 59 | 59 | tar -xzf "$TARBALL" | |
| 60 | 60 | ||
| 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" | |
| 65 | 84 | 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" | |
| 68 | 87 | fi | |
| 69 | 88 | } | |
| 70 | 89 | ||
| 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 | |
| 76 | 177 | ||
| 77 | 178 | info "installed: $INSTALL_DIR/vev" | |
| 78 | - | case ":$PATH:" in | |
| 179 | + | case ":${PATH:-}:" in | |
| 79 | 180 | *":$INSTALL_DIR:"*) "$INSTALL_DIR/vev" --version ;; | |
| 80 | 181 | *) info "note: $INSTALL_DIR is not in your PATH" ;; | |
| 81 | 182 | 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 dir | 62 | + | # are checked and staged before a live file is replaced. |
| 63 | - | if [ -w "$2" ]; then | 63 | + | 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 | else | 84 | 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 | fi | 87 | fi | ||
| 69 | } | 88 | } | ||
| 70 | 89 | ||||
| 71 | - | mkdir -p "$INSTALL_DIR" 2>/dev/null || true | 90 | + | 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:" in | 179 | + | 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 | esac | 182 | esac | ||
M internal/app/version_test.go +3 -5
| @@ -17,10 +17,8 @@ func TestVersionDefaults(t *testing.T) { | |||
| 17 | 17 | } | |
| 18 | 18 | ||
| 19 | 19 | 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) | |
| 25 | 23 | } | |
| 26 | 24 | } | |
| @@ -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 | } | ||