diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..be62d98 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,13 @@ +name: 'Test Preternatural Build Action' +on: + pull_request: + branches: [ main ] + +jobs: + test-swift-package: + runs-on: ghcr.io/cirruslabs/macos-runner:sonoma + steps: + - name: Test Swift Package Basic Functionality + uses: tahabebek/MockPackage/BuildAction@main + with: + path: ${{ github.sha }} diff --git a/README.md b/README.md index 3fe4357..4bae161 100644 --- a/README.md +++ b/README.md @@ -2,53 +2,31 @@ This repository contains GitHub Actions for use with [Preternatural CLI](https://github.com/PreternaturalAI/CLI-release). +## Actions -## Preternatural Build Action - -This action allows you to run the Preternatural CLI build command on your repositories with support for a specific Xcode version. - -### Inputs - -- `xcode-version`: (Required) Xcode version to use. -- `derived_data_path`: (Optional) The path to the derived data folder. -- `build_all_platforms`: (Optional) Set to `true` to build for all supported platforms. Defaults to `false`. -- `configuration`: (Optional) Build configuration (debug or release). Defaults to `debug`. - -#### Example Usage +### 1. Preternatural Build Action +Runs the Preternatural CLI build command on your repositories. ```yaml -name: Run Preternatural Build -uses: PreternaturalAI/github-action/preternatural-build@main -with: - xcode-version: '16' - build_all_platforms: 'false' - derived_data_path: '~/Library/Developer/Xcode/DerivedData' - configuration: 'release' +- uses: PreternaturalAI/preternatural-build-action@v1 ``` -## Preternatural Archive & Notarize Action +### 2. Preternatural Archive & Notarize Action +Archives and notarizes macOS applications using the Preternatural CLI. -This action allows you to archive and notarize a MacOS application using the Preternatural CLI. +```yaml +- uses: PreternaturalAI/preternatural-archive-action@v1 + with: + notarization_username: ${{ secrets.NOTARIZATION_USERNAME }} + notarization_password: ${{ secrets.NOTARIZATION_PASSWORD }} + build_certificate_base64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + p12_password: ${{ secrets.P12_PASSWORD }} +``` -### Inputs +## Documentation -- `xcode-version`: (Required) Xcode version to use. -- `notarization_username`: (Required) Your Apple ID email. -- `notarization_password`: (Required) App specific password to use for notarization. See [here](https://support.apple.com/en-us/102654) for steps on how to create one. -- `notarization_team_id`: (Optional) App Store Connect Team ID for notarization. If not provided, this will be automatically extracted from the Xcode project. The Team ID provided here should match the Team ID of the certificate. -- `build_certificate_base64`: (Required) Your Apple Developer ID Certificate encoded in base64. Follow the steps in [this tutorial](https://localazy.com/blog/how-to-automatically-sign-macos-apps-using-github-actions) to get the base64 string of your certificate. -- `p12_password`: (Required) Password for the `Developer ID Application` certificate. +See [USING.md](./USING.md) for detailed documentation, examples, and troubleshooting. -#### Example Usage +## License -```yaml -name: Archive and Notarize MacOS App -uses: PreternaturalAI/github-action/preternatural-archive@main -with: - xcode-version: '16' - notarization_username: ${{ secrets.NOTARIZATION_USERNAME }} - notarization_password: ${{ secrets.NOTARIZATION_PASSWORD }} - notarization_team_id: ${{ secrets.NOTARIZATION_TEAM_ID }} - build_certificate_base64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} - p12_password: ${{ secrets.P12_PASSWORD }} -``` +[License Type] - See LICENSE file for details diff --git a/USING.md b/USING.md new file mode 100644 index 0000000..df52c5d --- /dev/null +++ b/USING.md @@ -0,0 +1,158 @@ +# Preternatural GitHub Actions Usage Guide + +This document catalogs the available Preternatural GitHub Actions and their usage examples. + +## Table of Contents +- [Build Action](#build-action) +- [Archive & Notarize Action](#archive--notarize-action) + +## Action Source Files + +- [Build Action Source](preternatural-build/action.yml) - Implementation of the build action +- [Archive Action Source](preternatural-archive/action.yml) - Implementation of the archive and notarize action + +## Build Action + +The Build Action runs Preternatural build commands on repositories with specified Xcode configurations. + +### Build Action Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `xcode-version` | Xcode version to use for building | No | `latest-stable` | +| `platforms` | Target platforms to build for | No | `["macOS"]` | +| `configurations` | Build configurations to use | No | `["debug", "release"]` | +| `derived_data_path` | Path to the derived data folder | No | `DerivedData/ProjectBuild` | + +### Build Action Examples + +#### Basic Build +```yaml +steps: + - uses: PreternaturalAI/preternatural-build-action@v1 +``` + +#### Multi-Platform Build +```yaml +steps: + - uses: PreternaturalAI/preternatural-build-action@v1 + with: + platforms: '["iOS", "macOS", "tvOS"]' +``` + +#### Debug-Only Build with Custom Derived Data +```yaml +steps: + - uses: PreternaturalAI/preternatural-build-action@v1 + with: + configurations: '["debug"]' + derived_data_path: 'CustomDerivedData' +``` + +#### All Platforms Release Build +```yaml +steps: + - uses: PreternaturalAI/preternatural-build-action@v1 + with: + platforms: '["iOS", "macOS", "tvOS", "watchOS", "visionOS"]' + configurations: '["release"]' +``` + +#### Build with Specific Xcode Version +```yaml +steps: + - uses: PreternaturalAI/preternatural-build-action@v1 + with: + xcode-version: '14.3.1' + platforms: '["macOS"]' +``` + +## Archive & Notarize Action + +The Archive & Notarize Action creates and notarizes a macOS application using the Preternatural CLI. + +### Archive Action Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `xcode-version` | Xcode version to use | Yes | `latest-stable` | +| `notarization_username` | App Store Connect Username | Yes | - | +| `notarization_password` | App Store Connect Password | Yes | - | +| `notarization_team_id` | App Store Connect Team ID | No | - | +| `build_certificate_base64` | Base64-encoded Apple certificate | Yes | - | +| `p12_password` | Password for the P12 certificate | Yes | - | + +### Archive Action Examples + +#### Basic Archive and Notarize +```yaml +steps: + - uses: PreternaturalAI/preternatural-archive-action@v1 + with: + notarization_username: ${{ secrets.NOTARIZATION_USERNAME }} + notarization_password: ${{ secrets.NOTARIZATION_PASSWORD }} + build_certificate_base64: ${{ secrets.BUILD_CERTIFICATE }} + p12_password: ${{ secrets.P12_PASSWORD }} +``` + +#### Archive with Team ID and Specific Xcode Version +```yaml +steps: + - uses: PreternaturalAI/preternatural-archive-action@v1 + with: + xcode-version: '15.2' + notarization_username: ${{ secrets.NOTARIZATION_USERNAME }} + notarization_password: ${{ secrets.NOTARIZATION_PASSWORD }} + notarization_team_id: 'YOUR_TEAM_ID' + build_certificate_base64: ${{ secrets.BUILD_CERTIFICATE }} + p12_password: ${{ secrets.P12_PASSWORD }} +``` + +#### Archive with Custom Team Setup +```yaml +steps: + - uses: PreternaturalAI/preternatural-archive-action@v1 + with: + xcode-version: '15.2' + notarization_username: ${{ secrets.NOTARIZATION_USERNAME }} + notarization_password: ${{ secrets.NOTARIZATION_PASSWORD }} + notarization_team_id: ${{ secrets.TEAM_ID }} + build_certificate_base64: ${{ secrets.ENTERPRISE_CERTIFICATE }} + p12_password: ${{ secrets.ENTERPRISE_CERT_PASSWORD }} +``` + +#### Archive for Enterprise Distribution +```yaml +steps: + - uses: PreternaturalAI/preternatural-archive-action@v1 + with: + notarization_username: ${{ secrets.ENTERPRISE_USERNAME }} + notarization_password: ${{ secrets.ENTERPRISE_PASSWORD }} + notarization_team_id: ${{ secrets.ENTERPRISE_TEAM_ID }} + build_certificate_base64: ${{ secrets.ENTERPRISE_CERTIFICATE }} + p12_password: ${{ secrets.ENTERPRISE_CERT_PASSWORD }} +``` + +## Features + +### Build Action Features +- Derived data caching +- Detailed build logs +- Multi-platform support +- Various project structure support + +### Archive Action Features +- Automatic certificate installation +- Notarization process handling +- Artifact uploading +- Team ID support +- Secure secrets handling + +## Requirements + +- macOS runner +- GitHub Actions environment +- For notarization: + - Valid Apple Developer account + - App Store Connect credentials + - Valid certificate and provisioning profile diff --git a/preternatural-build/action.yml b/preternatural-build/action.yml index 9f62ff2..a0dbe5e 100644 --- a/preternatural-build/action.yml +++ b/preternatural-build/action.yml @@ -1,131 +1,172 @@ name: 'Preternatural Build Action' description: 'Run Preternatural build command on repositories with a specified Xcode version' inputs: - derived_data_path: - description: 'The path to the derived data folder' + xcode-version: + description: 'Xcode version to use' required: false + default: 'latest-stable' platforms: description: 'Target platforms (array of: iOS, macOS, tvOS, watchOS, visionOS, all)' required: false default: '["macOS"]' - xcode-version: - description: 'Xcode version to use' - required: true configurations: description: 'Build configurations (array of: debug, release)' required: false default: '["debug", "release"]' + derived_data_path: + description: 'The path to the derived data folder' + required: false + default: DerivedData/ProjectBuild + runs: using: 'composite' steps: - - name: Install Preternatural - if: ${{ !env.ACT }} # Skipping when run locally. - shell: bash - run: | - brew tap PreternaturalAI/preternatural - brew install preternatural - - name: Cache derived data - uses: actions/cache@v3 - if: ${{ !env.ACT }} # Skipping when run locally. - with: - path: ${{ inputs.derived_data_path || '~/Library/Developer/Xcode/DerivedData' }} - key: ${{ runner.os }}-derived-data-xcode-${{ inputs.xcode-version }} - restore-keys: | - ${{ runner.os }}-derived-data-xcode-${{ inputs.xcode-version }} - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ inputs.xcode-version }} - - - name: Execute Preternatural build command - id: build + + - name: Install Preternatural + if: ${{ !env.ACT }} shell: bash run: | - defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES - COMMAND="preternatural build" - if [ -n "${{ inputs.derived_data_path }}" ]; then - COMMAND="$COMMAND --derived-data-path '${{ inputs.derived_data_path }}'" - fi + brew tap PreternaturalAI/preternatural + brew install preternatural - # Handle platforms array - PLATFORMS="${{ join(fromJSON(inputs.platforms), ',') }}" - if [ -n "$PLATFORMS" ]; then - COMMAND="$COMMAND --platforms '$PLATFORMS'" + - name: Generate project cache key + id: cache-key + shell: bash + run: | + if ls *.xcodeproj/project.pbxproj 1> /dev/null 2>&1; then + echo "Found Xcode project" + PROJECT_NAME=$(basename *.xcodeproj .xcodeproj) + HASH=$(shasum *.xcodeproj/project.pbxproj | cut -d' ' -f1) + elif ls *.xcworkspace/contents.xcworkspacedata 1> /dev/null 2>&1; then + echo "Found Xcode workspace" + PROJECT_NAME=$(basename *.xcworkspace .xcworkspace) + HASH=$(shasum *.xcworkspace/contents.xcworkspacedata | cut -d' ' -f1) + elif [ -f "Package.swift" ]; then + echo "Found Swift package" + PROJECT_NAME=$(swift package describe --type json | awk '/^{/,/^}/' 2>/dev/null | jq -r '.name') + if [ -f "Package.resolved" ]; then + HASH=$(shasum Package.resolved | cut -d' ' -f1) + else + HASH=$(shasum Package.swift | cut -d' ' -f1) + fi + else + echo "No recognized project structure found" + PROJECT_NAME="unknown" + HASH=$(date +%s | shasum | cut -d' ' -f1) fi - # Handle configurations array - CONFIGS="${{ join(fromJSON(inputs.configurations), ',') }}" - if [ -n "$CONFIGS" ]; then - COMMAND="$COMMAND --configurations '$CONFIGS'" - fi + echo "project-name=$PROJECT_NAME" >> $GITHUB_OUTPUT + + PLATFORMS_KEY=$(echo '${{ inputs.platforms }}' | tr -d '[]"' | sed 's/, /-/g' | sed 's/,/-/g') + echo "platforms-key=$PLATFORMS_KEY" >> $GITHUB_OUTPUT + echo "package-hash=$HASH" >> $GITHUB_OUTPUT - echo "Executing command: $COMMAND" - eval $COMMAND - continue-on-error: true - - name: Clear caches and retry build - id: retry_build - if: steps.build.outcome == 'failure' + - name: Cache project derived data + id: cache-derived-data + uses: actions/cache@v4 + if: ${{ !env.ACT }} + with: + path: ${{ github.workspace }}/${{ inputs.derived_data_path }} + key: ${{ steps.cache-key.outputs.project-name }}-derived-data-v1-${{ runner.os }}-xcode${{ inputs.xcode-version }}-platforms-${{ steps.cache-key.outputs.platforms-key }}-${{ steps.cache-key.outputs.package-hash }} + restore-keys: | + ${{ steps.cache-key.outputs.project-name }}-derived-data-v1-${{ runner.os }}-xcode${{ inputs.xcode-version }}-platforms- + + - name: Debug Cache Status + if: ${{ !env.ACT }} shell: bash run: | - echo "Initial build failed. Clearing caches and retrying..." - - # Allow individual commands to fail without stopping the script - set +e + echo "==== Cache Status ====" - # Clear SwiftPM caches - if [ "${{ !env.ACT }}" == "true" ]; then - rm -rf $HOME/Library/org.swift.swiftpm - rm -rf $HOME/Library/Caches/org.swift.swiftpm - fi + echo "Project Cache:" + echo "Cache key components:" + echo "- Runner OS: ${{ runner.os }}" + echo "- Xcode Version: ${{ inputs.xcode-version }}" + echo "- Platforms: ${{ inputs.platforms }}" + echo "- Package.resolved hash: ${{ hashFiles('Package.resolved') }}" + echo "- Package.swift hash: ${{ hashFiles('Package.swift') }}" - # Force package update - rm -rf .build - swift package update - swift package resolve + PROJECT_DERIVED_DATA=${{ github.workspace }}/${{ inputs.derived_data_path }} + echo "Project derived data path: $PROJECT_DERIVED_DATA" - # Clear derived data - if [ -n "${{ inputs.derived_data_path }}" ]; then - rm -rf "${{ inputs.derived_data_path }}" + if [ "${{ steps.cache-derived-data.outputs.cache-hit }}" == "true" ]; then + echo "Project cache: HIT" + if [ -d "$PROJECT_DERIVED_DATA" ]; then + echo "Directory exists and contains:" + find "$PROJECT_DERIVED_DATA" -type d -maxdepth 2 + echo "Total size:" + du -sh "$PROJECT_DERIVED_DATA" + else + echo "WARNING: Project cache hit but directory doesn't exist!" + fi else - rm -rf $HOME/Library/Developer/Xcode/DerivedData - fi - - # Retry build - COMMAND="preternatural build" - if [ -n "${{ inputs.derived_data_path }}" ]; then - COMMAND="$COMMAND --derived-data-path '${{ inputs.derived_data_path }}'" + echo "Project cache: MISS" fi + echo "==================================" + + - name: Create project build script + shell: bash + run: | + echo "==== Create Project Build Script ====" - # Handle platforms array - PLATFORMS="${{ join(fromJSON(inputs.platforms), ',') }}" - if [ -n "$PLATFORMS" ]; then - COMMAND="$COMMAND --platforms '$PLATFORMS'" - fi - - # Handle configurations array - CONFIGS="${{ join(fromJSON(inputs.configurations), ',') }}" - if [ -n "$CONFIGS" ]; then - COMMAND="$COMMAND --configurations '$CONFIGS'" - fi + PLATFORMS=$(echo '${{ inputs.platforms }}' | tr -d '[]' | sed 's/, /,/g') + CONFIGURATIONS=$(echo '${{ inputs.configurations }}' | tr -d '[]' | sed 's/, /,/g') + DERIVED_DATA_PATH=${{ github.workspace }}/${{ inputs.derived_data_path }} - echo "Retrying command: $COMMAND" - eval $COMMAND - - # Capture the exit code of the last command (the build retry) - BUILD_EXIT_CODE=$? - - # Exit with the build's exit code - exit $BUILD_EXIT_CODE + echo "Processed values:" + echo "PLATFORMS: $PLATFORMS" + echo "CONFIGURATIONS: $CONFIGURATIONS" + echo "DERIVED_DATA_PATH: $DERIVED_DATA_PATH" + + cat > project_build_utils.sh << EOF + #!/bin/bash + PLATFORMS="${PLATFORMS}" + CONFIGURATIONS="${CONFIGURATIONS}" + DERIVED_DATA_PATH="${DERIVED_DATA_PATH}" + + project_build_cmd() { + cmd="preternatural build" + if [[ -n "\${DERIVED_DATA_PATH}" ]]; then + cmd="\$cmd --derived-data-path \$(printf %q "\${DERIVED_DATA_PATH}")" + fi + if [[ -n "\${PLATFORMS}" ]]; then + cmd="\$cmd --platforms \${PLATFORMS}" + fi + if [[ -n "\${CONFIGURATIONS}" ]]; then + cmd="\$cmd --configurations \${CONFIGURATIONS}" + fi + echo "\$cmd" + } + EOF + chmod +x project_build_utils.sh + export PLATFORMS + export CONFIGURATIONS + export DERIVED_DATA_PATH + echo "==================================" + - name: Execute project build command + id: build + shell: bash + run: | + echo "==== Execute Project Build Command ====" + source project_build_utils.sh + echo "Loaded project_build_utils.sh" + defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES + echo "Set IDESkipMacroFingerprintValidation" + COMMAND=$(project_build_cmd) + echo "Executing command: $COMMAND" + eval "$COMMAND" + echo "Command execution completed with status: $?" + echo "==================================" + - name: Find and copy all xcactivity logs if: failure() shell: bash run: | - if [ -n "${{ inputs.derived_data_path }}" ]; then - DERIVED_DATA_PATH="${{ inputs.derived_data_path }}" - else - DERIVED_DATA_PATH="$HOME/Library/Developer/Xcode/DerivedData" - fi + DERIVED_DATA_PATH=${{ github.workspace }}/${{ inputs.derived_data_path }} echo "Searching for logs in: $DERIVED_DATA_PATH" mkdir -p ./artifacts @@ -161,7 +202,7 @@ runs: mkdir -p ./json_logs for log in ./artifacts/*.xcactivitylog; do json_file="./json_logs/$(basename "$log" .xcactivitylog).json" - xclogparser parse --file "$log" --output "$json_file" + xclogparser parse --file "$log" --reporter json --output "$json_file" echo "Contents of $json_file:" cat "$json_file" echo "--------------------------------" @@ -169,7 +210,7 @@ runs: - name: Upload xcactivity logs (JSON) if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: xcactivity-logs-json path: ./json_logs/*.json @@ -177,7 +218,7 @@ runs: - name: Upload xcactivity logs (Raw) if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: xcactivity-logs-raw path: ./artifacts/*.xcactivitylog