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
8 changes: 5 additions & 3 deletions .github/workflows/publish-agent-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
# - Off the hot path, not a required check. The image closure is the heavy
# nix build; keeping it here leaves PR latency untouched and a publish flake
# never reds the required merge gate.
# - Failure ownership. `agent-image/` is not a moon project, so the gate never
# builds it — the main-branch Actions failure notification is the surfacing
# mechanism for a red publish.
# - Failure ownership. The image BUILD is gated pre-merge by the
# `compass-agent-image` moon project (agent-image/moon.yml), so a build
# break is caught on the PR, not here. This workflow owns only the PUSH
# half — a GHCR/registry failure moving the two-tag set — surfaced by the
# main-branch Actions failure notification.
#
# It also enumerates NO moon projects, so it re-creates none of the
# silent-staleness failure the ONE-JOB doctrine targets: the gate's coverage
Expand Down
5 changes: 5 additions & 0 deletions .moon/workspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ projects:
compass-eng-docs: 'apps/eng-docs'
# The backend Go module: server + comms + the generated compass.v1 Go stubs.
compass-go: 'go'
# The compass-agent runtime image: a standalone nix devenv whose build
# realises the exact artifact the publish lane ships. Registered here so the
# image build is in the gate — affected-detection runs it only on PRs that
# touch the image closure (see agent-image/moon.yml).
compass-agent-image: 'agent-image'
# The toolchain version-parity gate: asserts CI's PATH holds the dev shell's
# toolchain, and carries the unit tests for its own comparison logic.
toolchain-parity: 'tools/toolchain'
Expand Down
79 changes: 79 additions & 0 deletions agent-image/moon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# yaml-language-server: $schema=https://moonrepo.dev/schemas/project.json
#
# compass-agent-image (agent-image/): the compass-agent runtime image. A
# standalone nix devenv, not a bun package — the build is
# `nix run path:../forks/devenv#devenv -- container build agent`, the same
# fork-pinned derivation the publish lane ships (agent-image/publish.sh) and the
# local `dogfood:agent-image` load produce. A green build here is proof the
# exact artifact that publishes still builds.
#
# WHY THIS IS A MOON PROJECT. Registering it in .moon/workspace.yml is what puts
# the image build in the gate: on a PR `moon ci :ci` runs this project's `ci`
# only when the PR affects the image closure (the `inputs` below), and every
# push to main runs it unconditionally (ci.yml's affected-PR / full-main split).
# Before this, agent-image/ was outside moon, so an image-build break — a
# bun-drift assert (toolchain.nix), a stale FOD hash or a broken bundle
# (entrypoint.nix) — surfaced only post-merge in the publish workflow, while a
# consumer waited on a tag. The build is heavy (the image closure is the
# dominant CI cost, the reason ci.yml's timeout is 90m), but affected-detection
# keeps it off every PR that does not touch the closure, and the two vendored
# forks already build through nix in this same gate.
layer: 'application'
language: 'nix'

workspace:
inheritedTasks:
# Not a bun package, so the tag-bun `install` must never run here; `lint`
# and `format` are whole-repo tasks on the root project, never inherited.
# agent-image carries no `bun` tag, so nothing is inherited today — this is
# the same defensive guard the sibling nix projects carry, in case
# tag-bun.yml later grows a task a nix tree must not run.
exclude: ['install', 'lint', 'format']

tasks:
build:
# Realise the image exactly as the publish lane and the dogfood load do —
# the same fork-pinned derivation, so a green build here proves the exact
# artifact that publishes builds. Runs from agent-image/ (moon's default
# project-dir cwd) so the `path:../forks/*` flake refs resolve, matching
# publish.sh. The substituters the build fetches from are named in ci.yml's
# install-nix-action config, the reviewed trust surface for this gate.
#
# cache:false — nix owns its store-path caching; moon must not false-green a
# build it did not re-run (the same posture the fork builds take).
command: 'nix run path:../forks/devenv#devenv -- container build agent'
options:
cache: false
runInCI: true
# The image's build closure. MIRRORS publish-agent-image.yml's
# on.push.paths — the reviewed source of truth for what changes the
# published artifact — so affected-detection schedules this build on exactly
# the PRs a republish fires on. Plus .prototools: the build's toolchain.nix
# bun-drift assert reads it, so a pin move there can red this build (publish
# excludes .prototools because it cannot change the output — but reddening on
# the assert at PR time is precisely this gate's purpose). A leading `/` is
# workspace-root-relative; the bare glob is this project's own tree (image
# definition, toolchain, entrypoint FOD).
#
# A benign warn is expected here: moon's task hasher logs "Attempted to hash
# input forks/devenv/tests/keycloak-certs/realms but only files can be
# hashed" because the vendored devenv fork ships a directory SYMLINK at that
# path (a keycloak test fixture, never in the image closure). It is cosmetic
# and cannot skew this gate: affected-scheduling is VCS-touched-file based
# (a change to, or a repoint of, any path under the globs below still runs
# the build), and the hasher's cache key — the only thing the unhashable
# node would feed — is unused because this task is cache:false.
inputs:
- '**/*'
- '/packages/compass-agent/**'
- '/forks/devenv/**'
- '/forks/nix2container/**'
- '/package.json'
- '/bun.lock'
- '/.prototools'

ci:
deps: ['build']
options:
cache: false
runInCI: true
30 changes: 30 additions & 0 deletions docs/architecture/build-and-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,36 @@ also settles the immutability guard's first-publish edge: the guard inspects
authenticatable) package guarantees the absent-tag inspect classifies cleanly
rather than on a not-yet-existent repository's error shape.

### Pre-merge build check

`agent-image/` is registered as the `compass-agent-image` moon project
(`.moon/workspace.yml`), so the CI gate builds the image on any PR that
affects its closure. Before this the image was outside moon and had zero
pre-merge coverage — an image-build break surfaced only post-merge in the
publish workflow, while a consumer waited on a tag.

The project's `build` task realises the image with the same fork-pinned
derivation the publish lane ships
(`nix run path:../forks/devenv#devenv -- container build agent`), so a green
build proves the exact artifact that publishes still builds — both an
eval-time break (a bun-pin drift against the `agent-image/toolchain.nix`
assert) and a realise-time break (an `agent-image/entrypoint.nix` FOD-hash
invalidation or a broken bundle), the full class.

The build is heavy — the image closure is the dominant CI cost, the reason
the gate's timeout is 90m — but it is not paid on every PR. `moon ci` runs a
PR's *affected* projects only, and the task's `inputs` scope it to the image
closure: the `agent-image/` tree, the two vendored forks, `packages/compass-agent/`,
the root `package.json` and `bun.lock`, and `.prototools`. A PR that touches
none of those never builds the image; every push to main runs it
unconditionally in the full sweep. Its `inputs` mirror the publish workflow's
`on.push.paths` — the reviewed source of truth for what changes the published
artifact — plus `.prototools` (which publish excludes because a pin move there
cannot change the output, but which reddens the toolchain assert, exactly what
this gate exists to catch at PR time). As a project in the one-job gate it is a
required check: a build break blocks merge, the same posture as the vendored
forks' nix builds.

## Caching

**moon task cache** — whole-task-output caching, keyed by an inputs hash.
20 changes: 14 additions & 6 deletions docs/designs/platform/compass-agent-image-publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,10 @@ silent-staleness failure the doctrine targets. What it *does* buy:
`agent-image/toolchain.nix:50-56`; an FOD-hash invalidation,
`entrypoint.nix:116`) would otherwise sit unnoticed while a consumer waits on
a tag. Publish failures are owned by compass-runner (this record's owner);
the main-branch Actions failure notification is the surfacing mechanism. A
cheap eval-only pre-merge check (`nix eval` the image drv without realising
it) is deferred hardening, noted in T4.
the main-branch Actions failure notification is the surfacing mechanism for a
push failure. The image BUILD is now gated pre-merge by the
`compass-agent-image` moon project (the deferred-hardening item below,
implemented), so a build break is caught on the PR rather than here.

Triggers: `push: branches: [main]` with a `paths:` filter (next section), plus
`workflow_dispatch` as the manual/backstop lane. No PR trigger — no secret or
Expand Down Expand Up @@ -483,9 +484,16 @@ Interfaces:
`docs/architecture/build-and-ci.md`.
- [ ] One-time (whoever lands T2): set the GHCR package **public** after the
first push (Matt's ruling — compass is OSS).
- [ ] Deferred hardening (T4-adjacent): a cheap eval-only pre-merge check
(`nix eval` the image drv without realising it) so an image-build break
is caught at PR time instead of in the post-merge publish.
- [x] Deferred hardening (T4-adjacent): gate the image build pre-merge so an
image-build break is caught at PR time instead of in the post-merge
publish. Implemented as the `compass-agent-image` moon project
(`agent-image/moon.yml`), which realises the full image in the CI gate on
any PR affecting the image closure. An earlier eval-only variant (`nix
eval` the drv without realising) was superseded by Matt's ruling to build
the full image: eval-only caught only the eval-time bun-drift assert,
whereas the moon build also catches realise-time breaks (FOD-hash
invalidation, a broken bundle), and reuses the same nix-in-gate posture
the vendored forks already establish.

## Open Questions

Expand Down
Loading