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:
Jordan Montgomery
2026-07-24 13:14:58 -04:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 09fea47ce4
commit a0d795583d
3 changed files with 303 additions and 1 deletions
@@ -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"
+16
View File
@@ -244,6 +244,22 @@ Under Fleet's Apple Developer team (`8VBZ3948LU`, the team that owns the pinned
Re-encode and update the secrets when a profile expires or the signing certificate is rotated. To inspect a profile — its entitlements and, crucially, the certs it authorizes — dump it with `security cms -D -i <profile>.provisionprofile`; the `DeveloperCertificates` array must contain the CI signing cert above.
## Releasing
[`.github/workflows/release-fleet-desktop-macos.yml`](../../.github/workflows/release-fleet-desktop-macos.yml) publishes a tagged, signed, notarized build to `https://download.fleetdm.com/fleet-desktop-macos/v<version>/`. Releases are immutable — a version that already exists on download.fleetdm.com cannot be overwritten. No GitHub Release is created; the git tag is the release marker.
1. Bump `CFBundleShortVersionString` (and `CFBundleVersion`) in `FleetDesktop/Info.plist` and merge to `main`.
2. Tag the commit and push the tag:
```bash
git tag fleet-desktop-macos-v<version>
git push origin fleet-desktop-macos-v<version>
```
3. In the Actions tab, run **Release Fleet Desktop (macOS)**, selecting the tag in the "Use workflow from" dropdown.
The workflow fails before building if the selected ref isn't a `fleet-desktop-macos-v*` tag on `main`, if the tag version doesn't match `Info.plist`, or if that version is already uploaded. It builds via the CI workflow above, uploads the pkg plus a `meta.json` (`version`, `fleet_desktop_pkg_sha256`, `fleet_desktop_pkg_url`), then downloads the pkg back from the public URL and verifies its SHA256 before succeeding. The checksum and URLs are written to the run summary.
The `testing` input uploads to `download-testing.fleetdm.com` instead of production — use it for the first run after changing the workflow.
## License
Licensed under the MIT Expat license via the repository [root LICENSE](../LICENSE).