Reconcile Gitea distribution with current alpha behavior and release tooling

Hop-State: A_06FN55REVAR7VQG4BWJBCD8
Hop-Proposal: R_06FN55QTEWKH16NSR59YJYG
Hop-Task: T_06FN3MBF98GWD4NA5PA1RWG
Hop-Attempt: AT_06FN55BQ80H2S8WHNDD24G0
This commit is contained in:
Hop
2026-07-11 12:10:59 -07:00
parent 23a4891099
commit de99aad4d4
28 changed files with 1565 additions and 21 deletions
+84
View File
@@ -0,0 +1,84 @@
[CmdletBinding()]
param(
[string]$GiteaUrl = $(if ($env:HOP_GITEA_URL) { $env:HOP_GITEA_URL } else { "https://githop.xyz" }),
[string]$Repository = $(if ($env:HOP_REPOSITORY) { $env:HOP_REPOSITORY } else { "hop/hop" }),
[string]$Version = $(if ($env:HOP_VERSION) { $env:HOP_VERSION } else { "latest" }),
[string]$InstallDir = $(if ($env:HOP_INSTALL_DIR) { $env:HOP_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA "Programs\Hop" }),
[switch]$SkipSkill,
[switch]$SkipPath
)
$ErrorActionPreference = "Stop"
$GiteaUrl = $GiteaUrl.TrimEnd("/")
switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()) {
"X64" { $arch = "amd64" }
"Arm64" { $arch = "arm64" }
default { throw "Unsupported Windows architecture: $($_)" }
}
$asset = "hop_windows_${arch}.zip"
if ($Version -eq "latest") {
# Gitea's /releases/latest endpoint omits prereleases. Select the newest
# published release so the normal installer also works during Hop's alpha.
$releases = @(Invoke-RestMethod -Uri "$GiteaUrl/api/v1/repos/$Repository/releases?draft=false&page=1&limit=1")
$latestRelease = $releases | Select-Object -First 1
$tag = $latestRelease.tag_name
if (-not $tag) { throw "Could not determine the latest published release" }
} else {
$tag = if ($Version.StartsWith("v")) { $Version } else { "v$Version" }
}
if ($tag -notmatch '^[A-Za-z0-9._-]+$') {
throw "Release API returned an unsafe tag: $tag"
}
$releaseUrl = "$GiteaUrl/$Repository/releases/download/$tag"
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("hop-install-" + [guid]::NewGuid())
New-Item -ItemType Directory -Path $tempDir | Out-Null
try {
$archivePath = Join-Path $tempDir $asset
$checksumsPath = Join-Path $tempDir "checksums.txt"
Write-Host "Downloading $asset..."
Invoke-WebRequest -UseBasicParsing -Uri "$releaseUrl/$asset" -OutFile $archivePath
Invoke-WebRequest -UseBasicParsing -Uri "$releaseUrl/checksums.txt" -OutFile $checksumsPath
$escapedAsset = [regex]::Escape($asset)
$checksumLine = Get-Content $checksumsPath | Where-Object { $_ -match "^([a-fA-F0-9]{64})\s+\*?$escapedAsset$" } | Select-Object -First 1
if (-not $checksumLine) { throw "checksums.txt does not contain $asset" }
$expected = ([regex]::Match($checksumLine, "^[a-fA-F0-9]{64}")).Value.ToLowerInvariant()
$actual = (Get-FileHash -Algorithm SHA256 $archivePath).Hash.ToLowerInvariant()
if ($actual -ne $expected) { throw "Checksum verification failed for $asset" }
$extractPath = Join-Path $tempDir "archive"
Expand-Archive -Path $archivePath -DestinationPath $extractPath
$binary = Join-Path $extractPath "hop.exe"
if (-not (Test-Path $binary)) { throw "Release archive does not contain hop.exe" }
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
$installedBinary = Join-Path $InstallDir "hop.exe"
Copy-Item -Force $binary $installedBinary
if (-not $SkipPath) {
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
$pathEntries = @($userPath -split ";" | Where-Object { $_ })
if ($pathEntries -notcontains $InstallDir) {
$newUserPath = (($pathEntries + $InstallDir) -join ";")
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
Write-Host "Added $InstallDir to your user PATH."
}
if (($env:Path -split ";") -notcontains $InstallDir) {
$env:Path = "$InstallDir;$env:Path"
}
}
if (-not $SkipSkill) {
& $installedBinary skill install --force
}
Write-Host "Installed $(& $installedBinary version)"
Write-Host "Binary: $installedBinary"
if (-not $SkipSkill) {
Write-Host "Restart Codex if it is open, then use it normally in any Git repository."
}
} finally {
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir
}
+107
View File
@@ -0,0 +1,107 @@
#!/bin/sh
set -eu
gitea_url=${HOP_GITEA_URL:-https://githop.xyz}
gitea_url=${gitea_url%/}
repository=${HOP_REPOSITORY:-hop/hop}
requested_version=${HOP_VERSION:-latest}
install_dir=${HOP_INSTALL_DIR:-"$HOME/.local/bin"}
install_skill=${HOP_INSTALL_SKILL:-1}
modify_path=${HOP_MODIFY_PATH:-1}
fail() {
printf 'hop installer: %s\n' "$*" >&2
exit 1
}
command -v curl >/dev/null 2>&1 || fail "curl is required"
command -v tar >/dev/null 2>&1 || fail "tar is required"
case $(uname -s) in
Darwin) os=darwin ;;
Linux) os=linux ;;
*) fail "unsupported operating system: $(uname -s)" ;;
esac
case $(uname -m) in
x86_64 | amd64) arch=amd64 ;;
arm64 | aarch64) arch=arm64 ;;
*) fail "unsupported architecture: $(uname -m)" ;;
esac
asset="hop_${os}_${arch}.tar.gz"
if [ "$requested_version" = latest ]; then
# Gitea's /releases/latest endpoint omits prereleases. Hop is distributed as
# an alpha today, so select the newest published release from the list API.
latest_json=$(curl -fL --retry 3 --proto '=https' --tlsv1.2 \
"$gitea_url/api/v1/repos/$repository/releases?draft=false&page=1&limit=1")
tag=$(printf '%s' "$latest_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
[ -n "$tag" ] || fail "could not determine the latest published release"
else
case "$requested_version" in
v*) tag=$requested_version ;;
*) tag="v${requested_version}" ;;
esac
fi
case "$tag" in
*[!A-Za-z0-9._-]*) fail "release API returned an unsafe tag: $tag" ;;
esac
release_url="$gitea_url/$repository/releases/download/${tag}"
tmp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t hop-install)
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT HUP INT TERM
printf 'Downloading %s...\n' "$asset"
curl -fL --retry 3 --proto '=https' --tlsv1.2 \
-o "$tmp_dir/$asset" "$release_url/$asset"
curl -fL --retry 3 --proto '=https' --tlsv1.2 \
-o "$tmp_dir/checksums.txt" "$release_url/checksums.txt"
expected=$(awk -v name="$asset" '$2 == name || $2 == "*" name { print $1; exit }' "$tmp_dir/checksums.txt")
[ -n "$expected" ] || fail "checksums.txt does not contain $asset"
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$tmp_dir/$asset" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
actual=$(shasum -a 256 "$tmp_dir/$asset" | awk '{print $1}')
else
fail "sha256sum or shasum is required to verify the download"
fi
[ "$actual" = "$expected" ] || fail "checksum verification failed for $asset"
tar -xzf "$tmp_dir/$asset" -C "$tmp_dir"
[ -f "$tmp_dir/hop" ] || fail "release archive does not contain hop"
mkdir -p "$install_dir"
cp "$tmp_dir/hop" "$install_dir/hop"
chmod 0755 "$install_dir/hop"
case ":${PATH:-}:" in
*":$install_dir:"*) ;;
*)
if [ "$modify_path" = 1 ] && [ "$install_dir" = "$HOME/.local/bin" ]; then
case ${SHELL:-} in
*/zsh) profile="$HOME/.zprofile" ;;
*) profile="$HOME/.profile" ;;
esac
path_line='export PATH="$HOME/.local/bin:$PATH"'
if ! grep -F "$path_line" "$profile" >/dev/null 2>&1; then
printf '\n%s\n' "$path_line" >>"$profile"
printf 'Added ~/.local/bin to PATH in %s.\n' "$profile"
fi
else
printf 'Add %s to PATH before opening a new terminal.\n' "$install_dir" >&2
fi
;;
esac
if [ "$install_skill" = 1 ]; then
"$install_dir/hop" skill install --force
fi
printf 'Installed %s\n' "$("$install_dir/hop" version)"
printf 'Binary: %s\n' "$install_dir/hop"
if [ "$install_skill" = 1 ]; then
printf 'Restart Codex if it is open, then use it normally in any Git repository.\n'
fi
+72
View File
@@ -0,0 +1,72 @@
[CmdletBinding()]
param()
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("hop-installer-test-" + [guid]::NewGuid())
$fixtures = Join-Path $tempDir "fixtures"
$payload = Join-Path $tempDir "payload"
$installDir = Join-Path $tempDir "install"
$testArch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -eq "Arm64") { "arm64" } else { "amd64" }
$asset = "hop_windows_${testArch}.zip"
New-Item -ItemType Directory -Path $tempDir | Out-Null
New-Item -ItemType Directory -Path $fixtures, $payload | Out-Null
try {
$binary = Join-Path $payload "hop.exe"
& go build -trimpath `
-ldflags "-X githop.xyz/hop/hop/internal/hop.Version=9.9.9-installer-test" `
-o $binary (Join-Path $root "cmd/hop")
if ($LASTEXITCODE -ne 0) { throw "Could not build installer test binary" }
$archive = Join-Path $fixtures $asset
Compress-Archive -Path $binary -DestinationPath $archive
$hash = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant()
"$hash $asset" | Set-Content -Encoding ascii (Join-Path $fixtures "checksums.txt")
function Invoke-RestMethod {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$Uri)
if (-not $Uri.EndsWith("/releases?draft=false&page=1&limit=1")) {
throw "Unexpected installer API URL: $Uri"
}
return ,([pscustomobject]@{
tag_name = "v9.9.9-installer-test"
prerelease = $true
draft = $false
})
}
function Invoke-WebRequest {
[CmdletBinding()]
param(
[switch]$UseBasicParsing,
[Parameter(Mandatory)][string]$Uri,
[Parameter(Mandatory)][string]$OutFile
)
if ($Uri.EndsWith("/checksums.txt")) {
Copy-Item (Join-Path $fixtures "checksums.txt") $OutFile
} elseif ($Uri.EndsWith("/$asset")) {
Copy-Item (Join-Path $fixtures $asset) $OutFile
} else {
throw "Unexpected installer asset URL: $Uri"
}
}
& (Join-Path $root "scripts/install.ps1") `
-GiteaUrl "https://gitea.test" `
-Repository "hop/hop" `
-InstallDir $installDir `
-SkipSkill `
-SkipPath
$installed = Join-Path $installDir "hop.exe"
if (-not (Test-Path $installed)) { throw "Installer did not install hop.exe" }
$version = & $installed version
if ($LASTEXITCODE -ne 0 -or $version -ne "hop 9.9.9-installer-test") {
throw "Unexpected installed version: $version"
}
Write-Host "PowerShell installer smoke test passed."
} finally {
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir
}
+83
View File
@@ -0,0 +1,83 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$root"
tmp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t hop-installer-test)
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT HUP INT TERM
case $(uname -s) in
Darwin) os=darwin ;;
Linux) os=linux ;;
*) printf 'installer smoke test does not support %s\n' "$(uname -s)" >&2; exit 1 ;;
esac
case $(uname -m) in
x86_64 | amd64) arch=amd64 ;;
arm64 | aarch64) arch=arm64 ;;
*) printf 'installer smoke test does not support %s\n' "$(uname -m)" >&2; exit 1 ;;
esac
asset="hop_${os}_${arch}.tar.gz"
mkdir -p "$tmp_dir/payload" "$tmp_dir/fixtures" "$tmp_dir/mock-bin" "$tmp_dir/home"
go build -trimpath \
-ldflags '-X githop.xyz/hop/hop/internal/hop.Version=9.9.9-installer-test' \
-o "$tmp_dir/payload/hop" "$root/cmd/hop"
tar -czf "$tmp_dir/fixtures/$asset" -C "$tmp_dir/payload" hop
if command -v sha256sum >/dev/null 2>&1; then
hash=$(sha256sum "$tmp_dir/fixtures/$asset" | awk '{print $1}')
else
hash=$(shasum -a 256 "$tmp_dir/fixtures/$asset" | awk '{print $1}')
fi
printf '%s %s\n' "$hash" "$asset" >"$tmp_dir/fixtures/checksums.txt"
cat >"$tmp_dir/mock-bin/curl" <<'MOCK_CURL'
#!/bin/sh
set -eu
output=
url=
while [ "$#" -gt 0 ]; do
case "$1" in
-o) shift; output=$1 ;;
http://* | https://*) url=$1 ;;
esac
shift
done
case "$url" in
*/api/v1/repos/*/releases\?draft=false\&page=1\&limit=1)
printf '[{"tag_name":"v9.9.9-installer-test","prerelease":true,"draft":false}]'
;;
*/checksums.txt)
cp "$HOP_TEST_FIXTURES/checksums.txt" "$output"
;;
*/hop_*.tar.gz)
cp "$HOP_TEST_FIXTURES/${url##*/}" "$output"
;;
*)
printf 'unexpected installer URL: %s\n' "$url" >&2
exit 1
;;
esac
MOCK_CURL
chmod 0755 "$tmp_dir/mock-bin/curl"
HOME="$tmp_dir/home" \
PATH="$tmp_dir/mock-bin:$PATH" \
HOP_TEST_FIXTURES="$tmp_dir/fixtures" \
HOP_GITEA_URL="https://gitea.test" \
HOP_REPOSITORY="hop/hop" \
HOP_INSTALL_DIR="$tmp_dir/home/bin" \
HOP_MODIFY_PATH=0 \
sh "$root/scripts/install.sh"
version=$($tmp_dir/home/bin/hop version)
[ "$version" = "hop 9.9.9-installer-test" ] || {
printf 'unexpected installed version: %s\n' "$version" >&2
exit 1
}
[ -s "$tmp_dir/home/.codex/skills/hop/SKILL.md" ] || {
printf 'installer did not install the embedded Hop skill\n' >&2
exit 1
}