-
Notifications
You must be signed in to change notification settings - Fork 0
feat(release): publish checksums and verify them before installing #3707
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,6 +120,64 @@ get_latest_version() { | |
|
|
||
| # Download file silently | ||
| # Download file silently | ||
| # Hash a file with whichever SHA-256 tool the platform ships. | ||
| sha256_of() { | ||
| if command -v sha256sum >/dev/null 2>&1; then | ||
| sha256sum "$1" | awk '{print $1}' | ||
| elif command -v shasum >/dev/null 2>&1; then | ||
| shasum -a 256 "$1" | awk '{print $1}' | ||
| else | ||
| return 1 | ||
| fi | ||
| } | ||
|
|
||
| # Verify a staged download against the release's published SHA256SUMS. | ||
| # | ||
| # Fails closed: an unverified binary is not installed. Releases published before | ||
| # the manifest existed have no SHA256SUMS asset, so pinning to one of those needs | ||
| # the escape hatch, which has to be set deliberately. | ||
| verify_checksum() { | ||
| FILE="$1" | ||
| NAME="$2" | ||
| VER="$3" | ||
| WORK="$4" | ||
|
|
||
| if [ "${VERYFRONT_INSTALL_SKIP_CHECKSUM:-}" = "1" ]; then | ||
| printf "\r${ORANGE}Skipping checksum verification (VERYFRONT_INSTALL_SKIP_CHECKSUM=1)${NC}\n" | ||
| return 0 | ||
| fi | ||
|
|
||
| SUMS_URL="https://github.com/${REPO}/releases/download/v${VER}/SHA256SUMS" | ||
| SUMS_FILE="${WORK}/SHA256SUMS" | ||
|
|
||
| if ! download "$SUMS_URL" "$SUMS_FILE" 2>/dev/null; then | ||
| printf "\r%s\n" "Install failed: no SHA256SUMS published for v${VER}." >&2 | ||
| echo " The binary was downloaded but not installed, because it could not be verified." >&2 | ||
| echo " Releases published before checksums existed have no manifest." >&2 | ||
| echo " To install anyway, re-run with VERYFRONT_INSTALL_SKIP_CHECKSUM=1." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| EXPECTED=$(awk -v want="$NAME" '$2 == want || $2 == "*" want { print $1; exit }' "$SUMS_FILE") | ||
| if [ -z "$EXPECTED" ]; then | ||
| printf "\r%s\n" "Install failed: ${NAME} is not listed in SHA256SUMS for v${VER}." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| ACTUAL=$(sha256_of "$FILE") || { | ||
| printf "\r%s\n" "Install failed: no sha256sum or shasum available to verify the download." >&2 | ||
| exit 1 | ||
| } | ||
|
|
||
| if [ "$ACTUAL" != "$EXPECTED" ]; then | ||
| printf "\r%s\n" "Install failed: checksum mismatch for ${NAME}." >&2 | ||
| echo " expected ${EXPECTED}" >&2 | ||
| echo " actual ${ACTUAL}" >&2 | ||
| echo " The download was discarded and nothing was installed." >&2 | ||
| exit 1 | ||
| fi | ||
| } | ||
|
|
||
| download() { | ||
| URL="$1" | ||
| DEST="$2" | ||
|
|
@@ -158,12 +216,21 @@ main() { | |
| # Create install directory | ||
| mkdir -p "$INSTALL_DIR" | ||
|
|
||
| # Download binary | ||
| BINARY_PATH="${INSTALL_DIR}/veryfront" | ||
|
|
||
| # Download to a staging file first: a binary is only moved into place after its | ||
| # checksum matches, so a truncated or tampered download never becomes the | ||
| # installed executable. | ||
| STAGING_DIR=$(mktemp -d "${TMPDIR:-/tmp}/veryfront-install.XXXXXX") || { | ||
| echo "Error: could not create a temporary directory" >&2 | ||
| exit 1 | ||
| } | ||
| trap 'rm -rf "$STAGING_DIR"' EXIT INT TERM | ||
| STAGED_BINARY="${STAGING_DIR}/${BINARY_NAME}" | ||
|
Comment on lines
+224
to
+229
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- files ---'
git ls-files scripts/install.sh scripts/install.ps1
printf '%s\n' '--- shell outline ---'
ast-grep outline scripts/install.sh --lang bash 2>/dev/null || true
printf '%s\n' '--- PowerShell outline ---'
ast-grep outline scripts/install.ps1 --lang powershell 2>/dev/null || true
printf '%s\n' '--- shell relevant sections ---'
cat -n scripts/install.sh | sed -n '150,290p'
printf '%s\n' '--- PowerShell relevant sections ---'
cat -n scripts/install.ps1 | sed -n '1,180p'
printf '%s\n' '--- installer references ---'
rg -n 'STAGING_DIR|STAGED_BINARY|INSTALL_DIR|stagingDir|stagedBinary|Move-Item|mv |install ' scripts/install.sh scripts/install.ps1Repository: veryfront/veryfront-code Length of output: 14441 🌐 Web query:
💡 Result: GNU Coreutils mv When moving files or directories across filesystem boundaries, the GNU coreutils mv command is not atomic [1][2]. Because the rename(2) system call is restricted to a single filesystem, mv must fallback to a copy-and-delete strategy [3][4]. This process involves copying the source data to the destination and subsequently removing the original [3][5]. If this sequence is interrupted (e.g., by a process failure, system crash, or lack of permissions during the deletion phase), data may remain in both locations or the operation may partially fail [3][1]. While mv attempts to ensure that if the copy fails, the partial destination data is cleaned up, it does not provide transactional atomicity across the entire operation [3]. PowerShell Move-Item The Move-Item cmdlet follows similar operational logic when moving items across volumes or boundaries that do not support a direct, atomic move [6][7]. - Mechanism: Move-Item attempts a direct move (often utilizing the underlying MoveTo method) [7][8]. If this fails—typically due to an IOException such as an EXDEV error (cross-device link)—it falls back to a copy-and-delete approach [7][8][9]. - Overwrite Semantics: By default, if the destination path already exists, Move-Item raises an error [6][10]. To overwrite an existing item, the -Force parameter must be used [6]. - Behavior nuances: Move-Item's implementation of -Force and its fallback behavior can vary depending on the provider [6]. Users should be aware that because the fallback method is not atomic, errors during the deletion of the source item after a successful copy can leave data in both locations [6][7]. In some PowerShell versions, incorrect fallback triggers were identified as regressions, reinforcing that the "move" is not inherently atomic when it involves cross-volume or complex path logic [7][8]. Top results: [3][1][6][7][8][2] Citations:
🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import os
import tempfile
from pathlib import Path
# Validate the shell-side path construction and whether mktemp accepts the
# installation-directory template. Use only an isolated temporary directory.
with tempfile.TemporaryDirectory() as root:
install_dir = Path(root) / "install dir"
install_dir.mkdir()
template = str(install_dir / ".veryfront-install.XXXXXX")
print("template:", template)
print("same st_dev as parent:", os.stat(install_dir).st_dev == os.stat(root).st_dev)
# Check the platform's mktemp behavior without invoking the installer.
import subprocess
result = subprocess.run(
["mktemp", "-d", template],
text=True, capture_output=True, check=False,
)
print("mktemp exit:", result.returncode)
print("mktemp stderr:", result.stderr.strip())
if result.returncode == 0:
staged = Path(result.stdout.strip()) / "veryfront-linux"
print("staging parent:", staged.parent.parent)
print("staging directory exists:", staged.parent.is_dir())
print("staging directory device matches install:", os.stat(staged.parent).st_dev == os.stat(install_dir).st_dev)
staged.parent.rmdir()
PY
printf '%s\n' '--- local command versions ---'
mv --version | head -n 2
pwsh --version 2>/dev/null || true
powershell -Version 2 2>/dev/null || trueRepository: veryfront/veryfront-code Length of output: 531 Create staging directories on the installation filesystem. When the destination uses a different filesystem or volume from the system temporary directory,
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| # Download with spinner | ||
| printf "${ORANGE}Installing Veryfront v%s...${NC}" "$VERSION" | ||
| download "$DOWNLOAD_URL" "$BINARY_PATH" & | ||
| download "$DOWNLOAD_URL" "$STAGED_BINARY" & | ||
| PID=$! | ||
| SPINNER='|/-\' | ||
| i=0 | ||
|
|
@@ -179,7 +246,10 @@ main() { | |
| exit 1 | ||
| fi | ||
|
|
||
| chmod +x "$BINARY_PATH" | ||
| verify_checksum "$STAGED_BINARY" "$BINARY_NAME" "$VERSION" "$STAGING_DIR" | ||
|
|
||
| chmod +x "$STAGED_BINARY" | ||
| mv -f "$STAGED_BINARY" "$BINARY_PATH" | ||
|
|
||
| # Add to PATH if not already there | ||
| NEEDS_SOURCE="" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
--versionselects an RC release, this fails every installation because the prerelease workflow in.github/workflows/cicd.ymllines 788-818 uploads the binaries andinstall.shbut noSHA256SUMS; only the stable-release path generates the manifest. Consequently, RC users must disable the new verification entirely withVERYFRONT_INSTALL_SKIP_CHECKSUM=1. Generate and upload the same manifest in the prerelease job so version-pinned RC installs remain usable and verified.Useful? React with 👍 / 👎.