diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1f747c..7c4e5c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,11 +104,11 @@ jobs: xcode_components: '"MetalToolchain"' disk_size: 380 - macos_version: tahoe - xcode_versions: '"26.5","27-beta","26.4.1","26.3","26.2","26.1.1","26.0.1"' + xcode_versions: '"26.6","27-beta-2","26.5","27-beta","26.4.1","26.3","26.2","26.1.1","26.0.1"' additional_ios_builds: "18.6" additional_tvos_builds: "" xcode_components: '"MetalToolchain"' - disk_size: 330 + disk_size: 520 env: ADDITIONAL_IOS_BUILDS: ${{ matrix.additional_ios_builds }} ADDITIONAL_TVOS_BUILDS: ${{ matrix.additional_tvos_builds }} diff --git a/.github/workflows/template-validation.yml b/.github/workflows/template-validation.yml new file mode 100644 index 0000000..eb58199 --- /dev/null +++ b/.github/workflows/template-validation.yml @@ -0,0 +1,380 @@ +name: Template Builds + +on: + pull_request: + paths: + - ".github/workflows/monthly.yml" + - ".github/workflows/release.yml" + - ".github/workflows/template-validation.yml" + - "data/**" + - "scripts/**" + - "templates/**" + +permissions: + contents: read + packages: read + +concurrency: + group: template-builds-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} + FASTLANE_USER: ${{ secrets.FASTLANE_USER }} + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_INSTALL_CLEANUP: 1 + PACKER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TART_REGISTRY_HOSTNAME: ghcr.io + TART_REGISTRY_USERNAME: ${{ github.actor }} + TART_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + +jobs: + packer-validate: + name: Packer Validate + runs-on: macos-15 + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Packer + uses: hashicorp/setup-packer@v3 + + - name: Install validation dependencies + run: | + brew install ansible + + - name: Prepare validation inputs + run: | + mkdir -p "$HOME/XcodesCache" + touch "$HOME/XcodesCache/Xcode_26.6.xip" + + - name: Validate templates + run: | + set -euo pipefail + + validate() { + local template="$1" + shift + + packer init "$template" + packer validate "$@" "$template" + } + + for template in templates/vanilla-*.pkr.hcl; do + validate "$template" + done + + validate templates/base.pkr.hcl \ + -var vm_name=template-validation-base + + validate templates/disable-sip.pkr.hcl \ + -var vm_name=template-validation-disable-sip + + validate templates/disable-sip-with-username.pkr.hcl \ + -var vm_name=template-validation-disable-sip-user + + validate templates/exex-script.pkr.hcl \ + -var vm_name=template-validation-exec \ + -var script_path=scripts/finalize-tahoe.sh + + validate templates/resolve-macos-number.pkr.hcl \ + -var vm_base_name=template-validation-base \ + -var vm_name=template-validation-resolve \ + -var resolve_file=macos-version.txt + + validate templates/xcode.pkr.hcl \ + -var macos_version=tahoe \ + -var 'xcode_version=["26.6"]' \ + -var expected_runtimes_file=data/expected.tahoe.runtimes.txt + + build-vanilla: + name: Build Vanilla Image (${{ matrix.macos_version }}) + needs: packer-validate + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: [self-hosted, macOS, ARM64] + timeout-minutes: 180 + strategy: + fail-fast: false + max-parallel: 1 + matrix: + macos_version: + - tahoe + - sequoia + - sonoma + - monterey + env: + MACOS_VERSION: ${{ matrix.macos_version }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Select image + id: select + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + + git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt + + build=false + if grep -Fxq "templates/vanilla-$MACOS_VERSION.pkr.hcl" changed-files.txt; then + build=true + fi + + echo "build=$build" >> "$GITHUB_OUTPUT" + + - name: Tool versions + if: steps.select.outputs.build == 'true' + run: | + tart --version + packer --version + + - name: Build vanilla image + if: steps.select.outputs.build == 'true' + run: | + packer init "templates/vanilla-$MACOS_VERSION.pkr.hcl" + packer build "templates/vanilla-$MACOS_VERSION.pkr.hcl" + + - name: Cleanup + if: always() && steps.select.outputs.build == 'true' + run: | + tart delete "$MACOS_VERSION-vanilla" || true + + build-base: + name: Build Base Image (${{ matrix.macos_version }}) + needs: packer-validate + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: [self-hosted, macOS, ARM64] + timeout-minutes: 180 + strategy: + fail-fast: false + max-parallel: 1 + matrix: + include: + - macos_version: sonoma + disable_sip_template: disable-sip.pkr.hcl + - macos_version: sequoia + disable_sip_template: disable-sip-with-username.pkr.hcl + - macos_version: tahoe + disable_sip_template: disable-sip-with-username.pkr.hcl + env: + DISABLE_SIP_TEMPLATE: ${{ matrix.disable_sip_template }} + MACOS_VERSION: ${{ matrix.macos_version }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Select image + id: select + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + + git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt + + build=false + if grep -Eq '^(templates/base\.pkr\.hcl|templates/disable-sip.*\.pkr\.hcl|data/(github_known_hosts|limit\.maxfiles\.plist|setup-info-template\.json|tart-guest-.*\.plist)|scripts/install-actions-runner\.sh|ansible/)' changed-files.txt; then + build=true + fi + + echo "build=$build" >> "$GITHUB_OUTPUT" + + - name: Tool versions + if: steps.select.outputs.build == 'true' + run: | + tart --version + packer --version + + - name: Pull vanilla image + if: steps.select.outputs.build == 'true' + run: | + tart pull "ghcr.io/cirruslabs/macos-$MACOS_VERSION-vanilla:latest" + tart clone "ghcr.io/cirruslabs/macos-$MACOS_VERSION-vanilla:latest" "$MACOS_VERSION-base" + + - name: Disable SIP + if: steps.select.outputs.build == 'true' + run: | + packer init "templates/$DISABLE_SIP_TEMPLATE" + packer build -var "vm_name=$MACOS_VERSION-base" "templates/$DISABLE_SIP_TEMPLATE" + + - name: Build base image + if: steps.select.outputs.build == 'true' + run: | + packer init templates/base.pkr.hcl + packer build -var "vm_name=$MACOS_VERSION-base" templates/base.pkr.hcl + + - name: Cleanup + if: always() && steps.select.outputs.build == 'true' + run: | + tart delete "$MACOS_VERSION-base" || true + + build-runner: + name: Build Runner Image (${{ matrix.macos_version }}) + needs: packer-validate + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: [self-hosted, macOS, ARM64] + timeout-minutes: 180 + strategy: + fail-fast: false + max-parallel: 1 + matrix: + include: + - macos_version: tahoe + xcode_versions: '"26.6","27-beta-2","26.5","27-beta","26.4.1","26.3","26.2","26.1.1","26.0.1"' + additional_ios_builds: "18.6" + additional_tvos_builds: "" + xcode_components: '"MetalToolchain"' + disk_size: 520 + - macos_version: sequoia + xcode_versions: '"26.0.1",16.4,16.3,16.2,16.1,16' + additional_ios_builds: "18.5,18.4,18.2,17.5" + additional_tvos_builds: "17.5" + xcode_components: '"MetalToolchain"' + disk_size: 380 + env: + ADDITIONAL_IOS_BUILDS: ${{ matrix.additional_ios_builds }} + ADDITIONAL_TVOS_BUILDS: ${{ matrix.additional_tvos_builds }} + DISK_SIZE: ${{ matrix.disk_size }} + MACOS_VERSION: ${{ matrix.macos_version }} + XCODE_COMPONENTS: ${{ matrix.xcode_components }} + XCODE_VERSIONS: ${{ matrix.xcode_versions }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Select image + id: select + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + + git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt + + build=false + if grep -Fxq ".github/workflows/release.yml" changed-files.txt; then + build=true + elif grep -Fxq "data/expected.$MACOS_VERSION.runtimes.txt" changed-files.txt; then + build=true + elif grep -Fxq "scripts/finalize-$MACOS_VERSION.sh" changed-files.txt; then + build=true + elif grep -Eq '^(templates/xcode\.pkr\.hcl|data/setup-info-template\.json|scripts/install-actions-runner\.sh)$' changed-files.txt; then + build=true + fi + + echo "build=$build" >> "$GITHUB_OUTPUT" + + - name: Tool versions + if: steps.select.outputs.build == 'true' + run: | + tart --version + packer --version + + - name: Pull base image + if: steps.select.outputs.build == 'true' + run: | + tart pull "ghcr.io/cirruslabs/macos-$MACOS_VERSION-base:latest" + + - name: Prepare Xcode archives + if: steps.select.outputs.build == 'true' + run: | + set -euo pipefail + + source ~/.zprofile || true + + if ! command -v xcodes >/dev/null; then + brew install xcodes + fi + + mkdir -p "$HOME/XcodesCache" + IFS=',' read -ra versions <<< "$XCODE_VERSIONS" + + for raw_version in "${versions[@]}"; do + version="${raw_version//\"/}" + target="$HOME/XcodesCache/Xcode_${version}.xip" + + if [[ -f "$target" ]]; then + echo "Using cached Xcode $version at $target" + continue + fi + + echo "Downloading Xcode $version" + if [[ -z "${FASTLANE_SESSION:-}" ]]; then + echo "::error::Missing $target and FASTLANE_SESSION is not configured. Pre-cache the Xcode archive on the runner or add Apple Developer auth secrets." + exit 1 + fi + + download_args=(download "$version" --directory "$HOME/XcodesCache" --use-fastlane-auth) + if [[ -n "${FASTLANE_USER:-}" ]]; then + download_args+=(--fastlane-user "$FASTLANE_USER") + fi + xcodes "${download_args[@]}" + + candidate="" + case "$version" in + 27-beta-2) + candidate="$HOME/XcodesCache/Xcode_27_beta_2.xip" + ;; + 27-beta) + candidate="$HOME/XcodesCache/Xcode_27_beta.xip" + ;; + *) + candidate="$(find "$HOME/XcodesCache" -maxdepth 1 -type f -name "Xcode_${version}*.xip" -print -quit)" + ;; + esac + + if [[ -n "$candidate" && -f "$candidate" && "$candidate" != "$target" ]]; then + mv "$candidate" "$target" + fi + + test -f "$target" + done + + - name: Build runner image + if: steps.select.outputs.build == 'true' + run: | + packer init templates/xcode.pkr.hcl + packer build \ + -var tag=runner \ + -var "disk_size=$DISK_SIZE" \ + -var disk_free_mb=100000 \ + -var "macos_version=$MACOS_VERSION" \ + -var "xcode_version=[$XCODE_VERSIONS]" \ + -var "additional_ios_builds=[$ADDITIONAL_IOS_BUILDS]" \ + -var "additional_tvos_builds=[$ADDITIONAL_TVOS_BUILDS]" \ + -var "xcode_components=[$XCODE_COMPONENTS]" \ + -var "expected_runtimes_file=data/expected.$MACOS_VERSION.runtimes.txt" \ + templates/xcode.pkr.hcl + + - name: Finalize runner image + if: steps.select.outputs.build == 'true' + run: | + if [[ -f "scripts/finalize-$MACOS_VERSION.sh" ]]; then + packer build \ + -var "vm_name=$MACOS_VERSION-xcode:runner" \ + -var "script_path=scripts/finalize-$MACOS_VERSION.sh" \ + templates/exex-script.pkr.hcl + else + echo "Skipping prepare script for $MACOS_VERSION" + fi + + - name: Cleanup + if: always() && steps.select.outputs.build == 'true' + run: | + tart delete "$MACOS_VERSION-xcode:runner" || true diff --git a/data/expected.tahoe.runtimes.txt b/data/expected.tahoe.runtimes.txt index f127beb..1f2ea35 100644 --- a/data/expected.tahoe.runtimes.txt +++ b/data/expected.tahoe.runtimes.txt @@ -5,12 +5,11 @@ iOS 26.1 (26.1 - 23B86) - com.apple.CoreSimulator.SimRuntime.iOS-26-1 iOS 26.3 (26.3.1 - 23D8133) - com.apple.CoreSimulator.SimRuntime.iOS-26-3 iOS 26.4 (26.4.1 - 23E254a) - com.apple.CoreSimulator.SimRuntime.iOS-26-4 iOS 26.5 (26.5 - 23F77) - com.apple.CoreSimulator.SimRuntime.iOS-26-5 -tvOS 26.2 (26.2 - 23K51) - com.apple.CoreSimulator.SimRuntime.tvOS-26-2 -tvOS 26.4 (26.4 - 23L243a) - com.apple.CoreSimulator.SimRuntime.tvOS-26-4 +iOS 27.0 (27.0 - 24A5355p) - com.apple.CoreSimulator.SimRuntime.iOS-27-0 +iOS 27.0 (27.0 - 24A5370g) - com.apple.CoreSimulator.SimRuntime.iOS-27-0 tvOS 26.5 (26.5 - 23L470) - com.apple.CoreSimulator.SimRuntime.tvOS-26-5 -watchOS 26.2 (26.2 - 23S303) - com.apple.CoreSimulator.SimRuntime.watchOS-26-2 -watchOS 26.4 (26.4 - 23T240b) - com.apple.CoreSimulator.SimRuntime.watchOS-26-4 +tvOS 27.0 (27.0 - 24J5305f) - com.apple.CoreSimulator.SimRuntime.tvOS-27-0 watchOS 26.5 (26.5 - 23T570) - com.apple.CoreSimulator.SimRuntime.watchOS-26-5 -visionOS 26.2 (26.2 - 23N301) - com.apple.CoreSimulator.SimRuntime.xrOS-26-2 -visionOS 26.4 (26.4.1 - 23O249a) - com.apple.CoreSimulator.SimRuntime.xrOS-26-4 +watchOS 27.0 (27.0 - 24R5305f) - com.apple.CoreSimulator.SimRuntime.watchOS-27-0 visionOS 26.5 (26.5 - 23O470) - com.apple.CoreSimulator.SimRuntime.xrOS-26-5 +visionOS 27.0 (27.0 - 24M5306g) - com.apple.CoreSimulator.SimRuntime.xrOS-27-0 diff --git a/templates/xcode.pkr.hcl b/templates/xcode.pkr.hcl index 01ef7c5..f1f1990 100644 --- a/templates/xcode.pkr.hcl +++ b/templates/xcode.pkr.hcl @@ -16,39 +16,39 @@ variable "xcode_version" { } variable "additional_ios_builds" { - type = list(string) + type = list(string) default = [] } variable "additional_tvos_builds" { - type = list(string) + type = list(string) default = [] } variable "xcode_components" { - type = list(string) - default = [] + type = list(string) + default = [] description = "Additional Xcode components to download." } variable "expected_runtimes_file" { - type = string - default = "" + type = string + default = "" description = "Path to file containing expected simulator runtimes. If empty, runtime verification is skipped." } variable "tag" { - type = string + type = string default = "" } variable "disk_size" { - type = number + type = number default = 140 } variable "disk_free_mb" { - type = number + type = number default = 15000 } @@ -146,7 +146,7 @@ build { } provisioner "file" { - sources = [ for version in var.xcode_version : pathexpand("~/XcodesCache/Xcode_${version}.xip")] + sources = [for version in var.xcode_version : pathexpand("~/XcodesCache/Xcode_${version}.xip")] destination = "/Users/admin/Downloads/" } @@ -161,7 +161,7 @@ build { // select the latest one as the default dynamic "provisioner" { for_each = local.xcode_install_provisioners - labels = ["shell"] + labels = ["shell"] content { inline = provisioner.value.inline } @@ -169,11 +169,11 @@ build { dynamic "provisioner" { for_each = length(var.xcode_version) > 2 ? [2] : [] - labels = ["shell"] + labels = ["shell"] content { inline = [ "source ~/.zprofile", - "sudo xcodes select '${var.xcode_version[2]}'", + "sudo xcode-select -s /Applications/Xcode_${var.xcode_version[2]}.app/Contents/Developer", "xcodebuild -downloadAllPlatforms", ] } @@ -181,11 +181,11 @@ build { dynamic "provisioner" { for_each = length(var.xcode_version) > 1 ? [1] : [] - labels = ["shell"] + labels = ["shell"] content { inline = [ "source ~/.zprofile", - "sudo xcodes select '${var.xcode_version[1]}'", + "sudo xcode-select -s /Applications/Xcode_${var.xcode_version[1]}.app/Contents/Developer", "xcodebuild -downloadAllPlatforms", ] } @@ -194,7 +194,7 @@ build { provisioner "shell" { inline = [ "source ~/.zprofile", - "sudo xcodes select '${var.xcode_version[0]}'", + "sudo xcode-select -s /Applications/Xcode_${var.xcode_version[0]}.app/Contents/Developer", "xcodebuild -downloadAllPlatforms", ] } @@ -232,8 +232,9 @@ build { "brew install libimobiledevice ideviceinstaller ios-deploy carthage", "brew install xcbeautify swiftformat swiftlint swiftgen licenseplist", "brew install mint", - "brew tap tuist/tuist", - "brew install --formula tuist", + "git clone --depth 1 https://github.com/tuist/homebrew-tuist.git \"$(brew --repository)/Library/Taps/tuist/homebrew-tuist\"", + "rm -rf \"$(brew --repository)/Library/Taps/tuist/homebrew-tuist/Casks\"", + "tuist_version=$(ruby -ne 'if $_ =~ %r{/download/([^/]+)/}; puts $1; exit; end' \"$(brew --repository)/Library/Taps/tuist/homebrew-tuist/Aliases/tuist\") && brew trust --formula \"tuist/tuist/tuist@$tuist_version\" && brew install --formula \"tuist/tuist/tuist@$tuist_version\"", "rbenv install 3.3.10", "rbenv global 3.3.10", # fastlane conflicts with 3.4.0+ https://github.com/fastlane/fastlane/issues/29527 "gem update", @@ -247,7 +248,7 @@ build { // Copy expected runtimes file if provided dynamic "provisioner" { for_each = var.expected_runtimes_file != "" ? [1] : [] - labels = ["file"] + labels = ["file"] content { source = var.expected_runtimes_file destination = "/Users/admin/runtimes.expected.txt" @@ -257,7 +258,7 @@ build { // Verify simulator runtimes match expected list if file was provided dynamic "provisioner" { for_each = var.expected_runtimes_file != "" ? [1] : [] - labels = ["shell"] + labels = ["shell"] content { inline = [ "source ~/.zprofile",