Skip to content

feat: add invoke bootstrap, a stack that loads itself - #27

Open
lancamat1 wants to merge 2 commits into
mainfrom
feat/invoke-bootstrap
Open

feat: add invoke bootstrap, a stack that loads itself#27
lancamat1 wants to merge 2 commits into
mainfrom
feat/invoke-bootstrap

Conversation

@lancamat1

@lancamat1 lancamat1 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Two commits, separable: 6a0e37f adds invoke bootstrap; cc1f999 makes a generated repository pass its own invoke lint. Drop the second if you'd rather take it separately.


1. invoke bootstrap

One command from a freshly generated repository to a populated Infrahub, via the mechanism
Infrahub actually runs in production: it clones the repository and loads what
.infrahub.yml declares.

uv sync --all-packages && source .venv/bin/activate
invoke bootstrap

It publishes the current commit as a bare Git origin the containers can reach, starts the
stack, registers it with Infrahub as a CoreRepository, and waits for the import to
finish — polling sync_status and the tracked commit rather than sleeping a fixed
interval, so it cannot report success for an instance that loaded nothing.

Why

Getting a generated repo into a usable state took invoke start, then a wait for the API
(start returns before the server answers), then load-schema / load-menu /
load-objects. That is a serviceable dev loop, but every step pushes files in from the host
with infrahubctl — which is not how the repository will be consumed in production, and it
means a new user's first experience of Infrahub's Git integration is no experience of it.

bootstrap sits alongside the load-* tasks; they are untouched and remain the faster loop
while iterating on a schema that has not been committed yet.

.infrahub.yml becomes a template

This is the part worth a close look. .infrahub.yml declared only schemas, regardless
of which features were enabled:

---
schemas:
  - schemas

So a repository import from a generated repo never picked up its objects or menus, even with
objects: true / menus: true. Generating with getting_started: true produced an
objects/example.yml that Infrahub would not load.

It is now .infrahub.yml.jinja and declares each section when the matching feature is on:

Answers Rendered .infrahub.yml
all features on schemas, objects, menus
getting_started: true schemas, objects
everything off schemas

The origin is a bare mirror, not the working copy

infrahub-demo-dc and infrahub-demo-sp mount the working copy into the task worker
(./:/upstream) and register that path. I started there; two things push away from it:

  • Git worktrees. In a worktree checkout, .git is a file pointing at the main
    repository's admin directory, which is outside the mount, so the in-container clone fails
    with not a git repository. Anyone using git worktree — increasingly common with
    agent-driven workflows — cannot bootstrap at all.
  • Push-back. A CoreRepository is read-write and Infrahub pushes proposed-change
    branches to the origin. Git refuses a push to the checked-out branch of a non-bare
    repository, so a merged proposed change cannot land back in a working-copy origin.

So bootstrap publishes HEAD into .origin/<name>.git (gitignored) and the new
docker-compose.override.yml mounts that at /remote — the same mount point
infrahub_sdk.testing.repository.GitRepo and opsmill/infrahub's own tasks/dev.py use.
The container path is constant rather than derived from the checkout directory, so renaming
or copying the repo never desyncs it from the location Infrahub recorded.

The cost is one extra gitignored directory and a git push; the payoff is that it works
from a worktree and the merge-back path is not broken by construction.

Two accepted trade-offs, documented in the task docstring and the README

  • Only committed history is loaded. The origin is a mirror of HEAD, so an uncommitted
    edit is invisible to Infrahub. bootstrap warns when the working tree is dirty rather
    than letting a missing change look like a broken import.
  • It loads onto the default branch. That is what a bootstrap is for — an empty instance
    has no data to migrate and nothing to preview. The README points at the branch +
    proposed-change workflow for once there is data.

2. A generated repository now passes its own invoke lint

invoke lint could not pass on a fresh generation, in any configuration — which makes a red
lint gate meaningless as a signal for someone starting from this template. All of these are
in the template's own scaffolding; none are in the bootstrap task.

  • lint_mypy pointed at infrahub_sdk, a directory a generated repository does not
    have, so mypy failed outright before checking anything: Cannot read file 'infrahub_sdk'.
    It now targets the paths that exist — which of lib/, scripts/ and tests/ are present
    depends on the features chosen, and mypy errors on a path that is not there.
  • preview = true alongside select = ["ALL"] opts a generated repository into every
    unstable ruff rule, so its lint gate can go red on a ruff release with no code change.
    Two such rules were already firing (noqa-comments, write-whole-file). Set to false,
    with a comment saying how to opt back in. The write-whole-file suggestion was worth
    taking on its own terms, so download_compose_file now uses Path.write_text; the
    noqa-comments one wanted # ruff:ignore[...], itself an unstable syntax, so # noqa
    stays.
  • lib/example.py had a wrong annotation: nodes: list[InfrahubNode] while the body
    called nodes.keys(). client.schema.all() returns MutableMapping[str, MainSchemaTypesAPI], so the example users copy was misdescribing its own input.
  • Import order (third-party before first-party), missing return annotations, a missing
    tests/integration/__init__.py (INP001 on both test files), a missing trailing newline
    in objects/example.yml, and ruff format on test_infrahub.py.
  • scripts/* gets a scoped INP001 ignore rather than an __init__.py — those are
    standalone entry points, not an importable package.

Also: the README's testing section named INFRAHUB_TESTING_IMAGE_VERSION. That one is
internal to infrahub-testcontainers and always overwritten in create_env_file; the
variable actually read from the environment is INFRAHUB_TESTING_IMAGE_VER
(helpers.py:23). Corrected, and the paragraph now says what the default is.


Verified

Rendered all three answer sets and ran the real thing against Infrahub 1.11.0.

Render invoke lint ruff format --check invoke bootstrap
All features on pass clean 89s → in-sync / online; example BuiltinTag "Yellow" in the graph
getting_started: true pass clean
Everything off pass clean 73s → in-sync / online

The all-features render is the case I was least sure of: a menus/ directory containing
nothing but .gitkeep does not break the import, and neither does an empty schemas/.
Re-runs were exercised in the downstream repo this came from: same commit returns in ~2s, a
new commit waits out the once-a-minute scheduled sync (~67s) and confirms the new commit.

The only remaining yamllint output is a document-start warning on
.copier-answers.yml, which copier generates itself.

🤖 Generated with Claude Code

lancamat1 and others added 2 commits August 20, 2026 22:15
Getting a generated repository into a usable state took `invoke start`,
then a wait for the API, then load-schema / load-menu / load-objects — and
none of that exercises the mechanism Infrahub actually runs in production,
which is reading a Git repository and loading what .infrahub.yml declares.

`invoke bootstrap` publishes the current commit as a bare Git origin the
containers can reach, starts the stack, registers it with Infrahub, and
waits for the import to genuinely finish — polling sync_status and the
tracked commit rather than sleeping, so it cannot report success for an
instance that loaded nothing.

.infrahub.yml declared only `schemas`, whatever features were enabled, so a
repository import never picked up a generated repo's objects or menus. It
becomes a template and declares `objects` and `menus` when those features
are on.

The origin is a bare mirror under .origin rather than the working copy that
the OpsMill demo repos mount: in a git worktree checkout `.git` is a file
pointing outside the mount, so the in-container clone fails, and Git
refuses the proposed-change branches Infrahub pushes back to a non-bare
repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`invoke lint` could not pass on a freshly generated repository, which makes
a red lint gate meaningless as a signal for anyone starting from this
template. Every fix here is in the template's own scaffolding, not in the
bootstrap task.

- lint_mypy pointed at `infrahub_sdk`, a directory a generated repository
  does not have, so mypy failed outright before checking anything. It now
  targets the paths that exist, since which of lib/, scripts/ and tests/
  are present depends on the features chosen.
- `preview = true` with `select = ["ALL"]` opts a generated repository into
  every unstable ruff rule, so its lint gate can go red on a ruff release
  with no code change. Two such rules were already firing.
- lib/example.py annotated `nodes` as `list[InfrahubNode]` while the body
  called `nodes.keys()`. client.schema.all() returns a mapping keyed by
  kind, so the annotation was wrong in an example users copy.
- Import order, return annotations, a missing tests/integration/__init__.py,
  a missing trailing newline in objects/example.yml, and ruff formatting.

README: the testing section named INFRAHUB_TESTING_IMAGE_VERSION, which is
internal to infrahub-testcontainers and always overwritten. The variable it
reads from the environment is INFRAHUB_TESTING_IMAGE_VER.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lancamat1
lancamat1 force-pushed the feat/invoke-bootstrap branch from 9b531fb to cc1f999 Compare August 20, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant