Files
Outpost/scripts/package-dmg.sh
T
Puranjay Savar Mattas 3e7d8097ad fix(scripts): correctly detect an annotated tag at HEAD in changelog range
git rev-parse on an annotated tag (git tag -a, what the release process
uses) returns the tag object's hash, not the commit hash it points to
- so the HEAD == tag comparison never matched, and the script always
fell into the "not tagged yet" branch. Fixed by peeling the tag down
to its commit with ^{commit}. Verified against a real annotated tag:
now correctly shows "Changelog (<tag>)" with actual content instead
of an empty "since <tag>" section.

Also: untrack Outpost.xcodeproj/xcuserdata (already gitignored, but
was committed before that rule existed, so Xcode kept dirtying it and
blocking pulls - including the one that just happened) and ignore
scripts/package-dmg.sh's dist/ output.
2026-08-14 03:44:47 +01:00

148 lines
5.3 KiB
Bash
Executable File

#!/bin/bash
# Packages an already-exported Outpost.app into a distributable DMG with an
# Applications-folder symlink for drag-to-install, then prints a Markdown
# changelog to the terminal for pasting into the Gitea release notes —
# nothing is written to the repo, this is generated fresh from git log
# every time.
#
# Usage: scripts/package-dmg.sh /path/to/Outpost.app [output-dir]
#
# Produces: <output-dir>/Outpost-<version>.dmg
# Version/build number are read directly from the app's own Info.plist, so
# this always matches whatever MARKETING_VERSION was set to at archive time.
set -euo pipefail
APP_PATH="${1:?Usage: package-dmg.sh /path/to/Outpost.app [output-dir]}"
OUT_DIR="${2:-$(pwd)/dist}"
if [ ! -d "$APP_PATH" ]; then
echo "error: '$APP_PATH' not found or not a directory" >&2
exit 1
fi
APP_NAME="$(basename "$APP_PATH" .app)"
VERSION="$(defaults read "$APP_PATH/Contents/Info" CFBundleShortVersionString)"
BUILD="$(defaults read "$APP_PATH/Contents/Info" CFBundleVersion)"
mkdir -p "$OUT_DIR"
STAGING_DIR="$(mktemp -d)"
trap 'rm -rf "$STAGING_DIR"' EXIT
echo "Staging '$APP_NAME.app' ($VERSION build $BUILD)..."
cp -R "$APP_PATH" "$STAGING_DIR/"
ln -s /Applications "$STAGING_DIR/Applications"
# Not notarized (no paid Apple Developer Program membership) — Gatekeeper
# blocks it on a downloader's first launch. This is the standard bypass,
# shipped in the DMG itself instead of repeated in every release's notes.
cat > "$STAGING_DIR/Read Me First.txt" <<'EOF'
Outpost isn't notarized by Apple (that requires a paid Apple Developer
Program membership), so macOS Gatekeeper will block it on first launch
with "cannot be opened because the developer cannot be verified."
To open it anyway:
1. Drag Outpost.app to Applications.
2. Right-click (or Control-click) Outpost.app and choose "Open".
3. Click "Open" in the dialog that appears.
You only need to do this once — after that it launches normally.
If macOS instead shows "Outpost is damaged and can't be opened," that's
the same Gatekeeper quarantine flag, just a different message on newer
macOS versions. Fix it from Terminal:
xattr -cr /Applications/Outpost.app
EOF
DMG_NAME="${APP_NAME}-${VERSION}.dmg"
DMG_PATH="$OUT_DIR/$DMG_NAME"
rm -f "$DMG_PATH"
echo "Building $DMG_NAME..."
hdiutil create \
-volname "$APP_NAME $VERSION" \
-srcfolder "$STAGING_DIR" \
-ov \
-format UDZO \
"$DMG_PATH"
echo "Done: $DMG_PATH"
# --- Changelog -------------------------------------------------------------
# Groups commits by conventional-commit type (feat/fix/.../other) between the
# previous tag and whatever's being released. Two workflows are supported:
# - already tagged (HEAD == latest tag): diff against the tag before it.
# - not tagged yet (HEAD ahead of latest tag): diff since that tag.
# Falls back to the full log if there are no tags at all yet.
print_section() {
title="$1"
content="$2"
if [ -n "$content" ]; then
printf '### %s\n\n%s\n' "$title" "$content"
fi
}
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -z "$REPO_ROOT" ]; then
echo "" >&2
echo "warning: not inside a git repository, skipping changelog" >&2
exit 0
fi
LATEST_TAG="$(git -C "$REPO_ROOT" describe --tags --abbrev=0 2>/dev/null || true)"
if [ -z "$LATEST_TAG" ]; then
RANGE="HEAD"
HEADING="Changelog"
else
HEAD_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)"
# `^{commit}` peels an annotated tag down to the commit it points at —
# without it, `rev-parse` on an annotated tag (e.g. `git tag -a`) returns
# the tag *object's* hash, which never equals a commit hash, so this
# comparison would always take the "not tagged yet" branch below even
# when HEAD genuinely is the tagged commit.
TAG_COMMIT="$(git -C "$REPO_ROOT" rev-parse "${LATEST_TAG}^{commit}")"
if [ "$HEAD_COMMIT" = "$TAG_COMMIT" ]; then
PREV_TAG="$(git -C "$REPO_ROOT" describe --tags --abbrev=0 "${LATEST_TAG}^" 2>/dev/null || true)"
RANGE="${PREV_TAG:+$PREV_TAG..}$LATEST_TAG"
HEADING="Changelog ($LATEST_TAG)"
else
RANGE="$LATEST_TAG..HEAD"
HEADING="Changelog (since $LATEST_TAG)"
fi
fi
FEAT="" ; FIX="" ; PERF="" ; REFACTOR="" ; DOCS="" ; TESTS="" ; CHORE="" ; OTHER=""
while IFS='|' read -r hash subject; do
[ -z "${hash:-}" ] && continue
case "$subject" in
feat*) FEAT="${FEAT}- ${subject#*: } (${hash})"$'\n' ;;
fix*) FIX="${FIX}- ${subject#*: } (${hash})"$'\n' ;;
perf*) PERF="${PERF}- ${subject#*: } (${hash})"$'\n' ;;
refactor*) REFACTOR="${REFACTOR}- ${subject#*: } (${hash})"$'\n' ;;
docs*) DOCS="${DOCS}- ${subject#*: } (${hash})"$'\n' ;;
test*) TESTS="${TESTS}- ${subject#*: } (${hash})"$'\n' ;;
chore*) CHORE="${CHORE}- ${subject#*: } (${hash})"$'\n' ;;
*) OTHER="${OTHER}- ${subject} (${hash})"$'\n' ;;
esac
done <<EOF
$(git -C "$REPO_ROOT" log "$RANGE" --no-merges --pretty=format:'%h|%s' 2>/dev/null)
EOF
echo ""
echo "---"
echo ""
echo "## $HEADING"
echo ""
print_section "Features" "$FEAT"
print_section "Fixes" "$FIX"
print_section "Performance" "$PERF"
print_section "Refactors" "$REFACTOR"
print_section "Documentation" "$DOCS"
print_section "Tests" "$TESTS"
print_section "Chores" "$CHORE"
print_section "Other" "$OTHER"