-
Notifications
You must be signed in to change notification settings - Fork 2
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Sindri Upgrade Feature - Technical Implementation Plan
This document outlines the technical implementation plan for adding in-place upgrade capability to Sindri instances.
Overview
The sindri upgrade command enables users to upgrade a running Sindri instance from one version to another without requiring a full container rebuild. This applies to both local development environments and deployed instances (Fly.io, Docker, DevPod).
Goals
- Seamless upgrades: Upgrade Sindri CLI, extensions, and schemas in-place
- Safe rollback: Automatic backup with one-command rollback capability
- Extension upgrades: Upgrade installed extensions and their package dependencies
- Testable in CI: Verify upgrade paths before releases
- Minimal downtime: No container restart required for most upgrades
Non-Goals (Phase 1)
- Upgrading system binaries (
mise,claude) - these require container rebuild - Cross-major-version migrations with breaking schema changes
- Automatic extension compatibility resolution
Architecture
Components to Upgrade
Based on Dockerfile lines 90-92, upgrades target:
/docker/lib/ # Extension definitions, schemas, common.sh, profiles, registry
/docker/cli/ # sindri, extension-manager, VERSION
/docker/deploy/ # Provider adapters (docker, fly, devpod, k8s)
Upgrade Flow
┌─────────────────────────────────────────────────────────────────────┐
│ sindri upgrade │
├─────────────────────────────────────────────────────────────────────┤
│ 1. Check Context (inside container vs local dev) │
│ 2. Read current VERSION (/docker/cli/VERSION or cli/VERSION) │
│ 3. Query GitHub API for latest release │
│ 4. Compare versions (semver) │
│ 5. If newer version available: │
│ a. Download release tarball │
│ b. Extract to temp directory │
│ c. Backup current installation │
│ d. Apply new files │
│ e. Validate installation │
│ f. Upgrade installed extensions (optional) │
│ 6. Report success/failure │
└─────────────────────────────────────────────────────────────────────┘
Directory Structure
/docker/
├── .backup/ # Versioned backups (NEW)
│ └── 1.5.0/ # Backup of version 1.5.0
│ ├── lib/
│ ├── cli/
│ └── deploy/
├── .upgrade/ # Temporary upgrade staging (NEW)
│ └── sindri-1.6.0/ # Extracted release
├── lib/ # Extension system (upgraded in-place)
├── cli/ # CLI tools (upgraded in-place)
└── deploy/ # Adapters (upgraded in-place)
Implementation
Phase 1: Core Upgrade Command
File: cli/sindri (additions)
# New command: upgrade
# sindri upgrade # Upgrade to latest version
# sindri upgrade --check # Check for updates without applying
# sindri upgrade --rollback # Restore previous version
# sindri upgrade --version X # Upgrade to specific version
# sindri upgrade --extensions # Also upgrade installed extensionsNew Module: cli/upgrade.sh
Primary upgrade logic module with functions:
| Function | Description |
|---|---|
check_upgrade_available |
Query GitHub API, compare versions |
download_release |
Download and extract release tarball |
backup_current |
Create versioned backup of current installation |
apply_upgrade |
Copy new files over existing installation |
validate_upgrade |
Run sanity checks post-upgrade |
rollback |
Restore from backup |
upgrade_extensions |
Trigger extension upgrades |
Version Comparison
# Semantic version comparison
# Returns: 0 if equal, 1 if v1 > v2, 2 if v1 < v2
version_compare() {
local v1="$1" v2="$2"
# Strip 'v' prefix if present
v1="${v1#v}"
v2="${v2#v}"
# Compare major.minor.patch
...
}GitHub API Integration
# Query latest release
get_latest_release() {
local repo="pacphi/sindri"
gh api "repos/${repo}/releases/latest" --jq '.tag_name' 2>/dev/null || \
curl -s "https://api.github.com/repos/${repo}/releases/latest" | jq -r '.tag_name'
}
# Query specific release
get_release_info() {
local version="$1"
gh api "repos/${repo}/releases/tags/v${version}" 2>/dev/null || \
curl -s "https://api.github.com/repos/${repo}/releases/tags/v${version}"
}Phase 2: Release Artifact Enhancement
Current Release Assets
The release workflow (.github/workflows/release.yml) currently produces:
install.sh- Fresh installation scriptQUICK_REFERENCE.md- Documentation
New Release Asset: sindri-upgrade-{version}.tar.gz
Add to release workflow:
- name: Create upgrade tarball
run: |
version="${{ needs.validate-tag.outputs.version }}"
# Create minimal upgrade package
mkdir -p sindri-upgrade-${version}
cp -a docker/lib sindri-upgrade-${version}/
cp -a cli sindri-upgrade-${version}/
cp -a deploy sindri-upgrade-${version}/
# Include upgrade manifest
cat > sindri-upgrade-${version}/UPGRADE_MANIFEST.json << EOF
{
"version": "${version}",
"created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"components": ["lib", "cli", "deploy"],
"schemaVersion": "1.0",
"minUpgradeFrom": "1.0.0",
"checksums": {
"lib": "$(find sindri-upgrade-${version}/lib -type f -exec sha256sum {} \; | sha256sum | cut -d' ' -f1)",
"cli": "$(find sindri-upgrade-${version}/cli -type f -exec sha256sum {} \; | sha256sum | cut -d' ' -f1)",
"deploy": "$(find sindri-upgrade-${version}/deploy -type f -exec sha256sum {} \; | sha256sum | cut -d' ' -f1)"
}
}
EOF
tar czf sindri-upgrade-${version}.tar.gz sindri-upgrade-${version}/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
install.sh
QUICK_REFERENCE.md
sindri-upgrade-${{ needs.validate-tag.outputs.version }}.tar.gzPhase 3: Extension Upgrade Integration
Extension Upgrade Schema (Already Exists)
From docker/lib/schemas/extension.schema.json:
upgrade:
strategy: automatic|manual|none
mise:
upgradeAll: true
tools: [tool1, tool2]
apt:
packages: [pkg1, pkg2]
updateFirst: true
script:
path: upgrade.sh
timeout: 600New Module: cli/extension-manager-modules/upgrade.sh
# Upgrade single extension
upgrade_extension() {
local ext_name="$1"
local ext_dir="${DOCKER_LIB}/extensions/${ext_name}"
# Read upgrade strategy
local strategy
strategy=$(load_yaml "${ext_dir}/extension.yaml" ".upgrade.strategy" 2>/dev/null || echo "automatic")
case "$strategy" in
automatic)
upgrade_extension_automatic "$ext_name"
;;
manual)
upgrade_extension_manual "$ext_name"
;;
none)
print_warning "Extension $ext_name does not support upgrades"
;;
esac
}
# Automatic upgrade (mise-based)
upgrade_extension_automatic() {
local ext_name="$1"
local ext_dir="${DOCKER_LIB}/extensions/${ext_name}"
# Check for mise upgrade config
if load_yaml "${ext_dir}/extension.yaml" ".upgrade.mise" &>/dev/null; then
local upgrade_all
upgrade_all=$(load_yaml "${ext_dir}/extension.yaml" ".upgrade.mise.upgradeAll" 2>/dev/null || echo "true")
if [[ "$upgrade_all" == "true" ]]; then
mise upgrade --yes
else
local tools
tools=$(load_yaml "${ext_dir}/extension.yaml" ".upgrade.mise.tools[]" 2>/dev/null)
for tool in $tools; do
mise upgrade "$tool" --yes
done
fi
fi
# Check for apt upgrade config
if load_yaml "${ext_dir}/extension.yaml" ".upgrade.apt" &>/dev/null; then
local update_first
update_first=$(load_yaml "${ext_dir}/extension.yaml" ".upgrade.apt.updateFirst" 2>/dev/null || echo "true")
[[ "$update_first" == "true" ]] && sudo apt-get update
local packages
packages=$(load_yaml "${ext_dir}/extension.yaml" ".upgrade.apt.packages[]" 2>/dev/null)
if [[ -n "$packages" ]]; then
sudo apt-get upgrade -y $packages
fi
fi
}
# Manual upgrade (script-based)
upgrade_extension_manual() {
local ext_name="$1"
local ext_dir="${DOCKER_LIB}/extensions/${ext_name}"
local script_path
script_path=$(load_yaml "${ext_dir}/extension.yaml" ".upgrade.script.path" 2>/dev/null)
if [[ -n "$script_path" && -f "${ext_dir}/${script_path}" ]]; then
local timeout
timeout=$(load_yaml "${ext_dir}/extension.yaml" ".upgrade.script.timeout" 2>/dev/null || echo "600")
timeout "$timeout" bash "${ext_dir}/${script_path}"
fi
}New Command: extension-manager upgrade
Add to cli/extension-manager:
upgrade)
if [[ $# -eq 0 ]]; then
# Upgrade all installed extensions
local installed
installed=$(get_active_extensions)
for ext in $installed; do
upgrade_extension "$ext"
done
else
# Upgrade specific extension
upgrade_extension "$1"
fi
;;Phase 4: CI/CD Testing
New Workflow: .github/workflows/test-upgrade.yml
name: Test Upgrade Path
on:
pull_request:
branches: [main]
paths:
- 'cli/**'
- 'docker/**'
- 'deploy/**'
workflow_dispatch:
inputs:
from_version:
description: 'Version to upgrade from'
required: true
default: 'latest'
to_version:
description: 'Version to upgrade to'
required: false
default: 'current'
jobs:
test-upgrade-local:
name: Test Local Upgrade
runs-on: ubuntu-latest
steps:
- name: Checkout current branch
uses: actions/checkout@v6
with:
path: current
- name: Determine versions
id: versions
run: |
# Get 'from' version (previous release or specified)
if [[ "${{ inputs.from_version }}" == "latest" || -z "${{ inputs.from_version }}" ]]; then
from_version=$(gh api repos/${{ github.repository }}/releases/latest --jq .tag_name | tr -d 'v')
else
from_version="${{ inputs.from_version }}"
fi
# Get 'to' version (current branch or specified)
if [[ "${{ inputs.to_version }}" == "current" || -z "${{ inputs.to_version }}" ]]; then
to_version=$(cat current/cli/VERSION)
else
to_version="${{ inputs.to_version }}"
fi
echo "from=$from_version" >> $GITHUB_OUTPUT
echo "to=$to_version" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout 'from' version
uses: actions/checkout@v6
with:
ref: v${{ steps.versions.outputs.from }}
path: from-version
- name: Set up Docker
uses: docker/setup-buildx-action@v3
- name: Build 'from' version image
run: |
cd from-version
docker build -t sindri:from .
- name: Start container with 'from' version
run: |
docker run -d --name sindri-upgrade-test \
-v sindri-test-home:/alt/home/developer \
sindri:from sleep infinity
- name: Install test extensions (baseline)
run: |
docker exec sindri-upgrade-test \
/docker/cli/extension-manager install nodejs
docker exec sindri-upgrade-test \
/docker/cli/extension-manager status
- name: Copy 'to' version into container
run: |
# Simulate upgrade tarball
docker cp current/docker/lib sindri-upgrade-test:/tmp/upgrade-lib
docker cp current/cli sindri-upgrade-test:/tmp/upgrade-cli
docker cp current/deploy sindri-upgrade-test:/tmp/upgrade-deploy
- name: Run upgrade
run: |
docker exec sindri-upgrade-test bash -c '
# Backup current
mkdir -p /docker/.backup/$(cat /docker/cli/VERSION)
cp -a /docker/lib /docker/.backup/$(cat /docker/cli/VERSION)/
cp -a /docker/cli /docker/.backup/$(cat /docker/cli/VERSION)/
cp -a /docker/deploy /docker/.backup/$(cat /docker/cli/VERSION)/
# Apply upgrade
rsync -a /tmp/upgrade-lib/ /docker/lib/
rsync -a /tmp/upgrade-cli/ /docker/cli/
rsync -a /tmp/upgrade-deploy/ /docker/deploy/
echo "Upgrade complete"
'
- name: Validate upgrade
run: |
# Check version updated
docker exec sindri-upgrade-test cat /docker/cli/VERSION
# Check CLI works
docker exec sindri-upgrade-test /docker/cli/sindri --version
# Check extension-manager works
docker exec sindri-upgrade-test /docker/cli/extension-manager list
# Check installed extensions still valid
docker exec sindri-upgrade-test /docker/cli/extension-manager validate nodejs
- name: Test rollback
run: |
docker exec sindri-upgrade-test bash -c '
# Rollback to previous version
backup_version=$(ls /docker/.backup/ | head -1)
cp -a /docker/.backup/${backup_version}/lib/* /docker/lib/
cp -a /docker/.backup/${backup_version}/cli/* /docker/cli/
cp -a /docker/.backup/${backup_version}/deploy/* /docker/deploy/
echo "Rollback complete"
'
# Verify rollback
docker exec sindri-upgrade-test /docker/cli/sindri --version
- name: Cleanup
if: always()
run: |
docker rm -f sindri-upgrade-test || true
docker volume rm sindri-test-home || true
test-upgrade-fly:
name: Test Fly.io Upgrade
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup flyctl
uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy 'from' version
run: |
# Deploy older version to test app
echo "Deploy from version and test upgrade via SSH"
# flyctl deploy --app sindri-upgrade-test --image ghcr.io/pacphi/sindri:${{ inputs.from_version }}
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: Run upgrade via SSH
run: |
echo "SSH into app and run: sindri upgrade"
# flyctl ssh console --app sindri-upgrade-test -C "/docker/cli/sindri upgrade"Integration with Release Workflow
Add upgrade testing to .github/workflows/release.yml:
# New job: Test upgrade before publishing
test-upgrade-path:
runs-on: ubuntu-latest
needs: [validate-tag, build-image]
steps:
- uses: actions/checkout@v6
- name: Get previous version
id: prev
run: |
prev_tag=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
echo "version=${prev_tag#v}" >> $GITHUB_OUTPUT
- name: Test upgrade from previous release
if: steps.prev.outputs.version != ''
run: |
# Pull previous version
docker pull ghcr.io/${{ github.repository }}:${{ steps.prev.outputs.version }}
# Start container
docker run -d --name upgrade-test \
ghcr.io/${{ github.repository }}:${{ steps.prev.outputs.version }} \
sleep infinity
# Copy new version files
docker cp docker/lib upgrade-test:/tmp/new-lib
docker cp cli upgrade-test:/tmp/new-cli
docker cp deploy upgrade-test:/tmp/new-deploy
# Apply upgrade
docker exec upgrade-test bash -c '
rsync -a /tmp/new-lib/ /docker/lib/
rsync -a /tmp/new-cli/ /docker/cli/
rsync -a /tmp/new-deploy/ /docker/deploy/
'
# Validate
docker exec upgrade-test /docker/cli/sindri --version
docker exec upgrade-test /docker/cli/extension-manager listCLI Interface
sindri upgrade
Usage: sindri upgrade [OPTIONS]
Upgrade Sindri to a newer version.
Options:
--check Check for updates without applying
--version <ver> Upgrade to specific version (default: latest)
--extensions Also upgrade installed extensions
--rollback Restore previous version from backup
--force Skip confirmation prompts
--dry-run Show what would be upgraded without making changes
-h, --help Show this help message
Examples:
sindri upgrade # Upgrade to latest version
sindri upgrade --check # Check if update available
sindri upgrade --version 1.6.0 # Upgrade to specific version
sindri upgrade --extensions # Upgrade Sindri and all extensions
sindri upgrade --rollback # Restore previous version
Environment:
SINDRI_UPGRADE_REPO Override GitHub repository (default: pacphi/sindri)
SINDRI_UPGRADE_CHANNEL Release channel: stable, beta, alpha (default: stable)
extension-manager upgrade
Usage: extension-manager upgrade [EXTENSION]
Upgrade extensions to latest versions.
Arguments:
EXTENSION Specific extension to upgrade (optional)
If omitted, upgrades all installed extensions
Options:
--dry-run Show what would be upgraded
--force Skip confirmation prompts
-h, --help Show this help message
Examples:
extension-manager upgrade # Upgrade all extensions
extension-manager upgrade nodejs # Upgrade nodejs extension only
Rollback Strategy
Backup Format
/docker/.backup/
└── 1.5.0/
├── lib/
│ ├── extensions/
│ ├── schemas/
│ ├── profiles.yaml
│ └── ...
├── cli/
│ ├── sindri
│ ├── extension-manager
│ └── VERSION
└── deploy/
└── adapters/
Rollback Process
sindri upgrade --rollback
# 1. List available backups
# 2. Select version to restore (default: most recent)
# 3. Validate backup integrity
# 4. Copy backup over current installation
# 5. Validate restored installationBackup Retention
- Keep last 3 versions by default
- Configurable via
SINDRI_BACKUP_RETAINenvironment variable - Automatic cleanup during upgrade
Error Handling
Pre-upgrade Checks
| Check | Action on Failure |
|---|---|
| Network connectivity | Abort with helpful message |
| Disk space (100MB free) | Abort with warning |
Write permissions to /docker/ |
Abort with permission instructions |
| GitHub API rate limit | Retry with backoff, then abort |
| Invalid current VERSION | Warn, attempt upgrade anyway |
Upgrade Failure Recovery
- Download fails: Clean temp, report error, no changes made
- Backup fails: Abort upgrade, report disk space issue
- Apply fails: Automatic rollback from backup
- Validation fails: Prompt for manual rollback
Validation Steps
validate_upgrade() {
local errors=0
# Check VERSION file updated
[[ -f /docker/cli/VERSION ]] || ((errors++))
# Check sindri CLI works
/docker/cli/sindri --version >/dev/null 2>&1 || ((errors++))
# Check extension-manager works
/docker/cli/extension-manager list >/dev/null 2>&1 || ((errors++))
# Check schemas are valid JSON
for schema in /docker/lib/schemas/*.json; do
jq empty "$schema" 2>/dev/null || ((errors++))
done
return $errors
}Testing Strategy
Unit Tests
| Test | Location |
|---|---|
| Version comparison | test/unit/upgrade/version_compare.bats |
| Backup creation | test/unit/upgrade/backup.bats |
| Rollback | test/unit/upgrade/rollback.bats |
| GitHub API parsing | test/unit/upgrade/github_api.bats |
Integration Tests
| Test | Location |
|---|---|
| Full upgrade cycle | test/integration/upgrade_cycle.bats |
| Extension upgrade | test/integration/extension_upgrade.bats |
| Cross-version compat | CI workflow test-upgrade.yml |
Manual Testing Matrix
| From Version | To Version | Provider | Test Result |
|---|---|---|---|
| 1.4.0 | 1.5.0 | Docker | |
| 1.4.0 | 1.5.0 | Fly.io | |
| 1.5.0 | 1.6.0 | Docker | |
| 1.5.0 | 1.6.0 | Fly.io | |
| 1.5.0 | 1.6.0 | DevPod |
Implementation Phases
Phase 1: Core Upgrade (Week 1-2)
- Create
cli/upgrade.shmodule - Add
upgradecommand tocli/sindri - Implement version check and comparison
- Implement download and extraction
- Implement backup and restore
- Add basic validation
Phase 2: Release Integration (Week 2-3)
- Add upgrade tarball to release workflow
- Create
UPGRADE_MANIFEST.jsonformat - Add checksum verification
- Document upgrade in release notes
Phase 3: Extension Upgrades (Week 3-4)
- Create
cli/extension-manager-modules/upgrade.sh - Add
upgradecommand to extension-manager - Implement mise-based upgrades
- Implement apt-based upgrades
- Implement script-based upgrades
Phase 4: CI Testing (Week 4-5)
- Create
test-upgrade.ymlworkflow - Add upgrade tests to release workflow
- Create upgrade test matrix
- Document testing procedures
Phase 5: Documentation & Polish (Week 5-6)
- Update CLI.md with upgrade commands
- Update RELEASE.md with upgrade testing
- Create UPGRADING.md guide
- Add troubleshooting section
Security Considerations
- Release Verification: Verify release checksums before applying
- GitHub API: Use authenticated requests when possible
- File Permissions: Preserve ownership and permissions during upgrade
- Backup Security: Backups contain same files, no additional exposure
- Network Security: HTTPS-only for all downloads
Open Questions
- Minimum supported upgrade path: Should we support upgrading from any version, or only N-1?
- Extension compatibility: How to handle extension schema changes between versions?
- Partial upgrades: Should we support upgrading only CLI without extensions?
- Automatic upgrades: Should there be a
--autoflag for unattended upgrades? - Notification: Should Sindri check for updates on startup and notify users?
References
- RELEASE.md - Current release process
- extension.schema.json - Extension upgrade schema
- Dockerfile - Build process and file locations
- GitHub Releases API
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request