diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6275775 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + + - name: Run Swift tests + run: swift test + + - name: Check release script syntax + run: zsh -n Scripts/*.sh + + - name: Package unsigned release + env: + GITHUB_REPOSITORY: tcballard/Meanwhile + RELEASE_OUTPUT_DIR: ${{ runner.temp }}/meanwhile-dist + SWIFT_BUILD_PATH: ${{ runner.temp }}/meanwhile-build + run: ./Scripts/release-unsigned.sh + + - name: Verify packaged artifacts + run: | + test -f "${{ runner.temp }}/meanwhile-dist/Meanwhile-0.1.0-unsigned.zip" + test -f "${{ runner.temp }}/meanwhile-dist/meanwhile.rb" + ruby -c "${{ runner.temp }}/meanwhile-dist/meanwhile.rb" diff --git a/README.md b/README.md index 31cbd44..adcb1ce 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,12 @@ GITHUB_REPOSITORY="owner/Meanwhile" \ ./Scripts/release.sh ``` -The script produces `dist/Meanwhile-0.1.0.zip` and `dist/meanwhile.rb`. Publishing -the archive, cask, and GitHub release remains an explicit release-owner action. +The script builds and signs the app in a temporary local directory, then +produces `dist/Meanwhile-0.1.0.zip` and `dist/meanwhile.rb`. It verifies the +stapled app and a clean extraction of the final archive before returning. +Publishing the archive, cask, and GitHub release remains an explicit +release-owner action. Set `RELEASE_OUTPUT_DIR` to write the final artifacts +somewhere other than `dist`. ## Package layout @@ -125,5 +129,3 @@ the archive, cask, and GitHub release remains an explicit release-owner action. dispositions, terminal focus, and integration installation. - `Meanwhile`: the menu-bar executable. - `MeanwhileHook`: the Claude/Codex hook and Claude status-line helper. - -See [ROADMAP.md](ROADMAP.md) for the release gate and post-v0.1 decision rule. diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 5b039a9..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,38 +0,0 @@ -# Meanwhile roadmap - -This is a decision tool, not a backlog. - -## Product rules - -**The wait-gate is the product.** No thinking agent means glyph only. A -`needs-you` event may preempt the gate, but ordinary work never appears while -the user is typing. - -Sources must be glance-actionable in roughly 10 seconds. Ordering is fixed: -needs-you, failing CI, reviews, then oldest first. `Peripheral` is already an -extracted package target. - -## v0.1 — Product cut - -**Scope:** Event-driven Claude and Codex session states, needs-you preemption, -Claude status-line output, 10-second debounce, snooze/dismiss, failing CI and -review sources, focus follow, parallel-session awareness, configuration, -signed packaging, notarization tooling, and generated Homebrew cask metadata. - -**Stable release gate:** Publish a stable release only after the automated tests and strict bundle -signature verification pass, the hook installer is exercised against clean and -existing Claude/Codex settings, and a real agent permission prompt focuses the -correct terminal. Developer ID notarization and a release-host URL must exist -before the stable public archive or cask is published. An earlier testing build -may be distributed only when its tag, GitHub release, archive, and Homebrew cask -all identify it clearly as an unsigned pre-release. - -**Stop condition:** If a week of real self-use does not produce repeated -glances and measurable review/CI clearance, stop at v0.1 and do not manufacture -another source. - -## Beyond v0.1 - -There is no scheduled v0.2. More work earns time only after external users -return in multiple weeks and request a specific change. Re-cost Meanwhile -against the rest of the portfolio before accepting that work. diff --git a/Scripts/make-cask.sh b/Scripts/make-cask.sh index d899f0d..28ba227 100755 --- a/Scripts/make-cask.sh +++ b/Scripts/make-cask.sh @@ -11,17 +11,17 @@ REPOSITORY="$1" VERSION="$2" SHA256="$3" CHANNEL="${4:-stable}" -OUTPUT="$ROOT/dist/meanwhile.rb" +OUTPUT="${CASK_OUTPUT_PATH:-$ROOT/dist/meanwhile.rb}" if [[ "$CHANNEL" == "unsigned" ]]; then DESCRIPTION="Unsigned pre-release of Meanwhile — for testing only" - CAVEATS=$'\n\n caveats <<~EOS\n This is an unsigned pre-release for testing only. macOS Gatekeeper will\n block its first launch. If you trust this build, explicitly remove quarantine:\n\n xattr -dr com.apple.quarantine /Applications/Meanwhile.app\n EOS' + CAVEATS=$'\n caveats <<~EOS\n This is an unsigned pre-release for testing only. macOS Gatekeeper will\n block its first launch. If you trust this build, explicitly remove quarantine:\n\n xattr -dr com.apple.quarantine /Applications/Meanwhile.app\n EOS' else DESCRIPTION="Use coding-agent wait time for reviews and failing CI" CAVEATS="" fi -mkdir -p "$ROOT/dist" +mkdir -p "${OUTPUT:h}" cat > "$OUTPUT" <= :sonoma" + depends_on macos: :sonoma + app "Meanwhile.app" -$CAVEATS zap trash: [ "~/Library/Application Support/Meanwhile", "~/Library/Preferences/com.meanwhile.Meanwhile.plist", ] +$CAVEATS end EOF diff --git a/Scripts/package-app.sh b/Scripts/package-app.sh index 77dd2fb..1d9ffc7 100755 --- a/Scripts/package-app.sh +++ b/Scripts/package-app.sh @@ -4,7 +4,8 @@ set -euo pipefail ROOT="${0:A:h:h}" CONFIGURATION="${CONFIGURATION:-debug}" CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY:--}" -APP="$ROOT/dist/Meanwhile.app" +APP_OUTPUT_DIR="${APP_OUTPUT_DIR:-$ROOT/dist}" +APP="$APP_OUTPUT_DIR/Meanwhile.app" CONTENTS="$APP/Contents" BUILD_PATH="${SWIFT_BUILD_PATH:-$ROOT/.build}" diff --git a/Scripts/release-unsigned.sh b/Scripts/release-unsigned.sh index 67f9e67..eb69791 100755 --- a/Scripts/release-unsigned.sh +++ b/Scripts/release-unsigned.sh @@ -4,18 +4,37 @@ set -euo pipefail ROOT="${0:A:h:h}" : "${GITHUB_REPOSITORY:?Set GITHUB_REPOSITORY to owner/repository}" +TEMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/meanwhile-unsigned-release.XXXXXX")" +cleanup() { + rm -rf "$TEMP_ROOT" +} +trap cleanup EXIT INT TERM + export CONFIGURATION=release export CODE_SIGN_IDENTITY=- export SWIFT_BUILD_PATH="${SWIFT_BUILD_PATH:-${TMPDIR:-/tmp}/meanwhile-unsigned-release-build}" +export APP_OUTPUT_DIR="$TEMP_ROOT/package" + +OUTPUT_DIR="${RELEASE_OUTPUT_DIR:-$ROOT/dist}" +mkdir -p "$OUTPUT_DIR" + APP="$("$ROOT/Scripts/package-app.sh")" BASE_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP/Contents/Info.plist")" VERSION="$BASE_VERSION-unsigned" -ZIP="$ROOT/dist/Meanwhile-$VERSION.zip" +ZIP="$OUTPUT_DIR/Meanwhile-$VERSION.zip" +rm -f "$ZIP" ditto -c -k --keepParent "$APP" "$ZIP" + +VERIFY_DIR="$TEMP_ROOT/verify" +mkdir -p "$VERIFY_DIR" +ditto -x -k "$ZIP" "$VERIFY_DIR" +codesign --verify --deep --strict --verbose=2 "$VERIFY_DIR/Meanwhile.app" + SHA256="$(shasum -a 256 "$ZIP" | awk '{print $1}')" +export CASK_OUTPUT_PATH="$OUTPUT_DIR/meanwhile.rb" "$ROOT/Scripts/make-cask.sh" "$GITHUB_REPOSITORY" "$VERSION" "$SHA256" unsigned echo "UNSIGNED PRE-RELEASE — FOR TESTING ONLY" echo "$ZIP" -echo "$ROOT/dist/meanwhile.rb" +echo "$CASK_OUTPUT_PATH" diff --git a/Scripts/release.sh b/Scripts/release.sh index 331d1fb..eb8db29 100755 --- a/Scripts/release.sh +++ b/Scripts/release.sh @@ -6,19 +6,44 @@ ROOT="${0:A:h:h}" : "${NOTARYTOOL_PROFILE:?Set NOTARYTOOL_PROFILE to an xcrun notarytool keychain profile}" : "${GITHUB_REPOSITORY:?Set GITHUB_REPOSITORY to owner/repository}" +TEMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/meanwhile-release.XXXXXX")" +cleanup() { + rm -rf "$TEMP_ROOT" +} +trap cleanup EXIT INT TERM + export CONFIGURATION=release +export SWIFT_BUILD_PATH="${SWIFT_BUILD_PATH:-${TMPDIR:-/tmp}/meanwhile-release-build}" +export APP_OUTPUT_DIR="$TEMP_ROOT/package" + +OUTPUT_DIR="${RELEASE_OUTPUT_DIR:-$ROOT/dist}" +mkdir -p "$OUTPUT_DIR" + APP="$("$ROOT/Scripts/package-app.sh")" VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP/Contents/Info.plist")" -ZIP="$ROOT/dist/Meanwhile-$VERSION.zip" +ZIP="$OUTPUT_DIR/Meanwhile-$VERSION.zip" +rm -f "$ZIP" ditto -c -k --keepParent "$APP" "$ZIP" xcrun notarytool submit "$ZIP" --keychain-profile "$NOTARYTOOL_PROFILE" --wait xcrun stapler staple "$APP" xcrun stapler validate "$APP" +codesign --verify --deep --strict --verbose=2 "$APP" + +rm -f "$ZIP" ditto -c -k --keepParent "$APP" "$ZIP" +VERIFY_DIR="$TEMP_ROOT/verify" +mkdir -p "$VERIFY_DIR" +ditto -x -k "$ZIP" "$VERIFY_DIR" +VERIFIED_APP="$VERIFY_DIR/Meanwhile.app" +codesign --verify --deep --strict --verbose=2 "$VERIFIED_APP" +spctl --assess --type execute --verbose=2 "$VERIFIED_APP" +xcrun stapler validate "$VERIFIED_APP" + SHA256="$(shasum -a 256 "$ZIP" | awk '{print $1}')" +export CASK_OUTPUT_PATH="$OUTPUT_DIR/meanwhile.rb" "$ROOT/Scripts/make-cask.sh" "$GITHUB_REPOSITORY" "$VERSION" "$SHA256" echo "$ZIP" -echo "$ROOT/dist/meanwhile.rb" +echo "$CASK_OUTPUT_PATH"