diff --git a/.buildkite/cleanup-pr-build-branches.sh b/.buildkite/cleanup-pr-build-branches.sh new file mode 100755 index 00000000..13db49b6 --- /dev/null +++ b/.buildkite/cleanup-pr-build-branches.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +set -euo pipefail + +# Deletes `pr-build/` branches whose PR is closed (merged or rejected). +# Runs on trunk pushes so the just-merged PR's branch gets cleaned up +# immediately, and any orphans accumulated from prior failures get swept too. + +if [[ "${BUILDKITE_BRANCH:-}" != "trunk" ]]; then + echo "Not a trunk build (branch=${BUILDKITE_BRANCH:-unset}), skipping" + exit 0 +fi + +if [[ -z "${GITHUB_TOKEN:-}" ]]; then + echo "GITHUB_TOKEN not set, cannot query PR state" >&2 + exit 1 +fi + +GITHUB_REPO="wordpress-mobile/GutenbergKit" + +echo '--- :robot_face: Use bot for Git operations' +source use-bot-for-git + +echo "--- :mag: Listing pr-build/* branches on origin" +mapfile -t branches < <( + git ls-remote --heads origin 'refs/heads/pr-build/*' \ + | awk '{print $2}' \ + | sed 's|^refs/heads/||' +) + +echo "Found ${#branches[@]} pr-build branches" + +if [[ ${#branches[@]} -eq 0 ]]; then + exit 0 +fi + +echo "--- :github: Checking PR state for each branch" +to_delete=() +for branch in "${branches[@]}"; do + pr_number="${branch#pr-build/}" + if ! [[ "$pr_number" =~ ^[0-9]+$ ]]; then + echo "Skipping $branch (unexpected suffix)" + continue + fi + + response=$( + curl --silent --show-error \ + --write-out $'\n%{http_code}' \ + --header "Authorization: Bearer ${GITHUB_TOKEN}" \ + --header "Accept: application/vnd.github+json" \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${GITHUB_REPO}/pulls/${pr_number}" + ) + http_code=$(printf '%s' "$response" | tail -n1) + body=$(printf '%s' "$response" | sed '$d') + + if [[ "$http_code" != "200" ]]; then + echo "Skipping $branch (HTTP $http_code from GitHub)" + continue + fi + + state=$(printf '%s' "$body" | jq -r '.state') + + if [[ "$state" == "closed" ]]; then + echo "Marking $branch for deletion (PR #$pr_number is closed)" + to_delete+=("$branch") + else + echo "Keeping $branch (PR #$pr_number is $state)" + fi +done + +if [[ ${#to_delete[@]} -eq 0 ]]; then + echo "No closed PR branches to delete" + exit 0 +fi + +echo "--- :wastebasket: Deleting ${#to_delete[@]} stale branches" +chunk_size=50 +for ((i=0; i<${#to_delete[@]}; i+=chunk_size)); do + chunk=("${to_delete[@]:i:chunk_size}") + refspecs=() + for branch in "${chunk[@]}"; do + refspecs+=(":refs/heads/${branch}") + done + git push origin "${refspecs[@]}" +done diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index fac81e13..4ac39985 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -80,8 +80,8 @@ steps: - label: ':xcode: Build XCFramework' key: build-xcframework depends_on: - - build-react - - swift-test-library + - build-react + - swift-test-library command: | buildkite-agent artifact download dist.tar.gz . tar -xzf dist.tar.gz @@ -97,6 +97,7 @@ steps: - label: ':s3: Publish XCFramework to S3' depends_on: build-xcframework + if: build.pull_request.id == null command: | buildkite-agent artifact download '*.xcframework.zip' . buildkite-agent artifact download '*.xcframework.zip.checksum.txt' . @@ -107,6 +108,12 @@ steps: bundle exec fastlane publish_to_s3 version:${NEW_VERSION:-${BUILDKITE_TAG:-$BUILDKITE_COMMIT}} plugins: *plugins + - label: ':swift: :package: Publish PR XCFramework' + depends_on: build-xcframework + if: build.pull_request.id != null + command: .buildkite/publish-pr-xcframework.sh + plugins: *plugins + - label: ':ios: Test iOS E2E' depends_on: build-react command: | @@ -149,3 +156,8 @@ steps: - 'android/Gutenberg/build/outputs/androidTest-results/connected/**/*' - 'android/Gutenberg/build/outputs/buildkite-logs/**/*' - 'android/Gutenberg/build/outputs/connected_android_test_additional_output/**/*' + + - label: ':wastebasket: Clean up `pr-build/*` branches for closed PRs' + if: build.branch == "trunk" + command: .buildkite/cleanup-pr-build-branches.sh + plugins: *plugins diff --git a/.buildkite/publish-pr-xcframework.sh b/.buildkite/publish-pr-xcframework.sh new file mode 100755 index 00000000..a25b0354 --- /dev/null +++ b/.buildkite/publish-pr-xcframework.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -euo pipefail + +if [[ "${BUILDKITE_PULL_REQUEST:-false}" == "false" ]]; then + echo "Not a PR build, skipping PR XCFramework publish" + exit 0 +fi + +# Skip on fork PRs: bot credentials and S3 secrets aren't available, and we +# couldn't push the snapshot branch back to the canonical repo anyway. +if [[ -n "${BUILDKITE_PULL_REQUEST_REPO:-}" ]] \ + && [[ "$BUILDKITE_PULL_REQUEST_REPO" != *"wordpress-mobile/GutenbergKit"* ]]; then + echo "PR is from a fork (${BUILDKITE_PULL_REQUEST_REPO}), skipping XCFramework publish" + exit 0 +fi + +echo '--- :robot_face: Use bot for Git operations' +source use-bot-for-git + +echo '--- :arrow_down: Downloading XCFramework artifacts' +buildkite-agent artifact download '*.xcframework.zip' . --step "build-xcframework" +buildkite-agent artifact download '*.xcframework.zip.checksum.txt' . --step "build-xcframework" + +echo '--- :rubygems: Setting up Gems' +install_gems + +echo "--- :rocket: Publishing PR build for PR #${BUILDKITE_PULL_REQUEST}" +bundle exec fastlane publish_pr_xcframework diff --git a/fastlane/Fastfile b/fastlane/Fastfile index c4af8566..d5984c4e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,9 +1,14 @@ # frozen_string_literal: true +require 'fastlane/plugin/wpmreleasetoolkit' + PROJECT_ROOT = File.expand_path('..', __dir__) APPLE_TEAM_ID = 'PZYM8XX95Q' +GITHUB_REPO = 'wordpress-mobile/GutenbergKit' +XCFRAMEWORK_COMMENT_REUSE_ID = 'gutenbergkit-xcframework-build' + ASC_API_KEY_ENV_VARS = %w[ APP_STORE_CONNECT_API_KEY_KEY_ID APP_STORE_CONNECT_API_KEY_ISSUER_ID @@ -15,6 +20,12 @@ CODE_SIGNING_STORAGE_ENV_VARS = %w[ MATCH_S3_SECRET_ACCESS_KEY ].freeze +# Set up the release-toolkit env manager so lanes can read CI metadata +# (`pull_request_number`, `commit_hash`, etc.) via a single canonical helper. +# We don't ship a `.env` file; on CI the values come straight from the process +# environment, and off-CI the manager will warn (not fail) about the missing file. +Fastlane::Wpmreleasetoolkit::EnvManager.set_up(env_file_name: 'gutenbergkit.env') + before_all do setup_ci end @@ -39,6 +50,27 @@ lane :publish_to_s3 do |options| ) end +lane :publish_pr_xcframework do + env = Fastlane::Wpmreleasetoolkit::EnvManager.default! + pr_number = env.pull_request_number + UI.user_error!('publish_pr_xcframework must run on a PR build (BUILDKITE_PULL_REQUEST is unset or "false")') if pr_number.nil? + + branch_name = "pr-build/#{pr_number}" + version = "pr-builds/#{pr_number}" + + publish_to_s3(version: version) + push_xcframework_snapshot_branch(branch_name: branch_name, version: version, checksum: xcframework_checksum) + + body = xcframework_comment_body(branch_name: branch_name, commit_sha: env.commit_hash) + comment_on_pr( + project: GITHUB_REPO, + pr_number: pr_number, + body: body, + reuse_identifier: XCFRAMEWORK_COMMENT_REUSE_ID + ) + post_buildkite_annotation(body: body) +end + lane :xcframework_sign do sh( 'codesign', @@ -123,3 +155,45 @@ def get_required_env!(key) UI.user_error!("Environment variable `#{key}` is not set.") end + +def push_xcframework_snapshot_branch(branch_name:, version:, checksum:) + package_swift = File.join(PROJECT_ROOT, 'Package.swift') + rewrite_resources_mode!(package_swift, version: version, checksum: checksum) + + sh("git checkout -B #{branch_name}") + git_commit(path: package_swift, message: "Update Package.swift for #{version}") + sh("git push -f origin #{branch_name}") +end + +def rewrite_resources_mode!(package_swift, version:, checksum:) + prefix = 'let resourcesMode: DependencyMode =' + replacement = %(#{prefix} .release(version: "#{version}", checksum: "#{checksum}")\n) + + lines = File.readlines(package_swift) + matches = lines.count { |line| line.start_with?(prefix) } + UI.user_error!("Expected exactly one `#{prefix}` line in Package.swift, found #{matches}") unless matches == 1 + + rewritten = lines.map { |line| line.start_with?(prefix) ? replacement : line } + File.write(package_swift, rewritten.join) +end + +def xcframework_comment_body(branch_name:, commit_sha:) + short_sha = (commit_sha || 'unknown')[0, 8] + <<~MARKDOWN + ## XCFramework Build + + This PR's XCFramework is available for testing. Add the following to your `Package.swift`: + + ```swift + .package(url: "https://github.com/#{GITHUB_REPO}", branch: "#{branch_name}") + ``` + + Built from #{short_sha} + MARKDOWN +end + +def post_buildkite_annotation(body:) + return unless ENV['BUILDKITE_AGENT_ACCESS_TOKEN'] + + buildkite_annotate(context: 'xcframework', style: 'info', message: body) +end