From fb91dfca94900b38a74ee960d7fea2c596647930 Mon Sep 17 00:00:00 2001 From: Hephaestus Date: Tue, 26 May 2026 00:19:56 +0000 Subject: [PATCH 1/3] Resolve release DMG packaging merge --- .github/workflows/release.yml | 18 ++++- docs/INSTALLATION.md | 34 +++++++++ scripts/ci/run-fast-checks.sh | 1 + scripts/package-release-dmg.sh | 103 ++++++++++++++++++++++++++++ scripts/test-package-release-dmg.sh | 48 +++++++++++++ 5 files changed, 203 insertions(+), 1 deletion(-) create mode 100755 scripts/package-release-dmg.sh create mode 100755 scripts/test-package-release-dmg.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62a6bd6..8a043d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -149,6 +149,9 @@ jobs: - name: Package release archive run: ./scripts/package-release-archive.sh "${GITHUB_REF_NAME}" + - name: Package release DMG + run: ./scripts/package-release-dmg.sh "${GITHUB_REF_NAME}" + - name: Validate release archive contents run: | archive="dist/apw-macos-${GITHUB_REF_NAME}.tar.gz" @@ -160,10 +163,23 @@ jobs: - name: Validate universal release binaries run: ./scripts/verify-universal-binaries.sh + - name: Validate release DMG checksum + run: | + dmg="dist/apw-macos-${GITHUB_REF_NAME}.dmg" + test -f "$dmg" + test -f "$dmg.sha256" + ( + cd dist + shasum -a 256 -c "apw-macos-${GITHUB_REF_NAME}.dmg.sha256" + ) + - name: Publish release assets uses: softprops/action-gh-release@v2 with: - files: dist/apw-macos-${{ github.ref_name }}.tar.gz + files: | + dist/apw-macos-${{ github.ref_name }}.tar.gz + dist/apw-macos-${{ github.ref_name }}.dmg + dist/apw-macos-${{ github.ref_name }}.dmg.sha256 publish-homebrew-tap: name: Publish Homebrew tap PR diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 75bb39b..76def24 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -91,6 +91,40 @@ listed in `docs/bootstrap/onboarding.md` are configured. If those optional secrets are absent, the workflow emits a warning and publishes an unstapled archive rather than failing unrelated release automation. +## Install from a release DMG + +Release DMGs are named `apw-macos-vX.Y.Z.dmg`. Each release also publishes a +matching `apw-macos-vX.Y.Z.dmg.sha256` file for checksum verification. + +Download both files from the GitHub release, then verify the DMG before opening +it: + +```bash +shasum -a 256 -c apw-macos-vX.Y.Z.dmg.sha256 +``` + +Open the DMG and install the app bundle: + +```bash +hdiutil attach apw-macos-vX.Y.Z.dmg +cp -R "/Volumes/APW vX.Y.Z/APW.app" /Applications/APW.app +install -m 0755 "/Volumes/APW vX.Y.Z/bin/apw" /usr/local/bin/apw +hdiutil detach "/Volumes/APW vX.Y.Z" +``` + +Then run the first-use setup: + +```bash +apw --version +apw status --json +apw app install +apw app launch +``` + +Gatekeeper should accept release DMGs built by the release workflow. If macOS +blocks the app, stop the install and verify that the release asset was signed +and notarized before use. + ## Homebrew APW uses a **formula-plus-app-install** Homebrew model for the v2 line. The diff --git a/scripts/ci/run-fast-checks.sh b/scripts/ci/run-fast-checks.sh index 3d84d49..3d6f6a1 100755 --- a/scripts/ci/run-fast-checks.sh +++ b/scripts/ci/run-fast-checks.sh @@ -45,5 +45,6 @@ done < <(find .github/scripts scripts -type f -name '*.sh' -print0) ./scripts/test-verify-universal-binaries.sh ./scripts/test-universal-release-config.sh ./scripts/test-notarize-native-app.sh +./scripts/test-package-release-dmg.sh echo "APW fast checks passed." diff --git a/scripts/package-release-dmg.sh b/scripts/package-release-dmg.sh new file mode 100755 index 0000000..b0f367e --- /dev/null +++ b/scripts/package-release-dmg.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +set -euo pipefail + +print_help() { + cat <<'HELP' +Usage: ./scripts/package-release-dmg.sh VERSION_OR_TAG + +Create dist/apw-macos-.dmg and a matching .sha256 file. + +The DMG contains: + - APW.app/ + - bin/apw + - Applications -> /Applications + +Prerequisites: + - cargo release binary at rust/target/release/apw + - native app bundle at native-app/dist/APW.app + - hdiutil on macOS +HELP +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + print_help + exit 0 +fi + +if [[ $# -ne 1 || -z "${1:-}" ]]; then + echo "VERSION_OR_TAG is required." >&2 + print_help >&2 + exit 1 +fi + +ROOT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VERSION_OR_TAG="$1" +BIN_PATH="$ROOT_DIR/rust/target/release/apw" +APP_DIR="$ROOT_DIR/native-app/dist/APW.app" +DIST_DIR="$ROOT_DIR/dist" +STAGING_DIR="$DIST_DIR/dmg-staging" +DMG_PATH="$DIST_DIR/apw-macos-${VERSION_OR_TAG}.dmg" +CHECKSUM_PATH="$DMG_PATH.sha256" +MOUNT_DIR="$DIST_DIR/dmg-mount" +HDIUTIL_BIN="${HDIUTIL_BIN:-hdiutil}" + +if ! command -v "$HDIUTIL_BIN" >/dev/null 2>&1; then + echo "hdiutil not found. DMG packaging must run on macOS." >&2 + exit 1 +fi + +if [[ ! -x "$BIN_PATH" ]]; then + echo "Missing executable release binary: $BIN_PATH" >&2 + echo "Build it first with: cargo build --manifest-path rust/Cargo.toml --release" >&2 + exit 1 +fi + +if [[ ! -d "$APP_DIR" ]]; then + echo "Missing native app bundle: $APP_DIR" >&2 + echo "Build it first with: ./scripts/build-native-app.sh" >&2 + exit 1 +fi + +if [[ ! -f "$APP_DIR/Contents/Info.plist" || ! -x "$APP_DIR/Contents/MacOS/APW" ]]; then + echo "Native app bundle is incomplete: $APP_DIR" >&2 + exit 1 +fi + +cleanup() { + if mount | grep -q " on $MOUNT_DIR "; then + "$HDIUTIL_BIN" detach "$MOUNT_DIR" >/dev/null 2>&1 || true + fi + rm -rf "$STAGING_DIR" "$MOUNT_DIR" +} +trap cleanup EXIT + +rm -rf "$STAGING_DIR" "$MOUNT_DIR" "$DMG_PATH" "$CHECKSUM_PATH" +mkdir -p "$STAGING_DIR/bin" "$DIST_DIR" + +cp "$BIN_PATH" "$STAGING_DIR/bin/apw" +cp -R "$APP_DIR" "$STAGING_DIR/APW.app" +ln -s /Applications "$STAGING_DIR/Applications" + +"$HDIUTIL_BIN" create \ + -volname "APW ${VERSION_OR_TAG}" \ + -srcfolder "$STAGING_DIR" \ + -ov \ + -format UDZO \ + "$DMG_PATH" + +if [[ "${APW_SKIP_DMG_MOUNT_SMOKE:-0}" != "1" ]]; then + mkdir -p "$MOUNT_DIR" + "$HDIUTIL_BIN" attach -nobrowse -readonly -mountpoint "$MOUNT_DIR" "$DMG_PATH" >/dev/null + test -x "$MOUNT_DIR/bin/apw" + test -f "$MOUNT_DIR/APW.app/Contents/Info.plist" + test -x "$MOUNT_DIR/APW.app/Contents/MacOS/APW" + "$HDIUTIL_BIN" detach "$MOUNT_DIR" >/dev/null +fi + +( + cd "$DIST_DIR" + shasum -a 256 "$(basename "$DMG_PATH")" > "$(basename "$CHECKSUM_PATH")" +) + +echo "$DMG_PATH" +echo "$CHECKSUM_PATH" diff --git a/scripts/test-package-release-dmg.sh b/scripts/test-package-release-dmg.sh new file mode 100755 index 0000000..1d7c903 --- /dev/null +++ b/scripts/test-package-release-dmg.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WORK_DIR="$(mktemp -d)" +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +fake_hdiutil="$WORK_DIR/hdiutil" +fake_log="$WORK_DIR/hdiutil.log" + +cat > "$fake_hdiutil" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +echo "$*" >> "$FAKE_HDIUTIL_LOG" +if [[ "${1:-}" == "create" ]]; then + output="${@: -1}" + mkdir -p "$(dirname "$output")" + printf 'fake dmg\n' > "$output" +fi +EOF +chmod +x "$fake_hdiutil" + +mkdir -p \ + "$ROOT_DIR/rust/target/release" \ + "$ROOT_DIR/native-app/dist/APW.app/Contents/MacOS" + +printf '#!/usr/bin/env bash\necho apw\n' > "$ROOT_DIR/rust/target/release/apw" +chmod +x "$ROOT_DIR/rust/target/release/apw" +printf '#!/usr/bin/env bash\necho APW\n' > "$ROOT_DIR/native-app/dist/APW.app/Contents/MacOS/APW" +chmod +x "$ROOT_DIR/native-app/dist/APW.app/Contents/MacOS/APW" +printf '\n' > "$ROOT_DIR/native-app/dist/APW.app/Contents/Info.plist" + +rm -f "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg" "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg.sha256" + +FAKE_HDIUTIL_LOG="$fake_log" \ +HDIUTIL_BIN="$fake_hdiutil" \ +APW_SKIP_DMG_MOUNT_SMOKE=1 \ + "$ROOT_DIR/scripts/package-release-dmg.sh" v9.9.9 >"$WORK_DIR/package.out" + +test -f "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg" +test -f "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg.sha256" +grep -q "create" "$fake_log" +grep -Eq "^[0-9a-f]{64} apw-macos-v9.9.9.dmg$" "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg.sha256" + +rm -f "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg" "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg.sha256" From c6f88cff571dbe3a9f192066e44318c2a3a8012e Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr Date: Sun, 24 May 2026 05:41:33 +0100 Subject: [PATCH 2/3] Verify DMG staging payload in tests --- scripts/test-package-release-dmg.sh | 63 +++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/scripts/test-package-release-dmg.sh b/scripts/test-package-release-dmg.sh index 1d7c903..a495251 100755 --- a/scripts/test-package-release-dmg.sh +++ b/scripts/test-package-release-dmg.sh @@ -3,7 +3,22 @@ set -euo pipefail ROOT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" WORK_DIR="$(mktemp -d)" +BIN_PATH="$ROOT_DIR/rust/target/release/apw" +APP_PATH="$ROOT_DIR/native-app/dist/APW.app" +DMG_PATH="$ROOT_DIR/dist/apw-macos-v9.9.9.dmg" +CHECKSUM_PATH="$DMG_PATH.sha256" cleanup() { + rm -f "$DMG_PATH" "$CHECKSUM_PATH" + rm -rf "$APP_PATH" + rm -f "$BIN_PATH" + if [[ -f "$WORK_DIR/apw.backup" ]]; then + mkdir -p "$(dirname "$BIN_PATH")" + mv "$WORK_DIR/apw.backup" "$BIN_PATH" + fi + if [[ -d "$WORK_DIR/APW.app.backup" ]]; then + mkdir -p "$(dirname "$APP_PATH")" + mv "$WORK_DIR/APW.app.backup" "$APP_PATH" + fi rm -rf "$WORK_DIR" } trap cleanup EXIT @@ -16,6 +31,23 @@ cat > "$fake_hdiutil" <<'EOF' set -euo pipefail echo "$*" >> "$FAKE_HDIUTIL_LOG" if [[ "${1:-}" == "create" ]]; then + srcfolder="" + for ((i = 1; i <= $#; i++)); do + if [[ "${!i}" == "-srcfolder" ]]; then + next=$((i + 1)) + srcfolder="${!next}" + break + fi + done + if [[ -z "$srcfolder" ]]; then + echo "missing -srcfolder" >&2 + exit 1 + fi + test -x "$srcfolder/bin/apw" + test -f "$srcfolder/APW.app/Contents/Info.plist" + test -x "$srcfolder/APW.app/Contents/MacOS/APW" + test -L "$srcfolder/Applications" + [[ "$(readlink "$srcfolder/Applications")" == "/Applications" ]] output="${@: -1}" mkdir -p "$(dirname "$output")" printf 'fake dmg\n' > "$output" @@ -23,26 +55,31 @@ fi EOF chmod +x "$fake_hdiutil" +if [[ -f "$BIN_PATH" ]]; then + mv "$BIN_PATH" "$WORK_DIR/apw.backup" +fi +if [[ -d "$APP_PATH" ]]; then + mv "$APP_PATH" "$WORK_DIR/APW.app.backup" +fi + mkdir -p \ - "$ROOT_DIR/rust/target/release" \ - "$ROOT_DIR/native-app/dist/APW.app/Contents/MacOS" + "$(dirname "$BIN_PATH")" \ + "$APP_PATH/Contents/MacOS" -printf '#!/usr/bin/env bash\necho apw\n' > "$ROOT_DIR/rust/target/release/apw" -chmod +x "$ROOT_DIR/rust/target/release/apw" -printf '#!/usr/bin/env bash\necho APW\n' > "$ROOT_DIR/native-app/dist/APW.app/Contents/MacOS/APW" -chmod +x "$ROOT_DIR/native-app/dist/APW.app/Contents/MacOS/APW" -printf '\n' > "$ROOT_DIR/native-app/dist/APW.app/Contents/Info.plist" +printf '#!/usr/bin/env bash\necho apw\n' > "$BIN_PATH" +chmod +x "$BIN_PATH" +printf '#!/usr/bin/env bash\necho APW\n' > "$APP_PATH/Contents/MacOS/APW" +chmod +x "$APP_PATH/Contents/MacOS/APW" +printf '\n' > "$APP_PATH/Contents/Info.plist" -rm -f "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg" "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg.sha256" +rm -f "$DMG_PATH" "$CHECKSUM_PATH" FAKE_HDIUTIL_LOG="$fake_log" \ HDIUTIL_BIN="$fake_hdiutil" \ APW_SKIP_DMG_MOUNT_SMOKE=1 \ "$ROOT_DIR/scripts/package-release-dmg.sh" v9.9.9 >"$WORK_DIR/package.out" -test -f "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg" -test -f "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg.sha256" +test -f "$DMG_PATH" +test -f "$CHECKSUM_PATH" grep -q "create" "$fake_log" -grep -Eq "^[0-9a-f]{64} apw-macos-v9.9.9.dmg$" "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg.sha256" - -rm -f "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg" "$ROOT_DIR/dist/apw-macos-v9.9.9.dmg.sha256" +grep -Eq "^[0-9a-f]{64} apw-macos-v9.9.9.dmg$" "$CHECKSUM_PATH" From f0f321218d34db00bcac3c6e4e16b2b856df79fa Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr Date: Sun, 24 May 2026 06:38:26 +0100 Subject: [PATCH 3/3] Keep DMG mounted for app install docs --- docs/INSTALLATION.md | 13 ++++++++----- scripts/test-package-release-dmg.sh | 8 ++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 76def24..17b9963 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -106,18 +106,21 @@ shasum -a 256 -c apw-macos-vX.Y.Z.dmg.sha256 Open the DMG and install the app bundle: ```bash +MOUNT="/Volumes/APW vX.Y.Z" hdiutil attach apw-macos-vX.Y.Z.dmg -cp -R "/Volumes/APW vX.Y.Z/APW.app" /Applications/APW.app -install -m 0755 "/Volumes/APW vX.Y.Z/bin/apw" /usr/local/bin/apw -hdiutil detach "/Volumes/APW vX.Y.Z" +install -m 0755 "$MOUNT/bin/apw" /usr/local/bin/apw +(cd "$MOUNT" && ./bin/apw app install) +cp -R "$MOUNT/APW.app" /Applications/APW.app +hdiutil detach "$MOUNT" ``` -Then run the first-use setup: +Keep the DMG mounted until `apw app install` completes; the installer discovers +`APW.app` from the mounted volume before copying it into the per-user runtime +directory. Then run the first-use launch check: ```bash apw --version apw status --json -apw app install apw app launch ``` diff --git a/scripts/test-package-release-dmg.sh b/scripts/test-package-release-dmg.sh index a495251..800d615 100755 --- a/scripts/test-package-release-dmg.sh +++ b/scripts/test-package-release-dmg.sh @@ -83,3 +83,11 @@ test -f "$DMG_PATH" test -f "$CHECKSUM_PATH" grep -q "create" "$fake_log" grep -Eq "^[0-9a-f]{64} apw-macos-v9.9.9.dmg$" "$CHECKSUM_PATH" + +awk ' + /^## Install from a release DMG/ { inside = 1 } + /^## Homebrew/ { inside = 0 } + inside && /apw app install/ && !install_line { install_line = NR } + inside && /hdiutil detach/ { detach_line = NR } + END { exit !(install_line && detach_line && install_line < detach_line) } +' "$ROOT_DIR/docs/INSTALLATION.md"