Skip to content

feat: Linux support — graceful automation skip on non-macOS (fixes #31) - #51

Open
YasienDwieb wants to merge 1 commit into
AlmanacCode:mainfrom
YasienDwieb:linux-support-option-a
Open

feat: Linux support — graceful automation skip on non-macOS (fixes #31)#51
YasienDwieb wants to merge 1 commit into
AlmanacCode:mainfrom
YasienDwieb:linux-support-option-a

Conversation

@YasienDwieb

Copy link
Copy Markdown

Fixes #31.

Problem

codealmanac setup --yes (and config set automation.*) crash on Linux:

codealmanac: launchctl bootstrap failed for com.codealmanac.sync: [Errno 2] No such file or directory: 'launchctl'

create_app unconditionally wires AutomationService with LaunchdSchedulerAdapter, which shells out to macOS launchctl. The crash also left a half-configured global state (config.toml written before the scheduler failed).

Approach — Option A: graceful skip, not a rewrite

Every lifecycle/read command is already platform-neutral; the only macOS blocker is scheduled automation, and SchedulerAdapter is already a port. So:

  • UnsupportedSchedulerAdapter — a no-op implementation of the existing SchedulerAdapter port (no subprocess, no ~/Library/LaunchAgents writes; reports "not installed").
  • core/platform.scheduler_supported() — single platform predicate. create_app selects launchd on macOS, the unsupported adapter elsewhere. Platform detection lives only at the composition root.
  • Render reflects the outcome, not the platform. AutomationTaskApplyResult gains a scheduled field set from the adapter's real result (install().installed). Setup output now says "Scheduled automation unavailable" with a "run sync/garden manually" pointer instead of falsely claiming schedules were installed. Render stays free of sys.platform checks.

Because nothing fails on the unsupported path, the half-configured-state bug is gone as a side effect — setup completes and exits 0.

Real Linux scheduling (systemd user timers) is deliberately left as a follow-up (Option B); this PR unblocks all Linux usage today.

Verification

  • uv run ruff check . clean; 570 tests pass (6 new in tests/test_linux_support.py, platform forced via sys.platform so they're host-independent).
  • Smoke-tested on real Ubuntu: the former crash path (config set automation.sync.enabled true) exits 0, automation status works, no launchd artifacts created.
  • macOS output is unchanged: launchd's install().installed is True, so every existing render branch is taken as before.

A design writeup is in docs/plans/2026-07-24-linux-support-option-a.md.

Select the scheduler adapter at the composition root: launchd on macOS, a
no-op UnsupportedSchedulerAdapter elsewhere. setup/config no longer crash on
launchctl off macOS; render reports scheduling unavailable from the adapter's
actual outcome via a new AutomationTaskApplyResult.scheduled field.
@Hotragn

Hotragn commented Aug 23, 2026

Copy link
Copy Markdown

@YasienDwieb I'm not a maintainer, but I mentioned this PR in #67 as the one that introduces the seam the other platform PRs work around, and it felt wrong to say that without testing it. I'm on Windows 11, so I checked it there — a platform this PR doesn't claim but does affect.

It works. Concrete A/B on the two commands that reach the scheduler.

On main:

$ codealmanac automation status
  File ".../integrations/automation/scheduler/launchd.py", line 147, in launchd_target
    return f"gui/{os.getuid()}"
AttributeError: module 'os' has no attribute 'getuid'. Did you mean: 'getpid'?

$ codealmanac config set automation.sync.enabled true
  ... same AttributeError

On this branch:

$ codealmanac automation status
sync automation: not installed
garden automation: not installed
update automation: not installed
   exit=0

$ codealmanac config set automation.sync.enabled true
config: automation.sync.enabled = true
   exit=0

I also confirmed the adapter genuinely touches nothing — no ~/Library/LaunchAgents directory is created on Windows, which is the failure I'd flagged in #31 where LaunchdSchedulerAdapter.install writes the plist before it boots. So the fix is real on Windows too, not just Linux, and scheduler_supported() reading sys.platform == "darwin" is the right shape for that.

Two things I found that aren't objections, just follow-ups you may or may not want in scope:

1. The suite is still red on Windows — the tests bypass the seam. uv run pytest on this branch gives 13 failed, 556 passed (your 6 new tests pass). The five test_automation_service.py failures remain because those tests construct LaunchdSchedulerAdapter() directly rather than going through the composition root, so they never see scheduler_supported():

src/codealmanac/integrations/automation/scheduler/launchd.py:147: AttributeError

That's arguably correct — they're macOS-behaviour tests — but it means this PR fixes the product without making the suite runnable off-darwin. A @pytest.mark.skipif(sys.platform != "darwin") on that class would finish the job. I mention it only because "does CI go green" is likely the first thing a reviewer checks.

2. plist_path still leaks a macOS path into the non-macOS --json contract. not_installed() returns plist_path=job.plist_path, and plist_path_for is unconditional, so on Windows:

{
  "task": "sync",
  "label": "com.codealmanac.sync",
  "plist_path": "C:\Users\...\Library\LaunchAgents\com.codealmanac.sync.plist",
  "installed": false
}

A Windows-shaped path to a launchd artifact that can never exist. Nothing is written, so it's cosmetic rather than a bug — but it's the field I argued in #67 has leaked from integrations/ up into the service-layer ScheduledJob/ScheduledJobStatus models and the public JSON surface. Genuinely out of scope for this PR; I'd rather see this land as-is than see it grow.

For what it's worth I still think this is the PR that should go in first out #48/#32/#51, because it's the only one that adds a place for the decision to live rather than guarding another call site. If it lands I'm happy to send the skipif markers as a follow-up rather than bundling them here, and I'll keep testing on Windows for anyone who wants it — per #24 the team doesn't have a box for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: setup --yes crashes on Linux because scheduled automation is macOS/launchd-only

2 participants