Compare commits

...
13 Commits
Author SHA1 Message Date
james.maes 634dc9f688 Merge pull request 'ci: build on the shared ci-workflows macOS workflow (build only)' (#2) from ci/shared-macos into master
macos / macos (push) Successful in 51s
build / macos (push) Successful in 51s
2026-11-08 23:33:40 +00:00
James MaesandClaude Opus 5.5 da5779f872 fix: capture weakly in the outer resolve closures (Swift 6.3 ImplicitStrongCapture)
macos / macos (push) Successful in 52s
build / macos (push) Successful in 52s
The background closures in ExceptionsTab.applyContent and
ExceptionEditorView.resolveHeaderAsync captured row/self strongly while the
inner main-queue closure asked for weak; Xcode 27's Swift diagnoses the
mismatch (#ImplicitStrongCapture), an error under warnings-as-errors. Both
closures now capture weakly, so neither keeps the row or view alive while
the app info resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:32:05 -06:00
James MaesandClaude Opus 5.5 874971aa47 ci: one-line Release build command
macos / macos (push) Failing after 17s
build / macos (push) Failing after 17s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:30:44 -06:00
James MaesandClaude Opus 5.5 756943f2b5 ci: compile the Release scheme at macOS 12.0 on Xcode 27
macos / macos (push) Failing after 2s
build / macos (push) Failing after 2s
grogu has only Xcode 27.2, which rejects the 10.13 deployment target. The
CI build is a compile check at 12.0 with the deprecation group downgraded
(as config/test-base.xcconfig); the shipping build stays on the GitHub
mirror with Xcode 26.0.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:29:44 -06:00
James MaesandClaude Opus 5.5 c675f1817f ci: select Xcode 26.0.1, as ci_cd.yml does
macos / macos (push) Failing after 1s
build / macos (push) Failing after 2s
Xcode 27.2 rejects the app's macOS 10.13 deployment target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:28:37 -06:00
James MaesandClaude Opus 5.5 c2be713446 ci: build on the shared ci-workflows macOS workflow (build only)
macos / macos (push) Failing after 6s
build / macos (push) Failing after 6s
Gitea CI on grogu (kof-macos-arm64): Guards B and C from guard.yml (the
de-paywall marker and if-false wrap in computeState(), no conflict markers),
an unsigned Release build, and build-for-testing of the Test scheme. The
tests (Guard A) are not run on CI: the runner has no GUI session for
testmanagerd; docs/contributing.md documents running scripts/run_tests.sh
locally before merging.

config/test-base.xcconfig takes the unit-test-only Xcode 27 fix from PR #1
(test target 12.0, deprecation group downgraded, every other warning still
an error); the shipping app keeps 10.13. The .github workflows stay for the
GitHub mirror.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:27:40 -06:00
semantic-release-bot dc40fa43bc chore(release): 100.2.1 [skip ci]
## [100.2.1](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.2.0...v100.2.1) (2026-06-16)
2026-06-16 21:21:18 +00:00
james.maes 2ae7ded357 docs: record auto-update live, cask v100.2.0, release-gate decouple
Reflect v100.2.0 (auto-update via the GitHub Pages feed), the Homebrew cask bump, and the will-release gating that lets non-release pushes build and test without cutting a release.
2026-06-16 16:16:42 -05:00
james.maes dd15f22031 ci: gate release apparatus on semantic-release will-release decision
determine_next_version.sh writes an empty VERSION.txt when no commit since the last tag is release-worthy; gate cert-import/sign-build/notarize/appcast/release (and version injection) on that, so non-release pushes build+test without cutting a release or needing [skip ci].
2026-06-16 16:11:32 -05:00
semantic-release-bot 4ecfc4606f chore(release): 100.2.0 [skip ci]
# [100.2.0](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.1.0...v100.2.0) (2026-06-16)

### Features

* **updates:** enable Sparkle auto-update via GitHub Pages appcast ([45457cb](https://github.com/KofTwentyTwo/CommandTabFree/commit/45457cb))
2026-06-16 21:05:45 +00:00
james.maes 45457cb313 feat(updates): enable Sparkle auto-update via GitHub Pages appcast
Point DOMAIN at the repo's GitHub Pages site (serves the gh-pages appcast.xml), enable SUEnableAutomaticChecks, and remove the in-tree appcast skeleton (feed is published out-of-tree). Ships an auto-updating build.
2026-06-16 16:02:04 -05:00
james.maes 62e0226f01 ci: stop release tail clobbering .fork-sync-state cursor [skip ci]
Reset .fork-sync-state to v11.3.0 (last-synced upstream tag) and remove the release-tail step that overwrote it with the fork release tag, which would stall upstream-sync. Update SESSION-STATE/TODO/CICD-STATUS/README for the live signed+notarized v100.1.0 release.
2026-06-16 15:32:55 -05:00
github-actions[bot] 92254c021f chore(sync-state): advance to v100.1.0 [skip ci] 2026-06-16 19:48:28 +00:00
15 changed files with 134 additions and 94 deletions

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
# Gitea CI on the shared KofTwentyTwo/ci-workflows macOS workflow (grogu). The .github workflows keep
# running on the GitHub mirror (release, notarization, appcast); this one is build only and unsigned.
#
# Guard B the de-paywall marker and the if-false wrap are still in computeState() (docs/PLAN-maintained-fork.md §4.4)
# Guard C no unresolved merge conflict markers in the tree
# build the Release scheme, unsigned (a compile check at macOS 12.0: grogu has only Xcode 27)
# Guard A the Test scheme is compiled (build-for-testing) but not run: the grogu runner has no GUI session for
# testmanagerd. Run scripts/run_tests.sh locally before merging (docs/contributing.md).
name: build
on:
push:
branches: ['**']
jobs:
macos:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/macos.yaml@v1
with:
commands: |
F=src/pro/license/LicenseManager.swift
[ -f "$F" ] || { echo "::error::Guard B: $F not found; chokepoint moved? Refresh the de-paywall patch (§6.2)."; exit 1; }
grep -q 'depaywall' "$F" || { echo "::error::PAYWALL RELOCKED: 'depaywall' marker missing from $F (§6.2)."; exit 1; }
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' "$F" || { echo "::error::PAYWALL RELOCKED: 'if false { … }' wrap missing from $F (§6.2)."; exit 1; }
echo "Guard B OK"
if git -c grep.lineNumber=false grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- . ':(exclude)docs/'; then
echo "::error::UNRESOLVED CONFLICT MARKERS present; resolve before merge (§4.4 Guard C)."; exit 1
fi
echo "Guard C OK"
# grogu has only Xcode 27.2, which rejects the app's 10.13 deployment target (the shipping build is the GitHub
# mirror's, on Xcode 26.0.1). This compile check builds at 12.0 with the same deprecation downgrade as
# config/test-base.xcconfig; every other Swift warning stays an error.
xcodebuild -quiet -project alt-tab-macos.xcodeproj -scheme Release -derivedDataPath DerivedData CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO MACOSX_DEPLOYMENT_TARGET=12.0 SWIFT_TREAT_WARNINGS_AS_ERRORS=NO 'OTHER_SWIFT_FLAGS=$(inherited) -warnings-as-errors -Wwarning DeprecatedDeclaration' build
xcodebuild -quiet -project alt-tab-macos.xcodeproj -scheme Test -configuration Release -derivedDataPath DerivedData build-for-testing
+35 -27
View File
@@ -134,12 +134,30 @@ jobs:
# meaningless and can mis-bump, so gate to push (PLAN §4.2).
- run: scripts/determine_next_version.sh
if: github.event_name == 'push'
- run: scripts/replace_environment_variables_in_app.sh
# alt-tab-free [depaywall]: decouple the release apparatus from "every signing-ready push".
# determine_next_version.sh writes an EMPTY VERSION.txt when no commit since the last tag is
# release-worthy (semantic-release dry-run finds no bump). Gate the sign/notarize/release steps
# on this so a docs/ci/chore push still runs guards + commitlint + run_tests (Release-config
# compile check) but does NOT sign-build, notarize, or cut a release — no [skip ci] needed (§4.3).
- name: release-decision — did semantic-release compute a version?
id: release-decision
if: github.event_name == 'push'
run: |
if [ -n "$(tr -d '[:space:]' < "$VERSION_FILE" 2>/dev/null)" ]; then
echo "will-release=true" >> "$GITHUB_OUTPUT"
echo "Release-worthy: semantic-release will cut v$(cat "$VERSION_FILE")."
else
echo "will-release=false" >> "$GITHUB_OUTPUT"
echo "::notice::No release-worthy commit — build/test only; notarize+release tail skipped."
fi
# Inject the release version only when releasing; an empty CURRENT_PROJECT_VERSION would crash
# App.version (see config/local.xcconfig). Non-release builds use the committed 0.0.0 default.
- run: scripts/replace_environment_variables_in_app.sh
if: github.event_name == 'push' && steps.release-decision.outputs.will-release == 'true'
# codesign needs APPLE_P12_CERTIFICATE — gated on signing-ready so a not-yet-provisioned fork
# does not RED on the missing-cert import (PLAN §3.3/§4.3).
- run: scripts/codesign/setup_ci_master.sh
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# Guard A — the anti-relock unit test (appended to LicenseManagerTests.swift). Runs on both
# push and PR (no secrets needed); on the push path it sits before the sign/tag steps below
# so a relock fails BEFORE any irreversible side-effect (PLAN §4.4). This is what keeps push
@@ -152,7 +170,7 @@ jobs:
# prove the tree compiles without a cert. When the owner adds the secrets AND sets the fork's
# Developer ID in config/local.xcconfig, this builds signed for the release tail (PLAN §3.3/§4.3).
- name: build app (signed — owner cert provisioned)
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
run: scripts/build_app.sh
- name: build app (ad-hoc — until signing-provisioned)
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready != 'true'
@@ -160,24 +178,24 @@ jobs:
# --- Release tail: requires the owner's Apple Developer ID + Sparkle secrets. Skipped cleanly
# (NOT failed) when they are absent, so push CI stays GREEN until the owner provisions them. ---
- run: scripts/package_and_notarize_release.sh
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# alt-tab-free [depaywall]: upload_symbols_to_appcenter.sh REMOVED — telemetry is off (PLAN §3.4).
- run: scripts/update_appcast.sh
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# alt-tab-free [depaywall]: update_readme_and_website.sh REMOVED — it queried the UPSTREAM
# lwouis/alt-tab-macos GitHub API for stars/downloads and rewrote docs/contributors.md from the
# UPSTREAM contributor graph (semantic mismatch for the fork) and dispatched the lwouis website.
# Cosmetic only; dropped per PLAN §4.0/§4.3 (alongside update_website.sh, already removed).
- run: npx semantic-release
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
- id: set_release_info
run: scripts/extract_latest_changelog.sh
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# alt-tab-free [depaywall]: belt-and-suspenders copy of Guard B + Guard C immediately before the
# release is created. The load-bearing copy is at the top of the job (pre-tag-push); this second
# copy guards the gh-release object itself (PLAN §4.4).
- name: anti-relock + anti-conflict re-check (belt-and-suspenders)
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
run: |
grep -q 'depaywall' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: depaywall marker missing from computeState()"; exit 1; }
@@ -190,29 +208,19 @@ jobs:
# (release.config.js has no @semantic-release/github plugin, so softprops is the sole release
# creator — PLAN §4.3). repository defaults to ${{ github.repository }} = the fork.
- uses: softprops/action-gh-release@v3
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true'
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
with:
repository: ${{ github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ steps.set_release_info.outputs.tag_name }}
body: ${{ steps.set_release_info.outputs.body }}
files: ${{ env.XCODE_BUILD_PATH }}/*.zip
# alt-tab-free [depaywall]: advance the committed last-synced-tag state to the released tag and
# commit it, so the upstream-sync cron syncs the OLDEST upstream tag strictly newer than this —
# not "newest tag minus --is-ancestor". This makes resolve-in-order hold by construction and
# prevents a closed-unmerged sync PR from silently dropping commits (PLAN §4.2). Skipped with
# [skip ci] so it does not re-trigger the workflow.
- name: advance .fork-sync-state to the released tag
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true'
run: |
tag="${{ steps.set_release_info.outputs.tag_name }}"
[ -n "$tag" ] || { echo "no tag_name output — skipping .fork-sync-state advance"; exit 0; }
printf '%s\n' "$tag" > .fork-sync-state
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if ! git diff --quiet -- .fork-sync-state; then
git add .fork-sync-state
git commit -m "chore(sync-state): advance to $tag [skip ci]"
git push origin "HEAD:${{ github.ref_name }}"
fi
# alt-tab-free [depaywall]: the release tail intentionally does NOT advance .fork-sync-state.
# That file is the UPSTREAM-sync cursor — the last-synced UPSTREAM tag (e.g. v11.3.0), read by
# upstream_sync.yml to pick "the oldest upstream tag strictly newer than it." The fork versions
# independently (semantic-release cuts v100.x), so a prior revision's step that wrote the FORK
# release tag here set the cursor to v100.x — after which no upstream tag is ever "newer" and
# upstream-sync would silently never fire. The cursor must instead be advanced by the
# upstream-sync merge flow, recording the UPSTREAM tag it synced (to be wired with
# upstream_sync.yml — PLAN §4.2). Until then .fork-sync-state stays at the last real upstream tag.
# alt-tab-free [depaywall]: update_website.sh (lwouis website dispatch) REMOVED (PLAN §3.4/§4.2).
+3
View File
@@ -20,3 +20,6 @@ codesign.p12
!/.claude/skills/
/scripts/l10n/missing-translations.json
/ai/output/
# alt-tab-free [depaywall]: the Sparkle feed is published OUT OF TREE to the gh-pages branch by
# scripts/update_appcast.sh; the in-tree appcast.xml is removed (PLAN §3.3).
/appcast.xml
+2 -2
View File
@@ -64,8 +64,8 @@
Auto-checks stay off (below) until DOMAIN serves a real appcast host. -->
<string>1YfoCJDk/T8GQx1Gpf3O4ApFgDkBiN6oe/z/Kfnxs/M=</string>
<key>SUEnableAutomaticChecks</key>
<!-- alt-tab-free: off until DOMAIN serves a real appcast (avoids failed update checks). -->
<string>false</string>
<!-- alt-tab-free: enabled 2026-06-16 — DOMAIN serves the appcast on GitHub Pages (gh-pages). -->
<string>true</string>
<key>SUScheduledCheckInterval</key>
<string>604800</string>
+5 -5
View File
@@ -18,11 +18,11 @@ license as upstream. See [`LICENCE.md`](LICENCE.md) for the full text and
brew install --cask koftwentytwo/tap/commandtabfree
```
CommandTabFree is **not yet signed or notarized**, so the cask removes the download
quarantine to avoid a Gatekeeper prompt. You can also download the **`.dmg`** (or `.zip`)
from the [Releases](https://github.com/KofTwentyTwo/CommandTabFree/releases) page and drag
CommandTabFree to Applications — since it's unsigned, right-click the app and choose **Open**
on first launch. On first launch, grant **Accessibility** and **Screen Recording** in System
CommandTabFree is **signed and notarized** (as of v100.1.0), so it launches cleanly through
Gatekeeper — no right-click-**Open** workaround needed. Install via the Homebrew cask above, or
download the **`.dmg`** (or `.zip`) from the
[Releases](https://github.com/KofTwentyTwo/CommandTabFree/releases) page and drag CommandTabFree
to Applications. On first launch, grant **Accessibility** and **Screen Recording** in System
Settings. Universal binary (Apple Silicon + Intel), macOS 10.13+.
## Based on AltTab by Louis Pontoise
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" standalone="yes"?>
<!--
alt-tab-free [depaywall]: this in-tree appcast.xml has been TRUNCATED to an empty Sparkle feed
skeleton. The fork publishes its real Sparkle feed OUT OF TREE (the gh-pages branch) via
scripts/update_appcast.sh, so the live entries are NOT served from this tracked file (PLAN §3.3).
OWNER ACTION (before publish): `git rm appcast.xml` and add `/appcast.xml` to .gitignore (the
identity agent owns .gitignore). Serving the feed from the tree would (1) conflict on every
upstream + fork release and (2) re-introduce lwouis-key-signed entries the fork's SUPublicEDKey
cannot verify. It is left present-but-empty only so the tree stays buildable until removed.
-->
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>alt-tab-free</title>
<language>en</language>
</channel>
</rss>
+9
View File
@@ -1,3 +1,12 @@
## [100.2.1](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.2.0...v100.2.1) (2026-06-16)
# [100.2.0](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.1.0...v100.2.0) (2026-06-16)
### Features
* **updates:** enable Sparkle auto-update via GitHub Pages appcast ([45457cb](https://github.com/KofTwentyTwo/CommandTabFree/commit/45457cb))
# [100.1.0](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.0.3...v100.1.0) (2026-06-16)
+7 -10
View File
@@ -30,16 +30,13 @@ PRODUCT_BUNDLE_IDENTIFIER = com.koftwentytwo.commandtabfree
CURRENT_PROJECT_VERSION = 0.0.0
// --- Hosts (overrides base.xcconfig :20-21) ---
// OWNER-OVERRIDABLE-BEFORE-PUBLISH: fork-owned hosts.
// TODO: replace fork.invalid with the fork's real web host. DOMAIN must serve
// https://<DOMAIN>/appcast.xml (Sparkle feed) and /support, or update checks /
// the in-app Support affordance 404. fork.invalid is a deliberately NON-routable
// placeholder (RFC 6761 reserved .invalid TLD) so nothing phones home before publish.
DOMAIN = fork.invalid
// TODO: API_DOMAIN drives the user-feedback POST (FeedbackWindow). The activate
// fingerprint POST is closed separately at the handleCustomUrl handler (§2/§3.4),
// so this may be made ROUTABLE for feedback without re-arming exfiltration.
// Decide feedback strategy at bootstrap (§3.4 option a/b/c).
// DOMAIN serves the Sparkle appcast at https://<DOMAIN>/appcast.xml. Set 2026-06-16 to the repo's
// GitHub Pages site (gh-pages branch, published by scripts/update_appcast.sh). A kof22.com custom
// domain can be added later — GitHub redirects the github.io URL, so shipped builds keep updating.
// (/support still 404s here — harmless until a support page exists.)
DOMAIN = koftwentytwo.github.io/CommandTabFree
// API_DOMAIN drives the user-feedback POST (FeedbackWindow) — left NON-routable: feedback is deferred
// (§3.4). The activate fingerprint POST is closed separately at the handleCustomUrl handler.
API_DOMAIN = fork.invalid/api
// --- Code signing (overrides release.xcconfig :5) ---
+5 -2
View File
@@ -1,8 +1,11 @@
// docs: https://help.apple.com/xcode/mac/11.4/#/itcaec37c2a6
SWIFT_VERSION = 5.8
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES
MACOSX_DEPLOYMENT_TARGET = 10.13
SWIFT_TREAT_WARNINGS_AS_ERRORS = NO // replaced by the explicit flags below so the deprecation group can be downgraded after them
// Raising the unit-test target to 12.0 turns uses of APIs deprecated in macOS 11/12 (kept for the 10.13 app) into warnings; only that
// group is downgraded here (after -warnings-as-errors, which would otherwise win); every other warning stays an error.
OTHER_SWIFT_FLAGS = $(inherited) -warnings-as-errors -Wwarning DeprecatedDeclaration
MACOSX_DEPLOYMENT_TARGET = 12.0 // unit-tests only: Xcode 27 supports 12.0+; the shipping app keeps 10.13 in base.xcconfig
CODE_SIGNING_REQUIRED = NO // no need for codesign
CODE_SIGNING_ALLOWED = NO // no need for codesign
ONLY_ACTIVE_ARCH = YES // only need to run on the host machine
+2 -2
View File
@@ -1,7 +1,7 @@
# CI/CD readiness — CommandTabFree (fork of lwouis/alt-tab-macos)
**Date:** 2026-06-16 **Branch:** `master` **Repo:** `KofTwentyTwo/CommandTabFree` (public)
**Status:** push CI verified **fully green** on the real runner (run `27634828800`, commit `6070f0cb`).
**Status:** publishing pipeline + Sparkle auto-update **LIVE** — signed + notarized **v100.2.0** released (Developer ID `James Maes (2X834TJ5MA)`, `spctl`-accepted + stapled), feed served on GitHub Pages (`koftwentytwo.github.io/CommandTabFree/appcast.xml`). The release apparatus (cert-import → sign-build → notarize → appcast → semantic-release → gh-release) is gated on a `will-release` signal (empty `VERSION.txt` ⇒ no release), so docs/ci pushes build+test without cutting a release or needing `[skip ci]`.
**Authoritative design:** `docs/PLAN-maintained-fork.md` §4; owner checklist `docs/EXECUTION-STATUS.md` §3.4/§3.5.
This is the state after the CI-fix pass. The pass applied only **safe, owner-secret-independent**
@@ -57,7 +57,7 @@ Environment.
| Code-sign keychain import | `APPLE_P12_CERTIFICATE` | `setup_ci_master.sh` |
| Notarize + staple | `APPLE_ID` + `APPLE_PASSWORD` + `APPLE_TEAM_ID` | `package_and_notarize_release.sh` |
| Sparkle appcast sign + publish to gh-pages | `SPARKLE_ED_PRIVATE_KEY` (+ a real `DOMAIN`/feed host) | `update_appcast.sh` (now `--ed-key-file -`) |
| Tag push + GitHub Release | ambient `GITHUB_TOKEN` (auto) — only gated alongside the above so a release isn't cut unsigned | `npx semantic-release`, `softprops/action-gh-release`, `.fork-sync-state` advance |
| Tag push + GitHub Release | ambient `GITHUB_TOKEN` (auto) — only gated alongside the above so a release isn't cut unsigned | `npx semantic-release`, `softprops/action-gh-release` (the release-tail `.fork-sync-state` advance step was **REMOVED** 2026-06-16 — it clobbered the upstream-sync cursor with the fork release tag; the cursor must be advanced by the sync-merge flow instead — see `ci_cd.yml`) |
| Sync PR fires `guard.yml`/`ci_cd.yml` PR checks | `SYNC_BOT_TOKEN` (GitHub App token or fork PAT — NOT default `GITHUB_TOKEN`) | `upstream_sync.yml` (fail-closed if missing) |
> The release tail is **all-or-nothing**: it requires the full Apple set **and** the Sparkle key
+12 -10
View File
@@ -1,18 +1,20 @@
# SESSION-STATE — CommandTabFree (de-paywalled AltTab fork)
_Updated 2026-06-16. `master` carries the fork; **push CI is fully green** (gate + tests + ad-hoc build; release tail skips until owner secrets). Public pre-release **v100.0.3** live; Homebrew tap live._
_Updated 2026-06-16. `master` carries the fork; the **full CI/CD publishing pipeline + auto-update are LIVE** — signed + notarized **v100.2.0** released (Gatekeeper-accepted), Sparkle feed served on GitHub Pages. Homebrew tap live._
## ✅ CI GREEN — remaining work is owner-gated only
## ✅ SIGNED + NOTARIZED RELEASE LIVE
**Push CI is fully green** as of 2026-06-16 (run `27634828800`, commit `6070f0cb`). Every owner-secret-independent step passes — Guard A/B/C, commitlint, the genstrings generated-files gate, version compute, `run_tests.sh`, and an **ad-hoc** `build_app.sh`. The entire release tail (notarize / appcast / semantic-release / gh-release / sync-state advance) SKIPS cleanly until the owner adds the Apple/Sparkle secrets — by design (CICD-STATUS bucket A).
The complete release pipeline ran green end-to-end (run `27643432063`, commit `2f9fac17`): cert import → **signed** `build_app.sh` (Developer ID **`James Maes (2X834TJ5MA)`**) → notarize + staple → appcast→gh-pages → semantic-release → GitHub Release. **[v100.1.0](https://github.com/KofTwentyTwo/CommandTabFree/releases/tag/v100.1.0)** is published; the downloaded asset verifies `spctl` = "accepted, source=Notarized Developer ID" and `stapler validate` passes (offline-clean, no quarantine prompt). Non-release pushes stay green via the ad-hoc `build_app.sh` path (run `27634828800`).
Two fixes landed this session:
1. **`fix(l10n)` `d1e1b296`** — regenerated the base `resources/l10n/Localizable.strings` from source. It had drifted after the About redesign + brand sweep; the gate regenerates via `genstrings` and diffs (the file is a pure generated mirror — keys ARE the English source literals, so it can't be hand-branded). The diff was verified byte-for-byte against CI's own Xcode 26.0.1 genstrings output before pushing. Attribution preserved (it's in the source, not hand-edited). Dead paywall strings stay upstream-branded (merge-surface minimal).
2. **`fix(ci)` `6070f0cb`** — the Release build was inheriting upstream's Developer ID (`Developer ID Application: Louis Pontoise (QXD7GW8FHY)`, in `config/release.xcconfig:5`) and failing on CI with exit 65 (no such cert). `build_app.sh` now forwards xcodebuild overrides (`"$@"`), and `ci_cd.yml` builds **ad-hoc** (`CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO OTHER_CODE_SIGN_FLAGS=--timestamp=none`) when the release gate is not signing-ready. Auto-switches to signed when the owner provisions the cert AND sets the fork Developer ID in `config/local.xcconfig` (currently commented, lines 45-50).
**How signing is wired:** `config/local.xcconfig` sets `CODE_SIGN_IDENTITY` + `CODE_SIGN_STYLE = Manual` + `TEAM_ID 2X834TJ5MA`; the vendored Sparkle helpers (`vendor/Sparkle/Helpers/*`) are committed **re-signed** under the same Developer ID; `setup_ci_master.sh` imports the cert with an optional `APPLE_P12_PASSWORD`. The release secrets (`APPLE_P12_CERTIFICATE`/`APPLE_P12_PASSWORD`/`APPLE_ID`/`APPLE_PASSWORD`/`APPLE_TEAM_ID` + `SPARKLE_ED_PRIVATE_KEY`) are set in the `production` env. Getting here also required two CI fixes earlier this session: `d1e1b296` (regenerate base `Localizable.strings` to clear the genstrings gate) and `6070f0cb` (ad-hoc build until the cert existed).
**Owner-gated next steps (NOT automatable; details in EXECUTION-STATUS §3.3–3.5 + CICD-STATUS.md bucket C):** Apple Developer ID → `APPLE_P12_CERTIFICATE`/`APPLE_ID`/`APPLE_PASSWORD`/`APPLE_TEAM_ID` in the `production` env (unblocks signed+notarized releases) **and** set `CODE_SIGN_IDENTITY` in `config/local.xcconfig`; real `DOMAIN`/appcast host to replace `fork.invalid`; `SYNC_BOT_TOKEN` (auto upstream-sync PRs); external cron for `upstream_sync.yml`. The 3 sync labels (`sync`/`conflict`/`chokepoint-refresh`) were created this session. Already set: `SPARKLE_ED_PRIVATE_KEY` + `production` env + Sparkle public key in Info.plist.
**Auto-update is LIVE (shipped in v100.2.0):** `DOMAIN = koftwentytwo.github.io/CommandTabFree`, `SUEnableAutomaticChecks = true`, the in-tree `appcast.xml` skeleton removed + gitignored. GitHub Pages serves `https://koftwentytwo.github.io/CommandTabFree/appcast.xml` (gh-pages branch, published by `update_appcast.sh`); the feed carries v100.1.0 + v100.2.0 enclosures. A kof22.com custom domain can be added later (GitHub redirects the github.io URL — no broken builds). **Homebrew cask** bumped to v100.2.0 + quarantine-strip postflight dropped (`KofTwentyTwo/homebrew-tap`). **Release-gate decoupled:** the sign/notarize/release apparatus is gated on a `will-release` signal (empty `VERSION.txt` ⇒ no release), so docs/ci pushes build+test without cutting a release or needing `[skip ci]` (ci_cd.yml).
**Repo facts:** GitHub = `KofTwentyTwo/CommandTabFree` (renamed from alt-tab-free; local dir is still `~/Git.Local/kof22/alt-tab-free`, SSH remote points at CommandTabFree). Latest release v100.0.3 (zip + dmg). ⌘⇥ app/menu-bar icon is FINAL (owner-approved). master tip is green (functional fixes `d1e1b296` + `6070f0cb`; docs/labels/TODO commits sit on top), working tree clean. Owner-gated checklist: `docs/TODO.md`.
**Remaining (owner-gated, NOT automatable):**
- **Upstream-sync**: provision `SYNC_BOT_TOKEN` + an external `repository_dispatch` cron; re-wire the `.fork-sync-state` cursor advance into the sync-merge flow (recording the UPSTREAM tag). `.fork-sync-state` = `v11.3.0`; the 3 sync labels exist.
- **(Optional)** kof22.com custom domain for the appcast (one DNS CNAME → `koftwentytwo.github.io`).
**Repo facts:** GitHub = `KofTwentyTwo/CommandTabFree` (local dir `~/Git.Local/kof22/alt-tab-free`, SSH remote). Latest release **v100.2.0** (signed + notarized + auto-updating). ⌘⇥ icon FINAL. Owner-gated checklist: `docs/TODO.md`.
## What this is
@@ -22,8 +24,8 @@ Two fixes landed this session:
- De-paywall + anti-relock tests + frozen identity (`com.koftwentytwo.commandtabfree`) + brand sweep + ⌘⇥ icon (final) — all on **`master`** (was `depaywall-free`, fast-forward merged, HEAD `c7d2d968`, pushed to origin; source is public for GPL §6).
- **Sparkle EdDSA keypair generated.** Public key in `Info.plist` (`SUPublicEDKey`); private key in 1Password item **"CommandTabFree-Sparkle-EdDSA"** + GitHub Actions secret `SPARKLE_ED_PRIVATE_KEY` (env `production`); keychain account `commandtabfree`. `SUEnableAutomaticChecks=false` until a real appcast host exists. This fixed the launch-time "updater failed to start" alert.
- **Public release v100.0.3** (pre-release, **unsigned / not notarized**, universal x86_64+arm64) — ⌘⇥ menu-bar icon, redesigned About (KofTwentyTwo identity + AltTab attribution links), and a **full l10n rebrand** (product name → CommandTabFree across base/en + all ~20 translated languages; upstream attribution kept). Earlier v100.0.0/v100.0.1/v100.0.2 superseded.
- **Homebrew tap live:** `brew install --cask koftwentytwo/tap/commandtabfree` (`KofTwentyTwo/homebrew-tap` → `Casks/commandtabfree.rb`). Cask strips the quarantine (postflight `xattr`) for the unsigned app.
- **CI/CD wired** (GitHub Actions), **fully green on push** (run `27634828800`, commit `6070f0cb`). Passing: Guard A/B/C, commitlint, generated-files gate, version compute, `run_tests.sh`, ad-hoc `build_app.sh`. The sign/notarize/appcast/semantic-release/gh-release tail is gated on a `release-gate` step that **auto-activates** when `APPLE_*` secrets are added (so missing secrets SKIP, not fail). Fixed this session (2026-06-16): regenerated base `Localizable.strings` to clear the genstrings gate; build ad-hoc until the owner provisions the Apple cert (was inheriting upstream's Developer ID → exit 65). Earlier this session: Guard B grep now matches the shipped `guard false else { return .pro }` patch (was checking the PLAN's never-shipped `if false`/`alt-tab-free [depaywall]` form); `update_appcast.sh` → `--ed-key-file`; commitlint sync-merge `ignores`; dropped upstream-only scripts (AppCenter/website); commit headers must be ≤72 chars. Full status: `docs/CICD-STATUS.md`.
- **Homebrew tap live:** `brew install --cask koftwentytwo/tap/commandtabfree` (`KofTwentyTwo/homebrew-tap` → `Casks/commandtabfree.rb`). Cask strips the quarantine (postflight `xattr`) — was needed for the unsigned v100.0.x; now that v100.1.0 is notarized, drop that postflight and bump the cask.
- **CI/CD wired** (GitHub Actions), **fully green on push** (run `27634828800`, commit `6070f0cb`). Passing: Guard A/B/C, commitlint, generated-files gate, version compute, `run_tests.sh`, ad-hoc `build_app.sh`. The sign/notarize/appcast/semantic-release/gh-release tail is gated on a `release-gate` step that **auto-activates** when `APPLE_*` secrets are added — secrets were added 2026-06-16 and the **full tail is now proven** (signed+notarized **v100.1.0**, run `27643432063`). Fixed this session (2026-06-16): regenerated base `Localizable.strings` to clear the genstrings gate; build ad-hoc until the owner provisions the Apple cert (was inheriting upstream's Developer ID → exit 65). Earlier this session: Guard B grep now matches the shipped `guard false else { return .pro }` patch (was checking the PLAN's never-shipped `if false`/`alt-tab-free [depaywall]` form); `update_appcast.sh` → `--ed-key-file`; commitlint sync-merge `ignores`; dropped upstream-only scripts (AppCenter/website); commit headers must be ≤72 chars. Full status: `docs/CICD-STATUS.md`.
- **Local daily driver installed:** `/Applications/CommandTabFree.app` = **Release** build (no QA menu) signed with the `Local Self-Signed` cert (stable signature → no TCC re-grant loop). Remaining: user grants Accessibility + Screen Recording on first launch.
### Build recipes (verified this session)
+15 -17
View File
@@ -1,25 +1,23 @@
# TODO — CommandTabFree
_Updated 2026-06-16. Push CI is fully green. Everything below is **OWNER-GATED** — no agent can do these (they need Apple credentials, DNS/hosting, or tokens). Full detail: `docs/CICD-STATUS.md` bucket C, `docs/EXECUTION-STATUS.md` §3._
_Updated 2026-06-16. The **publishing pipeline + auto-update are LIVE** — **v100.2.0** released (signed, notarized, Gatekeeper-accepted, auto-updating). The only substantial remaining work is upstream-sync automation. Full detail: `docs/CICD-STATUS.md`, `docs/EXECUTION-STATUS.md` §3._
## Blocking the full publishing pipeline (signed/notarized releases + auto-update)
- [ ] **Apple Developer ID** — obtain a "Developer ID Application" cert for the fork; base64 it into secret `APPLE_P12_CERTIFICATE`; re-sign vendored `vendor/Sparkle/Helpers/*` under the same ID (PLAN §3.3).
- [ ] **Set `CODE_SIGN_IDENTITY`** in `config/local.xcconfig` (lines 45-50, currently commented) to the fork's Developer ID. CI then builds **signed** instead of ad-hoc (the `build app (signed …)` step activates).
- [ ] **Notarization creds** — `APPLE_ID`, `APPLE_PASSWORD` (app-specific password), `APPLE_TEAM_ID` in the `production` Environment.
- [ ] **Real `DOMAIN`/appcast host** — replace `fork.invalid` in `config/local.xcconfig`; serve `https://<DOMAIN>/appcast.xml` (gh-pages); `git rm appcast.xml` + gitignore it; flip `SUEnableAutomaticChecks` back on (PLAN §3.2).
## ✅ Done this session (2026-06-16)
- [x] **Signed + notarized releases** — Developer ID `James Maes (2X834TJ5MA)`; cert + creds in `production` secrets; `CODE_SIGN_IDENTITY`/Manual/`TEAM_ID` in `config/local.xcconfig`; vendored Sparkle helpers re-signed. Full tail proven (runs `27643432063` v100.1.0, `27647918163` v100.2.0).
- [x] **Auto-update** — `DOMAIN = koftwentytwo.github.io/CommandTabFree`, `SUEnableAutomaticChecks = true`, in-tree `appcast.xml` removed + gitignored. GitHub Pages serves the gh-pages feed; v100.2.0 ships auto-updating.
- [x] **Homebrew cask** — bumped to v100.2.0 + quarantine-strip postflight dropped (`KofTwentyTwo/homebrew-tap`).
- [x] **Release-gate decouple** — sign/notarize/release apparatus gated on a `will-release` signal (empty `VERSION.txt` ⇒ no release); non-release pushes build+test without cutting a release or needing `[skip ci]`.
- [x] genstrings gate fixed (`d1e1b296`); ad-hoc-until-cert build (`6070f0cb`); `.fork-sync-state` reset to `v11.3.0` + release tail no longer clobbers it; 3 sync labels created.
> When all four land, the release tail (`release-gate` → notarize → appcast → semantic-release → gh-release) flips on automatically — no further code change.
## Remaining — Upstream-sync automation (owner-gated)
- [ ] **`SYNC_BOT_TOKEN`** — GitHub App installation token (or fork PAT with contents + pull-requests write). Default `GITHUB_TOKEN` does NOT re-trigger PR checks, so guards never run on the sync PR.
- [ ] **External cron** — hit `repository_dispatch` (type `upstream-sync`) from an external scheduler (immune to GitHub's 60-day in-repo `schedule:` auto-disable).
- [ ] **Re-wire the `.fork-sync-state` cursor advance** into the upstream-sync merge flow so it records the synced **UPSTREAM** tag (not the fork release tag) — `upstream_sync.yml` (PLAN §4.2).
## Upstream-sync automation (independent of publishing)
- [ ] **`SYNC_BOT_TOKEN`** — GitHub App installation token (preferred) or fork PAT with contents + pull-requests write. Default `GITHUB_TOKEN` does NOT re-trigger PR checks, so guards never run on the sync PR (`upstream_sync.yml` fails closed without it).
- [ ] **External cron** — hit `repository_dispatch` (type `upstream-sync`) from an external scheduler (immune to GitHub's 60-day in-repo `schedule:` auto-disable) (PLAN §4.2).
- [x] Sync PR labels `sync` / `conflict` / `chokepoint-refresh` created (2026-06-16).
## Optional
- [ ] kof22.com custom domain for the appcast (one DNS CNAME → `koftwentytwo.github.io`; GitHub redirects the github.io URL, so existing builds keep updating).
## Bootstrap acceptance tests (after the above)
## Bootstrap acceptance tests (after upstream-sync is wired)
- [ ] Sync PR through the bot shows guard checks running.
- [ ] A deliberately-relocked branch goes RED on `guard.yml`.
- [ ] Fork build N→N+1 auto-updates via Sparkle.
## Done this session (2026-06-16)
- [x] genstrings consistency gate fixed — regenerated base `Localizable.strings` from source (`d1e1b296`).
- [x] `build_app.sh` builds ad-hoc until cert provisioned; was failing on upstream's Developer ID (`6070f0cb`). Push CI fully green (run `27634828800`).
- [x] Fork build N→N+1 auto-updates via Sparkle — infra live (feed served + v100.2.0 auto-update-enabled); end-to-end N→N+1 confirmed on the next release.
+6
View File
@@ -11,6 +11,12 @@ This project has minimal dependency on Xcode-only features (e.g. InterfaceBuilde
* `scripts/codesign/setup_local.sh` to generate a local self-signed certificate, to avoid having to re-check the `System Preferences > Security & Privacy` permissions on every build
* Either open `alt-tab-macos.xcodeproj` with Xcode, or use the CLI: `xcodebuild -project alt-tab-macos.xcodeproj -scheme Debug` to build the .app with the `Debug` build configuration
### Tests (run locally before merging)
The Gitea CI (`.gitea/workflows/build.yaml`) runs Guards B and C (the de-paywall markers and conflict markers), builds the `Release` scheme unsigned and compiles the `Test` scheme (`build-for-testing`), but does not run the tests: its macOS runner has no GUI session, so `xcodebuild test` cannot reach `testmanagerd`. Run them, including the anti-relock tests in `src/pro/license/LicenseManagerTests.swift` (Guard A, `docs/PLAN-maintained-fork.md` §4.4), locally before merging, especially an upstream sync:
* `scripts/run_tests.sh` (the `Test` scheme, `-configuration Release`)
## Mac development
Mac development ecosystem is pretty terrible in general. They keep piling on the tech stacks on top of each other, so you have C APIs, ObjC APIs, Swift APIs, Interface builder, Playgrounds, Swift UI, Mac Catalyst. All these are bridging with each other with a bunch of macros, SDKs glue, compiler flags, compatibility mode, XCode legacy build system, etc. For alt-tab, we are on Swift 5.0. Note that swift just recently started being stable, but overall any change of version breaks a lot of stuff. Swift itself is the mainstream language with the worst governance I’ve seen in modern times.
@@ -291,7 +291,7 @@ class ExceptionEditorView: NSView {
}
private func resolveHeaderAsync(for bundleId: String) {
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let info = AppDisplayInfo.resolve(bundleId: bundleId)
DispatchQueue.main.async { [weak self] in
guard let self, self.entry.bundleIdentifier == bundleId else { return }
@@ -319,7 +319,7 @@ class ExceptionsTab {
// BundleId changed (or first paint): show a synchronous placeholder, then resolve async.
row.setIcon(AppDisplayInfo.genericIcon, size: iconSize)
row.setContent(bundleId, summary)
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.global(qos: .userInitiated).async { [weak row] in
let info = AppDisplayInfo.resolve(bundleId: bundleId)
DispatchQueue.main.async { [weak row] in
guard let row else { return }