Skip to content

StorageScope v0.4.0 — Polish, notarize scaffolding, audit hardening, contributor docs

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 21 Jun 04:13
0868167

v0.4.0 — Polish, cache maintenance, contributor docs, notarize scaffolding

Version-worth of polish + reliability work since v0.3.0 (~14 PRs). All changes are backwards-compatible additions and refactors; no breaking changes.

Polish (SwiftUI helper extensions)

Two new View extensions in Sources/StorageScope/Support/CardBackground.swift standardize repeated background patterns that had drifted across the view layer.

  • cardBackground(_:radius:) (#26) — replaces 22 hard-coded .background(.regularMaterial/.thinMaterial, in: RoundedRectangle(cornerRadius: 8)) call sites across 9 view files. Default radius: 8 matches the shipping pattern.
  • selectionBackground(isSelected:tint:radius:) (#31) — replaces 4 hard-coded Color.accentColor.opacity(0.X) : Color.clear highlight patterns across CleanupReviewView, TreeExplorerView, SidebarView, StorageItemTable. Preserves per-site tint (0.18 default, 0.24 sidebar) and radius (nil for list rows, 6 sidebar, 8 cards).

Bundle polish (#20) tidied three smaller things in one PR:

  • TrashReviewActions struct — collapses TrashConfirmationSheet's four-closure init into a single value type
  • String.nonEmpty consolidation — removed three duplicate private extensions, single shared internal extension String lives in StorageFormat.swift alongside the existing formatter helpers
  • Cached relative-date rendering in recent-scan sidebar rows — RecentScanRow view caches the .formatted(.relative(presentation: .named)) string via @State populated from .onAppear, instead of computing on every body evaluation

Duplicate-hash cache maintenance (#21)

DuplicateHashCache.purgeStale(except:) drops entries whose underlying files no longer exist (or whose paths become unreadable) after every scan. Wired into ScanStore's existing detached Task.detached(priority: .utility) persist step so the purge runs alongside hashCache.persist() without blocking the main actor. Returns the dropped count.

Fixes the slow leak from prior versions where deleted/moved files' hash entries lingered in ~/Library/Caches/StorageScopeDuplicateHashCache.json until the 10,000-entry cap evicted the oldest-mtime ones, with no way for the cache to self-cleanse. The Clear Cache button in Settings (added in v0.3.0) remains the heavy hammer; purgeStale is now the automatic background counterpart.

Public-upload audit hardening (#22, #23)

script/public_upload_audit.sh gained two real fixes:

  1. POSIX grep portability. The script's secret-detection loop previously called rg (ripgrep) with || true. macOS GitHub Actions runners don't ship ripgrep by default, and || true was silently swallowing the resulting "command not found" exit code alongside the "no matches" exit code — so the entire secret-detection step had been no-oping in CI since the script was introduced. Replaced rg -I -n -i with POSIX grep -E -H -n -i (universally available, same extended regex syntax). Real hardcoded secrets (sk-proj-..., ghp_...) now trip the audit as intended; the || true correctly covers only the "no matches" exit.

  2. sk- regex tightened. sk-[A-Za-z0-9]+sk-[A-Za-z0-9]{20,}. The old pattern false-positived on docs/macos-disk-space-analyzer.html (where "sk-s" matched with no word boundary) and similar pages. The {20,} minimum matches real OpenAI keys (40+ chars after sk-) while dropping the false positive.

  3. Env var reference allowlist. PR #19 added references to APP_STORE_CONNECT_API_KEY_ID, APP_STORE_CONNECT_API_ISSUER_ID, APP_STORE_CONNECT_API_KEY_FILEPATH in script/notarize_dmg.sh and CONTRIBUTING.md. These match the api[_-]?key regex but carry no checked-in secret value — they're declarations and references to credentials the maintainer supplies in the local shell. Added safe_env_var_ref_patterns and a post-loop filter step that drops lines matching those patterns before the audit fails. Word-boundary anchors on the variable names preserve the precision: any line mentioning APP_STORE_CONNECT_API_KEY_ID as a word is safe; hardcoded secrets still trip.

Notarize script scaffolding (#19)

script/notarize_dmg.sh — new maintainer-only script submitting a Developer ID-signed DMG to Apple's notary service via xcrun notarytool submit, stapling the result via xcrun stapler staple, and validating the final DMG. Reads App Store Connect credentials only from environment variables (APP_STORE_CONNECT_API_KEY_ID, APP_STORE_CONNECT_API_ISSUER_ID, APP_STORE_CONNECT_API_KEY_FILEPATH) — never from CLI flags or repo files — and redacts the key id and issuer id when echoing. Detects whether the input DMG is ad-hoc signed (the current default from script/export_dmg.sh) and exits with guidance to re-export with STORAGESCOPE_SIGN_IDENTITY="Developer ID Application: ..." set.

CONTRIBUTING.md gains a ## Notarization (for maintainers) section covering one-time ASC API key setup, required env vars, Developer ID-signed DMG production, script invocation, and credential rotation guidance.

.gitignore adds *.p8 and AuthKey_*.p8 so App Store Connect API keys cannot be committed.

This unblocks the actual notarization pipeline for when an Apple Developer ID certificate is in hand — it does not perform notarization itself, which still requires Developer ID credentials.

Contributor docs (#28)

CONTRIBUTING.md gains a ## Developer Fixtures section documenting ./script/build_and_run.sh --fixture-scan and the eight optional flags that customize the launched app's starting state (--duplicates, --mark-stale, --view, --select-verified-cleanup, --query, --size-filter, --sort, --cleanup-lane). Documents the fixture root layout, four example workflows, and the SmartView raw values usable with --view.

The fixture-scan mode landed in PRs #1, #5, #6, and #12 across v0.1.0-v0.3.0; contributor docs never caught up until now.

Test coverage depth (#18)

verifySizeGroupSurfacesExactMatchesAcrossMultiDirFixture — new stress-test for the v0.3.0-shipped FileSystemScanner.verifySizeGroup(_:cancellation:) API. Five-sibling-directory fixture with mixed same-content patterns: common-content group (5 files, all 5 dirs verified), partial-content group (3 files, only dir0/dir1/dir2 verified; dir3/dir4 stay unverified at different content), unique-content per dir (never verified). Asserts exactly 2 verified groups returned, matching sanity checks on group membership and byte size, distinct checksums, no verified group touches the unique-content file.

Documentation site

The static documentation site under docs/ gained several maintainer contributions during the v0.4.0 cycle:

  • Shared site header/footer + landing page content expansion (#25)
  • Privacy and changelog pages, linked from the site footer (#27)
  • Article and Breadcrumb JSON-LD structured data + walkthrough cards on guides (#29)
  • FAQ page with FAQPage JSON-LD, linked from every page footer (#30)
  • v0.3.0 DMG download surface surfacing (#24)

Why this release is minor (not major, not patch)

Per semver: pre-1.0 minor is the right call versus patch because the changes add new functionality (notarize scaffolding, hash cache purge, parallel-scan property test) rather than only fixing bugs. Versus major because there are no breaking API changes to FileSystemScanner.scan(...) or ScanStore's public surface — every change behaves the same way for existing callers.

Deferred from v0.4.0

  • Sparkle 2.x auto-update integration — investigation only, no code shipped. Technically compatible with StorageScope's SwiftPM-only build and the local-first privacy posture (set SUSendSystemProfile=false, appcast over HTTPS, the framework makes no calls beyond the feed fetch + archive download). But it provides zero user-trustworthy value while DMGs are ad-hoc signed — Sparkle auto-update only delivers trustworthy updates when archives are Developer-ID-signed AND notarized. Deferred to v0.5.0 / v0.6.0, gated on the notarization pipeline (PR #19's scaffolding) being live first. Full integration shape documented in maintainer notes for that spike.
  • ScanStore split into focused @Observable types — the v0.4.0 plan originally called for splitting ScanStore into ScanState / FilterState / SelectionState and migrating to the @Observable macro. Deferred because the migration has real SwiftUI-binding surface risks (the @Published didSet observers don't translate cleanly) that I shouldn't ship solo against no UI test net. Candidate for v0.5.0 as an isolated research-session refactor.
  • Swift 6 strict concurrency spike — bumped swift-tools-version to 6.0 and fixing the resulting strict-concurrency errors (including the implicit-self-in-closure warning the v0.3.0 parallel scanner introduced) deferred until after the ScanStore split lands, since both touch the same files.

How to upgrade

Existing users on v0.3.0 or earlier can move directly to v0.4.0. No settings migrations, no backup steps required. The hash cache persists unchanged.

Building from source

bash ./script/build_and_run.sh    # build app bundle, sign ad-hoc, launch
./script/export_dmg.sh             # export a local DMG (ad-hoc signed)

Verification

  • swift test — 54 tests across 5 suites pass
  • plutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy — clean
  • bash -n across all 5 scripts — clean
  • ./script/public_upload_audit.sh — passes (and now actually runs in CI for the first time)
  • bash ./script/build_and_run.sh — builds, signs, launches the app