Skip to content

Commit 4daacac

Browse files
committed
build: use shared mac release tooling
1 parent 668f6be commit 4daacac

11 files changed

Lines changed: 47 additions & 425 deletions

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
## Project Structure & Modules
44
- `Sources/CodexBar`: Swift 6 menu bar app (usage/credits probes, icon renderer, settings). Keep changes small and reuse existing helpers.
55
- `Tests/CodexBarTests`: XCTest coverage for usage parsing, status probes, icon patterns; mirror new logic with focused tests.
6-
- `Scripts`: build/package helpers (`package_app.sh`, `sign-and-notarize.sh`, `make_appcast.sh`, `build_icon.sh`, `compile_and_run.sh`).
6+
- `Scripts`: build/package helpers (`package_app.sh`, `sign-and-notarize.sh`, `make_appcast.sh`, `build_icon.sh`, `compile_and_run.sh`). Release wrappers call `Scripts/mac-release`, which resolves `MAC_RELEASE_TOOL` or the shared `agent-scripts` checkout.
77
- `docs`: release notes and process (`docs/RELEASING.md`, screenshots). Root-level zips/appcast are generated artifacts—avoid editing except during releases.
88

99
## Build, Test, Run
1010
- Dev loop: `./Scripts/compile_and_run.sh` kills old instances, runs `swift build` + `swift test`, packages, relaunches `CodexBar.app`, and confirms it stays running.
1111
- Quick build/test: `swift build` (debug) or `swift build -c release`; `swift test` for the full XCTest suite.
1212
- Package locally: `./Scripts/package_app.sh` to refresh `CodexBar.app`, then restart with `pkill -x CodexBar || pkill -f CodexBar.app || true; cd /Users/steipete/Projects/codexbar && open -n /Users/steipete/Projects/codexbar/CodexBar.app`.
13-
- Release flow: `./Scripts/sign-and-notarize.sh` (arm64 notarized zip) and `./Scripts/make_appcast.sh <zip> <feed-url>`; follow validation steps in `docs/RELEASING.md`.
13+
- Release flow: `./Scripts/release.sh`; app metadata lives in `.mac-release.env`, repo build/signing stays in `Scripts/sign-and-notarize.sh`, and validation steps live in `docs/RELEASING.md`.
1414

1515
## Coding Style & Naming
1616
- Enforce SwiftFormat/SwiftLint: run `swiftformat Sources Tests` and `swiftlint --strict`. 4-space indent, 120-char lines, explicit `self` is intentional—do not remove.

Scripts/changelog-to-html.sh

Lines changed: 1 addition & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,4 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
3-
4-
VERSION=${1:-}
5-
CHANGELOG_FILE=${2:-}
6-
7-
if [[ -z "$VERSION" ]]; then
8-
echo "Usage: $0 <version> [changelog_file]" >&2
9-
exit 1
10-
fi
11-
123
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
13-
if [[ -z "$CHANGELOG_FILE" ]]; then
14-
if [[ -f "$SCRIPT_DIR/../CHANGELOG.md" ]]; then
15-
CHANGELOG_FILE="$SCRIPT_DIR/../CHANGELOG.md"
16-
elif [[ -f "CHANGELOG.md" ]]; then
17-
CHANGELOG_FILE="CHANGELOG.md"
18-
elif [[ -f "../CHANGELOG.md" ]]; then
19-
CHANGELOG_FILE="../CHANGELOG.md"
20-
else
21-
echo "Error: Could not find CHANGELOG.md" >&2
22-
exit 1
23-
fi
24-
fi
25-
26-
if [[ ! -f "$CHANGELOG_FILE" ]]; then
27-
echo "Error: Changelog file '$CHANGELOG_FILE' not found" >&2
28-
exit 1
29-
fi
30-
31-
extract_version_section() {
32-
local version=$1
33-
local file=$2
34-
awk -v version="$version" '
35-
BEGIN { found=0 }
36-
/^## / {
37-
if ($0 ~ "^##[[:space:]]+" version "([[:space:]].*|$)") { found=1; next }
38-
if (found) { exit }
39-
}
40-
found { print }
41-
' "$file"
42-
}
43-
44-
markdown_to_html() {
45-
local text=$1
46-
text=$(echo "$text" | sed 's/^### \(.*\)$/<h3>\1<\/h3>/')
47-
text=$(echo "$text" | sed 's/^## \(.*\)$/<h2>\1<\/h2>/')
48-
text=$(echo "$text" | sed 's/^- \*\*\([^*]*\)\*\*\(.*\)$/<li><strong>\1<\/strong>\2<\/li>/')
49-
text=$(echo "$text" | sed 's/^- \([^*].*\)$/<li>\1<\/li>/')
50-
text=$(echo "$text" | sed 's/\*\*\([^*]*\)\*\*/<strong>\1<\/strong>/g')
51-
text=$(echo "$text" | sed 's/`\([^`]*\)`/<code>\1<\/code>/g')
52-
text=$(echo "$text" | sed 's/\[\([^]]*\)\](\([^)]*\))/<a href="\2">\1<\/a>/g')
53-
echo "$text"
54-
}
55-
56-
version_content=$(extract_version_section "$VERSION" "$CHANGELOG_FILE")
57-
if [[ -z "$version_content" ]]; then
58-
echo "<h2>CodexBar $VERSION</h2>"
59-
echo "<p>Latest CodexBar update.</p>"
60-
echo "<p><a href=\"https://github.com/steipete/CodexBar/blob/main/CHANGELOG.md\">View full changelog</a></p>"
61-
exit 0
62-
fi
63-
64-
echo "<h2>CodexBar $VERSION</h2>"
65-
66-
in_list=false
67-
while IFS= read -r line; do
68-
if [[ "$line" =~ ^- ]]; then
69-
if [[ "$in_list" == false ]]; then
70-
echo "<ul>"
71-
in_list=true
72-
fi
73-
markdown_to_html "$line"
74-
else
75-
if [[ "$in_list" == true ]]; then
76-
echo "</ul>"
77-
in_list=false
78-
fi
79-
if [[ -n "$line" ]]; then
80-
markdown_to_html "$line"
81-
fi
82-
fi
83-
done <<< "$version_content"
84-
85-
if [[ "$in_list" == true ]]; then
86-
echo "</ul>"
87-
fi
88-
89-
echo "<p><a href=\"https://github.com/steipete/CodexBar/blob/main/CHANGELOG.md\">View full changelog</a></p>"
4+
exec "$SCRIPT_DIR/mac-release" changelog-html "$@"

Scripts/check-release-assets.sh

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,4 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
3-
4-
ROOT=$(cd "$(dirname "$0")/.." && pwd)
5-
source "$HOME/Projects/agent-scripts/release/sparkle_lib.sh"
6-
7-
TAG=${1:-$(git describe --tags --abbrev=0)}
8-
ARTIFACT_PREFIX="CodexBar-macos-[A-Za-z0-9_+-]+-"
9-
10-
check_assets "$TAG" "$ARTIFACT_PREFIX"
11-
12-
VERSION=${TAG#v}
13-
if gh --live release view "$TAG" --json assets --jq '.assets[].name' >/dev/null 2>&1; then
14-
assets=$(gh --live release view "$TAG" --json assets --jq '.assets[].name')
15-
else
16-
assets=$(gh release view "$TAG" --json assets --jq '.assets[].name')
17-
fi
18-
missing=0
19-
for target in \
20-
macos-arm64 \
21-
macos-x86_64 \
22-
linux-aarch64 \
23-
linux-x86_64
24-
do
25-
asset="CodexBarCLI-v${VERSION}-${target}.tar.gz"
26-
checksum="${asset}.sha256"
27-
if ! printf "%s\n" "$assets" | grep -Fxq "$asset"; then
28-
echo "ERROR: CLI asset missing on release $TAG: $asset" >&2
29-
missing=1
30-
fi
31-
if ! printf "%s\n" "$assets" | grep -Fxq "$checksum"; then
32-
echo "ERROR: CLI checksum missing on release $TAG: $checksum" >&2
33-
missing=1
34-
fi
35-
done
36-
37-
if [[ "$missing" == "1" ]]; then
38-
exit 1
39-
fi
40-
41-
echo "Release $TAG has all CodexBarCLI tarballs and checksums."
3+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
4+
exec "$SCRIPT_DIR/mac-release" check-assets "$@"

Scripts/mac-release

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5+
ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
6+
cd "$ROOT"
7+
8+
if [[ -n "${MAC_RELEASE_TOOL:-}" ]]; then
9+
exec "$MAC_RELEASE_TOOL" "$@"
10+
fi
11+
12+
for candidate in \
13+
"$ROOT/../agent-scripts/skills/mac-app-release/scripts/mac-release" \
14+
"$HOME/Projects/agent-scripts/skills/mac-app-release/scripts/mac-release"; do
15+
if [[ -x "$candidate" ]]; then
16+
exec "$candidate" "$@"
17+
fi
18+
done
19+
20+
cat >&2 <<'EOF'
21+
Missing mac-release helper.
22+
Clone agent-scripts next to this repo or set MAC_RELEASE_TOOL=/path/to/mac-release.
23+
EOF
24+
exit 127

Scripts/make_appcast.sh

Lines changed: 2 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,4 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
3-
4-
ROOT=$(cd "$(dirname "$0")/.." && pwd)
5-
ZIP=${1:?
6-
"Usage: $0 CodexBar-macos-<arch>-<ver>.zip"}
7-
FEED_URL=${2:-"https://raw.githubusercontent.com/steipete/CodexBar/main/appcast.xml"}
8-
PRIVATE_KEY_FILE=${SPARKLE_PRIVATE_KEY_FILE:-}
9-
SPARKLE_CHANNEL=${SPARKLE_CHANNEL:-}
10-
if [[ -z "$PRIVATE_KEY_FILE" ]]; then
11-
echo "Set SPARKLE_PRIVATE_KEY_FILE to your ed25519 private key (Sparkle)." >&2
12-
exit 1
13-
fi
14-
if [[ ! -f "$ZIP" ]]; then
15-
echo "Zip not found: $ZIP" >&2
16-
exit 1
17-
fi
18-
19-
ZIP_DIR=$(cd "$(dirname "$ZIP")" && pwd)
20-
ZIP_NAME=$(basename "$ZIP")
21-
ZIP_BASE="${ZIP_NAME%.zip}"
22-
VERSION=${SPARKLE_RELEASE_VERSION:-}
23-
if [[ -z "$VERSION" ]]; then
24-
if [[ "$ZIP_NAME" =~ ^CodexBar-(macos-[A-Za-z0-9_+-]+-)?([0-9]+(\.[0-9]+){1,2}([-.][^.]*)?)\.zip$ ]]; then
25-
VERSION="${BASH_REMATCH[2]}"
26-
else
27-
echo "Could not infer version from $ZIP_NAME; set SPARKLE_RELEASE_VERSION." >&2
28-
exit 1
29-
fi
30-
fi
31-
32-
NOTES_HTML="${ZIP_DIR}/${ZIP_BASE}.html"
33-
KEEP_NOTES=${KEEP_SPARKLE_NOTES:-0}
34-
if [[ -x "$ROOT/Scripts/changelog-to-html.sh" ]]; then
35-
"$ROOT/Scripts/changelog-to-html.sh" "$VERSION" >"$NOTES_HTML"
36-
else
37-
echo "Missing Scripts/changelog-to-html.sh; cannot generate HTML release notes." >&2
38-
exit 1
39-
fi
40-
cleanup() {
41-
if [[ -n "${WORK_DIR:-}" ]]; then
42-
rm -rf "$WORK_DIR"
43-
fi
44-
if [[ "$KEEP_NOTES" != "1" ]]; then
45-
rm -f "$NOTES_HTML"
46-
fi
47-
}
48-
trap cleanup EXIT
49-
50-
DOWNLOAD_URL_PREFIX=${SPARKLE_DOWNLOAD_URL_PREFIX:-"https://github.com/steipete/CodexBar/releases/download/v${VERSION}/"}
51-
52-
# Sparkle provides generate_appcast; ensure it's on PATH (via SwiftPM build of Sparkle's bin) or Xcode dmg
53-
if ! command -v generate_appcast >/dev/null; then
54-
echo "generate_appcast not found in PATH. Install Sparkle tools (see Sparkle docs)." >&2
55-
exit 1
56-
fi
57-
58-
WORK_DIR=$(mktemp -d /tmp/codexbar-appcast.XXXXXX)
59-
60-
cp "$ROOT/appcast.xml" "$WORK_DIR/appcast.xml"
61-
cp "$ZIP" "$WORK_DIR/$ZIP_NAME"
62-
cp "$NOTES_HTML" "$WORK_DIR/$ZIP_BASE.html"
63-
64-
pushd "$WORK_DIR" >/dev/null
65-
generate_appcast \
66-
--ed-key-file "$PRIVATE_KEY_FILE" \
67-
--download-url-prefix "$DOWNLOAD_URL_PREFIX" \
68-
--embed-release-notes \
69-
--link "$FEED_URL" \
70-
"$WORK_DIR"
71-
popd >/dev/null
72-
73-
if [[ -n "$SPARKLE_CHANNEL" ]]; then
74-
python3 - "$WORK_DIR/appcast.xml" "$VERSION" "$SPARKLE_CHANNEL" <<'PY'
75-
import re
76-
import sys
77-
78-
path, version, channel = sys.argv[1], sys.argv[2], sys.argv[3]
79-
with open(path, "r", encoding="utf-8") as handle:
80-
lines = handle.read().splitlines()
81-
82-
target = f"<sparkle:shortVersionString>{version}</sparkle:shortVersionString>"
83-
try:
84-
index = next(i for i, line in enumerate(lines) if target in line)
85-
except StopIteration as exc:
86-
raise SystemExit(f"Could not find {target} in {path}") from exc
87-
88-
for j in range(index, -1, -1):
89-
if "<item" in lines[j]:
90-
line = lines[j]
91-
if "sparkle:channel" in line:
92-
line = re.sub(r'sparkle:channel="[^"]*"', f'sparkle:channel="{channel}"', line)
93-
else:
94-
line = line.replace("<item", f'<item sparkle:channel="{channel}"', 1)
95-
lines[j] = line
96-
break
97-
else:
98-
raise SystemExit(f"Could not find <item> for version {version} in {path}")
99-
100-
with open(path, "w", encoding="utf-8") as handle:
101-
handle.write("\n".join(lines) + "\n")
102-
PY
103-
echo "Tagged ${VERSION} with sparkle:channel=\"${SPARKLE_CHANNEL}\""
104-
fi
105-
106-
cp "$WORK_DIR/appcast.xml" "$ROOT/appcast.xml"
107-
108-
echo "Appcast generated (appcast.xml). Upload alongside $ZIP at $FEED_URL"
3+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
4+
exec "$SCRIPT_DIR/mac-release" make-appcast "$@"

Scripts/release.sh

Lines changed: 2 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,4 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
3-
4-
ROOT=$(cd "$(dirname "$0")/.." && pwd)
5-
cd "$ROOT"
6-
7-
source "$ROOT/version.env"
8-
source "$ROOT/Scripts/release_artifacts.sh"
9-
source "$HOME/Projects/agent-scripts/release/sparkle_lib.sh"
10-
11-
APPCAST="$ROOT/appcast.xml"
12-
APP_NAME="CodexBar"
13-
ARCHES_VALUE=${ARCHES:-"arm64 x86_64"}
14-
APP_ZIP=$(codexbar_app_zip_name "$MARKETING_VERSION" "$ARCHES_VALUE")
15-
DSYM_ZIP=$(codexbar_dsym_zip_name "$MARKETING_VERSION" "$ARCHES_VALUE")
16-
ARTIFACT_PREFIX="CodexBar-macos-[A-Za-z0-9_+-]+-"
17-
BUNDLE_ID="com.steipete.codexbar"
18-
TAG="v${MARKETING_VERSION}"
19-
20-
err() { echo "ERROR: $*" >&2; exit 1; }
21-
22-
require_clean_worktree
23-
ensure_changelog_finalized "$MARKETING_VERSION"
24-
ensure_appcast_monotonic "$APPCAST" "$MARKETING_VERSION" "$BUILD_NUMBER"
25-
26-
swiftformat Sources Tests >/dev/null
27-
swiftlint --strict
28-
swift test
29-
30-
# Note: run this script in the foreground; do not background it so it waits to completion.
31-
"$ROOT/Scripts/sign-and-notarize.sh"
32-
33-
KEY_FILE=$(clean_key "$SPARKLE_PRIVATE_KEY_FILE")
34-
trap 'rm -f "$KEY_FILE"' EXIT
35-
36-
probe_sparkle_key "$KEY_FILE"
37-
38-
clear_sparkle_caches "$BUNDLE_ID"
39-
40-
NOTES_FILE=$(mktemp /tmp/codexbar-notes.XXXXXX.md)
41-
extract_notes_from_changelog "$MARKETING_VERSION" "$NOTES_FILE"
42-
trap 'rm -f "$KEY_FILE" "$NOTES_FILE"' EXIT
43-
44-
git tag -s -f -m "${APP_NAME} ${MARKETING_VERSION}" "$TAG"
45-
git push -f origin "$TAG"
46-
47-
gh release create "$TAG" "$APP_ZIP" "$DSYM_ZIP" \
48-
--title "${APP_NAME} ${MARKETING_VERSION}" \
49-
--notes-file "$NOTES_FILE"
50-
51-
SPARKLE_PRIVATE_KEY_FILE="$KEY_FILE" \
52-
"$ROOT/Scripts/make_appcast.sh" \
53-
"$APP_ZIP" \
54-
"https://raw.githubusercontent.com/steipete/CodexBar/main/appcast.xml"
55-
56-
verify_appcast_entry "$APPCAST" "$MARKETING_VERSION" "$KEY_FILE"
57-
58-
git add "$APPCAST"
59-
git commit -m "docs: update appcast for ${MARKETING_VERSION}"
60-
git push origin main
61-
62-
if [[ "${RUN_SPARKLE_UPDATE_TEST:-0}" == "1" ]]; then
63-
PREV_TAG=$(git tag --sort=-v:refname | sed -n '2p')
64-
[[ -z "$PREV_TAG" ]] && err "RUN_SPARKLE_UPDATE_TEST=1 set but no previous tag found"
65-
"$ROOT/Scripts/test_live_update.sh" "$PREV_TAG" "v${MARKETING_VERSION}"
66-
fi
67-
68-
check_assets "$TAG" "$ARTIFACT_PREFIX"
69-
70-
git push origin --tags
71-
72-
echo "Release ${MARKETING_VERSION} complete."
3+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
4+
exec "$SCRIPT_DIR/mac-release" release "$@"

Scripts/sign-and-notarize.sh

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,6 @@ if [[ -z "${APP_STORE_CONNECT_API_KEY_P8:-}" || -z "${APP_STORE_CONNECT_KEY_ID:-
1717
echo "Missing APP_STORE_CONNECT_* env vars (API key, key id, issuer id)." >&2
1818
exit 1
1919
fi
20-
if [[ -z "${SPARKLE_PRIVATE_KEY_FILE:-}" ]]; then
21-
echo "SPARKLE_PRIVATE_KEY_FILE is required for release signing/verification." >&2
22-
exit 1
23-
fi
24-
if [[ ! -f "$SPARKLE_PRIVATE_KEY_FILE" ]]; then
25-
echo "Sparkle key file not found: $SPARKLE_PRIVATE_KEY_FILE" >&2
26-
exit 1
27-
fi
28-
key_lines=$(grep -v '^[[:space:]]*#' "$SPARKLE_PRIVATE_KEY_FILE" | sed '/^[[:space:]]*$/d')
29-
if [[ $(printf "%s\n" "$key_lines" | wc -l) -ne 1 ]]; then
30-
echo "Sparkle key file must contain exactly one base64 line (no comments/blank lines)." >&2
31-
exit 1
32-
fi
33-
3420
echo "$APP_STORE_CONNECT_API_KEY_P8" | sed 's/\\n/\n/g' > /tmp/codexbar-api-key.p8
3521
trap 'rm -f /tmp/codexbar-api-key.p8 /tmp/${APP_NAME}Notarize.zip' EXIT
3622

0 commit comments

Comments
 (0)