Release #405
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: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| bump: | |
| description: 'Version bump type' | |
| required: true | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| default: 'patch' | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| SENTINEL_SERVER_URL: ${{ secrets.SENTINEL_SERVER_URL }} | |
| SENTINEL_FIREBASE_PROJECT_ID: ${{ secrets.SENTINEL_FIREBASE_PROJECT_ID }} | |
| SENTINEL_FIREBASE_STORAGE_BUCKET: ${{ secrets.SENTINEL_FIREBASE_STORAGE_BUCKET }} | |
| SENTINEL_RELEASES_BASE_URL: ${{ secrets.SENTINEL_RELEASES_BASE_URL }} | |
| jobs: | |
| # ═══════════════════════════════════════════════════════════════════ | |
| # Auto-increment version, commit, tag, and push | |
| # ═══════════════════════════════════════════════════════════════════ | |
| version: | |
| name: Auto-increment version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.bump.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Compute next version | |
| id: bump | |
| run: | | |
| # Read current version from Cargo.toml (more robust regex for indentation/spaces) | |
| CURRENT=$(grep -E '^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]+"' Cargo.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/') | |
| echo "Current version: $CURRENT" | |
| if [ -z "$CURRENT" ]; then | |
| echo "::error::Could not parse version from Cargo.toml" | |
| exit 1 | |
| fi | |
| # Parse version components | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" | |
| BUMP="${{ github.event.inputs.bump }}" | |
| # Apply bump | |
| case "$BUMP" in | |
| major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; | |
| minor) MINOR=$((MINOR + 1)); PATCH=0 ;; | |
| patch) PATCH=$((PATCH + 1)) ;; | |
| esac | |
| NEXT="${MAJOR}.${MINOR}.${PATCH}" | |
| echo "Next version: $NEXT (bump=$BUMP)" | |
| echo "version=$NEXT" >> "$GITHUB_OUTPUT" | |
| shell: bash | |
| - name: Update Cargo.toml with new version | |
| run: | | |
| VERSION="${{ steps.bump.outputs.version }}" | |
| # Only replace the first occurrence (workspace.package version) | |
| sed -i "0,/^[[:space:]]*version[[:space:]]*=[[:space:]]*\".*\"/{s/^[[:space:]]*version[[:space:]]*=[[:space:]]*\".*\"/version = \"$VERSION\"/}" Cargo.toml | |
| echo "Updated Cargo.toml (first 15 lines):" | |
| head -15 Cargo.toml | |
| - name: Commit, tag, and push | |
| run: | | |
| VERSION="${{ steps.bump.outputs.version }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add Cargo.toml | |
| if git diff --staged --quiet; then | |
| echo "No changes to Cargo.toml, skipping commit" | |
| else | |
| git commit -m "release: v${VERSION}" | |
| git push origin HEAD || (echo "Push failed, checking for remote changes..."; git pull --rebase; git push origin HEAD) | |
| fi | |
| # Handle tag | |
| TAG="v${VERSION}" | |
| if git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then | |
| echo "Tag $TAG already exists locally" | |
| else | |
| git tag "$TAG" | |
| echo "Created tag $TAG" | |
| fi | |
| if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "$TAG"; then | |
| echo "Tag $TAG already exists on remote" | |
| else | |
| git push origin "$TAG" | |
| fi | |
| # ═══════════════════════════════════════════════════════════════════ | |
| # macOS — Universal binary + DMG + PKG (signed + notarized) | |
| # ═══════════════════════════════════════════════════════════════════ | |
| build-macos: | |
| name: Build macOS (DMG + PKG) | |
| runs-on: macos-latest | |
| needs: [version] | |
| env: | |
| RELEASE_VERSION: ${{ needs.version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: v${{ needs.version.outputs.version }} | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-apple-darwin,x86_64-apple-darwin | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Build ARM64 | |
| run: cargo build --release --features gui --package agent-core --target aarch64-apple-darwin | |
| - name: Build x86_64 | |
| run: cargo build --release --features gui --package agent-core --target x86_64-apple-darwin | |
| - name: Create universal binary | |
| run: | | |
| mkdir -p target/release | |
| lipo -create \ | |
| target/aarch64-apple-darwin/release/agent-core \ | |
| target/x86_64-apple-darwin/release/agent-core \ | |
| -output target/release/agent-core | |
| - name: Import Certificates | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_P12 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_INSTALLER_CERTIFICATE_P12: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_P12 }} | |
| APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }} | |
| run: | | |
| # Create variables | |
| CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12" | |
| INSTALLER_CERTIFICATE_PATH="$RUNNER_TEMP/build_installer_certificate.p12" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" | |
| KEYCHAIN_PASSWORD="$(openssl rand -base64 32)" | |
| if [ -z "$APPLE_CERTIFICATE" ]; then | |
| echo "::error::Missing APPLE_CERTIFICATE secret (base64-encoded .p12)." | |
| exit 1 | |
| fi | |
| # Create and unlock a dedicated keychain for CI signing | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" | |
| security default-keychain -s "$KEYCHAIN_PATH" | |
| # Save password to file to avoid shell escaping issues | |
| echo -n "$APPLE_CERTIFICATE_PASSWORD" > "$RUNNER_TEMP/password_app.txt" | |
| if [ -n "$APPLE_INSTALLER_CERTIFICATE_PASSWORD" ]; then | |
| echo -n "$APPLE_INSTALLER_CERTIFICATE_PASSWORD" > "$RUNNER_TEMP/password_inst.txt" | |
| fi | |
| # Import Developer ID Application certificate | |
| printf '%s' "$APPLE_CERTIFICATE" | tr -d '\n\r ' | base64 -D > "$CERTIFICATE_PATH" | |
| if [ ! -s "$CERTIFICATE_PATH" ]; then | |
| echo "::error::Decoded APPLE_CERTIFICATE is empty. Check that the secret is base64 of a .p12 (single line, not double-encoded)." | |
| exit 1 | |
| fi | |
| # Verify certificate password with several variants to handle different OpenSSL/LibreSSL versions | |
| CERT_VALID=false | |
| # 1. Try with -legacy (required for modern macOS exports if using OpenSSL 3) | |
| if openssl pkcs12 -legacy -in "$CERTIFICATE_PATH" -passin file:"$RUNNER_TEMP/password_app.txt" -info -noout >/dev/null 2>&1; then | |
| CERT_VALID=true | |
| echo "✅ Certificate verified with -legacy flag" | |
| # 2. Try without -legacy (standard on older OpenSSL or LibreSSL) | |
| elif openssl pkcs12 -in "$CERTIFICATE_PATH" -passin file:"$RUNNER_TEMP/password_app.txt" -info -noout >/dev/null 2>&1; then | |
| CERT_VALID=true | |
| echo "✅ Certificate verified without -legacy flag" | |
| fi | |
| if [ "$CERT_VALID" = false ]; then | |
| echo "::error::APPLE_CERTIFICATE password is invalid or the .p12 is corrupted. Re-export the .p12 and update APPLE_CERTIFICATE/APPLE_CERTIFICATE_PASSWORD." | |
| exit 1 | |
| fi | |
| security import "$CERTIFICATE_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild | |
| # Import Developer ID Installer certificate if available | |
| if [ -n "$APPLE_INSTALLER_CERTIFICATE_P12" ]; then | |
| printf '%s' "$APPLE_INSTALLER_CERTIFICATE_P12" | tr -d '\n\r ' | base64 -D > "$INSTALLER_CERTIFICATE_PATH" | |
| if [ ! -s "$INSTALLER_CERTIFICATE_PATH" ]; then | |
| echo "::error::Decoded APPLE_INSTALLER_CERTIFICATE_P12 is empty. Check that the secret is base64 of a .p12 (single line, not double-encoded)." | |
| exit 1 | |
| fi | |
| # Verify certificate password | |
| INSTALLER_CERT_VALID=false | |
| if openssl pkcs12 -legacy -in "$INSTALLER_CERTIFICATE_PATH" -passin file:"$RUNNER_TEMP/password_inst.txt" -info -noout >/dev/null 2>&1; then | |
| INSTALLER_CERT_VALID=true | |
| echo "✅ Installer certificate verified with -legacy flag" | |
| elif openssl pkcs12 -in "$INSTALLER_CERTIFICATE_PATH" -passin file:"$RUNNER_TEMP/password_inst.txt" -info -noout >/dev/null 2>&1; then | |
| INSTALLER_CERT_VALID=true | |
| echo "✅ Installer certificate verified without -legacy flag" | |
| fi | |
| if [ "$INSTALLER_CERT_VALID" = false ]; then | |
| echo "::error::APPLE_INSTALLER_CERTIFICATE_PASSWORD is invalid or the installer .p12 is corrupted. Re-export the .p12 and update secrets." | |
| exit 1 | |
| fi | |
| security import "$INSTALLER_CERTIFICATE_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_INSTALLER_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild | |
| echo " Developer ID Installer certificate imported successfully" | |
| else | |
| echo " No Developer ID Installer certificate provided" | |
| fi | |
| # Allow codesign/productbuild to access the private key in CI without UI prompts | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Export to environment for subsequent steps | |
| echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" | |
| echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV" | |
| # Remove temp files | |
| rm -f "$CERTIFICATE_PATH" | |
| rm -f "$INSTALLER_CERTIFICATE_PATH" | |
| rm -f "$RUNNER_TEMP/password_app.txt" | |
| rm -f "$RUNNER_TEMP/password_inst.txt" | |
| - name: Build PKG | |
| env: | |
| NOTARIZE: "false" | |
| VERSION: ${{ env.RELEASE_VERSION }} | |
| run: | | |
| # Ensure keychain is unlocked and in search list | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') | |
| # Discover identities from the imported keychain | |
| SIGNING_IDENTITY=$(security find-identity -v "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}') | |
| INSTALLER_IDENTITY=$(security find-identity -v "$KEYCHAIN_PATH" | grep "Developer ID Installer" | head -1 | awk -F'"' '{print $2}') | |
| echo "Discovered Signing Identity: $SIGNING_IDENTITY" | |
| echo "Discovered Installer Identity: $INSTALLER_IDENTITY" | |
| if [ -z "$SIGNING_IDENTITY" ]; then | |
| echo "::error::Developer ID Application certificate not found in keychain $KEYCHAIN_PATH!" | |
| security find-identity -v "$KEYCHAIN_PATH" | |
| exit 1 | |
| fi | |
| # Check if we have an installer certificate for notarization | |
| if [ -z "$INSTALLER_IDENTITY" ]; then | |
| echo "::warning::No Developer ID Installer certificate found. Package will be signed but not notarized." | |
| export NOTARIZE=false | |
| fi | |
| # Make scripts executable | |
| chmod +x macos/create-macos-developer-signed-installer.sh | |
| # Run the robust installer script | |
| # Pass the found identities explicitly | |
| SIGNING_IDENTITY="$SIGNING_IDENTITY" \ | |
| INSTALLER_IDENTITY="$INSTALLER_IDENTITY" \ | |
| ./macos/create-macos-developer-signed-installer.sh | |
| # Move the output to target/release for artifact upload | |
| mv "macos/dist/SentinelAgent-${VERSION}.pkg" "target/release/" | |
| - name: Notarize PKG | |
| timeout-minutes: 35 | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| VERSION="${{ env.RELEASE_VERSION }}" | |
| PKG_PATH="target/release/SentinelAgent-${VERSION}.pkg" | |
| if [ ! -f "$PKG_PATH" ]; then | |
| echo "::error::PKG not found at $PKG_PATH" | |
| exit 1 | |
| fi | |
| echo "Submitting $PKG_PATH for notarization..." | |
| SUBMIT_OUTPUT=$(xcrun notarytool submit "$PKG_PATH" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --output-format json \ | |
| --wait --timeout 30m) || true | |
| echo "$SUBMIT_OUTPUT" | |
| # Extract status from JSON output | |
| STATUS=$(echo "$SUBMIT_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','Unknown'))" 2>/dev/null || echo "Unknown") | |
| if [ "$STATUS" = "Accepted" ]; then | |
| echo "Notarization accepted! Stapling ticket..." | |
| xcrun stapler staple "$PKG_PATH" | |
| echo "PKG notarization and stapling complete!" | |
| elif [ "$STATUS" = "Invalid" ]; then | |
| # Fetch the log for diagnostic details | |
| SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "") | |
| if [ -n "$SUBMISSION_ID" ]; then | |
| echo "::group::Notarization Log" | |
| xcrun notarytool log "$SUBMISSION_ID" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" || true | |
| echo "::endgroup::" | |
| fi | |
| echo "::error::Notarization rejected by Apple. Check the log above for details." | |
| exit 1 | |
| else | |
| echo "::error::Notarization failed with status: $STATUS" | |
| echo "::error::Ensure APPLE_ID, APPLE_APP_PASSWORD, and APPLE_TEAM_ID secrets are correctly configured." | |
| exit 1 | |
| fi | |
| - name: Create and Staple Latest Aliases | |
| run: | | |
| VERSION="${{ env.RELEASE_VERSION }}" | |
| cd target/release | |
| # PKG | |
| if [ -f "SentinelAgent-${VERSION}.pkg" ]; then | |
| echo "Creating SentinelAgent-latest.pkg..." | |
| cp "SentinelAgent-${VERSION}.pkg" "SentinelAgent-latest.pkg" | |
| echo "Stapling latest PKG..." | |
| xcrun stapler staple "SentinelAgent-latest.pkg" || echo "Staple failed for latest PKG" | |
| fi | |
| - name: Verify Gatekeeper Compliance | |
| run: | | |
| VERSION="${{ env.RELEASE_VERSION }}" | |
| cd target/release | |
| echo "=== Verifying PKG signature ===" | |
| pkgutil --check-signature "SentinelAgent-${VERSION}.pkg" | |
| echo "=== Verifying PKG notarization (stapler) ===" | |
| xcrun stapler validate "SentinelAgent-${VERSION}.pkg" | |
| echo "=== Verifying Latest PKG signature ===" | |
| pkgutil --check-signature "SentinelAgent-latest.pkg" | |
| echo "=== Verifying Latest PKG notarization (stapler) ===" | |
| xcrun stapler validate "SentinelAgent-latest.pkg" | |
| - name: Generate checksums | |
| run: | | |
| VERSION="${{ env.RELEASE_VERSION }}" | |
| cd target/release | |
| shasum -a 256 "SentinelAgent-${VERSION}.pkg" > SHA256SUMS-macos.txt | |
| # Also generate for latest (Standard shasum on macOS) | |
| [ -f "SentinelAgent-latest.pkg" ] && shasum -a 256 "SentinelAgent-latest.pkg" | cut -d' ' -f1 > "SentinelAgent-latest.pkg.sha256" | |
| cat SHA256SUMS-macos.txt | |
| - name: Upload macOS artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sentinel-agent-macos | |
| path: | | |
| target/release/SentinelAgent-*.pkg | |
| target/release/SentinelAgent-*.sha256 | |
| target/release/SHA256SUMS-macos.txt | |
| # ═══════════════════════════════════════════════════════════════════ | |
| # Windows — MSI installer via WiX v4 | |
| # ═══════════════════════════════════════════════════════════════════ | |
| build-windows: | |
| name: Build Windows MSI | |
| runs-on: windows-latest | |
| needs: [version] | |
| permissions: | |
| contents: read | |
| env: | |
| RELEASE_VERSION: ${{ needs.version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: v${{ needs.version.outputs.version }} | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install OpenSSL (vcpkg) and set Perl for vendored builds | |
| run: | | |
| vcpkg install openssl:x64-windows-static-md | |
| echo "OPENSSL_DIR=C:\vcpkg\packages\openssl_x64-windows-static-md" >> $GITHUB_ENV | |
| echo "OPENSSL_STATIC=1" >> $GITHUB_ENV | |
| echo "PERL=C:/Strawberry/perl/bin/perl.exe" >> $GITHUB_ENV | |
| shell: bash | |
| - name: Build release binary | |
| run: | | |
| # Copy icon to expected locations for build scripts | |
| echo "Setting up icons for Windows build..." | |
| mkdir -p "crates/agent-gui/assets/icons" | |
| mkdir -p "crates/agent-core/assets/icons" | |
| cp "assets/icons/sentinel-agent.ico" "crates/agent-gui/assets/icons/" | |
| cp "assets/icons/sentinel-agent.ico" "crates/agent-core/assets/icons/" | |
| echo "Icons copied successfully" | |
| cargo build --release --features gui --package agent-core --no-default-features | |
| - name: Install WiX Toolset | |
| run: | | |
| dotnet tool install --global wix --version 4.0.0 | |
| wix extension add -g WixToolset.UI.wixext/4.0.0 | |
| wix extension add -g WixToolset.Util.wixext/4.0.0 | |
| wix extension add -g WixToolset.Firewall.wixext/4.0.0 | |
| - name: Generate self-signed certificate | |
| run: | | |
| $cert = New-SelfSignedCertificate ` | |
| -Type CodeSigningCert ` | |
| -Subject "CN=Sentinel GRC Agent, O=Cyber Threat Consulting" ` | |
| -CertStoreLocation Cert:\CurrentUser\My ` | |
| -NotAfter (Get-Date).AddYears(5) | |
| Export-Certificate -Cert $cert -FilePath sentinel-selfsigned.cer | |
| Write-Host "Generated sentinel-selfsigned.cer" | |
| shell: pwsh | |
| - name: Build MSI | |
| run: | | |
| $version = "${{ env.RELEASE_VERSION }}" | |
| # Verify required files exist | |
| if (-not (Test-Path "target\release\agent-core.exe")) { | |
| Write-Error "agent-core.exe not found in target\release" | |
| exit 1 | |
| } | |
| if (-not (Test-Path "assets\icons\sentinel-agent.ico")) { | |
| Write-Error "sentinel-agent.ico not found in assets\icons" | |
| exit 1 | |
| } | |
| if (-not (Test-Path "wix\main.wxs")) { | |
| Write-Error "main.wxs not found in wix directory" | |
| exit 1 | |
| } | |
| Write-Host "Building MSI with version: $version" | |
| Write-Host "TargetDir: .\target\release" | |
| Write-Host "AssetsDir: .\assets" | |
| wix build ` | |
| -d "TargetDir=.\target\release" ` | |
| -d "Version=$version" ` | |
| -d "AssetsDir=.\assets" ` | |
| -d "ServerUrl=${{ secrets.SENTINEL_SERVER_URL }}" ` | |
| -o "target\release\SentinelAgentSetup-$version.msi" ` | |
| wix/main.wxs ` | |
| -ext WixToolset.UI.wixext ` | |
| -ext WixToolset.Util.wixext ` | |
| -ext WixToolset.Firewall.wixext ` | |
| -v | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Error "WiX build failed with exit code: $LASTEXITCODE" | |
| exit $LASTEXITCODE | |
| } | |
| if (-not (Test-Path "target\release\SentinelAgentSetup-$version.msi")) { | |
| Write-Error "MSI file was not created" | |
| exit 1 | |
| } | |
| $msiSize = (Get-Item "target\release\SentinelAgentSetup-$version.msi").Length | |
| Write-Host "MSI created successfully: SentinelAgentSetup-$version.msi ($msiSize bytes)" | |
| shell: pwsh | |
| - name: Upload unsigned MSI for signing | |
| id: upload-unsigned | |
| if: ${{ vars.SIGNPATH_ORGANIZATION_ID != '' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sentinel-agent-windows-unsigned | |
| path: target/release/SentinelAgentSetup-${{ env.RELEASE_VERSION }}.msi | |
| - name: Sign MSI with SignPath (Direct API Fallback) | |
| if: ${{ vars.SIGNPATH_ORGANIZATION_ID != '' }} | |
| id: signpath | |
| run: | | |
| $version = "${{ env.RELEASE_VERSION }}" | |
| $msiPath = "target\release\SentinelAgentSetup-$version.msi" | |
| $outputDir = "target\release\signed" | |
| $outputPath = "$outputDir\SentinelAgentSetup-$version.msi" | |
| if (-not (Test-Path $outputDir)) { mkdir $outputDir -Force } | |
| Write-Host "Installing SignPath PowerShell module..." | |
| Install-Module -Name SignPath -Force -Scope CurrentUser -Repository PSGallery | |
| Write-Host "Submitting signing request to SignPath..." | |
| # We use direct parameters to bypass the specialized 'Trusted Build' login flow of the official Action | |
| $orgId = "${{ vars.SIGNPATH_ORGANIZATION_ID }}".Trim() | |
| $projectSlug = "${{ vars.SIGNPATH_PROJECT_SLUG }}".Trim() | |
| $policySlug = "${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}".Trim() | |
| $artifactSlug = "${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}".Trim() | |
| Submit-SigningRequest ` | |
| -InputArtifactPath $msiPath ` | |
| -ApiToken "${{ secrets.SIGNPATH_API_TOKEN }}" ` | |
| -OrganizationId $orgId ` | |
| -ProjectSlug $projectSlug ` | |
| -SigningPolicySlug $policySlug ` | |
| -ArtifactConfigurationSlug $artifactSlug ` | |
| -OutputArtifactPath $outputPath ` | |
| -WaitForCompletion | |
| if (Test-Path $outputPath) { | |
| Write-Host "MSI signed successfully" | |
| "outcome=success" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| } else { | |
| Write-Error "Signing failed: Output file not produced" | |
| exit 1 | |
| } | |
| shell: pwsh | |
| - name: Replace MSI with signed version | |
| if: ${{ vars.SIGNPATH_ORGANIZATION_ID != '' && steps.signpath.outputs.outcome == 'success' }} | |
| run: | | |
| $version = "${{ env.RELEASE_VERSION }}" | |
| $signedMsi = Get-ChildItem "target\release\signed\*.msi" | Select-Object -First 1 | |
| if ($signedMsi) { | |
| Copy-Item $signedMsi.FullName "target\release\SentinelAgentSetup-$version.msi" -Force | |
| Write-Host "MSI signed successfully with SignPath" | |
| } else { | |
| Write-Host "::warning::No signed MSI found in output directory" | |
| } | |
| shell: pwsh | |
| - name: Warn if SignPath not configured | |
| if: ${{ vars.SIGNPATH_ORGANIZATION_ID == '' }} | |
| run: | | |
| echo "::warning::SignPath not configured - Using free self-signed signing." | |
| echo "::warning::To enable SignPath, configure SIGNPATH_ORGANIZATION_ID, SIGNPATH_PROJECT_SLUG (vars) and SIGNPATH_API_TOKEN (secret)." | |
| - name: Generate free self-signed certificate (fallback) | |
| if: ${{ vars.SIGNPATH_ORGANIZATION_ID == '' }} | |
| run: | | |
| $cert = New-SelfSignedCertificate ` | |
| -Type CodeSigningCert ` | |
| -Subject "CN=Sentinel GRC Agent, O=Cyber Threat Consulting, C=FR" ` | |
| -CertStoreLocation Cert:\CurrentUser\My ` | |
| -KeyUsage DigitalSignature ` | |
| -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3","2.5.29.19={text}CA:FALSE") ` | |
| -NotAfter (Get-Date).AddYears(10) | |
| # Export PFX for signing | |
| $password = "Sentinel2024!Free" | |
| $securePassword = ConvertTo-SecureString -String $password -Force -AsPlainText | |
| Export-PfxCertificate -Cert $cert -FilePath "sentinel-selfsigned.pfx" -Password $securePassword | Out-Null | |
| # Export public certificate | |
| Export-Certificate -Cert $cert -FilePath "sentinel-selfsigned.cer" | Out-Null | |
| Write-Host "Generated free self-signed certificate (10 years validity)" | |
| shell: pwsh | |
| - name: Sign MSI with free certificate | |
| if: ${{ vars.SIGNPATH_ORGANIZATION_ID == '' }} | |
| run: | | |
| $version = "${{ env.RELEASE_VERSION }}" | |
| $password = "Sentinel2024!Free" | |
| $securePassword = ConvertTo-SecureString -String $password -Force -AsPlainText | |
| # Locate signtool.exe from Windows SDK (not in PATH by default on GH Actions runners) | |
| $signtoolExe = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter "signtool.exe" -ErrorAction SilentlyContinue | | |
| Where-Object { $_.FullName -match "x64" } | | |
| Sort-Object { [version]($_.Directory.Parent.Name) } -Descending | | |
| Select-Object -First 1 | |
| if (-not $signtoolExe) { | |
| Write-Host "::warning::signtool.exe not found in Windows SDK — skipping code signing" | |
| } else { | |
| $signtool = $signtoolExe.FullName | |
| Write-Host "Found signtool: $signtool" | |
| # Sign the MSI | |
| & $signtool sign /f "sentinel-selfsigned.pfx" /p $password /t http://timestamp.digicert.com /fd sha256 "target\release\SentinelAgentSetup-$version.msi" | |
| # Verify: self-signed certs are not in the trusted root store so verify /pa always fails — just confirm the file is signed | |
| $verifyResult = & $signtool verify /pa "target\release\SentinelAgentSetup-$version.msi" 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Host "::notice::Signature verification returned non-zero (expected for self-signed certificates)" | |
| Write-Host ($verifyResult | Out-String) | |
| } else { | |
| Write-Host "Signature verified successfully" | |
| } | |
| # Reset exit code so the step does not fail | |
| $global:LASTEXITCODE = 0 | |
| Write-Host "MSI signed successfully with free certificate" | |
| } | |
| # Cleanup | |
| Remove-Item "sentinel-selfsigned.pfx" -Force -ErrorAction SilentlyContinue | |
| Remove-Item "sentinel-selfsigned.cer" -Force -ErrorAction SilentlyContinue | |
| shell: pwsh | |
| - name: Generate checksums | |
| run: | | |
| $version = "${{ env.RELEASE_VERSION }}" | |
| cd target\release | |
| $hash = Get-FileHash "SentinelAgentSetup-$version.msi" -Algorithm SHA256 | |
| "$($hash.Hash.ToLower()) SentinelAgentSetup-$version.msi" | Out-File -Encoding ASCII "SHA256SUMS-windows.txt" | |
| Get-Content "SHA256SUMS-windows.txt" | |
| shell: pwsh | |
| - name: Upload Windows artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sentinel-agent-windows | |
| path: | | |
| target/release/SentinelAgentSetup-*.msi | |
| target/release/SHA256SUMS-windows.txt | |
| # ═══════════════════════════════════════════════════════════════════ | |
| # Linux — DEB package (Debian/Ubuntu) | |
| # ═══════════════════════════════════════════════════════════════════ | |
| build-deb: | |
| name: Build Linux DEB | |
| runs-on: ubuntu-latest | |
| needs: [version] | |
| env: | |
| OPENSSL_DIR: /usr | |
| OPENSSL_LIB_DIR: /usr/lib/x86_64-linux-gnu | |
| OPENSSL_INCLUDE_DIR: /usr/include/openssl | |
| RELEASE_VERSION: ${{ needs.version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: v${{ needs.version.outputs.version }} | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libgtk-3-dev libayatana-appindicator3-dev libssl-dev pkg-config libxdo-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libudev-dev | |
| - name: Install cargo-deb | |
| run: cargo install cargo-deb | |
| - name: Build release binary | |
| run: cargo build --release --features gui --package agent-core --no-default-features | |
| - name: Build DEB package | |
| run: cargo deb --package agent-core --no-build --deb-revision 1 | |
| - name: Generate checksums | |
| run: | | |
| cd target/debian | |
| for f in sentinel-agent_*.deb; do | |
| sha256sum "$f" >> SHA256SUMS-linux-deb.txt | |
| done | |
| cat SHA256SUMS-linux-deb.txt | |
| - name: Upload DEB artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sentinel-agent-linux-deb | |
| path: | | |
| target/debian/sentinel-agent_*.deb | |
| target/debian/SHA256SUMS-linux-deb.txt | |
| # ═══════════════════════════════════════════════════════════════════ | |
| # Linux — RPM package (RHEL/Fedora/CentOS) | |
| # ═══════════════════════════════════════════════════════════════════ | |
| build-rpm: | |
| name: Build Linux RPM | |
| runs-on: ubuntu-latest | |
| needs: [version] | |
| env: | |
| OPENSSL_DIR: /usr | |
| OPENSSL_LIB_DIR: /usr/lib/x86_64-linux-gnu | |
| OPENSSL_INCLUDE_DIR: /usr/include/openssl | |
| RELEASE_VERSION: ${{ needs.version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: v${{ needs.version.outputs.version }} | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libgtk-3-dev libayatana-appindicator3-dev libssl-dev pkg-config libxdo-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libudev-dev | |
| - name: Install cargo-generate-rpm | |
| run: cargo install cargo-generate-rpm | |
| - name: Build release binary | |
| run: cargo build --release --features gui --package agent-core --no-default-features | |
| - name: Strip binary | |
| run: strip target/release/agent-core | |
| - name: Build RPM package | |
| run: cargo generate-rpm -p crates/agent-core | |
| - name: Generate checksums | |
| run: | | |
| cd target/generate-rpm | |
| for f in sentinel-agent-*.rpm; do | |
| sha256sum "$f" >> SHA256SUMS-linux-rpm.txt | |
| done | |
| cat SHA256SUMS-linux-rpm.txt | |
| - name: Upload RPM artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sentinel-agent-linux-rpm | |
| path: | | |
| target/generate-rpm/sentinel-agent-*.rpm | |
| target/generate-rpm/SHA256SUMS-linux-rpm.txt | |
| # ═══════════════════════════════════════════════════════════════════ | |
| # SBOM Generation (CycloneDX) | |
| # ═══════════════════════════════════════════════════════════════════ | |
| sbom: | |
| name: Generate SBOM | |
| runs-on: ubuntu-latest | |
| needs: [version] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: v${{ needs.version.outputs.version }} | |
| - uses: dtolnay/rust-toolchain@stable | |
| - name: Install cargo-cyclonedx | |
| run: cargo install cargo-cyclonedx | |
| - name: Generate CycloneDX SBOM | |
| run: | | |
| cargo cyclonedx --format json --override-filename bom | |
| mkdir -p sbom | |
| cp crates/agent-core/bom.json sbom/sentinel-agent-${{ needs.version.outputs.version }}.cdx.json | |
| - name: Upload SBOM artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sentinel-agent-sbom | |
| path: sbom/ | |
| # ═══════════════════════════════════════════════════════════════════ | |
| # Artifact Attestation (SLSA provenance) | |
| # ═══════════════════════════════════════════════════════════════════ | |
| attestation: | |
| name: Attest build provenance | |
| runs-on: ubuntu-latest | |
| needs: [version, build-macos, build-windows, build-deb, build-rpm] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: v${{ needs.version.outputs.version }} | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Attest macOS artifacts | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: artifacts/sentinel-agent-macos/SentinelAgent-* | |
| - name: Attest Windows artifacts | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: artifacts/sentinel-agent-windows/SentinelAgentSetup-*.msi | |
| - name: Attest Linux DEB artifacts | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: artifacts/sentinel-agent-linux-deb/sentinel-agent_*.deb | |
| - name: Attest Linux RPM artifacts | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: artifacts/sentinel-agent-linux-rpm/sentinel-agent-*.rpm | |
| # ═══════════════════════════════════════════════════════════════════ | |
| # Upload all artifacts to Firebase Storage + GitHub Release | |
| # ═══════════════════════════════════════════════════════════════════ | |
| upload-releases: | |
| name: Upload to Firebase Storage | |
| runs-on: ubuntu-latest | |
| needs: [version, build-macos, build-windows, build-deb, build-rpm, sbom, attestation] | |
| env: | |
| RELEASE_VERSION: ${{ needs.version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: v${{ needs.version.outputs.version }} | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: List artifacts | |
| run: find artifacts -type f | sort | |
| - name: Validate Firebase/GCP secrets | |
| run: | | |
| MISSING="" | |
| [ -z "${{ secrets.GCP_SA_KEY }}" ] && MISSING="$MISSING GCP_SA_KEY" | |
| [ -z "${{ secrets.GCP_PROJECT_ID }}" ] && MISSING="$MISSING GCP_PROJECT_ID" | |
| [ -z "${{ secrets.FIREBASE_STORAGE_BUCKET }}" ] && MISSING="$MISSING FIREBASE_STORAGE_BUCKET" | |
| if [ -n "$MISSING" ]; then | |
| echo "::error::Missing required secrets:$MISSING — configure them in Settings → Secrets → Actions." | |
| exit 1 | |
| fi | |
| - name: Authenticate to Google Cloud | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| project_id: ${{ secrets.GCP_PROJECT_ID }} | |
| - name: Upload to Firebase Storage | |
| shell: bash | |
| env: | |
| RELEASE_VERSION: ${{ env.RELEASE_VERSION }} | |
| run: | | |
| # 3. Upload loop | |
| VERSION="${{ env.RELEASE_VERSION }}" | |
| BUCKET="gs://${{ secrets.FIREBASE_STORAGE_BUCKET }}/releases/agent" | |
| echo "=== Cleaning up existing 'latest' aliases and metadata (All Platforms) ===" | |
| # Remove all aliases and metadata across all subfolders to ensure a clean state | |
| # This covers macos, windows, linux_deb, linux_rpm | |
| gcloud storage rm "$BUCKET/**/latest*" \ | |
| "$BUCKET/**/*-latest*" \ | |
| "$BUCKET/**/sentinel-agent-latest*" \ | |
| "$BUCKET/release-info.json" \ | |
| "$BUCKET/SHA256SUMS*.txt" 2>/dev/null || true | |
| echo "=== Generating aliases and metadata ===" | |
| DATE=$(date -u +"%Y-%m-%d") | |
| # Helper to create latest alias and checksum | |
| create_latest() { | |
| local pattern="$1" | |
| local folder="$2" | |
| local latest_name="$3" | |
| # For macOS, we already created latest aliases on the macOS runner to ensure stapling/notarization | |
| if [[ "$folder" == "sentinel-agent-macos" ]]; then | |
| echo "Skipping local alias creation for macOS (pre-created on build runner)" | |
| return | |
| fi | |
| # Find the first matching file (should be only one per pattern) | |
| local file=$(ls artifacts/$folder/$pattern 2>/dev/null | head -n 1) | |
| if [ -n "$file" ]; then | |
| echo "Creating latest alias for $file -> $latest_name" | |
| cp "$file" "artifacts/$folder/$latest_name" | |
| # Create checksum for latest | |
| sha256sum "$file" | cut -d' ' -f1 > "artifacts/$folder/$latest_name.sha256" | |
| gcloud storage cp "artifacts/$folder/$latest_name" "$BUCKET/${folder##sentinel-agent-}/" | |
| gcloud storage cp "artifacts/$folder/$latest_name.sha256" "$BUCKET/${folder##sentinel-agent-}/" | |
| fi | |
| } | |
| create_latest "SentinelAgent-*.pkg" "sentinel-agent-macos" "SentinelAgent-latest.pkg" | |
| create_latest "SentinelAgentSetup-*.msi" "sentinel-agent-windows" "SentinelAgentSetup-latest.msi" | |
| create_latest "sentinel-agent_*.deb" "sentinel-agent-linux-deb" "sentinel-agent-latest-amd64.deb" | |
| create_latest "sentinel-agent-*.rpm" "sentinel-agent-linux-rpm" "sentinel-agent-latest.x86_64.rpm" | |
| echo "=== Processing macOS latest files ===" | |
| # macOS latest files are pre-created and stapled on macOS runner | |
| if [ -d "artifacts/sentinel-agent-macos" ]; then | |
| ls -R artifacts/sentinel-agent-macos/ | |
| [ -f "artifacts/sentinel-agent-macos/SentinelAgent-latest.pkg" ] && \ | |
| gcloud storage cp "artifacts/sentinel-agent-macos/SentinelAgent-latest.pkg" "$BUCKET/macos/" | |
| gcloud storage cp "artifacts/sentinel-agent-macos/SentinelAgent-latest.pkg.sha256" "$BUCKET/macos/" | |
| fi | |
| # Generate release-info.json | |
| jq -n --arg v "$VERSION" --arg d "$DATE" \ | |
| '{version: $v, date: $d, product: "agent"}' > release-info.json | |
| gcloud storage cp release-info.json "$BUCKET/release-info.json" | |
| echo "🚀 Starting atomic upload of versioned files..." | |
| # Upload all artifacts (versioned) | |
| gcloud storage cp "artifacts/sentinel-agent-macos/SentinelAgent-*" "$BUCKET/macos/" | |
| gcloud storage cp "artifacts/sentinel-agent-windows/SentinelAgentSetup-*" "$BUCKET/windows/" | |
| gcloud storage cp "artifacts/sentinel-agent-linux-deb/sentinel-agent_*" "$BUCKET/linux_deb/" | |
| gcloud storage cp "artifacts/sentinel-agent-linux-rpm/sentinel-agent-*" "$BUCKET/linux_rpm/" | |
| gcloud storage cp "artifacts/*/SHA256SUMS*.txt" "$BUCKET/" 2>/dev/null || true | |
| echo "=== Setting content types ===" | |
| gcloud storage objects update "$BUCKET/macos/*.pkg" --content-type="application/vnd.apple.installer+xml" 2>/dev/null || true | |
| gcloud storage objects update "$BUCKET/windows/*.msi" --content-type="application/x-msi" 2>/dev/null || true | |
| gcloud storage objects update "$BUCKET/linux_deb/*.deb" --content-type="application/vnd.debian.binary-package" 2>/dev/null || true | |
| gcloud storage objects update "$BUCKET/linux_rpm/*.rpm" --content-type="application/x-rpm" 2>/dev/null || true | |
| gcloud storage objects update "$BUCKET/release-info.json" --content-type="application/json" 2>/dev/null || true | |
| echo "=== Configuring Public Access and CORS ===" | |
| # Extract bucket name without gs:// and path | |
| BUCKET_NAME=$(echo "$BUCKET" | sed 's/gs:\/\/\([^\/]*\).*/\1/') | |
| # 1. Apply CORS configuration | |
| gcloud storage buckets update gs://$BUCKET_NAME --cors-file=.github/workflows/cors-config.json | |
| # 2. Grant public read access to the releases folder | |
| echo "Setting public read access on release objects" | |
| gsutil -m acl set -R public-read "$BUCKET" || true | |
| # 4. Set Cache-Control for 'latest' aliases to ensure they are not cached too long by CDN | |
| echo "Setting Cache-Control for latest aliases" | |
| gcloud storage objects update "$BUCKET/**/latest*" --cache-control="public, max-age=3600, no-cache" 2>/dev/null || true | |
| gcloud storage objects update "$BUCKET/release-info.json" --cache-control="public, max-age=300" 2>/dev/null || true | |
| # 5. Verify permissions are set correctly | |
| echo "Verifying public access on latest files" | |
| gsutil acl get "$BUCKET/macos/SentinelAgent-latest.pkg" 2>/dev/null || echo "Could not verify macOS pkg ACL" | |
| gsutil acl get "$BUCKET/windows/SentinelAgentSetup-latest.msi" 2>/dev/null || echo "Could not verify Windows MSI ACL" | |
| echo "✅ Release upload and configuration completed successfully" | |
| gcloud storage objects update "$BUCKET/**/*.txt" --content-type="text/plain" 2>/dev/null || true | |
| echo "✅ Enhanced release upload complete" | |
| - name: Verify uploads | |
| run: | | |
| BUCKET="gs://${{ secrets.FIREBASE_STORAGE_BUCKET }}/releases/agent" | |
| echo "=== Firebase Storage contents ===" | |
| gsutil ls -l "$BUCKET/macos/" 2>/dev/null || echo "(no macos files)" | |
| gsutil ls -l "$BUCKET/windows/" 2>/dev/null || echo "(no windows files)" | |
| gsutil ls -l "$BUCKET/linux_deb/" 2>/dev/null || echo "(no linux_deb files)" | |
| gsutil ls -l "$BUCKET/linux_rpm/" 2>/dev/null || echo "(no linux_rpm files)" | |
| - name: Verify public access | |
| run: | | |
| echo "=== Testing public download URLs ===" | |
| BASE_URL="https://storage.googleapis.com/${{ secrets.FIREBASE_STORAGE_BUCKET }}/releases/agent" | |
| # Test macOS | |
| echo "Testing macOS download..." | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/macos/SentinelAgent-latest.pkg") | |
| echo "macOS pkg HTTP status: $HTTP_CODE" | |
| # Test Windows | |
| echo "Testing Windows download..." | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/windows/SentinelAgentSetup-latest.msi") | |
| echo "Windows MSI HTTP status: $HTTP_CODE" | |
| # Test Linux DEB | |
| echo "Testing Linux DEB download..." | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/linux_deb/sentinel-agent_latest_amd64.deb") | |
| echo "Linux DEB HTTP status: $HTTP_CODE" | |
| # Test Linux RPM | |
| echo "Testing Linux RPM download..." | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/linux_rpm/sentinel-agent-latest.x86_64.rpm") | |
| echo "Linux RPM HTTP status: $HTTP_CODE" | |
| echo "✅ Public access verification complete" | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ env.RELEASE_VERSION }} | |
| name: "Sentinel Agent v${{ env.RELEASE_VERSION }}" | |
| body: | | |
| ## Sentinel GRC Agent v${{ env.RELEASE_VERSION }} | |
| ### Downloads | |
| | Platform | Package | Format | Notes | | |
| |----------|---------|--------|-------| | |
| | macOS (Universal) | `SentinelAgent-${{ env.RELEASE_VERSION }}.pkg` | PKG | Signed + Notarized | | |
| | Windows x64 | `SentinelAgentSetup-${{ env.RELEASE_VERSION }}.msi` | MSI | Signed via SignPath | | |
| | Linux (Debian/Ubuntu) | `sentinel-agent_${{ env.RELEASE_VERSION }}-1_amd64.deb` | DEB | | | |
| | Linux (RHEL/Fedora) | `sentinel-agent-${{ env.RELEASE_VERSION }}-1.x86_64.rpm` | RPM | | | |
| ### Installation | |
| **macOS**: Open PKG and follow installer prompts, or double-click PKG. | |
| **Windows**: Run the MSI installer as Administrator. | |
| **Linux (DEB)**: `sudo dpkg -i sentinel-agent_${{ env.RELEASE_VERSION }}-1_amd64.deb` | |
| **Linux (RPM)**: `sudo rpm -i sentinel-agent-${{ env.RELEASE_VERSION }}-1.x86_64.rpm` | |
| Then enroll: `sentinel-agent enroll --token <YOUR_TOKEN>` | |
| files: | | |
| artifacts/sentinel-agent-macos/SentinelAgent-*.pkg | |
| artifacts/sentinel-agent-macos/SHA256SUMS-macos.txt | |
| artifacts/sentinel-agent-windows/SentinelAgentSetup-*.msi | |
| artifacts/sentinel-agent-windows/SHA256SUMS-windows.txt | |
| artifacts/sentinel-agent-linux-deb/sentinel-agent_*.deb | |
| artifacts/sentinel-agent-linux-deb/SHA256SUMS-linux-deb.txt | |
| artifacts/sentinel-agent-linux-rpm/sentinel-agent-*.rpm | |
| artifacts/sentinel-agent-linux-rpm/SHA256SUMS-linux-rpm.txt | |
| artifacts/sentinel-agent-sbom/*.cdx.json | |
| generate_release_notes: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |