Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 27 additions & 25 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
name: Release

# Tag vX.Y.Z -> uniwersalny CloudMachine.app (Apple Silicon + Intel) podpisany
# STALYM certyfikatem, .dmg w GitHub Release i zaktualizowany cask w
# Tag vX.Y.Z -> universal CloudMachine.app (Apple Silicon + Intel) signed with
# a STABLE certificate, a .dmg in the GitHub Release and an updated cask in
# RenaCode/homebrew-tap (`brew install --cask renacode/tap/cloudmachine`).
#
# 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.
# A pull request that changes the build or the cask goes through the same
# pipeline as a dry run: ad-hoc signature, no release and no tap, artifacts
# downloadable from the run.
#
# 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.
# Secrets (tag only):
# CM_SIGNING_P12_BASE64, CM_SIGNING_P12_PASSWORD - the self-signed
# "CloudMachine Release Signing" certificate (see packaging/README.md).
# Without it the release does NOT build: an ad-hoc signature changes the
# app's identity on every release and macOS revokes Full Disk Access after
# every upgrade.
# HOMEBREW_TAP_TOKEN - token with write access to RenaCode/homebrew-tap.
#
# Nadal brak Developer ID i notaryzacji - Gatekeeper nie zna wydawcy; cask
# zdejmuje kwarantanne w postflight.
# Still no Developer ID and no notarization - Gatekeeper does not know the
# publisher; the cask removes the quarantine attribute in postflight.

on:
push:
Expand Down Expand Up @@ -49,7 +51,7 @@ jobs:
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."
echo "::error::Tag ${GITHUB_REF_NAME} != v${version} from mac-app/VERSION. Bump VERSION or fix the tag."
exit 1
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
Expand All @@ -65,7 +67,7 @@ jobs:
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."
echo "::error::CM_SIGNING_P12_* secrets are missing. An ad-hoc signed release would revoke Full Disk Access after every upgrade - aborting. See packaging/README.md."
exit 1
fi
keychain="$RUNNER_TEMP/signing.keychain-db"
Expand All @@ -80,8 +82,8 @@ jobs:
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.
# Add it to the search list, because `build-app` looks for the
# certificate with `security find-certificate -c` without naming a keychain.
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"
Expand All @@ -98,16 +100,16 @@ jobs:
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 ;;
echo "::error::$bin is not universal ($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.
# `build-app` silently falls back to ad-hoc when it cannot find the
# certificate - here that has to be an error, not a warning.
if [ "$IS_RELEASE" = "true" ] && ! grep -qF "Authority=$CM_SIGNING_CERT_NAME" <<<"$signature"; then
echo "::error::Wydanie nie jest podpisane certyfikatem '$CM_SIGNING_CERT_NAME'."
echo "::error::The release is not signed with the '$CM_SIGNING_CERT_NAME' certificate."
exit 1
fi

Expand All @@ -124,13 +126,13 @@ jobs:
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
echo "::error::An unfilled placeholder is left in the cask."; 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.
# `brew style` applies the cask rules only to files in a tap's Casks/ -
# on a loose file it checks it as plain Ruby and lets cask errors through.
- name: brew style + audit
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
Expand Down Expand Up @@ -167,7 +169,7 @@ jobs:
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:"
echo "::error::Release published, but HOMEBREW_TAP_TOKEN is missing - the cask in the tap was NOT updated. Contents to insert manually:"
cat mac-app/build/cloudmachine.rb
exit 1
fi
Expand All @@ -190,7 +192,7 @@ jobs:
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
echo "Cask unchanged."; exit 0
fi
git commit -m "cloudmachine ${{ steps.version.outputs.version }}"
git push
24 changes: 12 additions & 12 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
# Konfiguracja z danymi osobistymi (nazwy maszyn, limity) - kazdy uzytkownik ma wlasna
# Configuration with personal data (machine names, limits) - every user has their own
config/machines.json

# Logi
# Logs
*.log
logs/

# rclone config (zawiera tokeny OAuth) - nigdy nie commitowac
# rclone config (contains OAuth tokens) - never commit
rclone.conf
*.conf

# Wygenerowane pliki launchd (zawieraja lokalne sciezki uzytkownika)
# Generated launchd files (contain the user's local paths)
launchd/*.plist
!launchd/*.plist.template

# OS
.DS_Store

# Robocze/testowe sparsebundle uzywane recznie do eksperymentow z hdiutil/rclone -
# to sa binarne pliki testowe, nie kod projektu.
# Scratch/test sparsebundles used manually for hdiutil/rclone experiments -
# these are binary test files, not project code.
scratch/

# Globalny ~/.gitignore_global na tym koncie ignoruje katalogi "scripts" -
# to jest kluczowy kod tego projektu, wiec jawnie go odignorowujemy.
# The global ~/.gitignore_global on this account ignores "scripts" directories -
# that is key code for this project, so we explicitly un-ignore it.
!scripts/
!scripts/*.sh

# Lokalny stan narzedzi Claude / claude-flow / ruflo.
# To sa artefakty konkretnej maszyny i sesji (liczniki edycji, polityki,
# przyjeta konfiguracja) - nie opisuja projektu i nie naleza do repozytorium.
# Definicje agentow (.claude/agents/) i CLAUDE.md zostaja SLEDZONE celowo.
# Local state of the Claude / claude-flow / ruflo tools.
# These are artifacts of a specific machine and session (edit counters, policies,
# adopted configuration) - they do not describe the project and do not belong in
# the repository. Agent definitions (.claude/agents/) and CLAUDE.md stay TRACKED on purpose.
.claude-flow/
.claude/proven-config.json
.claude/.proven-config-version
Expand Down
46 changes: 24 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ else on the account. `operations/about` through the rclone rc gives real bytes.
- Nothing else at runtime. CloudMachine installs its own `rclone` and its own
copy of FUSE-T.

The menu-bar app, CLI output and notifications follow the system language:
Polish when Polish is the first preferred language, English otherwise.
`CM_LANGUAGE=en` or `CM_LANGUAGE=pl` overrides it. Logs are always English,
so they read the same whoever sends them to you.

### Current limitations

Worth knowing before you start, because none of them announce themselves:
Expand All @@ -122,9 +127,6 @@ Worth knowing before you start, because none of them announce themselves:
`config/machines.example.json` describes several machines with per-machine
`limit_gb` budgets, but no code enforces those budgets — what is checked is
the real free space on Drive, reported by rclone.
- **The interface speaks Polish.** The menu-bar app, the CLI output and the log
lines are in Polish; there is no language switch yet. The examples below quote
that output verbatim.
- **Not notarised.** Releases are signed with a self-signed certificate (local
builds ad hoc or with a local one, see below), not with an Apple Developer ID.
The Homebrew cask clears the quarantine flag; a DMG downloaded by hand gets
Expand Down Expand Up @@ -223,7 +225,7 @@ security add-generic-password -a client_secret -s cloudmachine-gdrive -w -U
Without `-w <value>`, `security` prompts — the secret stays out of your shell
history and out of `ps`.

The app window can do the same thing: the *Poświadczenia Google Drive
The app window can do the same thing: the *Google Drive Credentials
(OAuth 2.0)* card, folded away at the bottom since it is a once-ever step. It writes through the
`security` tool rather than the Keychain API on purpose — an entry created by
`SecItemAdd` gets an ACL limited to the program that made it, and reading it
Expand Down Expand Up @@ -258,31 +260,31 @@ cloudmachine-agent drive-status
```

```
Narzedzia: OK
Montowanie Drive: OK
Obraz podpiety: OK (/Volumes/CloudMachine)
Cache na dysku: 103 GB z 100G
Do wyslania: ~14 GB (462 pozycji)
Wolne na dysku: 288 GB
Kolejka wysylki: 0 w toku, 0 w kolejce, 0 bledow
Restart bez pytania: TAK - kolejka pusta
Wysylka: Wszystko wysłane na Google Drive
Cel Time Machine: /Volumes/CloudMachine
Backup: nie trwa
Tools: OK
Drive mount: OK
Image attached: OK (/Volumes/CloudMachine)
Cache on disk: 103 GB of 100G
To upload: ~14 GB (462 items)
Free on disk: 288 GB
Upload queue: 0 in progress, 0 queued, 0 errors
Restart without asking: YES - queue empty
Upload: Everything uploaded to Google Drive
TM destination: /Volumes/CloudMachine
Backup: not running
```

The number that matters is the upload queue. Until it returns to zero between
backups, part of the backup is still only on this Mac.

The `Wysylka:` line is the same verdict the app window shows, computed in one
The `Upload:` line is the same verdict the app window shows, computed in one
place so the two can never disagree. When it is not nominal it prints a second
line saying why, and whether it clears on its own.

It has three kinds of answer, not two. Besides "fine" and "broken" there is
**"unknown"** — printed when rclone does not answer the question about its
queue. That third state exists because of a specific lie: the queue read used
to time out, the caller substituted zeros for the missing numbers, and both the
CLI and the app then announced *Wszystko wysłane na Google Drive* while 386
CLI and the app then announced *Everything uploaded to Google Drive* while 386
bands sat unsent. A verdict computed from numbers nobody measured is worse than
no verdict, so now it says so.

Expand Down Expand Up @@ -316,9 +318,9 @@ cloudmachine-agent backup-health
```

```
Ostatnia udana kopia: 2026-09-12 18:25
Ostatnia proba: 2026-09-12 18:02
Cykl backupu: OK
Last successful backup: 2026-09-12 18:25
Last attempt: 2026-09-12 18:02
Backup cycle: OK
```

It reads the date of the last **completed** backup — `SnapshotDates` in
Expand Down Expand Up @@ -392,8 +394,8 @@ to report. Every run therefore drops its date into
`drive-status` and the app window show it:

```
Czujka backupu: 2026-09-25 22:04 (12 min temu)
Czujka backupu: 2026-09-22 03:10 (3 dni temu) - CZUJKA MOZE NIE CHODZIC
Backup watchdog: 2026-09-25 22:04 (12 min ago)
Backup watchdog: 2026-09-22 03:10 (3 days ago) - THE WATCHDOG MAY NOT BE RUNNING
```

The second line means nobody has been asking whether the backup works — not
Expand Down
12 changes: 6 additions & 6 deletions config/machines.example.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"_comment": "Skopiuj ten plik jako machines.json i dostosuj. Suma limit_gb obu Macow powinna zostac ponizej realnej pojemnosci dysku Google (zostaw margines bezpieczenstwa, np. 10-15%, na narzuty rclone/Google i przypadkowy wzrost miedzy sprawdzeniami watchdoga).",
"_comment": "Copy this file to machines.json and adjust it. The sum of limit_gb for both Macs should stay below the real capacity of the Google drive (leave a safety margin, e.g. 10-15%, for rclone/Google overhead and accidental growth between watchdog checks).",
"drive_total_gb": 5000,
"safety_margin_percent": 10,
"remote_name": "gdrive-cloudmachine",
"remote_root_folder": "CloudMachine",
"_bwlimit_comment": "Limit predkosci wysylania w Mbps (megabity/s, jak u dostawcow internetu) - to ustawienie jest per-Mac, wiec kazda maszyna moze miec inna wartosc w swoim lokalnym machines.json. 0 = bez limitu.",
"_bwlimit_comment": "Upload speed limit in Mbps (megabits/s, as internet providers quote it) - this setting is per-Mac, so each machine can have a different value in its local machines.json. 0 = no limit.",
"bwlimit_mbps": 0,
"machines": {
"macbook-pro-marcin": {
"display_name": "MacBook Pro Marcin",
"macbook-pro": {
"display_name": "MacBook Pro",
"limit_gb": 3000
},
"imac-domowy": {
"display_name": "iMac domowy",
"imac-home": {
"display_name": "iMac (home)",
"limit_gb": 1500
}
}
Expand Down
Loading
Loading