diff --git a/.github/workflows/build-fleetctl-msi.yml b/.github/workflows/build-fleetctl-msi.yml new file mode 100644 index 0000000000..326090ac7e --- /dev/null +++ b/.github/workflows/build-fleetctl-msi.yml @@ -0,0 +1,204 @@ +name: Build fleetctl Windows MSI + +# This workflow builds a signed Windows .msi installer for fleetctl +# for manual testing purposes only. +# +# NOTE: For production releases, the goreleaser workflow (.github/workflows/goreleaser-fleet.yaml) +# handles building and uploading the fleetctl MSI automatically when a fleet-* tag is pushed. +# This workflow is kept for manual testing only. +# +# TESTING: +# To test the MSI build process: +# 1. Go to Actions -> Build fleetctl Windows MSI -> Run workflow +# 2. Select your branch (e.g., main or feature branch) +# 3. Test mode defaults to true (recommended) - MSI will be built and uploaded as an artifact +# 4. To test release upload, set test_mode to false (use with caution) + +on: + workflow_dispatch: # Manual trigger for testing only + inputs: + test_mode: + description: "Test mode - will skip release upload if enabled (recommended: true)" + type: boolean + default: true + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + # fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference + shell: bash + +permissions: + contents: write # Needed to upload release assets + id-token: write # Needed for attestations + attestations: write # Needed to create build provenance attestations + +jobs: + build-sign-msi: + runs-on: windows-2022 + timeout-minutes: 60 + strategy: + matrix: + arch: [amd64, arm64] + steps: + - name: Harden Runner + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + fetch-depth: 0 + + - name: Extract version + id: extract_version + run: | + REF_NAME="${{ github.ref_name }}" + + # Check if running from a tag + if [[ "${{ github.ref }}" == refs/tags/* ]] && [[ "$REF_NAME" == fleet-* ]]; then + VERSION="${REF_NAME#fleet-}" + VERSION="${VERSION#v}" + TAG_NAME="$REF_NAME" + else + VERSION="test-$(date +%Y%m%d-%H%M%S)" + TAG_NAME="fleet-${VERSION}" + fi + + # For MSI, we need a numeric version (X.Y.Z). Use 0.0.0 for test builds. + if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + MSI_VERSION="$VERSION" + else + MSI_VERSION="0.0.0" + fi + + # Determine test mode + if [ "${{ github.event.inputs.test_mode }}" = "false" ]; then + IS_TEST_MODE="false" + else + IS_TEST_MODE="true" + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "msi_version=$MSI_VERSION" >> $GITHUB_OUTPUT + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "is_test_mode=$IS_TEST_MODE" >> $GITHUB_OUTPUT + echo "Fleet version: $VERSION (MSI version: $MSI_VERSION)" + if [ "$IS_TEST_MODE" = "true" ]; then + echo "TEST MODE: MSI will be built but NOT uploaded to release" + else + echo "PRODUCTION MODE: MSI will be uploaded to release $TAG_NAME" + fi + + - name: Set up Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: "go.mod" + + - name: Build fleetctl binary + run: | + VERSION="${{ steps.extract_version.outputs.version }}" + + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + BRANCH_NAME="${{ steps.extract_version.outputs.tag_name }}" + else + BRANCH_NAME="${GITHUB_REF#refs/heads/}" + fi + + LDFLAGS="-X github.com/fleetdm/fleet/v4/server/version.appName=fleetctl \ + -X github.com/fleetdm/fleet/v4/server/version.version=${VERSION} \ + -X github.com/fleetdm/fleet/v4/server/version.branch=${BRANCH_NAME} \ + -X github.com/fleetdm/fleet/v4/server/version.revision=${GITHUB_SHA} \ + -X github.com/fleetdm/fleet/v4/server/version.buildDate=$(date -u +%Y-%m-%d) \ + -X github.com/fleetdm/fleet/v4/server/version.buildUser=github-actions" + + echo "Building fleetctl for windows/${{ matrix.arch }}..." + CGO_ENABLED=0 GOOS=windows GOARCH=${{ matrix.arch }} go build \ + -trimpath \ + -ldflags "$LDFLAGS" \ + -o fleetctl.exe \ + ./cmd/fleetctl + + if [ ! -f fleetctl.exe ]; then + echo "Error: fleetctl.exe not found after build" + exit 1 + fi + + echo "Built fleetctl.exe ($(wc -c < fleetctl.exe) bytes)" + + - name: Install WiX 3.14.1 + run: | + curl -fSL -o wix314-binaries.zip \ + https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip + mkdir -p "$RUNNER_TEMP/wix" + unzip -q wix314-binaries.zip -d "$RUNNER_TEMP/wix" + echo "$RUNNER_TEMP/wix" >> $GITHUB_PATH + + - name: Setup DigiCert KeyLocker + env: + DIGICERT_KEYLOCKER_CERTIFICATE: ${{ secrets.DIGICERT_KEYLOCKER_CERTIFICATE }} + DIGICERT_KEYLOCKER_PASSWORD: ${{ secrets.DIGICERT_KEYLOCKER_PASSWORD }} + DIGICERT_KEYLOCKER_HOST_URL: ${{ secrets.DIGICERT_KEYLOCKER_HOST_URL }} + DIGICERT_API_KEY: ${{ secrets.DIGICERT_API_KEY }} + run: | + # Decode certificate + echo "$DIGICERT_KEYLOCKER_CERTIFICATE" | base64 --decode > /d/Certificate_pkcs12.p12 + + # Set environment variables for subsequent steps + echo "SM_HOST=$DIGICERT_KEYLOCKER_HOST_URL" >> "$GITHUB_ENV" + echo "SM_API_KEY=$DIGICERT_API_KEY" >> "$GITHUB_ENV" + echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV" + echo "SM_CLIENT_CERT_PASSWORD=$DIGICERT_KEYLOCKER_PASSWORD" >> "$GITHUB_ENV" + + # Add signing tools to PATH + echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH + echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH + echo "C:\Program Files\DigiCert\DigiCert Keylocker Tools" >> $GITHUB_PATH + + - name: Install DigiCert KeyLocker KSP + run: | + curl https://one.digicert.com/signingmanager/api-ui/v1/releases/Keylockertools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" --fail-with-body -o Keylockertools-windows-x64.msi + msiexec /i Keylockertools-windows-x64.msi /quiet /qn + smksp_registrar.exe list + smctl.exe keypair ls + C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user + shell: cmd + + - name: Sync certificates + run: | + smctl windows certsync + shell: cmd + + - name: Build and sign MSI + env: + DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT: ${{ secrets.DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT }} + SKIP_UPLOAD: ${{ inputs.test_mode && 'true' || 'false' }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_REF: ${{ github.ref }} + run: | + chmod +x tools/build-fleetctl-msi/main.sh + ./tools/build-fleetctl-msi/main.sh \ + fleetctl.exe \ + "${{ steps.extract_version.outputs.msi_version }}" \ + "${{ matrix.arch }}" + + - name: Upload MSI artifact (test mode) + if: ${{ inputs.test_mode }} + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + with: + name: fleetctl-msi-${{ matrix.arch }} + path: dist/fleetctl_v${{ steps.extract_version.outputs.msi_version }}_windows_${{ matrix.arch }}.msi + retention-days: 7 + + - name: Attest MSI + continue-on-error: true + uses: actions/attest-build-provenance@619dbb2e03e0189af0c55118e7d3c5e129e99726 # v2.0 + with: + subject-path: dist/fleetctl_v${{ steps.extract_version.outputs.msi_version }}_windows_${{ matrix.arch }}.msi + push-to-registry: false diff --git a/.github/workflows/goreleaser-fleet.yaml b/.github/workflows/goreleaser-fleet.yaml index 723a22ed3a..00c2e36c12 100644 --- a/.github/workflows/goreleaser-fleet.yaml +++ b/.github/workflows/goreleaser-fleet.yaml @@ -22,6 +22,8 @@ jobs: goreleaser: runs-on: ubuntu-22.04-4-cores environment: Docker Hub + outputs: + windows_msi_arches: ${{ steps.check_windows_binaries.outputs.arches }} permissions: contents: write id-token: write @@ -156,3 +158,187 @@ jobs: docker tag fleetdm/fleet:${TAG} quay.io/fleetdm/fleet:${TAG} docker push quay.io/fleetdm/fleet:${TAG} done + + - name: Check if Windows fleetctl binaries exist + id: check_windows_binaries + continue-on-error: true + run: | + # Goreleaser v2 places binaries at dist/__*/binary + ARCHES=() + for arch in amd64 arm64; do + EXE=$(find dist -name "fleetctl.exe" -path "*windows*${arch}*" -print -quit 2>/dev/null) + if [[ -n "$EXE" ]]; then + echo "Found: $EXE" + echo "windows_${arch}_path=$EXE" >> "$GITHUB_OUTPUT" + ARCHES+=("\"$arch\"") + else + echo "Warning: No fleetctl.exe found for windows/$arch" + fi + done + echo "arches=[$(IFS=,; echo "${ARCHES[*]}")]" >> "$GITHUB_OUTPUT" + + - name: Upload unsigned Windows fleetctl amd64 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: steps.check_windows_binaries.outputs.windows_amd64_path != '' + with: + name: fleetctl-unsigned-windows-amd64 + path: ${{ steps.check_windows_binaries.outputs.windows_amd64_path }} + retention-days: 1 + + - name: Upload unsigned Windows fleetctl arm64 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: steps.check_windows_binaries.outputs.windows_arm64_path != '' + with: + name: fleetctl-unsigned-windows-arm64 + path: ${{ steps.check_windows_binaries.outputs.windows_arm64_path }} + retention-days: 1 + + build-fleetctl-msi: + runs-on: windows-2022 + needs: goreleaser + if: needs.goreleaser.outputs.windows_msi_arches != '[]' + timeout-minutes: 60 + strategy: + matrix: + arch: ${{ fromJSON(needs.goreleaser.outputs.windows_msi_arches) }} + permissions: + contents: write + id-token: write + attestations: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: Download unsigned fleetctl binary + uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.8 + with: + name: fleetctl-unsigned-windows-${{ matrix.arch }} + + - name: Verify binary exists + run: | + if [[ ! -f "fleetctl.exe" ]]; then + echo "Error: fleetctl.exe not found after download" + ls -la + exit 1 + fi + echo "Binary found ($(wc -c < fleetctl.exe) bytes)" + + - name: Extract version from tag + id: version + run: | + TAG_NAME="${{ github.ref_name }}" + VERSION="${TAG_NAME#fleet-}" + VERSION="${VERSION#v}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Version: $VERSION" + + - name: Install WiX 3.14.1 + run: | + curl -fSL -o wix314-binaries.zip \ + https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip + mkdir -p "$RUNNER_TEMP/wix" + unzip -q wix314-binaries.zip -d "$RUNNER_TEMP/wix" + echo "$RUNNER_TEMP/wix" >> $GITHUB_PATH + + - name: Setup DigiCert KeyLocker + env: + DIGICERT_KEYLOCKER_CERTIFICATE: ${{ secrets.DIGICERT_KEYLOCKER_CERTIFICATE }} + DIGICERT_KEYLOCKER_PASSWORD: ${{ secrets.DIGICERT_KEYLOCKER_PASSWORD }} + DIGICERT_KEYLOCKER_HOST_URL: ${{ secrets.DIGICERT_KEYLOCKER_HOST_URL }} + DIGICERT_API_KEY: ${{ secrets.DIGICERT_API_KEY }} + run: | + echo "$DIGICERT_KEYLOCKER_CERTIFICATE" | base64 --decode > /d/Certificate_pkcs12.p12 + echo "SM_HOST=$DIGICERT_KEYLOCKER_HOST_URL" >> "$GITHUB_ENV" + echo "SM_API_KEY=$DIGICERT_API_KEY" >> "$GITHUB_ENV" + echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV" + echo "SM_CLIENT_CERT_PASSWORD=$DIGICERT_KEYLOCKER_PASSWORD" >> "$GITHUB_ENV" + echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH + echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH + echo "C:\Program Files\DigiCert\DigiCert Keylocker Tools" >> $GITHUB_PATH + + - name: Install DigiCert KeyLocker KSP + run: | + curl https://one.digicert.com/signingmanager/api-ui/v1/releases/Keylockertools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" --fail-with-body -o Keylockertools-windows-x64.msi + msiexec /i Keylockertools-windows-x64.msi /quiet /qn + smksp_registrar.exe list + smctl.exe keypair ls + C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user + shell: cmd + + - name: Sync certificates + run: | + smctl windows certsync + shell: cmd + + - name: Build and sign MSI + env: + DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT: ${{ secrets.DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT }} + SKIP_UPLOAD: "true" + run: | + chmod +x tools/build-fleetctl-msi/main.sh + ./tools/build-fleetctl-msi/main.sh \ + fleetctl.exe \ + "${{ steps.version.outputs.version }}" \ + "${{ matrix.arch }}" + + - name: Upload signed MSI artifact + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + with: + name: fleetctl-msi-${{ matrix.arch }} + path: dist/fleetctl_v${{ steps.version.outputs.version }}_windows_${{ matrix.arch }}.msi + retention-days: 1 + + - name: Attest MSI + continue-on-error: true + uses: actions/attest-build-provenance@619dbb2e03e0189af0c55118e7d3c5e129e99726 # v2.0 + with: + subject-path: dist/fleetctl_v${{ steps.version.outputs.version }}_windows_${{ matrix.arch }}.msi + + upload-fleetctl-msi: + needs: build-fleetctl-msi + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + with: + egress-policy: audit + + - name: Download signed MSI artifacts + uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.8 + with: + pattern: fleetctl-msi-* + merge-multiple: true + path: dist + + - name: Upload MSIs to release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG_NAME="${{ github.ref_name }}" + echo "Uploading MSIs to release $TAG_NAME..." + + # Wait for release to exist + MAX_WAIT=300 + ELAPSED=0 + while [ $ELAPSED -lt $MAX_WAIT ]; do + if gh release view "$TAG_NAME" --repo "$GITHUB_REPOSITORY" &>/dev/null; then + echo "Release found" + break + fi + echo "Waiting for release... (${ELAPSED}s/${MAX_WAIT}s)" + sleep 10 + ELAPSED=$((ELAPSED + 10)) + done + + for msi in dist/*.msi; do + echo "Uploading $msi..." + gh release upload "$TAG_NAME" "$msi" --repo "$GITHUB_REPOSITORY" --clobber + done + echo "Upload complete" diff --git a/changes/42972-add-fleetctl-msi-to-release b/changes/42972-add-fleetctl-msi-to-release new file mode 100644 index 0000000000..69bd66037f --- /dev/null +++ b/changes/42972-add-fleetctl-msi-to-release @@ -0,0 +1 @@ +- `fleetctl` is now also released as an `msi` for Windows diff --git a/tools/build-fleetctl-msi/fleetctl.wxs b/tools/build-fleetctl-msi/fleetctl.wxs new file mode 100644 index 0000000000..6a7b333f23 --- /dev/null +++ b/tools/build-fleetctl-msi/fleetctl.wxs @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/build-fleetctl-msi/main.sh b/tools/build-fleetctl-msi/main.sh new file mode 100755 index 0000000000..4dceb13e47 --- /dev/null +++ b/tools/build-fleetctl-msi/main.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +set -eo pipefail + +# This script builds a signed Windows .msi installer for fleetctl. +# It expects to run on a Windows GitHub Actions runner with Git Bash, +# with DigiCert KeyLocker already configured (env vars set, KSP installed, +# certs synced) and WiX 3.14 tools on PATH. +# +# Usage: ./main.sh +# version: semver like "4.72.0" or "v4.72.0" (v prefix stripped) +# arch: "amd64" or "arm64" +# +# Environment variables: +# DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT - SHA1 fingerprint for signtool +# SKIP_UPLOAD - set to "true" to skip GitHub release upload (default: skip) +# GITHUB_TOKEN, GITHUB_REPOSITORY, GITHUB_REF - required if uploading to release + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +check_env_var() { + if [[ -z "${!1}" ]]; then + echo "Error: Environment variable $1 not set." + exit 1 + fi +} + +# Parse arguments +FLEETCTL_EXE="$1" +VERSION="$2" +ARCH="${3:-amd64}" + +if [[ -z "$FLEETCTL_EXE" ]] || [[ -z "$VERSION" ]]; then + echo "Usage: $0 [arch]" + echo " version: semver like 4.72.0 or v4.72.0" + echo " arch: amd64 (default) or arm64" + exit 1 +fi + +if [[ ! -f "$FLEETCTL_EXE" ]]; then + echo "Error: fleetctl.exe not found at $FLEETCTL_EXE" + exit 1 +fi + +# Check required env var for signing +check_env_var "DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT" + +# Strip v prefix from version +VERSION="${VERSION#v}" + +# Validate version format (WiX requires X.Y.Z or X.Y.Z.W, each component 0-65535) +if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then + echo "Error: Version '$VERSION' is not a valid MSI version (expected X.Y.Z or X.Y.Z.W)" + exit 1 +fi + +# Map Go arch to WiX candle arch +case "$ARCH" in + amd64) WIX_ARCH="x64" ;; + arm64) WIX_ARCH="arm64" ;; + *) + echo "Error: Unsupported architecture '$ARCH' (expected amd64 or arm64)" + exit 1 + ;; +esac + +MSI_NAME="fleetctl_v${VERSION}_windows_${ARCH}.msi" +echo "Building $MSI_NAME..." + +# Create working directory +WORKDIR="$(mktemp -d)" +# Convert to Windows path for native Windows tools (signtool, candle, light) +WORKDIR_WIN="$(cygpath -w "$WORKDIR")" +cleanup() { + echo "Cleaning up..." + rm -rf "$WORKDIR" +} +trap cleanup EXIT + +# Set up working directory structure +mkdir -p "$WORKDIR/root" +cp "$FLEETCTL_EXE" "$WORKDIR/root/fleetctl.exe" + +# Substitute version placeholder in WiX template +sed "s/__VERSION__/$VERSION/g" "$SCRIPT_DIR/fleetctl.wxs" > "$WORKDIR/main.wxs" + +# Sign fleetctl.exe before packaging into MSI +# MSYS_NO_PATHCONV=1 prevents Git Bash from converting /flags to Windows paths +# Windows-native tools need Windows-style paths (WORKDIR_WIN) +echo "Signing fleetctl.exe..." +MSYS_NO_PATHCONV=1 signtool.exe sign /v /sha1 "$DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT" \ + /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 \ + /d "fleetctl by Fleet Device Management" /du "https://fleetdm.com" \ + "$WORKDIR_WIN\\root\\fleetctl.exe" +MSYS_NO_PATHCONV=1 signtool.exe verify /v /pa "$WORKDIR_WIN\\root\\fleetctl.exe" +echo "fleetctl.exe signed successfully" + +# Compile WiX source +echo "Running candle (WiX compiler)..." +candle.exe "$WORKDIR_WIN\\main.wxs" -arch "$WIX_ARCH" -out "$WORKDIR_WIN\\main.wixobj" + +# Link to create MSI +echo "Running light (WiX linker)..." +light.exe "$WORKDIR_WIN\\main.wixobj" -b "$WORKDIR_WIN" -out "$WORKDIR_WIN\\$MSI_NAME" -sval + +# Sign the MSI +echo "Signing MSI..." +MSYS_NO_PATHCONV=1 signtool.exe sign /v /sha1 "$DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT" \ + /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 \ + /d "fleetctl by Fleet Device Management" /du "https://fleetdm.com" \ + "$WORKDIR_WIN\\$MSI_NAME" +MSYS_NO_PATHCONV=1 signtool.exe verify /v /pa "$WORKDIR_WIN\\$MSI_NAME" +echo "MSI signed successfully" + +# Copy to output location +mkdir -p dist +cp "$WORKDIR/$MSI_NAME" "dist/$MSI_NAME" +echo "Package created: dist/$MSI_NAME" + +# Upload to release unless skipped +if [[ "$SKIP_UPLOAD" == "true" ]]; then + echo "Skipping release upload (SKIP_UPLOAD=true)" +elif [[ -n "$GITHUB_TOKEN" ]] && [[ -n "$GITHUB_REPOSITORY" ]] && [[ -n "$GITHUB_REF" ]] && command -v gh &> /dev/null; then + if [[ "$GITHUB_REF" != refs/tags/* ]]; then + echo "Error: GITHUB_REF is not a tag ref ($GITHUB_REF), cannot upload to release" + exit 1 + fi + TAG_NAME="${GITHUB_REF#refs/tags/}" + echo "Uploading package to release $TAG_NAME..." + + # Wait for release to exist (goreleaser creates it as draft) + MAX_WAIT=300 + ELAPSED=0 + INTERVAL=10 + + while [ $ELAPSED -lt $MAX_WAIT ]; do + if gh release view "$TAG_NAME" --repo "$GITHUB_REPOSITORY" &>/dev/null; then + echo "Release found, uploading package..." + break + fi + echo "Waiting for release to be created... (${ELAPSED}s/${MAX_WAIT}s)" + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) + done + + if [ $ELAPSED -ge $MAX_WAIT ]; then + echo "Warning: Release not found after waiting ${MAX_WAIT}s" + echo "Attempting upload anyway..." + fi + + echo "Uploading dist/$MSI_NAME to release $TAG_NAME" + gh release upload "$TAG_NAME" "dist/$MSI_NAME" --repo "$GITHUB_REPOSITORY" --clobber || { + echo "Upload failed, retrying once..." + sleep 5 + gh release upload "$TAG_NAME" "dist/$MSI_NAME" --repo "$GITHUB_REPOSITORY" --clobber || { + echo "Failed to upload package after retry" + exit 1 + } + } + + echo "Package uploaded successfully: dist/$MSI_NAME" +else + echo "Skipping release upload (GITHUB_TOKEN or gh not available)" +fi