From d9bedc35b93259cdf6651a73ee7a2aeb4bf7a721 Mon Sep 17 00:00:00 2001 From: Wesley Keetch Date: Fri, 25 Sep 2026 17:52:53 -0400 Subject: [PATCH] chore(ci): move quality checks to Xcode Cloud Add ci_scripts/ci_post_clone.sh so Xcode Cloud runs the commit-message policy. Pull request builds check every commit between the target and source commits, fetching history when the clone is shallow. Branch builds check only the built commit because Xcode Cloud does not expose the previous branch head. Remove the GitHub Actions workflow. Build and test now run as Xcode Cloud workflow actions for the RHOIDS and RHOIDSWatchTests schemes. --- .github/workflows/ios-quality.yml | 108 ------------------------------ CONTRIBUTING.md | 2 +- ci_scripts/ci_post_clone.sh | 73 ++++++++++++++++++++ 3 files changed, 74 insertions(+), 109 deletions(-) delete mode 100644 .github/workflows/ios-quality.yml create mode 100755 ci_scripts/ci_post_clone.sh diff --git a/.github/workflows/ios-quality.yml b/.github/workflows/ios-quality.yml deleted file mode 100644 index 65ef748..0000000 --- a/.github/workflows/ios-quality.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: iOS Quality Gate - -on: - pull_request: - push: - branches: - - main - - AppStore - -jobs: - commit-messages: - name: Commit Message Policy - runs-on: ubuntu-latest - - steps: - - name: Check out full history - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Validate introduced commit messages - env: - EVENT_NAME: ${{ github.event_name }} - BEFORE_SHA: ${{ github.event.before }} - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.sha }} - run: | - set -euo pipefail - - if [ "$EVENT_NAME" = "pull_request" ]; then - range="$BASE_SHA..$HEAD_SHA" - elif [ -n "$BEFORE_SHA" ] && [ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then - range="$BEFORE_SHA..$HEAD_SHA" - else - range="$HEAD_SHA^..$HEAD_SHA" - fi - - while IFS= read -r commit; do - message_file="$RUNNER_TEMP/commit-message-$commit.txt" - git show -s --format=%B "$commit" > "$message_file" - echo "Checking $commit" - python3 scripts/check_commit_message.py --file "$message_file" - done < <(git rev-list --reverse "$range") - - build-and-test: - name: Build and Test - runs-on: macos-latest - timeout-minutes: 45 - - steps: - - name: Check out - uses: actions/checkout@v5 - - - name: Show toolchain - run: | - sw_vers - xcodebuild -version - xcrun simctl list runtimes - - - name: Resolve compatible simulator destinations - id: destinations - run: | - set -euo pipefail - - IOS_DEVICE_ID="$(xcodebuild -showdestinations -project RHOIDS.xcodeproj -scheme RHOIDS | awk -F'id:' '/platform:iOS Simulator/ && /name:iPhone/ && !found { split($2, fields, ","); gsub(/^[[:space:]]+|[[:space:]]+$/, "", fields[1]); print fields[1]; found = 1 }')" - if [ -z "${IOS_DEVICE_ID}" ]; then - echo "The RHOIDS scheme has no compatible iOS Simulator destination." >&2 - exit 1 - fi - - WATCH_DEVICE_ID="$(xcodebuild -showdestinations -project RHOIDS.xcodeproj -scheme RHOIDSWatchTests | awk -F'id:' '/platform:watchOS Simulator/ && /name:Apple Watch/ && !found { split($2, fields, ","); gsub(/^[[:space:]]+|[[:space:]]+$/, "", fields[1]); print fields[1]; found = 1 }')" - if [ -z "${WATCH_DEVICE_ID}" ]; then - echo "The RHOIDSWatchTests scheme has no compatible watchOS Simulator destination." >&2 - exit 1 - fi - - echo "ios_device_id=${IOS_DEVICE_ID}" >> "${GITHUB_OUTPUT}" - echo "watch_device_id=${WATCH_DEVICE_ID}" >> "${GITHUB_OUTPUT}" - - - name: List project - run: xcodebuild -list -project RHOIDS.xcodeproj - - - name: Build iOS app - run: | - xcodebuild build \ - -project RHOIDS.xcodeproj \ - -scheme RHOIDS \ - -configuration Debug \ - -destination "id=${{ steps.destinations.outputs.ios_device_id }}" \ - -derivedDataPath "$RUNNER_TEMP/rhoids-derived-data" - - - name: Test iOS app - run: | - xcodebuild test \ - -project RHOIDS.xcodeproj \ - -scheme RHOIDS \ - -destination "id=${{ steps.destinations.outputs.ios_device_id }}" \ - -derivedDataPath "$RUNNER_TEMP/rhoids-derived-data" \ - -resultBundlePath "$RUNNER_TEMP/rhoids-ios-tests.xcresult" - - - name: Test watch app - run: | - xcodebuild test \ - -project RHOIDS.xcodeproj \ - -scheme RHOIDSWatchTests \ - -destination "id=${{ steps.destinations.outputs.watch_device_id }}" \ - -derivedDataPath "$RUNNER_TEMP/rhoids-watch-derived-data" \ - -resultBundlePath "$RUNNER_TEMP/rhoids-watch-tests.xcresult" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf0303d..03e81d2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,4 +56,4 @@ By contributing, you agree that your contribution is licensed under the reposito Commit messages must be professional, concise, and free of emoji, pictographs, decorative Unicode symbols, and emoji presentation characters. This applies to the subject and body, including commits created by automated tools. Conventional Commit prefixes are permitted but optional. Do not include AI attribution or generated-by trailers unless a human contributor explicitly requests them. -The tracked `commit-msg` hook enforces this locally after `scripts/install_git_hooks.sh` is run. CI applies the same validator to every commit introduced by a pull request or protected-branch push. +The tracked `commit-msg` hook enforces this locally after `scripts/install_git_hooks.sh` is run. Xcode Cloud applies the same validator through `ci_scripts/ci_post_clone.sh`: pull request builds check every commit the pull request introduces, and branch builds check the commit being built. diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh new file mode 100755 index 0000000..48b67a7 --- /dev/null +++ b/ci_scripts/ci_post_clone.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# Xcode Cloud post-clone step: apply the repository commit-message policy. +# +# Pull request builds check every commit in the pull request +# (CI_PULL_REQUEST_TARGET_COMMIT..CI_PULL_REQUEST_SOURCE_COMMIT). +# Branch and tag builds check only CI_COMMIT, because Xcode Cloud does not +# expose the previous head of the branch. Commits pushed directly to a branch +# in a batch are therefore only fully covered when they arrive through a +# pull request. +set -eu + +repo_root="${CI_PRIMARY_REPOSITORY_PATH:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}" +cd "$repo_root" + +remote="$(git remote | head -n 1)" + +fail() { + printf '%s\n' "ci_post_clone: $*" >&2 + exit 1 +} + +has_commit() { + git cat-file -e "$1^{commit}" 2>/dev/null +} + +ensure_full_history() { + if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then + [ -n "$remote" ] || fail "shallow clone with no remote to fetch history from" + echo "Fetching full history from $remote" + git fetch --quiet --unshallow "$remote" || fail "could not unshallow the clone" + fi +} + +ensure_commit() { + if ! has_commit "$1"; then + [ -n "$remote" ] || fail "commit $1 is missing and no remote is configured" + echo "Fetching missing commit $1 from $remote" + git fetch --quiet "$remote" "$1" || true + fi + has_commit "$1" || fail "commit $1 is not available in the clone" +} + +check_commit() { + message_file="${TMPDIR:-/tmp}/rhoids-commit-message-$1.txt" + git show -s --format=%B "$1" > "$message_file" + echo "Checking $1" + python3 scripts/check_commit_message.py --file "$message_file" + rm -f "$message_file" +} + +pr_target="${CI_PULL_REQUEST_TARGET_COMMIT:-}" +pr_source="${CI_PULL_REQUEST_SOURCE_COMMIT:-}" + +if [ -n "$pr_target" ] && [ -n "$pr_source" ]; then + ensure_full_history + ensure_commit "$pr_target" + ensure_commit "$pr_source" + base="$(git merge-base "$pr_target" "$pr_source")" || fail "no merge base between $pr_target and $pr_source" + commits="$(git rev-list --reverse "$base..$pr_source")" + if [ -z "$commits" ]; then + echo "No commits introduced by this pull request." + fi + for commit in $commits; do + check_commit "$commit" + done +elif [ -n "${CI_COMMIT:-}" ]; then + ensure_commit "$CI_COMMIT" + check_commit "$CI_COMMIT" +else + fail "neither pull request commits nor CI_COMMIT are set" +fi + +echo "Commit message policy passed."