Compare commits

..

1 Commits

Author SHA1 Message Date
1288c4bc2c feat(fleet): comms-block emitter + FLEET-LAUNCH runbook (#633)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Add `mosaic fleet comms-block <role> [--host]` — an explicit-arg, comms-only
emitter wrapping a new resolveCommsBlock() over readFleetCommsBlock. Unlike the
launch-time reader (which returns '' on any miss so composeContract can no-op
silently), the emitter fails loud: unknown role / missing roster → stderr +
exit 1, so an operator can safely preview any peer's view and a typo is never
a silent no-op.

Add docs/fleet/FLEET-LAUNCH.md: the canonical roster-driven launch path
(worker + orchestrator .env fold via MOSAIC_AGENT_COMMAND, which short-circuits
the line-44 yolo hardcode), 3 launch gotchas (flag conflict, MOSAIC_AGENT_NAME
baking, launchRuntime guards), the #632 preserve-list note, and the North-Star
A→B→webUI launch-config arc.

PATH A of the orchestrator-launch fix; PATH B (roster-native launch-config:
yolo toggle + command/channels emission) tracked as #636.

TDD: 6 new resolveCommsBlock cases; 177 fleet+comms tests green; typecheck,
eslint, prettier clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsgTQzV5YUGk1JtCLP4B83
2026-06-22 16:50:24 -05:00
7 changed files with 17 additions and 120 deletions

4
.npmrc
View File

@@ -1,5 +1 @@
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/ @mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/
# Pin the pnpm store to the same path the ci-base image warms (Dockerfile.ci),
# so the pipeline `pnpm install --prefer-offline` consumes the baked store
# instead of repopulating a fresh one.
store-dir=/root/.local/share/pnpm/store

View File

@@ -1,40 +0,0 @@
# Build & push the pre-baked CI base image (Dockerfile.ci) to the Gitea
# registry CI already publishes to. Reuses the exact kaniko + auth pattern
# from publish.yml (REGISTRY_USER/REGISTRY_PASS from_secret, /kaniko/.docker
# config.json). Other pipelines (ci.yml, publish.yml) pull `ci-base:latest`
# for their install step.
#
# Rebuild ONLY when the dependency set or the image recipe changes — a normal
# code push must not trigger a 25-min image build. `path` applies to push/PR
# events; `event: tag` (releases) rebuilds unconditionally so a tagged release
# always ships a fresh base.
when:
- event: tag
- event: [push, manual]
branch: main
path:
include:
- 'pnpm-lock.yaml'
- 'Dockerfile.ci'
steps:
build-ci-base:
image: gcr.io/kaniko-project/executor:debug
environment:
REGISTRY_USER:
from_secret: gitea_username
REGISTRY_PASS:
from_secret: gitea_password
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
commands:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
- |
# Lockfile-hash tag: an immutable identity for the exact dep set baked
# into this image. `:latest` is the mutable pointer pipelines consume.
LOCK_HASH=$(sha256sum pnpm-lock.yaml | cut -c1-12)
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/ci-base:latest"
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/ci-base:lock-$LOCK_HASH"
/kaniko/executor --context . --dockerfile Dockerfile.ci $DESTINATIONS

View File

@@ -1,9 +1,5 @@
# &node_image is the pre-baked CI base built by .woodpecker/ci-image.yml:
# node:24-alpine + python3/make/g++/postgresql-client + pnpm + a warm pnpm
# store. The install step resolves from the baked store (--prefer-offline)
# instead of paying a ~731s cold fetch + native compile every run.
variables: variables:
- &node_image 'git.mosaicstack.dev/mosaicstack/stack/ci-base:latest' - &node_image 'node:22-alpine'
- &enable_pnpm 'corepack enable' - &enable_pnpm 'corepack enable'
when: when:
@@ -19,9 +15,8 @@ steps:
image: *node_image image: *node_image
commands: commands:
- corepack enable - corepack enable
# python3/make/g++ are baked into ci-base; --prefer-offline resolves from - apk add --no-cache python3 make g++
# the baked pnpm store. - pnpm install --frozen-lockfile
- pnpm install --frozen-lockfile --prefer-offline
# Blocking gate: public framework package must contain no operator-specific # Blocking gate: public framework package must contain no operator-specific
# personal data or private $HOME defaults. Runs early (no node_modules needed). # personal data or private $HOME defaults. Runs early (no node_modules needed).
@@ -69,7 +64,8 @@ steps:
DATABASE_URL: postgresql://mosaic:mosaic@ci-postgres:5432/mosaic DATABASE_URL: postgresql://mosaic:mosaic@ci-postgres:5432/mosaic
commands: commands:
- *enable_pnpm - *enable_pnpm
# postgresql-client (pg_isready) is baked into ci-base. # Install postgresql-client for pg_isready
- apk add --no-cache postgresql-client
# Wait up to 60s for CI postgres to be ready; fail fast if it never comes up. # Wait up to 60s for CI postgres to be ready; fail fast if it never comes up.
- | - |
ready=0 ready=0

View File

@@ -2,9 +2,7 @@
# Runs only on main branch push/tag # Runs only on main branch push/tag
variables: variables:
# Pre-baked CI base (see .woodpecker/ci-image.yml): node:24-alpine + - &node_image 'node:22-alpine'
# toolchain + warm pnpm store. Kills the second cold install publish pays.
- &node_image 'git.mosaicstack.dev/mosaicstack/stack/ci-base:latest'
- &enable_pnpm 'corepack enable' - &enable_pnpm 'corepack enable'
# Heavy kaniko image builds (~25 min) — gate them so a merge that only touches # Heavy kaniko image builds (~25 min) — gate them so a merge that only touches
# the npm-only CLI (@mosaicstack/mosaic) or docs does NOT rebuild the platform # the npm-only CLI (@mosaicstack/mosaic) or docs does NOT rebuild the platform
@@ -33,8 +31,7 @@ steps:
image: *node_image image: *node_image
commands: commands:
- corepack enable - corepack enable
# Resolve from the baked pnpm store instead of a cold network fetch. - pnpm install --frozen-lockfile
- pnpm install --frozen-lockfile --prefer-offline
build: build:
image: *node_image image: *node_image

View File

@@ -1,45 +0,0 @@
# Pre-baked CI base image for Woodpecker pipelines.
#
# Purpose: eliminate the cold `pnpm install` that dominates every pipeline
# (~731s median). This image ships the native toolchain (no per-run `apk add`)
# AND a warm, content-addressable pnpm store with the dependency-tree tarballs
# already fetched at build time. `pnpm fetch` only populates the store from the
# lockfile — it does NOT run the native node-gyp builds (better-sqlite3,
# node-pty, sqlite3, canvas, sharp); those still compile at `pnpm install`,
# which is exactly why the musl toolchain stays baked into this image. A
# pipeline `pnpm install --frozen-lockfile --prefer-offline` then resolves
# tarballs from local hard-links (no network) and compiles natives against the
# already-present toolchain, in tens of seconds instead of ~731s.
#
# Rebuilt only when `pnpm-lock.yaml` or this Dockerfile change
# (see .woodpecker/ci-image.yml).
#
# Node version is pinned to 24 (Active LTS). This is the follow-up bump from
# node:22 — sequenced AFTER the CI cache work landed so the runtime change
# carries zero cache variables. node:26 stays held until it reaches LTS
# (Oct 2026); the Current line risks native-module (node-gyp) breakage on a
# runner that compiles better-sqlite3 / canvas / sharp / node-pty from source.
FROM node:24-alpine
# Native toolchain required to compile node-gyp deps on musl, plus the
# postgresql-client used by the test step's pg_isready readiness probe. `bash`
# is baked here too — the sanitization step in ci.yml otherwise does a per-run
# `apk add bash`.
RUN apk add --no-cache python3 make g++ postgresql-client bash
# Pin pnpm to the repo's packageManager version via corepack.
RUN corepack enable && corepack prepare pnpm@10.6.2 --activate
WORKDIR /app
# Pin the store location so the pipeline can point `store-dir` at the same path.
ENV PNPM_HOME=/root/.local/share/pnpm
RUN pnpm config set store-dir /root/.local/share/pnpm/store
# Warm the store. `pnpm fetch` populates the content-addressable store with the
# dependency tarballs directly from the lockfile (no package.json / workspace
# needed), so a baked store stays valid until the lockfile changes. Note:
# `fetch` does NOT compile native modules — that happens later at `pnpm install`
# in the pipeline, against the toolchain baked above.
COPY pnpm-lock.yaml ./
RUN pnpm fetch --frozen-lockfile

View File

@@ -2,20 +2,12 @@
when: when:
- event: [push, pull_request, manual] - event: [push, pull_request, manual]
# Dependencies are installed ONCE in the `install` step and every downstream
# step depends on it, reusing the populated node_modules from the shared
# workspace volume. Do NOT re-run `npm ci` per step — that pays the full cold
# install (network fetch + native rebuilds) N times and is the dominant cost
# in a pipeline.
#
# For best results, replace `&node_image` with a pre-baked CI base image that
# ships your toolchain (python3/make/g++ for native modules) and a warm npm
# cache, then keep `--prefer-offline` so installs resolve from the cache. See
# the Mosaic Stack repo's Dockerfile.ci + .woodpecker/ci-image.yml for the
# baked-image pattern.
variables: variables:
- &node_image 'node:20-alpine' - &node_image 'node:20-alpine'
- &gitleaks_image 'ghcr.io/gitleaks/gitleaks:v8.24.0' - &gitleaks_image 'ghcr.io/gitleaks/gitleaks:v8.24.0'
- &install_deps |
corepack enable
npm ci --ignore-scripts
steps: steps:
# Secret scanning (runs in parallel with install, no deps) # Secret scanning (runs in parallel with install, no deps)
@@ -25,18 +17,15 @@ steps:
- gitleaks git --redact --verbose --log-opts="HEAD~1..HEAD" - gitleaks git --redact --verbose --log-opts="HEAD~1..HEAD"
depends_on: [] depends_on: []
# Single cached install. Every other step depends on this and reuses the
# node_modules it produces in the shared workspace.
install: install:
image: *node_image image: *node_image
commands: commands:
- corepack enable - *install_deps
- npm ci --ignore-scripts --prefer-offline
depends_on: []
security-audit: security-audit:
image: *node_image image: *node_image
commands: commands:
- *install_deps
- npm audit --audit-level=high - npm audit --audit-level=high
depends_on: depends_on:
- install - install
@@ -46,6 +35,7 @@ steps:
environment: environment:
SKIP_ENV_VALIDATION: 'true' SKIP_ENV_VALIDATION: 'true'
commands: commands:
- *install_deps
- npm run lint - npm run lint
depends_on: depends_on:
- install - install
@@ -55,6 +45,7 @@ steps:
environment: environment:
SKIP_ENV_VALIDATION: 'true' SKIP_ENV_VALIDATION: 'true'
commands: commands:
- *install_deps
- npm run type-check - npm run type-check
depends_on: depends_on:
- install - install
@@ -64,6 +55,7 @@ steps:
environment: environment:
SKIP_ENV_VALIDATION: 'true' SKIP_ENV_VALIDATION: 'true'
commands: commands:
- *install_deps
- npm run test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}' - npm run test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}'
depends_on: depends_on:
- install - install
@@ -74,6 +66,7 @@ steps:
SKIP_ENV_VALIDATION: 'true' SKIP_ENV_VALIDATION: 'true'
NODE_ENV: 'production' NODE_ENV: 'production'
commands: commands:
- *install_deps
- npm run build - npm run build
depends_on: depends_on:
- lint - lint

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaicstack/mosaic", "name": "@mosaicstack/mosaic",
"version": "0.0.41", "version": "0.0.40",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git", "url": "https://git.mosaicstack.dev/mosaicstack/stack.git",