diff --git a/.github/workflows/master-push.yml b/.github/workflows/master-push.yml new file mode 100644 index 00000000000..415ff500f35 --- /dev/null +++ b/.github/workflows/master-push.yml @@ -0,0 +1,216 @@ +name: Prepare Release +on: + push: + branches: + - "dp/migration_release_pipeline" +jobs: + prepare: + runs-on: ubuntu-latest + name: Create Draft Release to store artifacts + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Export GITHUB_TOKEN to workspace + run: echo "GITHUB_ACCESS_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV + - name: Install ruby + uses: ruby/setup-ruby@ec02537da5712d66d4d50a0f33b7eb52773b5ed1 + with: + ruby-version: '3.1.2' + - name: Install Gems + run: | + gem install octokit + - name: Create draft release to store artifacts. + run: ruby ./scripts/github_release.rb create-draft-release + build-docs: + runs-on: ubuntu-latest + name: Package docs and upload them to the draft release + needs: prepare + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Get Token + id: token + uses: yuki0n0/action-appstoreconnect-token@v1.0 + with: + # UUID. Can get from App Store Connect. + issuer id: ${{ secrets.APPLE_STORE_CONNECT_ISSUER_ID }} + # Key ID. Can get from App Store Connect. + key id: ${{ secrets.APPLE_STORE_CONNECT_KEY_ID }} + # P8 private key. Can get from App Store Connect. + key: ${{ secrets.APPLE_STORE_CONNECT_API_KEY }} + - name: Run release_package-docs xcode cloud build and wait for it to finish, this xcode cloud upload realm-docs.zip assets to the artifacts draft release + run: | + XCODE_VERSION="$(ruby ./scripts/release-matrix.rb docs_version)" + ruby ./scripts/xcode_cloud_helper.rb --run-release-workflow "release_package-docs_${XCODE_VERSION}" --token ${{ steps.token.outputs.token }} + build-examples: + runs-on: macos-latest + name: Package examples and upload them to the draft release + needs: prepare + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Export GITHUB_TOKEN to workspace + run: echo "GITHUB_ACCESS_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV + - name: Install ruby + uses: ruby/setup-ruby@ec02537da5712d66d4d50a0f33b7eb52773b5ed1 + with: + ruby-version: '3.1.2' + - name: Install Gems + run: | + gem install octokit + gem install xcodeproj + - name: Run release_package-examples package and uploads the example folder + run: ./build.sh release_package-examples + build-product: + runs-on: ubuntu-latest + name: Package product for each xcode version. + needs: build-examples + strategy: + matrix: + xcode-version: ['14.1', '14.2', '14.3.1', '15.0.1', '15.1'] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Get Token + id: token + uses: yuki0n0/action-appstoreconnect-token@v1.0 + with: + # UUID. Can get from App Store Connect. + issuer id: ${{ secrets.APPLE_STORE_CONNECT_ISSUER_ID }} + # Key ID. Can get from App Store Connect. + key id: ${{ secrets.APPLE_STORE_CONNECT_KEY_ID }} + # P8 private key. Can get from App Store Connect. + key: ${{ secrets.APPLE_STORE_CONNECT_API_KEY }} + - name: Run release_package_${{ matrix.xcode-version }} which upload the package to the draft release and wait for it to complete or fail + run: ruby ./scripts/xcode_cloud_helper.rb --run-release-workflow "release_package_${{ matrix.xcode-version }}" --token ${{ steps.token.outputs.token }} + test-package-examples: + runs-on: ubuntu-latest + name: Test examples, using the packages releases + needs: build-product + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Get Token + id: token + uses: yuki0n0/action-appstoreconnect-token@v1.0 + with: + # UUID. Can get from App Store Connect. + issuer id: ${{ secrets.APPLE_STORE_CONNECT_ISSUER_ID }} + # Key ID. Can get from App Store Connect. + key id: ${{ secrets.APPLE_STORE_CONNECT_KEY_ID }} + # P8 private key. Can get from App Store Connect. + key: ${{ secrets.APPLE_STORE_CONNECT_API_KEY }} + - name: Run release_test-package-examples_15.1 which runs test over the packages produces by the previous step and wait for it to complete or fail + run: ruby ./scripts/xcode_cloud_helper.rb --run-release-workflow release_test-package-examples_15.1 --token ${{ steps.token.outputs.token }} + test-ios-static: + runs-on: ubuntu-latest + name: Run tests on iOS with configuration Static + needs: build-product + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Get Token + id: token + uses: yuki0n0/action-appstoreconnect-token@v1.0 + with: + # UUID. Can get from App Store Connect. + issuer id: ${{ secrets.APPLE_STORE_CONNECT_ISSUER_ID }} + # Key ID. Can get from App Store Connect. + key id: ${{ secrets.APPLE_STORE_CONNECT_KEY_ID }} + # P8 private key. Can get from App Store Connect. + key: ${{ secrets.APPLE_STORE_CONNECT_API_KEY }} + - name: Run release_ios-static_15.1 which runs test over the packages produces by the previous step and wait for it to complete or fail + run: ruby ./scripts/xcode_cloud_helper.rb --run-release-workflow release_test-ios-static_15.1 --token ${{ steps.token.outputs.token }} + test-osx-static: + runs-on: ubuntu-latest + name: Run tests on macOS + needs: build-product + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Get Token + id: token + uses: yuki0n0/action-appstoreconnect-token@v1.0 + with: + # UUID. Can get from App Store Connect. + issuer id: ${{ secrets.APPLE_STORE_CONNECT_ISSUER_ID }} + # Key ID. Can get from App Store Connect. + key id: ${{ secrets.APPLE_STORE_CONNECT_KEY_ID }} + # P8 private key. Can get from App Store Connect. + key: ${{ secrets.APPLE_STORE_CONNECT_API_KEY }} + - name: Run release_osx_15.1 which runs test over the packages produces by the previous step and wait for it to complete or fail + run: ruby ./scripts/xcode_cloud_helper.rb --run-release-workflow release_test-osx_15.1 --token ${{ steps.token.outputs.token }} + test-installation: + runs-on: ubuntu-latest + name: Run installation test for ${{ matrix.platform }}, ${{ matrix.installation }} and ${{ matrix.linkage }} + needs: build-product + env: + XCODE_VERSION: '14.3.1' + strategy: + matrix: + platform: [ios, osx, watchos, tvos, catalyst] + installation: [cocoapods, spm, carthage, xcframework] + linkage: [static, dynamic] + exclude: + - platform: catalyst + installation: carthage + - installation: carthage + linkage: static + - platform: osx + installation: xcframework + linkage: static + - platform: watchos + installation: xcframework + linkage: static + - platform: tvos + installation: xcframework + linkage: static + - platform: catalyst + installation: xcframework + linkage: static + - platform: osx + installation: xcframework + linkage: dynamic + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Get Token + id: token + uses: yuki0n0/action-appstoreconnect-token@v1.0 + with: + # UUID. Can get from App Store Connect. + issuer id: ${{ secrets.APPLE_STORE_CONNECT_ISSUER_ID }} + # Key ID. Can get from App Store Connect. + key id: ${{ secrets.APPLE_STORE_CONNECT_KEY_ID }} + # P8 private key. Can get from App Store Connect. + key: ${{ secrets.APPLE_STORE_CONNECT_API_KEY }} + - name: Export REALM_TEST_BRANCH + run: echo "REALM_TEST_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV + - name: Creates an XCode Cloud workflow to run the test and runs build + run: | + ruby ./scripts/xcode_cloud_helper.rb --create-release-workflow-and-run test-installation-${{ matrix.platform }}-${{ matrix.installation }}-${{ matrix.linkage }} --xcode-version "$XCODE_VERSION" --token ${{ steps.token.outputs.token }} --team-id ${{ secrets.APPLE_STORE_CONNECT_TEAM_ID }} + test-installation-xcode: + runs-on: ubuntu-latest + name: Run installation test on macOS only for for xcframework for all ${{ matrix.xcode_version }} + needs: build-product + strategy: + matrix: + xcode_version: ['14.1', '14.2', '14.3.1', '15.0.1', '15.1'] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Get Token + id: token + uses: yuki0n0/action-appstoreconnect-token@v1.0 + with: + # UUID. Can get from App Store Connect. + issuer id: ${{ secrets.APPLE_STORE_CONNECT_ISSUER_ID }} + # Key ID. Can get from App Store Connect. + key id: ${{ secrets.APPLE_STORE_CONNECT_KEY_ID }} + # P8 private key. Can get from App Store Connect. + key: ${{ secrets.APPLE_STORE_CONNECT_API_KEY }} + - name: Export REALM_TEST_BRANCH + run: echo "REALM_TEST_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV + - name: Creates an XCode Cloud workflow to run the test and runs build + run: | + ruby ./scripts/xcode_cloud_helper.rb --create-release-workflow-and-run test-installation-osx-xcframework-dynamic --xcode-version ${{ matrix.xcode_version }} --token ${{ steps.token.outputs.token }} --team-id ${{ secrets.APPLE_STORE_CONNECT_TEAM_ID }} \ No newline at end of file diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 00000000000..fb7b837d599 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,174 @@ +name: Publish release +on: workflow_dispatch +jobs: + prepare: + runs-on: macos-latest + name: Check if draft release is created + steps: + - name: Get draft release + id: get_release + uses: agners/get-draft-release@v0.1.0 + env: + GITHUB_TOKEN: ${{ github.token }} + tag-release: + runs-on: macos-latest + name: Tag Release + needs: prepare + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Extract branch name + shell: bash + run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT + id: extract_branch + - name: Tag release + run: ./build.sh publish-tag ${{ steps.extract_branch.outputs.branch }} + publish-docs: + runs-on: macos-latest + name: Publish docs to S3 Bucket + needs: prepare + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Export AWS Secrets to workspace + run: | + echo "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" >> $GITHUB_ENV + echo "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> $GITHUB_ENV + - name: Download docs and upload them to S3 + run: | + ./build.sh publish-docs + create-release: + runs-on: macos-latest + name: Create github release + needs: prepare + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Export GITHUB_TOKEN to workspace + run: echo "GITHUB_ACCESS_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV + - name: Create Github release + run: ./build.sh publish-github + publish-cocoapods: + runs-on: macos-latest + name: Publish Cocoapods specs + needs: prepare + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Read SDK version + id: get-version + run: | + version="$(sed -n 's/^VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")"pkgVersion=$(find . -type f -regex ".*Realm.[1-9].*.nupkg" -exec basename {} \; | sed -n 's/Realm\.\(.*\)\.nupkg$/\1/p') + echo "version=$version" >> $GITHUB_OUTPUT + - name: Export COCOAPODS_TRUNK_TOKEN to workspace + run: echo "COCOAPODS_TRUNK_TOKEN=${{ secrets.COCOAPODS_TRUNK_TOKEN }}" >> $GITHUB_ENV + - name: Publish + run: ./build.sh publish-cocoapods v${{ steps.get-version.outputs.version }} + update-update-checker: + runs-on: macos-latest + name: Publish Cocoapods specs + needs: prepare + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Export AWS Secrets to workspace + run: | + echo "AWS_ACCESS_KEY_ID=${{ secrets.UPDATE_CHECKER_ACCESS_KEY }}" >> $GITHUB_ENV + echo "AWS_SECRET_ACCESS_KEY=${{ secrets.UPDATE_CHECKER_SECRET_KEY }}" >> $GITHUB_ENV + - name: Create Github release + run: ./build.sh publish-update-checker + test-installation: + runs-on: ubuntu-latest + name: Run installation test for ${{ matrix.platform }}, ${{ matrix.installation }} and ${{ matrix.linkage }} + needs: update-update-checker + env: + XCODE_VERSION: '15.1' + strategy: + matrix: + platform: [ios, osx, watchos, tvos, catalyst, visionos] + installation: [cocoapods, spm, carthage, xcframework] + linkage: [static, dynamic] + exclude: + - installation: carthage + linkage: static + - platform: osx + installation: xcframework + linkage: static + - platform: watchos + installation: xcframework + linkage: static + - platform: tvos + installation: xcframework + linkage: static + - platform: catalyst + installation: xcframework + linkage: static + - platform: visionos + installation: xcframework + linkage: static + - platform: catalyst + installation: carthage + linkage: static + - platform: visionos + installation: carthage + - platform: visionos + installation: cocoapods + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Get Token + id: token + uses: yuki0n0/action-appstoreconnect-token@v1.0 + with: + # UUID. Can get from App Store Connect. + issuer id: ${{ secrets.APPLE_STORE_CONNECT_ISSUER_ID }} + # Key ID. Can get from App Store Connect. + key id: ${{ secrets.APPLE_STORE_CONNECT_KEY_ID }} + # P8 private key. Can get from App Store Connect. + key: ${{ secrets.APPLE_STORE_CONNECT_API_KEY }} + - name: Read SDK version + id: get-version + run: | + version="$(sed -n 's/^VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")"pkgVersion=$(find . -type f -regex ".*Realm.[1-9].*.nupkg" -exec basename {} \; | sed -n 's/Realm\.\(.*\)\.nupkg$/\1/p') + echo "version=$version" >> $GITHUB_OUTPUT + - name: Export REALM_TEST_RELEASE + run: echo "REALM_TEST_RELEASE=${{ steps.get-version.outputs.version }}" >> $GITHUB_ENV + - name: Creates an XCode Cloud workflow to run the test and runs build + run: | + ruby ./scripts/xcode_cloud_helper.rb --create-release-workflow-and-run test-installation-${{ matrix.platform }}-${{ matrix.installation }}-${{ matrix.linkage }} --xcode-version "$XCODE_VERSION" --token ${{ steps.token.outputs.token }} --team-id ${{ secrets.APPLE_STORE_CONNECT_TEAM_ID }} + post-slack-release: + runs-on: macos-latest + name: Publish to release Slack channel + needs: test-installation + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Prepare Changelog + run: ./build.sh prepare-publish-changelog + - name: Read SDK version + id: get-version + run: | + version="$(sed -n 's/^VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")"pkgVersion=$(find . -type f -regex ".*Realm.[1-9].*.nupkg" -exec basename {} \; | sed -n 's/Realm\.\(.*\)\.nupkg$/\1/p') + echo "version=$version" >> $GITHUB_OUTPUT + - name: 'Post to #realm-releases' + uses: realm/ci-actions/release-to-slack@v3 + with: + changelog: ExtractedChangelog/CHANGELOG.md + sdk: Swift + webhook-url: ${{ secrets.SLACK_RELEASE_WEBHOOK }} + version: ${{ steps.get-version.outputs.version }} + add-empty-changelog: + runs-on: macos-latest + name: Add empty changelog and push it to master, preparing for a future release + needs: post-slack-release + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Add empty changelog + run: ./build.sh add-empty-changelog + - name: Commit & Push changes to master + uses: actions-js/push@master + with: + github_token: ${{ github.token }} + + \ No newline at end of file diff --git a/build.sh b/build.sh index 886a7224c5b..7e9151b1590 100755 --- a/build.sh +++ b/build.sh @@ -31,11 +31,11 @@ fi if [ -n "$CI" ]; then DERIVED_DATA="$CI_DERIVED_DATA_PATH" - ROOT_WORKSPACE="$CI_WORKSPACE/" + ROOT_WORKSPACE="$CI_WORKSPACE" BRANCH="$CI_BRANCH" else DERIVED_DATA="build/DerivedData/Realm" - ROOT_WORKSPACE="" + ROOT_WORKSPACE="$(pwd)" BRANCH="$(git branch --show-current)" fi @@ -187,7 +187,7 @@ build_combined() { local product_name="$product.framework" local os_path="$build_products_path/$config${config_suffix}/$product_name" local simulator_path="$build_products_path/$config-$simulator_suffix/$product_name" - local out_path="build/$config/$platform" + local out_path="$ROOT_WORKSPACE/$config/$platform" local xcframework_path="$out_path/$product.xcframework" # Build for each platform @@ -308,7 +308,7 @@ fi ###################################### COMMAND="$1" -LINKAGE="Dynamic" +LINKAGE="dynamic" # Use Debug config if command ends with -debug, otherwise default to Release case "$COMMAND" in @@ -318,7 +318,7 @@ case "$COMMAND" in ;; *-static) COMMAND="${COMMAND%-static}" - LINKAGE="Static" + LINKAGE="static" CONFIGURATION="Static" ;; esac @@ -992,6 +992,12 @@ case "$COMMAND" in exit 0 ;; + "prepare-release-changelog") + realm_version="$2" + + exit 0 + ;; + "set-core-version") new_version="$2" old_version="$(sed -n 's/^REALM_CORE_VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")" @@ -1069,60 +1075,112 @@ case "$COMMAND" in # Release packaging ###################################### - "package-docs") - sh scripts/reset-simulators.sh + "release_package-examples") + ./scripts/package_examples.rb + zip --symlinks -r realm-examples.zip examples -x "examples/installation/*" + ./scripts/github_prepare.rb --upload-product realm-examples.zip --path "${GITHUB_WORKSPACE}/realm-examples.zip" + ;; + + "release_package-docs") sh build.sh docs - cd docs - zip -r realm-docs.zip objc_output swift_output + zip -r docs/realm-docs.zip docs/objc_output docs/swift_output + ./scripts/github_prepare.rb --upload-product realm-docs.zip --path "${ROOT_WORKSPACE}/docs/realm-docs.zip" ;; - "package-examples") + ("release_package") + XCODE_VERSION="$2" + PLATFORMS=$(./scripts/release-matrix.rb plaforms_for_version $XCODE_VERSION) + PLATFORMS_ARRAY=(${PLATFORMS//,/ }) + + # Package examples for package ./scripts/package_examples.rb zip --symlinks -r realm-examples.zip examples -x "examples/installation/*" - ;; - "package-build-scripts") - zip -r build-scripts.zip build.sh dependencies.list scripts examples/installation + # Create frameworks for each platform and zip it + for platform in ${PLATFORMS_ARRAY[@]}; do + sh build.sh "$platform-swift" + if [[ "$platform" == ios ]]; then + sh build.sh "$platform-static" + else + mkdir -p build/Static + fi + + FILE_NAME="realm-${platform}-${XCODE_VERSION}" + zip --symlinks -r "${FILE_NAME}.zip" "Release/${platform}" "Static/${platform}" + done + + # Create Realm(Static)/RealmSwift XCFrameworks zips combining all the platforms frameworks + VERSION="$(sed -n 's/^VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")" + find . -name 'realm-*-1*.zip' -maxdepth 1 \ + | sed 's@./realm-[a-z]*-\(.*\).zip@\1@' \ + | sort -u --version-sort \ + | xargs ./scripts/create-release-package.rb create-version-xcframeworks "${ROOT_WORKSPACE}/pkg" "${VERSION}" "${XCODE_VERSION}" + + + echo "Uploading data into draft release" + # Upload RealmSwift zips to draft release + ./scripts/github_prepare.rb --upload-product "realm-swift-${VERSION}_${XCODE_VERSION}.zip" --path "${ROOT_WORKSPACE}/pkg/realm-swift-${VERSION}_${XCODE_VERSION}.zip" + ./scripts/github_prepare.rb --upload-product "RealmSwift@${XCODE_VERSION}.spm.zip" --path "${ROOT_WORKSPACE}/pkg/RealmSwift@${XCODE_VERSION}.spm.zip" + + # Upload Realm zips to draft release, only for latest xcode version + if [ -f "${ROOT_WORKSPACE}pkg/Realm.spm.zip" ]; then + ./scripts/github_prepare.rb --upload-product "Realm.spm.zip" --path "${ROOT_WORKSPACE}pkg/Realm.spm.zip" + ./scripts/github_prepare.rb --upload-product "Carthage.xcframework.zip" --path "${ROOT_WORKSPACE}/pkg/Carthage.xcframework.zip" + fi ;; - "package-test-examples") + ("release_test-package-examples") + XCODE_VERSION="$2" VERSION="$(sed -n 's/^VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")" + dir="realm-swift-${VERSION}" + + # Download package for current xcode version + ./scripts/github_prepare.rb --download-asset "${dir}_${XCODE_VERSION}.zip" --path "${ROOT_WORKSPACE}/${dir}.zip" + + # Unzip it unzip "${dir}.zip" + # Copy the build.sh file into the downloaded directory cp "$0" "${dir}" - cp -r "${source_root}/scripts" "${dir}" + + # Copy the scripts into the downloaded directory + cp -r "${ROOT_WORKSPACE}/scripts" "${dir}" + cd "${dir}" + # Test Examples sh build.sh examples-ios sh build.sh examples-tvos sh build.sh examples-osx sh build.sh examples-ios-swift sh build.sh examples-tvos-swift - cd .. - rm -rf "${dir}" ;; - "package") - PLATFORM="$2" - sh build.sh "$PLATFORM-swift" - if [[ "$PLATFORM" == ios ]]; then - sh build.sh "$PLATFORM-static" - else - mkdir -p Static - fi - - cd build - zip --symlinks -r "realm-$PLATFORM-$REALM_XCODE_VERSION.zip" "Release/$PLATFORM" "Static/$PLATFORM" + ("release_test-ios") + echo "Test run on test action" + ;; + + ("release_test-osx") + echo "Test run on test action" ;; - "package-release") - version="$(sed -n 's/^VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")" - find . -name 'realm-*-1*.zip' -maxdepth 1 \ - | sed 's@./realm-[a-z]*-\(.*\).zip@\1@' \ - | sort -u --version-sort \ - | xargs ./scripts/create-release-package.rb "${WORKSPACE}/pkg" "${version}" + (release_test-installation-*) + target=(${COMMAND//-/ }) + platform="${target[2]}" + installation="${target[3]}" + VERSION="$(sed -n 's/^VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")" + + cd examples/installation + + # When the target finish in static $LINKAGE variable changes to static + echo "Testing installation ${installation} method in ${platform} for ${LINKAGE}" + ./build.rb "${platform}" "${installation}" "${LINKAGE}" ;; + ###################################### + # Publish + ###################################### + "test-package-release") # Generate a release package locally for testing purposes # Real releases should always be done via Jenkins @@ -1182,7 +1240,39 @@ case "$COMMAND" in "publish-github") VERSION="$(sed -n 's/^VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")" - ./scripts/github_release.rb "$VERSION" + XCODE_VERSIONS=$(./scripts/release-matrix.rb xcode_versions) + XCODE_VERSIONS_ARRAY=(${XCODE_VERSIONS//,/ }) + + # Create binary directory to store all the data to upload. + package_dir=release_pkg + mkdir -p "${package_dir}" + + # Download RealmSwift.xcframework/Realm.xcframework for each xcode version and pack it in a package containig all of them, created in the prepare step. + file_name=realm-swift-${VERSION} + for xcode_version in ${XCODE_VERSIONS_ARRAY[@]}; do + ./scripts/github_prepare.rb --download-asset "${file_name}_${xcode_version}.zip" --path "${ROOT_WORKSPACE}/${file_name}_${xcode_version}.zip" + + unzip -o "${file_name}_${xcode_version}.zip" + done + + zip --symlinks -r "${ROOT_WORKSPACE}/${package_dir}/${file_name}.zip" "${file_name}" + + # Download SPM files for each xcode version, created in the prepare step. + for xcode_version in ${XCODE_VERSIONS_ARRAY[@]}; do + spm_file_name=RealmSwift@${xcode_version}.spm.zip + ./scripts/github_prepare.rb --download-asset "${spm_file_name}" --path "${ROOT_WORKSPACE}/${package_dir}/${spm_file_name}" + done + + # Download the Obj-C xcframework created in the prepare step. + realm_file_name=Realm.spm.zip + ./scripts/github_prepare.rb --download-asset "${realm_file_name}" --path "${ROOT_WORKSPACE}/${package_dir}/${realm_file_name}" + + # Download the Carthage xcframework + carthage_file_name=Carthage.xcframework.zip + ./scripts/github_prepare.rb --download-asset "${carthage_file_name}" --path "${ROOT_WORKSPACE}/${package_dir}/${carthage_file_name}" + + # Prepare version for + ./scripts/github_release.rb create-release "$VERSION" ;; "publish-docs") @@ -1191,8 +1281,12 @@ case "$COMMAND" in if [[ $VERSION =~ $PRERELEASE_REGEX ]]; then exit 0 fi - rm -rf swift_output objc_output + + # Download docs from the draft release + ./scripts/github_prepare.rb --download-asset "realm-docs.zip" --path "${ROOT_WORKSPACE}/realm-docs.zip" + unzip realm-docs.zip + s3cmd put --recursive --acl-public --access_key=${AWS_ACCESS_KEY_ID} --secret_key=${AWS_SECRET_ACCESS_KEY} swift_output/ s3://realm-sdks/docs/realm-sdks/swift/${VERSION}/ s3cmd put --recursive --acl-public --access_key=${AWS_ACCESS_KEY_ID} --secret_key=${AWS_SECRET_ACCESS_KEY} swift_output/ s3://realm-sdks/docs/realm-sdks/swift/latest/ @@ -1209,28 +1303,32 @@ case "$COMMAND" in # update static.realm.io/update/cocoa printf "%s" "${VERSION}" > cocoa - s3cmd put cocoa s3://static.realm.io/update/ - rm cocoa + s3cmd put --recursive --acl-public --access_key=${AWS_ACCESS_KEY_ID} --secret_key=${AWS_SECRET_ACCESS_KEY} cocoa s3://static.realm.io/update/ ;; "publish-tag") git clone git@github.com:realm/realm-swift.git cd realm-swift - git checkout "$2" + git checkout "$BRANCH" VERSION="$(sed -n 's/^VERSION=\(.*\)$/\1/p' dependencies.list)" git tag -m "Release ${VERSION}" "v${VERSION}" git push origin "v${VERSION}" ;; "publish-cocoapods") + VERSION="$(sed -n 's/^VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")" git clone https://github.com/realm/realm-swift cd realm-swift - git checkout "$2" - ./scripts/reset-simulators.rb + git checkout "$VERSION" pod trunk push Realm.podspec --verbose --allow-warnings pod trunk push RealmSwift.podspec --verbose --allow-warnings --synchronous ;; + "prepare-publish-changelog") + VERSION="$(sed -n 's/^VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")" + ./scripts/github_release.rb package-release-notes "$VERSION" + ;; + "add-empty-changelog") empty_section=$(cat <>> Installing dependencies for ${CI_WORKFLOW}" brew install moreutils - - if [[ "$CI_WORKFLOW" == "docs"* ]]; then + if [[ "$CI_WORKFLOW" == "docs"* ]] || [[ "$CI_WORKFLOW" == *"docs"* ]]; then install_ruby gem install jazzy -v ${JAZZY_VERSION} elif [[ "$CI_WORKFLOW" == "swiftlint"* ]]; then brew install swiftlint - elif [[ "$CI_WORKFLOW" == "cocoapods"* ]]; then + elif [[ "$CI_WORKFLOW" == "cocoapods"* ]] || [[ "$CI_WORKFLOW" == *"cocoapods"* ]]; then install_ruby gem install cocoapods -v ${COCOAPODS_VERSION} - elif [[ "$$CI_WORKFLOW" = *"xcode"* ]] || [[ "$target" = "xcframework"* ]]; then + elif [[ "$CI_WORKFLOW" == *"xcode"* ]] || [[ "$target" == "xcframework"* ]]; then install_ruby + elif [[ "$CI_WORKFLOW" == *"carthage"* ]]; then + brew install carthage + fi + + if [[ "$CI_WORKFLOW" == "release"* ]]; then + echo "Installing release gems" + install_ruby + gem install octokit + gem install getoptlong + gem install xcodeproj + fi + + if [[ "$CI_WORKFLOW" == *"package_15.1" ]]; then + # We need to install the visionOS because is not installed by default in the XCode Cloud image, + # even if the build action selected platform is visionOS. + echo "Installing visionos" + xcodebuild -downloadPlatform visionOS fi } +INSTALLED=0 install_ruby() { # Ruby Installation - echo ">>> Installing new Version of ruby" - brew install rbenv ruby-build - rbenv install ${RUBY_VERSION} - rbenv global ${RUBY_VERSION} - echo 'export PATH=$HOME/.rbenv/bin:$PATH' >>~/.bash_profile - eval "$(rbenv init -)" + if [[ "$INSTALLED" == 0 ]]; then + echo ">>> Installing new Version of ruby" + brew install rbenv ruby-build + rbenv install ${RUBY_VERSION} + rbenv global ${RUBY_VERSION} + echo 'export PATH=$HOME/.rbenv/bin:$PATH' >>~/.bash_profile + eval "$(rbenv init -)" + INSTALLED=1 + fi } update_scheme_configuration() { @@ -77,11 +96,19 @@ install_dependencies # CI Workflows cd .. -# Get target name -TARGET=$(echo "$CI_WORKFLOW" | cut -f1 -d_) - -# Update schemes configuration -update_scheme_configuration ${TARGET} - -export target="${TARGET}" -sh -x build.sh ci-pr | ts \ No newline at end of file +if [[ "$CI_WORKFLOW" == "release"* ]]; then + echo "Release workflow" + TARGET="${CI_WORKFLOW%_*}" + XCODE_VERSION=${CI_WORKFLOW##*_} + sh -x build.sh "${TARGET}" "${XCODE_VERSION}" | ts +else + # CI PR Pipelines, use this path + # Get target name + TARGET=$(echo "$CI_WORKFLOW" | cut -f1 -d_) + + # Update schemes configuration + update_scheme_configuration ${TARGET} + + export target="${TARGET}" + sh -x build.sh ci-pr | ts +fi diff --git a/examples/installation/build.rb b/examples/installation/build.rb index be8803bce13..c6b4e80e807 100755 --- a/examples/installation/build.rb +++ b/examples/installation/build.rb @@ -118,11 +118,6 @@ def download_realm(platform, method, static) end File.write 'Cartfile', 'github "realm/realm-swift"' + version - # Carthage requires that a simulator exist, but `xcodebuild -list` is - # sometimes very slow if too many simulators exist, so delete all but one - # per platform - sh '../../scripts/reset-simulators.rb', '-firstOnly' - platformName = case platform when 'ios' then 'iOS' when 'osx' then 'Mac' diff --git a/scripts/create-release-package.rb b/scripts/create-release-package.rb index 06793349809..27ac681ef67 100755 --- a/scripts/create-release-package.rb +++ b/scripts/create-release-package.rb @@ -3,16 +3,19 @@ require 'fileutils' require 'pathname' require 'tmpdir' +require_relative "release-matrix" -raise 'usage: create-release-package.rb destination_path version [xcode_versions]' unless ARGV.length >= 3 +include RELEASE -DESTINATION = Pathname(ARGV[0]) -VERSION = ARGV[1] -XCODE_VERSIONS = ARGV[2..] +raise 'usage: create-release-package.rb destination_path version [xcode_version]' unless ARGV.length >= 3 + +DESTINATION = Pathname(ARGV[1]) +VERSION = ARGV[2] +XCODE_VERSION = ARGV[3] ROOT = Pathname(__FILE__).+('../..').expand_path BUILD_SH = Pathname(__FILE__).+('../../build.sh').expand_path VERBOSE = false -OBJC_XCODE_VERSION = XCODE_VERSIONS.last +OBJC_XCODE_VERSION = RELEASE::XCODE_VERSIONS.last.partition(" ").first def sh(*args) puts "executing: #{args.join(' ')}" if VERBOSE @@ -31,7 +34,7 @@ def create_xcframework(root, xcode_version, configuration, name) prefix = "#{root}/#{xcode_version}" output = "#{prefix}/#{configuration}/#{name}.xcframework" files = Dir.glob "#{prefix}/#{configuration}/*/#{name}.xcframework/*/#{name}.framework" - + puts "Creating xcframework to #{output} from #{prefix}/#{configuration}/*/#{name}.xcframework/*/#{name}.framework returning files #{files}" sh 'xcodebuild', '-create-xcframework', '-allow-internal-distribution', '-output', output, *files.flat_map {|f| ['-framework', f]} end @@ -39,93 +42,127 @@ def create_xcframework(root, xcode_version, configuration, name) def zip(name, *files) path = (DESTINATION + name).to_path FileUtils.rm_f path + puts "Zipping file #{name} into #{path}" sh 'zip', '--symlinks', '-r', path, *files end -puts "Packaging version #{VERSION} for Xcode versions #{XCODE_VERSIONS.join(', ')}" -FileUtils.mkdir_p DESTINATION - -Dir.mktmpdir do |tmp| - # The default temp directory is in /var, which is a symlink to /private/var - # xcodebuild's relative path resolution breaks due to this and we need to - # give it the fully resolved path - tmp = File.realpath tmp - - for version in XCODE_VERSIONS - puts "Extracting source binaries for Xcode #{version}" - FileUtils.mkdir_p "#{tmp}/#{version}" - Dir.chdir("#{tmp}/#{version}") do - for platform in platforms(version) - sh 'unzip', "#{ROOT}/realm-#{platform}-#{version}.zip" +def create_version_xcframeworks + puts "Packaging version #{VERSION} for Xcode version #{XCODE_VERSION}" + FileUtils.mkdir_p DESTINATION + + Dir.mktmpdir do |tmp| + # The default temp directory is in /var, which is a symlink to /private/var + # xcodebuild's relative path resolution breaks due to this and we need to + # give it the fully resolved path + tmp = File.realpath tmp + + # Extracts/Unzip all the binarios for each of the platforms for the current XCode version into a temp + # folder, which is gonna be used to generate the zip for each XCode version. + puts "Extracting source binaries for Xcode #{XCODE_VERSION}" + FileUtils.mkdir_p "#{tmp}/#{XCODE_VERSION}" + Dir.chdir("#{tmp}/#{XCODE_VERSION}") do + for platform in platforms(XCODE_VERSION) + puts "Unziping #{ROOT}/realm-#{platform}-#{XCODE_VERSION}.zip into #{tmp}/#{XCODE_VERSION}" + sh 'unzip', "#{ROOT}/realm-#{platform}-#{XCODE_VERSION}.zip" end end - end - for version in XCODE_VERSIONS - puts "Creating Swift XCFrameworks for Xcode #{version}" - create_xcframework tmp, version, 'Release', 'RealmSwift' - end + # Creates a RealmSwift.xcframework from each platform framework for any supported architecture (device and simulator). + puts "Creating Swift XCFrameworks for Xcode #{XCODE_VERSION}" + create_xcframework tmp, XCODE_VERSION, 'Release', 'RealmSwift' - puts 'Creating Obj-C XCFrameworks' - create_xcframework tmp, OBJC_XCODE_VERSION, 'Release', 'Realm' - create_xcframework tmp, OBJC_XCODE_VERSION, 'Static', 'Realm' - - puts 'Creating release package' - package_dir = "#{tmp}/realm-swift-#{VERSION}" - FileUtils.mkdir_p package_dir - sh 'cp', "#{ROOT}/LICENSE", package_dir - sh 'unzip', "#{ROOT}/realm-examples.zip", '-d', package_dir - for lang in %w(objc swift) - File.write "#{package_dir}/#{lang}-docs.webloc", %Q{ - - - - - URL - https://www.mongodb.com/docs/realm-sdks/${lang}/${version} - - - } - end - sh 'cp', '-Rca', "#{tmp}/#{OBJC_XCODE_VERSION}/Release/Realm.xcframework", "#{package_dir}" - FileUtils.mkdir_p "#{package_dir}/static" - sh 'cp', '-Rca', "#{tmp}/#{OBJC_XCODE_VERSION}/Static/Realm.xcframework", "#{package_dir}/static" - for version in XCODE_VERSIONS - FileUtils.mkdir_p "#{package_dir}/#{version}" - sh 'cp', '-Rca', "#{tmp}/#{version}/Release/RealmSwift.xcframework", "#{package_dir}/#{version}" - end + if XCODE_VERSION == OBJC_XCODE_VERSION + # Creates a Realm.xcframework from each platform framework for any supported architecture (device and simulator). + puts 'Creating Obj-C XCFrameworks, only for latest xcode version' + create_xcframework tmp, XCODE_VERSION, 'Release', 'Realm' + # Creates a Static Realm.xcframework from each platform framework for any supported architecture (device and simulator). + create_xcframework tmp, XCODE_VERSION, 'Static', 'Realm' + end - Dir.chdir(tmp) do - zip "realm-swift-#{VERSION}.zip", "realm-swift-#{VERSION}" - end + package_dir = "#{tmp}/realm-swift-#{VERSION}" + FileUtils.mkdir_p package_dir + puts "Creating release package on temp #{tmp}/realm-swift-#{VERSION} from generated xcframeworks" + + sh 'cp', "#{ROOT}/LICENSE", package_dir + sh 'unzip', "#{ROOT}/realm-examples.zip", '-d', package_dir + + for lang in %w(objc swift) + File.write "#{package_dir}/#{lang}-docs.webloc", %Q{ + + + + + URL + https://www.mongodb.com/docs/realm-sdks/${lang}/${version} + + + } + end - puts 'Creating SPM release zips' - Dir.chdir "#{tmp}/#{OBJC_XCODE_VERSION}/Release" do - zip 'Realm.spm.zip', "Realm.xcframework" - end - for version in XCODE_VERSIONS - Dir.chdir "#{tmp}/#{version}/Release" do - zip "RealmSwift@#{version}.spm.zip", 'RealmSwift.xcframework' + # Copy the generated RealmSwift.xcframework into a temp directory following this notation realm-swift-#{VERSION}/#{XCODE_VERSION}/ + puts "Copying Release XCFramework into a #{XCODE_VERSION} folder" + FileUtils.mkdir_p "#{package_dir}/#{XCODE_VERSION}" + sh 'cp', '-Rca', "#{tmp}/#{XCODE_VERSION}/Release/RealmSwift.xcframework", "#{package_dir}/#{XCODE_VERSION}" + + if XCODE_VERSION == OBJC_XCODE_VERSION + # Copy the generated Realm.xcframework into a temp directory following this notation realm-swift-#{VERSION} + puts 'Copying Release Obj-C XCFramework, only for latest xcode version' + sh 'cp', '-Rca', "#{tmp}/#{XCODE_VERSION}/Release/Realm.xcframework", "#{package_dir}" + + # Copy the generated Static Realm.xcframework into a temp directory following this notation realm-swift-#{VERSION}/static + puts 'Copying Static Obj-C XCFramework, only for latest xcode version' + FileUtils.mkdir_p "#{package_dir}/static" + sh 'cp', '-Rca', "#{tmp}/#{XCODE_VERSION}/Static/Realm.xcframework", "#{package_dir}/static" end - end -end -# Our normal Xcode 15 xcframework includes visionOS slices build with a beta -# version of Xcode, but Carthage doesn't like that so we have to build a -# separate xcframework without visionOS -puts 'Creating Carthage release zip' -Dir.mktmpdir do |tmp| - tmp = File.realpath tmp - Dir.chdir(tmp) do - for platform in platforms('14') - sh 'unzip', "#{ROOT}/realm-#{platform}-#{OBJC_XCODE_VERSION}.zip" + # Zip the generated RealmSwift/Realm(Static) xcframework into a generated realm-swift-#{VERSION}.zip + puts 'Packing all the xcframework into a zip, only for latest xcode version' + Dir.chdir(tmp) do + zip "realm-swift-#{VERSION}_#{XCODE_VERSION}.zip", "realm-swift-#{VERSION}" end - create_xcframework tmp, '', 'Release', 'RealmSwift' - create_xcframework tmp, '', 'Release', 'Realm' - Dir.chdir('Release') do - zip 'Carthage.xcframework.zip', 'Realm.xcframework', 'RealmSwift.xcframework' + # Zip generated RealmSwift.xcframework into a generated RealmSwift@#{XCODE_VERSION}.spm.zip + Dir.chdir "#{tmp}/#{XCODE_VERSION}/Release" do + zip "RealmSwift@#{XCODE_VERSION}.spm.zip", 'RealmSwift.xcframework' + end + + if XCODE_VERSION == OBJC_XCODE_VERSION + Dir.chdir "#{tmp}/#{XCODE_VERSION}/Release" do + # Zip generated Realm.xcframework into a generated Realm.spm.zip + puts 'Packing Obj-C xcframework into a zip' + zip "Realm.spm.zip", "Realm.xcframework" + end + end + end + + if XCODE_VERSION == OBJC_XCODE_VERSION + # Our normal Xcode 15 xcframework includes visionOS slices build with a beta + # version of Xcode, but Carthage doesn't like that so we have to build a + # separate xcframework without visionOS + puts 'Creating Carthage release zip' + Dir.mktmpdir do |tmp| + tmp = File.realpath tmp + FileUtils.mkdir_p "#{tmp}/#{XCODE_VERSION}" + Dir.chdir("#{tmp}/#{XCODE_VERSION}") do + for platform in platforms('14') + puts "unziping #{ROOT}/realm-#{platform}-#{OBJC_XCODE_VERSION}.zip into #{tmp}" + sh 'unzip', "#{ROOT}/realm-#{platform}-#{OBJC_XCODE_VERSION}.zip" + end + end + + puts "Creating xcframework in #{tmp}" + create_xcframework tmp, XCODE_VERSION, 'Release', 'RealmSwift' + create_xcframework tmp, XCODE_VERSION, 'Release', 'Realm' + + Dir.chdir "#{tmp}/#{XCODE_VERSION}/Release" do + puts "Zipping Carthage.xcframework.zip" + zip 'Carthage.xcframework.zip', 'Realm.xcframework', 'RealmSwift.xcframework' + end end end end +if ARGV[0] == 'create-version-xcframeworks' + create_version_xcframeworks +end + diff --git a/scripts/github_prepare.rb b/scripts/github_prepare.rb new file mode 100755 index 00000000000..f5ec6b01b55 --- /dev/null +++ b/scripts/github_prepare.rb @@ -0,0 +1,128 @@ +#!/usr/bin/env ruby + +require 'octokit' +require 'getoptlong' +require 'uri' +require 'open-uri' +require 'fileutils' + +require_relative "release-matrix" + +include RELEASE + +ACCESS_TOKEN = ENV['GITHUB_ACCESS_TOKEN'] +raise 'GITHUB_ACCESS_TOKEN must be set to create GitHub releases' unless ACCESS_TOKEN + +REPOSITORY = 'realm/realm-swift' + +def update_package_asset(path, name, content_type) + github = Octokit::Client.new + github.access_token = ENV['GITHUB_ACCESS_TOKEN'] + + response_get_releases = github.releases(REPOSITORY) + draft_release = response_get_releases.select {|release| release[:name] == 'Artifacts release' && release[:draft] == true } + puts "Draft release founded #{draft_release}" + + puts "Uploading asset #{name} for path: #{path}" + release_url = draft_release[0][:url] + puts "Release Url #{release_url}" + response_upload_asset = github.upload_asset(release_url, path, { :name => "#{name}", :content_type => content_type }) + puts response_upload_asset +end + +def upload_product(name, path) + puts "Uploading #{name} from #{path}" + update_package_asset(path, name, 'application/zip') +end + +def download_artifact(name, path) + github = Octokit::Client.new + github.access_token = ENV['GITHUB_ACCESS_TOKEN'] + + response_get_release = github.releases(REPOSITORY) + draft_release = response_get_release.select {|release| release[:name] == 'Artifacts release' && release[:draft] == true } + puts "Draft release founded #{draft_release}" + + release_url = draft_release[0][:url] + puts "Release Url #{release_url}" + response_current_assets = github.release_assets(release_url) + + puts "Find asset #{name}" + asset = response_current_assets.find{ |asset| asset[:name] == name } + + puts "Downloading asset #{asset[:url]}" + download(asset[:url], path) +end + +def download(url, path) + open(path, 'wb') do |file| + uri = URI.parse(url) + io = uri.open("Authorization" => "Bearer #{ENV['GITHUB_ACCESS_TOKEN']}", + "Accept" => "application/octet-stream", + ) + puts "Writing from temp #{io.path} to #{path}" + case io + when StringIO then + File.open(path, 'w') { |f| f.write(io.read) } + when Tempfile then + io.close; FileUtils.mv(io.path, path) + end + end +end + +opts = GetoptLong.new( + [ '--help', '-h', GetoptLong::NO_ARGUMENT ], + [ '--path', GetoptLong::REQUIRED_ARGUMENT ], + [ '--upload-product', GetoptLong::REQUIRED_ARGUMENT ], + [ '--download-asset', GetoptLong::REQUIRED_ARGUMENT ] +) + +option = '' +name = '' +path = '' + +opts.each do |opt, arg| + if opt != '--path' + option = opt + end + case opt + when '--help' + puts <<-EOF +hello [OPTION] ... + +-h, --help: + show help + +--upload-product + Upload docs to the draft release + + EOF + exit + when '--path' + if arg == '' + raise "Path is required to execute this" + else + path = arg + end + when '--upload-product', '--download-asset' + if arg == '' + raise "Name is required to execute this" + else + name = arg + end + end +end + +if option == '--upload-product' + if name == '' || path == '' + raise 'Missing product name or path.' + else + upload_product(name, path) + end +elsif option == '--download-asset' + if name == '' || path == '' + raise 'Missing product name or path.' + else + download_artifact(name, path) + end +end diff --git a/scripts/github_release.rb b/scripts/github_release.rb index 8888f8e4a77..0f06e73f58d 100755 --- a/scripts/github_release.rb +++ b/scripts/github_release.rb @@ -3,9 +3,7 @@ require 'pathname' require 'octokit' -raise 'usage: github_release.rb version' unless ARGV.length == 1 - -VERSION = ARGV[0] +VERSION = ARGV[1] ACCESS_TOKEN = ENV['GITHUB_ACCESS_TOKEN'] raise 'GITHUB_ACCESS_TOKEN must be set to create GitHub releases' unless ACCESS_TOKEN @@ -29,17 +27,53 @@ def release_notes(version) relevant.join.strip end -RELEASE_NOTES = release_notes(VERSION) +def create_draft_release + name = 'Artifacts release' + github = Octokit::Client.new + github.access_token = ENV['GITHUB_ACCESS_TOKEN'] + + puts 'Search for draft releases' # Previously created by a merge to master + releases = github.releases(REPOSITORY) + draft_releases = releases.select { |release| release[:name] == name && release[:draft] == true } + + draft_releases.each { |draf_release| + puts 'Deleting draft release' # Previously created by a merge to master + response = github.delete_release(draf_release[:url]) + } + + puts 'Creating GitHub draft release' + response = github.create_release(REPOSITORY, RELEASE, name: name, body: "Artifacts release", draft: true) + + puts "Succesfully created draft release #{response[:url]}" +end + +def create_release + release_notes = release_notes(VERSION) + github = Octokit::Client.new + github.access_token = ENV['GITHUB_ACCESS_TOKEN'] -github = Octokit::Client.new -github.access_token = ENV['GITHUB_ACCESS_TOKEN'] + puts 'Creating GitHub release' + prerelease = (VERSION =~ /alpha|beta|rc|preview/) ? true : false + response = github.create_release(REPOSITORY, RELEASE, name: RELEASE, body: release_notes, prerelease: false) + release_url = response[:url] -puts 'Creating GitHub release' -prerelease = (VERSION =~ /alpha|beta|rc|preview/) ? true : false -response = github.create_release(REPOSITORY, RELEASE, name: RELEASE, body: RELEASE_NOTES, prerelease: prerelease) -release_url = response[:url] + Dir.glob 'release_pkg/*.zip' do |upload| + puts "Uploading #{upload} to GitHub" + github.upload_asset(release_url, upload, content_type: 'application/zip') + end +end + +def package_release_notes + release_notes = release_notes(VERSION) + FileUtils.mkdir_p("ExtractedChangelog") + out_file = File.new("ExtractedChangelog/CHANGELOG.md", "w") + out_file.puts(release_notes) +end -Dir.glob 'build/*.zip' do |upload| - puts "Uploading #{upload} to GitHub" - github.upload_asset(release_url, upload, content_type: 'application/zip') +if ARGV[0] == 'create-draft-release' + create_draft_release +elsif ARGV[0] == 'create-release' + create_release +elsif ARGV[0] == 'package-release-notes' + package_release_notes end diff --git a/scripts/release-matrix.rb b/scripts/release-matrix.rb new file mode 100755 index 00000000000..6e664979369 --- /dev/null +++ b/scripts/release-matrix.rb @@ -0,0 +1,55 @@ +#!/usr/bin/env ruby + +module RELEASE + DOCS_XCODE_VERSION = '14.3.1' + XCODE_VERSIONS = ['14.1', '14.2', '14.3.1', '15.0.1', '15.1'] + PLATFORMS_NAMES = ['osx': 'macOS', 'ios': 'iOS', 'watchos': 'watchOS', 'tvos': 'tvOS', 'catalyst': 'Catalyst', 'visionos': 'visionOS'] + + all = ->(v) { true } + latest_only = ->(v) { v == XCODE_VERSIONS.last } + doc_version = ->(v) { v == DOCS_XCODE_VERSION } + + PLATFORMS = { + 'osx' => all, + 'ios' => all, + 'watchos' => all, + 'tvos' => all, + 'catalyst' => all, + 'visionos' => latest_only, + } + + RELEASE_XCODE_CLOUD_TARGETS = { + 'package-docs' => doc_version, + 'package' => all, + 'test-package-examples' => latest_only, + 'test-ios-static' => latest_only, + 'test-osx' => latest_only, + } +end + +def get_doc_version + puts "#{RELEASE::DOCS_XCODE_VERSION}" +end + +def plaforms_for_version(version) + platforms_version = [] + RELEASE::PLATFORMS.each { |platform, filter| + if filter.call(version) + platforms_version.append(platform) + end + } + puts "#{platforms_version.join(",")}" +end + +def get_xcode_versions + puts "#{RELEASE::XCODE_VERSIONS.join(",")}" +end + +if ARGV[0] == 'docs_version' + get_doc_version +elsif ARGV[0] == 'plaforms_for_version' + version = ARGV[1] + plaforms_for_version(version) +elsif ARGV[0] == 'xcode_versions' + get_xcode_versions +end diff --git a/scripts/xcode_cloud_helper.rb b/scripts/xcode_cloud_helper.rb index d4ab5af23c7..673e18c92be 100755 --- a/scripts/xcode_cloud_helper.rb +++ b/scripts/xcode_cloud_helper.rb @@ -5,10 +5,12 @@ require 'json' require "base64" require "jwt" -require_relative "pr-ci-matrix" require 'getoptlong' +require_relative "pr-ci-matrix" +require_relative "release-matrix" include WORKFLOWS +include RELEASE JWT_BEARER = '' TEAM_ID = '' @@ -29,9 +31,11 @@ def usage() Usage: ruby #{__FILE__} --create-workflow [name] --xcode-version [xcode_version] --token [token] Usage: ruby #{__FILE__} --delete-workflow [workflow_id] --token [token] Usage: ruby #{__FILE__} --build-workflow [workflow_id] --token [token] - Usage: ruby #{__FILE__} --update-workflows --token [token] + Usage: ruby #{__FILE__} --create-new-workflows --token [token] --team-id [team_id] + Usage: ruby #{__FILE__} --create-relase-new-workflow --token [token] --team-id [team_id] Usage: ruby #{__FILE__} --clear-unused-workflows --token [token] Usage: ruby #{__FILE__} --get-token --issuer-id [issuer_id] --key-id [key_id] --pk_path [pk_path] + Usage: ruby #{__FILE__} --run-release-workflow [name] --token [token] environment variables: END @@ -40,6 +44,11 @@ def usage() APP_STORE_URL="https://api.appstoreconnect.apple.com/v1" +def sh(*args) + puts "executing: #{args.join(' ')}" if false + system(*args, false ? {} : {:out => '/dev/null'}) || exit(1) +end + def get_jwt_bearer(issuer_id, key_id, pk_path) private_key = OpenSSL::PKey.read(File.read(pk_path)) info = { @@ -81,6 +90,34 @@ def get_products return list_products end +def get_build_actions(build_run) + response = get("/ciBuildRuns/#{build_run}/actions") + result = JSON.parse(response.body) + list_actions = [] + result.collect do |doc| + doc[1].each { |action| + if action.class == Hash + list_actions.append({ "id" => action["id"] }) + end + } + end + return list_actions +end + +def get_artifacts(build_action) + response = get("/ciBuildActions/#{build_action}/artifacts") + result = JSON.parse(response.body) + list_artifacts = [] + result.collect do |doc| + doc[1].each { |artifact| + if artifact.class == Hash + list_artifacts.append({ "id" => artifact["id"] }) + end + } + end + return list_artifacts +end + def get_repositories response = get("/scmRepositories") result = JSON.parse(response.body) @@ -134,6 +171,16 @@ def get_workflow_info(id) return response.body end +def get_build_action_info(id) + response = get("/ciBuildActions/#{id}") + return response.body +end + +def get_artifact_info(id) + response = get("/ciArtifacts/#{id}") + return response.body +end + def get(path) url = "#{APP_STORE_URL}#{path}" uri = URI.parse(url) @@ -151,7 +198,7 @@ def get(path) end end -def create_workflow(name, xcode_version) +def create_workflow(name, xcode_version, prefix = "") url = "#{APP_STORE_URL}/ciWorkflows" uri = URI.parse(url) http = Net::HTTP.new(uri.host, uri.port) @@ -159,7 +206,7 @@ def create_workflow(name, xcode_version) request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{JWT_BEARER}" request["Content-type"] = "application/json" - body = create_workflow_request(name, xcode_version) + body = create_workflow_request(name, xcode_version, prefix) request.body = body.to_json response = http.request(request) if response.code == "201" @@ -174,7 +221,7 @@ def create_workflow(name, xcode_version) end end -def create_workflow_request(name, xcode_version) +def create_workflow_request(name, xcode_version, prefix = "") build_action = get_action_for_target(name) pull_request_start_condition = { @@ -184,7 +231,7 @@ def create_workflow_request(name, xcode_version) } attributes = { - "name" => "#{name}_#{xcode_version}", + "name" => "#{prefix}#{name}_#{xcode_version}", "description" => 'Create by Github Action Update XCode Cloud Workflows', "isLockedForEditing" => false, "containerFilePath" => "Realm.xcodeproj", @@ -193,6 +240,7 @@ def create_workflow_request(name, xcode_version) "pullRequestStartCondition" => pull_request_start_condition, "actions" => build_action } + xcode_version_id = get_xcode_id(xcode_version) mac_os_id = get_macos_latest_release(xcode_version_id) relationships = @@ -212,7 +260,7 @@ def create_workflow_request(name, xcode_version) return body end -def update_workflow(id) +def update_workflow(id, data) url = "#{APP_STORE_URL}/ciWorkflows/#{id}" uri = URI.parse(url) http = Net::HTTP.new(uri.host, uri.port) @@ -220,16 +268,10 @@ def update_workflow(id) request = Net::HTTP::Patch.new(uri) request["Authorization"] = "Bearer #{JWT_BEARER}" request["Content-type"] = "application/json" - data = - { - "type" => "ciWorkflows", - "attributes" => { "isEnabled" => true }, - "id" => id - } body = { "data" => data } request.body = body.to_json response = http.request(request) - if response.code == "201" + if response.code == "200" result = JSON.parse(response.body) id = result["data"]["id"] puts "Worfklow updated #{id}" @@ -266,7 +308,7 @@ def start_build(id) data = { "type" => "ciBuildRuns", - "attributes" => {}, + "attributes" => { "clean" => true }, "relationships" => { "workflow" => { "data" => { "type" => "ciWorkflows", "id" => id }}} } body = { "data" => data } @@ -274,10 +316,11 @@ def start_build(id) response = http.request(request) if response.code == "201" result = JSON.parse(response.body) - id = result["data"]["id"] + build_id = result["data"]["id"] puts "Workflow build started with id: #{id}:" + puts "Running build https://appstoreconnect.apple.com/teams/#{TEAM_ID}/frameworks/#{get_realm_product_id}/builds/#{build_id}/" puts response.body - return id + return build_id else raise "Error: #{response.code} #{response.body}" end @@ -349,6 +392,49 @@ def create_new_workflows end end +def create_new_release_workflows + if !ENV.include?('CI') + print 'Are you sure you want to create this workflows?, this will create declared local workflows that may not currently working in other PRs [Y/N]\n' + user_input = STDIN.gets.chomp.downcase + else + user_input = 'y' + end + + if user_input == "y" + current_workflows = get_workflows() + .filter { |workflow| + workflow["attributes"]["name"].split('_').first == 'release' + } + .map { |workflow| + target = workflow["attributes"]["name"].split('_') + name = target[1] + version = target.last + { 'target' => name, 'version' => version } + } + + workflows_to_create = [] + RELEASE::RELEASE_XCODE_CLOUD_TARGETS.each { |name, filter| + RELEASE::XCODE_VERSIONS.each { |version| + if filter.call(version) + workflow = { "target" => name, "version" => version } + unless current_workflows.include? workflow + workflows_to_create.append(workflow) + end + end + } + } + + workflows_to_create.each { |workflow| + name = workflow['target'] + version = workflow['version'] + puts "Creating new workflow for target: #{name} and version #{version} for release" + workflow_id = create_workflow(name, version, 'release') + } + else + puts "No" + end +end + def delete_unused_workflows if !ENV.include?('CI') print "Are you sure you want to clear unused workflow?, this will delete not-declared local workflows that may be currently working in other PRs [Y/N]\n" @@ -369,7 +455,7 @@ def delete_unused_workflows remote_workflows = get_workflows remote_workflows.each.map { |workflow| - if workflow["attributes"]["name"].include? "Cocoa-prepare" + if workflow["attributes"]["name"].include? "release" return nil end @@ -384,6 +470,41 @@ def delete_unused_workflows end end +def delete_unused_release_workflows + if !ENV.include?('CI') + print "Are you sure you want to clear unused workflow?, this will delete not-declared local workflows that may be currently working in other PRs [Y/N]\n" + user_input = STDIN.gets.chomp.downcase + else + user_input = 'y' + end + + if user_input == "y" + local_workflows = ["release_package-docs_#{RELEASE::DOCS_XCODE_VERSION}"] + RELEASE::RELEASE_XCODE_CLOUD_TARGETS.each { |name, filter| + RELEASE::XCODE_VERSIONS.each { |version| + if filter.call(version) + local_workflows.append("release_#{name}_#{version}") + end + } + } + + remote_workflows = get_workflows + .filter { |workflow| + workflow["attributes"]["name"].split('_').first == 'release' + } + .map { |workflow| + name = workflow["attributes"]["name"] + unless local_workflows.include? name + puts "Deleting unused release workflow #{workflow["id"]} #{name}" + delete_workflow(workflow["id"]) + end + } + + else + puts "No" + end +end + def get_action_for_target(name) workflow_id = get_workflow_id_for_name(name) if workflow_id.nil? @@ -493,8 +614,9 @@ def get_xcode_id(version) list_xcodeversion = get_xcode_versions $xcode_list = list_xcodeversion end + list_xcodeversion.each do |xcode| - if xcode["name"] == "Xcode #{version}" + if xcode["name"].include? "#{version}" return xcode["id"] end end @@ -550,13 +672,78 @@ def get_workflow_id_for_name(name) $workflows_list = workflows end workflows.each do |workflow| - if workflow["attributes"]["name"].split('_')[0] == name + if workflow["attributes"]["name"] == name return workflow["id"] end end return nil end +def read_build_info(build_id) + response = get("/ciBuildRuns/#{build_id}") + return response.body +end + +def run_release_workflow(name) + puts "Running workflow #{name}" + workflow_id = get_workflow_id_for_name(name) + build_id = start_build(workflow_id) +end + +def check_status_and_wait(build_run) + begin + build_state = read_build_info(build_run) + result = JSON.parse(build_state) + status = result["data"]["attributes"]["executionProgress"] + puts "Current status #{status}" + puts 'Waiting' + if status == 'COMPLETE' + completed = true + end + end until completed == true or not sleep 20 + + build_state = read_build_info(build_run) + result = JSON.parse(build_state) + completion_status = result["data"]["attributes"]["completionStatus"] + #if completion_status != 'SUCCEEDED' + # puts "Completion status #{completion_status}" + # raise "Error running build" + #end + get_logs_for_build(build_run) + return +end + +def get_logs_for_build(build_run) + actions = get_build_actions(build_run) + artifacts = get_artifacts(actions[0]["id"]) + artifact_info = get_artifact_info(artifacts[0]["id"]) + result = JSON.parse(artifact_info) + artifact_url = result["data"]["attributes"]["downloadUrl"] + print_artifact_logs(artifact_url) +end + +def check_workflow_execution_status(build_id) + build_state = read_build_info(build_id) + result = JSON.parse(build_state) + status = result["data"]["attributes"]["executionProgress"] + return status +end + +def create_workflow_and_run(name, xcode_version, prefix) + workflow_id = create_workflow(name, xcode_version, prefix) + build_run = start_build(workflow_id) + check_status_and_wait(build_run) +end + +def print_artifact_logs(url) + sh 'curl', '--output', 'logs.zip', "#{url}" + sh 'unzip', "logs.zip" + file_name = Dir["RealmSwift*/ci_post_clone.log"] + text = File.readlines("#{file_name[0]}").map do |line| + puts line + end +end + opts = GetoptLong.new( [ '--help', '-h', GetoptLong::NO_ARGUMENT ], [ '--token', '-t', GetoptLong::REQUIRED_ARGUMENT ], @@ -565,6 +752,7 @@ def get_workflow_id_for_name(name) [ '--issuer-id', GetoptLong::REQUIRED_ARGUMENT ], [ '--key-id', GetoptLong::REQUIRED_ARGUMENT ], [ '--pk-path', GetoptLong::REQUIRED_ARGUMENT ], + [ '--prefix', GetoptLong::REQUIRED_ARGUMENT ], [ '--list-workflows', GetoptLong::NO_ARGUMENT ], [ '--list-products', GetoptLong::NO_ARGUMENT ], [ '--list-repositories', GetoptLong::NO_ARGUMENT ], @@ -576,17 +764,25 @@ def get_workflow_id_for_name(name) [ '--delete-workflow', GetoptLong::REQUIRED_ARGUMENT ], [ '--build-workflow', GetoptLong::REQUIRED_ARGUMENT ], [ '--create-new-workflows', GetoptLong::NO_ARGUMENT ], + [ '--create-new-release-workflows', GetoptLong::NO_ARGUMENT ], [ '--clear-unused-workflows', GetoptLong::NO_ARGUMENT ], + [ '--clear-unused-release-workflows', GetoptLong::NO_ARGUMENT ], + [ '--run-release-workflow', GetoptLong::REQUIRED_ARGUMENT ], + [ '--check-workflow-status', GetoptLong::REQUIRED_ARGUMENT ], + [ '--create-release-workflow-and-run', GetoptLong::REQUIRED_ARGUMENT ], [ '--get-token', GetoptLong::NO_ARGUMENT ] ) option = '' name = '' workflow_id = '' +build_id = '' xcode_version = '' issuer_id = '' key_id = '' pk_path = '' +release_workflow_name = '' +prefix = '' opts.each do |opt, arg| if opt != '--token' && opt != '--xcode-version' && opt != '--issuer-id' && opt != '--key-id' && opt != '--pk-path' && opt != '--team-id' @@ -618,6 +814,9 @@ def get_workflow_id_for_name(name) --pk-path [pk_path]: Apple Connect API path to private key file. +--prefix [prefix]: + Prefix name for a new workflow. + --list-workflows: Returns a list of current workflows for the RealmSwift product. @@ -651,10 +850,25 @@ def get_workflow_id_for_name(name) --create-new-workflows: Adds the missing workflows corresponding to the list of targets and xcode versions in `pr-ci-matrix.rb`. - + +--create-new-release-workflows + Create new workflows for the release pipeline. + --clear-unused-workflows: Clear all unused workflows which are not in the list of targets and xcode versions in `pr-ci-matrix.rb`. +--clear-unused-release-workflows + Clear all unused workflows for the release pipeline + +--run-release-workflow + Runs a release workflow + +--check-workflow-status + Check workflow status + +--create-release-workflow-and-run + Creates a workflow, runs it and wait for it to finish + --get-token: Get Apple Connect Store API Token for local use. @@ -682,11 +896,15 @@ def get_workflow_id_for_name(name) if arg != '' pk_path = arg end + when '--prefix' + if arg != '' + prefix = arg + end when '--info-workflow' if arg != '' workflow_id = arg end - when '--create-workflow' + when '--create-workflow', '--create-release-workflow-and-run' if arg != '' name = arg end @@ -698,6 +916,14 @@ def get_workflow_id_for_name(name) if arg != '' xcode_version = arg end + when '--run-release-workflow' + if arg != '' + release_workflow_name = arg + end + when '--check-workflow-status' + if arg != '' + build_id = arg + end end end @@ -725,7 +951,7 @@ def get_workflow_id_for_name(name) if name == '' || xcode_version == '' raise 'Needs name and xcode version' else - create_workflow(name, xcode_version) + create_workflow(name, xcode_version, prefix) end elsif option == '--update-workflow' if workflow_id == '' @@ -751,12 +977,38 @@ def get_workflow_id_for_name(name) else create_new_workflows end +elsif option == '--create-new-release-workflows' + if TEAM_ID == '' + raise 'Needs team id' + else + create_new_release_workflows + end elsif option == '--clear-unused-workflows' delete_unused_workflows +elsif option == '--clear-unused-release-workflows' + delete_unused_release_workflows elsif option == '--get-token' if issuer_id == '' || key_id == '' || pk_path == '' raise 'Needs issuer id, key id or pk id.' else get_jwt_bearer(issuer_id, key_id, pk_path) end +elsif option == '--run-release-workflow' + if release_workflow_name == '' + raise 'Needs workflow name to run.' + else + run_release_workflow(release_workflow_name) + end +elsif option == '--check-workflow-status' + if build_id == '' + raise 'Needs build id name to run.' + else + check_workflow_execution_status(build_id) + end +elsif option == '--create-release-workflow-and-run' + if name == '' || xcode_version == '' + raise 'Needs name and xcode version' + else + create_workflow_and_run(name, xcode_version, "release_") + end end