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. contentmay 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.