diff --git a/.kiro/specs/pkg-installer/.config.kiro b/.kiro/specs/pkg-installer/.config.kiro new file mode 100644 index 0000000..879696d --- /dev/null +++ b/.kiro/specs/pkg-installer/.config.kiro @@ -0,0 +1 @@ +{"specId": "68dbf316-3ca2-42d3-bab3-1d7d8c8ba56e", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/pkg-installer/design.md b/.kiro/specs/pkg-installer/design.md new file mode 100644 index 0000000..e55a9d6 --- /dev/null +++ b/.kiro/specs/pkg-installer/design.md @@ -0,0 +1,271 @@ +# Design Document: pkg-installer + +## Overview + +This feature adds a `.pkg` installer distribution channel for Wispr. The implementation extends the existing Makefile build pipeline with two new targets (`pkg` and `pkg-release`) that chain into the existing `notarize` target to avoid duplicating archive, signing, or notarization logic. + +The flow is: `notarize` (existing) → `pkgbuild` → `productbuild` → `productsign` → `notarytool` → `stapler`. The final artifact is a signed, notarized `.pkg` with a custom installer UI (branded background, welcome, readme, license screens) that installs Wispr.app to `/Applications`. + +All installer resources live in `pkg/resources/` and the distribution XML at `pkg/distribution.xml`, both version-controlled. The `installer_identity` (Developer ID Installer certificate name) is read from the existing `secrets/notarization.json`. + +## Architecture + +The design maximizes reuse of the existing Makefile infrastructure. No new scripts or build tools are introduced — everything is standard Apple command-line tools (`pkgbuild`, `productbuild`, `productsign`, `notarytool`, `stapler`, `spctl`) invoked from Make. + +### Build Flow + +```mermaid +flowchart TD + A["make pkg"] --> B["notarize (existing target)"] + B --> C["archive + sign app + notarize app + staple app"] + C --> D["pkgbuild → Component .pkg"] + D --> E["productbuild → Product .pkg (unsigned)"] + E --> F["productsign → Product .pkg (signed)"] + F --> G["notarytool → notarize .pkg"] + G --> H["stapler → staple .pkg"] + H --> I["spctl → verify .pkg"] + I --> J["build/export/wispr-VERSION.pkg"] + + K["make pkg-release VERSION=x.y.z"] --> L["Set MARKETING_VERSION"] + L --> A + J --> M["gh release create/upload"] +``` + +### Target Dependency Chain + +``` +pkg-release + └── pkg + └── notarize (existing) + └── archive (existing) + └── bump-build (existing) + └── _setup-api-key (existing) +``` + +The `pkg` target calls `notarize` as a prerequisite, then runs the pkg-specific steps. The `pkg-release` target sets the version, calls `pkg`, then uploads to GitHub Releases. This mirrors the `brew-release` pattern already in the Makefile. + +## Components and Interfaces + +### New Makefile Variables + +| Variable | Source | Value | +|----------|--------|-------| +| `INSTALLER_IDENTITY` | `secrets/notarization.json` → `.installer_identity` | Developer ID Installer certificate name | +| `COMPONENT_PKG` | Derived | `$(EXPORT_DIR)/wispr-component.pkg` | +| `PRODUCT_PKG` | Derived | `$(EXPORT_DIR)/wispr-unsigned.pkg` | +| `SIGNED_PKG` | Derived | `$(EXPORT_DIR)/wispr-signed.pkg` | +| `FINAL_PKG` | Derived | `$(EXPORT_DIR)/wispr-$(VERSION).pkg` | +| `PKG_RESOURCES` | Static | `$(CURDIR)/pkg/resources` | +| `DISTRIBUTION_XML` | Static | `$(CURDIR)/pkg/distribution.xml` | + +All existing variables (`BUNDLE_ID`, `SIGNING_IDENTITY`, `APP_PATH`, `EXPORT_DIR`, `ARCHIVE_PATH`, `API_KEY_PATH`, `API_KEY_ID`, `API_ISSUER`, `NOTARIZATION_JSON`) are reused as-is. + +### New Makefile Targets + +#### `pkg` target + +Orchestrates the full `.pkg` build pipeline: + +1. Depends on `notarize` — produces the signed, notarized `Wispr.app` at `$(APP_PATH)` +2. Reads `INSTALLER_IDENTITY` from `$(NOTARIZATION_JSON)` +3. Validates `INSTALLER_IDENTITY` is non-empty +4. Extracts `VERSION` from the Xcode project's `MARKETING_VERSION` (if not provided) +5. Runs `pkgbuild` to create the component package +6. Runs `productbuild` to create the product package with custom UI +7. Runs `productsign` to sign the product package +8. Runs `notarytool` to notarize the signed package (reuses `_setup-api-key` / `_cleanup-api-key`) +9. Runs `stapler` to staple the notarization ticket +10. Runs `spctl` to verify +11. Renames to final `wispr-.pkg` +12. Prints summary with path + +#### `pkg-release` target + +Mirrors `brew-release` pattern: + +1. Validates `VERSION` parameter is provided +2. Validates `gh` CLI is installed +3. Sets `MARKETING_VERSION` in the Xcode project via `sed` +4. Calls `make pkg` +5. Creates/updates GitHub Release tagged `v` +6. Uploads `.pkg` as release asset (alongside existing assets) + +### Command-Line Tool Usage + +| Step | Tool | Key Arguments | +|------|------|---------------| +| Component pkg | `pkgbuild` | `--root` (app path parent), `--component-plist` (not needed, single app), `--install-location /Applications`, `--identifier $(BUNDLE_ID)`, `--version $(VERSION)` | +| Product pkg | `productbuild` | `--distribution $(DISTRIBUTION_XML)`, `--resources $(PKG_RESOURCES)`, `--package-path $(EXPORT_DIR)` | +| Sign pkg | `productsign` | `--sign "$(INSTALLER_IDENTITY)"` | +| Notarize pkg | `notarytool` | `--key`, `--key-id`, `--issuer`, `--wait` | +| Staple pkg | `stapler` | `staple` | +| Verify pkg | `spctl` | `-a -vvv -t install` | + +### File Layout + +``` +repo/ +├── pkg/ +│ ├── distribution.xml # Installer flow definition +│ └── resources/ +│ ├── background.png # Branded installer background +│ ├── welcome.html # Welcome screen content +│ ├── readme.html # System requirements & post-install +│ └── license.txt # Apache License 2.0 (copy of LICENSE) +├── secrets/ +│ └── notarization.json # Now includes "installer_identity" field +├── Makefile # Extended with pkg + pkg-release targets +└── build/ + └── export/ + ├── Wispr.app # (existing, from notarize target) + └── wispr-.pkg # (new, final output) +``` + +## Data Models + +### `secrets/notarization.json` (extended) + +```json +{ + "apple_id": "[email]", + "team_id": "[team_id]", + "signing_identity": "Developer ID Application: [name] ([team_id])", + "installer_identity": "Developer ID Installer: [name] ([team_id])" +} +``` + +The only change is the addition of the `installer_identity` field. All existing fields remain unchanged. + +### `pkg/distribution.xml` + +```xml + + + Wispr + + + + + + + + + + + + + + + wispr-component.pkg + +``` + +Key design decisions: +- `customize="never"` — single component, no user choice needed +- `require-scripts="false"` — no pre/post-install scripts per requirements +- The `pkg-ref` version is `0` because the actual version is set in the component package via `pkgbuild --version` +- The `pkg-ref` filename matches the `COMPONENT_PKG` basename + +### Installer Resource Files + +| File | Format | Content | +|------|--------|---------| +| `background.png` | PNG, ~660×440 | Wispr branding with project color palette | +| `welcome.html` | HTML | Brief intro: what Wispr is, key features, on-device privacy | +| `readme.html` | HTML | System requirements (macOS 15.0+, microphone), post-install steps (grant permissions, download model) | +| `license.txt` | Plain text | Copy of the repo root `LICENSE` file (Apache 2.0) | + + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: Installer identity extraction round trip + +*For any* valid `notarization.json` file containing an `installer_identity` field with a non-empty string value, reading that field via `jq -r .installer_identity` shall produce the exact string stored in the JSON. + +**Validates: Requirements 3.2, 7.2** + +### Property 2: Output package filename follows version pattern + +*For any* valid semantic version string `X.Y.Z`, the final `.pkg` output filename shall be `wispr-X.Y.Z.pkg` and shall be located in the `build/export/` directory. + +**Validates: Requirements 5.2** + +### Property 3: Marketing version injection + +*For any* valid version string `X.Y.Z`, running the `sed` substitution on a `.pbxproj` file containing `MARKETING_VERSION = ;` shall produce a file where all `MARKETING_VERSION` entries equal `X.Y.Z`. + +**Validates: Requirements 6.1** + +### Property 4: Missing resource file detection + +*For any* single file removed from the required set {`background.png`, `welcome.html`, `readme.html`, `license.txt`} in `pkg/resources/`, the build pipeline shall fail with an error message that names the missing file. + +**Validates: Requirements 8.4** + +## Error Handling + +All error handling follows the existing Makefile pattern: print a descriptive message to stderr and `exit 1`. No partial artifacts are left behind on failure. + +| Error Condition | Message Pattern | Source Requirement | +|---|---|---| +| `pkgbuild` non-zero exit | `Error: pkgbuild failed` | 1.4 | +| `productbuild` non-zero exit | `Error: productbuild failed` | 2.8 | +| `productsign` non-zero exit | `Error: productsign failed` | 3.4 | +| `installer_identity` missing from JSON | `Error: installer_identity not found in ` | 7.3 | +| `installer_identity` not in keychain | `Error: certificate "" not found in keychain` | 3.3 | +| Notarization failure | `Error: notarization failed. Log: ` | 4.5 | +| Stapling failure | `Error: stapler failed` | 4.6 | +| Missing resource file | `Error: missing installer resource: ` | 8.4 | +| `VERSION` not provided (pkg-release) | `Usage: make pkg-release VERSION=x.y.z` | 6.5 | +| `gh` CLI not installed | `Error: gh CLI not installed` | 6.6 | + +Each tool invocation in the Makefile recipe uses `||` to catch failures: + +```makefile +@pkgbuild ... || { echo "Error: pkgbuild failed"; exit 1; } +``` + +The `_cleanup-api-key` target is called in a trap or final step to ensure the API key file is removed even on failure, matching the existing pattern. + +## Testing Strategy + +This feature is primarily a Makefile build pipeline — the "code" is shell commands orchestrated by Make. Testing focuses on two layers: + +### Unit Tests (Example-Based) + +Since the deliverable is a Makefile with shell commands, unit tests verify: + +1. **Static file validation**: Parse `pkg/distribution.xml` and verify it contains the required elements (background, welcome, readme, license, single choice, correct pkg-ref identifier) +2. **Resource file existence**: Verify all four required files exist in `pkg/resources/` +3. **JSON schema**: Verify `secrets/notarization.json` contains the `installer_identity` field +4. **Makefile target existence**: Verify `pkg` and `pkg-release` targets are defined +5. **Makefile dependency chain**: Verify `pkg` depends on `notarize` (grep the Makefile) +6. **No logic duplication**: Verify the `pkg` target recipe does not contain `xcodebuild archive`, `codesign --deep`, or other commands that belong to the `notarize` target + +These can be implemented as a shell-based test script (e.g., `test_pkg_installer.sh`) using `xmllint` for XML validation and `jq` for JSON validation. + +### Property-Based Tests + +Property-based tests use [Hypothesis](https://hypothesis.readthedocs.io/) (via `hypothesis` for shell/Python hybrid) or a shell-based approach with randomized inputs. Given the Makefile/shell nature of this project, a lightweight Python test file using `hypothesis` is the most practical choice. + +Each property test runs a minimum of 100 iterations. + +- **Feature: pkg-installer, Property 1: Installer identity extraction round trip** — Generate random valid JSON objects with an `installer_identity` string field, write to a temp file, extract via `jq -r .installer_identity`, assert output matches the original string. + +- **Feature: pkg-installer, Property 2: Output package filename follows version pattern** — Generate random semver strings (X.Y.Z where X, Y, Z are non-negative integers), construct the expected filename `wispr-X.Y.Z.pkg`, assert it matches the pattern and is rooted in `build/export/`. + +- **Feature: pkg-installer, Property 3: Marketing version injection** — Generate random semver strings and a template `.pbxproj` snippet containing `MARKETING_VERSION = ;`, run the `sed` command, assert all `MARKETING_VERSION` entries in the output equal the new version. + +- **Feature: pkg-installer, Property 4: Missing resource file detection** — For each file in the required set, create a temp `pkg/resources/` directory with that file removed, run the validation check, assert the error message contains the missing filename. + +### Integration Tests (Manual) + +Full end-to-end testing requires macOS with valid certificates and Apple credentials. These are run manually: + +1. `make pkg` — produces a valid, signed, notarized `.pkg` in `build/export/` +2. `make pkg-release VERSION=0.0.1-test` — produces the `.pkg` and uploads to a draft GitHub Release +3. Install the `.pkg` on a clean Mac — Wispr.app appears in `/Applications`, Gatekeeper accepts it +4. `pkgutil --check-signature ` — shows valid Developer ID Installer signature +5. `spctl -a -vvv -t install ` — shows accepted by Gatekeeper diff --git a/.kiro/specs/pkg-installer/requirements.md b/.kiro/specs/pkg-installer/requirements.md new file mode 100644 index 0000000..a7f687d --- /dev/null +++ b/.kiro/specs/pkg-installer/requirements.md @@ -0,0 +1,116 @@ +# Requirements Document + +## Introduction + +Wispr is distributed via the App Store and Homebrew. This feature adds a third distribution channel: a signed and notarized Apple `.pkg` installer with a custom installer UI. The `.pkg` installs Wispr.app to `/Applications` and is uploaded to GitHub Releases alongside the existing Homebrew zip. The build flow reuses the existing archive and notarization pipeline and extends the Makefile with new targets. + +## Glossary + +- **Build_Pipeline**: The set of Makefile targets that archive, sign, notarize, and release the Wispr app +- **Component_Pkg**: An intermediate `.pkg` file created by `pkgbuild` containing the app bundle and install location metadata +- **Product_Pkg**: The final user-facing `.pkg` installer created by `productbuild` from a Component_Pkg and a distribution XML, with custom installer UI screens +- **Distribution_XML**: An XML file that defines the Product_Pkg installer flow, including title, background, welcome, readme, license, and install choices +- **Installer_Resources**: Static files (background image, welcome text, readme text, license) displayed during the Product_Pkg installation wizard +- **Installer_Identity**: The "Developer ID Installer" signing certificate used to sign `.pkg` files for distribution outside the App Store +- **Notarization_Config**: The `secrets/notarization.json` file containing Apple ID, team ID, and signing identity values used by the Build_Pipeline +- **GitHub_Release**: A versioned release on GitHub containing downloadable artifacts (zip, pkg) and auto-generated release notes + +## Requirements + +### Requirement 1: Build Component Package + +**User Story:** As a developer, I want to create a component `.pkg` from the notarized app bundle, so that I have the building block for the final installer. + +#### Acceptance Criteria + +1. WHEN the `make pkg` target is invoked, THE Build_Pipeline SHALL archive and notarize the app using the existing `notarize` target before creating the Component_Pkg +2. WHEN the notarized app is available, THE Build_Pipeline SHALL invoke `pkgbuild` to create a Component_Pkg that installs Wispr.app to `/Applications` +3. THE Build_Pipeline SHALL set the Component_Pkg bundle identifier to `com.stormacq.mac.wispr` and the version to the current marketing version +4. IF `pkgbuild` exits with a non-zero status, THEN THE Build_Pipeline SHALL print an error message and stop execution + +### Requirement 2: Build Product Package with Custom Installer UI + +**User Story:** As a developer, I want to wrap the component package into a product installer with branded UI screens, so that users see a polished installation experience. + +#### Acceptance Criteria + +1. WHEN the Component_Pkg is available, THE Build_Pipeline SHALL invoke `productbuild` with the Distribution_XML and Installer_Resources to create the Product_Pkg +2. THE Distribution_XML SHALL define a single choice element that installs the Wispr component to `/Applications` +3. THE Distribution_XML SHALL reference a background image, a welcome screen, a readme screen, and a license screen from the Installer_Resources +4. THE Installer_Resources SHALL include a background image that uses the Wispr project color palette +5. THE Installer_Resources SHALL include a welcome HTML or text file introducing Wispr and its key features +6. THE Installer_Resources SHALL include a readme HTML or text file describing system requirements and post-install steps +7. THE Installer_Resources SHALL include the project license file (Apache License 2.0) +8. IF `productbuild` exits with a non-zero status, THEN THE Build_Pipeline SHALL print an error message and stop execution + +### Requirement 3: Sign the Product Package + +**User Story:** As a developer, I want the final `.pkg` to be signed with my Developer ID Installer certificate, so that macOS Gatekeeper accepts the installer. + +#### Acceptance Criteria + +1. WHEN the Product_Pkg is created, THE Build_Pipeline SHALL sign the Product_Pkg using `productsign` with the Installer_Identity +2. THE Build_Pipeline SHALL read the Installer_Identity name from the Notarization_Config file +3. IF the Installer_Identity is not found in the keychain, THEN THE Build_Pipeline SHALL print an error message identifying the missing certificate and stop execution +4. IF `productsign` exits with a non-zero status, THEN THE Build_Pipeline SHALL print an error message and stop execution + +### Requirement 4: Notarize and Staple the Product Package + +**User Story:** As a developer, I want the signed `.pkg` to be notarized and stapled by Apple, so that users can install it without Gatekeeper warnings. + +#### Acceptance Criteria + +1. WHEN the Product_Pkg is signed, THE Build_Pipeline SHALL submit the signed Product_Pkg to Apple notarization via `notarytool` using the existing API key credentials +2. THE Build_Pipeline SHALL wait for the notarization result before proceeding +3. WHEN notarization succeeds, THE Build_Pipeline SHALL staple the notarization ticket to the signed Product_Pkg using `stapler` +4. WHEN stapling is complete, THE Build_Pipeline SHALL verify the Product_Pkg with `spctl` and print the verification result +5. IF notarization fails, THEN THE Build_Pipeline SHALL print the notarization log URL and stop execution +6. IF stapling fails, THEN THE Build_Pipeline SHALL print an error message and stop execution + +### Requirement 5: Makefile `pkg` Target + +**User Story:** As a developer, I want a single `make pkg` command that builds, signs, notarizes, and staples the `.pkg` installer, reusing as much of the existing Makefile infrastructure as possible, so that I can produce the artifact in one step without duplicating build logic. + +#### Acceptance Criteria + +1. THE Build_Pipeline SHALL provide a `pkg` Makefile target that executes the full flow: archive, notarize app, build Component_Pkg, build Product_Pkg, sign Product_Pkg, notarize Product_Pkg, and staple Product_Pkg +2. WHEN `make pkg` completes successfully, THE Build_Pipeline SHALL output the final Product_Pkg to the `build/export/` directory with the filename `wispr-.pkg` +3. THE Build_Pipeline SHALL print a summary line with the path to the final Product_Pkg on successful completion +4. THE `pkg` target SHALL reuse the existing `notarize` target for the app archive and signing step +5. THE `pkg` target SHALL reuse the existing `_setup-api-key` and `_cleanup-api-key` targets for API key management +6. THE `pkg` target SHALL reuse the existing Makefile variables (`SIGNING_IDENTITY`, `API_KEY_PATH`, `API_KEY_ID`, `API_ISSUER`, `APP_PATH`, `EXPORT_DIR`, `ARCHIVE_PATH`) rather than redefining them +7. THE `pkg` target SHALL NOT duplicate any archive, app-signing, or notarization logic that already exists in the `notarize` target + +### Requirement 6: Makefile `pkg-release` Target + +**User Story:** As a developer, I want a `make pkg-release VERSION=x.y.z` command that builds the `.pkg` and uploads it to GitHub Releases, so that users can download the installer from the releases page. + +#### Acceptance Criteria + +1. WHEN `make pkg-release VERSION=x.y.z` is invoked, THE Build_Pipeline SHALL set the marketing version to the provided VERSION value in the Xcode project +2. THE Build_Pipeline SHALL invoke the `pkg` target to produce the signed and notarized Product_Pkg +3. WHEN the Product_Pkg is ready, THE Build_Pipeline SHALL create or update a GitHub Release tagged `v` and upload the Product_Pkg as a release asset +4. THE Build_Pipeline SHALL upload the Product_Pkg alongside any existing release assets without removing them +5. IF the VERSION parameter is not provided, THEN THE Build_Pipeline SHALL print a usage message and stop execution +6. IF the `gh` CLI tool is not installed, THEN THE Build_Pipeline SHALL print an error message and stop execution + +### Requirement 7: Installer Identity Configuration + +**User Story:** As a developer, I want the installer signing identity stored in the existing secrets configuration, so that the build pipeline can reference it consistently. + +#### Acceptance Criteria + +1. THE Notarization_Config SHALL include an `installer_identity` field containing the Developer ID Installer certificate name +2. THE Build_Pipeline SHALL read the `installer_identity` value from the Notarization_Config using `jq` +3. IF the `installer_identity` field is missing from the Notarization_Config, THEN THE Build_Pipeline SHALL print an error message specifying the expected field name and stop execution + +### Requirement 8: Installer Resource Files + +**User Story:** As a developer, I want the installer resource files stored in a dedicated directory in the repository, so that they are version-controlled and easy to maintain. + +#### Acceptance Criteria + +1. THE Build_Pipeline SHALL read Installer_Resources from a `pkg/resources/` directory at the repository root +2. THE Build_Pipeline SHALL read the Distribution_XML from `pkg/distribution.xml` at the repository root +3. THE `pkg/resources/` directory SHALL contain the background image, welcome file, readme file, and license file +4. WHEN any required Installer_Resources file is missing, THE Build_Pipeline SHALL print an error message listing the missing file and stop execution diff --git a/.kiro/specs/pkg-installer/tasks.md b/.kiro/specs/pkg-installer/tasks.md new file mode 100644 index 0000000..3248ce5 --- /dev/null +++ b/.kiro/specs/pkg-installer/tasks.md @@ -0,0 +1,146 @@ +# Implementation Plan: pkg-installer + +## Overview + +Extend the existing Makefile build pipeline with `pkg` and `pkg-release` targets that produce a signed, notarized `.pkg` installer with custom UI. All installer resources (`pkg/distribution.xml`, `pkg/resources/`) are created as static files. The `secrets/notarization.json` schema is extended with an `installer_identity` field. Property-based tests use Python/Hypothesis. + +## Tasks + +- [ ] 1. Create installer resource files and distribution XML + - [ ] 1.1 Create `pkg/resources/license.txt` by copying the repo root `LICENSE` file (Apache 2.0) + - _Requirements: 2.7, 8.1, 8.3_ + + - [ ] 1.2 Create `pkg/resources/welcome.html` introducing Wispr and its key features + - Brief HTML page: what Wispr is, on-device speech-to-text, privacy focus + - Reference `README.md` for content inspiration + - _Requirements: 2.5, 8.1, 8.3_ + + - [ ] 1.3 Create `pkg/resources/readme.html` with system requirements and post-install steps + - macOS 15.0+, microphone permission, model download on first launch + - _Requirements: 2.6, 8.1, 8.3_ + + - [ ] 1.4 Create `pkg/resources/background.png` placeholder + - Create a simple branded background image (660×440 PNG) using the Wispr project color palette + - Reference `artwork/icon.svg` and `artwork/icon-square.svg` for branding + - _Requirements: 2.4, 8.1, 8.3_ + + - [ ] 1.5 Create `pkg/distribution.xml` defining the installer flow + - Single choice element installing Wispr to `/Applications` + - Reference background, welcome, readme, and license from resources + - `customize="never"`, `require-scripts="false"` + - `pkg-ref` identifier `com.stormacq.mac.wispr` + - Use the exact XML structure from the design document + - _Requirements: 2.1, 2.2, 2.3, 8.2_ + +- [ ] 2. Checkpoint — Verify installer resources + - Ensure all four resource files exist in `pkg/resources/` and `pkg/distribution.xml` is valid XML. Ask the user if questions arise. + +- [ ] 3. Extend Makefile with `pkg` target + - [ ] 3.1 Add new Makefile variables for the pkg pipeline + - `INSTALLER_IDENTITY` read from `$(NOTARIZATION_JSON)` via `jq -r .installer_identity` + - `COMPONENT_PKG`, `PRODUCT_PKG`, `SIGNED_PKG`, `FINAL_PKG` derived paths in `$(EXPORT_DIR)` + - `PKG_RESOURCES` pointing to `$(CURDIR)/pkg/resources` + - `DISTRIBUTION_XML` pointing to `$(CURDIR)/pkg/distribution.xml` + - Reuse all existing variables (`BUNDLE_ID`, `SIGNING_IDENTITY`, `APP_PATH`, `EXPORT_DIR`, `ARCHIVE_PATH`, `API_KEY_PATH`, `API_KEY_ID`, `API_ISSUER`, `NOTARIZATION_JSON`) + - _Requirements: 5.6, 7.1, 7.2_ + + - [ ] 3.2 Add resource file validation step to the `pkg` target recipe + - Check that `pkg/distribution.xml`, `pkg/resources/background.png`, `pkg/resources/welcome.html`, `pkg/resources/readme.html`, `pkg/resources/license.txt` all exist + - Print error message naming the missing file and `exit 1` if any is absent + - _Requirements: 8.4_ + + - [ ] 3.3 Add `installer_identity` validation step + - Read `INSTALLER_IDENTITY` from `$(NOTARIZATION_JSON)` and verify it is non-empty + - Print `Error: installer_identity not found in ` and `exit 1` if missing + - _Requirements: 7.2, 7.3_ + + - [ ] 3.4 Add `pkgbuild` step to create the component package + - `pkgbuild --root --install-location /Applications --identifier $(BUNDLE_ID) --version $(VERSION) $(COMPONENT_PKG)` + - Error handling: `|| { echo "Error: pkgbuild failed"; exit 1; }` + - _Requirements: 1.1, 1.2, 1.3, 1.4_ + + - [ ] 3.5 Add `productbuild` step to create the product package with custom UI + - `productbuild --distribution $(DISTRIBUTION_XML) --resources $(PKG_RESOURCES) --package-path $(EXPORT_DIR) $(PRODUCT_PKG)` + - Error handling: `|| { echo "Error: productbuild failed"; exit 1; }` + - _Requirements: 2.1, 2.8_ + + - [ ] 3.6 Add `productsign` step to sign the product package + - `productsign --sign "$(INSTALLER_IDENTITY)" $(PRODUCT_PKG) $(SIGNED_PKG)` + - Error handling: `|| { echo "Error: productsign failed"; exit 1; }` + - _Requirements: 3.1, 3.2, 3.4_ + + - [ ] 3.7 Add notarization, stapling, and verification steps for the signed package + - Reuse `_setup-api-key` and `_cleanup-api-key` for API key management + - `notarytool submit` with `--key`, `--key-id`, `--issuer`, `--wait` + - `stapler staple` on the signed package + - `spctl -a -vvv -t install` to verify + - Error handling for notarization (print log URL), stapling, and verification failures + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 5.5_ + + - [ ] 3.8 Add final rename and summary output + - Rename signed+stapled package to `wispr-$(VERSION).pkg` in `$(EXPORT_DIR)` + - Print summary line with the path to the final package + - Wire the `pkg` target to depend on `notarize` (existing target) + - Ensure `pkg` does NOT duplicate any archive, app-signing, or notarization logic from `notarize` + - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.7_ + +- [ ] 4. Checkpoint — Verify `pkg` target structure + - Ensure the `pkg` target is defined, depends on `notarize`, and does not duplicate existing logic. Ask the user if questions arise. + +- [ ] 5. Extend Makefile with `pkg-release` target + - [ ] 5.1 Add `pkg-release` target with VERSION validation and `gh` CLI check + - Print usage message and `exit 1` if `VERSION` is not provided + - Print error and `exit 1` if `gh` CLI is not installed + - _Requirements: 6.5, 6.6_ + + - [ ] 5.2 Add version setting and `pkg` invocation + - Set `MARKETING_VERSION` in the Xcode project via `sed` (mirror `brew-release` pattern) + - Call `make pkg` to produce the signed and notarized package + - _Requirements: 6.1, 6.2_ + + - [ ] 5.3 Add GitHub Release creation and asset upload + - Create or update GitHub Release tagged `v` + - Upload `.pkg` as release asset alongside existing assets (do not remove them) + - Mirror the `brew-release` pattern for `gh release create` / `gh release upload` + - _Requirements: 6.3, 6.4_ + +- [ ] 6. Document `installer_identity` field in `secrets/notarization.json` + - Add a comment or update project documentation noting the new `installer_identity` field + - Provide the expected JSON schema: `"installer_identity": "Developer ID Installer: [name] ([team_id])"` + - _Requirements: 7.1_ + +- [ ] 7. Checkpoint — Verify full Makefile integration + - Ensure `pkg` and `pkg-release` targets are defined and follow existing Makefile patterns. Ensure all tests pass, ask the user if questions arise. + +- [ ] 8. Property-based tests (Python/Hypothesis) + - [ ]* 8.1 Write property test for installer identity extraction round trip + - **Property 1: Installer identity extraction round trip** + - Generate random valid JSON with a non-empty `installer_identity` string, write to temp file, extract via `jq -r .installer_identity`, assert output matches original + - **Validates: Requirements 3.2, 7.2** + + - [ ]* 8.2 Write property test for output package filename version pattern + - **Property 2: Output package filename follows version pattern** + - Generate random semver strings `X.Y.Z`, construct expected filename `wispr-X.Y.Z.pkg`, assert it matches the pattern and is rooted in `build/export/` + - **Validates: Requirements 5.2** + + - [ ]* 8.3 Write property test for marketing version injection + - **Property 3: Marketing version injection** + - Generate random semver strings and a `.pbxproj` snippet with `MARKETING_VERSION = ;`, run the `sed` substitution, assert all `MARKETING_VERSION` entries equal the new version + - **Validates: Requirements 6.1** + + - [ ]* 8.4 Write property test for missing resource file detection + - **Property 4: Missing resource file detection** + - For each file in {`background.png`, `welcome.html`, `readme.html`, `license.txt`}, create a temp `pkg/resources/` with that file removed, run the validation check, assert error message contains the missing filename + - **Validates: Requirements 8.4** + +- [ ] 9. Final checkpoint — Ensure all tests pass + - Ensure all tests pass, ask the user if questions arise. + +## Notes + +- Tasks marked with `*` are optional and can be skipped for faster MVP +- The user is responsible for obtaining the Developer ID Installer certificate +- The `background.png` in task 1.4 may need manual refinement for production quality +- Property tests use Python/Hypothesis and can be run with `pytest` +- Each task references specific requirements for traceability +- Checkpoints ensure incremental validation