Add fleet-desktop-macos release workflow (#49903)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #45524 # Checklist for submitter If some of the following don't apply, delete the relevant line. No changes file as this is purely workflow/release changes ## Testing Will be testing this on github as that's the only way and since this requires testing with a tagged build I'll have to merge to main, tag, then manually trigger - [ ] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Introduced an automated macOS Fleet Desktop release workflow for version-tagged releases. * Publishes the installer plus accompanying `meta.json` download metadata, including SHA256 checksums. * **Bug Fixes** * Added stronger pre-release validation (tag/version match and immutable release enforcement). * Verifies the downloaded package and metadata against the expected SHA256 to ensure the published artifacts are consistent. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
09fea47ce4
commit
a0d795583d
@@ -3,7 +3,9 @@ name: Build Fleet Desktop (macOS)
|
||||
# Builds the native macOS Fleet Desktop app (apps/fleet-desktop-macos/) and its
|
||||
# embedded Platform SSO extension (FleetPSSOExtension.appex), code signs and
|
||||
# notarizes them with Fleet's Developer ID certificates, and uploads the signed
|
||||
# .pkg as a workflow artifact. No GitHub Release is created.
|
||||
# .pkg as a workflow artifact. No GitHub Release is created. Also callable as a
|
||||
# reusable workflow (workflow_call), which is how release-fleet-desktop-macos.yml
|
||||
# produces the pkg it publishes to download.fleetdm.com.
|
||||
#
|
||||
# The app and extension carry managed Associated Domains entitlements
|
||||
# (com.apple.developer.associated-domains{,.mdm-managed}). Those are restricted
|
||||
@@ -28,6 +30,28 @@ on:
|
||||
- 'apps/fleet-desktop-macos/**'
|
||||
- '.github/workflows/fleet-desktop-macos-build.yml'
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
secrets:
|
||||
APPLE_APPLICATION_CERTIFICATE:
|
||||
required: true
|
||||
APPLE_APPLICATION_CERTIFICATE_PASSWORD:
|
||||
required: true
|
||||
APPLE_INSTALLER_CERTIFICATE:
|
||||
required: true
|
||||
APPLE_INSTALLER_CERTIFICATE_PASSWORD:
|
||||
required: true
|
||||
KEYCHAIN_PASSWORD:
|
||||
required: true
|
||||
APPLE_FLEET_DESKTOP_APP_PROFILE_B64:
|
||||
required: true
|
||||
APPLE_PSSO_EXT_PROFILE_B64:
|
||||
required: true
|
||||
APPLE_USERNAME:
|
||||
required: true
|
||||
APPLE_PASSWORD:
|
||||
required: true
|
||||
APPLE_TEAM_ID:
|
||||
required: true
|
||||
|
||||
# Cancel superseded runs on the same ref.
|
||||
concurrency:
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
name: Release Fleet Desktop (macOS)
|
||||
|
||||
# Publishes a tagged build of the native macOS Fleet Desktop app
|
||||
# (apps/fleet-desktop-macos/) to download.fleetdm.com.
|
||||
#
|
||||
# Run manually from the Actions tab, selecting a fleet-desktop-macos-v* tag in
|
||||
# the "Use workflow from" dropdown. The workflow:
|
||||
# 1. Fails fast unless the ref is a fleet-desktop-macos-v<x>.<y>.<z> tag whose
|
||||
# commit is on main, the tag version matches the app's
|
||||
# CFBundleShortVersionString, and that version is not already uploaded
|
||||
# (releases are immutable).
|
||||
# 2. Builds, signs, and notarizes the pkg via fleet-desktop-macos-build.yml.
|
||||
# 3. Uploads to R2:
|
||||
# - fleet-desktop-macos/v<version>/fleet_desktop-v<version>.pkg
|
||||
# - fleet-desktop-macos/v<version>/meta.json (version, fleet_desktop_pkg_sha256, fleet_desktop_pkg_url)
|
||||
# 4. Downloads the pkg back from the public URL and verifies its SHA256
|
||||
# matches the built artifact, then writes the checksum to the run summary.
|
||||
#
|
||||
# No GitHub Release is created; the git tag is the release marker.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
testing:
|
||||
description: "Upload to download-testing.fleetdm.com instead of production."
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
# Serialize releases so two runs can't race past the already-released check.
|
||||
# Never cancel a release mid-upload; queue instead.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
defaults:
|
||||
run:
|
||||
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
|
||||
shell: bash
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
R2_ACCESS_KEY_ID: ${{ inputs.testing && secrets.R2_DOWNLOAD_TESTING_ACCESS_KEY_ID || secrets.R2_DOWNLOAD_ACCESS_KEY_ID }}
|
||||
R2_ACCESS_KEY_SECRET: ${{ inputs.testing && secrets.R2_DOWNLOAD_TESTING_ACCESS_KEY_SECRET || secrets.R2_DOWNLOAD_ACCESS_KEY_SECRET }}
|
||||
R2_BUCKET: ${{ inputs.testing && 'download-testing' || 'download' }}
|
||||
BASE_URL: ${{ inputs.testing && 'https://download-testing.fleetdm.com' || 'https://download.fleetdm.com' }}
|
||||
RELEASE_PREFIX: fleet-desktop-macos
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
name: Pre-release checks
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Verify ref is a release tag
|
||||
run: |
|
||||
if [ "$GITHUB_REF_TYPE" != "tag" ]; then
|
||||
echo "::error::This workflow must be dispatched from a fleet-desktop-macos-v* tag, not a $GITHUB_REF_TYPE ('$GITHUB_REF_NAME'). Select the release tag in the 'Use workflow from' dropdown."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "$GITHUB_REF_NAME" =~ ^fleet-desktop-macos-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::Tag '$GITHUB_REF_NAME' does not match fleet-desktop-macos-v<major>.<minor>.<patch>."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify tagged commit is on main
|
||||
run: |
|
||||
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
|
||||
echo "::error::Tagged commit $GITHUB_SHA is not on main."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify tag version matches the app version
|
||||
id: version
|
||||
run: |
|
||||
tag_version="${GITHUB_REF_NAME#fleet-desktop-macos-v}"
|
||||
app_version=$(python3 -c 'import plistlib; print(plistlib.load(open("apps/fleet-desktop-macos/FleetDesktop/Info.plist", "rb"))["CFBundleShortVersionString"])')
|
||||
if [ "$tag_version" != "$app_version" ]; then
|
||||
echo "::error::Tag version ($tag_version) does not match CFBundleShortVersionString ($app_version) in apps/fleet-desktop-macos/FleetDesktop/Info.plist."
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$app_version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify version is not already released
|
||||
env:
|
||||
RCLONE_CONFIG_R2_TYPE: s3
|
||||
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
|
||||
RCLONE_CONFIG_R2_REGION: auto
|
||||
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true"
|
||||
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ env.R2_ACCESS_KEY_ID }}
|
||||
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ env.R2_ACCESS_KEY_SECRET }}
|
||||
RCLONE_CONFIG_R2_ENDPOINT: ${{ env.R2_ENDPOINT }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
sudo .github/scripts/rclone-install.sh
|
||||
: # Check via the R2 API rather than the public URL: it's authoritative,
|
||||
: # and a pre-upload GET of the URL could prime the CDN with a cached 404
|
||||
: # that the post-upload verification then trips over.
|
||||
: # Surface broken credentials/endpoint as their own failure before the
|
||||
: # exit-code handling below.
|
||||
rclone lsf "r2:${R2_BUCKET}" --max-depth 1 > /dev/null
|
||||
rc=0
|
||||
existing=$(rclone lsf "r2:${R2_BUCKET}/${RELEASE_PREFIX}/v${VERSION}/" 2>rclone-stderr.log) || rc=$?
|
||||
: # rclone exit 3 = directory not found, i.e. this version was never
|
||||
: # uploaded. Any other failure means we could not check; fail rather
|
||||
: # than risk overwriting an existing release.
|
||||
if [ "$rc" -ne 0 ] && [ "$rc" -ne 3 ]; then
|
||||
cat rclone-stderr.log >&2
|
||||
echo "::error::Could not determine whether v${VERSION} is already released (rclone exit code $rc)."
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$existing" ]; then
|
||||
echo "::error::${RELEASE_PREFIX}/v${VERSION}/ already exists at ${BASE_URL}. Releases are immutable; bump the version to publish a new build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build, sign, and notarize
|
||||
needs: checks
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/fleet-desktop-macos-build.yml
|
||||
secrets:
|
||||
APPLE_APPLICATION_CERTIFICATE: ${{ secrets.APPLE_APPLICATION_CERTIFICATE }}
|
||||
APPLE_APPLICATION_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APPLICATION_CERTIFICATE_PASSWORD }}
|
||||
APPLE_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_INSTALLER_CERTIFICATE }}
|
||||
APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
APPLE_FLEET_DESKTOP_APP_PROFILE_B64: ${{ secrets.APPLE_FLEET_DESKTOP_APP_PROFILE_B64 }}
|
||||
APPLE_PSSO_EXT_PROFILE_B64: ${{ secrets.APPLE_PSSO_EXT_PROFILE_B64 }}
|
||||
APPLE_USERNAME: ${{ secrets.APPLE_USERNAME }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
upload:
|
||||
name: Upload pkg to R2
|
||||
needs: [checks, build]
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
pkg_sha256: ${{ steps.prepare.outputs.pkg_sha256 }}
|
||||
env:
|
||||
VERSION: ${{ needs.checks.outputs.version }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code needed for R2 upload
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/actions/r2-upload/action.yml
|
||||
.github/scripts/rclone-install.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download built pkg artifact
|
||||
uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.6
|
||||
with:
|
||||
name: fleet_desktop-pkg
|
||||
|
||||
- name: Prepare files for R2 upload
|
||||
id: prepare
|
||||
run: |
|
||||
PKG_NAME="fleet_desktop-v${VERSION}.pkg"
|
||||
if [ ! -f "$PKG_NAME" ]; then
|
||||
echo "::error::Build artifact does not contain $PKG_NAME; the built app version disagrees with the tag."
|
||||
ls -la
|
||||
exit 1
|
||||
fi
|
||||
pkg_sha256=$(shasum -a 256 "$PKG_NAME" | cut -d ' ' -f 1)
|
||||
RELEASE_DIR="${RELEASE_PREFIX}/v${VERSION}"
|
||||
mkdir -p "$RELEASE_DIR"
|
||||
mv "$PKG_NAME" "$RELEASE_DIR/"
|
||||
echo "{
|
||||
\"fleet_desktop_pkg_url\": \"${BASE_URL}/${RELEASE_DIR}/${PKG_NAME}\",
|
||||
\"fleet_desktop_pkg_sha256\": \"${pkg_sha256}\",
|
||||
\"version\": \"${VERSION}\"
|
||||
}" > "$RELEASE_DIR/meta.json"
|
||||
: # Check that meta.json is valid
|
||||
jq -e . "$RELEASE_DIR/meta.json" > /dev/null
|
||||
echo "pkg_sha256=$pkg_sha256" >> "$GITHUB_OUTPUT"
|
||||
echo "upload_filenames=${RELEASE_DIR}/${PKG_NAME},${RELEASE_DIR}/meta.json" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload package
|
||||
uses: ./.github/actions/r2-upload
|
||||
with:
|
||||
filenames: ${{ steps.prepare.outputs.upload_filenames }}
|
||||
|
||||
verify:
|
||||
name: Verify uploaded package
|
||||
needs: [checks, upload]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.checks.outputs.version }}
|
||||
EXPECTED_SHA256: ${{ needs.upload.outputs.pkg_sha256 }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download release and verify checksum
|
||||
run: |
|
||||
RELEASE_DIR="${RELEASE_PREFIX}/v${VERSION}"
|
||||
PKG_URL="${BASE_URL}/${RELEASE_DIR}/fleet_desktop-v${VERSION}.pkg"
|
||||
|
||||
: # Retry to ride out CDN/object propagation right after upload.
|
||||
downloaded=false
|
||||
for attempt in $(seq 1 10); do
|
||||
if curl -fsSL -o downloaded.pkg "$PKG_URL"; then
|
||||
downloaded=true
|
||||
break
|
||||
fi
|
||||
echo "Attempt $attempt: $PKG_URL not available yet, retrying in 30s..."
|
||||
sleep 30
|
||||
done
|
||||
if [ "$downloaded" != "true" ]; then
|
||||
echo "::error::Could not download $PKG_URL after 10 attempts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
actual_sha256=$(shasum -a 256 downloaded.pkg | cut -d ' ' -f 1)
|
||||
echo "Expected SHA256: $EXPECTED_SHA256"
|
||||
echo "Actual SHA256: $actual_sha256"
|
||||
if [ "$actual_sha256" != "$EXPECTED_SHA256" ]; then
|
||||
echo "::error::Checksum mismatch for $PKG_URL."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -fsS -o meta.json "${BASE_URL}/${RELEASE_DIR}/meta.json"
|
||||
if [ "$(jq -r '.fleet_desktop_pkg_sha256' meta.json)" != "$EXPECTED_SHA256" ]; then
|
||||
echo "::error::meta.json sha256 does not match the built package."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Write release summary
|
||||
run: |
|
||||
RELEASE_DIR="${RELEASE_PREFIX}/v${VERSION}"
|
||||
{
|
||||
echo "## Fleet Desktop (macOS) v${VERSION} released"
|
||||
echo ""
|
||||
echo "- Package: ${BASE_URL}/${RELEASE_DIR}/fleet_desktop-v${VERSION}.pkg"
|
||||
echo "- meta.json: ${BASE_URL}/${RELEASE_DIR}/meta.json"
|
||||
echo "- SHA256: \`${EXPECTED_SHA256}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
Reference in New Issue
Block a user