From a0d795583dbbbb80a30593e311a91649dcb09be4 Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Fri, 24 Jul 2026 13:14:58 -0400 Subject: [PATCH] Add fleet-desktop-macos release workflow (#49903) **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 ## 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. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../workflows/fleet-desktop-macos-build.yml | 26 +- .../workflows/release-fleet-desktop-macos.yml | 262 ++++++++++++++++++ apps/fleet-desktop-macos/README.md | 16 ++ 3 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-fleet-desktop-macos.yml diff --git a/.github/workflows/fleet-desktop-macos-build.yml b/.github/workflows/fleet-desktop-macos-build.yml index cf4d7bbc8c..e7a17ae730 100644 --- a/.github/workflows/fleet-desktop-macos-build.yml +++ b/.github/workflows/fleet-desktop-macos-build.yml @@ -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: diff --git a/.github/workflows/release-fleet-desktop-macos.yml b/.github/workflows/release-fleet-desktop-macos.yml new file mode 100644 index 0000000000..9b8feacf5b --- /dev/null +++ b/.github/workflows/release-fleet-desktop-macos.yml @@ -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.. 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/fleet_desktop-v.pkg +# - fleet-desktop-macos/v/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..." + 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" diff --git a/apps/fleet-desktop-macos/README.md b/apps/fleet-desktop-macos/README.md index f4d72b43e1..76bb3e6f8e 100644 --- a/apps/fleet-desktop-macos/README.md +++ b/apps/fleet-desktop-macos/README.md @@ -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 .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/`. 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 + git push origin fleet-desktop-macos-v + ``` +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).