From 9a50057845f2a4d2b9824f6fce4fbd59c035e80f Mon Sep 17 00:00:00 2001 From: Gronod Date: Wed, 9 Sep 2026 22:56:02 +0100 Subject: [PATCH] feat(#32): packaging CI, signed release build, and DMG workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Gitea Actions macOS CI on a self-hosted runner. - Fetch Argyll sidecars before build. - Universal xcodebuild test with ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO. - CODE_SIGNING_ALLOWED=NO for local agent debug/test builds. - Optional release packaging job on milestone and version tags. - Add scripts/package-release.sh: - fetch-argyll → generate project → Release universal build. - Sign .app with CODESIGN_IDENTITY (Developer ID) or ad-hoc. - Re-verify every bundled Argyll Mach-O sidecar with codesign -dvv. - Build DMG with dmgbuild and optional notarize/staple. - Add scripts/verify-sidecar-signatures.sh for a hard-fail sidecar check. - Add scripts/dmgbuild-settings.py with optional background art support. - Update .gitignore to exclude *.dmg, *.zip, Release/, and notarization/. Refs #32 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .gitea/workflows/macos.yml | 57 +++++++++++++ .gitignore | 6 ++ scripts/dmgbuild-settings.py | 40 +++++++++ scripts/package-release.sh | 120 +++++++++++++++++++++++++++ scripts/verify-sidecar-signatures.sh | 45 ++++++++++ 5 files changed, 268 insertions(+) create mode 100644 .gitea/workflows/macos.yml create mode 100755 scripts/dmgbuild-settings.py create mode 100755 scripts/package-release.sh create mode 100755 scripts/verify-sidecar-signatures.sh diff --git a/.gitea/workflows/macos.yml b/.gitea/workflows/macos.yml new file mode 100644 index 0000000..35c3af7 --- /dev/null +++ b/.gitea/workflows/macos.yml @@ -0,0 +1,57 @@ +name: macOS CI + +on: + push: + branches: + - '**' + pull_request: + branches: + - 'milestone/m6-gamut-stage0-cgats-release' + +jobs: + build-and-test: + runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Fetch Argyll sidecars + run: scripts/fetch-argyll.sh + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + + - name: Generate Xcode project + run: xcodegen generate --project . + + - name: Build and test (universal) + run: | + xcodebuild test \ + -scheme ICCery \ + -destination 'platform=macOS' \ + ARCHS='arm64 x86_64' \ + ONLY_ACTIVE_ARCH=NO \ + CODE_SIGNING_ALLOWED=NO + + package: + needs: build-and-test + runs-on: self-hosted + if: github.ref == 'refs/heads/milestone/m6-gamut-stage0-cgats-release' || startsWith(github.ref, 'refs/tags/v') + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Package release + run: scripts/package-release.sh + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }} + DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }} + NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }} + NOTARIZE_PASSWORD: ${{ secrets.NOTARIZE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + + - name: Upload DMG artifact + uses: actions/upload-artifact@v4 + with: + name: iccery-dmg + path: ICCery-*.dmg diff --git a/.gitignore b/.gitignore index d131153..570b6c3 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,9 @@ ICCery.xcodeproj/ # macOS .DS_Store + +# Release artefacts (not git blobs) +*.dmg +*.zip +Release/ +notarization/ diff --git a/scripts/dmgbuild-settings.py b/scripts/dmgbuild-settings.py new file mode 100755 index 0000000..9bc33b5 --- /dev/null +++ b/scripts/dmgbuild-settings.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# scripts/dmgbuild-settings.py +# +# dmgbuild settings for ICCery. Set DMG_FILENAME and DMG_VOLUME_NAME in the +# environment, or accept the defaults. Background art can be supplied later by +# placing a PNG at Resources/dmg-background.png and setting DMG_BACKGROUND. + +import os + +filename = os.environ.get('DMG_FILENAME', 'ICCery.dmg') +volume_name = os.environ.get('DMG_VOLUME_NAME', 'ICCery') + +# Background art is optional. If the referenced PNG does not exist, fall back +# to a plain window. See docs/23-assets.md for the DMG background spec. +background = os.environ.get('DMG_BACKGROUND', 'Resources/dmg-background.png') +if background and not os.path.exists(background): + background = None + +icon = None + +# Window size is enough for the app icon and the Applications alias. +window_rect = ((100, 100), (640, 480)) + +# Use icon view without extra chrome. +default_view = 'icon-view' +show_status_bar = False +show_tab_view = False +show_toolbar = False +show_pathbar = False +show_sidebar = False +sidebar_width = 180 + +# Position the .app on the left and the Applications alias on the right. +icon_locations = { + 'ICCery.app': (140, 240), + 'Applications': (500, 240), +} + +# Symlink to /Applications for drag-and-drop install. +symlinks = {'Applications': '/Applications'} diff --git a/scripts/package-release.sh b/scripts/package-release.sh new file mode 100755 index 0000000..66f372f --- /dev/null +++ b/scripts/package-release.sh @@ -0,0 +1,120 @@ +#!/bin/sh +# scripts/package-release.sh +# +# Release packaging pipeline for ICCery v2 macOS. +# +# Steps: +# 1. Fetch and ad-hoc sign Argyll sidecars (scripts/fetch-argyll.sh). +# 2. Generate the Xcode project from project.yml. +# 3. Build a universal Release ICCery.app. +# 4. Sign the .app (Developer ID if CODESIGN_IDENTITY is set, else ad-hoc). +# 5. Hard-fail verify every bundled Mach-O sidecar with codesign -dvv. +# 6. Build a DMG with dmgbuild. +# 7. Optionally notarize and staple the DMG when notarization secrets exist. +# +# Required secrets (optional): +# CODESIGN_IDENTITY Developer ID Application identity name +# DEVELOPMENT_TEAM Apple development team ID (for xcodebuild signing) +# NOTARIZE_APPLE_ID Apple ID for notarytool +# NOTARIZE_PASSWORD App-specific password for notarytool +# APPLE_TEAM_ID Team ID for notarytool + +set -eu + +ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)" +cd "$ROOT" + +echo "==> Fetching Argyll sidecars" +scripts/fetch-argyll.sh + +echo "==> Generating Xcode project" +xcodegen generate --project . + +CONFIG="Release" +DEST="platform=macOS" + +# Default to ad-hoc signing. A real Developer ID can be injected via env. +IDENTITY="${CODESIGN_IDENTITY:--}" +DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM:-}" + +echo "==> Building universal Release app" +BUILD_EXTRA="" +if [ -n "$DEVELOPMENT_TEAM" ]; then + BUILD_EXTRA="DEVELOPMENT_TEAM=$DEVELOPMENT_TEAM" +fi + +# shellcheck disable=SC2086 +xcodebuild \ + -scheme ICCery \ + -destination "$DEST" \ + -configuration "$CONFIG" \ + ARCHS='arm64 x86_64' \ + ONLY_ACTIVE_ARCH=NO \ + CODE_SIGNING_ALLOWED=YES \ + CODE_SIGN_IDENTITY="$IDENTITY" \ + $BUILD_EXTRA \ + build + +echo "==> Locating built app" +BUILT_PRODUCTS_DIR="$(xcodebuild \ + -scheme ICCery \ + -destination "$DEST" \ + -configuration "$CONFIG" \ + -showBuildSettings \ + | sed -n 's/^ *BUILT_PRODUCTS_DIR = //p' \ + | head -n 1)" + +APP="$BUILT_PRODUCTS_DIR/ICCery.app" +if [ ! -d "$APP" ]; then + echo "error: built app not found at $APP" >&2 + exit 1 +fi +echo "App: $APP" + +# If a Developer ID identity was supplied, re-sign the .app bundle. Sidecars +# live in Resources/Argyll and remain ad-hoc signed by fetch-argyll.sh. +if [ -n "${CODESIGN_IDENTITY:-}" ] && [ "$CODESIGN_IDENTITY" != "-" ]; then + echo "==> Signing $APP with '$CODESIGN_IDENTITY'" + codesign --force --sign "$CODESIGN_IDENTITY" \ + --entitlements Resources/ICCery.entitlements \ + --options runtime \ + "$APP" +else + echo "==> App ad-hoc signed by xcodebuild; not re-signing" +fi + +echo "==> Verifying sidecar signatures" +scripts/verify-sidecar-signatures.sh "$APP" + +echo "==> Building DMG" +VERSION="$(plutil -extract CFBundleShortVersionString raw "$APP/Contents/Info.plist" 2>/dev/null || echo '2.0.0')" +BUILD_NUM="$(plutil -extract CFBundleVersion raw "$APP/Contents/Info.plist" 2>/dev/null || echo '1')" +DMG="ICCery-${VERSION}-${BUILD_NUM}.dmg" +VOLUME_NAME="ICCery ${VERSION}" + +if ! command -v dmgbuild >/dev/null 2>&1; then + echo "==> Installing dmgbuild" + pip3 install dmgbuild +fi + +DMG_FILENAME="$DMG" \ +DMG_VOLUME_NAME="$VOLUME_NAME" \ +dmgbuild -s scripts/dmgbuild-settings.py "$VOLUME_NAME" "$DMG" + +echo "DMG: $PWD/$DMG" + +# Optional notarization/stapling when credentials are present. +if [ -n "${NOTARIZE_APPLE_ID:-}" ] && \ + [ -n "${NOTARIZE_PASSWORD:-}" ] && \ + [ -n "${APPLE_TEAM_ID:-}" ]; then + echo "==> Submitting $DMG for notarization" + xcrun notarytool submit "$DMG" \ + --apple-id "$NOTARIZE_APPLE_ID" \ + --password "$NOTARIZE_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + xcrun stapler staple "$DMG" + echo "==> Stapled $DMG" +else + echo "==> Notarization credentials not set; skipping" +fi diff --git a/scripts/verify-sidecar-signatures.sh b/scripts/verify-sidecar-signatures.sh new file mode 100755 index 0000000..e553af8 --- /dev/null +++ b/scripts/verify-sidecar-signatures.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# scripts/verify-sidecar-signatures.sh +# +# Hard-fail check that every Mach-O Argyll sidecar shipped inside the built +# ICCery.app bundle is signed (ad-hoc or Developer ID). Run this in CI after +# xcodebuild and before packaging. +# +# Usage: scripts/verify-sidecar-signatures.sh + +set -eu + +APP="${1:-}" +if [ -z "$APP" ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +if [ ! -d "$APP" ]; then + echo "error: app bundle not found: $APP" >&2 + exit 1 +fi + +SIDECAR_DIR="$APP/Contents/Resources/Argyll" +if [ ! -d "$SIDECAR_DIR" ]; then + echo "error: Argyll sidecar directory not found: $SIDECAR_DIR" >&2 + exit 1 +fi + +UNSIGNED="" +for f in "$SIDECAR_DIR"/*; do + [ -f "$f" ] || continue + if file -b "$f" | grep -q 'Mach-O'; then + if ! codesign -dvv "$f" >/dev/null 2>&1; then + echo "error: unsigned Mach-O sidecar: $f" >&2 + UNSIGNED="$UNSIGNED $f" + fi + fi +done + +if [ -n "$UNSIGNED" ]; then + echo "error: unsigned Argyll sidecars remain:$UNSIGNED" >&2 + exit 1 +fi + +echo "OK: all Mach-O sidecars in $SIDECAR_DIR are signed" -- 2.39.5