From 1864e1a3e17003a595034e067b33200d358018c7 Mon Sep 17 00:00:00 2001 From: Gale W Date: Sun, 7 Jun 2026 17:14:36 -0400 Subject: [PATCH 1/2] docs: sync xcode guidance validation --- AGENTS.md | 2 +- scripts/repo-maintenance/validations/20-agents-guidance.sh | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 37ec748..63c4d5b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Apple / Xcode Project Workflow -- Use `xcode-build-run-workflow` for normal Xcode build, run, diagnostics, preview, file-membership, and guarded mutation work inside this existing project. +- Use `xcode-app-project-workflow` for normal Xcode workspace inspection, diagnostics, builds, runs, previews, file-membership checks, and guarded mutation work inside this existing project. - Use `xcode-testing-workflow` when the task is primarily about Swift Testing, XCTest, XCUITest, `.xctestplan`, flaky tests, retries, or test diagnosis. - Use `apple-ui-accessibility-workflow` when the task is primarily about SwiftUI accessibility semantics, Apple UI accessibility review, accessibility tree shaping, or UIKit/AppKit accessibility bridge behavior. - Use `sync-xcode-project-guidance` when the repo guidance for this project drifts and needs to be refreshed or merged forward. diff --git a/scripts/repo-maintenance/validations/20-agents-guidance.sh b/scripts/repo-maintenance/validations/20-agents-guidance.sh index 2f775a7..1c38cf9 100755 --- a/scripts/repo-maintenance/validations/20-agents-guidance.sh +++ b/scripts/repo-maintenance/validations/20-agents-guidance.sh @@ -15,6 +15,9 @@ agents_path="$REPO_ROOT/AGENTS.md" [ -s "$agents_path" ] || die "Expected $agents_path to be non-empty." for needle in \ + "xcode-app-project-workflow" \ + "sync-xcode-project-guidance" \ + "Never edit \`.pbxproj\` files directly." \ "scripts/repo-maintenance/validate-all.sh" \ "scripts/repo-maintenance/sync-shared.sh" \ "scripts/repo-maintenance/release.sh" From 32f827fca462d30a293cb7f0fbc6bc21266d7ba2 Mon Sep 17 00:00:00 2001 From: Gale W Date: Sun, 7 Jun 2026 19:39:03 -0400 Subject: [PATCH 2/2] docs: sync maintenance guidance --- .../workflows/validate-repo-maintenance.yml | 9 +- AGENTS.md | 4 +- scripts/repo-maintenance/config/release.env | 15 +- scripts/repo-maintenance/lib/common.sh | 142 +++++++++++ scripts/repo-maintenance/release.sh | 225 +++++++++++------- .../release/30-push-release.sh | 2 + .../release/40-github-release.sh | 10 +- .../validations/20-agents-guidance.sh | 3 - 8 files changed, 315 insertions(+), 95 deletions(-) diff --git a/.github/workflows/validate-repo-maintenance.yml b/.github/workflows/validate-repo-maintenance.yml index 771d91d..5e05590 100644 --- a/.github/workflows/validate-repo-maintenance.yml +++ b/.github/workflows/validate-repo-maintenance.yml @@ -12,9 +12,14 @@ on: jobs: validate: name: validate - runs-on: macos-latest + runs-on: macos-26 steps: - - uses: actions/checkout@v4 + # This is a validated floor, not a ceiling; update to newer stable official versions when validated. + - uses: actions/checkout@v6.0.2 + - name: Report selected Xcode + run: xcode-select --print-path + - name: Report Swift toolchain + run: xcrun swift --version - name: Install Swift repo-maintenance tools run: brew install swiftformat swiftlint - name: Run repo-maintenance validation diff --git a/AGENTS.md b/AGENTS.md index 63c4d5b..7b0ccd6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Apple / Xcode Project Workflow -- Use `xcode-app-project-workflow` for normal Xcode workspace inspection, diagnostics, builds, runs, previews, file-membership checks, and guarded mutation work inside this existing project. +- Use `xcode-build-run-workflow` for normal Xcode build, run, diagnostics, preview, file-membership, and guarded mutation work inside this existing project. - Use `xcode-testing-workflow` when the task is primarily about Swift Testing, XCTest, XCUITest, `.xctestplan`, flaky tests, retries, or test diagnosis. - Use `apple-ui-accessibility-workflow` when the task is primarily about SwiftUI accessibility semantics, Apple UI accessibility review, accessibility tree shaping, or UIKit/AppKit accessibility bridge behavior. - Use `sync-xcode-project-guidance` when the repo guidance for this project drifts and needs to be refreshed or merged forward. @@ -27,5 +27,5 @@ - Keep data flow straight and dependency direction unidirectional. - Treat the `.xcworkspace` or `.xcodeproj` as the source of truth for app integration, schemes, and build settings. - Prefer Xcode-aware tooling or `xcodebuild` over ad hoc filesystem assumptions when project structure or target membership is involved. -- Never edit `.pbxproj` files directly. If a project-file change is needed and no safe project-aware tool is available, stop and make that change through Xcode instead. +- Never edit `.pbxproj` files directly. If a project-file change is needed and no safe project-aware tool is available, stop and make that change through Xcode instead. When `.pbxproj` is tracked and Xcode, XcodeGen, or another project-aware workflow legitimately changes it, treat that diff as critical project state: review it, stage it, and commit it with the branch before any push, merge, release, or cleanup. - Validate Xcode-project changes with explicit `xcodebuild` commands when build or test integrity matters. diff --git a/scripts/repo-maintenance/config/release.env b/scripts/repo-maintenance/config/release.env index ebe4e10..5cb1e1c 100644 --- a/scripts/repo-maintenance/config/release.env +++ b/scripts/repo-maintenance/config/release.env @@ -1,5 +1,16 @@ # Repo-maintenance release defaults. REPO_MAINTENANCE_DEFAULT_RELEASE_MODE=standard REPO_MAINTENANCE_RELEASE_BRANCH=main -REPO_MAINTENANCE_SKIP_VERSION_BUMP=true -REPO_MAINTENANCE_PACKAGE_LOCAL_DMG=true +REPO_MAINTENANCE_REMOTE_CI_MODE=full + +# GitHub can accept branch, tag, PR, check, review, and release mutations before +# those surfaces are immediately readable. These defaults keep release scripts +# explicit about intentional waits instead of failing on transient indexing gaps. +REPO_MAINTENANCE_GH_WAIT_TIMEOUT_SECONDS=120 +REPO_MAINTENANCE_GH_WAIT_POLL_SECONDS=5 + +# Keep full local validation as the default release gate. For repositories whose +# GitHub CI is intentionally heavy, use --remote-ci-mode defer so release.sh +# pauses after branch push, PR creation, and initial check discovery. Codex can +# then use a native thread Timer/Wakeup or heartbeat automation to resume later +# instead of leaving a long-running shell process open just to poll GitHub. diff --git a/scripts/repo-maintenance/lib/common.sh b/scripts/repo-maintenance/lib/common.sh index 1e0405b..b0afa95 100755 --- a/scripts/repo-maintenance/lib/common.sh +++ b/scripts/repo-maintenance/lib/common.sh @@ -38,6 +38,148 @@ load_profile_env() { load_env_file "$REPO_MAINTENANCE_ROOT/config/profile.env" } +positive_integer_or_default() { + value="$1" + default_value="$2" + + case "$value" in + ''|*[!0-9]*) + printf '%s\n' "$default_value" + ;; + 0) + printf '%s\n' "$default_value" + ;; + *) + printf '%s\n' "$value" + ;; + esac +} + +is_semver_prerelease_tag() { + tag_name="$1" + case "$tag_name" in + v[0-9]*.[0-9]*.[0-9]*-*) + return 0 + ;; + *) + return 1 + ;; + esac +} + +expected_github_prerelease_value() { + tag_name="$1" + if is_semver_prerelease_tag "$tag_name"; then + printf '%s\n' "true" + else + printf '%s\n' "false" + fi +} + +github_release_create_prerelease_flag() { + tag_name="$1" + if is_semver_prerelease_tag "$tag_name"; then + printf '%s\n' "--prerelease" + fi +} + +verify_github_release_prerelease_metadata() { + tag_name="$1" + expected_value="$(expected_github_prerelease_value "$tag_name")" + + actual_value="$(gh release view "$tag_name" --json isPrerelease --jq .isPrerelease 2>/dev/null || true)" + case "$actual_value" in + true|false) + ;; + *) + die "GitHub release $tag_name exists, but its prerelease metadata was not readable. Confirm gh can read release JSON metadata before rerunning release.sh." + ;; + esac + + [ "$actual_value" = "$expected_value" ] || die "GitHub release $tag_name prerelease metadata mismatch: tag implies isPrerelease=$expected_value but GitHub reports isPrerelease=$actual_value. Update the release metadata or delete and recreate the release before rerunning release.sh." +} + +github_wait_timeout() { + value="$1" + default_timeout="$(positive_integer_or_default "${REPO_MAINTENANCE_GH_WAIT_TIMEOUT_SECONDS:-120}" 120)" + positive_integer_or_default "$value" "$default_timeout" +} + +github_wait_poll_seconds() { + value="$1" + default_poll_seconds="$(positive_integer_or_default "${REPO_MAINTENANCE_GH_WAIT_POLL_SECONDS:-5}" 5)" + positive_integer_or_default "$value" "$default_poll_seconds" +} + +wait_for_remote_branch() { + branch_name="$1" + timeout_seconds="$(github_wait_timeout "${REPO_MAINTENANCE_REMOTE_BRANCH_TIMEOUT_SECONDS:-}")" + poll_seconds="$(github_wait_poll_seconds "${REPO_MAINTENANCE_REMOTE_BRANCH_POLL_SECONDS:-}")" + elapsed_seconds="0" + + log "Waiting up to ${timeout_seconds}s for remote branch origin/$branch_name to become visible." + + while :; do + if git -C "$REPO_ROOT" ls-remote --exit-code --heads origin "$branch_name" >/dev/null 2>&1; then + log "Remote branch origin/$branch_name is visible." + return 0 + fi + + if [ "$elapsed_seconds" -ge "$timeout_seconds" ]; then + die "Remote branch origin/$branch_name was not visible after ${timeout_seconds}s. Confirm the branch push succeeded and that the origin remote is reachable before rerunning release.sh." + fi + + sleep "$poll_seconds" + elapsed_seconds=$((elapsed_seconds + poll_seconds)) + done +} + +wait_for_remote_tag() { + tag_name="$1" + timeout_seconds="$(github_wait_timeout "${REPO_MAINTENANCE_REMOTE_TAG_TIMEOUT_SECONDS:-}")" + poll_seconds="$(github_wait_poll_seconds "${REPO_MAINTENANCE_REMOTE_TAG_POLL_SECONDS:-}")" + elapsed_seconds="0" + + log "Waiting up to ${timeout_seconds}s for remote tag $tag_name to become visible." + + while :; do + if git -C "$REPO_ROOT" ls-remote --exit-code --tags origin "refs/tags/$tag_name" >/dev/null 2>&1; then + log "Remote tag $tag_name is visible." + return 0 + fi + + if [ "$elapsed_seconds" -ge "$timeout_seconds" ]; then + die "Remote tag $tag_name was not visible after ${timeout_seconds}s. Confirm the tag push succeeded and that GitHub has indexed the tag before rerunning release.sh." + fi + + sleep "$poll_seconds" + elapsed_seconds=$((elapsed_seconds + poll_seconds)) + done +} + +wait_for_github_release() { + tag_name="$1" + timeout_seconds="$(github_wait_timeout "${REPO_MAINTENANCE_GH_RELEASE_TIMEOUT_SECONDS:-}")" + poll_seconds="$(github_wait_poll_seconds "${REPO_MAINTENANCE_GH_RELEASE_POLL_SECONDS:-}")" + elapsed_seconds="0" + + log "Waiting up to ${timeout_seconds}s for GitHub release $tag_name to become readable." + + while :; do + if gh release view "$tag_name" >/dev/null 2>&1; then + log "GitHub release $tag_name is readable." + return 0 + fi + + if [ "$elapsed_seconds" -ge "$timeout_seconds" ]; then + die "GitHub release $tag_name was not readable after ${timeout_seconds}s. Confirm release creation succeeded and GitHub has indexed the release before rerunning release.sh." + fi + + sleep "$poll_seconds" + elapsed_seconds=$((elapsed_seconds + poll_seconds)) + done +} + ensure_git_repo() { git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "maintain-project-repo must run inside a git worktree rooted at $REPO_ROOT." } diff --git a/scripts/repo-maintenance/release.sh b/scripts/repo-maintenance/release.sh index acfe4c7..7151e19 100755 --- a/scripts/repo-maintenance/release.sh +++ b/scripts/repo-maintenance/release.sh @@ -12,12 +12,12 @@ mode="${REPO_MAINTENANCE_DEFAULT_RELEASE_MODE:-standard}" release_tag="" skip_validate="false" skip_gh_release="false" -skip_version_bump="${REPO_MAINTENANCE_SKIP_VERSION_BUMP:-false}" -package_local_dmg="${REPO_MAINTENANCE_PACKAGE_LOCAL_DMG:-false}" +skip_version_bump="false" base_branch="${REPO_MAINTENANCE_RELEASE_BRANCH:-main}" review_comments_addressed="false" skip_branch_cleanup="false" dry_run="false" +remote_ci_mode="${REPO_MAINTENANCE_REMOTE_CI_MODE:-full}" while [ "$#" -gt 0 ]; do case "$1" in @@ -41,14 +41,6 @@ while [ "$#" -gt 0 ]; do skip_version_bump="true" shift ;; - --package-local-dmg) - package_local_dmg="true" - shift - ;; - --skip-local-dmg) - package_local_dmg="false" - shift - ;; --base-branch) base_branch="${2:-}" shift 2 @@ -57,6 +49,10 @@ while [ "$#" -gt 0 ]; do review_comments_addressed="true" shift ;; + --remote-ci-mode) + remote_ci_mode="${2:-}" + shift 2 + ;; --skip-branch-cleanup) skip_branch_cleanup="true" shift @@ -68,7 +64,7 @@ while [ "$#" -gt 0 ]; do -h|--help) cat <<'USAGE' Usage: - release.sh --mode standard --version [--base-branch main] [--skip-validate] [--skip-version-bump] [--skip-gh-release] [--package-local-dmg|--skip-local-dmg] [--review-comments-addressed] [--skip-branch-cleanup] [--dry-run] + release.sh --mode standard --version [--base-branch main] [--skip-validate] [--skip-version-bump] [--skip-gh-release] [--review-comments-addressed] [--remote-ci-mode full|defer] [--skip-branch-cleanup] [--dry-run] release.sh --mode submodule --version [--skip-validate] [--skip-gh-release] [--dry-run] USAGE exit 0 @@ -85,6 +81,7 @@ export REPO_MAINTENANCE_RELEASE_MODE="$mode" export RELEASE_TAG="$release_tag" export REPO_MAINTENANCE_SKIP_GH_RELEASE="$skip_gh_release" export REPO_MAINTENANCE_DRY_RUN="$dry_run" +export REPO_MAINTENANCE_REMOTE_CI_MODE="$remote_ci_mode" ensure_clean_worktree() { status_output="$(git -C "$REPO_ROOT" status --porcelain)" @@ -105,6 +102,16 @@ ensure_semver_tag() { esac } +ensure_remote_ci_mode() { + case "$REPO_MAINTENANCE_REMOTE_CI_MODE" in + full|defer) + ;; + *) + die "Remote CI mode must be either full or defer. Use full to watch GitHub checks in this script, or defer to pause after initial check discovery and continue from a Codex wakeup." + ;; + esac +} + current_branch() { git -C "$REPO_ROOT" symbolic-ref --quiet --short HEAD || true } @@ -119,12 +126,18 @@ ensure_branch_release_context() { run_version_bump() { release_version="${RELEASE_TAG#v}" version_bump_script="$SELF_DIR/version-bump.sh" + head_subject="$(git -C "$REPO_ROOT" log -1 --format=%s 2>/dev/null || true)" if [ "$skip_version_bump" = "true" ]; then log "Skipping repo version bump because --skip-version-bump was requested." return 0 fi + if [ "$head_subject" = "release: bump versions for $RELEASE_TAG" ]; then + log "Version bump commit for $RELEASE_TAG is already at HEAD; continuing the release resume path." + return 0 + fi + [ -x "$version_bump_script" ] || die "Standard release mode expected an executable repo-specific version bump hook at $version_bump_script. Add that hook so the repo's version surfaces move together, or rerun with --skip-version-bump when this release intentionally has no version-bearing files." if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then @@ -145,10 +158,11 @@ run_version_bump() { create_release_tag() { head_sha="$(git -C "$REPO_ROOT" rev-parse HEAD)" - tag_sha="$(git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$RELEASE_TAG^{}" 2>/dev/null || true)" + tag_sha="$(git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$RELEASE_TAG" 2>/dev/null || true)" if [ -n "$tag_sha" ]; then - [ "$tag_sha" = "$head_sha" ] || die "Tag $RELEASE_TAG already exists and does not point at HEAD." + tag_commit_sha="$(git -C "$REPO_ROOT" rev-list -n 1 "$RELEASE_TAG")" + [ "$tag_commit_sha" = "$head_sha" ] || die "Tag $RELEASE_TAG already exists and does not point at HEAD." log "Tag $RELEASE_TAG already points at HEAD." return 0 fi @@ -162,17 +176,28 @@ create_release_tag() { log "Created annotated tag $RELEASE_TAG." } -push_branch_and_tag() { +push_release_branch() { branch_name="$1" if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then - log "Would push branch $branch_name and tag $RELEASE_TAG to origin." + log "Would push branch $branch_name to origin." return 0 fi git -C "$REPO_ROOT" push -u origin "$branch_name" + log "Pushed branch $branch_name." + wait_for_remote_branch "$branch_name" +} + +push_release_tag() { + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would push tag $RELEASE_TAG to origin." + return 0 + fi + git -C "$REPO_ROOT" push origin "$RELEASE_TAG" - log "Pushed branch $branch_name and tag $RELEASE_TAG." + log "Pushed tag $RELEASE_TAG." + wait_for_remote_tag "$RELEASE_TAG" } create_or_update_pr() { @@ -193,11 +218,11 @@ create_or_update_pr() { - prepares $RELEASE_TAG from branch \`$branch_name\` - keeps protected \`$base_branch\` updates behind pull request review and CI -- release tag \`$RELEASE_TAG\` was created locally before this PR so the reviewed release candidate is preserved exactly +- release tag \`$RELEASE_TAG\` will be created after CI and the review-comment gate pass, so failed or still-discussed release candidates do not get tagged ## Review Loop -Before merge, \`scripts/repo-maintenance/release.sh\` watches CI and stops on review comments unless the maintainer has already addressed or resolved them and reruns with \`--review-comments-addressed\`. +Before merge and tagging, \`scripts/repo-maintenance/release.sh\` watches CI and stops on review comments unless the maintainer has already addressed or resolved them and reruns with \`--review-comments-addressed\`. EOF pr_number="$(gh pr list --head "$branch_name" --base "$base_branch" --json number --jq '.[0].number // empty' --limit 1)" @@ -220,8 +245,6 @@ EOF watch_ci() { pr_number="$1" - attempts=0 - max_attempts=12 if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then log "Would watch CI for PR #$pr_number." @@ -229,27 +252,89 @@ watch_ci() { fi log "Watching CI for PR #$pr_number." + if ! gh pr checks "$pr_number" --watch; then + die "CI is not green for PR #$pr_number. Fix the failing checks, push the branch, and rerun release.sh so it can watch CI again." + fi + log "CI is green for PR #$pr_number." +} + +defer_remote_ci_if_requested() { + pr_number="$1" + branch_name="$2" + + [ "$REPO_MAINTENANCE_REMOTE_CI_MODE" = "defer" ] || return 1 + + if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then + log "Would defer remote CI after PR #$pr_number reports initial checks." + return 0 + fi + + pr_url="$(gh pr view "$pr_number" --json url --jq '.url')" + log "Remote CI mode is defer, so release.sh is pausing after local validation, branch push, PR creation, and initial check discovery." + log "Release is not complete yet. Let GitHub finish CI for PR #$pr_number, then continue from branch $branch_name with:" + log " bash scripts/repo-maintenance/release.sh --mode standard --version $RELEASE_TAG" + log "Codex should use a native thread Timer/Wakeup or heartbeat automation for this wait when available, then resume by checking $pr_url and rerunning the command above instead of leaving a shell script open to poll GitHub." + return 0 +} + +wait_for_initial_pr_checks() { + pr_number="$1" + timeout_seconds="$(github_wait_timeout "${REPO_MAINTENANCE_INITIAL_CHECK_TIMEOUT_SECONDS:-}")" + poll_seconds="$(github_wait_poll_seconds "${REPO_MAINTENANCE_INITIAL_CHECK_POLL_SECONDS:-}")" + elapsed_seconds="0" + last_state="no check data returned yet" + + log "Waiting up to ${timeout_seconds}s for GitHub to report initial checks on PR #$pr_number." + while :; do - checks_output="$(gh pr checks "$pr_number" --watch 2>&1)" && { - log "CI is green for PR #$pr_number." + last_state="$(gh pr checks "$pr_number" --json name,state,workflow --jq 'map(.name + ":" + .state) | join(", ")' 2>/dev/null || printf 'no checks reported')" + check_count="$(gh pr checks "$pr_number" --json name,state,workflow --jq 'length' 2>/dev/null || printf '0')" + case "$check_count" in + ''|*[!0-9]*) + check_count="0" + ;; + esac + + if [ "$check_count" -gt 0 ]; then + log "Found $check_count initial check(s) for PR #$pr_number." return 0 - } - - case "$checks_output" in - *"no checks reported"*) - attempts=$((attempts + 1)) - [ "$attempts" -lt "$max_attempts" ] || { - printf '%s\n' "$checks_output" >&2 - die "CI did not report checks for PR #$pr_number after waiting. Rerun release.sh once GitHub has created the check run." - } - log "GitHub has not reported checks for PR #$pr_number yet; waiting before retrying." - sleep 5 + fi + + if [ "$elapsed_seconds" -ge "$timeout_seconds" ]; then + die "No checks were reported for PR #$pr_number after ${timeout_seconds}s. Last observed state: $last_state. Confirm the GitHub Actions workflow triggers for the release branch, Actions is enabled, and the branch push succeeded before rerunning release.sh." + fi + + sleep "$poll_seconds" + elapsed_seconds=$((elapsed_seconds + poll_seconds)) + done +} + +wait_for_pr_review_state() { + pr_number="$1" + timeout_seconds="$(github_wait_timeout "${REPO_MAINTENANCE_PR_REVIEW_TIMEOUT_SECONDS:-}")" + poll_seconds="$(github_wait_poll_seconds "${REPO_MAINTENANCE_PR_REVIEW_POLL_SECONDS:-}")" + elapsed_seconds="0" + last_state="PR review/comment state has not been read yet" + + log "Waiting up to ${timeout_seconds}s for GitHub review/comment state on PR #$pr_number." + + while :; do + last_state="$(gh pr view "$pr_number" --json reviewDecision,comments,reviews --jq '"reviewDecision=" + (.reviewDecision // "") + ", comments=" + ((.comments | length) | tostring) + ", reviews=" + ((.reviews | length) | tostring)' 2>/dev/null || printf 'GitHub did not return PR review/comment state')" + case "$last_state" in + "GitHub did not return PR review/comment state") ;; *) - printf '%s\n' "$checks_output" >&2 - die "CI is not green for PR #$pr_number. Fix the failing checks, push the branch, and rerun release.sh so it can watch CI again." + log "GitHub review/comment state is readable for PR #$pr_number: $last_state." + return 0 ;; esac + + if [ "$elapsed_seconds" -ge "$timeout_seconds" ]; then + die "GitHub review/comment state for PR #$pr_number was not readable after ${timeout_seconds}s. Last observed state: $last_state. Confirm the PR exists and GitHub is returning review data before rerunning release.sh." + fi + + sleep "$poll_seconds" + elapsed_seconds=$((elapsed_seconds + poll_seconds)) done } @@ -261,8 +346,10 @@ check_pr_comments() { return 0 fi + wait_for_pr_review_state "$pr_number" + review_decision="$(gh pr view "$pr_number" --json reviewDecision --jq '.reviewDecision // ""')" - comment_count="$(gh pr view "$pr_number" --json comments,reviews --jq '([.comments[]?, .reviews[]?] | length)')" + comment_count="$(gh pr view "$pr_number" --json comments,reviews --jq '([.comments[]?, (.reviews[]? | select(.state == "COMMENTED"))] | length)')" if [ "$review_decision" = "CHANGES_REQUESTED" ]; then gh pr view "$pr_number" --comments @@ -300,7 +387,7 @@ fast_forward_base_branch() { git -C "$REPO_ROOT" pull --ff-only origin "$base_branch" log "Fast-forwarded local $base_branch." else - warn "Could not check out local $base_branch, likely because another worktree owns it. Fast-forward $base_branch from origin/$base_branch in that checkout before cleanup." + die "Could not check out local $base_branch, likely because another worktree owns it. Fast-forward $base_branch from origin/$base_branch in that checkout, then rerun release.sh so the release tag is created from the reviewed base branch." fi } @@ -311,58 +398,23 @@ create_github_release() { fi if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then - log "Would create a GitHub release for $RELEASE_TAG with gh release create --verify-tag." + prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" + log "Would create a GitHub release for $RELEASE_TAG with gh release create --verify-tag${prerelease_flag:+ $prerelease_flag}." return 0 fi if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + verify_github_release_prerelease_metadata "$RELEASE_TAG" log "GitHub release $RELEASE_TAG already exists." return 0 fi - gh release create "$RELEASE_TAG" --verify-tag --generate-notes + prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" + # shellcheck disable=SC2086 + gh release create "$RELEASE_TAG" --verify-tag --generate-notes $prerelease_flag log "Created GitHub release $RELEASE_TAG." -} - -package_local_release_dmg() { - if [ "$package_local_dmg" != "true" ]; then - return 0 - fi - - if [ "$REPO_MAINTENANCE_SKIP_GH_RELEASE" = "true" ]; then - die "--package-local-dmg needs a GitHub release object so it can upload the notarized DMG. Remove --skip-gh-release or package manually with scripts/package-notarized-dmg.sh." - fi - - if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then - log "Would package, notarize, staple, and verify a local DMG for $RELEASE_TAG from the tagged release candidate." - return 0 - fi - - head_sha="$(git -C "$REPO_ROOT" rev-parse HEAD)" - tag_sha="$(git -C "$REPO_ROOT" rev-parse "$RELEASE_TAG^{}")" - [ "$head_sha" = "$tag_sha" ] || die "Refusing to package the notarized DMG because HEAD does not match $RELEASE_TAG. HEAD: $head_sha. Tag: $tag_sha." - - "$REPO_ROOT/scripts/package-notarized-dmg.sh" --version "$RELEASE_TAG" - log "Packaged notarized local DMG assets for $RELEASE_TAG." -} - -upload_local_release_dmg() { - if [ "$package_local_dmg" != "true" ]; then - return 0 - fi - - if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then - log "Would upload local DMG assets for $RELEASE_TAG to the GitHub release." - return 0 - fi - - dmg_path="$REPO_ROOT/build/distribution/Gmax-$RELEASE_TAG.dmg" - sha_path="$dmg_path.sha256" - [ -f "$dmg_path" ] || die "Expected notarized DMG at $dmg_path before uploading release assets." - [ -f "$sha_path" ] || die "Expected notarized DMG checksum at $sha_path before uploading release assets." - - gh release upload "$RELEASE_TAG" "$dmg_path" "$sha_path" --clobber - log "Uploaded notarized local DMG assets for $RELEASE_TAG." + wait_for_github_release "$RELEASE_TAG" + verify_github_release_prerelease_metadata "$RELEASE_TAG" } cleanup_merged_branches() { @@ -395,6 +447,7 @@ run_standard_release() { ensure_git_repo ensure_gh_cli ensure_semver_tag + ensure_remote_ci_mode branch_name="$(ensure_branch_release_context)" ensure_clean_worktree @@ -404,17 +457,21 @@ run_standard_release() { run_version_bump ensure_clean_worktree - create_release_tag - push_branch_and_tag "$branch_name" + push_release_branch "$branch_name" create_or_update_pr "$branch_name" pr_number="$PR_NUMBER" + wait_for_initial_pr_checks "$pr_number" + if defer_remote_ci_if_requested "$pr_number" "$branch_name"; then + log "Standard release flow paused before remote CI watch for $RELEASE_TAG." + return 0 + fi watch_ci "$pr_number" check_pr_comments "$pr_number" - package_local_release_dmg merge_pr "$pr_number" fast_forward_base_branch + create_release_tag + push_release_tag create_github_release - upload_local_release_dmg cleanup_merged_branches "$branch_name" log "Standard release flow completed successfully for $RELEASE_TAG." } diff --git a/scripts/repo-maintenance/release/30-push-release.sh b/scripts/repo-maintenance/release/30-push-release.sh index 81d22d0..54de388 100755 --- a/scripts/repo-maintenance/release/30-push-release.sh +++ b/scripts/repo-maintenance/release/30-push-release.sh @@ -13,5 +13,7 @@ if [ "${REPO_MAINTENANCE_DRY_RUN:-false}" = "true" ]; then fi git -C "$REPO_ROOT" push -u origin "$branch_name" +wait_for_remote_branch "$branch_name" git -C "$REPO_ROOT" push origin "$RELEASE_TAG" +wait_for_remote_tag "$RELEASE_TAG" log "Pushed branch $branch_name and tag $RELEASE_TAG." diff --git a/scripts/repo-maintenance/release/40-github-release.sh b/scripts/repo-maintenance/release/40-github-release.sh index 348cfcc..2220e09 100755 --- a/scripts/repo-maintenance/release/40-github-release.sh +++ b/scripts/repo-maintenance/release/40-github-release.sh @@ -16,14 +16,20 @@ if ! command -v gh >/dev/null 2>&1; then fi if [ "${REPO_MAINTENANCE_DRY_RUN:-false}" = "true" ]; then - log "Would create a GitHub release for $RELEASE_TAG with gh release create --verify-tag." + prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" + log "Would create a GitHub release for $RELEASE_TAG with gh release create --verify-tag${prerelease_flag:+ $prerelease_flag}." exit 0 fi if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + verify_github_release_prerelease_metadata "$RELEASE_TAG" log "GitHub release $RELEASE_TAG already exists." exit 0 fi -gh release create "$RELEASE_TAG" --verify-tag --generate-notes +prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" +# shellcheck disable=SC2086 +gh release create "$RELEASE_TAG" --verify-tag --generate-notes $prerelease_flag log "Created GitHub release $RELEASE_TAG." +wait_for_github_release "$RELEASE_TAG" +verify_github_release_prerelease_metadata "$RELEASE_TAG" diff --git a/scripts/repo-maintenance/validations/20-agents-guidance.sh b/scripts/repo-maintenance/validations/20-agents-guidance.sh index 1c38cf9..2f775a7 100755 --- a/scripts/repo-maintenance/validations/20-agents-guidance.sh +++ b/scripts/repo-maintenance/validations/20-agents-guidance.sh @@ -15,9 +15,6 @@ agents_path="$REPO_ROOT/AGENTS.md" [ -s "$agents_path" ] || die "Expected $agents_path to be non-empty." for needle in \ - "xcode-app-project-workflow" \ - "sync-xcode-project-guidance" \ - "Never edit \`.pbxproj\` files directly." \ "scripts/repo-maintenance/validate-all.sh" \ "scripts/repo-maintenance/sync-shared.sh" \ "scripts/repo-maintenance/release.sh"