Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 68 additions & 69 deletions .github/workflows/distribute-macos-store.yml
Original file line number Diff line number Diff line change
@@ -1,40 +1,39 @@
name: Distribute (macOS)
# ── App Store Distribution ───────────────────────────────────────────────────
# Builds, signs, and uploads MacPacker to App Store Connect (TestFlight).
# Triggered by version tags or manual dispatch.
# ─────────────────────────────────────────────────────────────────────────────
name: Distribute App Store (macOS)

# Disabled while testing distribute-macos-external workflow
# on:
# push:
# tags:
# - 'v*'
on:
push:
tags: ['v*.*.*']
workflow_dispatch:

permissions:
contents: read

# ── Project identity ────────────────────────────────────────────────────────
# Hardcoded here because MacPacker uses a plain .xcodeproj (no Tuist/Project.swift).
# All other macOS projects that adopt this workflow and use Tuist should replace
# these with the dynamic extraction step shown in the iOS workflow.
env:
BUNDLE_ID: "com.sarensx.MacPacker" # main app
BUNDLE_ID_FINDER: "com.sarensx.MacPacker.FinderExtension" # Finder Extension
BUNDLE_ID_QUICKLOOK: "com.sarensx.MacPacker.QuickLookExtension" # Quick Look Extension
PROJECT_NAME: "MacPacker" # must match .xcodeproj scheme name
BUNDLE_ID: "com.sarensx.MacPacker"
BUNDLE_ID_FINDER: "com.sarensx.MacPacker.FinderExtension"
BUNDLE_ID_QUICKLOOK: "com.sarensx.MacPacker.QuickLookExtension"
PROJECT_NAME: "MacPacker Store"
# ────────────────────────────────────────────────────────────────────────────

jobs:
distribute:
runs-on: [self-hosted]
runs-on: [macos-26]
timeout-minutes: 60

steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0

# ── Resolve Swift package dependencies ─────────────────────────────────
# Required because MacPacker embeds a local Swift package. xcodebuild will
# resolve the SPM graph before archiving, but doing it explicitly here
# surfaces dependency errors early and keeps the archive step clean.
- name: Resolve Swift package dependencies
shell: bash
run: |
Expand All @@ -43,21 +42,26 @@ jobs:
-scheme "$PROJECT_NAME" \
-configuration Release

- name: Sync version and build number from tag
# ── Version & build number ─────────────────────────────────────────────
- name: Sync version and build number
shell: bash
run: |
set -euo pipefail

# Strip the leading "v" from the tag → "v1.2" becomes "1.2"
VERSION=${GITHUB_REF_NAME#v}
BUILD_NUMBER=$(date +%y%m%d%H%M%S)
BUILD_NUMBER=$(date -u +%y%m%d%H%M%S)

# Marketing version from Config/Version.xcconfig (single source of truth)
VERSION=$(grep 'MARKETING_VERSION' Config/Version.xcconfig | sed 's/.*= *//')

echo "Version: $VERSION"
echo "Build number: $BUILD_NUMBER"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV

agvtool new-marketing-version "$VERSION"
agvtool new-version -all "$BUILD_NUMBER"

# ── Keychain ───────────────────────────────────────────────────────────
- name: Set up keychain
shell: bash
env:
Expand All @@ -82,35 +86,34 @@ jobs:

rm /tmp/certificate.p12

# ── Provisioning profiles ──────────────────────────────────────────────
- name: Install provisioning profiles
shell: bash
env:
PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
PROVISIONING_PROFILE_FINDER_BASE64: ${{ secrets.PROVISIONING_PROFILE_FINDER_BASE64 }}
PROVISIONING_PROFILE_QUICKLOOK_BASE64: ${{ secrets.PROVISIONING_PROFILE_QUICKLOOK_BASE64 }}
STORE_PROFILE_BASE64: ${{ secrets.PROV_PROF_STORE_BASE64 }}
STORE_PROFILE_FINDER_BASE64: ${{ secrets.PROV_PROF_STORE_FINDER_BASE64 }}
STORE_PROFILE_QUICKLOOK_BASE64: ${{ secrets.PROV_PROF_STORE_QL_BASE64 }}
run: |
set -euo pipefail

PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILE_DIR"

# Decode all three profiles
echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode \
> "$PROFILE_DIR/macpacker.provisionprofile"
echo "$PROVISIONING_PROFILE_FINDER_BASE64" | base64 --decode \
> "$PROFILE_DIR/macpacker-finder.provisionprofile"
echo "$PROVISIONING_PROFILE_QUICKLOOK_BASE64" | base64 --decode \
> "$PROFILE_DIR/macpacker-quicklook.provisionprofile"
echo "$STORE_PROFILE_BASE64" | base64 --decode \
> "$PROFILE_DIR/macpacker-store.provisionprofile"
echo "$STORE_PROFILE_FINDER_BASE64" | base64 --decode \
> "$PROFILE_DIR/macpacker-store-finder.provisionprofile"
echo "$STORE_PROFILE_QUICKLOOK_BASE64" | base64 --decode \
> "$PROFILE_DIR/macpacker-store-quicklook.provisionprofile"

# Extract profile names for use in ExportOptions.plist
PROFILE_NAME=$(security cms -D \
-i "$PROFILE_DIR/macpacker.provisionprofile" \
-i "$PROFILE_DIR/macpacker-store.provisionprofile" \
| plutil -extract Name raw -)
PROFILE_NAME_FINDER=$(security cms -D \
-i "$PROFILE_DIR/macpacker-finder.provisionprofile" \
-i "$PROFILE_DIR/macpacker-store-finder.provisionprofile" \
| plutil -extract Name raw -)
PROFILE_NAME_QUICKLOOK=$(security cms -D \
-i "$PROFILE_DIR/macpacker-quicklook.provisionprofile" \
-i "$PROFILE_DIR/macpacker-store-quicklook.provisionprofile" \
| plutil -extract Name raw -)

echo "PROFILE_NAME=$PROFILE_NAME" >> $GITHUB_ENV
Expand All @@ -121,44 +124,45 @@ jobs:
echo "Finder Extension profile: $PROFILE_NAME_FINDER"
echo "Quick Look Extension profile: $PROFILE_NAME_QUICKLOOK"

- name: Archive
# ── App Store Connect API key ──────────────────────────────────────────
- name: Install App Store Connect API key
shell: bash
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_KEY_BASE64: ${{ secrets.ASC_KEY_BASE64 }}
run: |
set -euo pipefail
mkdir -p ~/.appstoreconnect/private_keys
echo "$ASC_KEY_BASE64" | base64 --decode \
> ~/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8

# Extract UUIDs for all three profiles — xcodebuild needs UUIDs at
# archive time, profile names are only used in ExportOptions.plist
PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"

PROFILE_UUID=$(security cms -D \
-i "$PROFILE_DIR/macpacker.provisionprofile" \
| plutil -extract UUID raw -)
PROFILE_UUID_FINDER=$(security cms -D \
-i "$PROFILE_DIR/macpacker-finder.provisionprofile" \
| plutil -extract UUID raw -)
PROFILE_UUID_QUICKLOOK=$(security cms -D \
-i "$PROFILE_DIR/macpacker-quicklook.provisionprofile" \
| plutil -extract UUID raw -)
# ── Archive ────────────────────────────────────────────────────────────
- name: Archive
shell: bash
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
run: |
set -euo pipefail

xcodebuild archive \
-scheme "$PROJECT_NAME" \
-configuration Release \
-archivePath /tmp/build/app.xcarchive \
-destination "generic/platform=macOS" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Distribution" \
PROVISIONING_PROFILE_SPECIFIER="$PROFILE_UUID" \
PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]:com.sarensx.MacPacker.FinderExtension="$PROFILE_UUID_FINDER" \
PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]:com.sarensx.MacPacker.QuickLookExtension="$PROFILE_UUID_QUICKLOOK"

MARKETING_VERSION=$VERSION \
CURRENT_PROJECT_VERSION=$BUILD_NUMBER \
-allowProvisioningUpdates \
-authenticationKeyPath ~/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8 \
-authenticationKeyID "$ASC_KEY_ID" \
-authenticationKeyIssuerID "$ASC_ISSUER_ID"

# ── Export for App Store ───────────────────────────────────────────────
- name: Export for App Store
shell: bash
run: |
set -euo pipefail

# ExportOptions.plist maps each bundle ID to its provisioning profile name.
# All three targets must be listed or the export will fall back to
# automatic signing and likely fail.
cat > /tmp/ExportOptions.plist << PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
Expand Down Expand Up @@ -188,20 +192,15 @@ jobs:
-exportPath /tmp/build/export \
-exportOptionsPlist /tmp/ExportOptions.plist

# ── Upload to TestFlight ───────────────────────────────────────────────
- name: Upload to TestFlight
shell: bash
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_BASE64: ${{ secrets.ASC_KEY_BASE64 }}
run: |
set -euo pipefail

mkdir -p ~/.appstoreconnect/private_keys
echo "$ASC_KEY_BASE64" | base64 --decode \
> ~/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8

# macOS export produces a .pkg, not an .ipa
OUTPUT=$(xcrun altool --upload-app \
--type macos \
--file /tmp/build/export/*.pkg \
Expand All @@ -210,23 +209,23 @@ jobs:

echo "$OUTPUT"

rm -f ~/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8

if echo "$OUTPUT" | grep -q "UPLOAD FAILED"; then
echo "ERROR: Upload to TestFlight failed"
exit 1
fi

# ── Cleanup ────────────────────────────────────────────────────────────
- name: Cleanup
if: always()
shell: bash
run: |
set -euo pipefail

security delete-keychain build.keychain || true
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/macpacker.provisionprofile"
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/macpacker-finder.provisionprofile"
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/macpacker-quicklook.provisionprofile"
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/macpacker-store.provisionprofile"
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/macpacker-store-finder.provisionprofile"
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/macpacker-store-quicklook.provisionprofile"
rm -rf /tmp/build
rm -f /tmp/ExportOptions.plist
security list-keychains -d user -s login.keychain
rm -f ~/.appstoreconnect/private_keys/AuthKey_*.p8
security list-keychains -d user -s login.keychain
11 changes: 7 additions & 4 deletions .github/workflows/distribute-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
build-number: ${{ steps.version.outputs.build-number }}
artifact-label: ${{ steps.version.outputs.artifact-label }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Determine version info
id: version
shell: bash
Expand All @@ -30,12 +33,12 @@ jobs:
# Timestamp-based build number in UTC (always increasing)
BUILD_NUMBER=$(date -u +%y%m%d%H%M%S)

# Extract version from tag: v1.3.0 -> 1.3.0
VERSION="${GITHUB_REF_NAME#v}"
# Marketing version from Config/Version.xcconfig (single source of truth)
VERSION=$(grep 'MARKETING_VERSION' Config/Version.xcconfig | sed 's/.*= *//')

# Validate semver format
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Tag '${GITHUB_REF_NAME}' does not contain a valid semver version"
echo "::error::Version '${VERSION}' from Version.xcconfig is not a valid semver version"
exit 1
fi

Expand All @@ -46,7 +49,7 @@ jobs:
echo "artifact-label=${ARTIFACT_LABEL}" >> $GITHUB_OUTPUT

echo "Release build"
echo " Marketing version: ${VERSION}"
echo " Marketing version: ${VERSION} (from Version.xcconfig)"
echo " Build number: ${BUILD_NUMBER}"
echo " Artifact label: ${ARTIFACT_LABEL}"

Expand Down
17 changes: 6 additions & 11 deletions .github/workflows/distribute-snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
pull_request:
types: [closed]
branches: ['main']
workflow_dispatch:

permissions:
contents: read
Expand All @@ -26,8 +27,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # need full history for git describe

- name: Determine version info
id: version
Expand All @@ -38,21 +37,17 @@ jobs:
# Timestamp-based build number in UTC (always increasing)
BUILD_NUMBER=$(date -u +%y%m%d%H%M%S)

# Marketing version from the latest v*.*.* tag
if VERSION_TAG=$(git describe --tags --abbrev=0 --match 'v*.*.*' 2>/dev/null); then
VERSION="${VERSION_TAG#v}"
else
VERSION="0.0.0"
fi
# Marketing version from Config/Version.xcconfig (single source of truth)
VERSION=$(grep 'MARKETING_VERSION' Config/Version.xcconfig | sed 's/.*= *//')

ARTIFACT_LABEL="build-${BUILD_NUMBER}"
ARTIFACT_LABEL="${VERSION}-beta-${BUILD_NUMBER}"

echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "build-number=${BUILD_NUMBER}" >> $GITHUB_OUTPUT
echo "artifact-label=${ARTIFACT_LABEL}" >> $GITHUB_OUTPUT

echo "Snapshot build"
echo " Marketing version: ${VERSION} (from last tag)"
echo " Marketing version: ${VERSION} (from Version.xcconfig)"
echo " Build number: ${BUILD_NUMBER}"
echo " Artifact label: ${ARTIFACT_LABEL}"

Expand Down Expand Up @@ -89,7 +84,7 @@ jobs:
`| DMG | [MacPacker-${label}.dmg](${baseUrl}/MacPacker-${label}.dmg) |`,
`| ZIP | [MacPacker-${label}.zip](${baseUrl}/MacPacker-${label}.zip) |`,
``,
`Build number: \`${{ needs.prepare.outputs.build-number }}\``,
`Version: \`${{ needs.prepare.outputs.version }}\` · Build: \`${{ needs.prepare.outputs.build-number }}\``,
].join('\n');

await github.rest.issues.createComment({
Expand Down
2 changes: 2 additions & 0 deletions Config/Signing.xcconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include "Version.xcconfig"

// Signing configuration for MacPacker
//
// To build with your own Apple Development Team:
Expand Down
3 changes: 3 additions & 0 deletions Config/Version.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Marketing version — single source of truth
// Update this before starting work on a new release
MARKETING_VERSION = 0.15.0
Loading