Merge remote-tracking branch 'origin/main' into feat/admin-cli #50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Device Agent Release | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: ['**'] | |
| paths: | |
| - 'packages/device-agent/**' | |
| permissions: | |
| contents: write | |
| jobs: | |
| detect-version: | |
| name: Detect Version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| tag_name: ${{ steps.version.outputs.tag_name }} | |
| is_prerelease: ${{ steps.version.outputs.is_prerelease }} | |
| portal_url: ${{ steps.version.outputs.portal_url }} | |
| api_url: ${{ steps.version.outputs.api_url }} | |
| release_name: ${{ steps.version.outputs.release_name }} | |
| auto_update_url: ${{ steps.version.outputs.auto_update_url }} | |
| s3_env: ${{ steps.version.outputs.s3_env }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Compute next version | |
| id: version | |
| run: | | |
| # Get the latest production tag (only match clean semver tags like device-agent-v1.0.0) | |
| LATEST_TAG=$(git tag -l 'device-agent-v*' --sort=-v:refname | grep -E '^device-agent-v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) | |
| if [ -z "$LATEST_TAG" ]; then | |
| # No existing tags - start at 1.0.0 | |
| NEXT_VERSION="1.0.0" | |
| else | |
| # Extract version and bump patch | |
| CURRENT_VERSION="${LATEST_TAG#device-agent-v}" | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" | |
| NEXT_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" | |
| fi | |
| BRANCH="${GITHUB_REF_NAME}" | |
| if [ "$BRANCH" = "release" ]; then | |
| TAG_NAME="device-agent-v${NEXT_VERSION}" | |
| IS_PRERELEASE="false" | |
| PORTAL_URL="https://portal.trycomp.ai" | |
| API_URL="https://api.trycomp.ai" | |
| RELEASE_NAME="Device Agent v${NEXT_VERSION}" | |
| S3_ENV="production" | |
| else | |
| TAG_NAME="device-agent-v${NEXT_VERSION}-staging.${GITHUB_RUN_NUMBER}" | |
| IS_PRERELEASE="true" | |
| PORTAL_URL="https://portal.staging.trycomp.ai" | |
| API_URL="https://api.staging.trycomp.ai" | |
| RELEASE_NAME="Device Agent v${NEXT_VERSION} (Staging #${GITHUB_RUN_NUMBER})" | |
| S3_ENV="staging" | |
| fi | |
| # Auto-update URL: proxied through the portal (no direct S3 access needed) | |
| AUTO_UPDATE_URL="${PORTAL_URL}/api/device-agent/updates" | |
| echo "version=$NEXT_VERSION" >> $GITHUB_OUTPUT | |
| echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT | |
| echo "portal_url=$PORTAL_URL" >> $GITHUB_OUTPUT | |
| echo "api_url=$API_URL" >> $GITHUB_OUTPUT | |
| echo "release_name=$RELEASE_NAME" >> $GITHUB_OUTPUT | |
| echo "auto_update_url=$AUTO_UPDATE_URL" >> $GITHUB_OUTPUT | |
| echo "s3_env=$S3_ENV" >> $GITHUB_OUTPUT | |
| echo "--- Version Info ---" | |
| echo "Latest tag: $LATEST_TAG" | |
| echo "Next version: $NEXT_VERSION" | |
| echo "Tag name: $TAG_NAME" | |
| echo "Pre-release: $IS_PRERELEASE" | |
| echo "Portal URL: $PORTAL_URL" | |
| echo "API URL: $API_URL" | |
| echo "Auto-update URL: $AUTO_UPDATE_URL" | |
| echo "S3 env: $S3_ENV" | |
| build-macos: | |
| name: Build macOS (.dmg + .zip) | |
| needs: detect-version | |
| runs-on: macos-latest | |
| defaults: | |
| run: | |
| working-directory: packages/device-agent | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Set package.json version | |
| env: | |
| VERSION: ${{ needs.detect-version.outputs.version }} | |
| run: | | |
| node -e " | |
| const pkg = require('./package.json'); | |
| pkg.version = process.env.VERSION; | |
| require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
| " | |
| - name: Build | |
| env: | |
| PORTAL_URL: ${{ needs.detect-version.outputs.portal_url }} | |
| API_URL: ${{ needs.detect-version.outputs.api_url }} | |
| AGENT_VERSION: ${{ needs.detect-version.outputs.version }} | |
| run: bun run build | |
| - name: Package macOS | |
| env: | |
| CSC_LINK: ${{ secrets.MAC_CSC_LINK }} | |
| CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| AUTO_UPDATE_URL: ${{ needs.detect-version.outputs.auto_update_url }} | |
| run: bun run package:mac | |
| - name: Upload macOS artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: device-agent-macos | |
| path: | | |
| packages/device-agent/release/*.dmg | |
| packages/device-agent/release/*.zip | |
| packages/device-agent/release/*.blockmap | |
| packages/device-agent/release/*.yml | |
| if-no-files-found: error | |
| build-windows: | |
| name: Build Windows (.exe) | |
| needs: detect-version | |
| runs-on: windows-latest | |
| defaults: | |
| run: | |
| working-directory: packages/device-agent | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Set package.json version | |
| env: | |
| VERSION: ${{ needs.detect-version.outputs.version }} | |
| shell: bash | |
| run: | | |
| node -e " | |
| const pkg = require('./package.json'); | |
| pkg.version = process.env.VERSION; | |
| require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
| " | |
| - name: Build | |
| env: | |
| PORTAL_URL: ${{ needs.detect-version.outputs.portal_url }} | |
| API_URL: ${{ needs.detect-version.outputs.api_url }} | |
| AGENT_VERSION: ${{ needs.detect-version.outputs.version }} | |
| run: bun run build | |
| - name: Package Windows (unsigned) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| AUTO_UPDATE_URL: ${{ needs.detect-version.outputs.auto_update_url }} | |
| run: bun run package:win | |
| - name: Setup Java for CodeSignTool | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'corretto' | |
| java-version: '11' | |
| - name: Sign Windows EXE with SSL.com CodeSignTool | |
| shell: powershell | |
| working-directory: packages/device-agent/release | |
| env: | |
| ESIGNER_USERNAME: ${{ secrets.ESIGNER_USERNAME }} | |
| ESIGNER_PASSWORD: ${{ secrets.ESIGNER_PASSWORD }} | |
| ESIGNER_CREDENTIAL_ID: ${{ secrets.ESIGNER_CREDENTIAL_ID }} | |
| ESIGNER_TOTP_SECRET: ${{ secrets.ESIGNER_TOTP_SECRET }} | |
| run: | | |
| if (-not $env:ESIGNER_USERNAME -or -not $env:ESIGNER_PASSWORD -or -not $env:ESIGNER_CREDENTIAL_ID -or -not $env:ESIGNER_TOTP_SECRET) { | |
| throw "One or more ESIGNER secrets are not configured. Cannot sign." | |
| } | |
| Invoke-WebRequest -Uri "https://github.com/SSLcom/CodeSignTool/releases/download/v1.3.0/CodeSignTool-v1.3.0-windows.zip" -OutFile "codesigntool.zip" | |
| Expand-Archive -Path "codesigntool.zip" -DestinationPath "codesigntool" | |
| $jar = Get-ChildItem -Path "codesigntool" -Recurse -Filter "code_sign_tool-*.jar" | Select-Object -First 1 | |
| if (-not $jar) { throw "CodeSignTool jar not found" } | |
| Write-Host "Found CodeSignTool jar at: $($jar.FullName)" | |
| $cstDir = $jar.Directory.Parent | |
| $releaseDir = Get-Location | |
| Get-ChildItem -Path $releaseDir -Filter "*.exe" | ForEach-Object { | |
| Write-Host "Signing $($_.Name)..." | |
| Push-Location $cstDir.FullName | |
| & java -Xmx1024M -jar "$($jar.FullName)" sign ` | |
| -username="$env:ESIGNER_USERNAME" ` | |
| -password="$env:ESIGNER_PASSWORD" ` | |
| -credential_id="$env:ESIGNER_CREDENTIAL_ID" ` | |
| -totp_secret="$env:ESIGNER_TOTP_SECRET" ` | |
| -input_file_path="$($_.FullName)" ` | |
| -override="true" | |
| $signExitCode = $LASTEXITCODE | |
| Pop-Location | |
| if ($signExitCode -ne 0) { throw "Code signing failed for $($_.Name) (exit code: $signExitCode)" } | |
| Write-Host "CodeSignTool completed for $($_.Name)" | |
| } | |
| - name: Verify Windows code signature | |
| shell: powershell | |
| working-directory: packages/device-agent/release | |
| run: | | |
| $allSigned = $true | |
| Get-ChildItem -Filter "*.exe" | ForEach-Object { | |
| $sig = Get-AuthenticodeSignature -FilePath $_.FullName | |
| Write-Host "File: $($_.Name)" | |
| Write-Host " Status: $($sig.Status)" | |
| Write-Host " Signer: $($sig.SignerCertificate.Subject)" | |
| Write-Host " Issuer: $($sig.SignerCertificate.Issuer)" | |
| Write-Host " Valid from: $($sig.SignerCertificate.NotBefore) to $($sig.SignerCertificate.NotAfter)" | |
| if ($sig.Status -ne 'Valid') { | |
| Write-Host "::error::Signature verification FAILED for $($_.Name) - Status: $($sig.Status)" | |
| $allSigned = $false | |
| } | |
| } | |
| if (-not $allSigned) { throw "One or more .exe files are NOT properly signed. Failing build." } | |
| - name: Recalculate latest.yml hash after signing | |
| shell: bash | |
| working-directory: packages/device-agent/release | |
| run: | | |
| # Code signing changed the exe, so the sha512 in latest.yml is now wrong. | |
| # Recalculate it from the signed binary. | |
| EXE_FILE=$(ls *.exe | head -1) | |
| if [ -z "$EXE_FILE" ]; then | |
| echo "ERROR: No .exe found in release/" | |
| exit 1 | |
| fi | |
| # Compute base64-encoded sha512 hash (electron-updater format) | |
| NEW_SHA512=$(openssl dgst -sha512 -binary "$EXE_FILE" | openssl base64 -A) | |
| NEW_SIZE=$(wc -c < "$EXE_FILE" | tr -d ' ') | |
| echo "Signed exe: $EXE_FILE" | |
| echo "New sha512: $NEW_SHA512" | |
| echo "New size: $NEW_SIZE" | |
| if [ -f "latest.yml" ]; then | |
| # Update the sha512 and size in latest.yml | |
| # Use /blockMapSize/! to skip blockMapSize lines (they also contain "size:") | |
| sed -i.bak "s|sha512: .*|sha512: ${NEW_SHA512}|" latest.yml | |
| sed -i.bak "/blockMapSize/!s|size: .*|size: ${NEW_SIZE}|" latest.yml | |
| rm -f latest.yml.bak | |
| echo "--- Updated latest.yml ---" | |
| cat latest.yml | |
| else | |
| echo "WARNING: latest.yml not found, skipping hash update" | |
| fi | |
| - name: Upload Windows artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: device-agent-windows | |
| path: | | |
| packages/device-agent/release/*.exe | |
| packages/device-agent/release/*.blockmap | |
| packages/device-agent/release/*.yml | |
| if-no-files-found: error | |
| build-linux: | |
| name: Build Linux (.AppImage, .deb) | |
| needs: detect-version | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: packages/device-agent | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Set package.json version | |
| env: | |
| VERSION: ${{ needs.detect-version.outputs.version }} | |
| run: | | |
| node -e " | |
| const pkg = require('./package.json'); | |
| pkg.version = process.env.VERSION; | |
| require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
| " | |
| - name: Build | |
| env: | |
| PORTAL_URL: ${{ needs.detect-version.outputs.portal_url }} | |
| API_URL: ${{ needs.detect-version.outputs.api_url }} | |
| AGENT_VERSION: ${{ needs.detect-version.outputs.version }} | |
| run: bun run build | |
| - name: Package Linux | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| AUTO_UPDATE_URL: ${{ needs.detect-version.outputs.auto_update_url }} | |
| run: bun run package:linux | |
| - name: Upload Linux artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: device-agent-linux | |
| path: | | |
| packages/device-agent/release/*.AppImage | |
| packages/device-agent/release/*.deb | |
| packages/device-agent/release/*.blockmap | |
| packages/device-agent/release/*.yml | |
| if-no-files-found: error | |
| release: | |
| name: Create GitHub Release | |
| needs: [detect-version, build-macos, build-windows, build-linux] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Download macOS artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: device-agent-macos | |
| path: artifacts/ | |
| - name: Download Windows artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: device-agent-windows | |
| path: artifacts/ | |
| - name: Download Linux artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: device-agent-linux | |
| path: artifacts/ | |
| - name: Create git tag | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git tag "${{ needs.detect-version.outputs.tag_name }}" -m "${{ needs.detect-version.outputs.release_name }}" | |
| git push origin "${{ needs.detect-version.outputs.tag_name }}" | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.detect-version.outputs.tag_name }} | |
| name: ${{ needs.detect-version.outputs.release_name }} | |
| body: | | |
| ## ${{ needs.detect-version.outputs.release_name }} | |
| **Environment:** ${{ needs.detect-version.outputs.is_prerelease == 'true' && 'Staging' || 'Production' }} | |
| **Portal:** ${{ needs.detect-version.outputs.portal_url }} | |
| ### Downloads | |
| - **macOS**: Download the `.dmg` file below (universal binary, Apple Silicon + Intel) | |
| - **Windows**: Download the `.exe` installer below | |
| - **Linux**: Download the `.AppImage` (portable) or `.deb` (Debian/Ubuntu) below | |
| ### What's included | |
| - Disk encryption check (FileVault / BitLocker / LUKS) | |
| - Antivirus detection (XProtect / Windows Defender / ClamAV + AppArmor/SELinux) | |
| - Password policy enforcement (minimum 8 characters) | |
| - Screen lock verification (5 minutes or less) | |
| - Auto-remediation for fixable settings with guided instructions | |
| ### Installation | |
| 1. Download the installer for your operating system | |
| 2. Run the installer and follow the prompts | |
| 3. Sign in with your Comp AI portal credentials | |
| 4. The agent will run in your system tray and check compliance automatically | |
| draft: false | |
| prerelease: ${{ needs.detect-version.outputs.is_prerelease == 'true' }} | |
| files: artifacts/* | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| upload-s3: | |
| name: Upload to S3 | |
| needs: [detect-version, build-macos, build-windows, build-linux] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts/ | |
| merge-multiple: true | |
| - name: List artifacts | |
| run: ls -la artifacts/ | |
| - name: Upload installers to S3 | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.APP_AWS_ACCESS_KEY_ID || secrets.APP_AWS_ACCESS_KEY_ID_STAGING }} | |
| AWS_SECRET_ACCESS_KEY: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.APP_AWS_SECRET_ACCESS_KEY || secrets.APP_AWS_SECRET_ACCESS_KEY_STAGING }} | |
| AWS_REGION: ${{ secrets.APP_AWS_REGION }} | |
| S3_BUCKET: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.FLEET_AGENT_BUCKET_NAME || secrets.FLEET_AGENT_BUCKET_NAME_STAGING }} | |
| VERSION: ${{ needs.detect-version.outputs.version }} | |
| S3_ENV: ${{ needs.detect-version.outputs.s3_env }} | |
| run: | | |
| PREFIX="device-agent/${S3_ENV}" | |
| # macOS (dmg for portal downloads) | |
| aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-arm64.dmg \ | |
| s3://${S3_BUCKET}/${PREFIX}/macos/CompAI-Device-Agent-${VERSION}-arm64.dmg | |
| aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-arm64.dmg \ | |
| s3://${S3_BUCKET}/${PREFIX}/macos/latest-arm64.dmg | |
| aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.dmg \ | |
| s3://${S3_BUCKET}/${PREFIX}/macos/CompAI-Device-Agent-${VERSION}-x64.dmg | |
| aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.dmg \ | |
| s3://${S3_BUCKET}/${PREFIX}/macos/latest-x64.dmg | |
| # Windows | |
| aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-setup.exe \ | |
| s3://${S3_BUCKET}/${PREFIX}/windows/CompAI-Device-Agent-${VERSION}-setup.exe | |
| aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-setup.exe \ | |
| s3://${S3_BUCKET}/${PREFIX}/windows/latest-setup.exe | |
| # Linux (.deb uses amd64, .AppImage uses x86_64 architecture naming) | |
| aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-amd64.deb \ | |
| s3://${S3_BUCKET}/${PREFIX}/linux/CompAI-Device-Agent-${VERSION}-amd64.deb | |
| aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-amd64.deb \ | |
| s3://${S3_BUCKET}/${PREFIX}/linux/latest-amd64.deb | |
| aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x86_64.AppImage \ | |
| s3://${S3_BUCKET}/${PREFIX}/linux/CompAI-Device-Agent-${VERSION}-x86_64.AppImage | |
| aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x86_64.AppImage \ | |
| s3://${S3_BUCKET}/${PREFIX}/linux/latest-x86_64.AppImage | |
| - name: Upload auto-update files to S3 | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.APP_AWS_ACCESS_KEY_ID || secrets.APP_AWS_ACCESS_KEY_ID_STAGING }} | |
| AWS_SECRET_ACCESS_KEY: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.APP_AWS_SECRET_ACCESS_KEY || secrets.APP_AWS_SECRET_ACCESS_KEY_STAGING }} | |
| AWS_REGION: ${{ secrets.APP_AWS_REGION }} | |
| S3_BUCKET: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.FLEET_AGENT_BUCKET_NAME || secrets.FLEET_AGENT_BUCKET_NAME_STAGING }} | |
| S3_ENV: ${{ needs.detect-version.outputs.s3_env }} | |
| run: | | |
| UPDATE_DIR="device-agent/${S3_ENV}/updates" | |
| # Upload all .yml files (latest-mac.yml, latest.yml, latest-linux.yml) | |
| for f in artifacts/*.yml; do | |
| [ -f "$f" ] && aws s3 cp "$f" "s3://${S3_BUCKET}/${UPDATE_DIR}/$(basename "$f")" | |
| done | |
| # Upload all .zip files (macOS auto-update archives) | |
| for f in artifacts/*.zip; do | |
| [ -f "$f" ] && aws s3 cp "$f" "s3://${S3_BUCKET}/${UPDATE_DIR}/$(basename "$f")" | |
| done | |
| # Upload all .blockmap files (delta update metadata) | |
| for f in artifacts/*.blockmap; do | |
| [ -f "$f" ] && aws s3 cp "$f" "s3://${S3_BUCKET}/${UPDATE_DIR}/$(basename "$f")" | |
| done | |
| # Upload .exe for Windows auto-update | |
| for f in artifacts/*.exe; do | |
| [ -f "$f" ] && aws s3 cp "$f" "s3://${S3_BUCKET}/${UPDATE_DIR}/$(basename "$f")" | |
| done | |
| # Upload .AppImage for Linux auto-update | |
| for f in artifacts/*.AppImage; do | |
| [ -f "$f" ] && aws s3 cp "$f" "s3://${S3_BUCKET}/${UPDATE_DIR}/$(basename "$f")" | |
| done | |
| echo "--- Auto-update files uploaded to s3://${S3_BUCKET}/${UPDATE_DIR}/ ---" | |
| aws s3 ls "s3://${S3_BUCKET}/${UPDATE_DIR}/" |