feat(firecracker): pull the infra rootfs as a prebuilt artifact (PRD 0069 Stage 2) #395

Merged
didericis merged 5 commits from nix-fixed-images-stage2 into main 2026-07-17 00:15:18 -04:00
Collaborator

What

Stage 2 of the docker-free Firecracker backend (#348, PRD 0069): the launch host stops building the fixed infra image with Docker. Instead the infra VM's rootfs is prebuilt off-host and pulled as a ready-to-boot ext4 artifact — the launch host does HTTP GET → sha256 verify → gunzip → boot, no Docker, no OCI/rootfs tooling.

This works because the infra rootfs is already host- and bottle-agnostic: authorized_keys and the guest IP ride the kernel cmdline, not the rootfs, so one published ext4 boots on any host.

(Branch name nix-fixed-images-stage2 is a vestige — Stage 2 was originally scoped as nix-built images; per discussion it became prebuild-and-pull. The PRD is updated to match.)

Approach (decisions made during design)

  • Prebuild the whole bootable ext4, don't pull an OCI image and assemble on the host — keeps the launch host to just HTTP + gunzip.
  • Gitea generic packages as the host: …/api/packages/<owner>/generic/bot-bottle-infra/<version>/rootfs.ext4.gz (+ .sha256). Release attachments were ruled out — this instance caps them at 100 MB with a filetype allowlist that excludes .ext4.
  • Version = content hash of everything baked into the rootfs (the shipped bot_bottle package + the three Dockerfiles + the init), so a launch host pulls the artifact matching its code and a content change can't silently boot a stale rootfs. Checksum mismatch fails closed.

Pieces

  • firecracker/infra_artifact.py — version hash, pull + verify + gunzip, config (BOT_BOTTLE_INFRA_ARTIFACT_BASE/OWNER/TOKEN, default this Gitea; reuses the shared Gitea token).
  • firecracker/infra_vm.pyensure_built/boot default to pull; BOT_BOTTLE_INFRA_BUILD=local keeps the docker build-from-source path for iterating on Dockerfiles. Docker-build trio extracted to build_infra_images_with_docker().
  • firecracker/publish_infra.py — the off-host half: python3 -m bot_bottle.backend.firecracker.publish_infra [--dry-run|--force] builds the images with Docker, mke2fs's the rootfs (with the buildah build slack), gzips, and PUTs it to the generic package.
  • Tests: test_infra_artifact.py (version determinism/sensitivity, download+verify, checksum fail-closed, 404→publish pointer, config overrides) + updated test_firecracker_infra_vm.py (default pulls without Docker; local mode still builds deps-before-infra).

⚠️ Rollout (not done in this PR)

  1. Enable Gitea packages + grant the publish token write:package (the generic/container package API returns 403 today).
  2. Publish an artifact (python3 -m …publish_infra) — until then the default pull path 404s; BOT_BOTTLE_INFRA_BUILD=local preserves today's behavior.
  3. CI auto-publish on release — follow-up.

Freeze/migrate's remaining host-Docker use is a separate PR (as agreed).

🤖 Generated with Claude Code

https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ

## What Stage 2 of the docker-free Firecracker backend (#348, PRD 0069): the launch host stops building the fixed **infra** image with Docker. Instead the infra VM's rootfs is **prebuilt off-host and pulled as a ready-to-boot ext4 artifact** — the launch host does `HTTP GET → sha256 verify → gunzip → boot`, no Docker, no OCI/rootfs tooling. This works because the infra rootfs is already **host- and bottle-agnostic**: `authorized_keys` and the guest IP ride the kernel cmdline, not the rootfs, so one published ext4 boots on any host. > _(Branch name `nix-fixed-images-stage2` is a vestige — Stage 2 was originally scoped as nix-built images; per discussion it became prebuild-and-pull. The PRD is updated to match.)_ ## Approach (decisions made during design) - **Prebuild the whole bootable ext4**, don't pull an OCI image and assemble on the host — keeps the launch host to just HTTP + gunzip. - **Gitea generic packages** as the host: `…/api/packages/<owner>/generic/bot-bottle-infra/<version>/rootfs.ext4.gz` (+ `.sha256`). Release attachments were ruled out — this instance caps them at 100 MB with a filetype allowlist that excludes `.ext4`. - **Version = content hash** of everything baked into the rootfs (the shipped `bot_bottle` package + the three Dockerfiles + the init), so a launch host pulls the artifact matching its code and a content change can't silently boot a stale rootfs. Checksum mismatch **fails closed**. ## Pieces - `firecracker/infra_artifact.py` — version hash, pull + verify + gunzip, config (`BOT_BOTTLE_INFRA_ARTIFACT_BASE/OWNER/TOKEN`, default this Gitea; reuses the shared Gitea token). - `firecracker/infra_vm.py` — `ensure_built`/`boot` default to pull; `BOT_BOTTLE_INFRA_BUILD=local` keeps the docker build-from-source path for iterating on Dockerfiles. Docker-build trio extracted to `build_infra_images_with_docker()`. - `firecracker/publish_infra.py` — the off-host half: `python3 -m bot_bottle.backend.firecracker.publish_infra [--dry-run|--force]` builds the images with Docker, mke2fs's the rootfs (with the buildah build slack), gzips, and PUTs it to the generic package. - Tests: `test_infra_artifact.py` (version determinism/sensitivity, download+verify, **checksum fail-closed**, 404→publish pointer, config overrides) + updated `test_firecracker_infra_vm.py` (default pulls without Docker; local mode still builds deps-before-infra). ## ⚠️ Rollout (not done in this PR) 1. **Enable Gitea packages** + grant the publish token `write:package` (the generic/container package API returns **403** today). 2. **Publish an artifact** (`python3 -m …publish_infra`) — until then the default pull path 404s; `BOT_BOTTLE_INFRA_BUILD=local` preserves today's behavior. 3. CI auto-publish on release — follow-up. Freeze/migrate's remaining host-Docker use is a **separate PR** (as agreed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
didericis-codex requested changes 2026-07-16 23:25:57 -04:00
Dismissed
didericis-codex left a comment
Collaborator

Two blocking findings in the artifact identity and publishing path. The focused unit suite passes (22 tests), but it does not exercise these cases.

Two blocking findings in the artifact identity and publishing path. The focused unit suite passes (22 tests), but it does not exercise these cases.
@@ -0,0 +66,4 @@
h = hashlib.sha256()
h.update(f"format={_ARTIFACT_FORMAT}\n".encode())
pkg = _REPO_ROOT / "bot_bottle"
for path in sorted(pkg.rglob("*.py")):
Collaborator

[P1] Include every copied runtime file in the artifact version. This loop hashes only *.py, but Dockerfile.gateway also copies bot_bottle/egress_entrypoint.sh into /app/egress-entrypoint.sh, and Dockerfile.orchestrator copies the package directory wholesale. A change to that shell entrypoint therefore leaves the version unchanged, so a launch host can fetch and boot an older rootfs even though its checkout contains different runtime code. Hash the actual Docker build inputs (respecting .dockerignore) or at minimum all files explicitly copied into these images, and add a regression test showing that changing the shell script changes the version.

**[P1] Include every copied runtime file in the artifact version.** This loop hashes only `*.py`, but `Dockerfile.gateway` also copies `bot_bottle/egress_entrypoint.sh` into `/app/egress-entrypoint.sh`, and `Dockerfile.orchestrator` copies the package directory wholesale. A change to that shell entrypoint therefore leaves the version unchanged, so a launch host can fetch and boot an older rootfs even though its checkout contains different runtime code. Hash the actual Docker build inputs (respecting `.dockerignore`) or at minimum all files explicitly copied into these images, and add a regression test showing that changing the shell script changes the version.
@@ -0,0 +127,4 @@
if args.force:
_delete(gz_url, token)
_delete(sha_url, token)
_put(gz_url, gz.read_bytes(), token)
Collaborator

[P1] Stream the rootfs upload instead of reading it into memory. gz.read_bytes() materializes the entire compressed ext4 in Python before urlopen sends it. This image is built with 8 GiB of writable slack and is explicitly expected to be hundreds of MB, so publishing can require another artifact-sized allocation and OOM a modest CI/build host. _put should accept a path/file and stream it (with an explicit content length if Gitea requires one); the checksum file can remain a small in-memory upload.

**[P1] Stream the rootfs upload instead of reading it into memory.** `gz.read_bytes()` materializes the entire compressed ext4 in Python before `urlopen` sends it. This image is built with 8 GiB of writable slack and is explicitly expected to be hundreds of MB, so publishing can require another artifact-sized allocation and OOM a modest CI/build host. `_put` should accept a path/file and stream it (with an explicit content length if Gitea requires one); the checksum file can remain a small in-memory upload.
didericis reviewed 2026-07-16 23:27:37 -04:00
@@ -0,0 +54,4 @@
def local_build_requested() -> bool:
"""True when the operator opted into the dev Docker-build path instead of
pulling the published artifact (`BOT_BOTTLE_INFRA_BUILD=local`)."""
return os.environ.get("BOT_BOTTLE_INFRA_BUILD", "").strip().lower() == "local"
Owner

If this isn't set, should fallback to a config setting from the sqlite db... that will I think require having the host do a request to the orchestrator via HTTP, since the db now lives in the infra vm. Evaluate the merits of that decision/help me determine whether it's worth it.

If this isn't set, should fallback to a config setting from the sqlite db... that will I think require having the host do a request to the orchestrator via HTTP, since the db now lives in the infra vm. Evaluate the merits of that decision/help me determine whether it's worth it.
didericis-codex reviewed 2026-07-16 23:29:00 -04:00
didericis-codex left a comment
Collaborator

I don’t think an orchestrator/SQLite fallback is worth adding for BOT_BOTTLE_INFRA_BUILD. This setting is needed while deciding how to obtain the rootfs, before the infra VM can be booted; on a cold start there is no orchestrator to query until after that decision succeeds, so an HTTP fallback creates a bootstrap cycle. It would also move a host bootstrap choice into the runtime-state database, while the registry module explicitly says declarative config stays under ~/.bot-bottle/.

My recommendation: keep the environment variable as the immediate override in this PR. If we want a persistent default later, read it from a small host-side declarative config file (env > host config > pull), not from the infra VM database. That works for first boot, recovery when the VM is unhealthy, and offline/local-build operation without introducing a new unauthenticated pre-boot control-plane endpoint.

I don’t think an orchestrator/SQLite fallback is worth adding for `BOT_BOTTLE_INFRA_BUILD`. This setting is needed while deciding how to obtain the rootfs, before the infra VM can be booted; on a cold start there is no orchestrator to query until after that decision succeeds, so an HTTP fallback creates a bootstrap cycle. It would also move a host bootstrap choice into the runtime-state database, while the registry module explicitly says declarative config stays under `~/.bot-bottle/`. My recommendation: keep the environment variable as the immediate override in this PR. If we want a persistent default later, read it from a small host-side declarative config file (env > host config > `pull`), not from the infra VM database. That works for first boot, recovery when the VM is unhealthy, and offline/local-build operation without introducing a new unauthenticated pre-boot control-plane endpoint.
@@ -0,0 +54,4 @@
def local_build_requested() -> bool:
"""True when the operator opted into the dev Docker-build path instead of
pulling the published artifact (`BOT_BOTTLE_INFRA_BUILD=local`)."""
return os.environ.get("BOT_BOTTLE_INFRA_BUILD", "").strip().lower() == "local"
Collaborator

I don’t think an orchestrator/SQLite fallback is worth adding for BOT_BOTTLE_INFRA_BUILD. This setting is needed while deciding how to obtain the rootfs, before the infra VM can be booted; on a cold start there is no orchestrator to query until after that decision succeeds, so an HTTP fallback creates a bootstrap cycle. It would also move a host bootstrap choice into the runtime-state database, while the registry module explicitly says declarative config stays under ~/.bot-bottle/.

My recommendation: keep the environment variable as the immediate override in this PR. If we want a persistent default later, read it from a small host-side declarative config file (env > host config > pull), not from the infra VM database. That works for first boot, recovery when the VM is unhealthy, and offline/local-build operation without introducing a new unauthenticated pre-boot control-plane endpoint.

I don’t think an orchestrator/SQLite fallback is worth adding for `BOT_BOTTLE_INFRA_BUILD`. This setting is needed while deciding how to obtain the rootfs, before the infra VM can be booted; on a cold start there is no orchestrator to query until after that decision succeeds, so an HTTP fallback creates a bootstrap cycle. It would also move a host bootstrap choice into the runtime-state database, while the registry module explicitly says declarative config stays under `~/.bot-bottle/`. My recommendation: keep the environment variable as the immediate override in this PR. If we want a persistent default later, read it from a small host-side declarative config file (env > host config > `pull`), not from the infra VM database. That works for first boot, recovery when the VM is unhealthy, and offline/local-build operation without introducing a new unauthenticated pre-boot control-plane endpoint.
Owner

@didericis-codex duh, right, good call. My bad. That said, it's unfortunate how the db can't be mounted on the host. A host mounted db would be a great way to centralize both state and config, think it'd be ideal. Env vars work fine for now, but it'd be a bit more versatile to drive this from a db. Would also be cool if we could have a global edit panel and view into this from the console. That way the console can drive headless servers more easily/without needing to go onto the host.

@didericis-codex duh, right, good call. My bad. That said, it's unfortunate how the db can't be mounted on the host. A host mounted db would be a great way to centralize both state and config, think it'd be ideal. Env vars work fine for now, but it'd be a bit more versatile to drive this from a db. Would also be cool if we could have a global edit panel and view into this from the console. That way the console can drive headless servers more easily/without needing to go onto the host.
didericis-codex reviewed 2026-07-16 23:38:18 -04:00
@@ -0,0 +54,4 @@
def local_build_requested() -> bool:
"""True when the operator opted into the dev Docker-build path instead of
pulling the published artifact (`BOT_BOTTLE_INFRA_BUILD=local`)."""
return os.environ.get("BOT_BOTTLE_INFRA_BUILD", "").strip().lower() == "local"
Collaborator

Agreed on the product goal: the console should have a global settings view and be able to manage a headless host without SSH. I’d separate that from making the SQLite file host-mounted, though. The registry ext4 is a block device attached read/write to the VM; mounting it concurrently on the host would give ext4 two independent writers and risks filesystem/DB corruption. Moving SQLite onto a shared filesystem also makes its locking/durability semantics the hard part.

A cleaner split seems to be:

  • Keep the tiny set of bootstrap settings needed to start/recover the infra VM (pull vs local, artifact registry location/auth) in host-owned config/env.
  • Put global runtime config in the central DB, owned exclusively by the orchestrator, and expose typed config CRUD over its HTTP API. The console can then view/edit it remotely without mounting the DB or logging into the host.
  • If the console must change bootstrap settings or recover a dead infra VM remotely, add a minimal host-side supervisor/API for those operations; the in-VM orchestrator cannot serve that role while it is down.

That preserves the centralized console experience for nearly everything while keeping the unavoidable pre-boot layer explicit and small. I’d treat the config API/console panel as a follow-up rather than expand this artifact PR.

Agreed on the product goal: the console should have a global settings view and be able to manage a headless host without SSH. I’d separate that from making the SQLite file host-mounted, though. The registry ext4 is a block device attached read/write to the VM; mounting it concurrently on the host would give ext4 two independent writers and risks filesystem/DB corruption. Moving SQLite onto a shared filesystem also makes its locking/durability semantics the hard part. A cleaner split seems to be: - Keep the tiny set of **bootstrap settings** needed to start/recover the infra VM (`pull` vs `local`, artifact registry location/auth) in host-owned config/env. - Put **global runtime config** in the central DB, owned exclusively by the orchestrator, and expose typed config CRUD over its HTTP API. The console can then view/edit it remotely without mounting the DB or logging into the host. - If the console must change bootstrap settings or recover a dead infra VM remotely, add a minimal host-side supervisor/API for those operations; the in-VM orchestrator cannot serve that role while it is down. That preserves the centralized console experience for nearly everything while keeping the unavoidable pre-boot layer explicit and small. I’d treat the config API/console panel as a follow-up rather than expand this artifact PR.
didericis added 2 commits 2026-07-16 23:42:05 -04:00
Stage 2 of the docker-free Firecracker backend (#348): stop building the
fixed infra image on the launch host. The infra VM's rootfs is host- and
bottle-agnostic (authorized_keys + guest IP ride the kernel cmdline, not the
rootfs), so it's built once off-host and published as a versioned, ready-to-
boot ext4; the launch host downloads + verifies + boots it — no Docker, no
image tooling, just HTTP + gunzip.

- infra_artifact.py: version = content hash of the rootfs inputs (the shipped
  bot_bottle package + the three Dockerfiles + the init), so a launch host
  pulls the artifact matching its code and a content change can't silently
  boot a stale rootfs. Pull + sha256-verify (fail-closed) + gunzip from a
  Gitea generic package; base/owner/token configurable, default this Gitea.
- infra_vm.ensure_built/boot default to the pull path; BOT_BOTTLE_INFRA_BUILD=
  local keeps the docker build-from-source path for iterating on Dockerfiles.
- publish_infra.py: the off-host half — builds the images with Docker, mke2fs
  the rootfs (with buildah slack), gzips, and PUTs it to the generic package.

Rollout note: default=pull means a launch 404s until an artifact is published;
until the Gitea packages endpoint is enabled + an artifact published, use
BOT_BOTTLE_INFRA_BUILD=local. Freeze/migrate's remaining docker use is a
separate PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
fix(firecracker): read only BOT_BOTTLE_INFRA_ARTIFACT_TOKEN for the artifact
lint / lint (push) Successful in 2m37s
test / unit (pull_request) Successful in 1m25s
test / integration (pull_request) Successful in 31s
test / coverage (pull_request) Successful in 1m31s
8d8a88aeeb
Drop the fallback to the general-purpose BOT_BOTTLE_CLAUDE_GITEA_TOKEN so
the artifact pull/publish uses a dedicated, package-scoped token that can
be granted (or revoked) independently of the general Gitea token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
didericis force-pushed nix-fixed-images-stage2 from a73aba935f to 8d8a88aeeb 2026-07-16 23:42:05 -04:00 Compare
Owner

@didericis-claude address the findings of the review

@didericis-claude address the findings of the review
didericis added 1 commit 2026-07-16 23:47:01 -04:00
fix(firecracker): hash all baked-in files + stream the artifact upload
lint / lint (push) Successful in 2m26s
test / unit (pull_request) Successful in 1m44s
test / integration (pull_request) Successful in 40s
test / coverage (pull_request) Successful in 1m32s
f2891a1634
Address PR #395 review (two P1s):

- Version hash covered only `bot_bottle/**.py`, but the image `COPY`s the
  whole package — non-Python inputs baked in (egress_entrypoint.sh,
  netpool.defaults.env) didn't change the version, so a launch host could
  boot a stale rootfs whose code differs from its checkout. Hash every
  regular file under bot_bottle/ (excluding __pycache__/.pyc). Regression
  tests: a shell-script change bumps the version; .pyc/__pycache__ don't.

- publish_infra `_put` read the whole (hundreds-of-MB) gz into memory via
  read_bytes(). Stream it from disk with an explicit Content-Length; the
  tiny .sha256 stays in-memory. Test asserts the body is the file object,
  not bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
Author
Collaborator

Addressed both blocking findings in f2891a1.

[P1] Artifact version now hashes every baked-in file, not just *.py. You're right — Dockerfile.orchestrator COPY bot_bottle's the whole package, so egress_entrypoint.sh (and netpool.defaults.env, the contrib Dockerfiles) are baked in but didn't move the version. infra_artifact_version now hashes every regular file under bot_bottle/ (excluding only __pycache__/.pyc, which are never copied) plus the three Dockerfiles and the init. Regression tests added: changing the shell entrypoint bumps the version; adding .pyc/__pycache__ does not. (infra_artifact_version gained a repo_root kwarg so the test can hash a synthetic tree.)

[P1] Upload streams from disk. _put no longer does gz.read_bytes(); it opens the .ext4.gz and passes the file object as the request body with an explicit Content-Length, so urlopen sends it in blocks instead of materializing a second artifact-sized buffer. The tiny .sha256 stays in-memory. Test asserts the body is the file object (has read, not bytes) with the right Content-Length.

On the BOT_BOTTLE_INFRA_BUILD / config-DB discussion (thanks for the detailed reasoning): agreed, and I've kept it as a plain env override in this PR — no orchestrator/SQLite fallback, no pre-boot control-plane endpoint. The "bootstrap settings in host-owned config vs. global runtime config in the orchestrator DB" split sounds right as a follow-up, out of scope here.

Also note: the full publish pipeline is now validated end-to-end via --dry-run — builds all three images, exports, mke2fs, gzips to a 195 MB rootfs.ext4.gz; only the upload PUTs remain (pending your go-ahead to actually publish).

Addressed both blocking findings in `f2891a1`. **[P1] Artifact version now hashes every baked-in file, not just `*.py`.** You're right — `Dockerfile.orchestrator` `COPY bot_bottle`'s the whole package, so `egress_entrypoint.sh` (and `netpool.defaults.env`, the contrib Dockerfiles) are baked in but didn't move the version. `infra_artifact_version` now hashes every regular file under `bot_bottle/` (excluding only `__pycache__`/`.pyc`, which are never copied) plus the three Dockerfiles and the init. Regression tests added: changing the shell entrypoint bumps the version; adding `.pyc`/`__pycache__` does not. (`infra_artifact_version` gained a `repo_root` kwarg so the test can hash a synthetic tree.) **[P1] Upload streams from disk.** `_put` no longer does `gz.read_bytes()`; it opens the `.ext4.gz` and passes the file object as the request body with an explicit `Content-Length`, so `urlopen` sends it in blocks instead of materializing a second artifact-sized buffer. The tiny `.sha256` stays in-memory. Test asserts the body is the file object (has `read`, not `bytes`) with the right `Content-Length`. On the `BOT_BOTTLE_INFRA_BUILD` / config-DB discussion (thanks for the detailed reasoning): agreed, and I've kept it as a plain env override in this PR — no orchestrator/SQLite fallback, no pre-boot control-plane endpoint. The "bootstrap settings in host-owned config vs. global runtime config in the orchestrator DB" split sounds right as a **follow-up**, out of scope here. Also note: the full publish pipeline is now validated end-to-end via `--dry-run` — builds all three images, exports, `mke2fs`, gzips to a **195 MB** `rootfs.ext4.gz`; only the upload `PUT`s remain (pending your go-ahead to actually publish).
Owner

@didericis-claude go ahead with the actual publish

@didericis-claude go ahead with the actual publish
didericis-codex approved these changes 2026-07-16 23:56:28 -04:00
didericis-codex left a comment
Collaborator

The two blocking findings are resolved in f2891a1. The artifact identity now covers every regular file copied into the image (while excluding generated bytecode), and the large rootfs upload is streamed from an open file with an explicit Content-Length. Focused tests pass (26/26), the full unit suite passes (1779/1779), and diff --check is clean.

The two blocking findings are resolved in f2891a1. The artifact identity now covers every regular file copied into the image (while excluding generated bytecode), and the large rootfs upload is streamed from an open file with an explicit Content-Length. Focused tests pass (26/26), the full unit suite passes (1779/1779), and diff --check is clean.
didericis added 1 commit 2026-07-16 23:59:43 -04:00
chore(firecracker): ship an about.txt description with the infra artifact
lint / lint (push) Successful in 2m55s
test / unit (pull_request) Successful in 1m47s
test / integration (pull_request) Successful in 38s
test / coverage (pull_request) Successful in 1m28s
e3d24b7e41
Generic packages have no description field, so publish_infra now uploads a
short about.txt alongside the rootfs on every publish — it's what identifies
the package as the Firecracker backend's infra rootfs on the package page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
didericis added 1 commit 2026-07-17 00:04:58 -04:00
chore(firecracker): name the artifact package bot-bottle-firecracker-infra
test / unit (pull_request) Successful in 1m19s
test / integration (pull_request) Successful in 23s
test / coverage (pull_request) Successful in 1m18s
lint / lint (push) Successful in 2m17s
test / unit (push) Successful in 1m18s
test / integration (push) Successful in 32s
test / coverage (push) Successful in 1m21s
Update Quality Badges / update-badges (push) Successful in 1m17s
e3e195f866
Rename the Gitea generic package from bot-bottle-infra to
bot-bottle-firecracker-infra so it's self-evident in the package list which
backend it serves (and leaves room for other artifacts, e.g. a shipped
kernel). The version slot stays the content hash — "firecracker" belongs in
the package name, not the version. Docker image / VM names are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
didericis merged commit e3e195f866 into main 2026-07-17 00:15:18 -04:00
didericis deleted branch nix-fixed-images-stage2 2026-07-17 00:15:20 -04:00
Sign in to join this conversation.