mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-26 10:31:17 +00:00
Move v5 maintenance flow onto release/5.1
This commit is contained in:
parent
deee730af2
commit
324f3be1c8
31 changed files with 548 additions and 256 deletions
2
.github/workflows/build-and-test.yml
vendored
2
.github/workflows/build-and-test.yml
vendored
|
|
@ -4,9 +4,11 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/5.1
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release/5.1
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
|
|
|||
18
.github/workflows/create-release.yml
vendored
18
.github/workflows/create-release.yml
vendored
|
|
@ -78,6 +78,16 @@ jobs:
|
|||
fi
|
||||
echo "[OK] VERSION file matches requested version ($REQUESTED_VERSION)"
|
||||
|
||||
- name: Enforce maintenance branch for 5.1.x releases
|
||||
run: |
|
||||
REQUESTED_VERSION="${{ steps.extract.outputs.version }}"
|
||||
CURRENT_REF="${GITHUB_REF_NAME}"
|
||||
if [[ "$REQUESTED_VERSION" =~ ^5\.1\.[0-9]+([-.].*)?$ ]] && [ "$CURRENT_REF" != "release/5.1" ]; then
|
||||
echo "::error::5.1.x releases must be cut from release/5.1 (current ref: $CURRENT_REF)"
|
||||
exit 1
|
||||
fi
|
||||
echo "[OK] Release ref ${CURRENT_REF} is valid for ${REQUESTED_VERSION}"
|
||||
|
||||
# Frontend checks run in parallel with backend tests
|
||||
frontend_checks:
|
||||
needs: prepare
|
||||
|
|
@ -497,7 +507,9 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
|
||||
run: |
|
||||
gh workflow run publish-docker.yml -f tag="${{ needs.prepare.outputs.tag }}"
|
||||
gh workflow run publish-docker.yml \
|
||||
--ref "${GITHUB_REF_NAME}" \
|
||||
-f tag="${{ needs.prepare.outputs.tag }}"
|
||||
echo "[OK] Docker publish workflow dispatched"
|
||||
|
||||
- name: Trigger demo server update
|
||||
|
|
@ -506,7 +518,9 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
|
||||
run: |
|
||||
gh workflow run update-demo-server.yml -f tag="${{ needs.prepare.outputs.tag }}"
|
||||
gh workflow run update-demo-server.yml \
|
||||
--ref "${GITHUB_REF_NAME}" \
|
||||
-f tag="${{ needs.prepare.outputs.tag }}"
|
||||
echo "[OK] Demo server update dispatched"
|
||||
|
||||
- name: Summary
|
||||
|
|
|
|||
2
.github/workflows/helm-ci.yml
vendored
2
.github/workflows/helm-ci.yml
vendored
|
|
@ -2,7 +2,7 @@ name: Helm CI
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, release/5.1]
|
||||
paths:
|
||||
- "deploy/helm/**"
|
||||
- ".github/workflows/helm-ci.yml"
|
||||
|
|
|
|||
2
.github/workflows/test-e2e.yml
vendored
2
.github/workflows/test-e2e.yml
vendored
|
|
@ -4,6 +4,7 @@ on:
|
|||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release/5.1
|
||||
paths:
|
||||
- 'frontend-modern/**'
|
||||
- 'internal/**'
|
||||
|
|
@ -13,6 +14,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/5.1
|
||||
- master
|
||||
paths:
|
||||
- 'frontend-modern/**'
|
||||
|
|
|
|||
1
.github/workflows/test-updates.yml
vendored
1
.github/workflows/test-updates.yml
vendored
|
|
@ -15,6 +15,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/5.1
|
||||
- master
|
||||
paths:
|
||||
- 'internal/updates/**'
|
||||
|
|
|
|||
12
.github/workflows/update-demo-server.yml
vendored
12
.github/workflows/update-demo-server.yml
vendored
|
|
@ -6,7 +6,7 @@ on:
|
|||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to deploy (e.g., v4.29.0)'
|
||||
description: 'Release tag to deploy (e.g., v5.1.28)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
|
|
@ -37,12 +37,12 @@ jobs:
|
|||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
TARGET="${{ steps.target.outputs.tag }}"
|
||||
LATEST=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.tag_name')
|
||||
LATEST=$(gh api "repos/${{ github.repository }}/releases?per_page=100" --jq 'map(select(.draft == false and .prerelease == false and (.tag_name | test("^v5\\.1\\.[0-9]+$"))))[0].tag_name')
|
||||
echo "Target tag: $TARGET"
|
||||
echo "Latest published tag: $LATEST"
|
||||
echo "Latest published v5.1 tag: $LATEST"
|
||||
if [ "$TARGET" != "$LATEST" ]; then
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Release is not the latest; skipping demo update."
|
||||
echo "Release is not the latest v5.1 stable release; skipping demo update."
|
||||
else
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
else
|
||||
echo "Release: ${{ github.event.release.tag_name }}"
|
||||
echo "Prerelease: ${{ github.event.release.prerelease }}"
|
||||
echo "Updating demo server to latest stable release..."
|
||||
echo "Updating demo server to latest stable v5.1 release..."
|
||||
fi
|
||||
|
||||
- name: Wait for release assets
|
||||
|
|
@ -123,7 +123,7 @@ jobs:
|
|||
TAG="${{ steps.target.outputs.tag }}"
|
||||
# Use set -o pipefail to ensure curl errors aren't masked by bash
|
||||
ssh -i ~/.ssh/id_ed25519 ${{ secrets.DEMO_SERVER_USER }}@${{ secrets.DEMO_SERVER_HOST }} \
|
||||
"set -o pipefail && curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash -s -- --version $TAG"
|
||||
"set -o pipefail && curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/install.sh | sudo bash -s -- --version $TAG"
|
||||
|
||||
- name: Verify update
|
||||
if: steps.gate.outputs.skip != 'true' && steps.current.outputs.skip_current != 'true'
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ If you use the legacy `docker-compose` binary, replace `docker compose` with `do
|
|||
### ProxmoxVE LXC (Manual)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/install.sh | bash
|
||||
```
|
||||
|
||||
This script installs/updates the **Pulse server**. Agent updates use the `/install.sh` command generated in **Settings → Agents → Installation commands**.
|
||||
|
|
@ -91,7 +91,7 @@ This script installs/updates the **Pulse server**. Agent updates use the `/insta
|
|||
### Systemd Service (Manual)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/install.sh | bash
|
||||
```
|
||||
|
||||
This script installs/updates the **Pulse server**. Agent updates use the `/install.sh` command generated in **Settings → Agents → Installation commands**.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
If you run Proxmox VE, use the official LXC installer (recommended):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/install.sh | bash
|
||||
```
|
||||
|
||||
Note: this installs the Pulse **server**. Agent installs use the command from **Settings → Agents → Installation commands** (served from `/install.sh` on your Pulse server).
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ If you run Proxmox VE, the easiest and most “Pulse-native” deployment is the
|
|||
Run this on your Proxmox host:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/install.sh | bash
|
||||
```
|
||||
|
||||
> **Note**: The GitHub `install.sh` is the **server** installer. The agent installer is served from your Pulse server at `/install.sh` (see **Settings → Agents → Installation commands**).
|
||||
|
|
@ -72,7 +72,7 @@ See [KUBERNETES.md](KUBERNETES.md) for ingress and persistence configuration.
|
|||
For Linux servers (VM or bare metal), use the official installer:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | sudo bash
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/install.sh | sudo bash
|
||||
```
|
||||
|
||||
> **Note**: This installs the Pulse server. Use the `/install.sh` endpoint from your Pulse UI for installing `pulse-agent` on monitored hosts.
|
||||
|
|
|
|||
|
|
@ -80,14 +80,14 @@ ssh -i /path/to/key root@node "cat /sys/class/thermal/thermal_zone0/temp"
|
|||
If you still have the old sensor proxy installed from prior releases, remove it from each **Proxmox host** (not the Pulse container) with the supported cleanup helper:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/scripts/uninstall-sensor-proxy.sh | \
|
||||
sudo bash -s -- --uninstall --purge
|
||||
```
|
||||
|
||||
If you also want to remove the `pulse-monitor@pam` API user/tokens for a full clean slate before re-adding the node, include `--remove-proxmox-access`:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/scripts/uninstall-sensor-proxy.sh | \
|
||||
sudo bash -s -- --uninstall --purge --remove-proxmox-access
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Preferred path:
|
|||
If you prefer CLI, use the official installer for the target version:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | \
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/install.sh | \
|
||||
sudo bash -s -- --version vX.Y.Z
|
||||
```
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ If you reset auth (for example by deleting `.env`), Pulse may require a bootstra
|
|||
The `pulse-sensor-proxy` from v4 is no longer needed — temperature monitoring is now handled by the unified agent. If you had the sensor proxy installed on your Proxmox hosts, remove it **on each host** after upgrading:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/scripts/uninstall-sensor-proxy.sh | \
|
||||
sudo bash -s -- --uninstall --purge
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ If guest disk values only populate for the first part of a large VM list, tune t
|
|||
### Diagnostic Script
|
||||
Run this on your Proxmox host to debug specific VMs:
|
||||
```bash
|
||||
curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/test-vm-disk.sh | bash
|
||||
curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/scripts/test-vm-disk.sh | bash
|
||||
```
|
||||
|
||||
## 📝 Notes
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Manage Pulse auto-updates on host-mode installations.
|
|||
| `pulse-update.service` | Runs the update script. |
|
||||
| `pulse-auto-update.sh` | Fetches release & restarts Pulse (`/opt/pulse/scripts/pulse-auto-update.sh`). |
|
||||
|
||||
**Release channel note:** the systemd timer script tracks GitHub `releases/latest` (stable). RC channel settings only affect the in-app update checker.
|
||||
**Release channel note:** the systemd timer script tracks the latest stable `5.1.x` release. RC channel settings only affect the in-app update checker.
|
||||
|
||||
## 🚀 Enable/Disable
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ If an update fails:
|
|||
2. The timer script keeps a temporary backup under `/tmp/pulse-backup-<timestamp>` during the update; failures auto-restore from that backup and then clean it up.
|
||||
3. If you need to pin a specific version, re-run the installer with a version:
|
||||
```bash
|
||||
curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | \
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/install.sh | \
|
||||
sudo bash -s -- --version vX.Y.Z
|
||||
```
|
||||
This installer updates the **Pulse server**. Agent updates use the `/install.sh` command generated in **Settings → Agents → Installation commands**.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Download from 'lucide-solid/icons/download';
|
|||
import type { UpdateInfo, VersionInfo, UpdatePlan } from '@/api/updates';
|
||||
import {
|
||||
V6_RC_ANNOUNCEMENT,
|
||||
V5_MAINTENANCE_BRANCH,
|
||||
isV5ReleaseLine,
|
||||
} from '@/constants/releaseAnnouncements';
|
||||
|
||||
|
|
@ -498,11 +499,11 @@ sudo tar -xzf pulse-v${props.updateInfo()?.latestVersion}-linux-amd64.tar.gz -C
|
|||
</div>
|
||||
<div class="ml-8 relative group">
|
||||
<code class="block p-3 bg-gray-900 dark:bg-gray-950 rounded-lg text-sm font-mono text-green-400 border border-gray-700">
|
||||
git pull origin main
|
||||
{`git pull origin ${V5_MAINTENANCE_BRANCH}`}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigator.clipboard.writeText('git pull origin main')}
|
||||
onClick={() => navigator.clipboard.writeText(`git pull origin ${V5_MAINTENANCE_BRANCH}`)}
|
||||
class="absolute top-2 right-2 p-1.5 rounded bg-gray-700 hover:bg-gray-600 text-gray-300 opacity-60 hover:opacity-100 transition-opacity"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { SecurityStatus } from '@/types/config';
|
||||
|
||||
export const V5_MAINTENANCE_BRANCH = 'release/5.1';
|
||||
|
||||
export const V6_RC_ANNOUNCEMENT = {
|
||||
id: 'v6-rc-testing-v6.0.0-rc.1',
|
||||
tag: 'v6.0.0-rc.1',
|
||||
|
|
|
|||
101
install.sh
101
install.sh
|
|
@ -25,7 +25,7 @@ ENABLE_AUTO_UPDATES=false
|
|||
FORCE_VERSION=""
|
||||
FORCE_CHANNEL=""
|
||||
ARCHIVE_OVERRIDE="${PULSE_ARCHIVE_PATH:-}"
|
||||
SOURCE_BRANCH="main"
|
||||
SOURCE_BRANCH="release/5.1"
|
||||
CURRENT_INSTALL_CTID=""
|
||||
CONTAINER_CREATED_FOR_CLEANUP=false
|
||||
BUILD_FROM_SOURCE_MARKER="$INSTALL_DIR/BUILD_FROM_SOURCE"
|
||||
|
|
@ -45,8 +45,11 @@ DEBIAN_TEMPLATE=""
|
|||
get_latest_release_from_redirect() {
|
||||
# Follow the GitHub "latest" redirect and extract the tag in a way that
|
||||
# tolerates intermediate redirects that omit /tag/ (issue #698).
|
||||
local target_url="${1:-https://github.com/$GITHUB_REPO/releases/latest}"
|
||||
local target_url="${1:-}"
|
||||
local effective_url=""
|
||||
if [[ -z "$target_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
local curl_cmd=(curl -fsSL --connect-timeout 5 --max-time 10 -o /dev/null -w '%{url_effective}' "$target_url")
|
||||
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
|
|
@ -77,6 +80,62 @@ get_latest_release_from_redirect() {
|
|||
return 0
|
||||
}
|
||||
|
||||
maintenance_raw_url() {
|
||||
local path="$1"
|
||||
printf 'https://raw.githubusercontent.com/%s/%s/%s\n' "$GITHUB_REPO" "$SOURCE_BRANCH" "$path"
|
||||
}
|
||||
|
||||
get_latest_maintenance_stable_version() {
|
||||
local latest_version=""
|
||||
local releases_json=""
|
||||
local feed_xml=""
|
||||
local api_url="https://api.github.com/repos/$GITHUB_REPO/releases"
|
||||
local feed_url="https://github.com/$GITHUB_REPO/releases.atom"
|
||||
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
releases_json=$(timeout 10 curl -fsSL --connect-timeout 5 --max-time 10 "$api_url" 2>/dev/null || true)
|
||||
else
|
||||
releases_json=$(curl -fsSL --connect-timeout 5 --max-time 10 "$api_url" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [[ -n "$releases_json" ]]; then
|
||||
latest_version=$(printf '%s' "$releases_json" | grep -oE '"tag_name":[[:space:]]*"v5\.1\.[0-9]+"' | head -1 | sed -E 's/.*"([^"]+)"/\1/' || true)
|
||||
fi
|
||||
|
||||
if [[ -z "$latest_version" ]]; then
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
feed_xml=$(timeout 10 curl -fsSL --connect-timeout 5 --max-time 10 "$feed_url" 2>/dev/null || true)
|
||||
else
|
||||
feed_xml=$(curl -fsSL --connect-timeout 5 --max-time 10 "$feed_url" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [[ -n "$feed_xml" ]]; then
|
||||
latest_version=$(printf '%s' "$feed_xml" | grep -oE '<title>Pulse v5\.1\.[0-9]+</title>' | head -1 | sed -E 's#<title>Pulse (v[^<]+)</title>#\1#' || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s\n' "$latest_version"
|
||||
}
|
||||
|
||||
resolve_bootstrap_install_script_url() {
|
||||
local requested_version="${1:-}"
|
||||
|
||||
if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
|
||||
maintenance_raw_url "install.sh"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "$requested_version" ]]; then
|
||||
if [[ "$requested_version" != v* ]]; then
|
||||
requested_version="v${requested_version}"
|
||||
fi
|
||||
printf 'https://github.com/%s/releases/download/%s/install.sh\n' "$GITHUB_REPO" "$requested_version"
|
||||
return
|
||||
fi
|
||||
|
||||
maintenance_raw_url "install.sh"
|
||||
}
|
||||
|
||||
detect_lxc_ctid() {
|
||||
local ctid=""
|
||||
|
||||
|
|
@ -1403,7 +1462,8 @@ create_lxc_container() {
|
|||
local script_source="/tmp/pulse_install_$$.sh"
|
||||
if [[ "$0" == "bash" ]] || [[ ! -f "$0" ]]; then
|
||||
# We're being piped, download the script with retry logic
|
||||
local download_url="https://github.com/rcourtman/Pulse/releases/latest/download/install.sh"
|
||||
local download_url
|
||||
download_url=$(resolve_bootstrap_install_script_url "$FORCE_VERSION")
|
||||
local download_success=false
|
||||
local download_error=""
|
||||
local max_retries=3
|
||||
|
|
@ -3120,9 +3180,9 @@ fi
|
|||
|
||||
echo "Updating Pulse..."
|
||||
if [[ ${#extra_args[@]} -gt 0 ]]; then
|
||||
curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash -s -- "${extra_args[@]}"
|
||||
curl -fsSL "$(maintenance_raw_url "install.sh")" | bash -s -- "${extra_args[@]}"
|
||||
else
|
||||
curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash
|
||||
curl -fsSL "$(maintenance_raw_url "install.sh")" | bash
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
|
@ -3143,7 +3203,8 @@ EOF
|
|||
}
|
||||
|
||||
download_auto_update_script() {
|
||||
local url="https://github.com/$GITHUB_REPO/releases/latest/download/pulse-auto-update.sh"
|
||||
local url
|
||||
url=$(maintenance_raw_url "scripts/pulse-auto-update.sh")
|
||||
local dest="/usr/local/bin/pulse-auto-update.sh"
|
||||
local attempts=0
|
||||
local max_attempts=3
|
||||
|
|
@ -3407,9 +3468,9 @@ print_completion() {
|
|||
echo " journalctl -u $SERVICE_NAME -f - View logs"
|
||||
echo
|
||||
echo -e "${YELLOW}Management:${NC}"
|
||||
echo " Update: curl -sSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash"
|
||||
echo " Reset: curl -sSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash -s -- --reset"
|
||||
echo " Uninstall: curl -sSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash -s -- --uninstall"
|
||||
echo " Update: curl -sSL $(maintenance_raw_url "install.sh") | bash"
|
||||
echo " Reset: curl -sSL $(maintenance_raw_url "install.sh") | bash -s -- --reset"
|
||||
echo " Uninstall: curl -sSL $(maintenance_raw_url "install.sh") | bash -s -- --uninstall"
|
||||
|
||||
# Show auto-update status if timer exists
|
||||
if systemctl list-unit-files --no-legend | grep -q "^pulse-update.timer"; then
|
||||
|
|
@ -3569,23 +3630,9 @@ main() {
|
|||
fi
|
||||
|
||||
# Get both stable and RC versions
|
||||
# Try GitHub API first, but have a fallback - with timeout protection
|
||||
# Stable is explicitly pinned to the v5.1 maintenance line.
|
||||
local STABLE_VERSION=""
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
STABLE_VERSION=$(timeout 10 curl -s --connect-timeout 5 --max-time 10 https://api.github.com/repos/$GITHUB_REPO/releases/latest 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' || true)
|
||||
else
|
||||
STABLE_VERSION=$(curl -s --connect-timeout 5 --max-time 10 https://api.github.com/repos/$GITHUB_REPO/releases/latest 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' || true)
|
||||
fi
|
||||
|
||||
# If rate limited or failed, try direct GitHub latest URL
|
||||
if [[ -z "$STABLE_VERSION" ]] || [[ "$STABLE_VERSION" == *"rate limit"* ]]; then
|
||||
# Use the GitHub latest release redirect to get version
|
||||
local redirect_version=""
|
||||
redirect_version=$(get_latest_release_from_redirect 2>/dev/null || true)
|
||||
if [[ -n "$redirect_version" ]]; then
|
||||
STABLE_VERSION="$redirect_version"
|
||||
fi
|
||||
fi
|
||||
STABLE_VERSION=$(get_latest_maintenance_stable_version)
|
||||
|
||||
# For RC, we need the API, so if it fails just use empty
|
||||
local RC_VERSION=""
|
||||
|
|
@ -4088,7 +4135,7 @@ parse_args() {
|
|||
SOURCE_BRANCH="$2"
|
||||
shift 2
|
||||
else
|
||||
SOURCE_BRANCH="main"
|
||||
SOURCE_BRANCH="release/5.1"
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
|
|
@ -4100,7 +4147,7 @@ parse_args() {
|
|||
echo " --stable Install latest stable version (default)"
|
||||
echo " --version VERSION Install specific version (e.g., v4.4.0-rc.1)"
|
||||
echo " --archive PATH Install from a local Pulse release tarball"
|
||||
echo " --source [BRANCH] Build and install from source (default: main)"
|
||||
echo " --source [BRANCH] Build and install from source (default: release/5.1)"
|
||||
echo " --enable-auto-updates Enable automatic stable updates (via systemd timer)"
|
||||
echo ""
|
||||
echo "Management options:"
|
||||
|
|
|
|||
|
|
@ -3452,9 +3452,9 @@ resource_id format: {instance}:{node}:{vmid} (colons, not dashes)
|
|||
Example: "delly:minipc:201" for VMID 201 on node minipc in instance delly
|
||||
|
||||
## Installing/Updating Pulse
|
||||
curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash
|
||||
curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/install.sh | bash
|
||||
After: systemctl enable pulse && systemctl start pulse
|
||||
Latest version: https://api.github.com/repos/rcourtman/Pulse/releases/latest`
|
||||
Stable v5 line: https://github.com/rcourtman/Pulse/releases?q=v5.1`
|
||||
|
||||
// Add custom context from AI settings (user's infrastructure description)
|
||||
s.mu.RLock()
|
||||
|
|
|
|||
|
|
@ -146,7 +146,8 @@ func TestHandleDownloadHostAgent_ProxyFromGitHub(t *testing.T) {
|
|||
binDir := setupTempPulseBin(t)
|
||||
router := &Router{
|
||||
projectRoot: t.TempDir(),
|
||||
installScriptClient: newTestInstallScriptClient(t, "https://github.com/rcourtman/Pulse/releases/latest/download/pulse-host-agent-freebsd-amd64", http.StatusOK, "freebsd-binary", nil),
|
||||
serverVersion: "5.1.28",
|
||||
installScriptClient: newTestInstallScriptClient(t, "https://github.com/rcourtman/Pulse/releases/download/v5.1.28/pulse-host-agent-freebsd-amd64", http.StatusOK, "freebsd-binary", nil),
|
||||
}
|
||||
|
||||
for _, path := range []string{
|
||||
|
|
@ -183,7 +184,8 @@ func TestHandleDownloadHostAgentChecksum_ProxyFromGitHub(t *testing.T) {
|
|||
binDir := setupTempPulseBin(t)
|
||||
router := &Router{
|
||||
projectRoot: t.TempDir(),
|
||||
installScriptClient: newTestInstallScriptClient(t, "https://github.com/rcourtman/Pulse/releases/latest/download/pulse-host-agent-freebsd-amd64", http.StatusOK, "freebsd-binary", nil),
|
||||
serverVersion: "5.1.28",
|
||||
installScriptClient: newTestInstallScriptClient(t, "https://github.com/rcourtman/Pulse/releases/download/v5.1.28/pulse-host-agent-freebsd-amd64", http.StatusOK, "freebsd-binary", nil),
|
||||
}
|
||||
|
||||
for _, path := range []string{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
|
@ -10,9 +11,44 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/updates"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const v5MaintenanceScriptRef = "release/5.1"
|
||||
|
||||
func releaseAssetTagFromServerVersion(serverVersion string) string {
|
||||
version := strings.TrimSpace(serverVersion)
|
||||
if version == "" || strings.EqualFold(version, "dev") {
|
||||
return ""
|
||||
}
|
||||
|
||||
parsed, err := updates.ParseVersion(version)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if parsed.Major == 0 && parsed.Minor == 0 && parsed.Patch == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
tag := fmt.Sprintf("v%d.%d.%d", parsed.Major, parsed.Minor, parsed.Patch)
|
||||
if parsed.Prerelease != "" {
|
||||
tag += "-" + parsed.Prerelease
|
||||
}
|
||||
|
||||
return tag
|
||||
}
|
||||
|
||||
func releaseAssetGitHubURL(assetName, serverVersion string) string {
|
||||
tag := releaseAssetTagFromServerVersion(serverVersion)
|
||||
if tag == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return "https://github.com/rcourtman/Pulse/releases/download/" + tag + "/" + assetName
|
||||
}
|
||||
|
||||
// handleDownloadUnifiedInstallScript serves the universal install.sh script
|
||||
func (r *Router) handleDownloadUnifiedInstallScript(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
||||
|
|
@ -106,6 +142,14 @@ func normalizeUnifiedAgentArch(arch string) string {
|
|||
}
|
||||
}
|
||||
|
||||
func installScriptGitHubURL(scriptName, serverVersion string) string {
|
||||
if releaseURL := releaseAssetGitHubURL(scriptName, serverVersion); releaseURL != "" {
|
||||
return releaseURL
|
||||
}
|
||||
|
||||
return "https://raw.githubusercontent.com/rcourtman/Pulse/" + v5MaintenanceScriptRef + "/scripts/" + scriptName
|
||||
}
|
||||
|
||||
// handleDownloadUnifiedAgent serves the pulse-agent binary
|
||||
func (r *Router) handleDownloadUnifiedAgent(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
||||
|
|
@ -199,7 +243,12 @@ func (r *Router) proxyAgentBinaryFromGitHub(w http.ResponseWriter, req *http.Req
|
|||
if strings.HasPrefix(normalized, "windows-") {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
githubURL := "https://github.com/rcourtman/Pulse/releases/latest/download/" + binaryName
|
||||
githubURL := releaseAssetGitHubURL(binaryName, r.serverVersion)
|
||||
if githubURL == "" {
|
||||
log.Error().Str("serverVersion", r.serverVersion).Msg("Cannot proxy agent binary without a released server version")
|
||||
http.Error(w, "Agent binary unavailable for non-release server builds", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("arch", normalized).Str("url", githubURL).Msg("Local agent binary not found, proxying from GitHub releases")
|
||||
|
||||
|
|
@ -256,7 +305,12 @@ func (r *Router) proxyHostAgentBinaryFromGitHub(w http.ResponseWriter, req *http
|
|||
if platform == "windows" {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
githubURL := "https://github.com/rcourtman/Pulse/releases/latest/download/" + binaryName
|
||||
githubURL := releaseAssetGitHubURL(binaryName, r.serverVersion)
|
||||
if githubURL == "" {
|
||||
log.Error().Str("serverVersion", r.serverVersion).Msg("Cannot proxy host agent binary without a released server version")
|
||||
http.Error(w, "Host agent binary unavailable for non-release server builds", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("platform", platform).
|
||||
|
|
@ -319,8 +373,7 @@ func (r *Router) proxyHostAgentBinaryFromGitHub(w http.ResponseWriter, req *http
|
|||
// proxyInstallScriptFromGitHub fetches an install script from GitHub releases
|
||||
// This is used as a fallback when scripts aren't available locally (e.g., LXC updates)
|
||||
func (r *Router) proxyInstallScriptFromGitHub(w http.ResponseWriter, req *http.Request, scriptName string) {
|
||||
// Use raw.githubusercontent.com to fetch from main branch
|
||||
githubURL := "https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/" + scriptName
|
||||
githubURL := installScriptGitHubURL(scriptName, r.serverVersion)
|
||||
|
||||
client := r.installScriptClient
|
||||
if client == nil {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ func TestDownloadUnifiedInstallScriptPS_MethodNotAllowed(t *testing.T) {
|
|||
|
||||
func TestDownloadUnifiedInstallScript_ProxyFallback(t *testing.T) {
|
||||
router, _ := setupUnifiedAgentRouter(t)
|
||||
expectedURL := "https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/install.sh"
|
||||
expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v5.1.28/install.sh"
|
||||
payload := "#!/bin/bash\necho hi"
|
||||
router.installScriptClient = newTestInstallScriptClient(t, expectedURL, http.StatusOK, payload, nil)
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ func TestDownloadUnifiedInstallScript_ProxyFallback(t *testing.T) {
|
|||
|
||||
func TestDownloadUnifiedInstallScriptPS_ProxyFallback(t *testing.T) {
|
||||
router, _ := setupUnifiedAgentRouter(t)
|
||||
expectedURL := "https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/install.ps1"
|
||||
expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v5.1.28/install.ps1"
|
||||
payload := "Write-Host 'hi'"
|
||||
router.installScriptClient = newTestInstallScriptClient(t, expectedURL, http.StatusOK, payload, nil)
|
||||
|
||||
|
|
@ -114,6 +114,26 @@ func TestDownloadUnifiedInstallScriptPS_ProxyFallback(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDownloadUnifiedInstallScript_ProxyFallback_DevBuildUsesMaintenanceBranch(t *testing.T) {
|
||||
router, _ := setupUnifiedAgentRouter(t)
|
||||
router.serverVersion = ""
|
||||
expectedURL := "https://raw.githubusercontent.com/rcourtman/Pulse/release/5.1/scripts/install.sh"
|
||||
payload := "#!/bin/bash\necho hi"
|
||||
router.installScriptClient = newTestInstallScriptClient(t, expectedURL, http.StatusOK, payload, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/install.sh", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.handleDownloadUnifiedInstallScript(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if strings.TrimSpace(w.Body.String()) != payload {
|
||||
t.Fatalf("unexpected response body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyInstallScriptFromGitHub_NonOK(t *testing.T) {
|
||||
router := &Router{
|
||||
installScriptClient: newTestInstallScriptClient(t, "", http.StatusNotFound, "", nil),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ func setupUnifiedAgentRouter(t *testing.T) (*Router, string) {
|
|||
|
||||
router := &Router{
|
||||
projectRoot: tempDir,
|
||||
serverVersion: "5.1.28",
|
||||
checksumCache: make(map[string]checksumCacheEntry),
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +131,7 @@ func TestDownloadUnifiedAgent_ProxyFromGitHub(t *testing.T) {
|
|||
// Ensure NO local files exist (temp dir is empty of binaries)
|
||||
// Set up a mock HTTP client to simulate GitHub response
|
||||
binaryContent := "fake binary content for proxy test"
|
||||
expectedURL := "https://github.com/rcourtman/Pulse/releases/latest/download/pulse-agent-linux-amd64"
|
||||
expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v5.1.28/pulse-agent-linux-amd64"
|
||||
router.installScriptClient = newTestInstallScriptClient(t, expectedURL, http.StatusOK, binaryContent, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
||||
|
|
@ -153,7 +154,7 @@ func TestDownloadUnifiedAgent_ProxyFromGitHub_Windows(t *testing.T) {
|
|||
router, _ := setupUnifiedAgentRouter(t)
|
||||
|
||||
binaryContent := "MZ fake windows binary"
|
||||
expectedURL := "https://github.com/rcourtman/Pulse/releases/latest/download/pulse-agent-windows-amd64.exe"
|
||||
expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v5.1.28/pulse-agent-windows-amd64.exe"
|
||||
router.installScriptClient = newTestInstallScriptClient(t, expectedURL, http.StatusOK, binaryContent, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=windows-amd64", nil)
|
||||
|
|
@ -216,3 +217,23 @@ func TestNormalizeUnifiedAgentArch(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseAssetTagFromServerVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
want string
|
||||
}{
|
||||
{name: "release", version: "5.1.28", want: "v5.1.28"},
|
||||
{name: "prerelease", version: "5.1.29-rc.1", want: "v5.1.29-rc.1"},
|
||||
{name: "build metadata stripped", version: "5.1.28+git.2.gabc123", want: "v5.1.28"},
|
||||
{name: "zero dev version rejected", version: "0.0.0-dev", want: ""},
|
||||
{name: "empty", version: "", want: ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, releaseAssetTagFromServerVersion(tt.version))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,17 +19,17 @@ import (
|
|||
|
||||
// InstallShAdapter wraps the install.sh script for systemd/LXC deployments
|
||||
type InstallShAdapter struct {
|
||||
history *UpdateHistory
|
||||
installScriptURL string
|
||||
logDir string
|
||||
history *UpdateHistory
|
||||
releaseAssetBaseURL string
|
||||
logDir string
|
||||
}
|
||||
|
||||
// NewInstallShAdapter creates a new install.sh adapter
|
||||
func NewInstallShAdapter(history *UpdateHistory) *InstallShAdapter {
|
||||
return &InstallShAdapter{
|
||||
history: history,
|
||||
installScriptURL: "https://github.com/rcourtman/Pulse/releases/latest/download/install.sh",
|
||||
logDir: "/var/log/pulse",
|
||||
history: history,
|
||||
releaseAssetBaseURL: "https://github.com/rcourtman/Pulse/releases/download",
|
||||
logDir: "/var/log/pulse",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,12 +88,21 @@ func (a *InstallShAdapter) Execute(ctx context.Context, request UpdateRequest, p
|
|||
Message: "Downloading installation script...",
|
||||
})
|
||||
|
||||
// Validate version string to prevent command injection
|
||||
// Version must match semantic versioning format (with optional 'v' prefix)
|
||||
versionPattern := regexp.MustCompile(`^v?\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$`)
|
||||
if !versionPattern.MatchString(request.Version) {
|
||||
return fmt.Errorf("invalid version format: %s", request.Version)
|
||||
}
|
||||
|
||||
installScriptURL := a.installScriptURLForVersion(request.Version)
|
||||
|
||||
log.Info().
|
||||
Str("url", a.installScriptURL).
|
||||
Str("url", installScriptURL).
|
||||
Str("version", request.Version).
|
||||
Msg("Downloading install script")
|
||||
|
||||
installScript, err := a.downloadInstallScript(ctx)
|
||||
installScript, err := a.downloadInstallScript(ctx, installScriptURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download install script: %w", err)
|
||||
}
|
||||
|
|
@ -105,13 +114,6 @@ func (a *InstallShAdapter) Execute(ctx context.Context, request UpdateRequest, p
|
|||
Message: "Preparing update...",
|
||||
})
|
||||
|
||||
// Validate version string to prevent command injection
|
||||
// Version must match semantic versioning format (with optional 'v' prefix)
|
||||
versionPattern := regexp.MustCompile(`^v?\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$`)
|
||||
if !versionPattern.MatchString(request.Version) {
|
||||
return fmt.Errorf("invalid version format: %s", request.Version)
|
||||
}
|
||||
|
||||
// Build command: bash install.sh --version vX.Y.Z
|
||||
args := []string{"-s", "--", "--version", request.Version}
|
||||
if request.Force {
|
||||
|
|
@ -211,6 +213,14 @@ func (a *InstallShAdapter) Execute(ctx context.Context, request UpdateRequest, p
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *InstallShAdapter) installScriptURLForVersion(version string) string {
|
||||
tag := strings.TrimSpace(version)
|
||||
if !strings.HasPrefix(tag, "v") {
|
||||
tag = "v" + tag
|
||||
}
|
||||
return fmt.Sprintf("%s/%s/install.sh", a.releaseAssetBaseURL, tag)
|
||||
}
|
||||
|
||||
// Rollback rolls back to a previous version
|
||||
func (a *InstallShAdapter) Rollback(ctx context.Context, eventID string) error {
|
||||
// Get the event from history
|
||||
|
|
@ -535,7 +545,7 @@ func (a *InstallShAdapter) waitForHealth(ctx context.Context, timeout time.Durat
|
|||
}
|
||||
|
||||
// downloadInstallScript downloads the install.sh script
|
||||
func (a *InstallShAdapter) downloadInstallScript(ctx context.Context) (string, error) {
|
||||
func (a *InstallShAdapter) downloadInstallScript(ctx context.Context, installScriptURL string) (string, error) {
|
||||
tmpDir, err := os.MkdirTemp("", "pulse-installsh-*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
@ -544,12 +554,12 @@ func (a *InstallShAdapter) downloadInstallScript(ctx context.Context) (string, e
|
|||
|
||||
scriptPath := filepath.Join(tmpDir, "install.sh")
|
||||
|
||||
cmd := exec.CommandContext(ctx, "curl", "-fsSL", "-o", scriptPath, a.installScriptURL)
|
||||
cmd := exec.CommandContext(ctx, "curl", "-fsSL", "-o", scriptPath, installScriptURL)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
checksumURL := a.installScriptURL + ".sha256"
|
||||
checksumURL := installScriptURL + ".sha256"
|
||||
checksumPath := scriptPath + ".sha256"
|
||||
cmd = exec.CommandContext(ctx, "curl", "-fsSL", "-o", checksumPath, checksumURL)
|
||||
if err := cmd.Run(); err != nil {
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ func TestInstallShAdapterExecuteSuccess(t *testing.T) {
|
|||
t.Setenv("PATH", strings.Join([]string{curlDir, bashDir, os.Getenv("PATH")}, string(os.PathListSeparator)))
|
||||
|
||||
adapter := &InstallShAdapter{
|
||||
installScriptURL: "http://example/install.sh",
|
||||
logDir: t.TempDir(),
|
||||
releaseAssetBaseURL: "http://example/releases/download",
|
||||
logDir: t.TempDir(),
|
||||
}
|
||||
|
||||
var updates []UpdateProgress
|
||||
|
|
@ -105,8 +105,8 @@ func TestInstallShAdapterExecuteInvalidVersion(t *testing.T) {
|
|||
t.Setenv("PATH", curlDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
adapter := &InstallShAdapter{
|
||||
installScriptURL: "http://example/install.sh",
|
||||
logDir: t.TempDir(),
|
||||
releaseAssetBaseURL: "http://example/releases/download",
|
||||
logDir: t.TempDir(),
|
||||
}
|
||||
|
||||
err := adapter.Execute(context.Background(), UpdateRequest{Version: "bad version"}, func(UpdateProgress) {})
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ func TestInstallShAdapterDownloadInstallScript(t *testing.T) {
|
|||
t.Setenv("PULSE_TEST_SHA", checksumPath)
|
||||
|
||||
adapter := NewInstallShAdapter(nil)
|
||||
got, err := adapter.downloadInstallScript(context.Background())
|
||||
got, err := adapter.downloadInstallScript(context.Background(), adapter.installScriptURLForVersion("v1.2.3"))
|
||||
if err != nil {
|
||||
t.Fatalf("downloadInstallScript error: %v", err)
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ func TestInstallShAdapterDownloadInstallScript(t *testing.T) {
|
|||
if err := os.WriteFile(checksumPath, []byte("deadbeef"), 0600); err != nil {
|
||||
t.Fatalf("write checksum: %v", err)
|
||||
}
|
||||
if _, err := adapter.downloadInstallScript(context.Background()); err == nil {
|
||||
if _, err := adapter.downloadInstallScript(context.Background(), adapter.installScriptURLForVersion("v1.2.3")); err == nil {
|
||||
t.Fatalf("expected checksum error")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ func TestInstallShAdapter_DownloadInstallScript(t *testing.T) {
|
|||
}
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
adapter := &InstallShAdapter{installScriptURL: "http://example/install.sh"}
|
||||
out, err := adapter.downloadInstallScript(context.Background())
|
||||
adapter := &InstallShAdapter{releaseAssetBaseURL: "http://example/releases/download"}
|
||||
out, err := adapter.downloadInstallScript(context.Background(), adapter.installScriptURLForVersion("v1.2.3"))
|
||||
if err != nil {
|
||||
t.Fatalf("downloadInstallScript error: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -478,17 +478,17 @@ func TestNewInstallShAdapter(t *testing.T) {
|
|||
t.Fatal("NewInstallShAdapter returned nil")
|
||||
}
|
||||
|
||||
if adapter.installScriptURL == "" {
|
||||
t.Error("installScriptURL should not be empty")
|
||||
if adapter.releaseAssetBaseURL == "" {
|
||||
t.Error("releaseAssetBaseURL should not be empty")
|
||||
}
|
||||
|
||||
if adapter.logDir == "" {
|
||||
t.Error("logDir should not be empty")
|
||||
}
|
||||
|
||||
expectedURL := "https://github.com/rcourtman/Pulse/releases/latest/download/install.sh"
|
||||
if adapter.installScriptURL != expectedURL {
|
||||
t.Errorf("installScriptURL = %q, want %q", adapter.installScriptURL, expectedURL)
|
||||
expectedURL := "https://github.com/rcourtman/Pulse/releases/download"
|
||||
if adapter.releaseAssetBaseURL != expectedURL {
|
||||
t.Errorf("releaseAssetBaseURL = %q, want %q", adapter.releaseAssetBaseURL, expectedURL)
|
||||
}
|
||||
|
||||
expectedLogDir := "/var/log/pulse"
|
||||
|
|
@ -496,3 +496,29 @@ func TestNewInstallShAdapter(t *testing.T) {
|
|||
t.Errorf("logDir = %q, want %q", adapter.logDir, expectedLogDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallShAdapter_InstallScriptURLForVersion(t *testing.T) {
|
||||
adapter := NewInstallShAdapter(nil)
|
||||
|
||||
tests := []struct {
|
||||
version string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
version: "5.1.28",
|
||||
want: "https://github.com/rcourtman/Pulse/releases/download/v5.1.28/install.sh",
|
||||
},
|
||||
{
|
||||
version: "v5.1.29-rc.1",
|
||||
want: "https://github.com/rcourtman/Pulse/releases/download/v5.1.29-rc.1/install.sh",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.version, func(t *testing.T) {
|
||||
if got := adapter.installScriptURLForVersion(tc.version); got != tc.want {
|
||||
t.Fatalf("installScriptURLForVersion(%q) = %q, want %q", tc.version, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -588,6 +588,80 @@ func (m *Manager) GetCachedUpdateInfo() *UpdateInfo {
|
|||
return m.checkCache[channel]
|
||||
}
|
||||
|
||||
func releaseMatchesCurrentLine(releaseVer, currentVer *Version) bool {
|
||||
if releaseVer == nil || currentVer == nil {
|
||||
return false
|
||||
}
|
||||
if currentVer.Major != 5 || currentVer.Minor != 1 {
|
||||
return true
|
||||
}
|
||||
return releaseVer.Major == currentVer.Major && releaseVer.Minor == currentVer.Minor
|
||||
}
|
||||
|
||||
func selectLatestReleaseForChannel(releases []ReleaseInfo, channel string, currentVer *Version) (*ReleaseInfo, error) {
|
||||
var newestRC *ReleaseInfo
|
||||
var newestRCVer *Version
|
||||
var newestStable *ReleaseInfo
|
||||
var newestStableVer *Version
|
||||
|
||||
for i := range releases {
|
||||
if releases[i].Draft {
|
||||
log.Debug().Str("tag", releases[i].TagName).Msg("Skipping draft release")
|
||||
continue
|
||||
}
|
||||
|
||||
releaseVer, err := ParseVersion(releases[i].TagName)
|
||||
if err != nil {
|
||||
log.Debug().Str("tag", releases[i].TagName).Err(err).Msg("Failed to parse release version")
|
||||
continue
|
||||
}
|
||||
|
||||
if !releaseMatchesCurrentLine(releaseVer, currentVer) {
|
||||
log.Debug().
|
||||
Str("tag", releases[i].TagName).
|
||||
Int("currentMajor", currentVer.Major).
|
||||
Int("currentMinor", currentVer.Minor).
|
||||
Msg("Skipping release outside current release line")
|
||||
continue
|
||||
}
|
||||
|
||||
if releases[i].Prerelease {
|
||||
if channel != "rc" {
|
||||
continue
|
||||
}
|
||||
if newestRCVer == nil || releaseVer.IsNewerThan(newestRCVer) {
|
||||
newestRC = &releases[i]
|
||||
newestRCVer = releaseVer
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if newestStableVer == nil || releaseVer.IsNewerThan(newestStableVer) {
|
||||
newestStable = &releases[i]
|
||||
newestStableVer = releaseVer
|
||||
}
|
||||
}
|
||||
|
||||
if channel == "rc" {
|
||||
if newestStable != nil && newestRC != nil {
|
||||
if newestStableVer.IsNewerThan(newestRCVer) {
|
||||
return newestStable, nil
|
||||
}
|
||||
return newestRC, nil
|
||||
}
|
||||
if newestStable != nil {
|
||||
return newestStable, nil
|
||||
}
|
||||
if newestRC != nil {
|
||||
return newestRC, nil
|
||||
}
|
||||
} else if newestStable != nil {
|
||||
return newestStable, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no releases found for channel %s in %d.%d.x line", channel, currentVer.Major, currentVer.Minor)
|
||||
}
|
||||
|
||||
// getLatestReleaseForChannel fetches the latest release from GitHub for a specific channel
|
||||
func (m *Manager) getLatestReleaseForChannel(ctx context.Context, channel string, currentVer *Version) (*ReleaseInfo, error) {
|
||||
if channel == "" {
|
||||
|
|
@ -633,7 +707,7 @@ func (m *Manager) getLatestReleaseForChannel(ctx context.Context, channel string
|
|||
Msg("GitHub API rate limit encountered, trying RSS fallback")
|
||||
|
||||
// Try RSS/Atom feed as fallback - doesn't count against rate limits
|
||||
if feedRelease, err := m.getLatestReleaseFromFeed(ctx, channel); err == nil {
|
||||
if feedRelease, err := m.getLatestReleaseFromFeed(ctx, channel, currentVer); err == nil {
|
||||
log.Info().Str("version", feedRelease.TagName).Msg("Got release info from RSS feed fallback")
|
||||
return feedRelease, nil
|
||||
}
|
||||
|
|
@ -660,123 +734,34 @@ func (m *Manager) getLatestReleaseForChannel(ctx context.Context, channel string
|
|||
return nil, fmt.Errorf("failed to decode releases: %w", err)
|
||||
}
|
||||
|
||||
// Find latest release based on channel
|
||||
// RC channel: return newest release (RC or stable), even if not newer than current
|
||||
// Stable channel: return newest stable release, even if not newer than current
|
||||
// The caller will determine if it's actually an update by comparing versions
|
||||
if channel == "rc" {
|
||||
// For RC channel: find newest release (RC or stable)
|
||||
// RC users should see both RCs and stable releases
|
||||
var newestRC *ReleaseInfo
|
||||
var newestStable *ReleaseInfo
|
||||
|
||||
for i := range releases {
|
||||
// Skip draft releases
|
||||
if releases[i].Draft {
|
||||
log.Debug().Str("tag", releases[i].TagName).Msg("Skipping draft release")
|
||||
continue
|
||||
}
|
||||
|
||||
releaseVer, err := ParseVersion(releases[i].TagName)
|
||||
if err != nil {
|
||||
log.Debug().Str("tag", releases[i].TagName).Err(err).Msg("Failed to parse release version")
|
||||
continue
|
||||
}
|
||||
|
||||
if releases[i].Prerelease {
|
||||
// Track newest RC
|
||||
if newestRC == nil {
|
||||
newestRC = &releases[i]
|
||||
} else {
|
||||
newestRCVer, _ := ParseVersion(newestRC.TagName)
|
||||
if releaseVer.IsNewerThan(newestRCVer) {
|
||||
newestRC = &releases[i]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Track newest stable
|
||||
if newestStable == nil {
|
||||
newestStable = &releases[i]
|
||||
} else {
|
||||
newestStableVer, _ := ParseVersion(newestStable.TagName)
|
||||
if releaseVer.IsNewerThan(newestStableVer) {
|
||||
newestStable = &releases[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the highest version among candidates
|
||||
// Stable versions are considered higher than RCs (4.22.0 > 4.22.0-rc.3)
|
||||
if newestStable != nil && newestRC != nil {
|
||||
stableVer, _ := ParseVersion(newestStable.TagName)
|
||||
rcVer, _ := ParseVersion(newestRC.TagName)
|
||||
if stableVer.IsNewerThan(rcVer) {
|
||||
isUpdate := stableVer.IsNewerThan(currentVer)
|
||||
if isUpdate {
|
||||
log.Info().Str("version", newestStable.TagName).Msg("Found stable update for RC user")
|
||||
} else {
|
||||
log.Info().Str("version", newestStable.TagName).Msg("On latest stable version")
|
||||
}
|
||||
return newestStable, nil
|
||||
}
|
||||
isUpdate := rcVer.IsNewerThan(currentVer)
|
||||
if isUpdate {
|
||||
log.Info().Str("version", newestRC.TagName).Msg("Found RC update")
|
||||
} else {
|
||||
log.Info().Str("version", newestRC.TagName).Msg("On latest RC version")
|
||||
}
|
||||
return newestRC, nil
|
||||
} else if newestStable != nil {
|
||||
isUpdate := newestStable.TagName != currentVer.String()
|
||||
if isUpdate {
|
||||
log.Info().Str("version", newestStable.TagName).Msg("Found stable update for RC user")
|
||||
} else {
|
||||
log.Info().Str("version", newestStable.TagName).Msg("On latest stable version")
|
||||
}
|
||||
return newestStable, nil
|
||||
} else if newestRC != nil {
|
||||
isUpdate := newestRC.TagName != currentVer.String()
|
||||
if isUpdate {
|
||||
log.Info().Str("version", newestRC.TagName).Msg("Found RC update")
|
||||
} else {
|
||||
log.Info().Str("version", newestRC.TagName).Msg("On latest RC version")
|
||||
}
|
||||
return newestRC, nil
|
||||
}
|
||||
} else {
|
||||
// For stable channel: find latest non-prerelease
|
||||
for i := range releases {
|
||||
// Skip draft releases
|
||||
if releases[i].Draft {
|
||||
log.Debug().Str("tag", releases[i].TagName).Msg("Skipping draft release")
|
||||
continue
|
||||
}
|
||||
|
||||
if releases[i].Prerelease {
|
||||
continue
|
||||
}
|
||||
|
||||
releaseVer, err := ParseVersion(releases[i].TagName)
|
||||
if err != nil {
|
||||
log.Debug().Str("tag", releases[i].TagName).Err(err).Msg("Failed to parse release version")
|
||||
continue
|
||||
}
|
||||
|
||||
// Found the latest stable release
|
||||
isUpdate := releaseVer.IsNewerThan(currentVer)
|
||||
if isUpdate {
|
||||
log.Info().Str("version", releases[i].TagName).Msg("Found stable update")
|
||||
} else {
|
||||
log.Info().Str("version", releases[i].TagName).Msg("On latest stable version")
|
||||
}
|
||||
return &releases[i], nil
|
||||
}
|
||||
release, err := selectLatestReleaseForChannel(releases, channel, currentVer)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Str("channel", channel).
|
||||
Int("currentMajor", currentVer.Major).
|
||||
Int("currentMinor", currentVer.Minor).
|
||||
Msg("No releases found for channel in current release line")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// No releases found at all for this channel
|
||||
log.Warn().Str("channel", channel).Msg("No releases found for channel")
|
||||
return nil, fmt.Errorf("no releases found for channel %s", channel)
|
||||
releaseVer, err := ParseVersion(release.TagName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse selected release version: %w", err)
|
||||
}
|
||||
|
||||
if releaseVer.IsNewerThan(currentVer) {
|
||||
if release.Prerelease {
|
||||
log.Info().Str("version", release.TagName).Msg("Found prerelease update")
|
||||
} else {
|
||||
log.Info().Str("version", release.TagName).Msg("Found stable update")
|
||||
}
|
||||
} else if release.Prerelease {
|
||||
log.Info().Str("version", release.TagName).Msg("On latest prerelease version")
|
||||
} else {
|
||||
log.Info().Str("version", release.TagName).Msg("On latest stable version")
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func (m *Manager) resolveChannel(requested string, currentInfo *VersionInfo) string {
|
||||
|
|
@ -795,7 +780,7 @@ func (m *Manager) resolveChannel(requested string, currentInfo *VersionInfo) str
|
|||
// getLatestReleaseFromFeed fetches the latest release from GitHub's Atom feed
|
||||
// This is used as a fallback when the API is rate-limited, as the Atom feed
|
||||
// doesn't count against API rate limits.
|
||||
func (m *Manager) getLatestReleaseFromFeed(ctx context.Context, channel string) (*ReleaseInfo, error) {
|
||||
func (m *Manager) getLatestReleaseFromFeed(ctx context.Context, channel string, currentVer *Version) (*ReleaseInfo, error) {
|
||||
feedURL := "https://github.com/rcourtman/Pulse/releases.atom"
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", feedURL, nil)
|
||||
|
|
@ -834,7 +819,7 @@ func (m *Manager) getLatestReleaseFromFeed(ctx context.Context, channel string)
|
|||
return nil, fmt.Errorf("no version tags found in feed")
|
||||
}
|
||||
|
||||
// Filter based on channel
|
||||
var releases []ReleaseInfo
|
||||
for _, match := range matches {
|
||||
if len(match) < 2 {
|
||||
continue
|
||||
|
|
@ -849,28 +834,27 @@ func (m *Manager) getLatestReleaseFromFeed(ctx context.Context, channel string)
|
|||
|
||||
isPrerelease := ver.IsPrerelease()
|
||||
|
||||
// For stable channel, skip prereleases
|
||||
if channel == "stable" && isPrerelease {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found a valid release for this channel
|
||||
log.Debug().
|
||||
Str("tag", tagName).
|
||||
Bool("prerelease", isPrerelease).
|
||||
Str("channel", channel).
|
||||
Msg("Found release from feed")
|
||||
|
||||
return &ReleaseInfo{
|
||||
releases = append(releases, ReleaseInfo{
|
||||
TagName: tagName,
|
||||
Name: "Pulse " + tagName,
|
||||
Prerelease: isPrerelease,
|
||||
// Note: Feed doesn't include full release notes or asset info
|
||||
// This is just for version checking - actual download still uses known URL patterns
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no suitable release found for channel %s", channel)
|
||||
release, err := selectLatestReleaseForChannel(releases, channel, currentVer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("tag", release.TagName).
|
||||
Bool("prerelease", release.Prerelease).
|
||||
Str("channel", channel).
|
||||
Msg("Found release from feed")
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func (m *Manager) createHistoryEntry(ctx context.Context, entry UpdateHistoryEntry) string {
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ func TestCheckForUpdatesWithChannel_AvailableUsesCache(t *testing.T) {
|
|||
releaseTime := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
releases := []ReleaseInfo{
|
||||
{
|
||||
TagName: "v99.0.0",
|
||||
Name: "v99.0.0",
|
||||
TagName: "v5.1.99",
|
||||
Name: "v5.1.99",
|
||||
Body: "Release notes",
|
||||
Prerelease: false,
|
||||
PublishedAt: releaseTime,
|
||||
|
|
@ -66,8 +66,8 @@ func TestCheckForUpdatesWithChannel_AvailableUsesCache(t *testing.T) {
|
|||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}{
|
||||
{
|
||||
Name: "pulse-v99.0.0-linux-amd64.tar.gz",
|
||||
BrowserDownloadURL: "https://example.com/pulse-v99.0.0-linux-amd64.tar.gz",
|
||||
Name: "pulse-v5.1.99-linux-amd64.tar.gz",
|
||||
BrowserDownloadURL: "https://example.com/pulse-v5.1.99-linux-amd64.tar.gz",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -87,8 +87,8 @@ func TestCheckForUpdatesWithChannel_AvailableUsesCache(t *testing.T) {
|
|||
if !info.Available {
|
||||
t.Fatalf("expected update to be available")
|
||||
}
|
||||
if info.LatestVersion != "99.0.0" {
|
||||
t.Fatalf("LatestVersion = %q, want 99.0.0", info.LatestVersion)
|
||||
if info.LatestVersion != "5.1.99" {
|
||||
t.Fatalf("LatestVersion = %q, want 5.1.99", info.LatestVersion)
|
||||
}
|
||||
if info.DownloadURL == "" {
|
||||
t.Fatalf("DownloadURL not set")
|
||||
|
|
@ -110,8 +110,8 @@ func TestCheckForUpdatesWithChannel_NoReleases(t *testing.T) {
|
|||
var hits int32
|
||||
releases := []ReleaseInfo{
|
||||
{
|
||||
TagName: "v99.0.0-rc.1",
|
||||
Name: "v99.0.0-rc.1",
|
||||
TagName: "v5.1.99-rc.1",
|
||||
Name: "v5.1.99-rc.1",
|
||||
Prerelease: true,
|
||||
PublishedAt: time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC),
|
||||
},
|
||||
|
|
@ -140,8 +140,8 @@ func TestCheckForUpdates_Wrapper(t *testing.T) {
|
|||
var hits int32
|
||||
releases := []ReleaseInfo{
|
||||
{
|
||||
TagName: "v99.1.0",
|
||||
Name: "v99.1.0",
|
||||
TagName: "v5.1.100",
|
||||
Name: "v5.1.100",
|
||||
Body: "Release notes",
|
||||
Prerelease: false,
|
||||
PublishedAt: time.Date(2024, 2, 3, 4, 5, 6, 0, time.UTC),
|
||||
|
|
@ -150,8 +150,8 @@ func TestCheckForUpdates_Wrapper(t *testing.T) {
|
|||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}{
|
||||
{
|
||||
Name: "pulse-v99.1.0-linux-amd64.tar.gz",
|
||||
BrowserDownloadURL: "https://example.com/pulse-v99.1.0-linux-amd64.tar.gz",
|
||||
Name: "pulse-v5.1.100-linux-amd64.tar.gz",
|
||||
BrowserDownloadURL: "https://example.com/pulse-v5.1.100-linux-amd64.tar.gz",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -171,7 +171,7 @@ func TestCheckForUpdates_Wrapper(t *testing.T) {
|
|||
if !info.Available {
|
||||
t.Fatalf("expected update to be available")
|
||||
}
|
||||
if info.LatestVersion != "99.1.0" {
|
||||
t.Fatalf("LatestVersion = %q, want 99.1.0", info.LatestVersion)
|
||||
if info.LatestVersion != "5.1.100" {
|
||||
t.Fatalf("LatestVersion = %q, want 5.1.100", info.LatestVersion)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,8 +122,16 @@ func TestGetLatestReleaseFromFeedMocked(t *testing.T) {
|
|||
t.Cleanup(func() { http.DefaultTransport = origTransport })
|
||||
|
||||
manager := NewManager(&config.Config{})
|
||||
stableCurrent, err := ParseVersion("4.36.1")
|
||||
if err != nil {
|
||||
t.Fatalf("parse stable current version: %v", err)
|
||||
}
|
||||
rcCurrent, err := ParseVersion("5.0.0-rc.0")
|
||||
if err != nil {
|
||||
t.Fatalf("parse rc current version: %v", err)
|
||||
}
|
||||
|
||||
release, err := manager.getLatestReleaseFromFeed(context.Background(), "stable")
|
||||
release, err := manager.getLatestReleaseFromFeed(context.Background(), "stable", stableCurrent)
|
||||
if err != nil {
|
||||
t.Fatalf("stable feed error: %v", err)
|
||||
}
|
||||
|
|
@ -131,7 +139,7 @@ func TestGetLatestReleaseFromFeedMocked(t *testing.T) {
|
|||
t.Fatalf("unexpected stable tag: %s", release.TagName)
|
||||
}
|
||||
|
||||
release, err = manager.getLatestReleaseFromFeed(context.Background(), "rc")
|
||||
release, err = manager.getLatestReleaseFromFeed(context.Background(), "rc", rcCurrent)
|
||||
if err != nil {
|
||||
t.Fatalf("rc feed error: %v", err)
|
||||
}
|
||||
|
|
@ -140,7 +148,7 @@ func TestGetLatestReleaseFromFeedMocked(t *testing.T) {
|
|||
}
|
||||
|
||||
feed = `<?xml version="1.0" encoding="UTF-8"?><feed></feed>`
|
||||
if _, err := manager.getLatestReleaseFromFeed(context.Background(), "stable"); err == nil {
|
||||
if _, err := manager.getLatestReleaseFromFeed(context.Background(), "stable", stableCurrent); err == nil {
|
||||
t.Fatal("expected error for empty feed")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,19 @@ func TestRCUpdateNotifications(t *testing.T) {
|
|||
expectUpdate: true,
|
||||
description: "RC user should see stable release even if RC number is same (4.22.0 > 4.22.0-rc.1)",
|
||||
},
|
||||
{
|
||||
name: "RC user ignores releases outside current line",
|
||||
currentVersion: "5.1.28-rc.1",
|
||||
releases: []ReleaseInfo{
|
||||
{TagName: "v6.0.0-rc.1", Prerelease: true},
|
||||
{TagName: "v6.0.0", Prerelease: false},
|
||||
{TagName: "v5.1.28", Prerelease: false},
|
||||
{TagName: "v5.1.28-rc.2", Prerelease: true},
|
||||
},
|
||||
expectedVersion: "v5.1.28",
|
||||
expectUpdate: true,
|
||||
description: "RC users on the maintenance line should not drift to the next major release",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -207,6 +220,18 @@ func TestStableUpdateNotifications(t *testing.T) {
|
|||
expectUpdate: true, // Returns latest version even if already on it (Available will be false)
|
||||
description: "Returns latest stable version even when already on it",
|
||||
},
|
||||
{
|
||||
name: "Stable user ignores newer stable outside current line",
|
||||
currentVersion: "5.1.27",
|
||||
releases: []ReleaseInfo{
|
||||
{TagName: "v6.0.0", Prerelease: false},
|
||||
{TagName: "v5.1.28", Prerelease: false},
|
||||
{TagName: "v5.1.27", Prerelease: false},
|
||||
},
|
||||
expectedVersion: "v5.1.28",
|
||||
expectUpdate: true,
|
||||
description: "Stable users on 5.1 should stay on the 5.1 maintenance line",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -249,6 +274,69 @@ func TestStableUpdateNotifications(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSelectLatestReleaseForChannel_LineAware(t *testing.T) {
|
||||
currentVer, err := ParseVersion("5.1.28")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse current version: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
channel string
|
||||
releases []ReleaseInfo
|
||||
expectedTag string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "stable selects newest stable in line",
|
||||
channel: "stable",
|
||||
releases: []ReleaseInfo{
|
||||
{TagName: "v6.0.0", Prerelease: false},
|
||||
{TagName: "v5.1.29", Prerelease: false},
|
||||
{TagName: "v5.1.30-rc.1", Prerelease: true},
|
||||
},
|
||||
expectedTag: "v5.1.29",
|
||||
},
|
||||
{
|
||||
name: "rc selects highest in line",
|
||||
channel: "rc",
|
||||
releases: []ReleaseInfo{
|
||||
{TagName: "v6.0.0-rc.1", Prerelease: true},
|
||||
{TagName: "v5.1.30-rc.1", Prerelease: true},
|
||||
{TagName: "v5.1.29", Prerelease: false},
|
||||
},
|
||||
expectedTag: "v5.1.30-rc.1",
|
||||
},
|
||||
{
|
||||
name: "stable errors when no matching line exists",
|
||||
channel: "stable",
|
||||
releases: []ReleaseInfo{
|
||||
{TagName: "v6.0.0", Prerelease: false},
|
||||
{TagName: "v6.0.1", Prerelease: false},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
release, err := selectLatestReleaseForChannel(tt.releases, tt.channel, currentVer)
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got release %s", release.TagName)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if release.TagName != tt.expectedTag {
|
||||
t.Fatalf("selected release %s, want %s", release.TagName, tt.expectedTag)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildDummyTarball(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ set -euo pipefail
|
|||
|
||||
# Configuration
|
||||
GITHUB_REPO="rcourtman/Pulse"
|
||||
DEFAULT_SOURCE_BRANCH="release/5.1"
|
||||
INSTALL_DIR="/opt/pulse"
|
||||
CONFIG_DIR="/etc/pulse"
|
||||
LOG_TAG="pulse-auto-update"
|
||||
|
|
@ -61,19 +62,27 @@ get_current_version() {
|
|||
# Get latest stable release from GitHub
|
||||
get_latest_stable_version() {
|
||||
local latest_version=""
|
||||
local releases_json=""
|
||||
local feed_xml=""
|
||||
local api_url="https://api.github.com/repos/$GITHUB_REPO/releases"
|
||||
local feed_url="https://github.com/$GITHUB_REPO/releases.atom"
|
||||
|
||||
# Get latest stable release (not pre-releases)
|
||||
latest_version=$(curl -s "https://api.github.com/repos/$GITHUB_REPO/releases/latest" | \
|
||||
grep '"tag_name":' | \
|
||||
sed -E 's/.*"([^"]+)".*/\1/' || true)
|
||||
|
||||
# Check if we got rate limited or failed
|
||||
if [[ -z "$latest_version" ]] || [[ "$latest_version" == *"rate limit"* ]]; then
|
||||
# Try direct GitHub latest URL as fallback
|
||||
latest_version=$(curl -sI "https://github.com/$GITHUB_REPO/releases/latest" | \
|
||||
grep -i '^location:' | \
|
||||
sed -E 's|.*tag/([^[:space:]]+).*|\1|' | \
|
||||
tr -d '\r' || true)
|
||||
releases_json=$(curl -fsSL --connect-timeout 5 --max-time 10 "$api_url" 2>/dev/null || true)
|
||||
if [[ -n "$releases_json" ]]; then
|
||||
latest_version=$(printf '%s' "$releases_json" | \
|
||||
grep -oE '"tag_name":[[:space:]]*"v5\.1\.[0-9]+"' | \
|
||||
head -1 | \
|
||||
sed -E 's/.*"([^"]+)"/\1/' || true)
|
||||
fi
|
||||
|
||||
if [[ -z "$latest_version" ]]; then
|
||||
feed_xml=$(curl -fsSL --connect-timeout 5 --max-time 10 "$feed_url" 2>/dev/null || true)
|
||||
if [[ -n "$feed_xml" ]]; then
|
||||
latest_version=$(printf '%s' "$feed_xml" | \
|
||||
grep -oE '<title>Pulse v5\.1\.[0-9]+</title>' | \
|
||||
head -1 | \
|
||||
sed -E 's#<title>Pulse (v[^<]+)</title>#\1#' || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "${latest_version:-}"
|
||||
|
|
@ -201,15 +210,17 @@ perform_update() {
|
|||
# Run install script with specific version
|
||||
local marker_file="$INSTALL_DIR/BUILD_FROM_SOURCE"
|
||||
local -a installer_args=(--version "$new_version")
|
||||
local install_script_url="https://raw.githubusercontent.com/$GITHUB_REPO/$DEFAULT_SOURCE_BRANCH/install.sh"
|
||||
if [[ -f "$marker_file" ]]; then
|
||||
local branch
|
||||
branch=$(tr -d '\r\n' <"$marker_file" 2>/dev/null || true)
|
||||
if [[ -n "$branch" ]]; then
|
||||
installer_args=(--source "$branch" "${installer_args[@]}")
|
||||
install_script_url="https://raw.githubusercontent.com/$GITHUB_REPO/$branch/install.sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
if curl -sSL "https://github.com/$GITHUB_REPO/releases/latest/download/install.sh" | \
|
||||
if curl -sSL "$install_script_url" | \
|
||||
bash -s -- "${installer_args[@]}" 2>&1 | \
|
||||
while IFS= read -r line; do
|
||||
log info "installer: $line"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue