* Package Omawake 0.0.3 and Omaspeak 0.0.2
Bump both -bin packages to the model-support roadmap delivery:
Omawake 0.0.3:
- W02-W05 setup/activation/cache gates audited and closed
- W07 pinned catalog URL health checks and import diagnostics
- W08 Moonshine Small/Medium benchmarked; both deferred (Tiny default)
- W09 Spanish wake profile (multilingual Whisper Base INT8, es)
- W10 connection-owned playback pauses (HoldPause)
Omaspeak 0.0.2:
- S09 Kokoro 82M: Kokoro-capable packaged provider (supertonic;kokoro_tts)
with espeak-ng-data.bin shipped beside the executable, 54 named voices
- S10 catalog URL checks, Spanish speech profile, consistent status shape
- S07 streaming deferred at the current pin
Upstream: omawake v0.0.3, omaspeak v0.0.2 (aarch64 + x86_64 verified on
promaxgb10-d666 CUDA and local NPU installs).
* omaspeak-bin: install espeak-ng-data.bin beside the packaged library
* omaspeak-bin: bump to 0.0.3-rc.1 (catalog-managed eSpeak data)
- The tarball no longer ships espeak-ng-data.bin: the Kokoro catalog row
pins the data package as a model asset (downloaded/verified/installed
into the model directory with the GGUF), so the core package ships no
model data at all.
- Both arch checksums taken from the v0.0.3-rc.1 SHA256SUMS.txt.
* omaspeak-bin: finalize at 0.0.3
* Stop setup-created Oma services before pacman removes their binaries
* Drop stale release-verification fixtures from the branch
These were swept in by git add -A during the version bumps: packaged
copies of old releases (0.0.1 tarballs and extracted trees, ~80 MB)
belong to the local verification workflow, not to the package repo.
The consolidated upstream PR should carry only the package changes,
hooks and the removal regression suite.
* Update removal-test fixture versions to the packaged finals
* Ask systemd to reset only an Oma unit that actually failed
The removal helper reset the failed state of every unit it stopped, but
systemd accepts ResetFailed for a unit that is in the failed state alone.
For any other state it answers that the unit is not loaded and exits
non-zero, and because the helper runs under errexit while the hook aborts
on failure, a healthy unit then aborted the whole transaction:
(2/2) Stop and remove omaspeak user services before package removal
Failed to reset failed state of unit omaspeak.service: Unit omaspeak.service not loaded.
:: Could not clean up omaspeak for jacob; removal aborted.
That is the ordinary case, as the packaged service ships disabled and an
enabled one is commonly stopped rather than failed. Read the active state
after the stop and ask for the reset only where it applies, so a failed
unit still loses its failed state along with its rate and restart counters
while a clean unit no longer fails the removal. A reset that a reachable
manager still refuses stays fatal.
Model the rule in the removal suite, where reset-failed now follows the
active state the way a real manager does, and cover both outcomes: an
inactive unit must not be asked for, a failed one must be reset between
the stop and the disable, and a refused reset must still fail the hook.
* Keep removal cleanup faithful to how systemd reads configuration
Both defects from the review of e025111 sat in the shared package-remove
helper, so both packages were affected the same way.
An offline user's drop-in was recognised by grep '^ExecStart=', while the
configuration parser throws away the whitespace around an assignment
(parse_line() strips the line and both halves of the assignment). A drop-in
naming a development build as
ExecStart =
ExecStart = /home/alice/build/omawake daemon
therefore went unmatched, and the helper cleared away the generated base unit
beside with its enablement links, right behind a service that was never meant
to be the package's. Match an assignment the way the parser accepts one. The
gate that decides whether a unit file is the generated one stays strict on
purpose: only the exact generated shape is ever deleted.
systemctl show-environment also prints every value the way a shell would read
it, through shell_maybe_quote(SHELL_ESCAPE_POSIX), so an XDG_CONFIG_HOME with a
space arrives as $'/home/alice/custom config'. The XDG_CONFIG_HOME=/* case saw
neither form and kept the home's .config directory quietly, leaving the unit in
the directory the manager really reads pointing at the removed binary. Decode
that quoting character by character, without letting the text become shell
syntax, and refuse a value that is neither a plain path nor a closed $'...'
quote rather than delete what would have to be guessed at. A value that is not
an absolute path stays the fallback it is in systemd itself.
The fixtures now hand the helper the very text a manager prints, quoted by a
mirror of that printer, and cover a quoted path with a space, an apostrophe and
a backslash, an unreadable quoted value, a relative one, and each spacing of an
offline override. Verified with the removal suite, 15 tests; the eight new
assertions fail against the helper as it was. The other suites were not run
here, as they reach for the network.
pkgrel 3 -> 4 and the helper's checksum, in both recipes.
Reported-by: spencerbull
* Decode systemd control escapes during service removal
systemctl C-escapes control bytes in show-environment output. Rejecting those valid values aborted package removal for every user, even when the affected account had no Oma service. Decode the printer’s named and octal escapes without evaluating shell syntax or stripping trailing newlines, and cover the real printer format in the fixtures.
Co-Authored-By: GPT-6 XHigh <noreply@openai.com>
---------
Co-authored-by: Spencer Bull <spencer@omarchy.org>
Co-authored-by: GPT-6 XHigh <noreply@openai.com>
A workflow_dispatch runs from master's head. That commit's PR merged
something unrelated, so looking the PR up by commit attached a failed
elsewhen report to #515, whose merge had nothing to do with elsewhen.
Dispatch runs now go to the log only. The log append also moves ahead
of the PR comment so the record exists by the time anyone follows the
comment to it.
Re-running publish for a package that is already live (a dispatch for
something that turned out fine, or a retry after a partial failure) made
bin/build report nothing to build and exit 2, which the publish step
treated as an error. The collect step now dry-runs first: if the channel
already holds master's version the package is recorded as
already-published and skipped, and a run where every package is in that
state exits cleanly with a record saying so.
When a package had no PR artifact and its build failed, the publish
step never ran, no record was written, and the report job failed
looking for it. The collect step now records each package's source
(PR artifact, built here, or build-failed) and writes the record itself
when a build fails, so the report can say plainly that nothing was
published and why.
A PR whose diff against its base is empty has already landed some other
way, typically a sync PR carrying the same bump or a merge from master
that swallowed it. Merging it records a change that isn't one and could
mask a real mistake. result now fails with a message saying to close it.
The publish job now writes publish-record.json describing every
channel/architecture slot it touched: the packages, whether the slot was
published or failed, the target (live or a proof prefix), the commit and
the run. A report job renders that as a comment on the PR the merge
commit came from (looked up by commit, so squash and rebase merges work)
and appends the record as one line to publish-log.jsonl in the bucket,
served next to the packages at https://pkgs.omarchy.org/publish-log.jsonl.
Failures are reported too, with the slots that landed before the failure,
which is when a human most needs to know.
Every pull request now builds the package directories it touches on
ephemeral DigitalOcean droplets, and every merge to master publishes the
resulting artifacts into the channels each package belongs to. The
repository host's timers become the fallback rather than the pipeline.
Build (.github/workflows/build-pr.yml)
One job per package per architecture, always against edge. The artifact
is labelled with the package directory's git tree hash. Tooling (bin/,
helpers/, build/) is checked out from the base branch; the PR supplies
only pkgbuilds/, so a PR can change what is built, never how. Builds
run only for trusted authors: collaborators, .github/VOUCHED.td, or a
PR carrying the build-approved label. A single required check, result,
aggregates the matrix.
Publish (.github/workflows/publish.yml, bin/publish-artifact)
One job per merge. It collects the PR artifacts for the merged tree,
builds anything that has none, then walks each channel/architecture
slot once: pull that database, repo-add every package that belongs in
it, upload packages, signatures, then the database. A published
filename is immutable; identical bytes under an existing name only
gain a database entry, different bytes are refused. Fast-ring packages
reach edge, rc and stable in the same run from the same file.
Matrix (bin/build-matrix)
Package x architecture, with the channels the artifact ships to,
decided by package_builds_for_mirror so CI and the host agree.
arch=any packages build once and land in every architecture database.
Builder (build/build.sh, bin/build, build/Dockerfile)
With no local published tree, plan against and resolve from the public
channel over HTTPS. PACKAGER is set to Omarchy. rclone is in the image.
Runners (ci/)
A controller droplet polls GitHub with curl and creates one g5 droplet
per queued job from cloud-init, deleting them when off or over-age.
Builders carry QEMU with credential support for aarch64. Operator SSH
keys come from GitHub. tests/controller.sh and tests/publish-artifact.sh
cover the decisions against fixtures and real makepkg output.
Tests run on pull requests only; branch protection requires result,
self-tests and build-isolation with up-to-date branches.
The root-run package hook changed ownership of paths below a
user-controlled home directory. A config symlink could redirect chown to
an arbitrary root-owned file during installation or upgrade.
Run the config writer as the target desktop user and remove the privileged
ownership changes. This also prevents the missing-config path from writing
through a user-controlled pathname as root. Add regression coverage and
bump the package release.
Reported-by: piratemoo (Esther) <22439214+piratemoo@users.noreply.github.com>
Link: https://github.com/piratemoo/Arbitrary-File-Ownership-Change-via-Symlink-LPE
Revert the native installer and updater packaging introduced by #325. Keep the prebuilt desktop and existing launcher, updating only the release tag, commit and archive checksum from the previous recipe.
Install Hermes into its writable native layout instead of shipping a frozen /opt desktop. Preserve the native update path, migrate tagged bootstraps, and keep existing CLI launchers until the desktop is ready.
Co-Authored-By: GPT-6 Codex (xhigh) <noreply@openai.com>
A release train has three human moments, each one command: start (release
branch on basecamp/omarchy + notes staging PR + edge→rc advance for
minor/major), rc (pin both PKGBUILDs to the branch head as X.Y.ZrcN on the
pkgs rc branch, trigger the rc channel build, wait for publish, optional RC
ISO), and ship (final pins → promote rc→stable → tag → pins to master → GitHub
release from the staging PR body → final ISO → website bump, each step
skip-if-done so a crashed run resumes).
Bare omarchy-release is the shepherd: it derives the train state from observed
reality (remote branches, rc-branch pins, published channel dbs, tags — no
state files) and offers the correct next step. Versions are inferred from
branch names (v4-0-2 ⇒ 4.0.2rcN ⇒ v4.0.2). ship refuses to promote a commit
no RC was cut from. pick is a multi-select over merged quattro PRs,
cherry-picking merge commits. doctor pre-flights every credential and
connection. self-test wired into CI.
bin/omarchy-pkgs stays as the pin engine, driven with its db URL pointed at
the rc channel and pins committed to the standing rc branch (rebuilt as
master + pins per cut and force-pushed; the server rc worktree follows with
reset --hard).
An empty min_release_age string now maps to unparseable rather than absent,
so "min_release_age": "" fails validation instead of silently running
with a zero-second quarantine. The end-to-end fixtures extend the
checked-in pkgver (.90/.91) so the test keeps working at any future mise
version. A Tests workflow runs bin/sync-upstream self-test and
bin/omarchy-pkgs self-test on every PR in the Arch container, making the
proof machine-checked instead of author-supplied. The README package
metadata field list documents upstream and min_release_age.
A review at xhigh found several ways this command could report success while delivering nothing, which is the exact failure it exists to prevent.
A trigger named in rebuild_on but missing from rebuilt_against was never examined, because the comparison walked the record rather than the declared list. Adding a dependency to a package already opted in left that dependency untracked forever. The comparison now walks the declared triggers, so a name the record does not carry reads as changed.
That also retires the separate baseline path. Recording a package's triggers without bumping pkgrel certified a build nobody had checked: a package already broken by a release that moved before it opted in would be recorded as current and never rebuilt. Opting in now costs one rebuild, which is much the cheaper mistake.
A bumped version was only checked against the checked-in one. The floor is what users already have, so a checkout that had fallen behind the repository could be bumped to a version pacman orders below the package it means to replace, with the record advancing regardless. The published database is now the floor, and an unreadable one warns rather than blocks.
Metadata that did not parse dropped its package out of an unscoped run without a word, an unreadable rebuild_on being indistinguishable from an absent one. It is now reported and fails the run.
The workflow reads versions from mirror.omarchy.org, the mirror the x86_64 builder itself uses, rather than whichever mirror the container defaulted to. A mirror running ahead of the builder would record a version the build never linked against, and nothing re-fires once the record matches.
aarch64 stays uncovered and is documented as such: those builds resolve from Arch Linux ARM, one record cannot describe two architectures, and only x86_64 is published today.
bin/sync-rebuilds --self-test covers each of these against a throwaway repository root with pacman and curl stubbed.
🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.
Co-Authored-By: Codex XHigh <codex@openai.com>
A package that links Qt private API has to be rebuilt whenever qt6-base moves, because Qt_6_PRIVATE_API symbols are not covered by the soname and pacman upgrades Qt out from under the installed binary while the dependency stays unversioned. Nothing here noticed. Both version gates ask whether the package's own source moved, and for a VCS package pinned to a commit that answer stays no through every Qt release.
Unlocking the build gate would not have been enough on its own. A rebuild that reuses the published version string produces a package pacman never offers anyone, so the trigger has to edit git and bump pkgrel, which is why it sits beside sync-aur and sync-upstream rather than inside check-versions or the builder. Once pkgrel moves, both existing gates already do the right thing untouched.
Packages opt in with rebuild_on in .omarchy/package.json. bin/sync-rebuilds records what each was last bumped for in rebuilt_against and compares that to core, extra and multilib, ignoring testing and kde-unstable because those are not what the builder links against. A package with no record yet is only recorded, never bumped: what its published build linked against is not knowable from here, so the first run establishes the baseline. For an AUR-synced package the bump is written as the dotted Omarchy pkgrel suffix in the metadata as well, since the next sync replaces the PKGBUILD wholesale and would otherwise drop it.
🤖 Generated by Opus 5 in Claude Code.
Some vendors publish a release feed of their own that is faster and more
precise than anyone's packaging of it. A package opts in with an
.omarchy/upstream.sh hook that reports the newest release as JSON, and the
driver rewrites pkgver, the checksum arrays the hook names, and pkgrel.
Writes are guarded on both ends: every assignment the update will touch is
verified to exist before anything is written, so a hook naming an array the
PKGBUILD lacks fails with the file untouched rather than half rewritten; and
pkgver is held to pacman's character set, because it lands in a file makepkg
sources as shell.
Ordering is vercmp's, not sort -V's -- they disagree about whether 1.0a
precedes 1.0, and pacman is what decides if a published package is an upgrade.
That is also why the workflow runs in an Arch container rather than straight on
the runner.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>