diff --git a/scripts/notarize-and-package.sh b/scripts/notarize-and-package.sh index aa8c8a6..321459f 100755 --- a/scripts/notarize-and-package.sh +++ b/scripts/notarize-and-package.sh @@ -29,8 +29,11 @@ # Default: NotarizationProfile # NOTARIZATION_PASSWORD Fallback app-specific password when the # keychain profile is absent. -# APP_VERSION Override CFBundleShortVersionString read -# from Info.plist (e.g. 0.0.1). +# APP_VERSION Override the release version (e.g. 0.0.1). +# Default: MARKETING_VERSION resolved from +# the Release build settings. (Info.plist is +# NOT read directly — it stores the +# unexpanded $(MARKETING_VERSION) variable.) # RELEASE_LABEL Optional suffix appended to filenames only # (not to the pkg version). E.g. "alpha" # produces InterlinedList-0.0.1-alpha.pkg. @@ -101,10 +104,28 @@ EOF : "${INSTALLER_IDENTITY:?INSTALLER_IDENTITY is required (Developer ID Installer). $(usage)}" # ─── Version + artifact names ──────────────────────────────────────────────── +# `App/Resources/Info.plist` stores CFBundleShortVersionString as the build +# variable `$(MARKETING_VERSION)`, which Xcode expands only at build time. +# Reading that file directly (PlistBuddy) yields the literal string +# `$(MARKETING_VERSION)` rather than a version number, which would then flow +# into the artifact filenames and into `pkgbuild --version`. Resolve the +# setting through xcodebuild so a real number is used. if [[ -z "${APP_VERSION:-}" ]]; then - APP_VERSION="$(/usr/libexec/PlistBuddy \ - -c 'Print :CFBundleShortVersionString' \ - "$ROOT_DIR/App/Resources/Info.plist")" + APP_VERSION="$(xcodebuild \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration "$CONFIGURATION" \ + -showBuildSettings 2>/dev/null \ + | awk -F' = ' '/[[:space:]]MARKETING_VERSION = /{gsub(/^[ \t]+/,"",$2); print $2; exit}')" +fi + +# Fail loudly rather than producing a release named after an unexpanded build +# variable (or an empty string) — both yield unusable artifacts, and the +# failure would otherwise surface only after a full archive + notarization. +if [[ -z "$APP_VERSION" || "$APP_VERSION" == *'$('* ]]; then + echo "!! Could not resolve a release version (got: '${APP_VERSION}')." >&2 + echo " Pass one explicitly, e.g. APP_VERSION=0.1.0 $(basename "$0")" >&2 + exit 1 fi if [[ -n "$RELEASE_LABEL" ]]; then diff --git a/work-consolidation.md b/work-consolidation.md index 751061b..a6cd443 100644 --- a/work-consolidation.md +++ b/work-consolidation.md @@ -2,9 +2,9 @@ **Single source of truth for remaining work, in execution order.** This file consolidates and replaces six older docs (see [Provenance](#provenance)): the parity gap lists, the backend-blocker index + paste-ready prompts, the Document Sync Agent plan, and the v1 release checklist. -- **Consolidated:** 2026-08-15 · **Last synced to code:** 2026-09-02 · **Branch:** `dev` (⚠️ `main` is behind at the 2026-08-17 state — 30 commits back; catch it up before a release cut) · **Bundle:** `com.interlinedlist.macos` · **Team:** `BJA9558E4B` +- **Consolidated:** 2026-08-15 · **Last synced to code:** 2026-09-05 · **Branch:** `dev` (⚠️ `main` is behind — catch it up before a release cut) · **Bundle:** `com.interlinedlist.macos` · **Team:** `BJA9558E4B` - **Structure:** [§1 Immediate work (do now)](#1-immediate-work--unblocked-do-now) → [§2 Blocked work (backend / spike-first)](#2-blocked-work--backend-gated-or-spike-first) → [§3 Final work (release & App Store)](#3-final-work--release--app-store) -- **Test baseline (all green, re-run 2026-09-02):** SyncAgent **53** · InterlinedKit **317** · InterlinedDomain **628** · InterlinedPersistence **135** · App target **`** TEST SUCCEEDED **`** (≈611 tests, static count). Packages verified this session under plain `swift test`; the App target ran green under `xcodebuild test … CODE_SIGNING_ALLOWED=NO` (the signing override is still required on this machine). *(Prior 2026-08-16 baseline was Kit 286 / Domain 590 / App 566; growth is from G4 GitHub issues, sharing collaborators/invites/visibility, and timeline cross-post links.)* +- **Test baseline (all green, re-run 2026-09-05):** InterlinedKit **314** · InterlinedDomain **619** · InterlinedPersistence **135** · App target **622** (`** TEST SUCCEEDED **`). *(Kit/Domain dipped from 317/628 because PR #19 removed the List Folders endpoint + service tests along with the feature.)* Packages verified this session under plain `swift test`; the App target ran green under `xcodebuild test … CODE_SIGNING_ALLOWED=NO` (the signing override is still required on this machine). *(Prior 2026-08-16 baseline was Kit 286 / Domain 590 / App 566; growth is from G4 GitHub issues, sharing collaborators/invites/visibility, and timeline cross-post links.)* - **Distribution model:** notarized **`.pkg`** (+ `.dmg`) is the **current** ship path (closed-source private repo, no `LICENSE`). Mac App Store is a **later** path on a separate branch. Billing is handled by the web app — the native app has **no** in-app-purchase surface; it only *reads* `customerStatus` to gate subscriber features. --- @@ -27,7 +27,7 @@ Milestones **M0–M7** feature work is complete; post-milestone items NW-1…NW- - **Sharing collaborators / invites / visibility** (PR #13, merged 2026-09-02) — extends the G3 sharing group: per-person document collaborators (search/add/set-role/remove), email invites for lists **and** documents, and a make-public visibility toggle. Full stack (Kit `SharingEndpoint`/`SharingDTO`, Domain `Sharing` models + `SharingService`, App `DocumentCollaborators*`/`Invites*`/`Visibility*` views + VMs) with Kit/Domain/App tests. Create paths are subscriber-gated. - **Timeline cross-post destination links** (PR #14, merged 2026-09-02) — a message row links out to where it was cross-posted (Bluesky/Mastodon/X/LinkedIn external URLs) via the Domain `Message` cross-post projection + mappers. -**Where we are now:** §1 (client-side parity) is effectively **exhausted** — every unblocked gap is built and merged; only tiny follow-ups remain (the G14 `ImagePrep` size-ceiling tail, and G4's two backend-blocked routes). The remaining levers are **§2** (backend-gated — nothing more buildable from the client until the backend moves; the one high-impact item is [P1-G](#p1-g-following-feed)) and **§3** (release engineering — the PKG/DMG ship path in [§3a](#3a-notarized-pkgdmg-release--the-current-ship-path), which is now the critical path to shipping). **Next phase: release engineering.** +**Where we are now:** §1 (client-side parity) is **exhausted** — every unblocked gap is built and merged, the G14 `ImagePrep` tail included (verified 2026-09-05). The only open §1-adjacent thread is G4's two backend-blocked routes, which are not a client gap. The remaining levers are **§2** (backend-gated — nothing more buildable from the client until the backend moves; the one high-impact item is [P1-G](#p1-g-following-feed)) and **§3** (release engineering — the PKG/DMG ship path in [§3a](#3a-notarized-pkgdmg-release--the-current-ship-path), which is now the critical path to shipping). **Next phase: release engineering.** --- @@ -35,7 +35,7 @@ Milestones **M0–M7** feature work is complete; post-milestone items NW-1…NW- Everything here is client-side and buildable today (the backend already exists or none is needed). Ordered by value. -> **Status 2026-09-02 — §1 is effectively done.** Every item below is built and merged to `dev`. The only open threads are (a) the small **G14** `ImagePrep` size-ceiling follow-up, and (b) **G4**'s issue **update**/**comment** routes, which are backend-blocked ([P1-H2](#p1-h2-github-issue-update-comment-routes)) — not a client gap. No further client-only parity work remains; pick up **§3 release** next. +> **Status 2026-09-05 — §1 is DONE.** Every item below is built and merged to `dev`, including the **G14** `ImagePrep` size-ceiling tail (verified in code 2026-09-05 — see G14). The one remaining open thread is **G4**'s issue **update**/**comment** routes, which are backend-blocked ([P1-H2](#p1-h2-github-issue-update-comment-routes)) — not a client gap. No client-only parity work remains; **§3 release is the critical path**. ### 1a. Parity features buildable now @@ -65,7 +65,7 @@ The `/api/github/*` routes are deployed (`GET /api/github/repos` → 400 "GitHub **G11a · LinkedIn posting target** — ✅ **Target-aware toggle shipped 2026-08-15.** `LinkedInService` is now wired into `AppEnvironment`; enabling the composer's LinkedIn cross-post toggle fetches `postingTargets()` and shows **which destination the post publishes to** ("Posting as …"), rolls the toggle back with a connect hint when the account has no LinkedIn target, and surfaces the org-scope-missing note — all mirroring the Bluesky/Mastodon readiness pattern and reusing the verified `crossPostToLinkedIn` request path. 6 composer tests. **Deferred (needs a verified wire shape):** a true multi-*target selector* and the `POST /api/linkedin/sync-pages` refresh both wait on a confirmed per-target request field; LinkedIn **org** pages are upstream-blocked (G11b). -**G14 · `/api/limits` composer validation** — ✅ **Message-length validation shipped 2026-08-15.** New Kit `Limits` endpoint + `LimitsDTO`; domain `ContentLimits` model + `ContentLimitsService` (fetch with `ContentLimits.default` fallback); wired into the composer as a live character counter + publish gate (over-limit disables Post, turns the counter/border red). 13 tests (4 Kit + 4 Domain + 5 App). **Remaining follow-up (smaller):** feed the same `ContentLimits` into `ImagePrep` so the image/video *size* ceilings are server-driven too — today `ImagePrep` keeps the matching hard-coded constants (which equal the live values). +**G14 · `/api/limits` composer validation** — ✅ **Message-length validation shipped 2026-08-15.** New Kit `Limits` endpoint + `LimitsDTO`; domain `ContentLimits` model + `ContentLimitsService` (fetch with `ContentLimits.default` fallback); wired into the composer as a live character counter + publish gate (over-limit disables Post, turns the counter/border red). 13 tests (4 Kit + 4 Domain + 5 App). **Tail also shipped — verified 2026-09-05.** The media *size* ceilings are server-driven end to end: `ImagePrep.Limits` + `prepare(_:limits:)` replace the hard-coded constants (which remain only as `Limits.default`), `ContentLimits.imagePrepLimits` projects the live values in, and both `MessagesService.uploadImage` (`MessagesService.swift:654`) and `DocumentsService.uploadImage` (`DocumentsService.swift:300`) pass them; `uploadVideo` (`MessagesService.swift:668`) uses the live `videoMaxBytes` with the static `maxVideoBytes` as fallback. `AppEnvironment` injects `ContentLimitsService` into both services. Covered by `ImagePrepTests` (custom-limits cases) and `MessagesServiceM6Tests` (live-limit override). ### 1b. Client-side follow-ups & polish (no backend) @@ -186,6 +186,14 @@ Ship gating is orthogonal to parity and can proceed in parallel with §1/§2. De One-time signing setup on the build machine, then the release run. Complete in order. +> **Pre-flight audit — 2026-09-05.** The pipeline was reviewed end-to-end *before* the first credentialed run, since every failure mode here only surfaces after a long archive + notarization cycle. Verified sound: all 8 `scripts/*.sh` parse (`bash -n`), `ExportOptions.plist` is `developer-id` + automatic signing, the sync-agent embed paths and both entitlements files exist, the Sparkle code path is fully wired (`SparkleController` + `UpdatesMenuCommands`, `SUFeedURL` set), and the appcast's `minimumSystemVersion` **15.0** matches `MACOSX_DEPLOYMENT_TARGET`. +> +> **One blocking bug found and fixed:** `notarize-and-package.sh` derived the release version by reading `CFBundleShortVersionString` straight out of `App/Resources/Info.plist` — but that file stores the *unexpanded* build variable `$(MARKETING_VERSION)`, so PlistBuddy returned the literal string. Every artifact would have been named `InterlinedList-$(MARKETING_VERSION).pkg` and `pkgbuild --version` would have received that same garbage. It now resolves `MARKETING_VERSION` via `xcodebuild -showBuildSettings` and hard-fails with an actionable message if the value is empty or still contains `$(`. Verified: default → `0.1.0`, explicit `APP_VERSION` honoured, guard exits 1. +> +> **Two decisions still open for the owner (not code bugs):** +> - **Version number is inconsistent across three places** — `MARKETING_VERSION` is **`0.1.0`**, `releases/appcast.xml` advertises **`0.0.1`** ("Version 0.0.1 Alpha", enclosure `InterlinedList-0.0.1-alpha.pkg`), and the tag step below says **`v1.0.0`**. Pick one before cutting the release; the appcast enclosure filename must match what the script actually produces or Sparkle will 404. +> - **Appcast URL** — `Info.plist`'s `SUFeedURL` points at `https://interlinedlist.com/appcast.xml` (matching the publish step below), but the comment block inside `releases/appcast.xml` documents the feed as living at `…/downloads/apple/appcast.xml`. Harmless today; reconcile so the served path and the polled path can't drift apart. + **Sparkle keys & Info.plist** - [ ] Generate the Sparkle Ed25519 key pair: `./bin/generate_keys` (store the private key in a password manager — never commit). - [ ] Paste the public key into `App/Resources/Info.plist` → `SUPublicEDKeyString` (currently `TODO_REPLACE_WITH_ED25519_PUBLIC_KEY`).