From 176fcc13698162d9501603492b2114d2bfeda89e Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Fri, 12 Dec 2025 11:53:06 -0500 Subject: [PATCH] feat(installer): use GitHub API digests for checksum verification Replace separate .sha256 checksum files with GitHub's built-in SHA256 digests from the release API. This simplifies the release process and leverages GitHub's native asset digest feature. Changes: - install.sh: Fetch release info and extract digest from API response - install.ps1: Use Invoke-RestMethod to get release data with digests - release.yml: Remove .sha256 file generation and upload steps Closes #103 --- .github/workflows/release.yml | 39 +---------- install.ps1 | 110 ++++++++++++++++++++---------- install.sh | 122 +++++++++++++++++++++++----------- 3 files changed, 160 insertions(+), 111 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index efb6373..bf161f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -209,38 +209,11 @@ jobs: cd .. shell: bash - - name: Generate SHA256 checksum (Unix) - if: matrix.goos != 'windows' - run: | - cd dist - ARCHIVE_NAME="dtvem-${{ github.event.inputs.version }}-${{ matrix.asset_name_suffix }}.${{ matrix.archive_ext }}" - if command -v sha256sum &> /dev/null; then - sha256sum "$ARCHIVE_NAME" > "$ARCHIVE_NAME.sha256" - else - shasum -a 256 "$ARCHIVE_NAME" > "$ARCHIVE_NAME.sha256" - fi - echo "Generated checksum:" - cat "$ARCHIVE_NAME.sha256" - shell: bash - - - name: Generate SHA256 checksum (Windows) - if: matrix.goos == 'windows' - run: | - $archiveName = "dtvem-${{ github.event.inputs.version }}-${{ matrix.asset_name_suffix }}.${{ matrix.archive_ext }}" - $archivePath = "dist/$archiveName" - $hash = (Get-FileHash -Path $archivePath -Algorithm SHA256).Hash.ToLower() - "$hash $archiveName" | Out-File -FilePath "dist/$archiveName.sha256" -Encoding ASCII -NoNewline - Write-Host "Generated checksum:" - Get-Content "dist/$archiveName.sha256" - shell: pwsh - - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-${{ matrix.asset_name_suffix }} - path: | - dist/dtvem-${{ github.event.inputs.version }}-${{ matrix.asset_name_suffix }}.${{ matrix.archive_ext }} - dist/dtvem-${{ github.event.inputs.version }}-${{ matrix.asset_name_suffix }}.${{ matrix.archive_ext }}.sha256 + path: dist/dtvem-${{ github.event.inputs.version }}-${{ matrix.asset_name_suffix }}.${{ matrix.archive_ext }} retention-days: 1 - name: Upload install scripts (linux-amd64 only) @@ -301,15 +274,10 @@ jobs: tag_name: v${{ github.event.inputs.version }} files: | artifacts/build-linux-amd64/dtvem-${{ github.event.inputs.version }}-linux-amd64.tar.gz - artifacts/build-linux-amd64/dtvem-${{ github.event.inputs.version }}-linux-amd64.tar.gz.sha256 artifacts/build-macos-amd64/dtvem-${{ github.event.inputs.version }}-macos-amd64.tar.gz - artifacts/build-macos-amd64/dtvem-${{ github.event.inputs.version }}-macos-amd64.tar.gz.sha256 artifacts/build-macos-arm64/dtvem-${{ github.event.inputs.version }}-macos-arm64.tar.gz - artifacts/build-macos-arm64/dtvem-${{ github.event.inputs.version }}-macos-arm64.tar.gz.sha256 artifacts/build-windows-amd64/dtvem-${{ github.event.inputs.version }}-windows-amd64.zip - artifacts/build-windows-amd64/dtvem-${{ github.event.inputs.version }}-windows-amd64.zip.sha256 artifacts/build-windows-arm64/dtvem-${{ github.event.inputs.version }}-windows-arm64.zip - artifacts/build-windows-arm64/dtvem-${{ github.event.inputs.version }}-windows-arm64.zip.sha256 install.sh install.ps1 body: | @@ -343,11 +311,6 @@ jobs: - ✅ Windows (amd64, arm64) - ✅ macOS (amd64, arm64/Apple Silicon) - ✅ Linux (amd64) - - ## Checksums - - SHA256 checksums are provided for each archive (`.sha256` files). - The installers automatically verify checksums before extraction. draft: false prerelease: false generate_release_notes: false diff --git a/install.ps1 b/install.ps1 index e74f318..a208956 100644 --- a/install.ps1 +++ b/install.ps1 @@ -35,38 +35,68 @@ function Write-Warning-Custom { Write-Host $Message } -function Get-LatestVersion { +# Global variable to store release data +$script:ReleaseData = $null + +function Get-ReleaseInfo { + param([string]$Version) + try { - $response = Invoke-RestMethod -Uri "https://api.github.com/repos/$REPO/releases/latest" - return $response.tag_name + if ($Version) { + $apiUrl = "https://api.github.com/repos/$REPO/releases/tags/$Version" + } + else { + $apiUrl = "https://api.github.com/repos/$REPO/releases/latest" + } + + $script:ReleaseData = Invoke-RestMethod -Uri $apiUrl + return $script:ReleaseData.tag_name } catch { - Write-Error-Custom "Failed to fetch latest version: $_" + Write-Error-Custom "Failed to fetch release information: $_" exit 1 } } +function Get-AssetDigest { + param([string]$AssetName) + + if (-not $script:ReleaseData) { + return $null + } + + # Find the asset with matching name + $asset = $script:ReleaseData.assets | Where-Object { $_.name -eq $AssetName } + + if (-not $asset) { + return $null + } + + # GitHub returns digest in format "sha256:hash" + if ($asset.digest -and $asset.digest.StartsWith("sha256:")) { + return $asset.digest.Substring(7) + } + + return $null +} + function Test-Checksum { param( [string]$FilePath, - [string]$ChecksumPath + [string]$ExpectedHash ) - if (-not (Test-Path $ChecksumPath)) { - Write-Error-Custom "Checksum file not found: $ChecksumPath" - return $false + if (-not $ExpectedHash) { + Write-Warning-Custom "No checksum available from GitHub API - skipping verification" + return $true } - # Read expected hash from checksum file (format: "hash filename") - $checksumContent = Get-Content $ChecksumPath -Raw - $expectedHash = ($checksumContent -split '\s+')[0].ToLower() - # Calculate actual hash $actualHash = (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash.ToLower() - if ($expectedHash -ne $actualHash) { + if ($ExpectedHash.ToLower() -ne $actualHash) { Write-Error-Custom "Checksum verification failed!" - Write-Error-Custom "Expected: $expectedHash" + Write-Error-Custom "Expected: $ExpectedHash" Write-Error-Custom "Actual: $actualHash" return $false } @@ -90,22 +120,33 @@ function Main { } Write-Info "Detected platform: windows-$ARCH" - # Get version (priority: DTVEM_VERSION env var > hardcoded > fetch latest) + # Determine version to install + $requestedVersion = $null if ($env:DTVEM_VERSION) { - $VERSION = $env:DTVEM_VERSION - Write-Info "Installing user-specified version: $VERSION" + $requestedVersion = $env:DTVEM_VERSION + Write-Info "Installing user-specified version: $requestedVersion" } elseif ($DTVEM_RELEASE_VERSION) { - $VERSION = $DTVEM_RELEASE_VERSION - Write-Info "Installing release version: $VERSION" + $requestedVersion = $DTVEM_RELEASE_VERSION + Write-Info "Installing release version: $requestedVersion" } else { Write-Info "Fetching latest release..." - $VERSION = Get-LatestVersion + } + + # Get release info from GitHub API + $VERSION = Get-ReleaseInfo -Version $requestedVersion + + if (-not $VERSION) { + Write-Error-Custom "Failed to determine version" + exit 1 + } + + if (-not $requestedVersion) { Write-Success "Latest version: $VERSION" } - # Strip "v" prefix from version for archive name (GitHub releases use v1.0.0 in paths, but archives are named 1.0.0) + # Strip "v" prefix from version for archive name $VERSION_NO_V = $VERSION.TrimStart('v') # Construct download URL @@ -114,6 +155,16 @@ function Main { Write-Info "Download URL: $DOWNLOAD_URL" + # Get expected checksum from GitHub API + Write-Info "Fetching checksum from GitHub API..." + $EXPECTED_HASH = Get-AssetDigest -AssetName $ARCHIVE_NAME + if ($EXPECTED_HASH) { + Write-Success "Got checksum: $($EXPECTED_HASH.Substring(0, 16))..." + } + else { + Write-Warning-Custom "Checksum not available from API (may be an older release)" + } + # Create temporary directory $TMP_DIR = Join-Path $env:TEMP "dtvem-install-$(Get-Random)" New-Item -ItemType Directory -Path $TMP_DIR -Force | Out-Null @@ -133,22 +184,9 @@ function Main { exit 1 } - # Download and verify checksum - $CHECKSUM_URL = "$DOWNLOAD_URL.sha256" - $CHECKSUM_PATH = Join-Path $TMP_DIR "$ARCHIVE_NAME.sha256" - - Write-Info "Downloading checksum..." - try { - Invoke-WebRequest -Uri $CHECKSUM_URL -OutFile $CHECKSUM_PATH -UseBasicParsing - } - catch { - Write-Error-Custom "Failed to download checksum file: $_" - Write-Error-Custom "URL: $CHECKSUM_URL" - exit 1 - } - + # Verify checksum Write-Info "Verifying checksum..." - if (-not (Test-Checksum -FilePath $ARCHIVE_PATH -ChecksumPath $CHECKSUM_PATH)) { + if (-not (Test-Checksum -FilePath $ARCHIVE_PATH -ExpectedHash $EXPECTED_HASH)) { Write-Error-Custom "Archive integrity check failed - aborting installation" exit 1 } diff --git a/install.sh b/install.sh index 513be09..321ecd2 100644 --- a/install.sh +++ b/install.sh @@ -68,12 +68,13 @@ detect_arch() { esac } -# Get latest release version from GitHub -get_latest_version() { +# Fetch URL content +fetch() { + local url=$1 if command -v curl &> /dev/null; then - curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' + curl -fsSL "$url" elif command -v wget &> /dev/null; then - wget -qO- "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' + wget -qO- "$url" else error "Neither curl nor wget found. Please install one of them." exit 1 @@ -92,19 +93,59 @@ download() { fi } -# Verify SHA256 checksum -verify_checksum() { - local file=$1 - local checksum_file=$2 +# Get release info from GitHub API +# Sets RELEASE_TAG and populates asset digests +get_release_info() { + local version=$1 + local api_url + + if [ -z "$version" ]; then + api_url="https://api.github.com/repos/${REPO}/releases/latest" + else + api_url="https://api.github.com/repos/${REPO}/releases/tags/${version}" + fi + + RELEASE_JSON=$(fetch "$api_url") + if [ -z "$RELEASE_JSON" ]; then + error "Failed to fetch release information" + exit 1 + fi - if [ ! -f "$checksum_file" ]; then - error "Checksum file not found: $checksum_file" + # Extract tag name + RELEASE_TAG=$(echo "$RELEASE_JSON" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') +} + +# Get SHA256 digest for an asset from the release JSON +# GitHub API returns digest in format: "sha256:hash" +get_asset_digest() { + local asset_name=$1 + + # Extract the digest for the specific asset + # The JSON structure has assets array with name and digest fields + # We use grep/sed to parse without requiring jq + local digest + digest=$(echo "$RELEASE_JSON" | \ + grep -A 5 "\"name\"[[:space:]]*:[[:space:]]*\"${asset_name}\"" | \ + grep -o '"digest"[[:space:]]*:[[:space:]]*"sha256:[^"]*"' | \ + head -1 | \ + sed 's/.*"digest"[[:space:]]*:[[:space:]]*"sha256:\([^"]*\)".*/\1/') + + if [ -z "$digest" ]; then return 1 fi - # Extract expected hash from checksum file (format: "hash filename") - local expected_hash - expected_hash=$(awk '{print $1}' "$checksum_file") + echo "$digest" +} + +# Verify SHA256 checksum using GitHub API digest +verify_checksum() { + local file=$1 + local expected_hash=$2 + + if [ -z "$expected_hash" ]; then + warning "No checksum available from GitHub API - skipping verification" + return 0 + fi # Calculate actual hash local actual_hash @@ -139,27 +180,35 @@ main() { ARCH=$(detect_arch) info "Detected platform: ${OS}-${ARCH}" - # Get version (priority: DTVEM_VERSION env var > hardcoded > fetch latest) + # Determine version to install + local requested_version="" if [ -n "$DTVEM_VERSION" ]; then - VERSION="$DTVEM_VERSION" - info "Installing user-specified version: $VERSION" + requested_version="$DTVEM_VERSION" + info "Installing user-specified version: $requested_version" elif [ -n "$DTVEM_RELEASE_VERSION" ]; then - VERSION="$DTVEM_RELEASE_VERSION" - info "Installing release version: $VERSION" + requested_version="$DTVEM_RELEASE_VERSION" + info "Installing release version: $requested_version" else info "Fetching latest release..." - VERSION=$(get_latest_version) - if [ -z "$VERSION" ]; then - error "Failed to fetch latest version" - exit 1 - fi + fi + + # Get release info from GitHub API + get_release_info "$requested_version" + VERSION="$RELEASE_TAG" + + if [ -z "$VERSION" ]; then + error "Failed to determine version" + exit 1 + fi + + if [ -z "$requested_version" ]; then success "Latest version: $VERSION" fi - # Strip "v" prefix from version for archive name (GitHub releases use v1.0.0 in paths, but archives are named 1.0.0) + # Strip "v" prefix from version for archive name VERSION_NO_V="${VERSION#v}" - # Construct download URL + # Construct asset name if [ "$OS" = "darwin" ]; then PLATFORM_NAME="macos" else @@ -171,6 +220,15 @@ main() { info "Download URL: $DOWNLOAD_URL" + # Get expected checksum from GitHub API + info "Fetching checksum from GitHub API..." + EXPECTED_HASH=$(get_asset_digest "$ARCHIVE_NAME") + if [ -n "$EXPECTED_HASH" ]; then + success "Got checksum: ${EXPECTED_HASH:0:16}..." + else + warning "Checksum not available from API (may be an older release)" + fi + # Create temporary directory TMP_DIR=$(mktemp -d) trap 'rm -rf $TMP_DIR' EXIT @@ -187,19 +245,9 @@ main() { success "Downloaded successfully" - # Download and verify checksum - CHECKSUM_URL="${DOWNLOAD_URL}.sha256" - CHECKSUM_PATH="$TMP_DIR/${ARCHIVE_NAME}.sha256" - - info "Downloading checksum..." - if ! download "$CHECKSUM_URL" "$CHECKSUM_PATH"; then - error "Failed to download checksum file" - error "URL: $CHECKSUM_URL" - exit 1 - fi - + # Verify checksum info "Verifying checksum..." - if ! verify_checksum "$ARCHIVE_PATH" "$CHECKSUM_PATH"; then + if ! verify_checksum "$ARCHIVE_PATH" "$EXPECTED_HASH"; then error "Archive integrity check failed - aborting installation" exit 1 fi