Compare commits

...
4 Commits
Author SHA1 Message Date
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> d40f0052d2 ci(macos): restore workflow, fix test signing and packaging (#32)
macOS CI / build-and-test (push) Successful in 10m54s
macOS CI / package (push) Failing after 5m2s
- Keep CI active on develop (push + PR) and tags v*.
- Generate the project with `xcodegen generate --spec project.yml`.
- build-for-testing / test-without-building share one derivedDataPath,
  destination, universal archs, and ad-hoc signing flags. The XCTest
  runner is never re-signed with --options runtime.
- Package job runs package-release.sh after tests pass and uploads the
  ICCery-*.dmg the script actually writes.
- README documents the env vars the pipeline reads.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 09:01:43 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 80408dd682 fix(packaging): use fixed derived-data path, correct sign/verify flow
package-release.sh previously parsed -showBuildSettings for the app path,
which could disagree with the actual build. Now use a single
derivedDataPath and locate Release/ICCery.app directly. Add Developer ID
signing with --timestamp and a post-sign verify, while leaving sidecars
ad-hoc. Re-verify sidecars after app signing and re-ad-hoc any that were
stripped. Install dmgbuild in a venv and use DMG_APP env.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 09:01:43 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 2ffcabbd52 fix(packaging): stage the built .app into the DMG (#32)
dmgbuild never set `files`, so the volume mounted empty. Export DMG_APP
from package-release.sh and require it in dmgbuild-settings.py, copying
the bundle by its source path. Keep the Applications symlink and the
optional background fallback.

Verified by mounting a test image and seeing ICCery.app + Applications.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 09:01:43 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 85f0de631c fix(packaging): make sidecar sign/verify recursive (#32, #165)
The Argyll tree inside the bundle is nested (macos-universal/, mocks/),
so a top-level-only scan left real binaries unverified. Scan recursively
under Contents/Resources/Argyll, hard-fail on any unsigned Mach-O or a
missing/empty sidecar dir, and sign fetched tools recursively too.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 09:01:43 +01:00
7 changed files with 150 additions and 75 deletions
+21 -23
View File
@@ -4,52 +4,50 @@ on:
push: push:
branches: branches:
- develop - develop
tags:
- 'v*'
pull_request: pull_request:
branches: branches:
- develop - develop
jobs: jobs:
build-and-test: build-and-test:
# Prefer a self-hosted Mac runner if your Gitea has one. If not,
# macos-14 works for this pipeline.
runs-on: macos-14 runs-on: macos-14
env:
DERIVED: build/DerivedData-test
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Fetch Argyll sidecars
run: scripts/fetch-argyll.sh
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
- name: Generate Xcode project - name: Generate Xcode project
run: xcodegen generate --project . run: xcodegen generate --spec project.yml
- name: Build and test (universal) - name: Build for testing (universal)
run: | run: |
xcodebuild build-for-testing \ xcodebuild build-for-testing \
-scheme ICCery \ -scheme ICCery \
-destination 'platform=macOS' \ -destination 'platform=macOS' \
-derivedDataPath "$DERIVED" \
-configuration Debug \
ARCHS='arm64 x86_64' \ ARCHS='arm64 x86_64' \
ONLY_ACTIVE_ARCH=NO \ ONLY_ACTIVE_ARCH=NO \
CODE_SIGNING_ALLOWED=YES \ CODE_SIGNING_ALLOWED=YES \
CODE_SIGN_IDENTITY='-' \ CODE_SIGN_IDENTITY='-'
CODE_SIGNING_REQUIRED=YES
BUILT_PRODUCTS_DIR="$(xcodebuild -scheme ICCery -destination 'platform=macOS' -showBuildSettings | sed -n 's/^ *BUILT_PRODUCTS_DIR = //p' | head -n 1)" - name: Test (universal)
if [ -d "$BUILT_PRODUCTS_DIR/ICCery.app" ]; then run: |
xattr -cr "$BUILT_PRODUCTS_DIR/ICCery.app" XCTESTRUN="$(find "$DERIVED" -name 'ICCery*.xctestrun' | head -n 1)"
codesign -f -s - --options runtime \ if [ -z "$XCTESTRUN" ] || [ ! -f "$XCTESTRUN" ]; then
--entitlements Resources/ICCery.entitlements \ echo "error: no xctestrun produced by build-for-testing" >&2
"$BUILT_PRODUCTS_DIR/ICCery.app" exit 1
fi fi
echo "xctestrun: $XCTESTRUN"
xcodebuild test \ xcodebuild test-without-building \
-scheme ICCery \ -xctestrun "$XCTESTRUN" \
-destination 'platform=macOS' \ -destination 'platform=macOS' \
ARCHS='arm64 x86_64' \ -derivedDataPath "$DERIVED"
ONLY_ACTIVE_ARCH=NO \
CODE_SIGNING_ALLOWED=YES \
CODE_SIGN_IDENTITY='-' \
CODE_SIGNING_REQUIRED=YES
package: package:
needs: build-and-test needs: build-and-test
+1
View File
@@ -26,3 +26,4 @@ ICCery.xcodeproj/
*.zip *.zip
Release/ Release/
notarization/ notarization/
build/
+22
View File
@@ -73,6 +73,28 @@ export GITEA_TOKEN=… # private releases
Do not open the generated xcodeproj as the source of truth. Edit `project.yml` and regenerate. Do not open the generated xcodeproj as the source of truth. Edit `project.yml` and regenerate.
## Release packaging
```bash
scripts/package-release.sh # fetch → sign → universal build → verify → DMG
```
The script builds with a fixed derived data path (`build/DerivedData`),
locates `Release/ICCery.app` from it, signs the bundle, recursively verifies
every bundled Mach-O sidecar (`scripts/verify-sidecar-signatures.sh`), and
writes `ICCery-${VERSION}-${BUILD_NUM}.dmg` via `dmgbuild`. Sidecars stay
ad-hoc signed inside the bundle — the app is never `codesign --deep`ed.
Environment variables read by the pipeline:
| Variable | Purpose |
|---|---|
| `GITEA_TOKEN` | private `gronod/argyllcms` release downloads |
| `ARGYLL_SERVER_URL` / `ARGYLL_REPO` / `ARGYLL_RELEASE_TAG` | sidecar release override |
| `CODESIGN_IDENTITY` | Developer ID identity for the outer `.app`; unset or `-` = ad-hoc |
| `DEVELOPMENT_TEAM` | team ID passed to `xcodebuild` when signing |
| `NOTARIZE_APPLE_ID` / `NOTARIZE_PASSWORD` / `APPLE_TEAM_ID` | `notarytool` + staple when all three are set |
## Layout ## Layout
``` ```
+16 -3
View File
@@ -1,15 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# scripts/dmgbuild-settings.py # scripts/dmgbuild-settings.py
# #
# dmgbuild settings for ICCery. Set DMG_FILENAME and DMG_VOLUME_NAME in the # dmgbuild settings for ICCery. Set DMG_APP, DMG_FILENAME and DMG_VOLUME_NAME
# environment, or accept the defaults. Background art can be supplied later by # in the environment, or accept the defaults. Background art can be supplied
# placing a PNG at Resources/dmg-background.png and setting DMG_BACKGROUND. # later by placing a PNG at Resources/dmg-background.png and setting
# DMG_BACKGROUND.
import os import os
import sys
filename = os.environ.get('DMG_FILENAME', 'ICCery.dmg') filename = os.environ.get('DMG_FILENAME', 'ICCery.dmg')
volume_name = os.environ.get('DMG_VOLUME_NAME', 'ICCery') volume_name = os.environ.get('DMG_VOLUME_NAME', 'ICCery')
# The built .app must be staged into the image. Without this the DMG mounts
# empty (#32). DMG_APP is exported by scripts/package-release.sh.
app_path = os.environ.get('DMG_APP', '')
if not app_path or not app_path.endswith('.app') or not os.path.isdir(app_path):
sys.stderr.write(
'error: DMG_APP must point at an existing .app bundle '
'(got %r)\n' % app_path)
sys.exit(1)
files = [app_path]
# Background art is optional. If the referenced PNG does not exist, fall back # 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. # to a plain window. See docs/23-assets.md for the DMG background spec.
background = os.environ.get('DMG_BACKGROUND', 'Resources/dmg-background.png') background = os.environ.get('DMG_BACKGROUND', 'Resources/dmg-background.png')
+14 -13
View File
@@ -111,22 +111,23 @@ find "$DEST" -type f -exec chmod 0755 {} +
xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true
# Ad-hoc sign every Mach-O (#165: unsigned arm64 → "Killed: 9"), then # Ad-hoc sign every Mach-O (#165: unsigned arm64 → "Killed: 9"), then
# verify — an unsigned sidecar fails the script. # verify — an unsigned sidecar fails the script. The tree may nest
for f in "$DEST"/*; do # (e.g. platform subdirs), so scan recursively.
[ -f "$f" ] || continue find "$DEST" -type f -exec sh -c \
if file -b "$f" | grep -q 'Mach-O'; then 'for p do file -b "$p" | grep -q "Mach-O" && codesign -f -s - "$p"; done' \
codesign -f -s - "$f" 2>/dev/null || true _ {} + 2>/dev/null || true
fi
done
UNSIGNED="" UNSIGNED=""
for f in "$DEST"/*; do while IFS= read -r f; do
[ -f "$f" ] || continue [ -f "$f" ] || continue
if file -b "$f" | grep -q 'Mach-O'; then if ! codesign -dvv "$f" >/dev/null 2>&1; then
if ! codesign -dvv "$f" >/dev/null 2>&1; then UNSIGNED="$UNSIGNED $f"
UNSIGNED="$UNSIGNED $f"
fi
fi fi
done done <<EOF
$(find "$DEST" -type f -exec sh -c \
'for p do file -b "$p" | grep -q "Mach-O" && printf "%s\n" "$p"; done' \
_ {} +)
EOF
if [ -n "$UNSIGNED" ]; then if [ -n "$UNSIGNED" ]; then
echo "error: unsigned binaries remain:$UNSIGNED" >&2 echo "error: unsigned binaries remain:$UNSIGNED" >&2
exit 1 exit 1
+55 -26
View File
@@ -6,7 +6,7 @@
# Steps: # Steps:
# 1. Fetch and ad-hoc sign Argyll sidecars (scripts/fetch-argyll.sh). # 1. Fetch and ad-hoc sign Argyll sidecars (scripts/fetch-argyll.sh).
# 2. Generate the Xcode project from project.yml. # 2. Generate the Xcode project from project.yml.
# 3. Build a universal Release ICCery.app. # 3. Build a universal Release ICCery.app with a fixed derived data path.
# 4. Sign the .app (Developer ID if CODESIGN_IDENTITY is set, else ad-hoc). # 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. # 5. Hard-fail verify every bundled Mach-O sidecar with codesign -dvv.
# 6. Build a DMG with dmgbuild. # 6. Build a DMG with dmgbuild.
@@ -24,16 +24,24 @@ set -eu
ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)" ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
cd "$ROOT" cd "$ROOT"
if [ ! -f project.yml ]; then
echo "error: project.yml not found in $ROOT" >&2
exit 1
fi
DERIVED="$ROOT/build/DerivedData"
mkdir -p "$DERIVED"
# Fetch sidecars first because the Xcode project copies them into the bundle.
echo "==> Fetching Argyll sidecars" echo "==> Fetching Argyll sidecars"
scripts/fetch-argyll.sh scripts/fetch-argyll.sh
echo "==> Generating Xcode project" echo "==> Generating Xcode project"
xcodegen generate --project . xcodegen generate --spec project.yml
CONFIG="Release" CONFIG="Release"
DEST="platform=macOS" DEST="platform=macOS"
# Default to ad-hoc signing. A real Developer ID can be injected via env.
IDENTITY="${CODESIGN_IDENTITY:--}" IDENTITY="${CODESIGN_IDENTITY:--}"
DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM:-}" DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM:-}"
@@ -43,11 +51,11 @@ if [ -n "$DEVELOPMENT_TEAM" ]; then
BUILD_EXTRA="DEVELOPMENT_TEAM=$DEVELOPMENT_TEAM" BUILD_EXTRA="DEVELOPMENT_TEAM=$DEVELOPMENT_TEAM"
fi fi
# shellcheck disable=SC2086
xcodebuild \ xcodebuild \
-scheme ICCery \ -scheme ICCery \
-destination "$DEST" \ -destination "$DEST" \
-configuration "$CONFIG" \ -configuration "$CONFIG" \
-derivedDataPath "$DERIVED" \
ARCHS='arm64 x86_64' \ ARCHS='arm64 x86_64' \
ONLY_ACTIVE_ARCH=NO \ ONLY_ACTIVE_ARCH=NO \
CODE_SIGNING_ALLOWED=YES \ CODE_SIGNING_ALLOWED=YES \
@@ -55,48 +63,69 @@ xcodebuild \
$BUILD_EXTRA \ $BUILD_EXTRA \
build build
echo "==> Locating built app" APP="$DERIVED/Build/Products/$CONFIG/ICCery.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 if [ ! -d "$APP" ]; then
echo "error: built app not found at $APP" >&2 echo "error: built app not found at $APP" >&2
exit 1 exit 1
fi fi
echo "App: $APP" echo "App: $APP"
# If a Developer ID identity was supplied, re-sign the .app bundle. Sidecars # Sidecars are copied into the bundle by the build phase. Re-signing the .app
# live in Resources/Argyll and remain ad-hoc signed by fetch-argyll.sh. # with --force should keep them intact, but verify after and repair any that
# got stripped. Do not codesign --deep the bundle with Developer ID — sidecars
# stay ad-hoc.
if [ -n "${CODESIGN_IDENTITY:-}" ] && [ "$CODESIGN_IDENTITY" != "-" ]; then if [ -n "${CODESIGN_IDENTITY:-}" ] && [ "$CODESIGN_IDENTITY" != "-" ]; then
echo "==> Signing $APP with '$CODESIGN_IDENTITY'" echo "==> Signing $APP with '$CODESIGN_IDENTITY'"
codesign --force --sign "$CODESIGN_IDENTITY" \ codesign --force --sign "$CODESIGN_IDENTITY" \
--entitlements Resources/ICCery.entitlements \ --entitlements Resources/ICCery.entitlements \
--options runtime \ --options runtime \
--timestamp \
--verbose \
"$APP" "$APP"
else
echo "==> App ad-hoc signed by xcodebuild; not re-signing" echo "==> Verifying app signature"
codesign --verify --deep --strict --verbose=2 "$APP"
fi fi
echo "==> Verifying sidecar signatures" echo "==> Verifying sidecar signatures"
scripts/verify-sidecar-signatures.sh "$APP" if ! scripts/verify-sidecar-signatures.sh "$APP"; then
echo "==> Re-applying ad-hoc signature to sidecars"
while IFS= read -r f; do
[ -f "$f" ] || continue
codesign -f -s - --options runtime "$f" 2>/dev/null || true
done <<EOF
$(find "$APP/Contents/Resources/Argyll" -type f -exec sh -c \
'for p do file -b "$p" | grep -q "Mach-O" && printf "%s\n" "$p"; done' \
_ {} +)
EOF
echo "==> Re-verifying sidecar signatures"
scripts/verify-sidecar-signatures.sh "$APP"
fi
echo "==> Installing / locating dmgbuild"
if ! command -v dmgbuild >/dev/null 2>&1; then
VENV="$ROOT/build/.venv-dmgbuild"
if [ ! -d "$VENV/bin" ]; then
python3 -m venv "$VENV"
"$VENV/bin/pip" install --upgrade pip
"$VENV/bin/pip" install dmgbuild
fi
PATH="$VENV/bin:$PATH"
export PATH
fi
if ! command -v dmgbuild >/dev/null 2>&1; then
echo "error: dmgbuild not available. Try 'python3 -m venv .venv && pip install dmgbuild'" >&2
exit 1
fi
echo "==> Building DMG" echo "==> Building DMG"
VERSION="$(plutil -extract CFBundleShortVersionString raw "$APP/Contents/Info.plist" 2>/dev/null || echo '2.0.0')" VERSION="$(plutil -extract CFBundleShortVersionString raw "$APP/Contents/Info.plist" 2>/dev/null || printf '2.0.0')"
BUILD_NUM="$(plutil -extract CFBundleVersion raw "$APP/Contents/Info.plist" 2>/dev/null || echo '1')" BUILD_NUM="$(plutil -extract CFBundleVersion raw "$APP/Contents/Info.plist" 2>/dev/null || printf '1')"
DMG="ICCery-${VERSION}-${BUILD_NUM}.dmg" DMG="ICCery-${VERSION}-${BUILD_NUM}.dmg"
VOLUME_NAME="ICCery ${VERSION}" VOLUME_NAME="ICCery ${VERSION}"
if ! command -v dmgbuild >/dev/null 2>&1; then DMG_APP="$APP" \
echo "==> Installing dmgbuild"
pip3 install dmgbuild
fi
DMG_FILENAME="$DMG" \ DMG_FILENAME="$DMG" \
DMG_VOLUME_NAME="$VOLUME_NAME" \ DMG_VOLUME_NAME="$VOLUME_NAME" \
dmgbuild -s scripts/dmgbuild-settings.py "$VOLUME_NAME" "$DMG" dmgbuild -s scripts/dmgbuild-settings.py "$VOLUME_NAME" "$DMG"
+21 -10
View File
@@ -2,8 +2,10 @@
# scripts/verify-sidecar-signatures.sh # scripts/verify-sidecar-signatures.sh
# #
# Hard-fail check that every Mach-O Argyll sidecar shipped inside the built # 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 # ICCery.app bundle is signed (ad-hoc or Developer ID). The Argyll tree is
# xcodebuild and before packaging. # nested (Vendor/Argyll/macos-universal/… is rsynced into Resources/Argyll by
# the project.yml post-build script), so this scan is recursive — #165 applies
# to binaries at any depth, including mocks/.
# #
# Usage: scripts/verify-sidecar-signatures.sh <path/to/ICCery.app> # Usage: scripts/verify-sidecar-signatures.sh <path/to/ICCery.app>
@@ -26,20 +28,29 @@ if [ ! -d "$SIDECAR_DIR" ]; then
exit 1 exit 1
fi fi
MACHO_COUNT=0
UNSIGNED="" UNSIGNED=""
for f in "$SIDECAR_DIR"/*; do while IFS= read -r f; do
[ -f "$f" ] || continue [ -f "$f" ] || continue
if file -b "$f" | grep -q 'Mach-O'; then MACHO_COUNT=$((MACHO_COUNT + 1))
if ! codesign -dvv "$f" >/dev/null 2>&1; then if ! codesign -dvv "$f" >/dev/null 2>&1; then
echo "error: unsigned Mach-O sidecar: $f" >&2 echo "error: unsigned Mach-O sidecar: $f" >&2
UNSIGNED="$UNSIGNED $f" UNSIGNED="$UNSIGNED $f"
fi
fi fi
done done <<EOF
$(find "$SIDECAR_DIR" -type f -exec sh -c \
'for p do file -b "$p" | grep -q "Mach-O" && printf "%s\n" "$p"; done' \
_ {} +)
EOF
if [ -n "$UNSIGNED" ]; then if [ -n "$UNSIGNED" ]; then
echo "error: unsigned Argyll sidecars remain:$UNSIGNED" >&2 echo "error: unsigned Argyll sidecars remain:$UNSIGNED" >&2
exit 1 exit 1
fi fi
echo "OK: all Mach-O sidecars in $SIDECAR_DIR are signed" if [ "$MACHO_COUNT" -eq 0 ]; then
echo "error: no Mach-O sidecars found under $SIDECAR_DIR" >&2
exit 1
fi
echo "OK: all $MACHO_COUNT Mach-O sidecars under $SIDECAR_DIR are signed"