Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Validate and build

on:
pull_request:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: validate-${{ github.ref }}
cancel-in-progress: true

jobs:
darwin:
runs-on: macos-26
timeout-minutes: 90
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
# Validate the contributor's history, without a synthetic merge identity.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: DeterminateSystems/determinate-nix-action@8d87e8d5e5b8a8309d4281094560f127d9a265f1 # v3.22.5
- name: Validate and build the public configuration
run: scripts/ci
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,17 @@ scripts/validate
# Build without changing the live system
scripts/rebuild build

# Review package and Homebrew/MAS declaration changes before activation
scripts/rebuild preview

# Build and activate local changes
scripts/rebuild switch

# Intentionally update pinned Nix inputs, Filen Menubar, and OMC, then build
scripts/update

# Check installed state without restoring or changing it
scripts/doctor
```

Homebrew and Mac App Store application removal is never automatic. Review
Expand All @@ -134,6 +140,8 @@ Homebrew and Mac App Store application removal is never automatic. Review
application settings
- [Post-install verification](docs/restore-verification.md) — thorough automated
and manual checks
- [Disposable-Mac rehearsal](docs/restore-rehearsal.md) — first activation,
interrupted restore, and repeated activation
- [Pre-wipe checklist](docs/pre-wipe-checklist.md) — required checks before
erasing an existing Mac
- [Public release safety](docs/public-release.md) — PII and Git-history policy
3 changes: 2 additions & 1 deletion docs/app-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ The `mini` host composes:
the pinned Erlang/OTP 29 plus Elixir 1.20 toolchain, and the pinned Rust
compiler, Cargo, formatter, linter, and language server;
- `desktop`: external-display support and desktop menu-bar behavior;
- `personal`: communication, news, media, archive, and document utilities;
- `personal`: communication, news, media, archive, document utilities, and
the pinned Filen Menubar application;
- `work`: individual Microsoft Office apps, Teams, and Slack;
- `gaming`: GeForce NOW.

Expand Down
11 changes: 11 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ Homebrew/MAS provide GUI and vendor applications. Homebrew cleanup, automatic
updates, and upgrades are disabled during activation; removal is always a
separate reviewed operation.

The development profile also imports the Home Manager development toolchain,
OMC, Zed settings, and GitHub CLI. The personal profile owns Filen Menubar.
The base Home Manager configuration keeps core tools and uses Vim unless the
development profile selects Zed. Pure flake checks verify both the full `mini`
composition and a base-only composition.

Otty's file and SSH URL associations run in Home Manager's user session after
Homebrew installation. Errors remain visible and file handlers are read back.
The generation also retains a public Brewfile and feature manifest under
`etc/mac-setup/` for previews and diagnostics.

Chezmoi and other dotfile managers are intentionally not part of this design.

## Bootstrap model
Expand Down
75 changes: 74 additions & 1 deletion docs/maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ scripts/validate
# Build without changing the live system
scripts/rebuild build

# Build and show package, closure-size, and Homebrew/MAS declaration changes
scripts/rebuild preview

# Build and activate
scripts/rebuild switch

Expand All @@ -19,12 +22,82 @@ scripts/update

# Compare declared Homebrew/MAS state with the live Mac
scripts/homebrew-dry-run

# Read-only post-install checks (use --json for a machine-readable report)
scripts/doctor
```

`scripts/validate` enters the pinned validation shell automatically when the
active generation does not yet provide a required tool. It therefore also
works before the first activation of a newly added validator.

`scripts/rebuild preview` compares the candidate with `/run/current-system`.
Each new generation saves its Brewfile under `etc/mac-setup/`; when the active
generation predates this metadata, the first preview prints all candidate
declarations. This compares intended app selection, not vendor-managed GUI
application versions. The same built output can be inspected again with
`scripts/preview-system ./result` without repeating validation or the build.

`scripts/update` prepares all three pin files in a temporary, filtered candidate
checkout, then validates and builds it once. Only a successful candidate whose
original checkout and host selectors remain unchanged is promoted. Failed
downloads, hashing, or builds leave the working pins unchanged. Promotion saves
an ignored recovery journal and restores its own changes on a catchable failure.
An uncatchable interruption can leave `.local/update-recovery.*`; inspect its
`original/` and `candidate/` files before resuming. Concurrent edits are preserved.
Check that no update is active before removing a stale `.local/update.lock`.
If the pins were already staged, restage their reviewed changes to satisfy index
and working-tree parity. No updated pins are committed or activated automatically.

## Rollback and recovery

List generations before choosing a rollback:

```bash
sudo darwin-rebuild --list-generations
```

After reviewing the target, restore the previous generation with:

```bash
sudo darwin-rebuild --rollback
```

For a particular listed generation, use
`sudo darwin-rebuild --switch-generation NUMBER`. This runs that generation's
activation. It restores Nix-managed packages and configuration, but does not
reverse vendor app updates, Homebrew/MAS installations, seeded writable settings,
1Password restores, keychain imports, or manual profile approvals. Its Homebrew
activation may install missing apps declared by that older generation.
Do not garbage-collect the known-good generation until recovery is verified.
After rollback, diagnose the checkout before applying it again; rollback does
not change Git files or dependency pins.

If Fish or the normal terminal is unavailable, use Terminal.app with `/bin/zsh`
and invoke `/run/current-system/sw/bin/darwin-rebuild` explicitly.

## CI and restore rehearsal

The macOS Actions workflow runs `scripts/ci`: the repository suite and all flake
checks, including the complete public system build and profile-composition
assertions. It uses Apple Silicon macOS, a pinned Determinate installer action,
full Git history, and read-only repository permissions. It never activates or
requires private state. Run it locally with `scripts/ci` when changing CI checks.
The hosted build uses macOS 26; it does not certify macOS 27 GUI behavior.

Use the [disposable-Mac rehearsal](restore-rehearsal.md) for activation,
interruption/resume, vendor approvals, and repeated activation.

## Checkout location and shortcuts

Setup and successful switching record the selected checkout outside Git at
`~/Library/Application Support/mac-setup/checkout`. The `mac-setup` launcher and
Fish's `rebuild`, `update`, `fishconf`, and `nixconf` use this private record.
`MAC_SETUP_CONFIG_DIR` overrides it for one invocation; the default remains
`~/.config/mac-setup` when no record exists. Candidate builds never change it.

## Release pin review

`scripts/update` queries GitHub for Filen Menubar's latest published stable
release. When a newer version exists, it requires the expected Apple Silicon
DMG, verifies the downloaded bytes against GitHub's release-asset SHA-256
Expand All @@ -49,7 +122,7 @@ never accepts cleanup, and separately installed apps can remain intentional.
`topgrade` updates supported user tools and package managers, including pnpm.
It deliberately skips Nix, Home Manager, npm-global packages, and its own
self-update because those have repository or project owners. Use
`scripts/update` for Nix inputs.
`scripts/update` for Nix inputs. Docker/container image updates are also disabled.

pnpm global executables live below `$PNPM_HOME/bin`, which activation creates
and Fish adds to `PATH`. Do not run `pnpm setup`; it would mutate shell
Expand Down
2 changes: 2 additions & 0 deletions docs/pre-wipe-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Do not erase the Mac until every final gate is green.
- [ ] `scripts/validate` passes.
- [ ] `scripts/rebuild build` succeeds twice without an unexpected second change.
- [ ] Bootstrap was rehearsed in build-only mode.
- [ ] The [disposable-Mac rehearsal](restore-rehearsal.md) covers first activation,
private-restore resume, and repeated activation for the selected revision.
- [ ] Homebrew cleanup remains `none`, or a cleanup dry-run has been reviewed line by line.
- [ ] Every required app/tool is declared or documented as a manual/vendor-synced restore.
- [ ] No Chezmoi dependency or restore step remains.
Expand Down
44 changes: 44 additions & 0 deletions docs/restore-rehearsal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Disposable-Mac restore rehearsal

Use a spare Apple Silicon Mac or a disposable macOS VM on Apple hardware.
Take a VM snapshot where supported. Perform this rehearsal on macOS 27 or newer
before relying on a clean restore. CI covers public builds, not interactive
macOS activation or vendor authentication.

Keep results outside the public repository: exact Git revision, macOS version,
chosen profiles/accounts, commands used, observed failures, and recovery steps.
Do not publish private account identifiers, machine paths, or authentication logs.

1. Follow the README's fresh-Mac prerequisites and build-only bootstrap. For an
unpublished candidate, copy a reviewed public checkout and use its `setup.sh
--config-dir` option. Record `git rev-parse HEAD` plus any uncommitted diff.
Use `scripts/rebuild preview` to inspect the candidate before proceeding.
2. Run `./setup.sh --config-dir "$PWD" --apply` inside that checkout. Complete
App Management approval if needed and use the exact printed resume command.
Confirm Fish is the login shell and double-click a harmless test shell script
from Finder. Verify that an `ssh://` link opens Otty without initiating a
connection to an unreviewed host.
3. Run `scripts/doctor --skip git --skip ssh --skip gpg --skip mail --skip filen`.
Confirm selected baseline checks pass and intentionally omitted private
components are reported as skipped. Run `mac-setup doctor` from outside the
checkout to exercise the recorded location and Fish shortcuts.
4. Start `scripts/finish-setup` with the account/skip options appropriate for the
rehearsal. Stop at a private-restore approval or sign-in prompt. Resume using
the exact `scripts/finish-setup` command printed by setup, or the same direct
invocation if interrupted with Control-C. Verify completed steps remain
intact; do not rerun the public activation to resume private restore.
5. Complete the selected restores and run `scripts/doctor` with matching
`--mail-account` and `--skip` options. Follow the separate interactive checks
in [post-install verification](restore-verification.md), including signed
Git commits, GPG fingerprints, S/MIME decryption, and representative sync.
6. Change an Otty appearance setting and a Zed setting. Run
`scripts/rebuild switch` again, then rerun doctor. Confirm those edits survive,
no duplicate profiles appear, Filen's agent remains loaded, and OMC setup has
not replaced existing user configuration. Check file and URL handlers again.
7. List generations and rehearse the documented rollback on this disposable
machine. Verify what Nix restores and record vendor or mutable state that
remains. Keep a known-good generation until these checks pass.

A rehearsal is complete only when the actual activation, interruption/resume,
and second activation have been observed. A passing CI job or two cached builds
alone does not complete it. Record any skipped components explicitly.
Loading
Loading