Manual

Declaring files

file() is the core: everything else is a shorthand that calls it. And how a declared tree is written out, archived, or absorbed from playbooks you have.

file()#

file(path, content, mode=None, only=None) is the core. Everything else is a shorthand that calls it.

  • Common leading indentation is stripped, so triple-quoted blocks can be indented to match the surrounding code, and a trailing newline is added.
  • Parent directories are implicit: file("roles/nginx/tasks/main.yml", ...) just works.
  • content may be a dict or list, dumped as YAML (JSON if PyYAML is missing): file("group_vars/all.yml", {"nginx_port": 8080}).
  • Redeclaring a path replaces it. Paths are relative and may not escape the tree.
  • include(src, dest=None) reads a file off disk (binary safe, keeps +x); include_tree(src, dest="") takes a whole directory.

Unless you declare your own, an inventory (localhost ansible_connection=local) and a minimal ansible.cfg are added so the result runs as-is; --bare turns that off. A requirements.yml appears if you declared any galaxy dependency.

Writing the tree out#

generate [DEST] writes the playbook directory (default out). It records what it wrote in a .instamix marker, so re-generating removes files you deleted from the script. It refuses to write into a non-empty directory it did not create unless you pass --force. diff [DEST] shows what generate DEST would change.

Archives#

generate writes a tar archive instead when DEST is named like one: .tar, .tar.gz/.tgz, .tar.bz2/.tbz2 or .tar.xz/.txz. - writes the archive to stdout, gzipped by default. -z/--compress gz|bz2|xz|none overrides the compression the name implies, and it also forces an archive for any other name:

$ python site.py generate site.tar.gz -p prod
$ python site.py generate - | ssh host 'mkdir -p pb && tar xzf - -C pb'
$ python site.py generate bundle -z xz

Archives are reproducible. Entries are sorted, owned by root, with modes 0644/0755 unless declared, and dated SOURCE_DATE_EPOCH if that is set. They leave out the .instamix marker. From Python, use archive(dest, compression=None).

Absorbing what you have#

$ imix import ./old-playbooks -o site.py

import is byte-exact: importing a tree and generating it again reproduces the original files, including binary ones and files without a trailing newline.

What import leaves out#

import leaves out files that usually hold secrets or personal state: .env and .env.* (but not .env.example/.sample/.template/.dist), private keys (*.pem, *.key, id_rsa, …), vault password files, .netrc, .pypirc, .npmrc, Terraform state, *.local.* (such as .claude/settings.local.json), *.retry and .ansible/.

It also leaves out any file whose contents look like a credential: DigitalOcean, Tailscale, Anthropic, OpenRouter, OpenAI, GitHub, Telegram, AWS, Slack and Google tokens, and private keys (instamix.SECRET_PATTERNS; placeholders like sk-ant-xxx do not match).

It names what it skipped, and why, on stderr and in a comment at the top of the script; import --all takes everything. The name list is instamix.IMPORT_BLACKLIST. VCS and cache directories (.git, .venv, __pycache__, …) are never imported.

The source of this page

    Type to search. ↑ ↓ to move, enter to open.