Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc40fa43bc | ||
|
|
2ae7ded357 | ||
|
|
dd15f22031 | ||
|
|
4ecfc4606f | ||
|
|
45457cb313 | ||
|
|
62e0226f01 | ||
|
|
92254c021f | ||
|
|
0aca4d9400 | ||
|
|
2f9fac17b7 | ||
|
|
4a007e88c9 | ||
|
|
885c52567d | ||
|
|
6070f0cbb9 | ||
|
|
d1e1b29623 | ||
|
|
2588101096 | ||
|
|
6313c295f3 | ||
|
|
d3e7529d91 | ||
|
|
287c4a07c4 | ||
|
|
eb95ad2d52 | ||
|
|
b9e871f176 | ||
|
|
39f10cfba3 | ||
|
|
5b590bd6e5 |
No files matched your search
+93
-37
@@ -30,6 +30,9 @@ env:
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_P12_CERTIFICATE: ${{ secrets.APPLE_P12_CERTIFICATE }}
|
||||
# alt-tab-free [depaywall]: optional .p12 export password (empty if exported password-less).
|
||||
# Not part of the release-gate AND — the cert import tolerates an empty password.
|
||||
APPLE_P12_PASSWORD: ${{ secrets.APPLE_P12_PASSWORD }}
|
||||
SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
|
||||
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
|
||||
GITHUB_EVENT_AFTER: ${{ github.event.after }}
|
||||
@@ -68,17 +71,44 @@ jobs:
|
||||
# just before action-gh-release as belt-and-suspenders.
|
||||
- name: anti-relock — verify de-paywall marker present (Guard B)
|
||||
run: |
|
||||
grep -q 'alt-tab-free \[depaywall\]' src/pro/license/LicenseManager.swift \
|
||||
grep -q 'depaywall' src/pro/license/LicenseManager.swift \
|
||||
|| { echo "PAYWALL RELOCKED: depaywall marker missing from computeState()"; exit 1; }
|
||||
# The marker proves the patch is present; pair with the if-false wrap the patch introduces.
|
||||
# A bare `return .pro` grep is NOT load-bearing (it matches the unpatched line 187 too).
|
||||
grep -Eq 'if[[:space:]]+false[[:space:]]*\{' src/pro/license/LicenseManager.swift \
|
||||
|| { echo "PAYWALL RELOCKED: 'if false { ... }' wrap missing from computeState()"; exit 1; }
|
||||
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' src/pro/license/LicenseManager.swift \
|
||||
|| { echo "PAYWALL RELOCKED: 'guard false else' (forced .pro) missing from computeState()"; exit 1; }
|
||||
- name: anti-conflict — no unresolved merge markers (Guard C)
|
||||
run: |
|
||||
if git -c grep.lineNumber=false grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- . ':(exclude)docs/'; then
|
||||
echo "UNRESOLVED CONFLICT MARKERS present — resolve before merge/release"; exit 1
|
||||
fi
|
||||
# alt-tab-free [depaywall]: release-readiness gate (PLAN §3.3/§4.3 owner-secret checklist).
|
||||
# The fork ships unsigned/ad-hoc until the OWNER provisions the Apple Developer ID cert and the
|
||||
# Sparkle key. We do NOT hard-fail when those secrets are absent — that would make every push
|
||||
# RED on a fork that is correctly built but not yet signing-provisioned. Instead we compute a
|
||||
# boolean from secret presence and SKIP the sign/notarize/appcast/release tail cleanly when it
|
||||
# is false. The build + Guard A (run_tests.sh) still run on push, so push CI is GREEN today and
|
||||
# the release tail FLIPS ON automatically the moment the owner adds the secrets. Secrets are not
|
||||
# available in step-level `if:` directly, so map them to env and emit an output here.
|
||||
- name: release-gate — are signing/notarization secrets present?
|
||||
id: release-gate
|
||||
env:
|
||||
HAS_P12: ${{ secrets.APPLE_P12_CERTIFICATE != '' }}
|
||||
HAS_APPLE_ID: ${{ secrets.APPLE_ID != '' }}
|
||||
HAS_APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD != '' }}
|
||||
HAS_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID != '' }}
|
||||
HAS_SPARKLE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY != '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$HAS_P12" = "true" ] && [ "$HAS_APPLE_ID" = "true" ] \
|
||||
&& [ "$HAS_APPLE_PASSWORD" = "true" ] && [ "$HAS_APPLE_TEAM_ID" = "true" ] \
|
||||
&& [ "$HAS_SPARKLE_KEY" = "true" ]; then
|
||||
echo "signing-ready=true" >> "$GITHUB_OUTPUT"
|
||||
echo "All signing/notarization/Sparkle secrets present — release tail WILL run on push."
|
||||
else
|
||||
echo "signing-ready=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::Signing/notarization/Sparkle secrets absent — release tail (codesign/notarize/appcast/semantic-release/gh-release) is SKIPPED. Build + Guard A still run. Provision APPLE_P12_CERTIFICATE / APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID / SPARKLE_ED_PRIVATE_KEY in the 'production' Environment to enable releases (PLAN §3.3/§3.4)."
|
||||
fi
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 16 # PLAN §4.3: keep node-16; the package.json engines mismatch only warns
|
||||
@@ -104,37 +134,73 @@ jobs:
|
||||
# meaningless and can mis-bump, so gate to push (PLAN §4.2).
|
||||
- run: scripts/determine_next_version.sh
|
||||
if: github.event_name == 'push'
|
||||
# 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'
|
||||
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'
|
||||
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; on the push path it sits before the sign/tag steps below (PLAN §4.4).
|
||||
# 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
|
||||
# CI GREEN today on the signing-unprovisioned fork.
|
||||
- run: scripts/run_tests.sh
|
||||
- run: scripts/build_app.sh
|
||||
if: github.event_name == 'push'
|
||||
# alt-tab-free [depaywall]: build the Release product to prove the tree compiles. The fork
|
||||
# inherits upstream's Developer ID via config/release.xcconfig (CODE_SIGN_IDENTITY), which is
|
||||
# absent on CI until the owner provisions the Apple cert — so when the release gate is NOT
|
||||
# signing-ready we build AD-HOC (CODE_SIGN_IDENTITY=-, signing not required, no timestamp) to
|
||||
# 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' && 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'
|
||||
run: scripts/build_app.sh CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO OTHER_CODE_SIGN_FLAGS=--timestamp=none
|
||||
# --- 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'
|
||||
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'
|
||||
- run: scripts/update_readme_and_website.sh
|
||||
if: github.event_name == 'push'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
|
||||
run: |
|
||||
grep -q 'alt-tab-free \[depaywall\]' src/pro/license/LicenseManager.swift \
|
||||
grep -q 'depaywall' src/pro/license/LicenseManager.swift \
|
||||
|| { echo "PAYWALL RELOCKED: depaywall marker missing from computeState()"; exit 1; }
|
||||
grep -Eq 'if[[:space:]]+false[[:space:]]*\{' src/pro/license/LicenseManager.swift \
|
||||
|| { echo "PAYWALL RELOCKED: 'if false { ... }' wrap missing from computeState()"; exit 1; }
|
||||
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' src/pro/license/LicenseManager.swift \
|
||||
|| { echo "PAYWALL RELOCKED: 'guard false else' (forced .pro) missing from computeState()"; exit 1; }
|
||||
if git -c grep.lineNumber=false grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- . ':(exclude)docs/'; then
|
||||
echo "UNRESOLVED CONFLICT MARKERS present — resolve before release"; exit 1
|
||||
fi
|
||||
@@ -142,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'
|
||||
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'
|
||||
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).
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
echo "::error::PAYWALL RELOCKED: 'depaywall' marker missing from $F — the de-paywall flip was lost in a merge. Refresh the patch (§6.2)."
|
||||
exit 1
|
||||
}
|
||||
grep -Eq 'if[[:space:]]+false[[:space:]]*\{' "$F" || {
|
||||
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' "$F" || {
|
||||
echo "::error::PAYWALL RELOCKED: 'if false { … }' wrap missing from $F — the de-paywall body-suppress was lost. Refresh the patch (§6.2)."
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -12,6 +12,19 @@ CommandTabFree is itself released under the **GNU General Public License v3.0**
|
||||
license as upstream. See [`LICENCE.md`](LICENCE.md) for the full text and
|
||||
[`NOTICE.md`](NOTICE.md) for the GPL §5(a) statement of changes made in this fork.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
brew install --cask koftwentytwo/tap/commandtabfree
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
This software is built on AltTab (`lwouis/alt-tab-macos`) by Louis Pontoise and its
|
||||
@@ -27,7 +40,7 @@ The complete corresponding source for every released binary is this repository a
|
||||
matching release tag:
|
||||
|
||||
```
|
||||
https://github.com/KofTwentyTwo/alt-tab-free
|
||||
https://github.com/KofTwentyTwo/CommandTabFree
|
||||
```
|
||||
|
||||
For a given release `vX.Y.Z`, the source is the repository tree at that tag, including
|
||||
@@ -46,6 +59,4 @@ deliberately not conveyed; GPL-3.0 does not require these to be distributed.
|
||||
|
||||
CommandTabFree is a free, GPL-3.0 fork of <a href="https://github.com/lwouis/alt-tab-macos">AltTab</a> by Louis Pontoise.
|
||||
|
||||
<a href="https://jb.gg/OpenSource"><img src="docs/readme/sponsor.svg" alt="Sponsored by JetBrains" width="900"/></a>
|
||||
|
||||
</div>
|
||||
-17
@@ -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>
|
||||
@@ -1,3 +1,25 @@
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** build ad-hoc until Apple cert provisioned ([6070f0c](https://github.com/KofTwentyTwo/CommandTabFree/commit/6070f0c))
|
||||
* **l10n:** regenerate base Localizable.strings from source ([d1e1b29](https://github.com/KofTwentyTwo/CommandTabFree/commit/d1e1b29))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **release:** sign + notarize with fork Developer ID ([2f9fac1](https://github.com/KofTwentyTwo/CommandTabFree/commit/2f9fac1))
|
||||
|
||||
# [11.3.0](https://github.com/lwouis/alt-tab-macos/compare/v11.2.0...v11.3.0) (2026-06-04)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
// alt-tab-free [depaywall]: commitlint config with an `ignores` predicate so a `chore(sync): merge
|
||||
// upstream` PR (and the upstream commits it drags in through the merge's second parent) does NOT fail
|
||||
// commitlint and block the release job (PLAN §4.3). ci_cd.yml lints the WHOLE pushed range on push and
|
||||
// base..head on a PR, both of which include upstream's own (non-conventional) messages on a sync merge.
|
||||
// `defaultIgnores: true` keeps config-conventional's built-in "Merge …" ignore; the predicates below
|
||||
// drop the synthetic merge subject and any "Merge …" / "Revert …" line we don't author.
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
defaultIgnores: true,
|
||||
ignores: [
|
||||
(msg) => /^chore\(sync\): merge upstream /.test(msg), // our synthetic merge commit (§4.2)
|
||||
(msg) => /^Merge /.test(msg), // belt-and-suspenders (merge commits)
|
||||
(msg) => /^Revert /.test(msg), // upstream revert subjects
|
||||
],
|
||||
}
|
||||
+14
-15
@@ -30,24 +30,23 @@ 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) ---
|
||||
// TODO: fork Developer ID — set to the fork's own signing identity, e.g.
|
||||
// CODE_SIGN_IDENTITY = Developer ID Application: <Fork Org> (TEAMID)
|
||||
// Required for notarization/Gatekeeper. Also re-sign vendored Sparkle helpers
|
||||
// under this same Developer ID (§3.3 / §4.6) or notarization rejects.
|
||||
// CODE_SIGN_IDENTITY = Developer ID Application: <Fork Org> (TEAMID)
|
||||
// Fork Developer ID — signed, notarizable builds (PLAN §3.3 / §4.6). Set 2026-06-16.
|
||||
// The vendored Sparkle helpers (vendor/Sparkle/Helpers/*) are re-signed under this SAME
|
||||
// identity (a committed change) — required or notarization rejects the nested code.
|
||||
// Manual style + TEAM_ID so the headless CI runner signs without an Xcode-managed account.
|
||||
CODE_SIGN_IDENTITY = Developer ID Application: James Maes (2X834TJ5MA)
|
||||
CODE_SIGN_STYLE = Manual
|
||||
TEAM_ID = 2X834TJ5MA
|
||||
|
||||
// --- Telemetry off (overrides base.xcconfig; AppCenter no-ops on empty secret) ---
|
||||
// Intentionally empty: telemetry is disabled (reinforced by Edit 3, §2).
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
## 1. What this project is, and the licensing position
|
||||
|
||||
**alt-tab-free** ([github.com/KofTwentyTwo/alt-tab-free](https://github.com/KofTwentyTwo/alt-tab-free)) is a re-host of the popular open-source macOS window switcher **AltTab** ([github.com/lwouis/alt-tab-macos](https://github.com/lwouis/alt-tab-macos)). It is a pure Swift 5.8 AppKit application — no SwiftUI, no Interface Builder (per [`AGENTS.md`](../AGENTS.md)).
|
||||
**alt-tab-free** ([github.com/KofTwentyTwo/CommandTabFree](https://github.com/KofTwentyTwo/CommandTabFree)) is a re-host of the popular open-source macOS window switcher **AltTab** ([github.com/lwouis/alt-tab-macos](https://github.com/lwouis/alt-tab-macos)). It is a pure Swift 5.8 AppKit application — no SwiftUI, no Interface Builder (per [`AGENTS.md`](../AGENTS.md)).
|
||||
|
||||
### It is not a diverged fork — it is upstream + a paywall commit
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# CI/CD readiness — CommandTabFree (fork of lwouis/alt-tab-macos)
|
||||
|
||||
**Date:** 2026-06-16 **Branch:** `master` **Repo:** `KofTwentyTwo/CommandTabFree` (public)
|
||||
**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**
|
||||
adaptations so that **push CI is GREEN today** on the not-yet-signing-provisioned fork, while the
|
||||
release tail **flips on automatically** the moment the owner adds the Apple/Sparkle secrets. No secret
|
||||
values were invented; no DOMAIN fabricated; notarization is not enabled without a cert.
|
||||
|
||||
---
|
||||
|
||||
## What changed in this pass
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `.github/workflows/ci_cd.yml` | Added a `release-gate` step that emits `signing-ready` from secret presence (`APPLE_P12_CERTIFICATE`/`APPLE_ID`/`APPLE_PASSWORD`/`APPLE_TEAM_ID`/`SPARKLE_ED_PRIVATE_KEY`). Gated codesign/notarize/appcast/semantic-release/extract-changelog/gh-release/sync-state-advance behind `push && signing-ready=='true'`. Removed the `update_readme_and_website.sh` step (queried the **upstream** lwouis API — semantic mismatch, cosmetic). |
|
||||
| `scripts/update_appcast.sh` | `sign_update -s $KEY` (DEPRECATED; per `--help` "no longer supported for newly generated keys" — would reject the fork's fresh EdDSA key) → `printf '%s' "$SPARKLE_ED_PRIVATE_KEY" \| sign_update --ed-key-file -`. No `-p` (default output is the `sparkle:edSignature/length` attr pair the enclosure needs; verified). Key now off the argv. |
|
||||
| `commitlint.config.js` | Added `defaultIgnores: true` + `ignores` predicates (drop `chore(sync): merge upstream …`, `Merge …`, `Revert …`) so a sync merge's dragged-in upstream messages don't fail commitlint and block the release job (PLAN §4.3). |
|
||||
| `scripts/update_readme_and_website.sh` | **Removed** — hardcoded `lwouis/alt-tab-macos` API + `lwouis` contributor graph; cosmetic SVG stats; no live caller after the workflow edit. |
|
||||
| `scripts/update_website.sh` | **Removed** — dispatched to upstream `lwouis/alt-tab-website`; already had no live caller. |
|
||||
| `scripts/upload_symbols_to_appcenter.sh` | **Removed** — hardcoded upstream AppCenter org; telemetry is off (PLAN §3.4); no live caller. |
|
||||
|
||||
All three workflow YAMLs validate (`yaml.safe_load` OK); `commitlint.config.js` loads in node.
|
||||
|
||||
---
|
||||
|
||||
## (A) WORKING / green NOW, without any owner secret
|
||||
|
||||
These run on every push and PR and need no Apple/Sparkle/DOMAIN/bot secret. They are what makes push
|
||||
CI GREEN today (vs. the prior all-14-runs-RED state — note that RED was a *missing depaywall marker*,
|
||||
already fixed on this branch; this pass ensures it does not go RED again on missing secrets).
|
||||
|
||||
| Capability | Where | Confidence |
|
||||
|---|---|---|
|
||||
| **Guard A** anti-relock unit tests (`run_tests.sh`, Test scheme) | `ci_cd.yml` push+PR; `guard.yml` PR (macos-15) | High — verified GREEN in EXECUTION-STATUS §2.2 (493 exec, 16 skipped, 0 fail) |
|
||||
| **Guard B** de-paywall marker + `if false` wrap grep | `ci_cd.yml` first push step + PR; `guard.yml` PR | High — marker present at `LicenseManager.swift:177` |
|
||||
| **Guard C** unresolved-conflict-marker grep | `ci_cd.yml` first push step + PR; `guard.yml` PR | High |
|
||||
| **Compile the full tree** (`build_app.sh`, **ad-hoc**) | `ci_cd.yml` push | Verified GREEN on CI (run 27634828800). NOTE: the bare `build_app.sh` inherits upstream's Developer ID from `config/release.xcconfig:5` and fails with exit 65 on CI; the build step now runs ad-hoc (`CODE_SIGN_IDENTITY=-`) when `release-gate` is not signing-ready, and signed otherwise. `build_app.sh` forwards xcodebuild overrides via `"$@"`. |
|
||||
| **commitlint** (push range + PR base..head, with `ignores`) | `ci_cd.yml` | Med-High — `ignores` should absorb upstream merge msgs; confirm on FIRST real sync PR |
|
||||
| **generated-files-up-to-date** check | `ci_cd.yml` push+PR | High |
|
||||
| **semantic-release dry-run** version compute (`determine_next_version.sh`) | `ci_cd.yml` push | High — dry-run, no push, no secret |
|
||||
| **Release tail SKIPS cleanly** when secrets absent (warning, not failure) | `ci_cd.yml` `release-gate` | High — `signing-ready=false` path emits `::warning::` and skips |
|
||||
|
||||
> Note: full GREEN on the GitHub-hosted **macos-15** runner with **Xcode 26.0.1** is now CONFIRMED by a
|
||||
> real run (`27634828800`, commit `6070f0cb`): gate + `run_tests.sh` + ad-hoc `build_app.sh` all pass and
|
||||
> the release tail skips cleanly on absent secrets.
|
||||
|
||||
## (B) READY — flips ON automatically when the owner adds secret X
|
||||
|
||||
No further code change needed; the gate enables these the moment the secret(s) exist in the `production`
|
||||
Environment.
|
||||
|
||||
| Capability | Enabling secret(s) | Gated step(s) |
|
||||
|---|---|---|
|
||||
| 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` (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
|
||||
> (`signing-ready` is the AND of all five). This is deliberate — cutting a tag/Release without a signed,
|
||||
> notarized, Sparkle-signed artifact would publish an un-updatable or Gatekeeper-blocked build and burn a
|
||||
> version number. If you want to test the build path before the Apple cert exists, push runs build + Guard
|
||||
> A and stops cleanly.
|
||||
|
||||
## (C) OWNER-BLOCKED — exact action required (cannot be done by agents)
|
||||
|
||||
| # | Blocker | Exact owner action |
|
||||
|---|---|---|
|
||||
| 1 | Apple Developer ID cert | Obtain a **Developer ID Application** cert for the fork; base64 it into secret `APPLE_P12_CERTIFICATE`; set `CODE_SIGN_IDENTITY` in `config/local.xcconfig` (currently commented). Re-sign the vendored `vendor/Sparkle/Helpers/*` under the SAME ID or notarization rejects (PLAN §3.3). |
|
||||
| 2 | Notarization creds | Set secrets `APPLE_ID`, `APPLE_PASSWORD` (app-specific password), `APPLE_TEAM_ID` in the `production` Environment. |
|
||||
| 3 | Sparkle private key | Store `SPARKLE_ED_PRIVATE_KEY` (already generated 2026-06-15) in the `production` Environment. Confirm the `Info.plist` `SUPublicEDKey` is the real public half (currently may be a placeholder). |
|
||||
| 4 | `production` Environment | Create the GitHub Environment named **`production`** (matches `ci_cd.yml: environment: production`) with **no required-reviewer protection** that would stall releases; attach secrets #1–#3. |
|
||||
| 5 | Real DOMAIN / appcast host | Replace `DOMAIN`/`API_DOMAIN` placeholders (`fork.invalid`) in `config/local.xcconfig` with fork-owned routable hosts; stand up `https://<DOMAIN>/appcast.xml` (gh-pages); `git rm appcast.xml` + gitignore it (PLAN §3.2). Until done, auto-update has no feed even with the key. |
|
||||
| 6 | `SYNC_BOT_TOKEN` | Provision a GitHub App installation token (preferred) or fork PAT with contents + pull-requests write. Default `GITHUB_TOKEN` does NOT re-trigger PR checks → guards never run on the sync PR (`upstream_sync.yml` fails closed without it). |
|
||||
| 7 | PR labels | `gh label create sync` / `conflict` / `chokepoint-refresh` (`upstream_sync.yml` fails closed if any is missing). |
|
||||
| 8 | External scheduler | Hit `repository_dispatch` (type `upstream-sync`) from an external cron (immune to GitHub's 60-day in-repo `schedule:` auto-disable); add the two-trigger liveness alert (PLAN §4.2). |
|
||||
| 9 | Xcode re-pin discipline | When an upstream sync bumps `ci_cd.yml`'s pinned Xcode, re-pin `guard.yml`'s `Xcode_26.0.1` line to match (EXECUTION-STATUS §3.5). |
|
||||
| 10 | Bootstrap acceptance tests | (a) sync PR through the bot shows guard checks; (b) a deliberately-relocked branch goes RED on `guard.yml`; (c) fork build N→N+1 auto-updates via Sparkle (PLAN §4.2/§4.4/§6.1). |
|
||||
|
||||
---
|
||||
|
||||
## Remaining risks (need a real CI run to confirm)
|
||||
|
||||
- **macos-15 + Xcode_26.0.1 availability** on GitHub-hosted runners is assumed, not verified here.
|
||||
- **commitlint `ignores`** is verified to load and match our synthetic subject, but the exact set of
|
||||
*upstream* non-conforming messages can't be enumerated until a real `chore(sync)` merge — re-verify on
|
||||
the FIRST sync PR (PLAN §4.3 says confirm on first sync).
|
||||
- **`semantic-release` v15 / commitlint v8 on node-16**: kept per PLAN §4.3 (engines mismatch only warns);
|
||||
unverified end-to-end on the runner since the release tail can't run without secrets.
|
||||
- **`update_appcast.sh` gh-pages publish path** (orphan-branch seed, append-before-`</channel>`) is logic
|
||||
the agents can't exercise without `GITHUB_TOKEN` + a real repo; verify on the first signed release.
|
||||
- **`set -x` in `update_appcast.sh`** will print the `printf '%s' "$SPARKLE_ED_PRIVATE_KEY"` command in
|
||||
the (private-repo) log trace; the key is no longer on the argv/process list, but consider `set +x` around
|
||||
the sign line if the repo ever goes from private logs to public. (Not changed in this pass — out of scope.)
|
||||
@@ -188,23 +188,18 @@ per-component owner actions gathered from the scaffolding agents merged in.
|
||||
|
||||
### 3.1 Brand / identity / trademark (BLOCKING for a clean publish)
|
||||
|
||||
- [ ] **Pick a trademark-safe fork name** (avoid "AltTab"/"alt-tab" confusion) and run the PLAN §3.2
|
||||
brand sweep across the tree, then **regenerate l10n** (`genstrings` per the §3.2 procedure) for
|
||||
every file the sweep moved/renamed.
|
||||
- [ ] **Set the REAL `PRODUCT_NAME`** in `config/local.xcconfig` if `AltTabFree` is not final, AND set
|
||||
`ci_cd.yml` `APP_NAME` equal to it (currently placeholder `AltTabFree`).
|
||||
- [ ] **Set the REAL fork bundle id in `config/local.xcconfig` and FREEZE it once** (placeholder:
|
||||
`io.github.koftwentytwo.alt-tab-free`). Changing it later orphans every user's keychain +
|
||||
UserDefaults state (LicenseManager derives the suite names from `App.bundleIdentifier`). Decide
|
||||
PLAN §3.1: (a) fresh id, or (b) reuse-existing-id + migration. **Freeze the bundle id.**
|
||||
- [ ] **Replace placeholder identity in `README.md` AND `NOTICE.md`** — final product name, final
|
||||
bundle id, and the fork GitHub URL (placeholder `https://github.com/koftwentytwo/alt-tab-free`).
|
||||
- [x] **Fork name + brand sweep + l10n (DONE 2026-06-15)** — name is **CommandTabFree** (trademark-distinct);
|
||||
brand sweep done; l10n rebranded across base/en + all ~20 language tables, upstream attribution kept.
|
||||
- [x] **`PRODUCT_NAME = CommandTabFree` (DONE)** in `config/local.xcconfig`. Verify `ci_cd.yml` `APP_NAME`
|
||||
matches when CI is wired.
|
||||
- [x] **Fork bundle id FROZEN (DONE)** — `com.koftwentytwo.commandtabfree` (option (a) fresh id) in `config/local.xcconfig`.
|
||||
- [x] **`README.md` + `NOTICE.md` (DONE)** — final product name, real fork GitHub URL; README now includes the brew install.
|
||||
- [ ] **At release time, set `NOTICE.md`'s upstream version + modification date** to the actual sync
|
||||
point (currently `v11.3.0` / `2026-06-10`). Keep `NOTICE.md` current per PLAN §6.2 on every future
|
||||
sync that changes the fork's modification surface. **Note:** `NOTICE.md` lists
|
||||
"App.handleCustomUrl (activate disabled)" as a change — the §2 Edit-2 neutralization HAS landed
|
||||
(`App.swift:474`), so that line is accurate; if you ever opt NOT to neutralize it, remove that line.
|
||||
- [ ] **Replace the app icon** (do not ship lwouis's mark).
|
||||
- [x] **App icon DONE (owner-approved 2026-06-15)** — the ⌘⇥ glyph (app + menu bar) is the chosen mark, not lwouis's mark. No replacement planned.
|
||||
- [ ] **Add `config/local.xcconfig` and `package.json` to the PLAN §6.2 "keep ours" recurring-conflict
|
||||
set** so a future merge can't re-introduce upstream identity or the MIT license string.
|
||||
|
||||
|
||||
+20
-6
@@ -1,15 +1,31 @@
|
||||
# SESSION-STATE — CommandTabFree (de-paywalled AltTab fork)
|
||||
|
||||
_Updated 2026-06-15. `master` now carries the fork (merged + pushed). 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._
|
||||
|
||||
## ✅ SIGNED + NOTARIZED RELEASE LIVE
|
||||
|
||||
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`).
|
||||
|
||||
**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).
|
||||
|
||||
**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).
|
||||
|
||||
**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
|
||||
`KofTwentyTwo/alt-tab-free` is a fork of **AltTab** (`lwouis/alt-tab-macos`, GPL-3.0) that neutralizes the v11 Pro paywall so all features are free, rebranded **CommandTabFree**. Strategy: thin, merge-stable patch tracking upstream forever (`docs/PLAN-maintained-fork.md`).
|
||||
`KofTwentyTwo/CommandTabFree` is a fork of **AltTab** (`lwouis/alt-tab-macos`, GPL-3.0) that neutralizes the v11 Pro paywall so all features are free, rebranded **CommandTabFree**. Strategy: thin, merge-stable patch tracking upstream forever (`docs/PLAN-maintained-fork.md`).
|
||||
|
||||
## ✅ DONE
|
||||
- De-paywall + anti-relock tests + frozen identity (`com.koftwentytwo.commandtabfree`) + brand sweep + placeholder icon — all on **`master`** (was `depaywall-free`, fast-forward merged, HEAD `c7d2d968`, pushed to origin; source is public for GPL §6).
|
||||
- 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.
|
||||
- **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)
|
||||
@@ -21,8 +37,6 @@ _Updated 2026-06-15. `master` now carries the fork (merged + pushed). Public pre
|
||||
- **Apple Developer ID** (long pole): set `CODE_SIGN_IDENTITY` in `config/local.xcconfig`, re-sign Sparkle helpers, notarize → re-release signed/notarized, then drop the cask's quarantine postflight.
|
||||
- **Real DOMAIN/appcast** (kof22.com ready): replace `fork.invalid` in `local.xcconfig`, serve `/appcast.xml`, flip `SUEnableAutomaticChecks` back on.
|
||||
- **Wire CI:** `update_appcast.sh` uses the deprecated `sign_update -s` — switch to `--ed-key-file` (or `--account commandtabfree`) for the new key; create `sync`/`conflict`/`chokepoint-refresh` labels; external cron for `upstream_sync.yml`.
|
||||
- **Real icon artwork** (replace placeholder).
|
||||
|
||||
## Gotchas
|
||||
- Bundle id FROZEN (`com.koftwentytwo.commandtabfree`); keychain/UserDefaults suites derive from it.
|
||||
- Don't `git revert 9147a4a8` (mixes paywall with a 1869-file reorg).
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# TODO — CommandTabFree
|
||||
|
||||
_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._
|
||||
|
||||
## ✅ 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.
|
||||
|
||||
## 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).
|
||||
|
||||
## 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 upstream-sync is wired)
|
||||
- [ ] Sync PR through the bot shows guard checks running.
|
||||
- [ ] A deliberately-relocked branch goes RED on `guard.yml`.
|
||||
- [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.
|
||||
@@ -232,7 +232,7 @@ update from the fork's own releases, and attempt to notarize/sign under the upst
|
||||
| 6 | **Sparkle private EdDSA key** | `ci_cd.yml:13`; `update_appcast.sh:9` | `$SPARKLE_ED_PRIVATE_KEY` | **C** — fork's new private key. Must match #5 or updates won't verify. |
|
||||
| 7 | **Appcast feed domain** | `config/base.xcconfig:20` (`DOMAIN`) → `Endpoints.swift:6-7` | `alt-tab.app` → `https://alt-tab.app/appcast.xml` | **C** — fork's domain, OR repoint feed at fork's GitHub-hosted appcast. |
|
||||
| 8 | **API domain (license + feedback)** | `config/base.xcconfig:21` (`API_DOMAIN`) → `Endpoints.swift:5,11-12` | `alt-tab.app/api` → `https://alt-tab.app/api/v1/license`, `.../v1/feedback` | **R** — license API is paywall; remove with `src/pro`. Feedback endpoint must be repointed or removed. |
|
||||
| 9 | **Appcast download URL** | `update_appcast.sh:18` | `https://github.com/lwouis/alt-tab-macos/releases/download/v<v>/...` | **C** — fork's GitHub releases URL (`KofTwentyTwo/alt-tab-free`). |
|
||||
| 9 | **Appcast download URL** | `update_appcast.sh:18` | `https://github.com/lwouis/alt-tab-macos/releases/download/v<v>/...` | **C** — fork's GitHub releases URL (`KofTwentyTwo/CommandTabFree`). |
|
||||
| 10 | **Appcast release-notes link** | `update_appcast.sh:16`; existing items in `appcast.xml` | `https://alt-tab.app/changelog-bare` | **C/R** — fork URL or drop the link. |
|
||||
| 11 | **License tier cookie** | `src/pro/license/LicenseCookie.swift` | cookie on `alt-tab.app` | **R** — remove with paywall. |
|
||||
| 12 | **Website dispatch repo + token** | `update_website.sh:5`; `ci_cd.yml:69` | `lwouis/alt-tab-website` + `$WEBSITE_DISPATCH_TOKEN` | **R/C** — fork has no upstream website; remove this CI step or repoint. |
|
||||
|
||||
@@ -30,7 +30,7 @@ modified. All paths are relative to the repo root unless noted.
|
||||
`semantic-release-bot`; there are **no fork-specific commits**. The entire paywall was
|
||||
introduced **upstream** by lwouis in a single commit, `9147a4a8 "feat: introducing
|
||||
alt-tab pro!"` (v11.0.0, 2026-04-05). The "alt-tab-free" repo has only re-hosted the
|
||||
code under `KofTwentyTwo/alt-tab-free` — **no code-level rebrand has happened yet**:
|
||||
code under `KofTwentyTwo/CommandTabFree` — **no code-level rebrand has happened yet**:
|
||||
bundle id, app name, domain, GitHub URL, and SUPublicEDKey are all still the upstream
|
||||
`com.lwouis.alt-tab-macos` / `alt-tab.app` / `lwouis/alt-tab-macos` values.
|
||||
|
||||
@@ -242,7 +242,7 @@ git log --oneline -- src/pro # only 3 commits ever touch src/pro
|
||||
git log --oneline --diff-filter=A -- src/pro | tail -5 # src/pro first appears in 9147a4a8
|
||||
git log -40 --format='%an' | sort | uniq -c # 34 lwouis, 6 semantic-release-bot
|
||||
git rev-list --count HEAD # 1850 commits total
|
||||
git remote -v # origin = github.com:KofTwentyTwo/alt-tab-free.git
|
||||
git remote -v # origin = github.com:KofTwentyTwo/CommandTabFree.git
|
||||
```
|
||||
|
||||
Findings:
|
||||
@@ -257,7 +257,7 @@ Findings:
|
||||
fixes after the pro launch (`f3fe535a`, `cddc89dd`, `e0b1778d`, etc.) — is `lwouis`.
|
||||
There are **zero commits by KofTwentyTwo / the current owner**. The "alt-tab-free" repo
|
||||
is a mirror of upstream `lwouis/alt-tab-macos` re-hosted under a new origin
|
||||
(`git@github.com:KofTwentyTwo/alt-tab-free.git`), with the codebase otherwise unchanged.
|
||||
(`git@github.com:KofTwentyTwo/CommandTabFree.git`), with the codebase otherwise unchanged.
|
||||
The README still markets it as "AltTab Pro" and links `https://alt-tab.app/`
|
||||
(`README.md:1`).
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* %@ is the app name */
|
||||
"%@ is a free, de-paywalled fork. All upstream work and credit belong to Louis Pontoise and AltTab’s contributors." = "%@ is a free, de-paywalled fork. All upstream work and credit belong to Louis Pontoise and AltTab’s contributors.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"%@ now has a Pro tier" = "%@ now has a Pro tier";
|
||||
|
||||
@@ -22,13 +25,13 @@
|
||||
/* No comment provided by engineer. */
|
||||
"A debug profile (versions, settings, hardware) is attached to help diagnose the issue." = "A debug profile (versions, settings, hardware) is attached to help diagnose the issue.";
|
||||
|
||||
/* Attribution to the upstream project AltTab; do not translate the product names */
|
||||
"A free fork of AltTab by Louis Pontoise — GPL-3.0" = "A free fork of AltTab by Louis Pontoise — GPL-3.0";
|
||||
/* No comment provided by engineer. */
|
||||
"A KofTwentyTwo project" = "A KofTwentyTwo project";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"A new version of CommandTabFree is available" = "A new version of CommandTabFree is available";
|
||||
|
||||
/* Menubar option. %@ is CommandTabFree */
|
||||
/* Menubar option. %@ is AltTab */
|
||||
"About %@" = "About %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
@@ -86,16 +89,16 @@
|
||||
"Alphabetical Order" = "Alphabetical Order";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"CommandTabFree is a free, open-source window switcher for macOS. Pro features are available with a 14-day free trial." = "CommandTabFree is a free, open-source window switcher for macOS. Pro features are available with a 14-day free trial.";
|
||||
"AltTab is a free, open-source window switcher for macOS. Pro features are available with a 14-day free trial." = "AltTab is a free, open-source window switcher for macOS. Pro features are available with a 14-day free trial.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"CommandTabFree Pro" = "CommandTabFree Pro";
|
||||
"AltTab Pro" = "AltTab Pro";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"CommandTabFree Pro adds 4 features beyond the free switcher." = "CommandTabFree Pro adds 4 features beyond the free switcher.";
|
||||
"AltTab Pro adds 4 features beyond the free switcher." = "AltTab Pro adds 4 features beyond the free switcher.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"CommandTabFree Pro is still available" = "CommandTabFree Pro is still available";
|
||||
"AltTab Pro is still available" = "AltTab Pro is still available";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Always" = "Always";
|
||||
@@ -172,6 +175,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Automatic activation failed" = "Automatic activation failed";
|
||||
|
||||
/* Attribution to the upstream project AltTab; do not translate the product names */
|
||||
"Based on AltTab by Louis Pontoise" = "Based on AltTab by Louis Pontoise";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Brief description of the bug" = "Brief description of the bug";
|
||||
|
||||
@@ -344,7 +350,7 @@
|
||||
"Gesture" = "Gesture";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Get more from CommandTabFree with Pro" = "Get more from CommandTabFree with Pro";
|
||||
"Get more from AltTab with Pro" = "Get more from AltTab with Pro";
|
||||
|
||||
/* Menubar option */
|
||||
"Get Pro" = "Get Pro";
|
||||
@@ -422,7 +428,7 @@
|
||||
"License doesn't cover this version" = "License doesn't cover this version";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"License will remain valid and usable to activate CommandTabFree on any machine" = "License will remain valid and usable to activate CommandTabFree on any machine";
|
||||
"License will remain valid and usable to activate AltTab on any machine" = "License will remain valid and usable to activate AltTab on any machine";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Light" = "Light";
|
||||
@@ -520,6 +526,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Ordering & Grouping" = "Ordering & Grouping";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Original source" = "Original source";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Overridden in Shortcut:" = "Overridden in Shortcut:";
|
||||
|
||||
@@ -562,7 +571,7 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Pro Lifetime license activated for %@" = "Pro Lifetime license activated for %@";
|
||||
|
||||
/* %@ is CommandTabFree */
|
||||
/* %@ is AltTab */
|
||||
"Quit %@" = "Quit %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
@@ -874,9 +883,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Visible Spaces" = "Visible Spaces";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Website" = "Website";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome to %@" = "Welcome to %@";
|
||||
|
||||
@@ -920,7 +926,7 @@
|
||||
"You can’t undo this action." = "You can’t undo this action.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You have 14 days to try all Pro features. After that, Pro features will step back and the core window switcher will keep working exactly as before.\n\nCommandTabFree stays free and open-source. Pro is an optional one-time purchase that funds continued development." = "You have 14 days to try all Pro features. After that, Pro features will step back and the core window switcher will keep working exactly as before.\n\nCommandTabFree stays free and open-source. Pro is an optional one-time purchase that funds continued development.";
|
||||
"You have 14 days to try all Pro features. After that, Pro features will step back and the core window switcher will keep working exactly as before.\n\nAltTab stays free and open-source. Pro is an optional one-time purchase that funds continued development." = "You have 14 days to try all Pro features. After that, Pro features will step back and the core window switcher will keep working exactly as before.\n\nAltTab stays free and open-source. Pro is an optional one-time purchase that funds continued development.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You may need to disable some conflicting system gestures" = "You may need to disable some conflicting system gestures";
|
||||
@@ -938,10 +944,10 @@
|
||||
"You've been using %d Pro features.\nThey'll switch back to defaults on Day 15." = "You've been using %d Pro features.\nThey'll switch back to defaults on Day 15.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You've used CommandTabFree %@ times — %@ of those used Pro features." = "You've used CommandTabFree %1$@ times — %2$@ of those used Pro features.";
|
||||
"You've used AltTab %@ times — %@ of those used Pro features." = "You've used AltTab %1$@ times — %2$@ of those used Pro features.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You've used CommandTabFree %@ times." = "You've used CommandTabFree %@ times.";
|
||||
"You've used AltTab %@ times." = "You've used AltTab %@ times.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your 14-day Pro trial just ended" = "Your 14-day Pro trial just ended";
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
set -ex
|
||||
|
||||
set -o pipefail && xcodebuild -project alt-tab-macos.xcodeproj -scheme Release -derivedDataPath DerivedData | scripts/xcbeautify
|
||||
set -o pipefail && xcodebuild -project alt-tab-macos.xcodeproj -scheme Release -derivedDataPath DerivedData "$@" | scripts/xcbeautify
|
||||
file "$BUILD_DIR/$XCODE_BUILD_PATH/$APP_NAME.app/Contents/MacOS/$APP_NAME"
|
||||
@@ -7,4 +7,4 @@ certificateFile="codesign"
|
||||
# Recreate the certificate from the secure environment variable
|
||||
echo "$APPLE_P12_CERTIFICATE" | base64 --decode > $certificateFile.p12
|
||||
|
||||
scripts/codesign/import_certificate_into_new_keychain.sh "$certificateFile" ""
|
||||
scripts/codesign/import_certificate_into_new_keychain.sh "$certificateFile" "${APPLE_P12_PASSWORD:-}"
|
||||
@@ -31,7 +31,14 @@ version="$(cat "$VERSION_FILE")"
|
||||
date="$(date +'%a, %d %b %Y %H:%M:%S %z')"
|
||||
minimumSystemVersion="$(awk -F ' = ' '/MACOSX_DEPLOYMENT_TARGET/ { print $2; }' < config/base.xcconfig)"
|
||||
zipName="$APP_NAME-$version.zip"
|
||||
edSignatureAndLength=$(vendor/Sparkle/bin/sign_update -s $SPARKLE_ED_PRIVATE_KEY "$XCODE_BUILD_PATH/$zipName")
|
||||
# alt-tab-free [depaywall]: the `-s <private-key>` flag is DEPRECATED and, per `sign_update --help`,
|
||||
# "no longer supported for newly generated keys" — and the fork's EdDSA keypair was newly generated
|
||||
# (EXECUTION-STATUS §3.3, 2026-06-15), so `-s` would REJECT it. Pipe the secret to the documented
|
||||
# `--ed-key-file -` (read key from stdin) form instead. Do NOT add `-p`: the default output is the
|
||||
# `sparkle:edSignature="…" length="…"` attribute pair the <enclosure> needs verbatim, whereas `-p`
|
||||
# prints only the bare signature and would break the enclosure. Reading from stdin also keeps the key
|
||||
# off the argv (process listing), unlike the old `-s $KEY` form.
|
||||
edSignatureAndLength=$(printf '%s' "$SPARKLE_ED_PRIVATE_KEY" | vendor/Sparkle/bin/sign_update --ed-key-file - "$XCODE_BUILD_PATH/$zipName")
|
||||
|
||||
# GPL §6(d) corresponding-source pointer for object code conveyed via Sparkle auto-update.
|
||||
correspondingSource="https://github.com/$fork_repo/archive/refs/tags/v$version.tar.gz"
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -exu
|
||||
|
||||
github_api_request() {
|
||||
local url="$1"
|
||||
curl -s \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/lwouis/alt-tab-macos$url"
|
||||
}
|
||||
|
||||
unicode_sort() {
|
||||
python3 -c 'import sys, unicodedata; print("".join(sorted((line for line in sys.stdin if line.strip()), key=lambda s: unicodedata.normalize("NFKD", s.casefold()))), end="")'
|
||||
}
|
||||
|
||||
github_contributors() {
|
||||
github_api_request "/contributors" |
|
||||
jq -r '.[]|("[" + .login + "](" + .html_url + ")")' |
|
||||
sed -e '/semantic-release-bot/d' |
|
||||
unicode_sort |
|
||||
awk '{printf "%s%s", sep, $0; sep=", "} END{print ""}'
|
||||
}
|
||||
|
||||
# Rewrite only the "Developed the app" section of docs/contributors.md, preserving the
|
||||
# frozen "Localized the app" section below it.
|
||||
update_developer_contributors() {
|
||||
local file="docs/contributors.md"
|
||||
{
|
||||
echo "## [Developed the app](https://github.com/lwouis/alt-tab-macos/graphs/contributors)"
|
||||
echo
|
||||
github_contributors
|
||||
echo
|
||||
sed -n '/## Localized the app/,$p' "$file"
|
||||
} > "$file.tmp" && mv "$file.tmp" "$file"
|
||||
}
|
||||
|
||||
get_total_downloads() {
|
||||
local downloads=0
|
||||
local page=1
|
||||
while true; do
|
||||
local response
|
||||
response=$(github_api_request "/releases?per_page=100&page=$page")
|
||||
# Safely sum all download counts, even if assets are empty
|
||||
local count
|
||||
count=$(echo "$response" | jq '[.[]?.assets[]?.download_count // 0] | add // 0')
|
||||
downloads=$((downloads + count))
|
||||
# Stop if no more releases
|
||||
if [[ $(echo "$response" | jq 'length') -lt 1 ]]; then
|
||||
break
|
||||
fi
|
||||
page=$((page + 1))
|
||||
done
|
||||
echo "$downloads"
|
||||
}
|
||||
|
||||
get_stars() {
|
||||
github_api_request "" | jq '.stargazers_count'
|
||||
}
|
||||
|
||||
format_number() {
|
||||
local NUM=$1
|
||||
if (( NUM >= 1000000 )); then
|
||||
printf "%.1fM" "$(echo "$NUM/1000000" | bc -l)"
|
||||
elif (( NUM >= 1000 )); then
|
||||
printf "%.0fK" "$(echo "$NUM/1000" | bc -l)"
|
||||
else
|
||||
printf "%d" "$NUM"
|
||||
fi
|
||||
}
|
||||
|
||||
downloads=$(format_number "$(get_total_downloads)")
|
||||
stars=$(format_number "$(get_stars)")
|
||||
|
||||
# Stats are baked into docs/readme/main.svg (one consolidated SVG that holds
|
||||
# the hero, stats, CTAs, and screenshot). Each stat text element is preceded by
|
||||
# an XML comment marker — anchor sed on the marker and replace whatever value
|
||||
# follows up to the next `<` (i.e. the closing `</text>`).
|
||||
sed -i "" -E "s|(<!--downloads-->)[^<]*|\1${downloads}|" "docs/readme/main.svg"
|
||||
sed -i "" -E "s|(<!--stars-->)[^<]*|\1${stars}|" "docs/readme/main.svg"
|
||||
|
||||
update_developer_contributors
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -exu
|
||||
|
||||
gh api repos/lwouis/alt-tab-website/dispatches -f event_type=update-website
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -exu
|
||||
|
||||
ownerName="alt-tab-macos"
|
||||
appName="alt-tab-macos"
|
||||
url="https://api.appcenter.ms/v0.1/apps/$ownerName/$appName"
|
||||
version="$(cat "$VERSION_FILE")"
|
||||
|
||||
# Upload a single .dSYM bundle to AppCenter (zip, request slot, PUT to blob, mark committed).
|
||||
# Called once per dSYM produced by the Release build:
|
||||
# - AltTab.app.dSYM host binary + everything statically linked (AppCenter, AppCenterCrashes, ShortcutRecorder)
|
||||
# - Sparkle.framework.dSYM Sparkle is the only .dynamic SPM product, so it has its own dSYM
|
||||
# CrashReporter.xcframework ships without dSYMs (PLCrashReporter is binary-only); AppCenter has
|
||||
# server-side symbols for it.
|
||||
function upload_dsym() {
|
||||
local symbolFile="$1"
|
||||
ditto -c -k --keepParent "$symbolFile" "$symbolFile.zip"
|
||||
|
||||
local c1 symbol_upload_id upload_url
|
||||
c1="$(curl -X POST "$url/symbol_uploads" \
|
||||
-H "X-API-Token: $APPCENTER_TOKEN" \
|
||||
-H "accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{ \"symbol_type\": \"Apple\", \"version\": \"$version\" }")"
|
||||
symbol_upload_id="$(jq -r '.symbol_upload_id' <<<"$c1")"
|
||||
upload_url="$(jq -r '.upload_url' <<<"$c1")"
|
||||
|
||||
curl -X PUT "$upload_url" \
|
||||
-H "x-ms-blob-type: BlockBlob" \
|
||||
--upload-file "$symbolFile.zip"
|
||||
|
||||
curl -X PATCH "$url/symbol_uploads/$symbol_upload_id" \
|
||||
-H "X-API-Token: $APPCENTER_TOKEN" \
|
||||
-H "accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "status": "committed" }'
|
||||
}
|
||||
|
||||
cd "$XCODE_BUILD_PATH"
|
||||
upload_dsym "$APP_NAME.app.dSYM"
|
||||
upload_dsym "Sparkle.framework.dSYM"
|
||||
+1
-1
@@ -13,7 +13,7 @@ class App: AppCenterApplication {
|
||||
static let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as! String
|
||||
static let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
|
||||
static let licence = Bundle.main.object(forInfoDictionaryKey: "NSHumanReadableCopyright") as! String
|
||||
static let repository = "https://github.com/KofTwentyTwo/alt-tab-free"
|
||||
static let repository = "https://github.com/KofTwentyTwo/CommandTabFree"
|
||||
static let appIconReps = CGImage.allNamed("app.icns")
|
||||
|
||||
static func appIcon(for size: NSSize) -> CGImage {
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user