├── .github ├── CODEOWNERS └── workflows │ ├── update-release-tags.yml │ └── validate.yml ├── .gitignore ├── LICENSE ├── README.md ├── action.yml ├── dist ├── index.js ├── index.js.map ├── licenses.txt └── sourcemap-register.js ├── package-lock.json ├── package.json ├── src ├── AppStoreConnectClient.ts ├── AppleCredential.ts ├── XcodeProject.ts ├── index.ts ├── utilities.ts └── xcode.ts └── tsconfig.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @buildalon/buildalon 2 | -------------------------------------------------------------------------------- /.github/workflows/update-release-tags.yml: -------------------------------------------------------------------------------- 1 | name: Update Release Tags 2 | on: 3 | push: 4 | tags: ['*'] 5 | workflow_dispatch: 6 | jobs: 7 | update-release-tags: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - uses: RageAgainstThePixel/update-action-release-tags@v1 16 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: ['main'] 5 | pull_request: 6 | types: [opened, reopened, synchronize, ready_for_review] 7 | branches: ['*'] 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | jobs: 14 | unity-build: 15 | if: github.event.pull_request.draft == false 16 | name: '(${{ matrix.unity-version }}) ${{ matrix.build-target }}' 17 | permissions: 18 | contents: read 19 | env: 20 | VERSION: '' 21 | TEMPLATE_PATH: '' 22 | EXPORT_OPTION: '' 23 | UNITY_PROJECT_PATH: '' 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | os: [macos-latest] 29 | unity-version: [2021.x, 2022.x, 6000.x] 30 | build-target: 31 | - iOS 32 | - StandaloneOSX 33 | - VisionOS 34 | exclude: 35 | - os: macos-latest 36 | unity-version: 2021.x 37 | build-target: VisionOS 38 | - os: macos-latest 39 | unity-version: 2022.x 40 | build-target: VisionOS 41 | steps: 42 | - uses: actions/checkout@v4 43 | - run: 'npm install -g openupm-cli' 44 | - uses: buildalon/unity-setup@v1 45 | with: 46 | version-file: 'None' 47 | build-targets: ${{ matrix.build-target }} 48 | unity-version: ${{ matrix.unity-version }} 49 | - name: Find Unity Template Path and Version 50 | run: | 51 | $rootPath = $env:UNITY_EDITOR_PATH -replace "Editor.*", "" 52 | Write-Host "ROOT_PATH=$rootPath" 53 | $templatePath = Get-ChildItem -Recurse -Filter "com.unity.template.3d*.tgz" -Path $rootPath | Select-Object -First 1 | Select-Object -ExpandProperty FullName 54 | Write-Host "TEMPLATE_PATH=$templatePath" 55 | echo "TEMPLATE_PATH=$templatePath" >> $env:GITHUB_ENV 56 | $projectPath = "${{ github.workspace }}/UnityProject" 57 | echo "UNITY_PROJECT_PATH=$projectPath" >> $env:GITHUB_ENV 58 | 59 | # Read version from package.json instead of git tags 60 | $packageJsonPath = "${{ github.workspace }}/package.json" 61 | $packageJson = Get-Content -Raw -Path $packageJsonPath | ConvertFrom-Json 62 | $version = $packageJson.version 63 | 64 | if ($version -match '^\d+\.\d+\.\d+$') { 65 | Write-Host "Version from package.json: $version" 66 | } else { 67 | Write-Host "Version: $version is not a valid version string" 68 | exit 1 69 | } 70 | echo "VERSION=$version" >> $env:GITHUB_ENV 71 | 72 | # if the unity-version is 6000.x then set export option to app-store-connect otherwise set it to development 73 | if ('${{ matrix.unity-version }}' -eq '6000.x') { 74 | echo "EXPORT_OPTION=app-store-connect" >> $env:GITHUB_ENV 75 | } else { 76 | if ('${{ matrix.build-target }}' -eq 'StandaloneOSX') { 77 | if ('${{ matrix.unity-version }}' -eq '2022.x') { 78 | echo "EXPORT_OPTION=steam" >> $env:GITHUB_ENV 79 | } else { 80 | echo "EXPORT_OPTION=developer-id" >> $env:GITHUB_ENV 81 | } 82 | } else { 83 | echo "EXPORT_OPTION=development" >> $env:GITHUB_ENV 84 | } 85 | } 86 | shell: pwsh 87 | - uses: buildalon/activate-unity-license@v1 88 | with: 89 | license: 'Personal' 90 | username: ${{ secrets.UNITY_USERNAME }} 91 | password: ${{ secrets.UNITY_PASSWORD }} 92 | - uses: buildalon/unity-action@v1 93 | name: Create Test Project 94 | with: 95 | log-name: 'create-test-project' 96 | args: '-quit -nographics -batchmode -createProject "${{ github.workspace }}/UnityProject" -cloneFromTemplate "${{ env.TEMPLATE_PATH }}"' 97 | - run: openupm add com.virtualmaker.buildalon 98 | name: Add Build Pipeline Package 99 | working-directory: ${{ github.workspace }}/UnityProject 100 | - uses: buildalon/unity-action@v1 101 | name: '${{ matrix.build-target }}-Validate' 102 | with: 103 | build-target: ${{ matrix.build-target }} 104 | log-name: '${{ matrix.build-target }}-Validate' 105 | args: '-quit -nographics -batchmode -executeMethod Buildalon.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset' 106 | - uses: buildalon/unity-action@v1 107 | name: '${{ matrix.build-target }}-Build' 108 | with: 109 | build-target: ${{ matrix.build-target }} 110 | log-name: '${{ matrix.build-target }}-Build' 111 | args: '-quit -nographics -batchmode -executeMethod Buildalon.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity -export -enableAppleAutomaticSigning -bundleIdentifier com.test.buildalon.xcode -versionName ${{ env.VERSION }}' 112 | - name: Update Info.Plist with encryption compliance 113 | shell: bash 114 | run: | 115 | set -xe 116 | # find the Info.plist file in the build directory 117 | # MacOSStandalone Info.plist path: /Users/runner/work/unity-xcode-builder/unity-xcode-builder/UnityProject/Builds/StandaloneOSX/com.test.buildalon.xcode/UnityProject/UnityProject/Info.plist 118 | # all others: /Users/runner/work/unity-xcode-builder/unity-xcode-builder/UnityProject/Builds/iOS/com.test.buildalon.xcode/Info.plist 119 | EXPORT_OPTION=${{ env.EXPORT_OPTION }} 120 | if [ "$EXPORT_OPTION" != "app-store-connect" ]; then 121 | exit 0 122 | fi 123 | TARGET_PLATFORM=${{ matrix.build-target }} 124 | if [ "$TARGET_PLATFORM" == "StandaloneOSX" ]; then 125 | INFO_PLIST_PATH="${{ env.UNITY_PROJECT_PATH }}/Builds/${{ matrix.build-target }}/com.test.buildalon.xcode/UnityProject/UnityProject/Info.plist" 126 | else 127 | INFO_PLIST_PATH="${{ env.UNITY_PROJECT_PATH }}/Builds/${{ matrix.build-target }}/com.test.buildalon.xcode/Info.plist" 128 | fi 129 | # make sure plist buddy is installed 130 | if ! command -v /usr/libexec/PlistBuddy &> /dev/null 131 | then 132 | echo "PlistBuddy could not be found" 133 | exit 1 134 | fi 135 | # set ITSAppUsesNonExemptEncryption to false in Info.plist using PlistBuddy 136 | /usr/libexec/PlistBuddy -c "Add :ITSAppUsesNonExemptEncryption bool false" "$INFO_PLIST_PATH" 137 | - uses: ./ # buildalon/unity-xcode-builder 138 | id: xcode-build 139 | with: 140 | project-path: ${{ env.UNITY_PROJECT_PATH }}/Builds/${{ matrix.build-target }}/**/*.xcodeproj 141 | app-store-connect-key: ${{ secrets.APP_STORE_CONNECT_KEY }} 142 | app-store-connect-key-id: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} 143 | app-store-connect-issuer-id: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} 144 | team-id: ${{ secrets.APPLE_TEAM_ID }} 145 | export-option: ${{ env.EXPORT_OPTION }} 146 | notarize: ${{ matrix.unity-version != '6000.x' }} 147 | archive-type: pkg 148 | test-groups: Beta 149 | developer-id-application-certificate: ${{ secrets.DEVELOPER_ID_APPLICATION_CERT }} 150 | developer-id-application-certificate-password: ${{ secrets.SIGNING_CERT_PASSWORD }} 151 | developer-id-installer-certificate: ${{ secrets.DEVELOPER_ID_INSTALLER_CERT }} 152 | developer-id-installer-certificate-password: ${{ secrets.SIGNING_CERT_PASSWORD }} 153 | - name: print outputs 154 | if: always() 155 | run: | 156 | echo "Executable: ${{ steps.xcode-build.outputs.executable }}" 157 | echo "Output Directory: ${{ steps.xcode-build.outputs.output-directory }}" 158 | ls -R "${{ steps.xcode-build.outputs.output-directory }}" 159 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Virtual Maker Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Buildalon unity-xcode-builder 2 | 3 | [![Discord](https://img.shields.io/discord/939721153688264824.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/VM9cWJ9rjH) [![marketplace](https://img.shields.io/static/v1?label=&labelColor=505050&message=Buildalon%20Actions&color=FF1E6F&logo=github-actions&logoColor=0076D6)](https://github.com/marketplace?query=buildalon) [![validate](https://github.com/buildalon/unity-xcode-builder/actions/workflows/validate.yml/badge.svg?branch=main)](https://github.com/buildalon/unity-xcode-builder/actions/workflows/validate.yml) 4 | 5 | A GitHub Action to take Unity exported Xcode projects and automate the process of building, signing, archiving, notarizing, and uploading to Apple App Store Connect or Steam. 6 | 7 | > [!NOTE] 8 | > Steam uploads require an additional action step: [`upload-steam`](https://github.com/buildalon/upload-steam) 9 | 10 | ## How to use 11 | 12 | ### workflow 13 | 14 | To archive, export, and upload directly to Apple App Store Connect, use the following workflow configuration: 15 | 16 | ```yaml 17 | steps: 18 | - uses: buildalon/unity-xcode-builder@v1 19 | id: xcode-build 20 | with: 21 | project-path: '/path/to/your/build/output/directory' 22 | app-store-connect-key: ${{ secrets.APP_STORE_CONNECT_KEY }} 23 | app-store-connect-key-id: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} 24 | app-store-connect-issuer-id: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} 25 | team-id: ${{ secrets.APPLE_TEAM_ID }} 26 | 27 | - run: | 28 | echo ${{ steps.xcode-build.outputs.executable }} 29 | ls -al ${{ steps.xcode-build.outputs.output-directory }} 30 | ``` 31 | 32 | ### inputs 33 | 34 | This action requires several secrets that need to be setup in the repository or organization's action secret store. 35 | 36 | - `APP_STORE_CONNECT_KEY`: The App Store Connect API AuthKey_*.p8 key encoded as base64 string. 37 | - `APP_STORE_CONNECT_KEY_ID`: The App Store Connect API key id. 38 | - `APP_STORE_CONNECT_ISSUER_ID`: The issuer ID of the App Store Connect API key. 39 | 40 | > [!TIP] 41 | > You can easily encode a file to base64 using the following command in linux, mac, or windows bash terminal: 42 | > 43 | > ```bash 44 | > openssl base64 -in ./AuthKey_*.p8 -out ./AuthKey_*.txt 45 | > ``` 46 | 47 | | name | description | required | 48 | | ---- | ----------- | -------- | 49 | | `xcode-version` | The version of Xcode to use for building the Xcode project. | Defaults to the [latest version of Xcode on the runner](https://github.com/actions/runner-images#available-images). | 50 | | `project-path` | The directory that contains the exported xcode project from Unity. | Defaults to searching the workspace for `.xcodeproj` | 51 | | `app-store-connect-key` | The App Store Connect API AuthKey_*.p8 key encoded as base64 string. | true | 52 | | `app-store-connect-key-id` | The App Store Connect API key id. | true | 53 | | `app-store-connect-issuer-id` | The issuer ID of the App Store Connect API key. | true | 54 | | `manual-signing-certificate` | Exported signing certificate.p12 encoded as base64 string. Overrides the automatic signing in Xcode. | Defaults to Automatic signing. | 55 | | `manual-signing-certificate-password` | The password for the exported certificate. | Required if `manual-signing-certificate` is provided. | 56 | | `manual-signing-identity` | The signing identity to use for signing the Xcode project. | Parsed from the `manual-signing-certificate` if not provided. | 57 | | `provisioning-profile` | The provisioning profile to use as base64 string. Use when manually signing the Xcode project. | Defaults to Automatic signing. | 58 | | `provisioning-profile-name` | The name of the provisioning profile file, including the type to use for signing the Xcode project. Must end with either `.mobileprovision` or `.provisionprofile`. | Required if `provisioning-profile` is provided. | 59 | | `team-id` | The team ID to use for signing the Xcode project. | Defaults to parsing team ID from `manual-signing-certificate` if provided. | 60 | | `bundle-id` | The bundle ID of the Xcode project. Overrides the value in the exported Unity project. | Defaults to parsing bundle ID from `.xcodeproj`. | 61 | | `configuration` | The configuration to build the Xcode project with. | Defaults to `Release`. | 62 | | `scheme` | The scheme to use when building the xcode project. | false | 63 | | `destination` | The destination to use when building the xcode project. | Defaults to `generic/platform={platform}`. | 64 | | `platform` | The platform to build for. Can be one of `iOS`, `macOS`, `tvOS`, `visionOS`. | Defaults to parsing platform from `.xcodeproj`. | 65 | | `platform-sdk-version` | The version of the platform SDK to use for building the Xcode project. | Defaults to the latest version of the platform SDK defined in the `.xcodeproj`. | 66 | | `export-option` | The export option to use for exporting the Xcode project. Can be one of `app-store-connect`, `steam`, `release-testing`, `enterprise`, `debugging`, `developer-id`, `mac-application`. | Defaults to `development` | 67 | | `export-option-plist` | The path to custom export option plist file to use when exporting the Xcode project. | Overrides `export-option`. | 68 | | `entitlements-plist` | The path to custom entitlements plist file. | Generates [default hardened runtime entitlements](https://developer.apple.com/documentation/security/hardened-runtime) if not provided. | 69 | | `notarize` | Whether to notarize the exported Xcode project. | Defaults to `true` if `export-option !== app-store-connect`. | 70 | | `archive-type` | The archive type to use when exporting macOS applications when not uploading to the App Store. Can be one of `app` or `pkg`. | Defaults to `app`. Forces `app` if `export-option === steam`. | 71 | | `upload` | Whether to upload the exported Xcode project to App Store Connect. | Defaults to `true` if `export-option === app-store-connect`. | 72 | | `whats-new` | When `uploading === true`, Let your testers know what you would like them to test in this build. This information will be available to testers in all groups who have access to this build. | Defaults to the last git commit sha, current branch name, and commit message up to 4000 characters. | 73 | | `auto-increment-build-number` | Whether to automatically increment the `CFBundleVersion` in the Xcode project. | Defaults to `true` if `export-option === app-store-connect`. | 74 | | `test-groups` | One or more test groups to automatically add to the build when uploading to TestFlight. When using multiple groups, separate them with commas. | None by default. | 75 | | `submit-for-review` | Whether to submit the build for review when uploading to App Store Connect. | Defaults to `false`. | 76 | | `developer-id-application-certificate` | The `Developer ID Application` certificate encoded as base64 string. | Required if `export-option === steam` or `export-option === developer-id` or `notarize === true`. | 77 | | `developer-id-application-certificate-password` | The password for the `Developer ID Application` certificate. | Required if `developer-id-application-certificate` is provided. | 78 | | `developer-id-installer-certificate` | The `Developer ID Installer` certificate encoded as base64 string. | Required when creating an installer package for macOS application. | 79 | | `developer-id-installer-certificate-password` | The password for the `Developer ID Installer` certificate. | Required if `developer-id-installer-certificate` is provided. | 80 | 81 | ### outputs 82 | 83 | - `executable`: Path to the exported archive executable. 84 | - `output-directory`: The path to the export output directory. 85 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Buildalon unity-xcode-builder 2 | description: A GitHub Action to build, archive, and upload Unity exported xcode projects. 3 | branding: 4 | icon: command 5 | color: red 6 | inputs: 7 | xcode-version: 8 | description: The version of Xcode to use for building the Xcode project. Defaults to the [latest version of Xcode on the runner](https://github.com/actions/runner-images#available-images). 9 | required: false 10 | default: latest 11 | project-path: 12 | description: The directory that contains the exported xcode project from Unity. 13 | required: false 14 | app-store-connect-key: 15 | description: The App Store Connect API AuthKey_*.p8 key encoded as base64 string. 16 | required: true 17 | app-store-connect-key-id: 18 | description: The App Store Connect API key id. 19 | required: true 20 | app-store-connect-issuer-id: 21 | description: The issuer ID of the App Store Connect API key. 22 | required: true 23 | certificate: 24 | description: Exported signing certificate.p12 encoded as base64 string. Overrides the automatic signing in Xcode. 25 | required: false 26 | deprecationMessage: use `manual-signing-certificate` instead. 27 | manual-signing-certificate: 28 | description: Exported signing certificate.p12 encoded as base64 string. Overrides the automatic signing in Xcode. 29 | required: false 30 | certificate-password: 31 | description: The password for the exported certificate. Required if `certificate` is provided. 32 | required: false 33 | deprecationMessage: use `manual-signing-certificate-password` instead. 34 | manual-signing-certificate-password: 35 | description: The password for the exported certificate. Required if `manual-signing-certificate` is provided. 36 | required: false 37 | signing-identity: 38 | description: The signing identity to use for signing the Xcode project. Parsed from the `manual-signing-certificate` if not provided. 39 | required: false 40 | deprecationMessage: use `manual-signing-identity` instead. 41 | manual-signing-identity: 42 | description: The signing identity to use for signing the Xcode project. Parsed from the `manual-signing-certificate` if not provided. 43 | required: false 44 | provisioning-profile: 45 | description: The provisioning profile to use as base64 string. Use when manually signing the Xcode project. 46 | required: false 47 | provisioning-profile-name: 48 | description: The name of the provisioning profile file, including the type to use for signing the Xcode project. Must end with either `.mobileprovision` or `.provisionprofile`. Required if `provisioning-profile` is provided. 49 | required: false 50 | team-id: 51 | description: The team ID to use for signing the Xcode project. Defaults to parsing team ID from `manual-signing-certificate` if provided. 52 | required: false 53 | bundle-id: 54 | description: The bundle ID of the Xcode project. Overrides the value in the exported Unity project. Defaults to parsing bundle ID from `.xcodeproj`. 55 | required: false 56 | configuration: 57 | description: The configuration to build the Xcode project with. Defaults to `Release`. 58 | required: false 59 | default: 'Release' 60 | scheme: 61 | description: The scheme to use when building the xcode project. 62 | required: false 63 | destination: 64 | description: The destination to use when building the xcode project. Defaults to `generic/platform={platform}`. 65 | required: false 66 | platform: 67 | description: The platform to build for. Can be one of `iOS`, `macOS`, `tvOS`, `visionOS`. Defaults to parsing platform from `.xcodeproj`. 68 | required: false 69 | platform-sdk-version: 70 | description: The version of the platform SDK to use for building the Xcode project. Defaults to the latest version of the platform SDK defined in the `.xcodeproj`. 71 | required: false 72 | export-option: 73 | description: The export option to use for exporting the Xcode project. Can be one of `app-store-connect`, `steam`, `release-testing`, `enterprise`, `debugging`, `developer-id`, `mac-application`. Defaults to `development` 74 | required: false 75 | default: development 76 | export-option-plist: 77 | description: The path to custom export option plist file to use when exporting the Xcode project. Overrides `export-option`. 78 | required: false 79 | entitlements-plist: 80 | description: The path to custom entitlements plist file. Generates [default hardened runtime entitlements](https://developer.apple.com/documentation/security/hardened-runtime) if not provided. 81 | required: false 82 | notarize: 83 | description: Whether to notarize the exported Xcode project. Defaults to `true` if `export-option !== app-store-connect`. 84 | required: false 85 | archive-type: 86 | description: The archive type to use when exporting macOS applications when not uploading to the App Store. Can be one of `app` or `pkg`. Defaults to `app`. Forces `app` if `export-option === steam`. 87 | required: false 88 | default: app 89 | upload: 90 | description: Whether to upload the exported Xcode project to App Store Connect. Defaults to `true` if `export-option === app-store-connect`. 91 | required: false 92 | whats-new: 93 | description: When `uploading === true`, Let your testers know what you would like them to test in this build. This information will be available to testers in all groups who have access to this build. Defaults to the last git commit sha, current branch name, and commit message up to 4000 characters. 94 | required: false 95 | auto-increment-build-number: 96 | description: Whether to automatically increment the `CFBundleVersion` in the Xcode project. Defaults to `true` if `export-option === app-store-connect`. 97 | required: false 98 | default: 'true' 99 | test-groups: 100 | description: One or more test groups to automatically add to the build when uploading to TestFlight. When using multiple groups, separate them with commas. None by default. 101 | required: false 102 | submit-for-review: 103 | description: Whether to submit the build for review when uploading to TestFlight. Defaults to `false`. 104 | required: false 105 | developer-id-application-certificate: 106 | description: The `Developer ID Application` certificate encoded as base64 string. Required if `export-option === steam` or `export-option === developer-id` or `notarize === true`. 107 | required: false 108 | developer-id-application-certificate-password: 109 | description: The password for the `Developer ID Application` certificate. Required if `developer-id-application-certificate` is provided. 110 | required: false 111 | developer-id-installer-certificate: 112 | description: The `Developer ID Installer` certificate encoded as base64 string. Required when creating an installer package for macOS application. 113 | required: false 114 | developer-id-installer-certificate-password: 115 | description: The password for the `Developer ID Installer` certificate. Required if `developer-id-installer-certificate` is provided. 116 | required: false 117 | outputs: 118 | executable: 119 | description: The path to the generated archive executable. 120 | output-directory: 121 | description: The path to the export output directory. 122 | runs: 123 | using: 'node20' 124 | main: 'dist/index.js' 125 | post: 'dist/index.js' 126 | -------------------------------------------------------------------------------- /dist/sourcemap-register.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={650:e=>{var r=Object.prototype.toString;var n=typeof Buffer.alloc==="function"&&typeof Buffer.allocUnsafe==="function"&&typeof Buffer.from==="function";function isArrayBuffer(e){return r.call(e).slice(8,-1)==="ArrayBuffer"}function fromArrayBuffer(e,r,t){r>>>=0;var o=e.byteLength-r;if(o<0){throw new RangeError("'offset' is out of bounds")}if(t===undefined){t=o}else{t>>>=0;if(t>o){throw new RangeError("'length' is out of bounds")}}return n?Buffer.from(e.slice(r,r+t)):new Buffer(new Uint8Array(e.slice(r,r+t)))}function fromString(e,r){if(typeof r!=="string"||r===""){r="utf8"}if(!Buffer.isEncoding(r)){throw new TypeError('"encoding" must be a valid string encoding')}return n?Buffer.from(e,r):new Buffer(e,r)}function bufferFrom(e,r,t){if(typeof e==="number"){throw new TypeError('"value" argument must not be a number')}if(isArrayBuffer(e)){return fromArrayBuffer(e,r,t)}if(typeof e==="string"){return fromString(e,r)}return n?Buffer.from(e):new Buffer(e)}e.exports=bufferFrom},274:(e,r,n)=>{var t=n(339);var o=Object.prototype.hasOwnProperty;var i=typeof Map!=="undefined";function ArraySet(){this._array=[];this._set=i?new Map:Object.create(null)}ArraySet.fromArray=function ArraySet_fromArray(e,r){var n=new ArraySet;for(var t=0,o=e.length;t=0){return r}}else{var n=t.toSetString(e);if(o.call(this._set,n)){return this._set[n]}}throw new Error('"'+e+'" is not in the set.')};ArraySet.prototype.at=function ArraySet_at(e){if(e>=0&&e{var t=n(190);var o=5;var i=1<>1;return r?-n:n}r.encode=function base64VLQ_encode(e){var r="";var n;var i=toVLQSigned(e);do{n=i&a;i>>>=o;if(i>0){n|=u}r+=t.encode(n)}while(i>0);return r};r.decode=function base64VLQ_decode(e,r,n){var i=e.length;var s=0;var l=0;var c,p;do{if(r>=i){throw new Error("Expected more digits in base 64 VLQ value.")}p=t.decode(e.charCodeAt(r++));if(p===-1){throw new Error("Invalid base64 digit: "+e.charAt(r-1))}c=!!(p&u);p&=a;s=s+(p<{var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");r.encode=function(e){if(0<=e&&e{r.GREATEST_LOWER_BOUND=1;r.LEAST_UPPER_BOUND=2;function recursiveSearch(e,n,t,o,i,a){var u=Math.floor((n-e)/2)+e;var s=i(t,o[u],true);if(s===0){return u}else if(s>0){if(n-u>1){return recursiveSearch(u,n,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return n1){return recursiveSearch(e,u,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return u}else{return e<0?-1:e}}}r.search=function search(e,n,t,o){if(n.length===0){return-1}var i=recursiveSearch(-1,n.length,e,n,t,o||r.GREATEST_LOWER_BOUND);if(i<0){return-1}while(i-1>=0){if(t(n[i],n[i-1],true)!==0){break}--i}return i}},680:(e,r,n)=>{var t=n(339);function generatedPositionAfter(e,r){var n=e.generatedLine;var o=r.generatedLine;var i=e.generatedColumn;var a=r.generatedColumn;return o>n||o==n&&a>=i||t.compareByGeneratedPositionsInflated(e,r)<=0}function MappingList(){this._array=[];this._sorted=true;this._last={generatedLine:-1,generatedColumn:0}}MappingList.prototype.unsortedForEach=function MappingList_forEach(e,r){this._array.forEach(e,r)};MappingList.prototype.add=function MappingList_add(e){if(generatedPositionAfter(this._last,e)){this._last=e;this._array.push(e)}else{this._sorted=false;this._array.push(e)}};MappingList.prototype.toArray=function MappingList_toArray(){if(!this._sorted){this._array.sort(t.compareByGeneratedPositionsInflated);this._sorted=true}return this._array};r.H=MappingList},758:(e,r)=>{function swap(e,r,n){var t=e[r];e[r]=e[n];e[n]=t}function randomIntInRange(e,r){return Math.round(e+Math.random()*(r-e))}function doQuickSort(e,r,n,t){if(n{var t;var o=n(339);var i=n(345);var a=n(274).I;var u=n(449);var s=n(758).U;function SourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}return n.sections!=null?new IndexedSourceMapConsumer(n,r):new BasicSourceMapConsumer(n,r)}SourceMapConsumer.fromSourceMap=function(e,r){return BasicSourceMapConsumer.fromSourceMap(e,r)};SourceMapConsumer.prototype._version=3;SourceMapConsumer.prototype.__generatedMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_generatedMappings",{configurable:true,enumerable:true,get:function(){if(!this.__generatedMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__generatedMappings}});SourceMapConsumer.prototype.__originalMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_originalMappings",{configurable:true,enumerable:true,get:function(){if(!this.__originalMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__originalMappings}});SourceMapConsumer.prototype._charIsMappingSeparator=function SourceMapConsumer_charIsMappingSeparator(e,r){var n=e.charAt(r);return n===";"||n===","};SourceMapConsumer.prototype._parseMappings=function SourceMapConsumer_parseMappings(e,r){throw new Error("Subclasses must implement _parseMappings")};SourceMapConsumer.GENERATED_ORDER=1;SourceMapConsumer.ORIGINAL_ORDER=2;SourceMapConsumer.GREATEST_LOWER_BOUND=1;SourceMapConsumer.LEAST_UPPER_BOUND=2;SourceMapConsumer.prototype.eachMapping=function SourceMapConsumer_eachMapping(e,r,n){var t=r||null;var i=n||SourceMapConsumer.GENERATED_ORDER;var a;switch(i){case SourceMapConsumer.GENERATED_ORDER:a=this._generatedMappings;break;case SourceMapConsumer.ORIGINAL_ORDER:a=this._originalMappings;break;default:throw new Error("Unknown order of iteration.")}var u=this.sourceRoot;a.map((function(e){var r=e.source===null?null:this._sources.at(e.source);r=o.computeSourceURL(u,r,this._sourceMapURL);return{source:r,generatedLine:e.generatedLine,generatedColumn:e.generatedColumn,originalLine:e.originalLine,originalColumn:e.originalColumn,name:e.name===null?null:this._names.at(e.name)}}),this).forEach(e,t)};SourceMapConsumer.prototype.allGeneratedPositionsFor=function SourceMapConsumer_allGeneratedPositionsFor(e){var r=o.getArg(e,"line");var n={source:o.getArg(e,"source"),originalLine:r,originalColumn:o.getArg(e,"column",0)};n.source=this._findSourceIndex(n.source);if(n.source<0){return[]}var t=[];var a=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,i.LEAST_UPPER_BOUND);if(a>=0){var u=this._originalMappings[a];if(e.column===undefined){var s=u.originalLine;while(u&&u.originalLine===s){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}else{var l=u.originalColumn;while(u&&u.originalLine===r&&u.originalColumn==l){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}}return t};r.SourceMapConsumer=SourceMapConsumer;function BasicSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sources");var u=o.getArg(n,"names",[]);var s=o.getArg(n,"sourceRoot",null);var l=o.getArg(n,"sourcesContent",null);var c=o.getArg(n,"mappings");var p=o.getArg(n,"file",null);if(t!=this._version){throw new Error("Unsupported version: "+t)}if(s){s=o.normalize(s)}i=i.map(String).map(o.normalize).map((function(e){return s&&o.isAbsolute(s)&&o.isAbsolute(e)?o.relative(s,e):e}));this._names=a.fromArray(u.map(String),true);this._sources=a.fromArray(i,true);this._absoluteSources=this._sources.toArray().map((function(e){return o.computeSourceURL(s,e,r)}));this.sourceRoot=s;this.sourcesContent=l;this._mappings=c;this._sourceMapURL=r;this.file=p}BasicSourceMapConsumer.prototype=Object.create(SourceMapConsumer.prototype);BasicSourceMapConsumer.prototype.consumer=SourceMapConsumer;BasicSourceMapConsumer.prototype._findSourceIndex=function(e){var r=e;if(this.sourceRoot!=null){r=o.relative(this.sourceRoot,r)}if(this._sources.has(r)){return this._sources.indexOf(r)}var n;for(n=0;n1){v.source=l+_[1];l+=_[1];v.originalLine=i+_[2];i=v.originalLine;v.originalLine+=1;v.originalColumn=a+_[3];a=v.originalColumn;if(_.length>4){v.name=c+_[4];c+=_[4]}}m.push(v);if(typeof v.originalLine==="number"){d.push(v)}}}s(m,o.compareByGeneratedPositionsDeflated);this.__generatedMappings=m;s(d,o.compareByOriginalPositions);this.__originalMappings=d};BasicSourceMapConsumer.prototype._findMapping=function SourceMapConsumer_findMapping(e,r,n,t,o,a){if(e[n]<=0){throw new TypeError("Line must be greater than or equal to 1, got "+e[n])}if(e[t]<0){throw new TypeError("Column must be greater than or equal to 0, got "+e[t])}return i.search(e,r,o,a)};BasicSourceMapConsumer.prototype.computeColumnSpans=function SourceMapConsumer_computeColumnSpans(){for(var e=0;e=0){var t=this._generatedMappings[n];if(t.generatedLine===r.generatedLine){var i=o.getArg(t,"source",null);if(i!==null){i=this._sources.at(i);i=o.computeSourceURL(this.sourceRoot,i,this._sourceMapURL)}var a=o.getArg(t,"name",null);if(a!==null){a=this._names.at(a)}return{source:i,line:o.getArg(t,"originalLine",null),column:o.getArg(t,"originalColumn",null),name:a}}}return{source:null,line:null,column:null,name:null}};BasicSourceMapConsumer.prototype.hasContentsOfAllSources=function BasicSourceMapConsumer_hasContentsOfAllSources(){if(!this.sourcesContent){return false}return this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some((function(e){return e==null}))};BasicSourceMapConsumer.prototype.sourceContentFor=function SourceMapConsumer_sourceContentFor(e,r){if(!this.sourcesContent){return null}var n=this._findSourceIndex(e);if(n>=0){return this.sourcesContent[n]}var t=e;if(this.sourceRoot!=null){t=o.relative(this.sourceRoot,t)}var i;if(this.sourceRoot!=null&&(i=o.urlParse(this.sourceRoot))){var a=t.replace(/^file:\/\//,"");if(i.scheme=="file"&&this._sources.has(a)){return this.sourcesContent[this._sources.indexOf(a)]}if((!i.path||i.path=="/")&&this._sources.has("/"+t)){return this.sourcesContent[this._sources.indexOf("/"+t)]}}if(r){return null}else{throw new Error('"'+t+'" is not in the SourceMap.')}};BasicSourceMapConsumer.prototype.generatedPositionFor=function SourceMapConsumer_generatedPositionFor(e){var r=o.getArg(e,"source");r=this._findSourceIndex(r);if(r<0){return{line:null,column:null,lastColumn:null}}var n={source:r,originalLine:o.getArg(e,"line"),originalColumn:o.getArg(e,"column")};var t=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,o.getArg(e,"bias",SourceMapConsumer.GREATEST_LOWER_BOUND));if(t>=0){var i=this._originalMappings[t];if(i.source===n.source){return{line:o.getArg(i,"generatedLine",null),column:o.getArg(i,"generatedColumn",null),lastColumn:o.getArg(i,"lastGeneratedColumn",null)}}}return{line:null,column:null,lastColumn:null}};t=BasicSourceMapConsumer;function IndexedSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sections");if(t!=this._version){throw new Error("Unsupported version: "+t)}this._sources=new a;this._names=new a;var u={line:-1,column:0};this._sections=i.map((function(e){if(e.url){throw new Error("Support for url field in sections not implemented.")}var n=o.getArg(e,"offset");var t=o.getArg(n,"line");var i=o.getArg(n,"column");if(t{var t=n(449);var o=n(339);var i=n(274).I;var a=n(680).H;function SourceMapGenerator(e){if(!e){e={}}this._file=o.getArg(e,"file",null);this._sourceRoot=o.getArg(e,"sourceRoot",null);this._skipValidation=o.getArg(e,"skipValidation",false);this._sources=new i;this._names=new i;this._mappings=new a;this._sourcesContents=null}SourceMapGenerator.prototype._version=3;SourceMapGenerator.fromSourceMap=function SourceMapGenerator_fromSourceMap(e){var r=e.sourceRoot;var n=new SourceMapGenerator({file:e.file,sourceRoot:r});e.eachMapping((function(e){var t={generated:{line:e.generatedLine,column:e.generatedColumn}};if(e.source!=null){t.source=e.source;if(r!=null){t.source=o.relative(r,t.source)}t.original={line:e.originalLine,column:e.originalColumn};if(e.name!=null){t.name=e.name}}n.addMapping(t)}));e.sources.forEach((function(t){var i=t;if(r!==null){i=o.relative(r,t)}if(!n._sources.has(i)){n._sources.add(i)}var a=e.sourceContentFor(t);if(a!=null){n.setSourceContent(t,a)}}));return n};SourceMapGenerator.prototype.addMapping=function SourceMapGenerator_addMapping(e){var r=o.getArg(e,"generated");var n=o.getArg(e,"original",null);var t=o.getArg(e,"source",null);var i=o.getArg(e,"name",null);if(!this._skipValidation){this._validateMapping(r,n,t,i)}if(t!=null){t=String(t);if(!this._sources.has(t)){this._sources.add(t)}}if(i!=null){i=String(i);if(!this._names.has(i)){this._names.add(i)}}this._mappings.add({generatedLine:r.line,generatedColumn:r.column,originalLine:n!=null&&n.line,originalColumn:n!=null&&n.column,source:t,name:i})};SourceMapGenerator.prototype.setSourceContent=function SourceMapGenerator_setSourceContent(e,r){var n=e;if(this._sourceRoot!=null){n=o.relative(this._sourceRoot,n)}if(r!=null){if(!this._sourcesContents){this._sourcesContents=Object.create(null)}this._sourcesContents[o.toSetString(n)]=r}else if(this._sourcesContents){delete this._sourcesContents[o.toSetString(n)];if(Object.keys(this._sourcesContents).length===0){this._sourcesContents=null}}};SourceMapGenerator.prototype.applySourceMap=function SourceMapGenerator_applySourceMap(e,r,n){var t=r;if(r==null){if(e.file==null){throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, "+'or the source map\'s "file" property. Both were omitted.')}t=e.file}var a=this._sourceRoot;if(a!=null){t=o.relative(a,t)}var u=new i;var s=new i;this._mappings.unsortedForEach((function(r){if(r.source===t&&r.originalLine!=null){var i=e.originalPositionFor({line:r.originalLine,column:r.originalColumn});if(i.source!=null){r.source=i.source;if(n!=null){r.source=o.join(n,r.source)}if(a!=null){r.source=o.relative(a,r.source)}r.originalLine=i.line;r.originalColumn=i.column;if(i.name!=null){r.name=i.name}}}var l=r.source;if(l!=null&&!u.has(l)){u.add(l)}var c=r.name;if(c!=null&&!s.has(c)){s.add(c)}}),this);this._sources=u;this._names=s;e.sources.forEach((function(r){var t=e.sourceContentFor(r);if(t!=null){if(n!=null){r=o.join(n,r)}if(a!=null){r=o.relative(a,r)}this.setSourceContent(r,t)}}),this)};SourceMapGenerator.prototype._validateMapping=function SourceMapGenerator_validateMapping(e,r,n,t){if(r&&typeof r.line!=="number"&&typeof r.column!=="number"){throw new Error("original.line and original.column are not numbers -- you probably meant to omit "+"the original mapping entirely and only map the generated position. If so, pass "+"null for the original mapping instead of an object with empty or null values.")}if(e&&"line"in e&&"column"in e&&e.line>0&&e.column>=0&&!r&&!n&&!t){return}else if(e&&"line"in e&&"column"in e&&r&&"line"in r&&"column"in r&&e.line>0&&e.column>=0&&r.line>0&&r.column>=0&&n){return}else{throw new Error("Invalid mapping: "+JSON.stringify({generated:e,source:n,original:r,name:t}))}};SourceMapGenerator.prototype._serializeMappings=function SourceMapGenerator_serializeMappings(){var e=0;var r=1;var n=0;var i=0;var a=0;var u=0;var s="";var l;var c;var p;var f;var g=this._mappings.toArray();for(var h=0,d=g.length;h0){if(!o.compareByGeneratedPositionsInflated(c,g[h-1])){continue}l+=","}}l+=t.encode(c.generatedColumn-e);e=c.generatedColumn;if(c.source!=null){f=this._sources.indexOf(c.source);l+=t.encode(f-u);u=f;l+=t.encode(c.originalLine-1-i);i=c.originalLine-1;l+=t.encode(c.originalColumn-n);n=c.originalColumn;if(c.name!=null){p=this._names.indexOf(c.name);l+=t.encode(p-a);a=p}}s+=l}return s};SourceMapGenerator.prototype._generateSourcesContent=function SourceMapGenerator_generateSourcesContent(e,r){return e.map((function(e){if(!this._sourcesContents){return null}if(r!=null){e=o.relative(r,e)}var n=o.toSetString(e);return Object.prototype.hasOwnProperty.call(this._sourcesContents,n)?this._sourcesContents[n]:null}),this)};SourceMapGenerator.prototype.toJSON=function SourceMapGenerator_toJSON(){var e={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};if(this._file!=null){e.file=this._file}if(this._sourceRoot!=null){e.sourceRoot=this._sourceRoot}if(this._sourcesContents){e.sourcesContent=this._generateSourcesContent(e.sources,e.sourceRoot)}return e};SourceMapGenerator.prototype.toString=function SourceMapGenerator_toString(){return JSON.stringify(this.toJSON())};r.h=SourceMapGenerator},351:(e,r,n)=>{var t;var o=n(591).h;var i=n(339);var a=/(\r?\n)/;var u=10;var s="$$$isSourceNode$$$";function SourceNode(e,r,n,t,o){this.children=[];this.sourceContents={};this.line=e==null?null:e;this.column=r==null?null:r;this.source=n==null?null:n;this.name=o==null?null:o;this[s]=true;if(t!=null)this.add(t)}SourceNode.fromStringWithSourceMap=function SourceNode_fromStringWithSourceMap(e,r,n){var t=new SourceNode;var o=e.split(a);var u=0;var shiftNextLine=function(){var e=getNextLine();var r=getNextLine()||"";return e+r;function getNextLine(){return u=0;r--){this.prepend(e[r])}}else if(e[s]||typeof e==="string"){this.children.unshift(e)}else{throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+e)}return this};SourceNode.prototype.walk=function SourceNode_walk(e){var r;for(var n=0,t=this.children.length;n0){r=[];for(n=0;n{function getArg(e,r,n){if(r in e){return e[r]}else if(arguments.length===3){return n}else{throw new Error('"'+r+'" is a required argument.')}}r.getArg=getArg;var n=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;var t=/^data:.+\,.+$/;function urlParse(e){var r=e.match(n);if(!r){return null}return{scheme:r[1],auth:r[2],host:r[3],port:r[4],path:r[5]}}r.urlParse=urlParse;function urlGenerate(e){var r="";if(e.scheme){r+=e.scheme+":"}r+="//";if(e.auth){r+=e.auth+"@"}if(e.host){r+=e.host}if(e.port){r+=":"+e.port}if(e.path){r+=e.path}return r}r.urlGenerate=urlGenerate;function normalize(e){var n=e;var t=urlParse(e);if(t){if(!t.path){return e}n=t.path}var o=r.isAbsolute(n);var i=n.split(/\/+/);for(var a,u=0,s=i.length-1;s>=0;s--){a=i[s];if(a==="."){i.splice(s,1)}else if(a===".."){u++}else if(u>0){if(a===""){i.splice(s+1,u);u=0}else{i.splice(s,2);u--}}}n=i.join("/");if(n===""){n=o?"/":"."}if(t){t.path=n;return urlGenerate(t)}return n}r.normalize=normalize;function join(e,r){if(e===""){e="."}if(r===""){r="."}var n=urlParse(r);var o=urlParse(e);if(o){e=o.path||"/"}if(n&&!n.scheme){if(o){n.scheme=o.scheme}return urlGenerate(n)}if(n||r.match(t)){return r}if(o&&!o.host&&!o.path){o.host=r;return urlGenerate(o)}var i=r.charAt(0)==="/"?r:normalize(e.replace(/\/+$/,"")+"/"+r);if(o){o.path=i;return urlGenerate(o)}return i}r.join=join;r.isAbsolute=function(e){return e.charAt(0)==="/"||n.test(e)};function relative(e,r){if(e===""){e="."}e=e.replace(/\/$/,"");var n=0;while(r.indexOf(e+"/")!==0){var t=e.lastIndexOf("/");if(t<0){return r}e=e.slice(0,t);if(e.match(/^([^\/]+:\/)?\/*$/)){return r}++n}return Array(n+1).join("../")+r.substr(e.length+1)}r.relative=relative;var o=function(){var e=Object.create(null);return!("__proto__"in e)}();function identity(e){return e}function toSetString(e){if(isProtoString(e)){return"$"+e}return e}r.toSetString=o?identity:toSetString;function fromSetString(e){if(isProtoString(e)){return e.slice(1)}return e}r.fromSetString=o?identity:fromSetString;function isProtoString(e){if(!e){return false}var r=e.length;if(r<9){return false}if(e.charCodeAt(r-1)!==95||e.charCodeAt(r-2)!==95||e.charCodeAt(r-3)!==111||e.charCodeAt(r-4)!==116||e.charCodeAt(r-5)!==111||e.charCodeAt(r-6)!==114||e.charCodeAt(r-7)!==112||e.charCodeAt(r-8)!==95||e.charCodeAt(r-9)!==95){return false}for(var n=r-10;n>=0;n--){if(e.charCodeAt(n)!==36){return false}}return true}function compareByOriginalPositions(e,r,n){var t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0||n){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0){return t}t=e.generatedLine-r.generatedLine;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByOriginalPositions=compareByOriginalPositions;function compareByGeneratedPositionsDeflated(e,r,n){var t=e.generatedLine-r.generatedLine;if(t!==0){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0||n){return t}t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsDeflated=compareByGeneratedPositionsDeflated;function strcmp(e,r){if(e===r){return 0}if(e===null){return 1}if(r===null){return-1}if(e>r){return 1}return-1}function compareByGeneratedPositionsInflated(e,r){var n=e.generatedLine-r.generatedLine;if(n!==0){return n}n=e.generatedColumn-r.generatedColumn;if(n!==0){return n}n=strcmp(e.source,r.source);if(n!==0){return n}n=e.originalLine-r.originalLine;if(n!==0){return n}n=e.originalColumn-r.originalColumn;if(n!==0){return n}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsInflated=compareByGeneratedPositionsInflated;function parseSourceMapInput(e){return JSON.parse(e.replace(/^\)]}'[^\n]*\n/,""))}r.parseSourceMapInput=parseSourceMapInput;function computeSourceURL(e,r,n){r=r||"";if(e){if(e[e.length-1]!=="/"&&r[0]!=="/"){e+="/"}r=e+r}if(n){var t=urlParse(n);if(!t){throw new Error("sourceMapURL could not be parsed")}if(t.path){var o=t.path.lastIndexOf("/");if(o>=0){t.path=t.path.substring(0,o+1)}}r=join(urlGenerate(t),r)}return normalize(r)}r.computeSourceURL=computeSourceURL},997:(e,r,n)=>{n(591).h;r.SourceMapConsumer=n(952).SourceMapConsumer;n(351)},284:(e,r,n)=>{e=n.nmd(e);var t=n(997).SourceMapConsumer;var o=n(17);var i;try{i=n(147);if(!i.existsSync||!i.readFileSync){i=null}}catch(e){}var a=n(650);function dynamicRequire(e,r){return e.require(r)}var u=false;var s=false;var l=false;var c="auto";var p={};var f={};var g=/^data:application\/json[^,]+base64,/;var h=[];var d=[];function isInBrowser(){if(c==="browser")return true;if(c==="node")return false;return typeof window!=="undefined"&&typeof XMLHttpRequest==="function"&&!(window.require&&window.module&&window.process&&window.process.type==="renderer")}function hasGlobalProcessEventEmitter(){return typeof process==="object"&&process!==null&&typeof process.on==="function"}function globalProcessVersion(){if(typeof process==="object"&&process!==null){return process.version}else{return""}}function globalProcessStderr(){if(typeof process==="object"&&process!==null){return process.stderr}}function globalProcessExit(e){if(typeof process==="object"&&process!==null&&typeof process.exit==="function"){return process.exit(e)}}function handlerExec(e){return function(r){for(var n=0;n"}var n=this.getLineNumber();if(n!=null){r+=":"+n;var t=this.getColumnNumber();if(t){r+=":"+t}}}var o="";var i=this.getFunctionName();var a=true;var u=this.isConstructor();var s=!(this.isToplevel()||u);if(s){var l=this.getTypeName();if(l==="[object Object]"){l="null"}var c=this.getMethodName();if(i){if(l&&i.indexOf(l)!=0){o+=l+"."}o+=i;if(c&&i.indexOf("."+c)!=i.length-c.length-1){o+=" [as "+c+"]"}}else{o+=l+"."+(c||"")}}else if(u){o+="new "+(i||"")}else if(i){o+=i}else{o+=r;a=false}if(a){o+=" ("+r+")"}return o}function cloneCallSite(e){var r={};Object.getOwnPropertyNames(Object.getPrototypeOf(e)).forEach((function(n){r[n]=/^(?:is|get)/.test(n)?function(){return e[n].call(e)}:e[n]}));r.toString=CallSiteToString;return r}function wrapCallSite(e,r){if(r===undefined){r={nextPosition:null,curPosition:null}}if(e.isNative()){r.curPosition=null;return e}var n=e.getFileName()||e.getScriptNameOrSourceURL();if(n){var t=e.getLineNumber();var o=e.getColumnNumber()-1;var i=/^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/;var a=i.test(globalProcessVersion())?0:62;if(t===1&&o>a&&!isInBrowser()&&!e.isEval()){o-=a}var u=mapSourcePosition({source:n,line:t,column:o});r.curPosition=u;e=cloneCallSite(e);var s=e.getFunctionName;e.getFunctionName=function(){if(r.nextPosition==null){return s()}return r.nextPosition.name||s()};e.getFileName=function(){return u.source};e.getLineNumber=function(){return u.line};e.getColumnNumber=function(){return u.column+1};e.getScriptNameOrSourceURL=function(){return u.source};return e}var l=e.isEval()&&e.getEvalOrigin();if(l){l=mapEvalOrigin(l);e=cloneCallSite(e);e.getEvalOrigin=function(){return l};return e}return e}function prepareStackTrace(e,r){if(l){p={};f={}}var n=e.name||"Error";var t=e.message||"";var o=n+": "+t;var i={nextPosition:null,curPosition:null};var a=[];for(var u=r.length-1;u>=0;u--){a.push("\n at "+wrapCallSite(r[u],i));i.nextPosition=i.curPosition}i.curPosition=i.nextPosition=null;return o+a.reverse().join("")}function getErrorSource(e){var r=/\n at [^(]+ \((.*):(\d+):(\d+)\)/.exec(e.stack);if(r){var n=r[1];var t=+r[2];var o=+r[3];var a=p[n];if(!a&&i&&i.existsSync(n)){try{a=i.readFileSync(n,"utf8")}catch(e){a=""}}if(a){var u=a.split(/(?:\r\n|\r|\n)/)[t-1];if(u){return n+":"+t+"\n"+u+"\n"+new Array(o).join(" ")+"^"}}}return null}function printErrorAndExit(e){var r=getErrorSource(e);var n=globalProcessStderr();if(n&&n._handle&&n._handle.setBlocking){n._handle.setBlocking(true)}if(r){console.error();console.error(r)}console.error(e.stack);globalProcessExit(1)}function shimEmitUncaughtException(){var e=process.emit;process.emit=function(r){if(r==="uncaughtException"){var n=arguments[1]&&arguments[1].stack;var t=this.listeners(r).length>0;if(n&&!t){return printErrorAndExit(arguments[1])}}return e.apply(this,arguments)}}var S=h.slice(0);var _=d.slice(0);r.wrapCallSite=wrapCallSite;r.getErrorSource=getErrorSource;r.mapSourcePosition=mapSourcePosition;r.retrieveSourceMap=v;r.install=function(r){r=r||{};if(r.environment){c=r.environment;if(["node","browser","auto"].indexOf(c)===-1){throw new Error("environment "+c+" was unknown. Available options are {auto, browser, node}")}}if(r.retrieveFile){if(r.overrideRetrieveFile){h.length=0}h.unshift(r.retrieveFile)}if(r.retrieveSourceMap){if(r.overrideRetrieveSourceMap){d.length=0}d.unshift(r.retrieveSourceMap)}if(r.hookRequire&&!isInBrowser()){var n=dynamicRequire(e,"module");var t=n.prototype._compile;if(!t.__sourceMapSupport){n.prototype._compile=function(e,r){p[r]=e;f[r]=undefined;return t.call(this,e,r)};n.prototype._compile.__sourceMapSupport=true}}if(!l){l="emptyCacheBetweenOperations"in r?r.emptyCacheBetweenOperations:false}if(!u){u=true;Error.prepareStackTrace=prepareStackTrace}if(!s){var o="handleUncaughtExceptions"in r?r.handleUncaughtExceptions:true;try{var i=dynamicRequire(e,"worker_threads");if(i.isMainThread===false){o=false}}catch(e){}if(o&&hasGlobalProcessEventEmitter()){s=true;shimEmitUncaughtException()}}};r.resetRetrieveHandlers=function(){h.length=0;d.length=0;h=S.slice(0);d=_.slice(0);v=handlerExec(d);m=handlerExec(h)}},147:e=>{"use strict";e.exports=require("fs")},17:e=>{"use strict";e.exports=require("path")}};var r={};function __webpack_require__(n){var t=r[n];if(t!==undefined){return t.exports}var o=r[n]={id:n,loaded:false,exports:{}};var i=true;try{e[n](o,o.exports,__webpack_require__);i=false}finally{if(i)delete r[n]}o.loaded=true;return o.exports}(()=>{__webpack_require__.nmd=e=>{e.paths=[];if(!e.children)e.children=[];return e}})();if(typeof __webpack_require__!=="undefined")__webpack_require__.ab=__dirname+"/";var n={};(()=>{__webpack_require__(284).install()})();module.exports=n})(); -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unity-xcode-builder", 3 | "version": "1.3.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "unity-xcode-builder", 9 | "version": "1.3.1", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@actions/core": "^1.11.1", 13 | "@actions/exec": "^1.1.1", 14 | "@actions/github": "^6.0.1", 15 | "@actions/glob": "^0.5.0", 16 | "@rage-against-the-pixel/app-store-connect-api": "^3.8.0", 17 | "plist": "^3.1.0", 18 | "semver": "^7.7.2", 19 | "uuid": "^10.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^22.15.21", 23 | "@types/plist": "^3.0.5", 24 | "@types/semver": "^7.7.0", 25 | "@types/uuid": "^10.0.0", 26 | "@vercel/ncc": "^0.34.0", 27 | "shx": "^0.3.4", 28 | "typescript": "^5.8.3" 29 | } 30 | }, 31 | "node_modules/@actions/core": { 32 | "version": "1.11.1", 33 | "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", 34 | "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", 35 | "license": "MIT", 36 | "dependencies": { 37 | "@actions/exec": "^1.1.1", 38 | "@actions/http-client": "^2.0.1" 39 | } 40 | }, 41 | "node_modules/@actions/exec": { 42 | "version": "1.1.1", 43 | "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", 44 | "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", 45 | "license": "MIT", 46 | "dependencies": { 47 | "@actions/io": "^1.0.1" 48 | } 49 | }, 50 | "node_modules/@actions/github": { 51 | "version": "6.0.1", 52 | "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", 53 | "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", 54 | "license": "MIT", 55 | "dependencies": { 56 | "@actions/http-client": "^2.2.0", 57 | "@octokit/core": "^5.0.1", 58 | "@octokit/plugin-paginate-rest": "^9.2.2", 59 | "@octokit/plugin-rest-endpoint-methods": "^10.4.0", 60 | "@octokit/request": "^8.4.1", 61 | "@octokit/request-error": "^5.1.1", 62 | "undici": "^5.28.5" 63 | } 64 | }, 65 | "node_modules/@actions/glob": { 66 | "version": "0.5.0", 67 | "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.5.0.tgz", 68 | "integrity": "sha512-tST2rjPvJLRZLuT9NMUtyBjvj9Yo0MiJS3ow004slMvm8GFM+Zv9HvMJ7HWzfUyJnGrJvDsYkWBaaG3YKXRtCw==", 69 | "license": "MIT", 70 | "dependencies": { 71 | "@actions/core": "^1.9.1", 72 | "minimatch": "^3.0.4" 73 | } 74 | }, 75 | "node_modules/@actions/http-client": { 76 | "version": "2.2.3", 77 | "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", 78 | "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", 79 | "license": "MIT", 80 | "dependencies": { 81 | "tunnel": "^0.0.6", 82 | "undici": "^5.25.4" 83 | } 84 | }, 85 | "node_modules/@actions/io": { 86 | "version": "1.1.3", 87 | "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", 88 | "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", 89 | "license": "MIT" 90 | }, 91 | "node_modules/@fastify/busboy": { 92 | "version": "2.1.1", 93 | "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", 94 | "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", 95 | "license": "MIT", 96 | "engines": { 97 | "node": ">=14" 98 | } 99 | }, 100 | "node_modules/@hey-api/client-fetch": { 101 | "version": "0.4.4", 102 | "resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.4.4.tgz", 103 | "integrity": "sha512-ebh1JjUdMAqes/Rg8OvbjDqGWGNhgHgmPtHlkIOUtj3y2mUXqX2g9sVoI/rSKW/FdADPng/90k5AL7bwT8W2lA==", 104 | "license": "MIT", 105 | "funding": { 106 | "url": "https://github.com/sponsors/hey-api" 107 | } 108 | }, 109 | "node_modules/@octokit/auth-token": { 110 | "version": "4.0.0", 111 | "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", 112 | "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", 113 | "license": "MIT", 114 | "engines": { 115 | "node": ">= 18" 116 | } 117 | }, 118 | "node_modules/@octokit/core": { 119 | "version": "5.2.1", 120 | "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", 121 | "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", 122 | "license": "MIT", 123 | "dependencies": { 124 | "@octokit/auth-token": "^4.0.0", 125 | "@octokit/graphql": "^7.1.0", 126 | "@octokit/request": "^8.4.1", 127 | "@octokit/request-error": "^5.1.1", 128 | "@octokit/types": "^13.0.0", 129 | "before-after-hook": "^2.2.0", 130 | "universal-user-agent": "^6.0.0" 131 | }, 132 | "engines": { 133 | "node": ">= 18" 134 | } 135 | }, 136 | "node_modules/@octokit/endpoint": { 137 | "version": "9.0.6", 138 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", 139 | "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", 140 | "license": "MIT", 141 | "dependencies": { 142 | "@octokit/types": "^13.1.0", 143 | "universal-user-agent": "^6.0.0" 144 | }, 145 | "engines": { 146 | "node": ">= 18" 147 | } 148 | }, 149 | "node_modules/@octokit/graphql": { 150 | "version": "7.1.1", 151 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", 152 | "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", 153 | "license": "MIT", 154 | "dependencies": { 155 | "@octokit/request": "^8.4.1", 156 | "@octokit/types": "^13.0.0", 157 | "universal-user-agent": "^6.0.0" 158 | }, 159 | "engines": { 160 | "node": ">= 18" 161 | } 162 | }, 163 | "node_modules/@octokit/openapi-types": { 164 | "version": "24.2.0", 165 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", 166 | "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", 167 | "license": "MIT" 168 | }, 169 | "node_modules/@octokit/plugin-paginate-rest": { 170 | "version": "9.2.2", 171 | "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", 172 | "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", 173 | "license": "MIT", 174 | "dependencies": { 175 | "@octokit/types": "^12.6.0" 176 | }, 177 | "engines": { 178 | "node": ">= 18" 179 | }, 180 | "peerDependencies": { 181 | "@octokit/core": "5" 182 | } 183 | }, 184 | "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { 185 | "version": "20.0.0", 186 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", 187 | "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", 188 | "license": "MIT" 189 | }, 190 | "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { 191 | "version": "12.6.0", 192 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", 193 | "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", 194 | "license": "MIT", 195 | "dependencies": { 196 | "@octokit/openapi-types": "^20.0.0" 197 | } 198 | }, 199 | "node_modules/@octokit/plugin-rest-endpoint-methods": { 200 | "version": "10.4.1", 201 | "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", 202 | "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", 203 | "license": "MIT", 204 | "dependencies": { 205 | "@octokit/types": "^12.6.0" 206 | }, 207 | "engines": { 208 | "node": ">= 18" 209 | }, 210 | "peerDependencies": { 211 | "@octokit/core": "5" 212 | } 213 | }, 214 | "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { 215 | "version": "20.0.0", 216 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", 217 | "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", 218 | "license": "MIT" 219 | }, 220 | "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { 221 | "version": "12.6.0", 222 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", 223 | "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", 224 | "license": "MIT", 225 | "dependencies": { 226 | "@octokit/openapi-types": "^20.0.0" 227 | } 228 | }, 229 | "node_modules/@octokit/request": { 230 | "version": "8.4.1", 231 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", 232 | "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", 233 | "license": "MIT", 234 | "dependencies": { 235 | "@octokit/endpoint": "^9.0.6", 236 | "@octokit/request-error": "^5.1.1", 237 | "@octokit/types": "^13.1.0", 238 | "universal-user-agent": "^6.0.0" 239 | }, 240 | "engines": { 241 | "node": ">= 18" 242 | } 243 | }, 244 | "node_modules/@octokit/request-error": { 245 | "version": "5.1.1", 246 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", 247 | "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", 248 | "license": "MIT", 249 | "dependencies": { 250 | "@octokit/types": "^13.1.0", 251 | "deprecation": "^2.0.0", 252 | "once": "^1.4.0" 253 | }, 254 | "engines": { 255 | "node": ">= 18" 256 | } 257 | }, 258 | "node_modules/@octokit/types": { 259 | "version": "13.10.0", 260 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", 261 | "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", 262 | "license": "MIT", 263 | "dependencies": { 264 | "@octokit/openapi-types": "^24.2.0" 265 | } 266 | }, 267 | "node_modules/@rage-against-the-pixel/app-store-connect-api": { 268 | "version": "3.8.0", 269 | "resolved": "https://registry.npmjs.org/@rage-against-the-pixel/app-store-connect-api/-/app-store-connect-api-3.8.0.tgz", 270 | "integrity": "sha512-DowjZM4Ei8vrS5Ad98LfFlCNOgUcNGE8X3vtN/QweN/FTn+WWxtgNsK8hx7iMoPi2a4dYFdrBMoSWdb6VlTZIg==", 271 | "license": "MIT", 272 | "dependencies": { 273 | "@hey-api/client-fetch": "^0.4.4", 274 | "jose": "^5.9.6" 275 | } 276 | }, 277 | "node_modules/@types/node": { 278 | "version": "22.15.21", 279 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", 280 | "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", 281 | "dev": true, 282 | "license": "MIT", 283 | "dependencies": { 284 | "undici-types": "~6.21.0" 285 | } 286 | }, 287 | "node_modules/@types/plist": { 288 | "version": "3.0.5", 289 | "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", 290 | "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", 291 | "dev": true, 292 | "license": "MIT", 293 | "dependencies": { 294 | "@types/node": "*", 295 | "xmlbuilder": ">=11.0.1" 296 | } 297 | }, 298 | "node_modules/@types/semver": { 299 | "version": "7.7.0", 300 | "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", 301 | "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", 302 | "dev": true, 303 | "license": "MIT" 304 | }, 305 | "node_modules/@types/uuid": { 306 | "version": "10.0.0", 307 | "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", 308 | "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", 309 | "dev": true, 310 | "license": "MIT" 311 | }, 312 | "node_modules/@vercel/ncc": { 313 | "version": "0.34.0", 314 | "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.34.0.tgz", 315 | "integrity": "sha512-G9h5ZLBJ/V57Ou9vz5hI8pda/YQX5HQszCs3AmIus3XzsmRn/0Ptic5otD3xVST8QLKk7AMk7AqpsyQGN7MZ9A==", 316 | "dev": true, 317 | "license": "MIT", 318 | "bin": { 319 | "ncc": "dist/ncc/cli.js" 320 | } 321 | }, 322 | "node_modules/@xmldom/xmldom": { 323 | "version": "0.8.10", 324 | "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", 325 | "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", 326 | "license": "MIT", 327 | "engines": { 328 | "node": ">=10.0.0" 329 | } 330 | }, 331 | "node_modules/balanced-match": { 332 | "version": "1.0.2", 333 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 334 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 335 | "license": "MIT" 336 | }, 337 | "node_modules/base64-js": { 338 | "version": "1.5.1", 339 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 340 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 341 | "funding": [ 342 | { 343 | "type": "github", 344 | "url": "https://github.com/sponsors/feross" 345 | }, 346 | { 347 | "type": "patreon", 348 | "url": "https://www.patreon.com/feross" 349 | }, 350 | { 351 | "type": "consulting", 352 | "url": "https://feross.org/support" 353 | } 354 | ], 355 | "license": "MIT" 356 | }, 357 | "node_modules/before-after-hook": { 358 | "version": "2.2.3", 359 | "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", 360 | "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", 361 | "license": "Apache-2.0" 362 | }, 363 | "node_modules/brace-expansion": { 364 | "version": "1.1.11", 365 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 366 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 367 | "license": "MIT", 368 | "dependencies": { 369 | "balanced-match": "^1.0.0", 370 | "concat-map": "0.0.1" 371 | } 372 | }, 373 | "node_modules/concat-map": { 374 | "version": "0.0.1", 375 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 376 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 377 | "license": "MIT" 378 | }, 379 | "node_modules/deprecation": { 380 | "version": "2.3.1", 381 | "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", 382 | "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", 383 | "license": "ISC" 384 | }, 385 | "node_modules/fs.realpath": { 386 | "version": "1.0.0", 387 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 388 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 389 | "dev": true, 390 | "license": "ISC" 391 | }, 392 | "node_modules/function-bind": { 393 | "version": "1.1.2", 394 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 395 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 396 | "dev": true, 397 | "license": "MIT", 398 | "funding": { 399 | "url": "https://github.com/sponsors/ljharb" 400 | } 401 | }, 402 | "node_modules/glob": { 403 | "version": "7.2.3", 404 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 405 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 406 | "deprecated": "Glob versions prior to v9 are no longer supported", 407 | "dev": true, 408 | "license": "ISC", 409 | "dependencies": { 410 | "fs.realpath": "^1.0.0", 411 | "inflight": "^1.0.4", 412 | "inherits": "2", 413 | "minimatch": "^3.1.1", 414 | "once": "^1.3.0", 415 | "path-is-absolute": "^1.0.0" 416 | }, 417 | "engines": { 418 | "node": "*" 419 | }, 420 | "funding": { 421 | "url": "https://github.com/sponsors/isaacs" 422 | } 423 | }, 424 | "node_modules/hasown": { 425 | "version": "2.0.2", 426 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 427 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 428 | "dev": true, 429 | "license": "MIT", 430 | "dependencies": { 431 | "function-bind": "^1.1.2" 432 | }, 433 | "engines": { 434 | "node": ">= 0.4" 435 | } 436 | }, 437 | "node_modules/inflight": { 438 | "version": "1.0.6", 439 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 440 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 441 | "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", 442 | "dev": true, 443 | "license": "ISC", 444 | "dependencies": { 445 | "once": "^1.3.0", 446 | "wrappy": "1" 447 | } 448 | }, 449 | "node_modules/inherits": { 450 | "version": "2.0.4", 451 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 452 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 453 | "dev": true, 454 | "license": "ISC" 455 | }, 456 | "node_modules/interpret": { 457 | "version": "1.4.0", 458 | "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", 459 | "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", 460 | "dev": true, 461 | "license": "MIT", 462 | "engines": { 463 | "node": ">= 0.10" 464 | } 465 | }, 466 | "node_modules/is-core-module": { 467 | "version": "2.16.1", 468 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", 469 | "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", 470 | "dev": true, 471 | "license": "MIT", 472 | "dependencies": { 473 | "hasown": "^2.0.2" 474 | }, 475 | "engines": { 476 | "node": ">= 0.4" 477 | }, 478 | "funding": { 479 | "url": "https://github.com/sponsors/ljharb" 480 | } 481 | }, 482 | "node_modules/jose": { 483 | "version": "5.10.0", 484 | "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", 485 | "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", 486 | "license": "MIT", 487 | "funding": { 488 | "url": "https://github.com/sponsors/panva" 489 | } 490 | }, 491 | "node_modules/minimatch": { 492 | "version": "3.1.2", 493 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 494 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 495 | "license": "ISC", 496 | "dependencies": { 497 | "brace-expansion": "^1.1.7" 498 | }, 499 | "engines": { 500 | "node": "*" 501 | } 502 | }, 503 | "node_modules/minimist": { 504 | "version": "1.2.8", 505 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 506 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 507 | "dev": true, 508 | "license": "MIT", 509 | "funding": { 510 | "url": "https://github.com/sponsors/ljharb" 511 | } 512 | }, 513 | "node_modules/once": { 514 | "version": "1.4.0", 515 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 516 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 517 | "license": "ISC", 518 | "dependencies": { 519 | "wrappy": "1" 520 | } 521 | }, 522 | "node_modules/path-is-absolute": { 523 | "version": "1.0.1", 524 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 525 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 526 | "dev": true, 527 | "license": "MIT", 528 | "engines": { 529 | "node": ">=0.10.0" 530 | } 531 | }, 532 | "node_modules/path-parse": { 533 | "version": "1.0.7", 534 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 535 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 536 | "dev": true, 537 | "license": "MIT" 538 | }, 539 | "node_modules/plist": { 540 | "version": "3.1.0", 541 | "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", 542 | "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", 543 | "license": "MIT", 544 | "dependencies": { 545 | "@xmldom/xmldom": "^0.8.8", 546 | "base64-js": "^1.5.1", 547 | "xmlbuilder": "^15.1.1" 548 | }, 549 | "engines": { 550 | "node": ">=10.4.0" 551 | } 552 | }, 553 | "node_modules/rechoir": { 554 | "version": "0.6.2", 555 | "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", 556 | "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", 557 | "dev": true, 558 | "dependencies": { 559 | "resolve": "^1.1.6" 560 | }, 561 | "engines": { 562 | "node": ">= 0.10" 563 | } 564 | }, 565 | "node_modules/resolve": { 566 | "version": "1.22.10", 567 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", 568 | "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", 569 | "dev": true, 570 | "license": "MIT", 571 | "dependencies": { 572 | "is-core-module": "^2.16.0", 573 | "path-parse": "^1.0.7", 574 | "supports-preserve-symlinks-flag": "^1.0.0" 575 | }, 576 | "bin": { 577 | "resolve": "bin/resolve" 578 | }, 579 | "engines": { 580 | "node": ">= 0.4" 581 | }, 582 | "funding": { 583 | "url": "https://github.com/sponsors/ljharb" 584 | } 585 | }, 586 | "node_modules/semver": { 587 | "version": "7.7.2", 588 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", 589 | "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", 590 | "license": "ISC", 591 | "bin": { 592 | "semver": "bin/semver.js" 593 | }, 594 | "engines": { 595 | "node": ">=10" 596 | } 597 | }, 598 | "node_modules/shelljs": { 599 | "version": "0.8.5", 600 | "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", 601 | "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", 602 | "dev": true, 603 | "license": "BSD-3-Clause", 604 | "dependencies": { 605 | "glob": "^7.0.0", 606 | "interpret": "^1.0.0", 607 | "rechoir": "^0.6.2" 608 | }, 609 | "bin": { 610 | "shjs": "bin/shjs" 611 | }, 612 | "engines": { 613 | "node": ">=4" 614 | } 615 | }, 616 | "node_modules/shx": { 617 | "version": "0.3.4", 618 | "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", 619 | "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", 620 | "dev": true, 621 | "license": "MIT", 622 | "dependencies": { 623 | "minimist": "^1.2.3", 624 | "shelljs": "^0.8.5" 625 | }, 626 | "bin": { 627 | "shx": "lib/cli.js" 628 | }, 629 | "engines": { 630 | "node": ">=6" 631 | } 632 | }, 633 | "node_modules/supports-preserve-symlinks-flag": { 634 | "version": "1.0.0", 635 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 636 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 637 | "dev": true, 638 | "license": "MIT", 639 | "engines": { 640 | "node": ">= 0.4" 641 | }, 642 | "funding": { 643 | "url": "https://github.com/sponsors/ljharb" 644 | } 645 | }, 646 | "node_modules/tunnel": { 647 | "version": "0.0.6", 648 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", 649 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", 650 | "license": "MIT", 651 | "engines": { 652 | "node": ">=0.6.11 <=0.7.0 || >=0.7.3" 653 | } 654 | }, 655 | "node_modules/typescript": { 656 | "version": "5.8.3", 657 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 658 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 659 | "dev": true, 660 | "license": "Apache-2.0", 661 | "bin": { 662 | "tsc": "bin/tsc", 663 | "tsserver": "bin/tsserver" 664 | }, 665 | "engines": { 666 | "node": ">=14.17" 667 | } 668 | }, 669 | "node_modules/undici": { 670 | "version": "5.29.0", 671 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", 672 | "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", 673 | "license": "MIT", 674 | "dependencies": { 675 | "@fastify/busboy": "^2.0.0" 676 | }, 677 | "engines": { 678 | "node": ">=14.0" 679 | } 680 | }, 681 | "node_modules/undici-types": { 682 | "version": "6.21.0", 683 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 684 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 685 | "dev": true, 686 | "license": "MIT" 687 | }, 688 | "node_modules/universal-user-agent": { 689 | "version": "6.0.1", 690 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", 691 | "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", 692 | "license": "ISC" 693 | }, 694 | "node_modules/uuid": { 695 | "version": "10.0.0", 696 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", 697 | "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", 698 | "funding": [ 699 | "https://github.com/sponsors/broofa", 700 | "https://github.com/sponsors/ctavan" 701 | ], 702 | "license": "MIT", 703 | "bin": { 704 | "uuid": "dist/bin/uuid" 705 | } 706 | }, 707 | "node_modules/wrappy": { 708 | "version": "1.0.2", 709 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 710 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 711 | "license": "ISC" 712 | }, 713 | "node_modules/xmlbuilder": { 714 | "version": "15.1.1", 715 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", 716 | "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", 717 | "license": "MIT", 718 | "engines": { 719 | "node": ">=8.0" 720 | } 721 | } 722 | } 723 | } 724 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unity-xcode-builder", 3 | "version": "1.3.1", 4 | "description": "A GitHub Action to build, archive, and upload Unity exported xcode projects.", 5 | "author": "buildalon", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/buildalon/unity-xcode-builder.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/buildalon/unity-xcode-builder/issues" 13 | }, 14 | "homepage": "https://github.com/buildalon/unity-xcode-builder", 15 | "main": "dist/index.js", 16 | "keywords": [], 17 | "dependencies": { 18 | "@actions/core": "^1.11.1", 19 | "@actions/exec": "^1.1.1", 20 | "@actions/github": "^6.0.1", 21 | "@actions/glob": "^0.5.0", 22 | "@rage-against-the-pixel/app-store-connect-api": "^3.8.0", 23 | "plist": "^3.1.0", 24 | "semver": "^7.7.2", 25 | "uuid": "^10.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^22.15.21", 29 | "@types/plist": "^3.0.5", 30 | "@types/semver": "^7.7.0", 31 | "@types/uuid": "^10.0.0", 32 | "@vercel/ncc": "^0.34.0", 33 | "shx": "^0.3.4", 34 | "typescript": "^5.8.3" 35 | }, 36 | "scripts": { 37 | "build": "npm run clean && npm run bundle", 38 | "bundle": "ncc build src/index.ts -o dist --source-map --license licenses.txt", 39 | "watch": "ncc build src/index.ts -o dist --source-map --license licenses.txt --watch", 40 | "clean": "npm install && shx rm -rf dist/ out/ node_modules/ && npm ci" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AppStoreConnectClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AppStoreConnectClient, 3 | AppStoreConnectOptions 4 | } from '@rage-against-the-pixel/app-store-connect-api'; 5 | import { XcodeProject } from './XcodeProject'; 6 | import { 7 | Build, 8 | BuildsGetCollectionData, 9 | BetaBuildLocalization, 10 | BetaBuildLocalizationUpdateRequest, 11 | BetaBuildLocalizationsGetCollectionData, 12 | PrereleaseVersion, 13 | PreReleaseVersionsGetCollectionData, 14 | BetaBuildLocalizationCreateRequest, 15 | BetaGroupsGetCollectionData, 16 | BuildsBetaGroupsCreateToManyRelationshipData, 17 | BetaGroup, 18 | BetaAppReviewSubmissionsCreateInstanceData, 19 | BuildBetaDetailsGetCollectionData, 20 | BuildBetaDetailsUpdateInstanceData, 21 | BuildBetaDetail, 22 | } from '@rage-against-the-pixel/app-store-connect-api/dist/app_store_connect_api'; 23 | import { log } from './utilities'; 24 | import core = require('@actions/core'); 25 | 26 | let appStoreConnectClient: AppStoreConnectClient | null = null; 27 | 28 | export class UnauthorizedError extends Error { 29 | constructor(message: string) { 30 | super(message); 31 | this.name = 'UnauthorizedError'; 32 | } 33 | } 34 | 35 | async function getOrCreateClient(project: XcodeProject) { 36 | if (appStoreConnectClient) { return appStoreConnectClient; } 37 | if (!project.credential) { 38 | throw new UnauthorizedError('Missing AppleCredential!'); 39 | } 40 | const options: AppStoreConnectOptions = { 41 | issuerId: project.credential.appStoreConnectIssuerId, 42 | privateKeyId: project.credential.appStoreConnectKeyId, 43 | privateKey: project.credential.appStoreConnectKey, 44 | }; 45 | appStoreConnectClient = new AppStoreConnectClient(options); 46 | } 47 | 48 | function checkAuthError(error: any) { 49 | if (error && error.errors) { 50 | for (const e of error.errors) { 51 | if (e.status === '401') { 52 | throw new UnauthorizedError(e.message); 53 | } 54 | } 55 | } 56 | } 57 | 58 | export async function GetAppId(project: XcodeProject): Promise { 59 | await getOrCreateClient(project); 60 | const { data: response, error } = await appStoreConnectClient.api.AppsService.appsGetCollection({ 61 | query: { 'filter[bundleId]': [project.bundleId] } 62 | }); 63 | if (error) { 64 | checkAuthError(error); 65 | throw new Error(`Error fetching apps: ${JSON.stringify(error)}`); 66 | } 67 | log(`GET /appsGetCollection\n${JSON.stringify(response, null, 2)}`); 68 | if (!response) { 69 | throw new Error(`No apps found for bundle id ${project.bundleId}`); 70 | } 71 | if (response.data.length === 0) { 72 | throw new Error(`No apps found for bundle id ${project.bundleId}`); 73 | } 74 | if (response.data.length > 1) { 75 | log(`Multiple apps found for bundle id ${project.bundleId}!`); 76 | for (const app of response.data) { 77 | log(`[${app.id}] ${app.attributes?.bundleId}`); 78 | if (project.bundleId === app.attributes?.bundleId) { 79 | return app.id; 80 | } 81 | } 82 | } 83 | return response.data[0].id; 84 | } 85 | 86 | export async function GetLatestBundleVersion(project: XcodeProject): Promise { 87 | await getOrCreateClient(project); 88 | let { preReleaseVersion, build } = await getLastPreReleaseVersionAndBuild(project); 89 | if (!build) { 90 | build = await getLastPrereleaseBuild(preReleaseVersion); 91 | } 92 | return build?.attributes?.version; 93 | } 94 | 95 | function reMapPlatform(project: XcodeProject): ('IOS' | 'MAC_OS' | 'TV_OS' | 'VISION_OS') { 96 | switch (project.platform) { 97 | case 'iOS': 98 | return 'IOS'; 99 | case 'macOS': 100 | return 'MAC_OS'; 101 | case 'tvOS': 102 | return 'TV_OS'; 103 | case 'visionOS': 104 | return 'VISION_OS'; 105 | default: 106 | throw new Error(`Unsupported platform: ${project.platform}`); 107 | } 108 | } 109 | 110 | async function getLastPreReleaseVersionAndBuild(project: XcodeProject): Promise { 111 | if (!project.appId) { project.appId = await GetAppId(project); } 112 | const preReleaseVersionRequest: PreReleaseVersionsGetCollectionData = { 113 | query: { 114 | 'filter[app]': [project.appId], 115 | 'filter[platform]': [reMapPlatform(project)], 116 | 'filter[version]': [project.versionString], 117 | 'limit[builds]': 1, 118 | sort: ['-version'], 119 | include: ['builds'], 120 | limit: 1, 121 | } 122 | }; 123 | log(`GET /preReleaseVersions?${JSON.stringify(preReleaseVersionRequest.query)}`); 124 | const { data: preReleaseResponse, error: preReleaseError } = await appStoreConnectClient.api.PreReleaseVersionsService.preReleaseVersionsGetCollection(preReleaseVersionRequest); 125 | const responseJson = JSON.stringify(preReleaseResponse, null, 2); 126 | if (preReleaseError) { 127 | checkAuthError(preReleaseError); 128 | throw new Error(`Error fetching pre-release versions: ${responseJson}`); 129 | } 130 | log(responseJson); 131 | if (!preReleaseResponse || !preReleaseResponse.data || preReleaseResponse.data.length === 0) { 132 | return new PreReleaseVersionWithBuild({ preReleaseVersion: null, build: null }); 133 | } 134 | let lastBuild: Build = null; 135 | const buildsData = preReleaseResponse.data[0].relationships?.builds?.data; 136 | if (buildsData && buildsData.length > 0) { 137 | const lastBuildId = buildsData[0]?.id ?? null; 138 | if (lastBuildId) { 139 | lastBuild = preReleaseResponse.included?.find(i => i.type == 'builds' && i.id == lastBuildId) as Build; 140 | } 141 | } 142 | return new PreReleaseVersionWithBuild({ 143 | preReleaseVersion: preReleaseResponse.data[0], 144 | build: lastBuild 145 | }); 146 | } 147 | 148 | class PreReleaseVersionWithBuild { 149 | preReleaseVersion?: PrereleaseVersion; 150 | build?: Build; 151 | constructor({ preReleaseVersion, build }: { preReleaseVersion: PrereleaseVersion, build: Build }) { 152 | this.preReleaseVersion = preReleaseVersion; 153 | this.build = build; 154 | } 155 | } 156 | 157 | async function getLastPrereleaseBuild(prereleaseVersion: PrereleaseVersion): Promise { 158 | const buildsRequest: BuildsGetCollectionData = { 159 | query: { 160 | 'filter[preReleaseVersion]': [prereleaseVersion.id], 161 | sort: ['-version'], 162 | limit: 1 163 | } 164 | }; 165 | log(`GET /builds?${JSON.stringify(buildsRequest.query)}`); 166 | const { data: buildsResponse, error: responseError } = await appStoreConnectClient.api.BuildsService.buildsGetCollection(buildsRequest); 167 | if (responseError) { 168 | checkAuthError(responseError); 169 | throw new Error(`Error fetching builds: ${JSON.stringify(responseError, null, 2)}`); 170 | } 171 | const responseJson = JSON.stringify(buildsResponse, null, 2); 172 | if (!buildsResponse || !buildsResponse.data || buildsResponse.data.length === 0) { 173 | throw new Error(`No builds found! ${responseJson}`); 174 | } 175 | log(responseJson); 176 | return buildsResponse.data[0]; 177 | } 178 | 179 | async function getBetaBuildLocalization(build: Build): Promise { 180 | const betaBuildLocalizationRequest: BetaBuildLocalizationsGetCollectionData = { 181 | query: { 182 | 'filter[build]': [build.id], 183 | 'filter[locale]': ['en-US'], 184 | 'fields[betaBuildLocalizations]': ['whatsNew'] 185 | } 186 | }; 187 | log(`GET /betaBuildLocalizations?${JSON.stringify(betaBuildLocalizationRequest.query)}`); 188 | const { data: betaBuildLocalizationResponse, error: betaBuildLocalizationError } = await appStoreConnectClient.api.BetaBuildLocalizationsService.betaBuildLocalizationsGetCollection(betaBuildLocalizationRequest); 189 | const responseJson = JSON.stringify(betaBuildLocalizationResponse, null, 2); 190 | if (betaBuildLocalizationError) { 191 | checkAuthError(betaBuildLocalizationError); 192 | throw new Error(`Error fetching beta build localization: ${JSON.stringify(betaBuildLocalizationError, null, 2)}`); 193 | } 194 | if (!betaBuildLocalizationResponse || betaBuildLocalizationResponse.data.length === 0) { 195 | return null; 196 | } 197 | log(responseJson); 198 | return betaBuildLocalizationResponse.data[0]; 199 | } 200 | 201 | async function createBetaBuildLocalization(build: Build, whatsNew: string): Promise { 202 | const betaBuildLocalizationRequest: BetaBuildLocalizationCreateRequest = { 203 | data: { 204 | type: 'betaBuildLocalizations', 205 | attributes: { 206 | whatsNew: whatsNew, 207 | locale: 'en-US' 208 | }, 209 | relationships: { 210 | build: { 211 | data: { 212 | id: build.id, 213 | type: 'builds' 214 | } 215 | } 216 | } 217 | } 218 | } 219 | log(`POST /betaBuildLocalizations\n${JSON.stringify(betaBuildLocalizationRequest, null, 2)}`); 220 | const { data: response, error: responseError } = await appStoreConnectClient.api.BetaBuildLocalizationsService.betaBuildLocalizationsCreateInstance({ 221 | body: betaBuildLocalizationRequest 222 | }); 223 | const responseJson = JSON.stringify(betaBuildLocalizationRequest, null, 2); 224 | if (responseError) { 225 | checkAuthError(responseError); 226 | throw new Error(`Error creating beta build localization: ${JSON.stringify(responseError, null, 2)}`); 227 | } 228 | log(responseJson); 229 | return response.data; 230 | } 231 | 232 | async function updateBetaBuildLocalization(betaBuildLocalization: BetaBuildLocalization, whatsNew: string): Promise { 233 | const updateBuildLocalization: BetaBuildLocalizationUpdateRequest = { 234 | data: { 235 | id: betaBuildLocalization.id, 236 | type: 'betaBuildLocalizations', 237 | attributes: { 238 | whatsNew: whatsNew 239 | } 240 | } 241 | }; 242 | log(`POST /betaBuildLocalizations/${betaBuildLocalization.id}\n${JSON.stringify(updateBuildLocalization, null, 2)}`); 243 | const { error: updateError } = await appStoreConnectClient.api.BetaBuildLocalizationsService.betaBuildLocalizationsUpdateInstance({ 244 | path: { id: betaBuildLocalization.id }, 245 | body: updateBuildLocalization 246 | }); 247 | if (updateError) { 248 | checkAuthError(updateError); 249 | throw new Error(`Error updating beta build localization: ${JSON.stringify(updateError, null, 2)}`); 250 | } 251 | return betaBuildLocalization; 252 | } 253 | 254 | async function pollForValidBuild(project: XcodeProject, maxRetries: number = 180, interval: number = 30): Promise { 255 | log(`Polling build validation...`); 256 | let retries = 0; 257 | while (++retries < maxRetries) { 258 | await new Promise(resolve => setTimeout(resolve, interval * 1000)); 259 | core.info(`Polling for build... Attempt ${retries}/${maxRetries}`); 260 | let { preReleaseVersion, build } = await getLastPreReleaseVersionAndBuild(project); 261 | if (preReleaseVersion) { 262 | if (!build) { 263 | build = await getLastPrereleaseBuild(preReleaseVersion); 264 | } 265 | if (build) { 266 | const normalizedBuildVersion = normalizeVersion(build.attributes?.version); 267 | const normalizedProjectVersion = normalizeVersion(project.bundleVersion); 268 | switch (build.attributes?.processingState) { 269 | case 'VALID': 270 | if (normalizedBuildVersion === normalizedProjectVersion) { 271 | core.info(`Build ${build.attributes.version} is VALID`); 272 | return build; 273 | } else { 274 | core.info(`Waiting for ${project.bundleVersion}...`); 275 | } 276 | break; 277 | case 'FAILED': 278 | case 'INVALID': 279 | throw new Error(`Build ${build.attributes.version} === ${build.attributes.processingState}!`); 280 | default: 281 | core.info(`Build ${build.attributes.version} is ${build.attributes.processingState}...`); 282 | break; 283 | } 284 | } else { 285 | core.info(`Waiting for build ${preReleaseVersion.attributes?.version}...`); 286 | } 287 | } else { 288 | core.info(`Waiting for pre-release build ${project.versionString}...`); 289 | } 290 | } 291 | throw new Error('Timed out waiting for valid build!'); 292 | } 293 | 294 | export async function UpdateTestDetails(project: XcodeProject, whatsNew: string): Promise { 295 | core.info(`Updating test details...`); 296 | await getOrCreateClient(project); 297 | const build = await pollForValidBuild(project); 298 | const betaBuildLocalization = await getBetaBuildLocalization(build); 299 | if (!betaBuildLocalization) { 300 | core.info(`Creating beta build localization...`); 301 | await createBetaBuildLocalization(build, whatsNew); 302 | } else { 303 | core.info(`Updating beta build localization...`); 304 | await updateBetaBuildLocalization(betaBuildLocalization, whatsNew); 305 | } 306 | const testGroups = core.getInput('test-groups'); 307 | if (testGroups) { 308 | core.info(`Adding Beta groups: ${testGroups}`); 309 | const testGroupNames = testGroups.split(',').map(group => group.trim()); 310 | await AddBuildToTestGroups(project, build, testGroupNames); 311 | } 312 | const submitForReview = core.getInput('submit-for-review'); 313 | if (submitForReview) { 314 | core.info(`Submitting for review...`); 315 | await submitBetaBuildForReview(project, build); 316 | await autoNotifyBetaUsers(project, build); 317 | } 318 | } 319 | 320 | async function submitBetaBuildForReview(project: XcodeProject, build: Build): Promise { 321 | await getOrCreateClient(project); 322 | const payload: BetaAppReviewSubmissionsCreateInstanceData = { 323 | body: { 324 | data: { 325 | relationships: { 326 | build: { 327 | data: { 328 | id: build.id, 329 | type: 'builds' 330 | } 331 | } 332 | }, 333 | type: 'betaAppReviewSubmissions', 334 | } 335 | } 336 | }; 337 | log(`POST /betaAppReviewSubmissions\n${JSON.stringify(payload, null, 2)}`); 338 | const { data: response, error } = await appStoreConnectClient.api.BetaAppReviewSubmissionsService.betaAppReviewSubmissionsCreateInstance(payload); 339 | if (error) { 340 | checkAuthError(error); 341 | throw new Error(`Error submitting beta build for review: ${JSON.stringify(error, null, 2)}`); 342 | } 343 | const responseJson = JSON.stringify(response, null, 2); 344 | log(responseJson); 345 | if (!response || !response.data) { 346 | throw new Error(`No beta build review submission returned!\n${responseJson}`); 347 | } 348 | core.info(`Beta build is ${response.data.attributes?.betaReviewState ?? 'UNKNOWN'}`); 349 | } 350 | 351 | async function autoNotifyBetaUsers(project: XcodeProject, build: Build): Promise { 352 | await getOrCreateClient(project); 353 | let buildBetaDetail: BuildBetaDetail = null; 354 | if (!build.relationships?.buildBetaDetail) { 355 | buildBetaDetail = await getBetaAppBuildSubmissionDetails(build); 356 | } else { 357 | buildBetaDetail = build.relationships.buildBetaDetail.data; 358 | } 359 | if (!buildBetaDetail.attributes?.autoNotifyEnabled) { 360 | const payload: BuildBetaDetailsUpdateInstanceData = { 361 | path: { id: buildBetaDetail.id }, 362 | body: { 363 | data: { 364 | id: buildBetaDetail.id, 365 | type: 'buildBetaDetails', 366 | attributes: { 367 | autoNotifyEnabled: true 368 | } 369 | } 370 | } 371 | }; 372 | const { data: response, error } = await appStoreConnectClient.api.BuildBetaDetailsService.buildBetaDetailsUpdateInstance(payload); 373 | if (error) { 374 | checkAuthError(error); 375 | throw new Error(`Error updating beta build details: ${JSON.stringify(error, null, 2)}`); 376 | } 377 | const responseJson = JSON.stringify(response, null, 2); 378 | log(responseJson); 379 | } 380 | } 381 | 382 | async function getBetaAppBuildSubmissionDetails(build: Build): Promise { 383 | const payload: BuildBetaDetailsGetCollectionData = { 384 | query: { 385 | "filter[build]": [build.id], 386 | limit: 1 387 | } 388 | }; 389 | const { data: response, error } = await appStoreConnectClient.api.BuildBetaDetailsService.buildBetaDetailsGetCollection(payload); 390 | if (error) { 391 | checkAuthError(error); 392 | throw new Error(`Error fetching beta build details: ${JSON.stringify(error, null, 2)}`); 393 | } 394 | const responseJson = JSON.stringify(response, null, 2); 395 | if (!response || !response.data || response.data.length === 0) { 396 | throw new Error(`No beta build details found!`); 397 | } 398 | log(responseJson); 399 | return response.data[0]; 400 | } 401 | 402 | function normalizeVersion(version: string): string { 403 | return version.split('.').map(part => parseInt(part, 10).toString()).join('.'); 404 | } 405 | 406 | export async function AddBuildToTestGroups(project: XcodeProject, build: Build, testGroups: string[]): Promise { 407 | await getOrCreateClient(project); 408 | const betaGroups = (await getBetaGroupsByName(project, testGroups)).map(group => ({ 409 | type: group.type, 410 | id: group.id 411 | })); 412 | const payload: BuildsBetaGroupsCreateToManyRelationshipData = { 413 | path: { id: build.id }, 414 | body: { data: betaGroups } 415 | }; 416 | log(`POST /builds/${build.id}/relationships/betaGroups\n${JSON.stringify(payload, null, 2)}`); 417 | const { error } = await appStoreConnectClient.api.BuildsService.buildsBetaGroupsCreateToManyRelationship(payload); 418 | if (error) { 419 | checkAuthError(error); 420 | throw new Error(`Error adding build to test group: ${JSON.stringify(error, null, 2)}`); 421 | } 422 | } 423 | 424 | async function getBetaGroupsByName(project: XcodeProject, groupNames: string[]): Promise { 425 | await getOrCreateClient(project); 426 | const appId = project.appId || await GetAppId(project); 427 | const request: BetaGroupsGetCollectionData = { 428 | query: { 429 | 'filter[name]': groupNames, 430 | 'filter[app]': [appId], 431 | } 432 | } 433 | log(`GET /betaGroups?${JSON.stringify(request.query)}`); 434 | const { data: response, error } = await appStoreConnectClient.api.BetaGroupsService.betaGroupsGetCollection(request); 435 | if (error) { 436 | checkAuthError(error); 437 | throw new Error(`Error fetching test groups: ${JSON.stringify(error)}`); 438 | } 439 | const responseJson = JSON.stringify(response, null, 2); 440 | if (!response || !response.data || response.data.length === 0) { 441 | throw new Error(`No test groups found!`); 442 | } 443 | log(responseJson); 444 | return response.data; 445 | } 446 | -------------------------------------------------------------------------------- /src/AppleCredential.ts: -------------------------------------------------------------------------------- 1 | import core = require('@actions/core'); 2 | import exec = require('@actions/exec'); 3 | import uuid = require('uuid'); 4 | import fs = require('fs'); 5 | 6 | const security = '/usr/bin/security'; 7 | const temp = process.env['RUNNER_TEMP'] || '.'; 8 | const appStoreConnectKeyDir = `${process.env.HOME}/.appstoreconnect/private_keys`; 9 | 10 | export class AppleCredential { 11 | constructor( 12 | tempPassPhrase: string, 13 | keychainPath: string, 14 | appStoreConnectKeyId: string, 15 | appStoreConnectIssuerId: string, 16 | appStoreConnectKeyPath?: string, 17 | appStoreConnectKey?: string, 18 | teamId?: string, 19 | manualSigningIdentity?: string, 20 | manualProvisioningProfileUUID?: string 21 | ) { 22 | this.tempPassPhrase = tempPassPhrase; 23 | this.keychainPath = keychainPath; 24 | this.appStoreConnectKeyId = appStoreConnectKeyId; 25 | this.appStoreConnectIssuerId = appStoreConnectIssuerId; 26 | this.appStoreConnectKeyPath = appStoreConnectKeyPath; 27 | this.appStoreConnectKey = appStoreConnectKey; 28 | this.teamId = teamId; 29 | this.manualSigningIdentity = manualSigningIdentity; 30 | this.manualProvisioningProfileUUID = manualProvisioningProfileUUID; 31 | } 32 | tempPassPhrase: string; 33 | keychainPath: string; 34 | appStoreConnectKeyId: string; 35 | appStoreConnectIssuerId: string; 36 | appStoreConnectKeyPath?: string; 37 | appStoreConnectKey?: string; 38 | teamId?: string; 39 | manualSigningIdentity?: string; 40 | manualProvisioningProfileUUID?: string; 41 | bearerToken?: string; 42 | ascPublicId?: string; 43 | } 44 | 45 | // https://docs.github.com/en/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development#add-a-step-to-your-workflow 46 | export async function ImportCredentials(): Promise { 47 | try { 48 | core.startGroup('Importing credentials...'); 49 | const tempCredential = uuid.v4(); 50 | core.setSecret(tempCredential); 51 | core.saveState('tempCredential', tempCredential); 52 | const authenticationKeyID = core.getInput('app-store-connect-key-id', { required: true }).trim(); 53 | core.saveState('authenticationKeyID', authenticationKeyID); 54 | const authenticationKeyIssuerID = core.getInput('app-store-connect-issuer-id', { required: true }).trim(); 55 | core.saveState('authenticationKeyIssuerID', authenticationKeyIssuerID); 56 | const appStoreConnectKeyBase64 = core.getInput('app-store-connect-key', { required: true }).trim(); 57 | await fs.promises.mkdir(appStoreConnectKeyDir, { recursive: true }); 58 | const appStoreConnectKeyPath = `${appStoreConnectKeyDir}/AuthKey_${authenticationKeyID}.p8`; 59 | const appStoreConnectKey = Buffer.from(appStoreConnectKeyBase64, 'base64').toString('utf8'); 60 | core.setSecret(appStoreConnectKey); 61 | await fs.promises.writeFile(appStoreConnectKeyPath, appStoreConnectKey, 'utf8'); 62 | const keychainPath = `${temp}/${tempCredential}.keychain-db`; 63 | await exec.exec(security, ['create-keychain', '-p', tempCredential, keychainPath]); 64 | await exec.exec(security, ['set-keychain-settings', '-lut', '21600', keychainPath]); 65 | await unlockTemporaryKeychain(keychainPath, tempCredential); 66 | let manualSigningIdentity = core.getInput('manual-signing-identity') || core.getInput('signing-identity'); 67 | let certificateUUID: string | undefined; 68 | let teamId = core.getInput('team-id'); 69 | const manualSigningCertificateBase64 = core.getInput('manual-signing-certificate') || core.getInput('certificate'); 70 | let installedCertificates: boolean = false; 71 | if (manualSigningCertificateBase64) { 72 | const manualSigningCertificatePassword = core.getInput('manual-signing-certificate-password') || core.getInput('certificate-password'); 73 | if (!manualSigningCertificatePassword) { 74 | throw new Error('manual-signing-certificate-password is required when manual-signing-certificate is provided!'); 75 | } 76 | core.info('Importing manual signing certificate...'); 77 | await importCertificate( 78 | keychainPath, 79 | tempCredential, 80 | manualSigningCertificateBase64.trim(), 81 | manualSigningCertificatePassword.trim()); 82 | installedCertificates = true; 83 | if (!manualSigningIdentity) { 84 | let output = ''; 85 | core.info(`[command]${security} find-identity -v -p codesigning ${keychainPath}`); 86 | await exec.exec(security, ['find-identity', '-v', '-p', 'codesigning', keychainPath], { 87 | listeners: { 88 | stdout: (data: Buffer) => { 89 | output += data.toString(); 90 | } 91 | }, 92 | silent: true 93 | }); 94 | const match = output.match(/\d\) (?\w+) \"(?[^"]+)\"$/m); 95 | if (!match) { 96 | throw new Error('Failed to match signing identity!'); 97 | } 98 | certificateUUID = match.groups?.uuid; 99 | core.setSecret(certificateUUID); 100 | manualSigningIdentity = match.groups?.signing_identity; 101 | if (!manualSigningIdentity) { 102 | throw new Error('Failed to find signing identity!'); 103 | } 104 | if (!teamId) { 105 | const teamMatch = manualSigningIdentity.match(/(?[A-Z0-9]{10})\s/); 106 | if (!teamMatch) { 107 | throw new Error('Failed to match team id!'); 108 | } 109 | teamId = teamMatch.groups?.team_id; 110 | if (!teamId) { 111 | throw new Error('Failed to find team id!'); 112 | } 113 | core.setSecret(teamId); 114 | } 115 | core.info(output); 116 | } 117 | } 118 | const manualProvisioningProfileBase64 = core.getInput('provisioning-profile'); 119 | let manualProvisioningProfileUUID: string | undefined; 120 | if (manualProvisioningProfileBase64) { 121 | core.info('Importing provisioning profile...'); 122 | const provisioningProfileName = core.getInput('provisioning-profile-name', { required: true }); 123 | if (!provisioningProfileName.endsWith('.mobileprovision') && 124 | !provisioningProfileName.endsWith('.provisionprofile')) { 125 | throw new Error('Provisioning profile name must end with .mobileprovision or .provisionprofile'); 126 | } 127 | const provisioningProfilePath = `${temp}/${provisioningProfileName}`; 128 | core.saveState('provisioningProfilePath', provisioningProfilePath); 129 | const provisioningProfile = Buffer.from(manualProvisioningProfileBase64, 'base64').toString('binary'); 130 | await fs.promises.writeFile(provisioningProfilePath, provisioningProfile, 'binary'); 131 | const provisioningProfileContent = await fs.promises.readFile(provisioningProfilePath, 'utf8'); 132 | const uuidMatch = provisioningProfileContent.match(/UUID<\/key>\s*([^<]+)<\/string>/); 133 | if (uuidMatch) { 134 | manualProvisioningProfileUUID = uuidMatch[1]; 135 | } 136 | if (!manualProvisioningProfileUUID) { 137 | throw new Error('Failed to parse provisioning profile UUID'); 138 | } 139 | } 140 | const developerIdApplicationCertificateBase64 = core.getInput('developer-id-application-certificate'); 141 | if (developerIdApplicationCertificateBase64) { 142 | const developerIdApplicationCertificatePassword = core.getInput('developer-id-application-certificate-password'); 143 | if (!developerIdApplicationCertificatePassword) { 144 | throw new Error('developer-id-application-certificate-password is required when developer-id-application-certificate is provided!'); 145 | } 146 | core.info('Importing developer id application certificate...'); 147 | await importCertificate( 148 | keychainPath, 149 | tempCredential, 150 | developerIdApplicationCertificateBase64.trim(), 151 | developerIdApplicationCertificatePassword.trim()); 152 | installedCertificates = true; 153 | } 154 | const developerIdInstallerCertificateBase64 = core.getInput('developer-id-installer-certificate'); 155 | if (developerIdInstallerCertificateBase64) { 156 | const developerIdInstallerCertificatePassword = core.getInput('developer-id-installer-certificate-password'); 157 | if (!developerIdInstallerCertificatePassword) { 158 | throw new Error('developer-id-installer-certificate-password is required when developer-id-installer-certificate is provided!'); 159 | } 160 | core.info('Importing developer id installer certificate...'); 161 | await importCertificate( 162 | keychainPath, 163 | tempCredential, 164 | developerIdInstallerCertificateBase64.trim(), 165 | developerIdInstallerCertificatePassword.trim()); 166 | installedCertificates = true; 167 | } 168 | if (installedCertificates) { 169 | let output = ''; 170 | core.info(`[command]${security} find-identity -v ${keychainPath}`); 171 | const exitCode = await exec.exec(security, ['find-identity', '-v', keychainPath], { 172 | listeners: { 173 | stdout: (data: Buffer) => { 174 | output += data.toString(); 175 | } 176 | }, 177 | silent: true 178 | }); 179 | if (exitCode !== 0) { 180 | throw new Error(`Failed to list identities! Exit code: ${exitCode}`); 181 | } 182 | const matches = output.matchAll(/\d\) (?\w+) \"(?[^"]+)\"$/gm); 183 | for (const match of matches) { 184 | const uuid = match.groups?.uuid; 185 | const signingIdentity = match.groups?.signing_identity; 186 | if (uuid && signingIdentity) { 187 | core.setSecret(uuid); 188 | core.info(`Found identity: ${signingIdentity} (${uuid})`); 189 | } 190 | } 191 | } 192 | return new AppleCredential( 193 | tempCredential, 194 | keychainPath, 195 | authenticationKeyID, 196 | authenticationKeyIssuerID, 197 | appStoreConnectKeyPath, 198 | appStoreConnectKey, 199 | teamId, 200 | manualSigningIdentity, 201 | manualProvisioningProfileUUID 202 | ); 203 | } finally { 204 | core.endGroup(); 205 | } 206 | } 207 | 208 | export async function RemoveCredentials(): Promise { 209 | const provisioningProfilePath = core.getState('provisioningProfilePath'); 210 | if (provisioningProfilePath) { 211 | core.info('Removing provisioning profile...'); 212 | try { 213 | await fs.promises.unlink(provisioningProfilePath); 214 | } catch (error) { 215 | core.error(`Failed to remove provisioning profile!\n${error.stack}`); 216 | } 217 | } 218 | const tempCredential = core.getState('tempCredential'); 219 | if (tempCredential) { 220 | core.info('Removing keychain...'); 221 | const keychainPath = `${temp}/${tempCredential}.keychain-db`; 222 | await exec.exec(security, ['delete-keychain', keychainPath]); 223 | } else { 224 | core.error('Missing tempCredential state'); 225 | } 226 | const authenticationKeyID = core.getState('authenticationKeyID'); 227 | const appStoreConnectKeyPath = `${appStoreConnectKeyDir}/AuthKey_${authenticationKeyID}.p8`; 228 | const certificateDirectory = await getCertificateDirectory(); 229 | core.info('Removing credentials...'); 230 | try { 231 | await fs.promises.unlink(appStoreConnectKeyPath); 232 | } catch (error) { 233 | core.error(`Failed to remove app store connect key!\n${error.stack}`); 234 | } 235 | core.info('Removing certificate directory...'); 236 | try { 237 | await fs.promises.rm(certificateDirectory, { recursive: true, force: true }); 238 | } catch (error) { 239 | core.error(`Failed to remove certificate directory!\n${error.stack}`); 240 | } 241 | } 242 | 243 | async function getCertificateDirectory(): Promise { 244 | const certificateDirectory = `${temp}/certificates`; 245 | try { 246 | await fs.promises.access(certificateDirectory, fs.constants.R_OK) 247 | } catch (error) { 248 | core.debug(`Creating directory ${certificateDirectory}`); 249 | await fs.promises.mkdir(certificateDirectory, { recursive: true }); 250 | } 251 | return certificateDirectory; 252 | } 253 | 254 | async function importCertificate(keychainPath: string, tempCredential: string, certificateBase64: string, certificatePassword: string): Promise { 255 | const certificateDirectory = await getCertificateDirectory(); 256 | const certificatePath = `${certificateDirectory}/${tempCredential}-${uuid.v4()}.p12`; 257 | const certificate = Buffer.from(certificateBase64, 'base64'); 258 | await fs.promises.writeFile(certificatePath, certificate); 259 | await exec.exec(security, [ 260 | 'import', certificatePath, 261 | '-k', keychainPath, 262 | '-P', certificatePassword, 263 | '-A', '-t', 'cert', '-f', 'pkcs12' 264 | ]); 265 | const partitionList = 'apple-tool:,apple:,codesign:'; 266 | if (core.isDebug()) { 267 | core.info(`[command]${security} set-key-partition-list -S ${partitionList} -s -k ${tempCredential} ${keychainPath}`); 268 | } 269 | await exec.exec(security, [ 270 | 'set-key-partition-list', 271 | '-S', partitionList, 272 | '-s', '-k', tempCredential, 273 | keychainPath 274 | ], { 275 | silent: !core.isDebug() 276 | }); 277 | await exec.exec(security, ['list-keychains', '-d', 'user', '-s', keychainPath, 'login.keychain-db']); 278 | } 279 | 280 | async function unlockTemporaryKeychain(keychainPath: string, tempCredential: string): Promise { 281 | const exitCode = await exec.exec(security, ['unlock-keychain', '-p', tempCredential, keychainPath]); 282 | if (exitCode !== 0) { 283 | throw new Error(`Failed to unlock keychain! Exit code: ${exitCode}`); 284 | } 285 | } -------------------------------------------------------------------------------- /src/XcodeProject.ts: -------------------------------------------------------------------------------- 1 | import { AppleCredential } from './AppleCredential'; 2 | import { SemVer } from 'semver'; 3 | 4 | export class XcodeProject { 5 | constructor( 6 | projectPath: string, 7 | projectName: string, 8 | platform: string, 9 | destination: string, 10 | bundleId: string, 11 | projectDirectory: string, 12 | versionString: string, 13 | bundleVersion: string, 14 | scheme: string, 15 | credential: AppleCredential, 16 | xcodeVersion: SemVer 17 | ) { 18 | this.projectPath = projectPath; 19 | this.projectName = projectName; 20 | this.platform = platform; 21 | this.destination = destination; 22 | this.bundleId = bundleId; 23 | this.projectDirectory = projectDirectory; 24 | this.versionString = versionString; 25 | this.bundleVersion = bundleVersion; 26 | this.scheme = scheme; 27 | this.credential = credential 28 | this.xcodeVersion = xcodeVersion; 29 | this.isSteamBuild = false; 30 | } 31 | projectPath: string; 32 | projectName: string; 33 | bundleId: string; 34 | appId: string; 35 | projectDirectory: string; 36 | credential: AppleCredential; 37 | platform: string; 38 | destination: string; 39 | archivePath: string; 40 | exportPath: string; 41 | executablePath: string; 42 | exportOption: string; 43 | exportOptionsPath: string; 44 | entitlementsPath: string; 45 | versionString: string; 46 | bundleVersion: string; 47 | scheme: string; 48 | xcodeVersion: SemVer; 49 | autoIncrementBuildNumber: boolean; 50 | isSteamBuild: boolean; 51 | archiveType: string; 52 | notarize: boolean; 53 | isAppStoreUpload(): boolean { 54 | return this.exportOption === 'app-store' || this.exportOption === 'app-store-connect'; 55 | } 56 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import core = require('@actions/core'); 2 | import exec = require('@actions/exec'); 3 | import { 4 | GetProjectDetails, 5 | ArchiveXcodeProject, 6 | ExportXcodeArchive, 7 | ValidateApp, 8 | UploadApp 9 | } from './xcode'; 10 | import { 11 | ImportCredentials, 12 | RemoveCredentials 13 | } from './AppleCredential'; 14 | import semver = require('semver'); 15 | 16 | const IS_POST = !!core.getState('isPost'); 17 | 18 | const main = async () => { 19 | try { 20 | if (!IS_POST) { 21 | core.saveState('isPost', true); 22 | const credential = await ImportCredentials(); 23 | let xcodeVersionString = core.getInput('xcode-version'); 24 | if (xcodeVersionString) { 25 | core.info(`Setting xcode version to ${xcodeVersionString}`); 26 | let xcodeVersionOutput = ''; 27 | const installedExitCode = await exec.exec('xcodes', ['installed'], { 28 | listeners: { 29 | stdout: (data: Buffer) => { 30 | xcodeVersionOutput += data.toString(); 31 | } 32 | } 33 | }); 34 | if (installedExitCode !== 0) { 35 | throw new Error('Failed to get installed Xcode versions!'); 36 | } 37 | const installedXcodeVersions = xcodeVersionOutput.split('\n').map(line => { 38 | const match = line.match(/(\d+\.\d+(\s\w+)?)/); 39 | return match ? match[1] : null; 40 | }).filter(Boolean) as string[]; 41 | core.debug(`Installed Xcode versions:\n ${installedXcodeVersions.join('\n')}`); 42 | if (installedXcodeVersions.length === 0 || 43 | !xcodeVersionString.includes('latest')) { 44 | if (installedXcodeVersions.length === 0 || 45 | !installedXcodeVersions.includes(xcodeVersionString)) { 46 | const xcodesUsername = process.env.XCODES_USERNAME; 47 | const xcodesPassword = process.env.XCODES_PASSWORD; 48 | if (!xcodesUsername || !xcodesPassword) { 49 | throw new Error(`Xcode version ${xcodeVersionString} is not installed! Please set XCODES_USERNAME and XCODES_PASSWORD to download it.`); 50 | } 51 | core.info(`Downloading missing Xcode version ${xcodeVersionString}...`); 52 | const installExitCode = await exec.exec('xcodes', ['install', xcodeVersionString, '--select'], { 53 | env: { 54 | XCODES_USERNAME: xcodesUsername, 55 | XCODES_PASSWORD: xcodesPassword 56 | } 57 | }); 58 | if (installExitCode !== 0) { 59 | throw new Error(`Failed to install Xcode version ${xcodeVersionString}!`); 60 | } 61 | } else { 62 | core.info(`Selecting installed Xcode version ${xcodeVersionString}...`); 63 | const selectExitCode = await exec.exec('xcodes', ['select', xcodeVersionString]); 64 | if (selectExitCode !== 0) { 65 | throw new Error(`Failed to select Xcode version ${xcodeVersionString}!`); 66 | } 67 | } 68 | } else { 69 | core.info(`Selecting latest installed Xcode version ${xcodeVersionString}...`); 70 | xcodeVersionString = installedXcodeVersions[installedXcodeVersions.length - 1]; 71 | const selectExitCode = await exec.exec('xcodes', ['select', xcodeVersionString]); 72 | if (selectExitCode !== 0) { 73 | throw new Error(`Failed to select Xcode version ${xcodeVersionString}!`); 74 | } 75 | } 76 | } 77 | let xcodeVersionOutput = ''; 78 | await exec.exec('xcodebuild', ['-version'], { 79 | listeners: { 80 | stdout: (data: Buffer) => { 81 | xcodeVersionOutput += data.toString(); 82 | } 83 | } 84 | }); 85 | const xcodeVersionMatch = xcodeVersionOutput.match(/Xcode (?\d+\.\d+)/); 86 | if (!xcodeVersionMatch) { 87 | throw new Error('Failed to get Xcode version!'); 88 | } 89 | const selectedXcodeVersionString = xcodeVersionMatch.groups.version; 90 | if (!selectedXcodeVersionString) { 91 | throw new Error('Failed to parse Xcode version!'); 92 | } 93 | if (xcodeVersionString !== selectedXcodeVersionString) { 94 | throw new Error(`Selected Xcode version ${selectedXcodeVersionString} does not match requested version ${xcodeVersionString}!`); 95 | } 96 | let projectRef = await GetProjectDetails(credential, semver.coerce(xcodeVersionString)); 97 | projectRef = await ArchiveXcodeProject(projectRef); 98 | projectRef = await ExportXcodeArchive(projectRef); 99 | const uploadInput = core.getInput('upload') || projectRef.isAppStoreUpload().toString(); 100 | const upload = projectRef.isAppStoreUpload() && uploadInput === 'true'; 101 | core.debug(`uploadInput: ${upload}`); 102 | if (upload) { 103 | await ValidateApp(projectRef); 104 | await UploadApp(projectRef); 105 | } 106 | } else { 107 | await RemoveCredentials(); 108 | } 109 | } catch (error) { 110 | core.setFailed(error.stack); 111 | } 112 | } 113 | 114 | main(); 115 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | 2 | import core = require('@actions/core'); 3 | 4 | export function log(message: string, type: 'info' | 'warning' | 'error' = 'info') { 5 | if (type == 'info' && !core.isDebug()) { return; } 6 | const lines = message.split('\n'); 7 | const filteredLines = lines.filter((line) => line.trim() !== ''); 8 | const uniqueLines = Array.from(new Set(filteredLines)); 9 | let first = true; 10 | for (const line of uniqueLines) { 11 | if (first) { 12 | first = false; 13 | switch (type) { 14 | case 'info': 15 | core.info(line); 16 | break; 17 | case 'warning': 18 | core.warning(line); 19 | break; 20 | case 'error': 21 | core.error(line); 22 | break; 23 | } 24 | } else { 25 | core.info(line); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/xcode.ts: -------------------------------------------------------------------------------- 1 | import { XcodeProject } from './XcodeProject'; 2 | import { spawn } from 'child_process'; 3 | import { exec } from '@actions/exec'; 4 | import glob = require('@actions/glob'); 5 | import github = require('@actions/github'); 6 | import plist = require('plist'); 7 | import path = require('path'); 8 | import fs = require('fs'); 9 | import semver = require('semver'); 10 | import { log } from './utilities'; 11 | import { SemVer } from 'semver'; 12 | import core = require('@actions/core'); 13 | import { 14 | AppleCredential 15 | } from './AppleCredential'; 16 | import { 17 | GetLatestBundleVersion, 18 | UpdateTestDetails, 19 | UnauthorizedError, 20 | GetAppId, 21 | } from './AppStoreConnectClient'; 22 | 23 | const xcodebuild = '/usr/bin/xcodebuild'; 24 | const xcrun = '/usr/bin/xcrun'; 25 | const WORKSPACE = process.env.GITHUB_WORKSPACE || process.cwd(); 26 | 27 | export async function GetProjectDetails(credential: AppleCredential, xcodeVersion: SemVer): Promise { 28 | const projectPathInput = core.getInput('project-path') || `${WORKSPACE}/**/*.xcodeproj`; 29 | core.debug(`Project path input: ${projectPathInput}`); 30 | let projectPath = undefined; 31 | const globber = await glob.create(projectPathInput); 32 | const files = await globber.glob(); 33 | if (!files || files.length === 0) { 34 | throw new Error(`No project found at: ${projectPathInput}`); 35 | } 36 | core.debug(`Files found during search: ${files.join(', ')}`); 37 | const excludedProjects = ['GameAssembly', 'UnityFramework', 'Pods']; 38 | for (const file of files) { 39 | if (file.endsWith('.xcodeproj')) { 40 | const projectBaseName = path.basename(file, '.xcodeproj'); 41 | if (excludedProjects.includes(projectBaseName)) { 42 | continue; 43 | } 44 | core.debug(`Found Xcode project: ${file}`); 45 | projectPath = file; 46 | break; 47 | } 48 | } 49 | if (!projectPath) { 50 | throw new Error(`Invalid project-path! Unable to find .xcodeproj in ${projectPathInput}. ${files.length} files were found but none matched.\n${files.join(', ')}`); 51 | } 52 | core.debug(`Resolved Project path: ${projectPath}`); 53 | await fs.promises.access(projectPath, fs.constants.R_OK); 54 | const projectDirectory = path.dirname(projectPath); 55 | core.info(`Project directory: ${projectDirectory}`); 56 | const projectName = path.basename(projectPath, '.xcodeproj'); 57 | const scheme = await getProjectScheme(projectPath); 58 | const platform = await getSupportedPlatform(projectPath); 59 | core.info(`Platform: ${platform}`); 60 | if (!platform) { 61 | throw new Error('Unable to determine the platform to build for.'); 62 | } 63 | if (platform !== 'macOS') { 64 | await checkSimulatorsAvailable(platform); 65 | } 66 | const destination = core.getInput('destination') || `generic/platform=${platform}`; 67 | core.debug(`Using destination: ${destination}`); 68 | const bundleId = await getBuildSettings(projectPath, scheme, platform, destination); 69 | core.info(`Bundle ID: ${bundleId}`); 70 | if (!bundleId) { 71 | throw new Error('Unable to determine the bundle ID'); 72 | } 73 | let infoPlistPath = `${projectDirectory}/${projectName}/Info.plist`; 74 | if (!fs.existsSync(infoPlistPath)) { 75 | infoPlistPath = `${projectDirectory}/Info.plist`; 76 | } 77 | core.info(`Info.plist path: ${infoPlistPath}`); 78 | const infoPlistHandle = await fs.promises.open(infoPlistPath, fs.constants.O_RDONLY); 79 | let infoPlistContent: string; 80 | try { 81 | infoPlistContent = await fs.promises.readFile(infoPlistHandle, 'utf8'); 82 | } finally { 83 | await infoPlistHandle.close(); 84 | } 85 | const infoPlist = plist.parse(infoPlistContent) as any; 86 | let cFBundleShortVersionString: string = infoPlist['CFBundleShortVersionString']; 87 | if (cFBundleShortVersionString) { 88 | const semverRegex = /^(?\d+)\.(?\d+)\.(?\d+)/; 89 | const match = cFBundleShortVersionString.match(semverRegex); 90 | if (match) { 91 | const { major, minor, revision } = match.groups as { [key: string]: string }; 92 | cFBundleShortVersionString = `${major}.${minor}.${revision}`; 93 | infoPlist['CFBundleShortVersionString'] = cFBundleShortVersionString.toString(); 94 | try { 95 | core.info(`Updating Info.plist with CFBundleShortVersionString: ${cFBundleShortVersionString}`); 96 | await fs.promises.writeFile(infoPlistPath, plist.build(infoPlist)); 97 | } catch (error) { 98 | throw new Error(`Failed to update Info.plist!\n${error}`); 99 | } 100 | } else { 101 | throw new Error(`Invalid CFBundleShortVersionString format: ${cFBundleShortVersionString}`); 102 | } 103 | } 104 | core.info(`CFBundleShortVersionString: ${cFBundleShortVersionString}`); 105 | const cFBundleVersion = infoPlist['CFBundleVersion'] as string; 106 | core.info(`CFBundleVersion: ${cFBundleVersion}`); 107 | const projectRef = new XcodeProject( 108 | projectPath, 109 | projectName, 110 | platform, 111 | destination, 112 | bundleId, 113 | projectDirectory, 114 | cFBundleShortVersionString, 115 | cFBundleVersion, 116 | scheme, 117 | credential, 118 | xcodeVersion 119 | ); 120 | projectRef.autoIncrementBuildNumber = core.getInput('auto-increment-build-number') === 'true'; 121 | await getExportOptions(projectRef); 122 | if (projectRef.isAppStoreUpload()) { 123 | projectRef.appId = await GetAppId(projectRef); 124 | if (projectRef.autoIncrementBuildNumber) { 125 | let projectBundleVersionPrefix = ''; 126 | let projectBundleVersionNumber: number; 127 | if (!cFBundleVersion || cFBundleVersion.length === 0) { 128 | projectBundleVersionNumber = 0; 129 | } else if (cFBundleVersion.includes('.')) { 130 | const versionParts = cFBundleVersion.split('.'); 131 | projectBundleVersionNumber = parseInt(versionParts[versionParts.length - 1]); 132 | projectBundleVersionPrefix = versionParts.slice(0, -1).join('.') + '.'; 133 | } else { 134 | projectBundleVersionNumber = parseInt(cFBundleVersion); 135 | } 136 | let lastVersionNumber: number; 137 | let versionPrefix = ''; 138 | let lastBundleVersion: string = null; 139 | try { 140 | lastBundleVersion = await GetLatestBundleVersion(projectRef); 141 | } catch (error) { 142 | if (error instanceof UnauthorizedError) { 143 | throw error; 144 | } 145 | } 146 | if (!lastBundleVersion || lastBundleVersion.length === 0) { 147 | lastVersionNumber = -1; 148 | } 149 | else if (lastBundleVersion.includes('.')) { 150 | const versionParts = lastBundleVersion.split('.'); 151 | lastVersionNumber = parseInt(versionParts[versionParts.length - 1]); 152 | versionPrefix = versionParts.slice(0, -1).join('.') + '.'; 153 | } else { 154 | lastVersionNumber = parseInt(lastBundleVersion); 155 | } 156 | if (projectBundleVersionPrefix.length > 0 && projectBundleVersionPrefix !== versionPrefix) { 157 | core.debug(`Project version prefix: ${projectBundleVersionPrefix}`); 158 | core.debug(`Last bundle version prefix: ${versionPrefix}`); 159 | if (lastVersionNumber > projectBundleVersionNumber) { 160 | projectBundleVersionPrefix = versionPrefix; 161 | core.info(`Updated project version prefix to: ${projectBundleVersionPrefix}`); 162 | } 163 | } 164 | if (projectBundleVersionNumber <= lastVersionNumber) { 165 | projectBundleVersionNumber = lastVersionNumber + 1; 166 | core.info(`Auto Incremented bundle version ==> ${versionPrefix}${projectBundleVersionNumber}`); 167 | } 168 | infoPlist['CFBundleVersion'] = projectBundleVersionPrefix + projectBundleVersionNumber.toString(); 169 | projectRef.bundleVersion = projectBundleVersionPrefix + projectBundleVersionNumber.toString(); 170 | try { 171 | await fs.promises.writeFile(infoPlistPath, plist.build(infoPlist)); 172 | } catch (error) { 173 | log(`Failed to update Info.plist!\n${error}`, 'error'); 174 | } 175 | } 176 | } else { 177 | if (projectRef.platform === 'macOS') { 178 | const notarizeInput = core.getInput('notarize') || 'true'; 179 | core.debug(`Notarize input: ${notarizeInput}`); 180 | projectRef.notarize = 181 | notarizeInput === 'true' || 182 | projectRef.isSteamBuild || 183 | projectRef.archiveType === 'pkg' || 184 | projectRef.archiveType === 'dmg'; 185 | let output = ''; 186 | await exec('security', [ 187 | 'find-identity', 188 | '-v', projectRef.credential.keychainPath 189 | ], { 190 | listeners: { 191 | stdout: (data: Buffer) => { 192 | output += data.toString(); 193 | } 194 | }, 195 | silent: true 196 | }); 197 | if (!output.includes('Developer ID Application')) { 198 | throw new Error('Developer ID Application not found! developer-id-application-certificate input is required for notarization.'); 199 | } 200 | if (projectRef.archiveType === 'pkg' || projectRef.archiveType === 'dmg') { 201 | if (!output.includes('Developer ID Installer')) { 202 | throw new Error('Developer ID Installer not found! developer-id-installer-certificate input is required for notarization.'); 203 | } 204 | } 205 | } 206 | } 207 | const plistHandle = await fs.promises.open(infoPlistPath, fs.constants.O_RDONLY); 208 | try { 209 | infoPlistContent = await fs.promises.readFile(plistHandle, 'utf8'); 210 | } finally { 211 | await plistHandle.close(); 212 | } 213 | core.info(`------- Info.plist content: -------\n${infoPlistContent}\n-----------------------------------`); 214 | return projectRef; 215 | } 216 | 217 | async function checkSimulatorsAvailable(platform: string): Promise { 218 | const destinationArgs = ['simctl', 'list', 'devices', '--json']; 219 | let output = ''; 220 | if (!core.isDebug()) { 221 | core.info(`[command]${xcrun} ${destinationArgs.join(' ')}`); 222 | } 223 | await exec(xcrun, destinationArgs, { 224 | listeners: { 225 | stdout: (data: Buffer) => { 226 | output += data.toString(); 227 | } 228 | }, 229 | silent: !core.isDebug() 230 | }); 231 | const response = JSON.parse(output); 232 | const devices = response.devices; 233 | const platformDevices = Object.keys(devices) 234 | .filter(key => key.toLowerCase().includes(platform.toLowerCase())) 235 | .flatMap(key => devices[key]); 236 | if (platformDevices.length > 0) { 237 | return; 238 | } 239 | await exec(xcodebuild, ['-downloadPlatform', platform]); 240 | } 241 | 242 | async function getSupportedPlatform(projectPath: string): Promise { 243 | const projectFilePath = `${projectPath}/project.pbxproj`; 244 | core.debug(`.pbxproj file path: ${projectFilePath}`); 245 | await fs.promises.access(projectFilePath, fs.constants.R_OK); 246 | const content = await fs.promises.readFile(projectFilePath, 'utf8'); 247 | const platformName = core.getInput('platform') || matchRegexPattern(content, /\s+SDKROOT = (?\w+)/, 'platform'); 248 | if (!platformName) { 249 | throw new Error('Unable to determine the platform name from the build settings'); 250 | } 251 | const platformMap = { 252 | 'iphoneos': 'iOS', 253 | 'macosx': 'macOS', 254 | 'appletvos': 'tvOS', 255 | 'watchos': 'watchOS', 256 | 'xros': 'visionOS' 257 | }; 258 | return platformMap[platformName]; 259 | } 260 | 261 | async function getBuildSettings(projectPath: string, scheme: string, platform: string, destination: string): Promise { 262 | let buildSettingsOutput = ''; 263 | const projectSettingsArgs = [ 264 | 'build', 265 | '-project', projectPath, 266 | '-scheme', scheme, 267 | '-destination', destination, 268 | '-showBuildSettings' 269 | ]; 270 | if (!core.isDebug()) { 271 | core.info(`[command]${xcodebuild} ${projectSettingsArgs.join(' ')}`); 272 | } 273 | await exec(xcodebuild, projectSettingsArgs, { 274 | listeners: { 275 | stdout: (data: Buffer) => { 276 | buildSettingsOutput += data.toString(); 277 | } 278 | }, 279 | silent: !core.isDebug() 280 | }); 281 | let platformSdkVersion = core.getInput('platform-sdk-version') || null; 282 | if (!platformSdkVersion) { 283 | platformSdkVersion = matchRegexPattern(buildSettingsOutput, /\s+SDK_VERSION = (?[\d.]+)/, 'sdkVersion') || null; 284 | } 285 | if (platform !== 'macOS') { 286 | await downloadPlatformSdkIfMissing(platform, platformSdkVersion); 287 | } 288 | const bundleId = core.getInput('bundle-id') || matchRegexPattern(buildSettingsOutput, /\s+PRODUCT_BUNDLE_IDENTIFIER = (?[\w.-]+)/, 'bundleId'); 289 | if (!bundleId || bundleId === 'NO') { 290 | throw new Error('Unable to determine the bundle ID from the build settings'); 291 | } 292 | return bundleId; 293 | } 294 | 295 | function matchRegexPattern(string: string, pattern: RegExp, group: string | null): string { 296 | const match = string.match(pattern); 297 | if (!match) { 298 | throw new Error(`Failed to resolve: ${pattern}`); 299 | } 300 | return group ? match.groups?.[group] : match[1]; 301 | } 302 | 303 | async function getProjectScheme(projectPath: string): Promise { 304 | let scheme = core.getInput('scheme'); 305 | let projectInfoOutput = ''; 306 | if (!core.isDebug()) { 307 | core.info(`[command]${xcodebuild} -list -project ${projectPath} -json`); 308 | } 309 | await exec(xcodebuild, ['-list', '-project', projectPath, `-json`], { 310 | listeners: { 311 | stdout: (data: Buffer) => { 312 | projectInfoOutput += data.toString(); 313 | } 314 | }, 315 | silent: !core.isDebug() 316 | }); 317 | const projectInfo = JSON.parse(projectInfoOutput); 318 | const schemes = projectInfo.project.schemes as string[]; 319 | if (!schemes) { 320 | throw new Error('No schemes found in the project'); 321 | } 322 | core.debug(`Available schemes:`); 323 | schemes.forEach(s => core.debug(` > ${s}`)); 324 | if (!scheme) { 325 | if (schemes.includes('Unity-iPhone')) { 326 | scheme = 'Unity-iPhone'; 327 | } else if (schemes.includes('Unity-VisionOS')) { 328 | scheme = 'Unity-VisionOS'; 329 | } else { 330 | const excludedSchemes = ['GameAssembly', 'UnityFramework', 'Pods']; 331 | scheme = schemes.find(s => !excludedSchemes.includes(s) && !s.includes('Test')); 332 | } 333 | } 334 | if (!scheme) { 335 | throw new Error('Unable to determine the scheme to build'); 336 | } 337 | core.debug(`Using scheme: ${scheme}`); 338 | return scheme; 339 | } 340 | 341 | async function downloadPlatformSdkIfMissing(platform: string, version: string | null) { 342 | if (core.isDebug()) { 343 | await exec('xcodes', ['runtimes']); 344 | } 345 | if (version) { 346 | await exec('xcodes', ['runtimes', 'install', `${platform} ${version}`]); 347 | } 348 | } 349 | 350 | export async function ArchiveXcodeProject(projectRef: XcodeProject): Promise { 351 | const { projectPath, projectName, projectDirectory } = projectRef; 352 | const archivePath = `${projectDirectory}/${projectName}.xcarchive`; 353 | core.debug(`Archive path: ${archivePath}`); 354 | const configuration = core.getInput('configuration') || 'Release'; 355 | core.debug(`Configuration: ${configuration}`); 356 | let entitlementsPath = core.getInput('entitlements-plist'); 357 | if (!entitlementsPath && projectRef.platform === 'macOS') { 358 | await getDefaultEntitlementsMacOS(projectRef); 359 | } else { 360 | projectRef.entitlementsPath = entitlementsPath; 361 | } 362 | const { teamId, manualSigningIdentity, manualProvisioningProfileUUID, keychainPath } = projectRef.credential; 363 | const archiveArgs = [ 364 | 'archive', 365 | '-project', projectPath, 366 | '-scheme', projectRef.scheme, 367 | '-destination', projectRef.destination, 368 | '-configuration', configuration, 369 | '-archivePath', archivePath, 370 | `-authenticationKeyID`, projectRef.credential.appStoreConnectKeyId, 371 | `-authenticationKeyPath`, projectRef.credential.appStoreConnectKeyPath, 372 | `-authenticationKeyIssuerID`, projectRef.credential.appStoreConnectIssuerId 373 | ]; 374 | if (teamId) { 375 | archiveArgs.push(`DEVELOPMENT_TEAM=${teamId}`); 376 | } 377 | if (manualSigningIdentity) { 378 | archiveArgs.push( 379 | `CODE_SIGN_IDENTITY=${manualSigningIdentity}`, 380 | `EXPANDED_CODE_SIGN_IDENTITY=${manualSigningIdentity}`, 381 | `OTHER_CODE_SIGN_FLAGS=--keychain ${keychainPath}` 382 | ); 383 | } else { 384 | archiveArgs.push( 385 | `CODE_SIGN_IDENTITY=-`, 386 | `EXPANDED_CODE_SIGN_IDENTITY=-` 387 | ); 388 | } 389 | archiveArgs.push( 390 | `CODE_SIGN_STYLE=${manualProvisioningProfileUUID || manualSigningIdentity ? 'Manual' : 'Automatic'}` 391 | ); 392 | if (manualProvisioningProfileUUID) { 393 | archiveArgs.push(`PROVISIONING_PROFILE=${manualProvisioningProfileUUID}`); 394 | } else { 395 | archiveArgs.push( 396 | `AD_HOC_CODE_SIGNING_ALLOWED=YES`, 397 | `-allowProvisioningUpdates` 398 | ); 399 | } 400 | if (projectRef.entitlementsPath) { 401 | core.debug(`Entitlements path: ${projectRef.entitlementsPath}`); 402 | const entitlementsHandle = await fs.promises.open(projectRef.entitlementsPath, fs.constants.O_RDONLY); 403 | try { 404 | const entitlementsContent = await fs.promises.readFile(entitlementsHandle, 'utf8'); 405 | core.debug(`----- Entitlements content: -----\n${entitlementsContent}\n-----------------------------------`); 406 | } finally { 407 | await entitlementsHandle.close(); 408 | } 409 | archiveArgs.push(`CODE_SIGN_ENTITLEMENTS=${projectRef.entitlementsPath}`); 410 | } 411 | if (projectRef.platform === 'iOS') { 412 | archiveArgs.push('COPY_PHASE_STRIP=NO'); 413 | } 414 | if (projectRef.platform === 'macOS' && !projectRef.isAppStoreUpload()) { 415 | archiveArgs.push('ENABLE_HARDENED_RUNTIME=YES'); 416 | } 417 | if (!core.isDebug()) { 418 | archiveArgs.push('-quiet'); 419 | } else { 420 | archiveArgs.push('-verbose'); 421 | } 422 | if (core.isDebug()) { 423 | await execXcodeBuild(archiveArgs); 424 | } else { 425 | await execWithXcBeautify(archiveArgs); 426 | } 427 | projectRef.archivePath = archivePath 428 | return projectRef; 429 | } 430 | 431 | export async function ExportXcodeArchive(projectRef: XcodeProject): Promise { 432 | const { projectName, projectDirectory, archivePath, exportOptionsPath } = projectRef; 433 | projectRef.exportPath = `${projectDirectory}/${projectName}`; 434 | core.debug(`Export path: ${projectRef.exportPath}`); 435 | core.setOutput('output-directory', projectRef.exportPath); 436 | const { manualProvisioningProfileUUID } = projectRef.credential; 437 | const exportArgs = [ 438 | '-exportArchive', 439 | '-archivePath', archivePath, 440 | '-exportPath', projectRef.exportPath, 441 | '-exportOptionsPlist', exportOptionsPath, 442 | `-authenticationKeyID`, projectRef.credential.appStoreConnectKeyId, 443 | `-authenticationKeyPath`, projectRef.credential.appStoreConnectKeyPath, 444 | `-authenticationKeyIssuerID`, projectRef.credential.appStoreConnectIssuerId 445 | ]; 446 | if (!manualProvisioningProfileUUID) { 447 | exportArgs.push(`-allowProvisioningUpdates`); 448 | } 449 | if (!core.isDebug()) { 450 | exportArgs.push('-quiet'); 451 | } else { 452 | exportArgs.push('-verbose'); 453 | } 454 | if (core.isDebug()) { 455 | await execXcodeBuild(exportArgs); 456 | } else { 457 | await execWithXcBeautify(exportArgs); 458 | } 459 | if (projectRef.platform === 'macOS') { 460 | if (!projectRef.isAppStoreUpload()) { 461 | projectRef.executablePath = await getFirstPathWithGlob(`${projectRef.exportPath}/**/*.app`); 462 | if (projectRef.notarize) { 463 | await signMacOSAppBundle(projectRef); 464 | if (projectRef.isSteamBuild) { 465 | const isNotarized = await isAppBundleNotarized(projectRef.executablePath); 466 | if (!isNotarized) { 467 | const zipPath = path.join(projectRef.exportPath, projectRef.executablePath.replace('.app', '.zip')); 468 | await exec('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', projectRef.executablePath, zipPath]); 469 | await notarizeArchive(projectRef, zipPath, projectRef.executablePath); 470 | } 471 | } else if (projectRef.archiveType === 'pkg') { 472 | projectRef.executablePath = await createMacOSInstallerPkg(projectRef); 473 | } else if (projectRef.archiveType === 'dmg') { 474 | throw new Error('DMG export is not supported yet!'); 475 | } else { 476 | throw new Error(`Invalid archive type: ${projectRef.archiveType}`); 477 | } 478 | } 479 | } 480 | else { 481 | projectRef.executablePath = await getFirstPathWithGlob(`${projectRef.exportPath}/**/*.pkg`); 482 | } 483 | } else { 484 | projectRef.executablePath = await getFirstPathWithGlob(`${projectRef.exportPath}/**/*.ipa`); 485 | } 486 | try { 487 | await fs.promises.access(projectRef.executablePath, fs.constants.R_OK); 488 | } catch (error) { 489 | throw new Error(`Failed to export the archive at: ${projectRef.executablePath}`); 490 | } 491 | core.debug(`Exported executable: ${projectRef.executablePath}`); 492 | core.setOutput('executable', projectRef.executablePath); 493 | return projectRef; 494 | } 495 | 496 | export async function isAppBundleNotarized(appPath: string): Promise { 497 | let output = ''; 498 | if (!core.isDebug()) { 499 | core.info(`[command]stapler validate ${appPath}`); 500 | } 501 | await exec('stapler', ['validate', appPath], { 502 | silent: !core.isDebug(), 503 | listeners: { 504 | stdout: (data: Buffer) => { output += data.toString(); } 505 | }, 506 | ignoreReturnCode: true 507 | }); 508 | if (output.includes('The validate action worked!')) { 509 | return true; 510 | } 511 | if (output.includes('does not have a ticket stapled to it')) { 512 | return false; 513 | } 514 | throw new Error(`Failed to validate the notarization ticket!\n${output}`); 515 | } 516 | 517 | async function getFirstPathWithGlob(globPattern: string): Promise { 518 | const globber = await glob.create(globPattern); 519 | const files = await globber.glob(); 520 | if (files.length === 0) { 521 | throw new Error(`No file found at: ${globPattern}`); 522 | } 523 | return files[0]; 524 | } 525 | 526 | async function signMacOSAppBundle(projectRef: XcodeProject): Promise { 527 | const appPath = await getFirstPathWithGlob(`${projectRef.exportPath}/**/*.app`); 528 | await fs.promises.access(appPath, fs.constants.R_OK); 529 | const stat = await fs.promises.stat(appPath); 530 | if (!stat.isDirectory()) { 531 | throw new Error(`Not a valid app bundle: ${appPath}`); 532 | } 533 | await exec('xattr', ['-cr', appPath]); 534 | let findSigningIdentityOutput = ''; 535 | const findSigningIdentityExitCode = await exec('security', [ 536 | 'find-identity', 537 | '-p', 'codesigning', 538 | '-v', projectRef.credential.keychainPath 539 | ], { 540 | listeners: { 541 | stdout: (data: Buffer) => { 542 | findSigningIdentityOutput += data.toString(); 543 | } 544 | }, 545 | ignoreReturnCode: true 546 | }); 547 | if (findSigningIdentityExitCode !== 0) { 548 | log(findSigningIdentityOutput, 'error'); 549 | throw new Error(`Failed to find the signing identity!`); 550 | } 551 | const matches = findSigningIdentityOutput.matchAll(/\d\) (?\w+) \"(?[^"]+)\"$/gm); 552 | const signingIdentities = Array.from(matches).map(match => ({ 553 | uuid: match.groups?.['uuid'], 554 | signing_identity: match.groups?.['signing_identity'] 555 | })).filter(identity => identity.signing_identity.includes('Developer ID Application')); 556 | if (signingIdentities.length === 0) { 557 | throw new Error(`Failed to find the signing identity!`); 558 | } 559 | const developerIdApplicationSigningIdentity = signingIdentities[0].signing_identity; 560 | if (!developerIdApplicationSigningIdentity) { 561 | throw new Error(`Failed to find the Developer ID Application signing identity!`); 562 | } 563 | const codesignArgs = [ 564 | '--force', 565 | '--verify', 566 | '--timestamp', 567 | '--options', 'runtime', 568 | '--keychain', projectRef.credential.keychainPath, 569 | '--sign', developerIdApplicationSigningIdentity, 570 | ]; 571 | if (core.isDebug()) { 572 | codesignArgs.unshift('--verbose'); 573 | } 574 | await exec('find', [ 575 | appPath, 576 | '-name', '*.bundle', 577 | '-exec', 'find', '{}', '-name', '*.meta', '-delete', ';', 578 | '-exec', 'codesign', ...codesignArgs, '{}', ';' 579 | ]); 580 | await exec('find', [ 581 | appPath, 582 | '-name', '*.dylib', 583 | '-exec', 'codesign', ...codesignArgs, '{}', ';' 584 | ]); 585 | await exec('codesign', [ 586 | '--deep', 587 | ...codesignArgs, 588 | appPath 589 | ]); 590 | const verifyExitCode = await exec('codesign', [ 591 | '--verify', 592 | '--deep', 593 | '--strict', 594 | '--verbose=2', 595 | '--keychain', projectRef.credential.keychainPath, 596 | appPath 597 | ], { ignoreReturnCode: true }); 598 | if (verifyExitCode !== 0) { 599 | throw new Error('App bundle codesign verification failed!'); 600 | } 601 | } 602 | 603 | async function createMacOSInstallerPkg(projectRef: XcodeProject): Promise { 604 | core.info('Creating macOS installer pkg...'); 605 | let output = ''; 606 | const pkgPath = `${projectRef.exportPath}/${projectRef.projectName}.pkg`; 607 | const appPath = await getFirstPathWithGlob(`${projectRef.exportPath}/**/*.app`); 608 | const productBuildExitCode = await exec('xcrun', [ 609 | 'productbuild', 610 | '--component', 611 | appPath, '/Applications', 612 | pkgPath 613 | ], { 614 | listeners: { 615 | stdout: (data: Buffer) => { 616 | output += data.toString(); 617 | } 618 | }, 619 | ignoreReturnCode: true 620 | }); 621 | if (productBuildExitCode !== 0) { 622 | log(output, 'error'); 623 | throw new Error(`Failed to create the pkg!`); 624 | } 625 | try { 626 | await fs.promises.access(pkgPath, fs.constants.R_OK); 627 | } catch (error) { 628 | throw new Error(`Failed to create the pkg at: ${pkgPath}!`); 629 | } 630 | let findSigningIdentityOutput = ''; 631 | const findSigningIdentityExitCode = await exec('security', [ 632 | 'find-identity', 633 | '-v', projectRef.credential.keychainPath 634 | ], { 635 | listeners: { 636 | stdout: (data: Buffer) => { 637 | findSigningIdentityOutput += data.toString(); 638 | } 639 | }, 640 | ignoreReturnCode: true 641 | }); 642 | if (findSigningIdentityExitCode !== 0) { 643 | log(findSigningIdentityOutput, 'error'); 644 | throw new Error(`Failed to get the signing identity!`); 645 | } 646 | const matches = findSigningIdentityOutput.matchAll(/\d\) (?\w+) \"(?[^"]+)\"$/gm); 647 | const signingIdentities = Array.from(matches).map(match => ({ 648 | uuid: match.groups?.['uuid'], 649 | signing_identity: match.groups?.['signing_identity'] 650 | })).filter(identity => identity.signing_identity.includes('Developer ID Installer')); 651 | if (signingIdentities.length === 0) { 652 | throw new Error(`Failed to find the signing identity!`); 653 | } 654 | const developerIdInstallerSigningIdentity = signingIdentities[0].signing_identity; 655 | if (!developerIdInstallerSigningIdentity) { 656 | throw new Error(`Failed to find the Developer ID Installer signing identity!`); 657 | } 658 | const signedPkgPath = pkgPath.replace('.pkg', '-signed.pkg'); 659 | await exec('xcrun', [ 660 | 'productsign', 661 | '--sign', developerIdInstallerSigningIdentity, 662 | '--keychain', projectRef.credential.keychainPath, 663 | pkgPath, 664 | signedPkgPath 665 | ]); 666 | await exec('pkgutil', ['--check-signature', signedPkgPath]); 667 | await fs.promises.unlink(pkgPath); 668 | await fs.promises.rename(signedPkgPath, pkgPath); 669 | await notarizeArchive(projectRef, pkgPath, pkgPath); 670 | return pkgPath; 671 | } 672 | 673 | async function notarizeArchive(projectRef: XcodeProject, archivePath: string, staplePath: string): Promise { 674 | const notarizeArgs = [ 675 | 'notarytool', 676 | 'submit', 677 | '--key', projectRef.credential.appStoreConnectKeyPath, 678 | '--key-id', projectRef.credential.appStoreConnectKeyId, 679 | '--issuer', projectRef.credential.appStoreConnectIssuerId, 680 | '--team-id', projectRef.credential.teamId, 681 | '--wait', 682 | '--no-progress', 683 | '--output-format', 'json', 684 | ]; 685 | if (core.isDebug()) { 686 | notarizeArgs.push('--verbose'); 687 | } else { 688 | core.info(`[command]${xcrun} ${notarizeArgs.join(' ')} ${archivePath}`); 689 | } 690 | let notarizeOutput = ''; 691 | const notarizeExitCode = await exec(xcrun, [...notarizeArgs, archivePath], { 692 | silent: !core.isDebug(), 693 | listeners: { 694 | stdout: (data: Buffer) => { 695 | notarizeOutput += data.toString(); 696 | } 697 | }, 698 | ignoreReturnCode: true 699 | }); 700 | if (notarizeExitCode !== 0) { 701 | log(notarizeOutput, 'error'); 702 | throw new Error(`Failed to notarize the app!`); 703 | } 704 | log(notarizeOutput); 705 | const notaryResult = JSON.parse(notarizeOutput); 706 | if (notaryResult.status !== 'Accepted') { 707 | const notaryLogs = await getNotarizationLog(projectRef, notaryResult.id); 708 | throw new Error(`Notarization failed! Status: ${notaryResult.status}\n${notaryLogs}`); 709 | } 710 | const stapleArgs = [ 711 | 'stapler', 712 | 'staple', 713 | staplePath, 714 | ]; 715 | if (core.isDebug()) { 716 | stapleArgs.push('--verbose'); 717 | } else { 718 | core.info(`[command]${xcrun} ${stapleArgs.join(' ')}`); 719 | } 720 | let stapleOutput = ''; 721 | const stapleExitCode = await exec(xcrun, stapleArgs, { 722 | silent: !core.isDebug(), 723 | listeners: { 724 | stdout: (data: Buffer) => { 725 | stapleOutput += data.toString(); 726 | } 727 | }, 728 | ignoreReturnCode: true 729 | }); 730 | if (stapleExitCode !== 0) { 731 | log(stapleOutput, 'error'); 732 | throw new Error(`Failed to staple the notarization ticket!`); 733 | } 734 | log(stapleOutput); 735 | if (!stapleOutput.includes('The staple and validate action worked!')) { 736 | throw new Error(`Failed to staple the notarization ticket!\n${stapleOutput}`); 737 | } 738 | const notarization = await isAppBundleNotarized(staplePath); 739 | if (!notarization) { 740 | throw new Error(`Failed to notarize the app bundle!`); 741 | } 742 | } 743 | 744 | async function getNotarizationLog(projectRef: XcodeProject, id: string) { 745 | let output = ''; 746 | const notaryLogArgs = [ 747 | 'notarytool', 748 | 'log', 749 | id, 750 | '--key', projectRef.credential.appStoreConnectKeyPath, 751 | '--key-id', projectRef.credential.appStoreConnectKeyId, 752 | '--issuer', projectRef.credential.appStoreConnectIssuerId, 753 | '--team-id', projectRef.credential.teamId, 754 | ]; 755 | if (core.isDebug()) { 756 | notaryLogArgs.push('--verbose'); 757 | } 758 | const logExitCode = await exec(xcrun, notaryLogArgs, { 759 | listeners: { 760 | stdout: (data: Buffer) => { 761 | output += data.toString(); 762 | } 763 | }, 764 | ignoreReturnCode: true 765 | }); 766 | if (logExitCode !== 0) { 767 | throw new Error(`Failed to get notarization log!`); 768 | } 769 | } 770 | 771 | async function getExportOptions(projectRef: XcodeProject): Promise { 772 | const exportOptionPlistInput = core.getInput('export-option-plist'); 773 | let exportOptionsPath = undefined; 774 | if (!exportOptionPlistInput) { 775 | const exportOption = core.getInput('export-option') || 'development'; 776 | let method: string; 777 | if (projectRef.platform === 'macOS') { 778 | const archiveType = core.getInput('archive-type') || 'app'; 779 | projectRef.archiveType = archiveType; 780 | switch (exportOption) { 781 | case 'steam': 782 | method = 'developer-id'; 783 | projectRef.isSteamBuild = true; 784 | projectRef.archiveType = 'app'; 785 | break; 786 | case 'ad-hoc': 787 | method = 'development'; 788 | break; 789 | default: 790 | method = exportOption; 791 | break; 792 | } 793 | core.info(`Export Archive type: ${archiveType}`); 794 | } else { 795 | // revert back to development just in case user passes in steam for non-macos platforms 796 | if (exportOption === 'steam') { 797 | method = 'development'; 798 | } else { 799 | method = exportOption; 800 | } 801 | } 802 | // As of Xcode 15.4, the old export methods 'app-store', 'ad-hoc', and 'development' are now deprecated. 803 | // The new equivalents are 'app-store-connect', 'release-testing', and 'debugging'. 804 | const xcodeMinVersion = semver.coerce('15.4'); 805 | if (semver.gte(projectRef.xcodeVersion, xcodeMinVersion)) { 806 | switch (method) { 807 | case 'app-store': 808 | method = 'app-store-connect'; 809 | break; 810 | case 'ad-hoc': 811 | method = 'release-testing'; 812 | break; 813 | case 'development': 814 | method = 'debugging'; 815 | break; 816 | } 817 | } 818 | const exportOptions = { 819 | method: method, 820 | signingStyle: projectRef.credential.manualSigningIdentity ? 'manual' : 'automatic', 821 | teamID: `${projectRef.credential.teamId}` 822 | }; 823 | if (method === 'app-store-connect' && projectRef.autoIncrementBuildNumber) { 824 | exportOptions['manageAppVersionAndBuildNumber'] = true; 825 | } 826 | projectRef.exportOption = method; 827 | exportOptionsPath = `${projectRef.projectPath}/exportOptions.plist`; 828 | await fs.promises.writeFile(exportOptionsPath, plist.build(exportOptions)); 829 | } else { 830 | exportOptionsPath = exportOptionPlistInput; 831 | } 832 | core.info(`Export options path: ${exportOptionsPath}`); 833 | if (!exportOptionsPath) { 834 | throw new Error(`Invalid path for export-option-plist: ${exportOptionsPath}`); 835 | } 836 | const exportOptionsHandle = await fs.promises.open(exportOptionsPath, fs.constants.O_RDONLY); 837 | try { 838 | const exportOptionContent = await fs.promises.readFile(exportOptionsHandle, 'utf8'); 839 | core.info(`----- Export options content: -----\n${exportOptionContent}\n-----------------------------------`); 840 | const exportOptions = plist.parse(exportOptionContent); 841 | projectRef.exportOption = exportOptions['method']; 842 | } finally { 843 | await exportOptionsHandle.close(); 844 | } 845 | projectRef.exportOptionsPath = exportOptionsPath; 846 | } 847 | 848 | async function getDefaultEntitlementsMacOS(projectRef: XcodeProject): Promise { 849 | const entitlementsPath = `${projectRef.projectPath}/Entitlements.plist`; 850 | projectRef.entitlementsPath = entitlementsPath; 851 | try { 852 | await fs.promises.access(entitlementsPath, fs.constants.R_OK); 853 | core.debug(`Existing Entitlements.plist found at: ${entitlementsPath}`); 854 | return; 855 | } catch (error) { 856 | core.warning('Entitlements.plist not found, creating default Entitlements.plist...'); 857 | } 858 | const exportOption = projectRef.exportOption; 859 | let defaultEntitlements = undefined; 860 | switch (exportOption) { 861 | case 'app-store': 862 | case 'app-store-connect': 863 | defaultEntitlements = { 864 | 'com.apple.security.app-sandbox': true, 865 | 'com.apple.security.files.user-selected.read-only': true, 866 | }; 867 | break; 868 | default: 869 | // steam: https://partner.steamgames.com/doc/store/application/platforms#3 870 | defaultEntitlements = { 871 | 'com.apple.security.cs.disable-library-validation': true, 872 | 'com.apple.security.cs.allow-dyld-environment-variables': true, 873 | 'com.apple.security.cs.disable-executable-page-protection': true, 874 | }; 875 | break; 876 | } 877 | await fs.promises.writeFile(entitlementsPath, plist.build(defaultEntitlements)); 878 | } 879 | 880 | async function execXcodeBuild(xcodeBuildArgs: string[]) { 881 | let output = ''; 882 | const exitCode = await exec(xcodebuild, xcodeBuildArgs, { 883 | listeners: { 884 | stdout: (data: Buffer) => { 885 | output += data.toString(); 886 | }, 887 | stderr: (data: Buffer) => { 888 | output += data.toString(); 889 | } 890 | }, 891 | ignoreReturnCode: true 892 | }); 893 | await parseBundleLog(output); 894 | if (exitCode !== 0) { 895 | throw new Error(`xcodebuild exited with code: ${exitCode}`); 896 | } 897 | } 898 | 899 | async function execWithXcBeautify(xcodeBuildArgs: string[]) { 900 | try { 901 | await exec('xcbeautify', ['--version'], { silent: true }); 902 | } catch (error) { 903 | core.debug('Installing xcbeautify...'); 904 | await exec('brew', ['install', 'xcbeautify']); 905 | } 906 | const beautifyArgs = ['--quiet', '--is-ci', '--disable-logging']; 907 | const xcBeautifyProcess = spawn('xcbeautify', beautifyArgs, { 908 | stdio: ['pipe', process.stdout, process.stderr] 909 | }); 910 | core.info(`[command]${xcodebuild} ${xcodeBuildArgs.join(' ')}`); 911 | let errorOutput = ''; 912 | const exitCode = await exec(xcodebuild, xcodeBuildArgs, { 913 | listeners: { 914 | stdout: (data: Buffer) => { 915 | xcBeautifyProcess.stdin.write(data); 916 | }, 917 | stderr: (data: Buffer) => { 918 | xcBeautifyProcess.stdin.write(data); 919 | errorOutput += data.toString(); 920 | } 921 | }, 922 | silent: true, 923 | ignoreReturnCode: true 924 | }); 925 | xcBeautifyProcess.stdin.end(); 926 | await new Promise((resolve, reject) => { 927 | xcBeautifyProcess.stdin.on('finish', () => { 928 | xcBeautifyProcess.on('close', (code) => { 929 | if (code !== 0) { 930 | reject(new Error(`xcbeautify exited with code ${code}`)); 931 | } else { 932 | resolve(); 933 | } 934 | }); 935 | }); 936 | }); 937 | if (exitCode !== 0) { 938 | log(`xcodebuild error: ${errorOutput}`, 'error'); 939 | await parseBundleLog(errorOutput); 940 | throw new Error(`xcodebuild exited with code: ${exitCode}`); 941 | } 942 | } 943 | 944 | async function parseBundleLog(errorOutput: string) { 945 | const logFilePathMatch = errorOutput.match(/_createLoggingBundleAtPath:.*Created bundle at path "([^"]+)"/); 946 | if (!logFilePathMatch) { return; } 947 | const logFilePath = logFilePathMatch[1]; 948 | log(`Log file path: ${logFilePath}`, 'info'); 949 | try { 950 | await fs.promises.access(logFilePath, fs.constants.R_OK); 951 | const isDirectory = (await fs.promises.stat(logFilePath)).isDirectory(); 952 | if (isDirectory) { 953 | // list all files in the directory 954 | const files = await fs.promises.readdir(logFilePath); 955 | log(`Log file is a directory. Files: ${files.join(', ')}`, 'info'); 956 | return; 957 | } 958 | const logFileContent = await fs.promises.readFile(logFilePath, 'utf8'); 959 | log(`----- Log content: -----\n${logFileContent}\n-----------------------------------`, 'info'); 960 | } catch (error) { 961 | log(`Error reading log file: ${error.message}`, 'error'); 962 | } 963 | } 964 | 965 | export async function ValidateApp(projectRef: XcodeProject) { 966 | const platforms = { 967 | 'iOS': 'ios', 968 | 'macOS': 'macos', 969 | 'tvOS': 'appletvos', 970 | 'visionOS': 'xros' 971 | }; 972 | try { 973 | await fs.promises.access(projectRef.executablePath, fs.constants.R_OK); 974 | } catch (error) { 975 | throw new Error(`Failed to access the executable at: ${projectRef.executablePath}`); 976 | } 977 | const validateArgs = [ 978 | 'altool', 979 | '--validate-app', 980 | '--bundle-id', projectRef.bundleId, 981 | '--file', projectRef.executablePath, 982 | '--type', platforms[projectRef.platform], 983 | '--apiKey', projectRef.credential.appStoreConnectKeyId, 984 | '--apiIssuer', projectRef.credential.appStoreConnectIssuerId, 985 | '--output-format', 'json' 986 | ]; 987 | if (!core.isDebug()) { 988 | core.info(`[command]${xcrun} ${validateArgs.join(' ')}`); 989 | } else { 990 | validateArgs.push('--verbose'); 991 | } 992 | let output = ''; 993 | const exitCode = await exec(xcrun, validateArgs, { 994 | listeners: { 995 | stdout: (data: Buffer) => { 996 | output += data.toString(); 997 | } 998 | }, 999 | silent: !core.isDebug(), 1000 | ignoreReturnCode: true 1001 | }); 1002 | if (exitCode > 0) { 1003 | throw new Error(`Failed to validate app: ${JSON.stringify(JSON.parse(output), null, 2)}`); 1004 | } 1005 | } 1006 | 1007 | export async function UploadApp(projectRef: XcodeProject) { 1008 | const platforms = { 1009 | 'iOS': 'ios', 1010 | 'macOS': 'macos', 1011 | 'tvOS': 'appletvos', 1012 | 'visionOS': 'xros' 1013 | }; 1014 | const uploadArgs = [ 1015 | 'altool', 1016 | '--upload-package', projectRef.executablePath, 1017 | '--type', platforms[projectRef.platform], 1018 | '--apple-id', projectRef.appId, 1019 | '--bundle-id', projectRef.bundleId, 1020 | '--bundle-version', projectRef.bundleVersion, 1021 | '--bundle-short-version-string', projectRef.versionString, 1022 | '--apiKey', projectRef.credential.appStoreConnectKeyId, 1023 | '--apiIssuer', projectRef.credential.appStoreConnectIssuerId, 1024 | '--output-format', 'json' 1025 | ]; 1026 | if (!core.isDebug()) { 1027 | core.info(`[command]${xcrun} ${uploadArgs.join(' ')}`); 1028 | } else { 1029 | uploadArgs.push('--verbose'); 1030 | } 1031 | let output = ''; 1032 | const exitCode = await exec(xcrun, uploadArgs, { 1033 | listeners: { 1034 | stdout: (data: Buffer) => { 1035 | output += data.toString(); 1036 | } 1037 | }, 1038 | silent: !core.isDebug(), 1039 | ignoreReturnCode: true 1040 | }); 1041 | const outputJson = JSON.stringify(JSON.parse(output), null, 2); 1042 | if (exitCode !== 0) { 1043 | log(outputJson, 'error'); 1044 | throw new Error(`Failed to upload app!`); 1045 | } 1046 | core.debug(outputJson); 1047 | try { 1048 | const whatsNew = await getWhatsNew(); 1049 | core.info(`\n--------------- what's new ---------------\n${whatsNew}\n------------------------------------------\n`); 1050 | await UpdateTestDetails(projectRef, whatsNew); 1051 | } catch (error) { 1052 | log(`Failed to update test details!\n${error}`, 'error'); 1053 | } 1054 | } 1055 | 1056 | async function getWhatsNew(): Promise { 1057 | let whatsNew = core.getInput('whats-new'); 1058 | if (!whatsNew || whatsNew.length === 0) { 1059 | const head = github.context.eventName === 'pull_request' 1060 | ? github.context.payload.pull_request?.head.sha 1061 | : github.context.sha || 'HEAD'; 1062 | await execGit(['fetch', 'origin', head, '--depth=1']); 1063 | const commitSha = await execGit(['log', head, '-1', '--format=%h']); 1064 | const branchNameDetails = await execGit(['log', head, '-1', '--format=%d']); 1065 | const branchNameMatch = branchNameDetails.match(/\((?.+)\)/); 1066 | let branchName = ''; 1067 | if (branchNameMatch && branchNameMatch.groups) { 1068 | branchName = branchNameMatch.groups.branch; 1069 | if (branchName.includes(' -> ')) { 1070 | branchName = branchName.split(' -> ')[1]; 1071 | } 1072 | if (branchName.includes(',')) { 1073 | branchName = branchName.split(',')[1]; 1074 | } 1075 | } 1076 | let pullRequestInfo = ''; 1077 | if (github.context.eventName === 'pull_request') { 1078 | const prTitle = github.context.payload.pull_request?.title; 1079 | pullRequestInfo = `PR #${github.context.payload.pull_request?.number} ${prTitle}`; 1080 | } 1081 | const commitMessage = await execGit(['log', head, '-1', '--format=%B']); 1082 | whatsNew = `[${commitSha.trim()}] ${branchName.trim()}\n${pullRequestInfo}\n${commitMessage.trim()}`; 1083 | if (whatsNew.length > 4000) { 1084 | whatsNew = `${whatsNew.substring(0, 3997)}...`; 1085 | } 1086 | } 1087 | if (whatsNew.length === 0) { 1088 | throw new Error('Test details empty!'); 1089 | } 1090 | return whatsNew; 1091 | } 1092 | 1093 | async function execGit(args: string[]): Promise { 1094 | let output = ''; 1095 | if (!core.isDebug()) { 1096 | core.info(`[command]git ${args.join(' ')}`); 1097 | } 1098 | const exitCode = await exec('git', args, { 1099 | listeners: { 1100 | stdout: (data: Buffer) => { 1101 | output += data.toString(); 1102 | } 1103 | }, 1104 | silent: !core.isDebug() 1105 | }); 1106 | if (exitCode > 0) { 1107 | log(output, 'error'); 1108 | throw new Error(`Git failed with exit code: ${exitCode}`); 1109 | } 1110 | return output; 1111 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": false, 7 | "removeComments": true, 8 | "preserveConstEnums": true, 9 | "sourceMap": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "outDir": "dist", 12 | "declaration": false, 13 | "noEmit": true 14 | }, 15 | "compileOnSave": true, 16 | "include": [ 17 | "src/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | "src/**/*.spec.ts", 22 | "dist" 23 | ] 24 | } --------------------------------------------------------------------------------