Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/validate-repo-maintenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
15 changes: 13 additions & 2 deletions scripts/repo-maintenance/config/release.env
Original file line number Diff line number Diff line change
@@ -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.
142 changes: 142 additions & 0 deletions scripts/repo-maintenance/lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Expand Down
Loading
Loading