From 8efcb1cac830f448652b23217b9463920a897ac0 Mon Sep 17 00:00:00 2001 From: Marcin Beczynski Date: Tue, 6 Oct 2026 07:43:36 +0200 Subject: [PATCH 1/4] fix(sciezki): agent wolany z PATH wie, gdzie lezy `CommandLine.arguments[0]` przy wywolaniu z PATH to sama nazwa, a `URL(fileURLWithPath:)` doklejala ja do biezacego katalogu. Skutek: `cd /tmp && cloudmachine-agent version` meldowal "Build z drzewa roboczego" zamiast 1.2.0, a `install-launchd` wpisalby launchd nieistniejaca `/tmp/cloudmachine-agent`. Dotyczy dowiazania w /usr/local/bin i kazdej instalacji z Homebrew. Sciezke bierzemy z `Bundle.main.executableURL` (od jadra) po rozwinieciu dowiazan; Resources szukamy tez obok prawdziwej binarki. Co-Authored-By: Claude Opus 5.5 --- .../Sources/CloudMachineCore/AppVersion.swift | 2 +- .../Sources/CloudMachineCore/CMPaths.swift | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/mac-app/Sources/CloudMachineCore/AppVersion.swift b/mac-app/Sources/CloudMachineCore/AppVersion.swift index 1b5fe95..d0e3fed 100644 --- a/mac-app/Sources/CloudMachineCore/AppVersion.swift +++ b/mac-app/Sources/CloudMachineCore/AppVersion.swift @@ -78,7 +78,7 @@ public enum AppVersionReader { /// Przy `swift run` zadnego bundla nie ma i to nie jest blad - zwracamy /// `nil`, a wolajacy mowi wprost, ze to build z drzewa roboczego. public static func current( - executable: URL = URL(fileURLWithPath: CommandLine.arguments[0]).resolvingSymlinksInPath() + executable: URL = CMPaths.runningExecutable ) -> AppVersion? { let infoPlist = executable diff --git a/mac-app/Sources/CloudMachineCore/CMPaths.swift b/mac-app/Sources/CloudMachineCore/CMPaths.swift index 17afb12..59d928e 100644 --- a/mac-app/Sources/CloudMachineCore/CMPaths.swift +++ b/mac-app/Sources/CloudMachineCore/CMPaths.swift @@ -9,6 +9,20 @@ import Foundation /// Jedno miejsce prawdy dla GUI i CLI - wczesniej ta sama logika byla /// zduplikowana (raz jako common.sh, raz czesciowo w CloudMachineController). public enum CMPaths { + /// Prawdziwa sciezka do dzialajacej binarki, po rozwinieciu dowiazan. + /// + /// NIE `CommandLine.arguments[0]`: przy wywolaniu z PATH (dowiazanie + /// `/usr/local/bin/cloudmachine-agent`, binarka z Homebrew) powloka podaje + /// tam sama nazwe, a `URL(fileURLWithPath:)` dokleja ja do biezacego + /// katalogu. `cd /tmp && cloudmachine-agent version` meldowal wtedy "Build + /// z drzewa roboczego", a `install-launchd` wskazalby launchd binarke + /// `/tmp/cloudmachine-agent`, ktorej nie ma. `Bundle.main.executableURL` + /// bierze sciezke od jadra, niezaleznie od tego, jak polecenie wpisano. + public static var runningExecutable: URL { + (Bundle.main.executableURL ?? URL(fileURLWithPath: CommandLine.arguments[0])) + .resolvingSymlinksInPath() + } + /// Katalog z zasobami projektu (`launchd/`, `config/`) - w .app to /// `Contents/Resources`, w checkoutcie deweloperskim to korzen repo /// (rodzic `mac-app/`). `nil`, jesli zaden z tych katalogow nie istnieje @@ -23,7 +37,16 @@ public enum CMPaths { // ta binarka siedzi pod mac-app/.build///, wiec korzen // repo to 5 poziomow wyzej. Sprawdzamy tez plytsza sciezke na wypadek // uruchomienia bezposrednio z katalogu mac-app. - let exeDir = URL(fileURLWithPath: CommandLine.arguments[0]).deletingLastPathComponent() + let exeDir = runningExecutable.deletingLastPathComponent() + // Agent wolany przez dowiazanie: `Bundle.main` bywa wtedy liczony od + // katalogu dowiazania, nie od .app - wiec Resources szukamy tez obok + // prawdziwej binarki (Contents/MacOS -> Contents/Resources). + let bundleResources = exeDir.deletingLastPathComponent().appendingPathComponent("Resources") + if FileManager.default.fileExists( + atPath: bundleResources.appendingPathComponent("launchd").path) + { + return bundleResources + } var candidate = exeDir for _ in 0..<6 { if FileManager.default.fileExists(atPath: candidate.appendingPathComponent("launchd").path) { @@ -70,9 +93,9 @@ public enum CMPaths { /// 3. Fallback dla GUI uruchomionego przez `swift run` w drzewie repo - /// szukamy `cloudmachine-agent` w `.build/*/{release,debug}/` obok binarki GUI. public static var agentBinaryPath: URL? { - let selfURL = URL(fileURLWithPath: CommandLine.arguments[0]) + let selfURL = runningExecutable if selfURL.lastPathComponent == "cloudmachine-agent" { - return selfURL.standardizedFileURL + return selfURL } if let bundled = Bundle.main.executableURL?.deletingLastPathComponent().appendingPathComponent( "cloudmachine-agent"), From 5a40e7aa24a59f0d707dfe9ac5f62e7d7fa6b2ae Mon Sep 17 00:00:00 2001 From: Marcin Beczynski Date: Tue, 6 Oct 2026 07:43:36 +0200 Subject: [PATCH 2/4] fix(podpis): setup-signing-cert dziala z LibreSSL z macOS `/usr/bin/openssl` to LibreSSL 3.3.6, ktory nie zna `-legacy` i konczy `pkcs12 -export` bledem - komenda padala na kazdym nowym Macu. Flage dostaje juz tylko prawdziwy OpenSSL 3; LibreSSL i tak domyslnie pisze 3DES/RC2. Co-Authored-By: Claude Opus 5.5 --- .../CloudMachineAgent/SetupSigningCertCommand.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mac-app/Sources/CloudMachineAgent/SetupSigningCertCommand.swift b/mac-app/Sources/CloudMachineAgent/SetupSigningCertCommand.swift index 1313787..fedc9c4 100644 --- a/mac-app/Sources/CloudMachineAgent/SetupSigningCertCommand.swift +++ b/mac-app/Sources/CloudMachineAgent/SetupSigningCertCommand.swift @@ -74,10 +74,17 @@ struct SetupSigningCert: AsyncParsableCommand { // nie rozumie - bez tej flagi import konczy sie mylacym "MAC // verification failed (wrong password?)" mimo poprawnego hasla. -legacy // wraca do 3DES/RC2, ktore macOS poprawnie parsuje. + // + // Ale `/usr/bin/openssl` na macOS to LibreSSL, ktory flagi -legacy NIE + // ZNA i konczy sie bledem (sprawdzone na LibreSSL 3.3.6) - a 3DES/RC2 ma + // juz domyslnie. Flage dokladamy wiec tylko prawdziwemu OpenSSL 3. + let versionOutput = + (try? await ProcessRunner.run("/usr/bin/openssl", ["version"]))?.stdout ?? "" + let legacyFlag = versionOutput.hasPrefix("OpenSSL 3") ? ["-legacy"] : [] let pkcs12Status = try await InteractiveProcess.run( "/usr/bin/openssl", - [ - "pkcs12", "-export", "-legacy", "-out", p12File.path, "-inkey", keyFile.path, + ["pkcs12", "-export"] + legacyFlag + [ + "-out", p12File.path, "-inkey", keyFile.path, "-in", certFile.path, "-passout", "pass:cloudmachine-local", ]) guard pkcs12Status == 0 else { From 21d90bd21a2c3c992901cec2f8136f521509e361 Mon Sep 17 00:00:00 2001 From: Marcin Beczynski Date: Tue, 6 Oct 2026 07:43:36 +0200 Subject: [PATCH 3/4] feat(build): build-app --universal (Apple Silicon + Intel) Katalog z binarkami bierzemy z `swift build --show-bin-path`, bo przy kilku architekturach SwiftPM nie uzywa `.build/release`, a nazwa katalogu zmienia sie miedzy wersjami narzedzi. Co-Authored-By: Claude Opus 5.5 --- .../CloudMachineAgent/BuildAppCommand.swift | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/mac-app/Sources/CloudMachineAgent/BuildAppCommand.swift b/mac-app/Sources/CloudMachineAgent/BuildAppCommand.swift index 3f7dd87..d2b3daa 100644 --- a/mac-app/Sources/CloudMachineAgent/BuildAppCommand.swift +++ b/mac-app/Sources/CloudMachineAgent/BuildAppCommand.swift @@ -20,6 +20,12 @@ struct BuildApp: AsyncParsableCommand { "Buduje CloudMachine.app (Release) - GUI + cloudmachine-agent w Contents/MacOS/, plus launchd/config jako Resources." ) + @Flag( + name: .long, + help: + "Binarki dla Apple Silicon i Intela naraz (tak buduje wydanie w CI; lokalnie zbedne).") + var universal = false + func run() async throws { let macAppRoot = BuildPaths.macAppRoot let projectRoot = BuildPaths.projectRoot @@ -41,15 +47,31 @@ struct BuildApp: AsyncParsableCommand { // swift.org installer, TOOLCHAINS env var) moga miec inny `swift` niz // ten domyslny z Xcode. Oryginalny bash robil to samo (`swift build` // bez sciezki, resolved przez PATH powloki). - let buildStatus = try await InteractiveProcess.run( - "/usr/bin/env", ["swift", "build", "-c", "release", "--package-path", macAppRoot.path]) + let swiftArgs = + ["build", "-c", "release", "--package-path", macAppRoot.path] + + (universal ? ["--arch", "arm64", "--arch", "x86_64"] : []) + let buildStatus = try await InteractiveProcess.run("/usr/bin/env", ["swift"] + swiftArgs) guard buildStatus == 0 else { print("BLAD: swift build zakonczyl sie kodem \(buildStatus).") throw ExitCode.failure } - let appBinPath = macAppRoot.appendingPathComponent(".build/release/\(appName)App") - let agentBinPath = macAppRoot.appendingPathComponent(".build/release/cloudmachine-agent") + // Katalog z binarkami podaje sam SwiftPM: przy kilku architekturach to + // nie `.build/release`, tylko katalog zalezny od wersji narzedzi + // (`.build/apple/...` albo `.build/out/...`) - zgadywanie go zepsuloby + // sie przy pierwszej aktualizacji Xcode. + guard + let binPathResult = try? await ProcessRunner.run( + "/usr/bin/env", ["swift"] + swiftArgs + ["--show-bin-path"]), + binPathResult.succeeded + else { + print("BLAD: swift build --show-bin-path nie podal katalogu z binarkami.") + throw ExitCode.failure + } + let binDir = URL( + fileURLWithPath: binPathResult.stdout.trimmingCharacters(in: .whitespacesAndNewlines)) + let appBinPath = binDir.appendingPathComponent("\(appName)App") + let agentBinPath = binDir.appendingPathComponent("cloudmachine-agent") for path in [appBinPath, agentBinPath] { guard fm.fileExists(atPath: path.path) else { print("BLAD: nie znaleziono zbudowanej binarki pod \(path.path)") From 6e7b1fd9d5e60f205507a24569017757b9e9152e Mon Sep 17 00:00:00 2001 From: Marcin Beczynski Date: Tue, 6 Oct 2026 07:43:36 +0200 Subject: [PATCH 4/4] ci(release): podpisane wydanie uniwersalne i cask Homebrew Tag vX.Y.Z: testy, uniwersalny CloudMachine.app podpisany stalym certyfikatem, DMG + sha256 w GitHub Release, cask wypychany do RenaCode/homebrew-tap (`brew install --cask renacode/tap/cloudmachine`). Wydanie bez certyfikatu sie nie buduje: podpis ad-hoc cofalby Pelny dostep do dysku po kazdym `brew upgrade`. Tag niezgodny z mac-app/VERSION tez. PR dotykajacy budowania przechodzi ten sam potok na sucho. Cask nie zatrzymuje agentow launchd (Homebrew wykonuje `uninstall` takze przy upgradzie, a gdrive-buffer trzyma montowanie) i `zap` nie rusza ~/.cloudmachine (bufor niewyslanych kopii). Konfiguracja: packaging/README.md. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 182 ++++++++++++++++++++++++-- packaging/README.md | 101 ++++++++++++++ packaging/homebrew/cloudmachine.rb.in | 67 ++++++++++ 3 files changed, 337 insertions(+), 13 deletions(-) create mode 100644 packaging/README.md create mode 100644 packaging/homebrew/cloudmachine.rb.in diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f46ae4b..b842fa6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,22 +1,41 @@ -name: Release DMG +name: Release -# Buduje i publikuje CloudMachine-.dmg jako zalacznik GitHub Release -# przy kazdym pushu tagu w formacie vX.Y.Z. Podpis pozostaje ad-hoc (bez -# certyfikatu Apple Developer) - Gatekeeper nadal pokaze ostrzezenie -# "niezidentyfikowany deweloper" przy pierwszym uruchomieniu, patrz README. +# Tag vX.Y.Z -> uniwersalny CloudMachine.app (Apple Silicon + Intel) podpisany +# STALYM certyfikatem, .dmg w GitHub Release i zaktualizowany cask w +# RenaCode/homebrew-tap (`brew install --cask renacode/tap/cloudmachine`). # -# TODO (po zalozeniu konta Apple Developer): dodac krok `codesign` z realnym -# Developer ID certyfikatem (zaimportowanym z sekretow repo) i `notarytool` -# przed `make-dmg`, zeby usunac to ostrzezenie. +# Pull request zmieniajacy budowanie albo cask przechodzi ten sam potok na +# sucho: podpis ad-hoc, bez wydania i bez tapu, artefakty do pobrania z runu. +# +# Sekrety (tylko dla tagu): +# CM_SIGNING_P12_BASE64, CM_SIGNING_P12_PASSWORD - certyfikat self-signed +# "CloudMachine Release Signing" (patrz packaging/README.md). Bez niego +# wydanie sie NIE buduje: podpis ad-hoc zmienia tozsamosc appki przy +# kazdym wydaniu i macOS cofa Pelny dostep do dysku po kazdym upgradzie. +# HOMEBREW_TAP_TOKEN - token z prawem zapisu do RenaCode/homebrew-tap. +# +# Nadal brak Developer ID i notaryzacji - Gatekeeper nie zna wydawcy; cask +# zdejmuje kwarantanne w postflight. on: push: tags: - "v*.*.*" + pull_request: + paths: + - ".github/workflows/release.yml" + - "packaging/**" + - "mac-app/Sources/CloudMachineAgent/BuildAppCommand.swift" + - "mac-app/Sources/CloudMachineAgent/MakeDmgCommand.swift" + - "mac-app/Resources/Info.plist" + +env: + CM_SIGNING_CERT_NAME: CloudMachine Release Signing + IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/v') }} jobs: - build-and-release: - name: Build DMG and publish release + release: + name: Build, release, update tap runs-on: macos-14 permissions: contents: write @@ -25,16 +44,153 @@ jobs: with: fetch-depth: 0 - - name: Build .app (ad-hoc signed) + - name: Tag matches mac-app/VERSION + id: version + run: | + version="$(tr -d '[:space:]' < mac-app/VERSION)" + if [ "$IS_RELEASE" = "true" ] && [ "${GITHUB_REF_NAME}" != "v${version}" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} != v${version} z mac-app/VERSION. Podbij VERSION albo popraw tag." + exit 1 + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + + - name: swift test + working-directory: mac-app + run: swift test + + - name: Import signing certificate + if: env.IS_RELEASE == 'true' + env: + P12_BASE64: ${{ secrets.CM_SIGNING_P12_BASE64 }} + P12_PASSWORD: ${{ secrets.CM_SIGNING_P12_PASSWORD }} + run: | + if [ -z "$P12_BASE64" ] || [ -z "$P12_PASSWORD" ]; then + echo "::error::Brak sekretow CM_SIGNING_P12_*. Wydanie podpisane ad-hoc cofaloby Pelny dostep do dysku po kazdym upgradzie - przerywam. Patrz packaging/README.md." + exit 1 + fi + keychain="$RUNNER_TEMP/signing.keychain-db" + keychain_password="$(openssl rand -hex 16)" + printf '%s' "$P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12" + /usr/bin/openssl pkcs12 -in "$RUNNER_TEMP/cert.p12" -nokeys \ + -passin "pass:$P12_PASSWORD" -out "$RUNNER_TEMP/cert.pem" + + security create-keychain -p "$keychain_password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$keychain_password" "$keychain" + security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$P12_PASSWORD" \ + -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" + # Dopisujemy do listy wyszukiwania, bo `build-app` szuka certyfikatu + # przez `security find-certificate -c` bez wskazania keychaina. + security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"') + sudo security add-trusted-cert -d -r trustRoot -p codeSign \ + -k /Library/Keychains/System.keychain "$RUNNER_TEMP/cert.pem" + rm -f "$RUNNER_TEMP/cert.p12" + + - name: Build CloudMachine.app (universal) + working-directory: mac-app + run: swift run cloudmachine-agent build-app --universal + + - name: Verify architectures and signature working-directory: mac-app - run: swift run cloudmachine-agent build-app + run: | + for bin in CloudMachine cloudmachine-agent; do + archs="$(lipo -archs "build/CloudMachine.app/Contents/MacOS/$bin")" + echo "$bin: $archs" + case "$archs" in *arm64*x86_64*|*x86_64*arm64*) ;; *) + echo "::error::$bin nie jest uniwersalny ($archs)"; exit 1 ;; + esac + done + codesign --verify --deep --strict build/CloudMachine.app + signature="$(codesign -dv --verbose=2 build/CloudMachine.app 2>&1)" + echo "$signature" + # `build-app` po cichu spada do ad-hoc, gdy nie znajdzie certyfikatu - + # tu to musi byc blad, nie ostrzezenie. + if [ "$IS_RELEASE" = "true" ] && ! grep -qF "Authority=$CM_SIGNING_CERT_NAME" <<<"$signature"; then + echo "::error::Wydanie nie jest podpisane certyfikatem '$CM_SIGNING_CERT_NAME'." + exit 1 + fi - name: Package .dmg working-directory: mac-app run: swift run cloudmachine-agent make-dmg + - name: Render cask + id: cask + run: | + version="${{ steps.version.outputs.version }}" + dmg="mac-app/build/CloudMachine-${version}.dmg" + sha256="$(shasum -a 256 "$dmg" | cut -d' ' -f1)" + sed -e "s/__VERSION__/${version}/" -e "s/__SHA256__/${sha256}/" \ + packaging/homebrew/cloudmachine.rb.in > mac-app/build/cloudmachine.rb + if grep -q '__[A-Z0-9]*__' mac-app/build/cloudmachine.rb; then + echo "::error::W casku zostal niewypelniony znacznik."; exit 1 + fi + echo "${sha256} CloudMachine-${version}.dmg" > "mac-app/build/CloudMachine-${version}.dmg.sha256" + echo "dmg=${dmg}" >> "$GITHUB_OUTPUT" + + # Reguly dla caskow `brew style` stosuje tylko do plikow w Casks/ tapu - + # na luznym pliku sprawdza go jak zwykly Ruby i przepuszcza bledy caska. + - name: brew style + audit + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + run: | + brew tap-new --no-git renacode/ci-check + tap="$(brew --repository renacode/ci-check)" + mkdir -p "$tap/Casks" + cp mac-app/build/cloudmachine.rb "$tap/Casks/cloudmachine.rb" + brew style --cask renacode/ci-check/cloudmachine + brew audit --cask --strict renacode/ci-check/cloudmachine + + - name: Upload artifacts (dry run) + if: env.IS_RELEASE != 'true' + uses: actions/upload-artifact@v4 + with: + name: cloudmachine-${{ steps.version.outputs.version }}-dry-run + path: | + mac-app/build/*.dmg + mac-app/build/*.sha256 + mac-app/build/cloudmachine.rb + - name: Publish GitHub Release + if: env.IS_RELEASE == 'true' uses: softprops/action-gh-release@v2 with: - files: mac-app/build/*.dmg + files: | + mac-app/build/*.dmg + mac-app/build/*.sha256 generate_release_notes: true + + - name: Check tap token + if: env.IS_RELEASE == 'true' + env: + TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + run: | + if [ -z "$TAP_TOKEN" ]; then + echo "::error::Wydanie opublikowane, ale brak HOMEBREW_TAP_TOKEN - cask w tapie NIE zostal zaktualizowany. Zawartosc do recznego wstawienia:" + cat mac-app/build/cloudmachine.rb + exit 1 + fi + + - name: Check out tap + if: env.IS_RELEASE == 'true' + uses: actions/checkout@v4 + with: + repository: RenaCode/homebrew-tap + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + path: homebrew-tap + + - name: Push cask to tap + if: env.IS_RELEASE == 'true' + working-directory: homebrew-tap + run: | + mkdir -p Casks + cp ../mac-app/build/cloudmachine.rb Casks/cloudmachine.rb + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Casks/cloudmachine.rb + if git diff --cached --quiet; then + echo "Cask bez zmian."; exit 0 + fi + git commit -m "cloudmachine ${{ steps.version.outputs.version }}" + git push diff --git a/packaging/README.md b/packaging/README.md new file mode 100644 index 0000000..27d6660 --- /dev/null +++ b/packaging/README.md @@ -0,0 +1,101 @@ +# Packaging: releases and Homebrew + +```sh +brew install --cask renacode/tap/cloudmachine +``` + +The cask lives in [RenaCode/homebrew-tap](https://github.com/RenaCode/homebrew-tap) +and is generated: `.github/workflows/release.yml` fills +`homebrew/cloudmachine.rb.in` with the version and the DMG's sha256 and pushes +it to the tap. Edit the template here, never the copy in the tap. + +## Cutting a release + +1. Bump `mac-app/VERSION` on `main`. +2. `git tag v$(cat mac-app/VERSION) && git push origin --tags` + +The workflow refuses a tag that does not match `VERSION`, runs the tests, +builds a universal (Apple Silicon + Intel) `CloudMachine.app`, publishes +`CloudMachine-.dmg` with its `.sha256` as a GitHub Release, and +updates the cask. + +A pull request that touches the build or the cask runs the same pipeline dry: +ad-hoc signature, no release, no tap push; the DMG and the rendered cask are +attached to the run as artifacts. + +## One-time setup + +### 1. Signing certificate (`CM_SIGNING_P12_BASE64`, `CM_SIGNING_P12_PASSWORD`) + +Releases are signed with a self-signed certificate named +`CloudMachine Release Signing`. It is not an Apple Developer ID and does not +satisfy Gatekeeper; its only job is a **stable identity**. An ad-hoc signature +changes with every build, and macOS silently withdraws Full Disk Access after +every upgrade. The release job fails rather than publish an ad-hoc build. + +Generate it once and store it as repository secrets; the key never stays on +disk: + +```sh +work="$(mktemp -d)" +cat >"$work/cnf" <<'CNF' +[req] +distinguished_name = dn +x509_extensions = v3 +prompt = no +[dn] +CN = CloudMachine Release Signing +[v3] +keyUsage = critical, digitalSignature +extendedKeyUsage = critical, codeSigning +basicConstraints = critical, CA:false +CNF +/usr/bin/openssl req -x509 -newkey rsa:2048 -sha256 -days 7300 -nodes \ + -config "$work/cnf" -keyout "$work/key.pem" -out "$work/cert.pem" +pass="$(/usr/bin/openssl rand -hex 24)" +/usr/bin/openssl pkcs12 -export -inkey "$work/key.pem" -in "$work/cert.pem" \ + -out "$work/cert.p12" -passout "pass:$pass" +base64 -i "$work/cert.p12" | gh secret set CM_SIGNING_P12_BASE64 -R RenaCode/CloudMachine +printf '%s' "$pass" | gh secret set CM_SIGNING_P12_PASSWORD -R RenaCode/CloudMachine +rm -rf "$work" +``` + +Do this **once**. A new certificate is a new identity: every Mac would have to +grant Full Disk Access again. + +### 2. The tap repository and `HOMEBREW_TAP_TOKEN` + +1. Create the public repository `RenaCode/homebrew-tap` (empty is fine; the + workflow creates `Casks/`). +2. Create a fine-grained token limited to that repository with + *Contents: Read and write*, and store it: + `gh secret set HOMEBREW_TAP_TOKEN -R RenaCode/CloudMachine` + +Without the token the release is still published, but the job fails and +prints the cask, so a missing tap update cannot go unnoticed. + +## What the cask does and deliberately does not do + +- **Removes the quarantine attribute** after install. The app is not notarized, + so Gatekeeper would otherwise block it. +- **Does not stop launchd agents** on uninstall. Homebrew runs `uninstall` + directives on `brew upgrade` too, and stopping `gdrive-buffer` kills the + rclone process that holds the mount. +- **Does not reload agents** after an upgrade. Cask install steps run in a + sandbox without access to `~/Library/LaunchAgents`, and an upgrade replaces + the bundle with new files, after which the agents start normally. + `drive-status` reports a watchdog that stopped running. +- **`zap` leaves `~/.cloudmachine` alone.** It holds the upload buffer, which + may contain backups that have not reached Google Drive yet. + +## Already installed from source? + +`brew install` refuses to overwrite an existing `/Applications/CloudMachine.app`. +Take it over with `--adopt`, run from a terminal, not unattended: + +```sh +brew install --cask --adopt renacode/tap/cloudmachine +``` + +The release certificate differs from the local `CloudMachine Local Signing` +one, so grant Full Disk Access once more afterwards. diff --git a/packaging/homebrew/cloudmachine.rb.in b/packaging/homebrew/cloudmachine.rb.in new file mode 100644 index 0000000..110af3d --- /dev/null +++ b/packaging/homebrew/cloudmachine.rb.in @@ -0,0 +1,67 @@ +# Szablon caska. Workflow `release.yml` wstawia wersje i sha256 w miejsce +# znacznikow i wypycha wynik do RenaCode/homebrew-tap jako +# Casks/cloudmachine.rb. Edytuj TEN plik - kopie w tapie nadpisze nastepne +# wydanie. +cask "cloudmachine" do + version "__VERSION__" + sha256 "__SHA256__" + + url "https://github.com/RenaCode/CloudMachine/releases/download/v#{version}/CloudMachine-#{version}.dmg" + name "CloudMachine" + desc "Time Machine backups to Google Drive" + homepage "https://github.com/RenaCode/CloudMachine" + + livecheck do + url :url + strategy :github_latest + end + + depends_on macos: :sonoma + + app "CloudMachine.app" + + # Wydanie nie jest notaryzowane (brak konta Apple Developer), wiec Gatekeeper + # zablokowalby pobrana kopie. Cask pochodzi z tapu autora, a plik zgadza sie + # z sha256 powyzej. + # + # Agentow launchd cask NIE przeladowuje: kroki dzialaja w sandboxie + # Homebrew bez dostepu do ~/Library/LaunchAgents. Nie jest to potrzebne - + # upgrade podmienia bundle na nowe pliki (nowe inode'y), a to wlasnie ta + # procedura, po ktorej agenci startuja poprawnie. Gdyby czujka jednak + # stanela, `drive-status` i okno appki pokaza "CZUJKA MOZE NIE CHODZIC". + postflight_steps do + run "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "{{appdir}}/CloudMachine.app"] + end + + # Celowo BEZ `uninstall launchctl:` - Homebrew wykonuje dyrektywy + # `uninstall` takze przy `brew upgrade`, a zatrzymanie + # com.renacode.cloudmachine.gdrive-buffer zabija rclone, ktory trzyma + # montowanie: obraz wraca dopiero po ~20 min, a przerwana wysylka potrafi + # kosztowac katalog glowny wolumenu. Agentow zdejmuje sie recznie (caveats). + uninstall quit: "com.renacode.cloudmachine" + + # Celowo bez ~/.cloudmachine: tam lezy bufor wysylki, czyli kopie jeszcze + # niewyslane na Google Drive. Skasowanie go niszczy backup. + zap trash: [ + "~/Library/Logs/CloudMachine", + "~/Library/Preferences/com.renacode.cloudmachine.plist", + ] + + caveats <<~EOS + First-time setup (the agent lives inside the app bundle): + /Applications/CloudMachine.app/Contents/MacOS/cloudmachine-agent --help + See https://github.com/RenaCode/CloudMachine#setup for the full sequence. + + `brew upgrade` keeps the Google Drive mount running. Afterwards, check + that the backup watchdog still runs: `cloudmachine-agent drive-status`. + + Uninstalling does NOT stop the launchd agents or delete the upload buffer + in ~/.cloudmachine (it may hold backups not yet sent to Google Drive). + Before `brew uninstall`, run: + cloudmachine-agent prepare-shutdown + then remove ~/Library/LaunchAgents/com.renacode.cloudmachine.*.plist. + + Full Disk Access is tied to the release signing certificate: grant it once + and it survives upgrades. Moving from a locally built copy needs one re-grant. + EOS +end