Files
iccery-v2-mac/.gitea/workflows/macos.yml
gronod 14cd544e05
macOS CI / build-and-test (pull_request) Successful in 38m2s
macOS CI / package (pull_request) Skipped
macOS CI / build-and-test (push) Failing after 37m37s
macOS CI / package (push) Skipped
feat(#189): release-driven versioning — tag-stamped About + monotonic build
- scripts/version.sh resolves ICCERY_RELEASE_TAG / MARKETING_VERSION /
  CURRENT_PROJECT_VERSION from RELEASE_TAG env or `git describe`; tag
  builds hard-fail when the tag's X.Y.Z != project.yml MARKETING_VERSION
- build number = `git rev-list --count HEAD` (Apple: macOS
  CFBundleVersion must monotonically increase, no per-version reset)
- tag ships as a bundled ICCeryReleaseTag resource — a generated
  Info.plist can't carry custom keys (INFOPLIST_KEY_* allowlist, and
  ProcessInfoPlistFile runs after script phases); About shows
  "tag (marketing)", e.g. v2.0.0-pre2-grok (2.0.0)
- DMG named ICCery-<tag>-<build>.dmg for tagged/described builds
- CI twins: fetch-depth 0 + RELEASE_TAG env + stamped test builds
2026-09-15 13:10:19 +01:00

260 lines
11 KiB
YAML

name: macOS CI
on:
push:
branches:
- develop
tags:
- 'v*'
pull_request:
branches:
- develop
jobs:
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-12
env:
DERIVED: build/DerivedData-test
# Tag name on tag pushes, branch name otherwise — scripts/version.sh
# only honours values matching 'v[0-9]*'.
RELEASE_TAG: ${{ github.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history + tags: scripts/version.sh needs `git describe` and
# `git rev-list --count`; the default depth-1 clone has neither.
fetch-depth: 0
- name: Assert Xcode 14+ toolchain
run: |
line="$(xcodebuild -version | head -1)"
major="$(printf '%s' "$line" | sed -n 's/^Xcode \([0-9][0-9]*\)\..*/\1/p')"
if [ -z "$major" ] || [ "$major" -lt 14 ]; then
echo "Unexpected Xcode version: $line" >&2; exit 1
fi
echo "$line"
# Tag pushes whose name contains "prerelease" skip the test build and both
# test legs: they exist to package a build already validated elsewhere.
# The job still succeeds quickly so `package`'s `needs:` stays satisfied.
# Homebrew's xcodegen formula requires Xcode 15.3, which cannot be
# installed on macOS 12 (#109). The script installs a pinned
# prebuilt release instead. dmgbuild is not installed here — the
# test job does not package (#95).
- name: Ensure host tools
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: scripts/ensure-host-tools.sh
- name: Generate Xcode project
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: xcodegen generate --spec project.yml
# Tests only ever run on the runner's own architecture; build
# just that slice. Packaging (scripts/package-release.sh) still
# produces the universal Release binary.
- name: Build for testing (host arch)
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: |
# Stamp the test build with the same version triple packaging uses
# so CI-built apps are traceable (and mis-tags fail early) (#189).
eval "$(scripts/version.sh)"
echo "version=$MARKETING_VERSION build=$CURRENT_PROJECT_VERSION tag=$ICCERY_RELEASE_TAG"
xcodebuild build-for-testing \
-scheme ICCery \
-destination 'platform=macOS' \
-derivedDataPath "$DERIVED" \
-configuration Debug \
ARCHS="$(uname -m)" \
ONLY_ACTIVE_ARCH=NO \
CODE_SIGNING_ALLOWED=YES \
CODE_SIGN_IDENTITY='-' \
MARKETING_VERSION="$MARKETING_VERSION" \
CURRENT_PROJECT_VERSION="$CURRENT_PROJECT_VERSION" \
ICCERY_RELEASE_TAG="$ICCERY_RELEASE_TAG"
# Xcode embeds the shared ICCeryCore package framework into the app
# and the test bundle without signing it. Ad-hoc hosts still require
# every loaded dylib to carry a cdhash — dyld killed the test host at
# launch (run 31992) — so sign every embedded copy once the build is
# done (embed steps run after any build script phase) (#119).
- name: Sign package product frameworks
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: |
find "$DERIVED/Build/Products/Debug" -depth -name '*_PackageProduct.framework' -print0 \
| while IFS= read -r -d '' fw; do
echo "signing $fw"
codesign --force --sign - --timestamp=none "$fw"
done
- name: Test unit (ICCeryCoreTests)
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: |
XCTESTRUN="$(find "$DERIVED" -name 'ICCery*.xctestrun' | head -n 1)"
if [ -z "$XCTESTRUN" ] || [ ! -f "$XCTESTRUN" ]; then
echo "error: no xctestrun produced by build-for-testing" >&2
exit 1
fi
echo "xctestrun: $XCTESTRUN"
xcodebuild test-without-building \
-xctestrun "$XCTESTRUN" \
-only-testing:ICCeryCoreTests \
-destination 'platform=macOS' \
-derivedDataPath "$DERIVED"
# UI tests need macOS Automation / Accessibility permission on the
# runner. The self-hosted Mac intermittently times out enabling
# that mode (run 29700) or launches the app into
# `.runningBackground` without ever activating it (run 29804).
# Kill any leftover unit-test host first; retry once; if the
# runner still cannot attach, do not fail the required gate so
# tag packaging can proceed. Real XCTest assertion failures
# still fail the job.
- name: Test UI (ICCeryUITests)
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: |
set -o pipefail
XCTESTRUN="$(find "$DERIVED" -name 'ICCery*.xctestrun' | head -n 1)"
LOG="$DERIVED/ui-test.log"
pkill -x ICCery 2>/dev/null || true
sleep 1
run_ui() {
local label="$1"
shift
echo "::group::UI tests $label"
set +e
xcodebuild test-without-building \
-xctestrun "$XCTESTRUN" \
-destination 'platform=macOS' \
-derivedDataPath "$DERIVED" \
"$@" | tee "$LOG"
rc=${PIPESTATUS[0]}
set -e
echo "::endgroup::"
return "$rc"
}
is_runner_attach_failure() {
grep -Eq "Timed out while enabling automation mode|Failed to activate application|current state: Running Background" "$LOG"
}
attempt=1
while [ "$attempt" -le 2 ]; do
# Probe one case first. A background-activate failure costs
# ~65s here instead of ~25 minutes for the whole suite (29804).
if ! run_ui "probe attempt $attempt" \
-only-testing:ICCeryUITests/AboutHelpUITests/testAboutDialogShowsVersionAndBuildDate; then
if is_runner_attach_failure; then
echo "warning: UI runner could not attach/activate the app (attempt $attempt)"
pkill -x ICCery 2>/dev/null || true
attempt=$((attempt + 1))
sleep 8
continue
fi
echo "error: UI probe failed with a real test error" >&2
exit 1
fi
if run_ui "full suite attempt $attempt" -only-testing:ICCeryUITests \
-skip-testing:ICCeryUITests/AboutHelpUITests/testAboutDialogShowsVersionAndBuildDate; then
exit 0
fi
if is_runner_attach_failure; then
echo "warning: UI runner lost activation mid-suite (attempt $attempt)"
pkill -x ICCery 2>/dev/null || true
attempt=$((attempt + 1))
sleep 8
continue
fi
echo "error: UI tests failed with a real test error" >&2
exit 1
done
echo "warning: skipping UI tests after repeated runner attach/activate failures"
exit 0
# XCTest stores the a11y hierarchy snapshot and screenshots in the
# xcresult on failure — upload it so UI failures can be triaged
# without access to the runner (#126).
- name: Prepare Node CA bundle (failure path)
if: "failure() && !(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: |
NODE_CA_FILE="/tmp/macos-ca-bundle.pem"
security find-certificate -a -p \
/System/Library/Keychains/SystemRootCertificates.keychain \
/Library/Keychains/System.keychain \
> "$NODE_CA_FILE" 2>/dev/null || true
if [ ! -s "$NODE_CA_FILE" ] && [ -f /etc/ssl/cert.pem ]; then
cp /etc/ssl/cert.pem "$NODE_CA_FILE"
fi
- name: Upload UI test xcresult
if: "failure() && !(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
uses: actions/upload-artifact@v3
env:
NODE_EXTRA_CA_CERTS: /tmp/macos-ca-bundle.pem
with:
name: ui-test-xcresult
path: build/DerivedData-test/Logs/Test
package:
needs: build-and-test
runs-on: macos-12
if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history + tags: scripts/version.sh needs `git describe` and
# `git rev-list --count`; the default depth-1 clone has neither.
fetch-depth: 0
# scripts/package-release.sh runs `xcodegen generate` and dmgbuild;
# see build-and-test for why brew is not used on macOS 12 (#109).
# INSTALL_DMGBUILD isolates dmgbuild in build/.venv-dmgbuild so
# the test job never pip-installs it (#95).
- name: Ensure host tools
run: INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh
- name: Package release
run: scripts/package-release.sh
env:
RELEASE_TAG: ${{ github.ref_name }}
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: Prepare Node CA bundle
run: |
NODE_CA_FILE="/tmp/macos-ca-bundle.pem"
security find-certificate -a -p \
/System/Library/Keychains/SystemRootCertificates.keychain \
/Library/Keychains/System.keychain \
> "$NODE_CA_FILE" 2>/dev/null || true
if [ ! -s "$NODE_CA_FILE" ] && [ -f /etc/ssl/cert.pem ]; then
cp /etc/ssl/cert.pem "$NODE_CA_FILE"
fi
- name: Upload DMG artifact
uses: actions/upload-artifact@v3
env:
NODE_EXTRA_CA_CERTS: /tmp/macos-ca-bundle.pem
with:
name: iccery-dmg
path: ICCery-*.dmg
- name: Attach DMG to Gitea release
if: startsWith(github.ref, 'refs/tags/v')
run: scripts/attach-release-asset.sh
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITHUB_TOKEN: ${{ github.token }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REF_NAME: ${{ github.ref_name }}
GITHUB_SHA: ${{ github.sha }}