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
29 changes: 29 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,35 @@ jobs:
- name: Checkout
uses: actions/checkout@v7

- name: Validate release version
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
MARKETING_VERSIONS="$(
xcodebuild \
-project ComputerSolitaire.xcodeproj \
-scheme ComputerSolitaire \
-configuration Release \
-destination "generic/platform=macOS" \
-showBuildSettings |
awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3 }' |
sort -u
)"
VERSION_COUNT="$(printf '%s\n' "$MARKETING_VERSIONS" | awk 'NF { count++ } END { print count + 0 }')"

if [[ "$VERSION_COUNT" -ne 1 ]]; then
echo "Expected exactly one MARKETING_VERSION, but found $VERSION_COUNT."
printf '%s\n' "$MARKETING_VERSIONS"
exit 1
fi

EXPECTED_TAG="v$MARKETING_VERSIONS"
if [[ "$RELEASE_TAG" != "$EXPECTED_TAG" ]]; then
echo "Release tag $RELEASE_TAG does not match MARKETING_VERSION $MARKETING_VERSIONS."
echo "Expected tag: $EXPECTED_TAG"
exit 1
fi

- name: Import Apple signing certificate
env:
P12_BASE64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
Expand Down
168 changes: 168 additions & 0 deletions scripts/tag-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/bin/bash

set -euo pipefail

usage() {
cat <<'EOF'
Usage: ./scripts/tag-version.sh [--dry-run]

Create and push the release tag that matches the app's MARKETING_VERSION.

Options:
--dry-run Run every preflight check without creating or pushing the tag.
EOF
}

dry_run=false

case "${1:-}" in
"") ;;
--dry-run) dry_run=true ;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 2
;;
esac

if [[ $# -gt 1 ]]; then
usage >&2
exit 2
fi

for command in git xcodebuild; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Required command not found: $command" >&2
exit 1
fi
done

script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repository_root="$(git -C "$script_directory" rev-parse --show-toplevel 2>/dev/null)" || {
echo "The release script must be located inside the Computer Solitaire repository." >&2
exit 1
}
cd "$repository_root"

if [[ -n "$(git status --porcelain)" ]]; then
echo "The working tree must be clean before creating a release tag." >&2
git status --short >&2
exit 1
fi

if ! git remote get-url origin >/dev/null 2>&1; then
echo "The repository does not have an origin remote." >&2
exit 1
fi

echo "Fetching origin/main and release tags..."
git fetch origin refs/heads/main:refs/remotes/origin/main --tags

head_commit="$(git rev-parse HEAD)"
main_commit="$(git rev-parse refs/remotes/origin/main)"

if [[ "$head_commit" != "$main_commit" ]]; then
echo "HEAD must match origin/main before creating a release tag." >&2
echo "HEAD: $head_commit" >&2
echo "origin/main: $main_commit" >&2
exit 1
fi

versions="$({
xcodebuild \
-project ComputerSolitaire.xcodeproj \
-scheme ComputerSolitaire \
-configuration Release \
-destination "generic/platform=macOS" \
-showBuildSettings
} | awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3 }' | sort -u)"

version_count="$(printf '%s\n' "$versions" | awk 'NF { count++ } END { print count + 0 }')"
if [[ "$version_count" -ne 1 ]]; then
echo "Expected exactly one MARKETING_VERSION, but found $version_count." >&2
if [[ -n "$versions" ]]; then
printf '%s\n' "$versions" >&2
fi
exit 1
fi

version="$versions"
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "MARKETING_VERSION must use numeric major.minor.patch format: $version" >&2
exit 1
fi

tag="v$version"

if git show-ref --verify --quiet "refs/tags/$tag"; then
echo "Tag already exists: $tag" >&2
exit 1
fi

if ! remote_tag="$(git ls-remote --tags origin "refs/tags/$tag")"; then
echo "Unable to check whether $tag already exists on origin." >&2
exit 1
fi

if [[ -n "$remote_tag" ]]; then
echo "Tag already exists on origin: $tag" >&2
exit 1
fi

short_commit="$(git rev-parse --short HEAD)"
echo
echo "Release version: $version"
echo "Tag: $tag"
echo "Commit: $short_commit"

if [[ "$dry_run" == true ]]; then
echo
echo "Dry run complete. No tag was created or pushed."
exit 0
fi

echo
if ! read -r -p "Create and push $tag to origin? [y/N] " response; then
echo >&2
echo "Release cancelled." >&2
exit 1
fi

case "$response" in
y|Y|yes|YES|Yes) ;;
*)
echo "Release cancelled."
exit 0
;;
esac

git tag "$tag"

if ! git push origin "refs/tags/$tag"; then
echo "Tag push failed; removing the newly created local tag." >&2
git tag -d "$tag" >/dev/null
exit 1
fi

origin_url="$(git remote get-url origin)"
case "$origin_url" in
https://github.com/*)
repository_url="${origin_url%.git}"
;;
git@github.com:*)
repository_url="https://github.com/${origin_url#git@github.com:}"
repository_url="${repository_url%.git}"
;;
*)
repository_url=""
;;
esac

echo
echo "Pushed $tag. The GitHub release workflow has started."
if [[ -n "$repository_url" ]]; then
echo "$repository_url/actions/workflows/release.yml"
fi