Skip to content

Tags: malmoos/malmo

Tags

v0.5.0

Toggle v0.5.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #358 from malmoos/dev

Release v0.5.0

v0.4.2

Toggle v0.4.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #350 from malmoos/dev

Release v0.4.2 — hosted cloud image ships as a release asset

v0.4.1

Toggle v0.4.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Release v0.4.1 — hosted host-agent ships version-stamped (#342)

* Release flow: gate PRs into dev, one repo version, publish the image on release tags (#338)

* Fix CI to gate PRs into dev, and gate cloud-image publish on release tags

dev is now the default branch, but ci-go.yml and ci-web.yml only triggered
on pull_request: branches: [main] — every feature PR (which now targets
dev) ran no CI at all. Add dev to both triggers alongside main, since main
still needs gating for the dev->main PR that cuts a release.

Also give ci-cloud-image.yml a push: tags: ['v*'] trigger so a release tag
builds, boot-tests, and publishes the hosted cloud image automatically,
without duplicating the workflow_dispatch build-only/build+publish path.
Compute publish eligibility once (job-level SHOULD_PUBLISH) instead of
repeating a fragile per-step expression, and thread the release tag into
the Hetzner snapshot description/labels when one is present.

Update every doc that hardcoded main as the branch-off/PR-into target
(CLAUDE.md, README.md, docs/README.md, docs/dev/contributing.md,
docs/dev/running-locally.md, the PR template) to describe the dev/main
model, and document the release flow in docs/dev/contributing.md.

* Stamp real build identity: one repo VERSION + git commit, --version flags

Adds VERSION at the repo root (0.4.0) as the single source of truth for
malmo's one repo version (brain/UI/host-agent ship from one commit, so
there's one version, not three independently-bumped ones). A new dumb
internal/version package holds Version + Commit, both stamped at build
time via -ldflags -X (Makefile's new LDFLAGS, and cmd/brain/Dockerfile
via a --build-arg since .git is excluded from the Docker context).
cmd/brain, cmd/host-agent-real, and cmd/host-agent (fake) each gain a
--version flag.

internal/hostagent.AgentVersion (previously the hardcoded
"0.0.1-fake") now derives from the same stamped version.Version, with
no "-fake" suffix: the systemStatus handler it feeds is shared code
compiled into both the fake and the real binary, and already reports
placeholder data for both alike (Hostname is hardcoded "malmo-dev" the
same way) — a "-fake" suffix would have mislabeled host-agent-real too.

make build deliberately still excludes host-agent-real: it's Linux +
CGO + libpam0g-dev on both its build tags already, so folding it in
would break the inner loop on macOS/Windows/WSL2-without-headers. It's
still stamped via its own target for direct/cloud-image/nspawn builds.

* version-mismatch: compatible range against a minimum, not exact equality

cmd/brain's expectedAgentVersion (exact-match constant) is replaced by
minimumAgentVersion, a floor. checkAgentVersion now raises only when
host-agent's reported version is older than the floor; a newer agent
never raises. This matches UPDATES.md #7's compatibility-matrix model
(minimum_host_agent) and its update ordering (host-agent updates before
brain, #1), under which a brain running ahead of its paired agent
mid-rollout is normal, not a mismatch — the old exact-match check would
have false-alarmed on every box during an apt-driven host-agent rollout
window (#2, up to 24h).

Comparison uses golang.org/x/mod/semver, already resolved in the module
graph at v0.16.0 via a transitive test dependency (go mod why ->
modernc.org/libc.test), so `go mod tidy` promoted it to a direct
require with no new go.sum hashes — no new entry in the supply-chain
closure. Comparison strips any prerelease/build suffix first so a
suffix like "-fake" can't make an otherwise-current version look
older than a release core (x/mod/semver's prerelease-sorts-before-
release rule would otherwise make the in-repo fake agent flag itself
as mismatched against its own paired dev brain on every `make dev`).

Adds GET /api/v1/system/version (brain's own stamped version + commit,
no host round trip) on the existing system.go home; regenerates
api/openapi.{json,yaml} and the web-ui TS client to match.

Rewrites the version-mismatch health-issue detail string and its
built-in Summary off exact-equality wording ("expects %s") to the
minimum-version semantics in plain English. Adds
TestAgentVersionAcceptable (table-driven, pins older/newer/exact/
suffix/garbled cases) and rewrites the existing TestCheckAgentVersion_*
suite for the new semantics, including a dedicated newer-never-raises
case for the behavior this change exists to fix.

* ci-cloud-image: assert a pushed tag matches VERSION before the ~40min build

A release tag that doesn't match the repo's VERSION file (e.g. the
release PR forgot to bump it) used to only surface after the full
mkosi build + seeded-boot gate ran — miserable to discover 40 minutes
in. New early step, right after checkout, fails fast: v$(cat VERSION)
must equal github.ref_name on a tag push. Gated to the push event only,
so workflow_dispatch (no real version tag) is unaffected.

Reconciles the publish step's Hetzner snapshot label with VERSION now
that it exists: previously malmo-version=${MALMO_TAG}, left empty on a
workflow_dispatch run to avoid a "bogus" label since ref_name isn't a
version there. Now that VERSION is asserted equal to the tag on a tag
push, and is the real built version on every trigger, the label reads
$(cat VERSION) unconditionally — more accurate than the old
tag-only conditional, not a redundant second field.

ci-go.yml's path filter gains VERSION so a hypothetical release PR
whose only diff is the version bump still triggers the Go gate.

* Docs: one repo version + image-inherits-SemVer, and the range-based detector

BUILD.md # Versioning rewritten: flips the locked "SemVer for host-agent
and brain" (independent per-component) to one repo VERSION, and flips
the locked "CalVer for the ISO" to the image inheriting that SemVer —
both premised on multiple independently-versioned components bundled
into the image, which no longer holds once there's one version to
bundle. Artifact list and locked-decisions bullets updated to match.
Two DECISIONS.md entries (2026-07-16), one per flip, each with its own
why.

HEALTH.md # Version: the version-mismatch row and the locus-C catalog
row both updated from "lockstep pair" / "exact equality" wording to
the compatible-range semantics; the cross-cutting debounce policy's
1-shot exception example updated too.

RELEASE_MANIFEST.md: one-sentence note that its separate brain/ui
semver fields are always equal now that there's one repo version — not
a schema redesign, which stays out of scope (the manifest is unbuilt).

docs/dev/contributing.md # Release model: the VERSION bump is now an
explicit step in the release sequence (bump -> merge dev->main -> tag
matching VERSION -> tag publishes), and the ci-cloud-image tag-assert
gate is mentioned.

docs/architecture.md: internal/version added to the package table.

New progress entry docs/progress/repo-version-and-compat-range.md,
appended to the bottom of docs/progress/README.md's index.

* Phase 3: auto-tag main on VERSION bump, release+publish via reusable workflow

Add .github/workflows/release.yml, triggered on push to main. It checks
whether VERSION names a version with no matching vX.Y.Z tag yet; if the tag
already exists the run is a clean no-op (most merges to main don't bump
VERSION). If it doesn't exist, the workflow tags the merge commit, opens a
GitHub Release with auto-generated notes, then invokes ci-cloud-image.yml
directly as a reusable workflow (workflow_call, publish: true) to build and
publish the hosted cloud image.

Calling it directly rather than relying on ci-cloud-image.yml's existing
push: tags trigger is required, not stylistic: a tag pushed with the default
GITHUB_TOKEN does not fire another workflow's tag-push trigger (GitHub
suppresses GITHUB_TOKEN-authored push events to prevent recursive runs), so
relying on that event would silently tag a release and never build/publish
anything. push: tags stays on ci-cloud-image.yml as the manual escape hatch
for a human pushing a tag by hand; because the auto-tag never fires it, the
two paths can't double-build. ci-cloud-image.yml's SHOULD_PUBLISH and its
tag-assert step's `if: github.event_name == 'push'` already behave correctly
under workflow_call with no logic changes (inputs.publish is populated the
same way for workflow_call and workflow_dispatch), but both are commented to
say so explicitly for the next reader.

Tag+release are created before the image build, not after: a tag is a
source-level fact independent of whether an image was later published from
it, and a failed/slow multi-hour build is then a separable, retriable problem
that doesn't block the release existing or block a following release.

Updates docs/dev/contributing.md # Release model and docs/specs/BUILD.md #
Versioning, both of which described manual tagging by the maintainer; they
now describe the automatic flow. No progress entry: this is CI/workflow
tooling with no product-facing behavior change, matching the precedent set
by Phase 1 (contributing.md's "dev tooling goes in docs/dev/" rule).

* Gate the tag-assert and publish on the ref being a tag, not just event_name

In a reusable workflow the `github` context belongs to the CALLER, not the
called workflow. release.yml is triggered by `push: branches: [main]`, so
inside ci-cloud-image.yml on the workflow_call path github.event_name is
`push` (not `workflow_call`) and github.ref is `refs/heads/main`.

Two conditions keyed on `github.event_name == 'push'` were wrong as a result:

- The tag-assert step would RUN on every auto-release and compare `main`
  against `v<VERSION>`, exiting 1. release.yml pushes the tag and cuts the
  GitHub Release before calling us, so every release would have produced a
  tag and a Release with no image, failing 30 seconds in.
- SHOULD_PUBLISH evaluated true via the event_name half rather than
  inputs.publish. Correct today only by accident: a future caller passing
  publish=false would publish anyway.

Keying both on `startsWith(github.ref, 'refs/tags/')` distinguishes a genuine
tag push from a call made on a branch. The call path needs no assert — the
tag is derived from VERSION by release.yml, so it matches by construction.

* Lead the Hetzner snapshot description with the version

desc is what the Hetzner console shows as the snapshot's name in the images
list. The version was buried mid-string behind the run_id, so scanning the
list for 'which malmo is this' meant reading past noise. Version first.

* release.yml: fail loudly when VERSION is bumped to an already-tagged version

The decide step keyed only on "does the tag exist?", which conflates two
opposite situations. An ordinary merge that doesn't bump VERSION should
no-op quietly. But a merge that DOES bump VERSION to a version already
tagged is a real release that cannot be cut — and exiting green swallowed
it: you'd merge the release PR, see all-green, and get no tag movement, no
GitHub Release and no image, with nothing saying why.

Not hypothetical. This workflow tags before building (deliberately), so a
release whose image build fails leaves the tag behind. Merging the fix
without first deleting that tag hit exactly this path, and the failure was
silent.

Now three outcomes: no tag -> release; tag + VERSION unchanged -> skip; tag
+ VERSION bumped -> error naming both versions, where the tag points, and
how to recover. Needs fetch-depth: 0 to read VERSION at github.event.before
and to resolve the tag. Where the previous VERSION can't be read (first
push, force-push), it falls back to the old skip behaviour with a warning.

Reported by Greptile on #338.

* Stamp the hosted host-agent build so a released box doesn't self-flag (#340)

A v0.4.0 Hetzner box raised version-mismatch and blocked app installs while
otherwise healthy: dev/cloud/stage-control-plane.sh built the slim hosted
host-agent with a bare `go build` and no -ldflags. It's the only go build in
the cloud-image staging path (brain/UI go through Docker, whose Dockerfile
reads `cat VERSION` itself), so it shipped internal/version's unstamped "dev"
default. hostagent.AgentVersion is version.Version verbatim, so the agent
reported "dev" to the brain, semver can't parse it, an unparseable core sorts
before every valid version, and the check read it as older than 0.4.0.

The >= comparison was never at fault — a stamped 0.4.0 agent satisfies a 0.4.0
minimum by equality. minimumAgentVersion is untouched (it moves only on a real
protocol break) and no spec changes: BUILD.md # Versioning already required
every build to stamp both fields; the cloud build silently violated it.

stage_build_go now applies the Makefile's LDFLAGS via stage_version_ldflags.
Recomputed rather than inherited: bootstrap.sh runs as `sudo -E`, so make's
MALMO_VERSION/MALMO_COMMIT never reach it. git runs as CALLER (under sudo the
repo is owned by the invoking user and git refuses a dubious-ownership repo as
root), with the Makefile's `|| echo unknown` fallback rather than failing a
~40min image build; only Commit can degrade, Version comes from the file.

cloud-assertions.sh step 1c asserts the baked binary reports
"malmo X.Y.Z (g<sha>)". Verified it fails against the real unstamped v0.4.0
binary and passes against the stamped one. The boot lane is the only place
this is observable: the stamp is applied by the build command, so a `go test`
binary is unstamped by construction, and the release workflow's tag assert
checks the VERSION file rather than what landed in the binary.

* Bump VERSION to 0.4.1 (#341)

Prepares the 0.4.1 release (docs/dev/contributing.md # Release model: the
VERSION bump is what makes a dev->main merge a release).

Ships #340: the hosted host-agent build is now version-stamped. v0.4.0 images
are structurally broken — the agent reported "dev", which the brain's
minimumAgentVersion check reads as older than every release, so every box
provisioned from one raises version-mismatch and cannot install apps. The
baked binary can't self-heal, so the fix only reaches boxes via a new image,
which is what this bump cuts.

v0.4.0

Toggle v0.4.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Release flow: gate PRs into dev, one repo version, publish the image …

…on release tags (#338) (#339)

* Fix CI to gate PRs into dev, and gate cloud-image publish on release tags

dev is now the default branch, but ci-go.yml and ci-web.yml only triggered
on pull_request: branches: [main] — every feature PR (which now targets
dev) ran no CI at all. Add dev to both triggers alongside main, since main
still needs gating for the dev->main PR that cuts a release.

Also give ci-cloud-image.yml a push: tags: ['v*'] trigger so a release tag
builds, boot-tests, and publishes the hosted cloud image automatically,
without duplicating the workflow_dispatch build-only/build+publish path.
Compute publish eligibility once (job-level SHOULD_PUBLISH) instead of
repeating a fragile per-step expression, and thread the release tag into
the Hetzner snapshot description/labels when one is present.

Update every doc that hardcoded main as the branch-off/PR-into target
(CLAUDE.md, README.md, docs/README.md, docs/dev/contributing.md,
docs/dev/running-locally.md, the PR template) to describe the dev/main
model, and document the release flow in docs/dev/contributing.md.

* Stamp real build identity: one repo VERSION + git commit, --version flags

Adds VERSION at the repo root (0.4.0) as the single source of truth for
malmo's one repo version (brain/UI/host-agent ship from one commit, so
there's one version, not three independently-bumped ones). A new dumb
internal/version package holds Version + Commit, both stamped at build
time via -ldflags -X (Makefile's new LDFLAGS, and cmd/brain/Dockerfile
via a --build-arg since .git is excluded from the Docker context).
cmd/brain, cmd/host-agent-real, and cmd/host-agent (fake) each gain a
--version flag.

internal/hostagent.AgentVersion (previously the hardcoded
"0.0.1-fake") now derives from the same stamped version.Version, with
no "-fake" suffix: the systemStatus handler it feeds is shared code
compiled into both the fake and the real binary, and already reports
placeholder data for both alike (Hostname is hardcoded "malmo-dev" the
same way) — a "-fake" suffix would have mislabeled host-agent-real too.

make build deliberately still excludes host-agent-real: it's Linux +
CGO + libpam0g-dev on both its build tags already, so folding it in
would break the inner loop on macOS/Windows/WSL2-without-headers. It's
still stamped via its own target for direct/cloud-image/nspawn builds.

* version-mismatch: compatible range against a minimum, not exact equality

cmd/brain's expectedAgentVersion (exact-match constant) is replaced by
minimumAgentVersion, a floor. checkAgentVersion now raises only when
host-agent's reported version is older than the floor; a newer agent
never raises. This matches UPDATES.md #7's compatibility-matrix model
(minimum_host_agent) and its update ordering (host-agent updates before
brain, #1), under which a brain running ahead of its paired agent
mid-rollout is normal, not a mismatch — the old exact-match check would
have false-alarmed on every box during an apt-driven host-agent rollout
window (#2, up to 24h).

Comparison uses golang.org/x/mod/semver, already resolved in the module
graph at v0.16.0 via a transitive test dependency (go mod why ->
modernc.org/libc.test), so `go mod tidy` promoted it to a direct
require with no new go.sum hashes — no new entry in the supply-chain
closure. Comparison strips any prerelease/build suffix first so a
suffix like "-fake" can't make an otherwise-current version look
older than a release core (x/mod/semver's prerelease-sorts-before-
release rule would otherwise make the in-repo fake agent flag itself
as mismatched against its own paired dev brain on every `make dev`).

Adds GET /api/v1/system/version (brain's own stamped version + commit,
no host round trip) on the existing system.go home; regenerates
api/openapi.{json,yaml} and the web-ui TS client to match.

Rewrites the version-mismatch health-issue detail string and its
built-in Summary off exact-equality wording ("expects %s") to the
minimum-version semantics in plain English. Adds
TestAgentVersionAcceptable (table-driven, pins older/newer/exact/
suffix/garbled cases) and rewrites the existing TestCheckAgentVersion_*
suite for the new semantics, including a dedicated newer-never-raises
case for the behavior this change exists to fix.

* ci-cloud-image: assert a pushed tag matches VERSION before the ~40min build

A release tag that doesn't match the repo's VERSION file (e.g. the
release PR forgot to bump it) used to only surface after the full
mkosi build + seeded-boot gate ran — miserable to discover 40 minutes
in. New early step, right after checkout, fails fast: v$(cat VERSION)
must equal github.ref_name on a tag push. Gated to the push event only,
so workflow_dispatch (no real version tag) is unaffected.

Reconciles the publish step's Hetzner snapshot label with VERSION now
that it exists: previously malmo-version=${MALMO_TAG}, left empty on a
workflow_dispatch run to avoid a "bogus" label since ref_name isn't a
version there. Now that VERSION is asserted equal to the tag on a tag
push, and is the real built version on every trigger, the label reads
$(cat VERSION) unconditionally — more accurate than the old
tag-only conditional, not a redundant second field.

ci-go.yml's path filter gains VERSION so a hypothetical release PR
whose only diff is the version bump still triggers the Go gate.

* Docs: one repo version + image-inherits-SemVer, and the range-based detector

BUILD.md # Versioning rewritten: flips the locked "SemVer for host-agent
and brain" (independent per-component) to one repo VERSION, and flips
the locked "CalVer for the ISO" to the image inheriting that SemVer —
both premised on multiple independently-versioned components bundled
into the image, which no longer holds once there's one version to
bundle. Artifact list and locked-decisions bullets updated to match.
Two DECISIONS.md entries (2026-07-16), one per flip, each with its own
why.

HEALTH.md # Version: the version-mismatch row and the locus-C catalog
row both updated from "lockstep pair" / "exact equality" wording to
the compatible-range semantics; the cross-cutting debounce policy's
1-shot exception example updated too.

RELEASE_MANIFEST.md: one-sentence note that its separate brain/ui
semver fields are always equal now that there's one repo version — not
a schema redesign, which stays out of scope (the manifest is unbuilt).

docs/dev/contributing.md # Release model: the VERSION bump is now an
explicit step in the release sequence (bump -> merge dev->main -> tag
matching VERSION -> tag publishes), and the ci-cloud-image tag-assert
gate is mentioned.

docs/architecture.md: internal/version added to the package table.

New progress entry docs/progress/repo-version-and-compat-range.md,
appended to the bottom of docs/progress/README.md's index.

* Phase 3: auto-tag main on VERSION bump, release+publish via reusable workflow

Add .github/workflows/release.yml, triggered on push to main. It checks
whether VERSION names a version with no matching vX.Y.Z tag yet; if the tag
already exists the run is a clean no-op (most merges to main don't bump
VERSION). If it doesn't exist, the workflow tags the merge commit, opens a
GitHub Release with auto-generated notes, then invokes ci-cloud-image.yml
directly as a reusable workflow (workflow_call, publish: true) to build and
publish the hosted cloud image.

Calling it directly rather than relying on ci-cloud-image.yml's existing
push: tags trigger is required, not stylistic: a tag pushed with the default
GITHUB_TOKEN does not fire another workflow's tag-push trigger (GitHub
suppresses GITHUB_TOKEN-authored push events to prevent recursive runs), so
relying on that event would silently tag a release and never build/publish
anything. push: tags stays on ci-cloud-image.yml as the manual escape hatch
for a human pushing a tag by hand; because the auto-tag never fires it, the
two paths can't double-build. ci-cloud-image.yml's SHOULD_PUBLISH and its
tag-assert step's `if: github.event_name == 'push'` already behave correctly
under workflow_call with no logic changes (inputs.publish is populated the
same way for workflow_call and workflow_dispatch), but both are commented to
say so explicitly for the next reader.

Tag+release are created before the image build, not after: a tag is a
source-level fact independent of whether an image was later published from
it, and a failed/slow multi-hour build is then a separable, retriable problem
that doesn't block the release existing or block a following release.

Updates docs/dev/contributing.md # Release model and docs/specs/BUILD.md #
Versioning, both of which described manual tagging by the maintainer; they
now describe the automatic flow. No progress entry: this is CI/workflow
tooling with no product-facing behavior change, matching the precedent set
by Phase 1 (contributing.md's "dev tooling goes in docs/dev/" rule).

* Gate the tag-assert and publish on the ref being a tag, not just event_name

In a reusable workflow the `github` context belongs to the CALLER, not the
called workflow. release.yml is triggered by `push: branches: [main]`, so
inside ci-cloud-image.yml on the workflow_call path github.event_name is
`push` (not `workflow_call`) and github.ref is `refs/heads/main`.

Two conditions keyed on `github.event_name == 'push'` were wrong as a result:

- The tag-assert step would RUN on every auto-release and compare `main`
  against `v<VERSION>`, exiting 1. release.yml pushes the tag and cuts the
  GitHub Release before calling us, so every release would have produced a
  tag and a Release with no image, failing 30 seconds in.
- SHOULD_PUBLISH evaluated true via the event_name half rather than
  inputs.publish. Correct today only by accident: a future caller passing
  publish=false would publish anyway.

Keying both on `startsWith(github.ref, 'refs/tags/')` distinguishes a genuine
tag push from a call made on a branch. The call path needs no assert — the
tag is derived from VERSION by release.yml, so it matches by construction.

* Lead the Hetzner snapshot description with the version

desc is what the Hetzner console shows as the snapshot's name in the images
list. The version was buried mid-string behind the run_id, so scanning the
list for 'which malmo is this' meant reading past noise. Version first.

* release.yml: fail loudly when VERSION is bumped to an already-tagged version

The decide step keyed only on "does the tag exist?", which conflates two
opposite situations. An ordinary merge that doesn't bump VERSION should
no-op quietly. But a merge that DOES bump VERSION to a version already
tagged is a real release that cannot be cut — and exiting green swallowed
it: you'd merge the release PR, see all-green, and get no tag movement, no
GitHub Release and no image, with nothing saying why.

Not hypothetical. This workflow tags before building (deliberately), so a
release whose image build fails leaves the tag behind. Merging the fix
without first deleting that tag hit exactly this path, and the failure was
silent.

Now three outcomes: no tag -> release; tag + VERSION unchanged -> skip; tag
+ VERSION bumped -> error naming both versions, where the tag points, and
how to recover. Needs fetch-depth: 0 to read VERSION at github.event.before
and to resolve the tag. Where the previous VERSION can't be read (first
push, force-push), it falls back to the old skip behaviour with a warning.

Reported by Greptile on #338.