Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions scripts/notarize-and-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
18 changes: 13 additions & 5 deletions work-consolidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
Expand All @@ -27,15 +27,15 @@ 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.**

---

## 1. Immediate work — unblocked, do now

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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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`).
Expand Down