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.
This commit is contained in:
james.maes committed 2026-06-16 15:32:55 -05:00
1 parent 92254c021f
commit 62e0226f01
6 files changed
+50 -50

No files matched your search

+1 -1
View File
@@ -1 +1 @@
v100.1.0
v11.3.0
+8 -18
View File
@@ -197,22 +197,12 @@ jobs:
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).
+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
+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:** full release pipeline **proven** on the real runner — signed + notarized **v100.1.0** released (run `27643432063`, Developer ID `James Maes (2X834TJ5MA)`, `spctl`-accepted + stapled). Non-release pushes stay green via the ad-hoc build path (run `27634828800`).
**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
+11 -10
View File
@@ -1,18 +1,19 @@
# 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 is LIVE** — signed + notarized **v100.1.0** released and Gatekeeper-accepted. 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.
**Remaining (NOT yet done):**
- **Auto-update** (deferred by choice): `appcast.xml` is published to gh-pages, but the app's feed URL is still `fork.invalid` and `SUEnableAutomaticChecks=false`. To enable N→N+1 updates: set a real `DOMAIN` (kof22.com), serve gh-pages at `https://<DOMAIN>/appcast.xml`, flip checks on.
- **Homebrew cask**: now notarized — drop the quarantine-strip postflight and bump the cask to v100.1.0.
- **Upstream-sync**: `.fork-sync-state` reset to `v11.3.0` (the release tail no longer clobbers it — see ci_cd.yml). Before enabling upstream-sync, advance the cursor from the sync-merge flow (recording the UPSTREAM tag), and provision `SYNC_BOT_TOKEN` + an external cron. The 3 sync labels exist.
**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`.
**Repo facts:** GitHub = `KofTwentyTwo/CommandTabFree` (local dir `~/Git.Local/kof22/alt-tab-free`, SSH remote). Latest release **v100.1.0** (signed + notarized zip). ⌘⇥ icon FINAL. Owner-gated checklist: `docs/TODO.md`.
## What this is
@@ -22,8 +23,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)
+23 -14
View File
@@ -1,25 +1,34 @@
# 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 **signed + notarized publishing pipeline is LIVE** — **v100.1.0** is released (Gatekeeper-accepted). Remaining items are auto-update, the Homebrew cask, and upstream-sync. 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).
## ✅ Signed + notarized releases — DONE (2026-06-16)
- [x] Apple Developer ID `Developer ID Application: James Maes (2X834TJ5MA)` provisioned; cert + app-specific password in `production` secrets (`APPLE_P12_CERTIFICATE`/`APPLE_P12_PASSWORD`/`APPLE_ID`/`APPLE_PASSWORD`/`APPLE_TEAM_ID`).
- [x] `CODE_SIGN_IDENTITY` + Manual style + `TEAM_ID` set in `config/local.xcconfig`.
- [x] Vendored `vendor/Sparkle/Helpers/*` committed re-signed under the fork Developer ID.
- [x] Full release tail proven on CI (run `27643432063`): signed → notarized → stapled → released. **v100.1.0** published.
> When all four land, the release tail (`release-gate` → notarize → appcast → semantic-release → gh-release) flips on automatically — no further code change.
## Auto-update (deferred by choice — appcast already publishes to gh-pages)
- [ ] **Real `DOMAIN`** — replace `fork.invalid` in `config/local.xcconfig` with the fork host (kof22.com).
- [ ] Serve the gh-pages `appcast.xml` at `https://<DOMAIN>/appcast.xml` (CNAME the gh-pages site).
- [ ] `git rm` the in-tree `appcast.xml` skeleton + gitignore it (feed is published out-of-tree to gh-pages); flip `SUEnableAutomaticChecks` back on (PLAN §3.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).
## Homebrew cask (now that builds are notarized)
- [ ] Drop the quarantine-strip postflight (`xattr`) from `KofTwentyTwo/homebrew-tap` `Casks/commandtabfree.rb` — no longer needed for a notarized app.
- [ ] Bump the cask to v100.1.0.
## Upstream-sync automation
- [x] `.fork-sync-state` reset to `v11.3.0`; release tail no longer clobbers the cursor with the fork tag (`ci_cd.yml`).
- [ ] **Re-wire the 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).
- [ ] **`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).
- [x] Sync PR labels `sync` / `conflict` / `chokepoint-refresh` created.
## Bootstrap acceptance tests (after the above)
- [ ] 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.
- [ ] Fork build N→N+1 auto-updates via Sparkle (needs DOMAIN/appcast live).
## Done this session (2026-06-16)
## Done earlier 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] `build_app.sh` builds ad-hoc until cert provisioned; was failing on upstream's Developer ID (`6070f0cb`). Push CI green (run `27634828800`).