Skip to content

Release

Release #405

Workflow file for this run

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 }}