gitvars-render
gitvars exists so that your deployed assets can point back to their source.
Git knows the commit, its author, and the code behind a change. gitvars carries
that context into the files you deploy. Add placeholders to your configuration,
code, or documentation, and replace them with Git metadata during your build—
including links to the exact commit and source file.
Why gitvars exists
I built it for a recurring need in data engineering: labeling data assets and pipelines with the version of code behind them. When inspecting a table or an Airflow DAG, I wanted a direct route back to the change that produced it. Embedding Git metadata in labels, descriptions, and configuration makes that connection easier to follow when investigating an issue or understanding a deployment.
The same idea works for Terraform resources, application configuration, and
documentation. The example page uses gitvars to fill in its
Git metadata at build time.
Installation
The Python distribution is named gitvars-render and installs the gitvars command.
The Python import remains gitvars. It
requires Python 3.11+ and Git on PATH, with no runtime dependencies or network
calls during rendering.
Install the package from a source checkout into your Python environment,
replacing /path/to/gitvars-render with the directory containing its pyproject.toml:
python -m pip install /path/to/gitvars-render
Once this project is published to PyPI, install its release by package name:
python -m pip install gitvars-render
Usage
Run gitvars in the Git repository containing the files you want to process.
There are two commands:
| Command | Behavior |
|---|---|
gitvars render [PATH ...] |
Replace placeholders in selected files, in place by default, or write copies with --output-dir. |
gitvars check [PATH ...] |
Read selected files without writing. Exit 1 if rendering would change any file, or 0 if no changes are needed. |
Add --dry-run to render to preview a unified diff without writing anything.
Both commands read configuration from the nearest .gitvars found by searching
upward from the working directory, not from each input path. Use
--config FILE to select a specific configuration file. A configuration file is optional:
you can supply file selection and other options on the command line instead.
Select files and directories
Both commands accept zero, one, or several positional paths. Paths are files or directories relative to your working directory; absolute paths also work. You may mix files and directories, but every input must belong to the same Git repository. Overlapping inputs are deduplicated.
| Inputs | Selection |
|---|---|
| No paths | Scan the current directory recursively. |
| One or more directories | Scan those directories recursively. |
| One or more files | Select those files directly; no include patterns are required. |
Paths choose where to scan; include patterns choose which files to render.
Directory scans require include patterns in .gitvars or repeatable
--include options. Globs are relative to the repository root. Exclusions still
apply to explicitly supplied files, even though include patterns are unnecessary
for them.
For example, preview SQL files under models/ and Markdown files under docs/,
without requiring a configuration file:
gitvars render models docs --include '**/*.sql' --include '**/*.md' --dry-run
Or preview two specific files without --include:
gitvars render models/model.sql docs/pipeline.md --dry-run
To keep selection in configuration, create .gitvars at the repository root:
include = ["**/*.sql", "**/*.md"]
exclude = ["build/**", "dist/**", ".venv/**"]
With that configuration, no CLI --include is needed. Narrow the scan to the
two directories, or omit paths to scan the current directory:
gitvars render models docs --dry-run
gitvars check models docs
gitvars render --dry-run
For the complete configuration options and matching rules, see Configuration.
Choose where rendering writes
render defaults to in-place replacement. This is intended for fresh CI
checkouts: substitution consumes the placeholders. Local use is supported, but
every selected input must be tracked and have no staged or unstaged changes.
Unrelated modified files do not block rendering. Restore the templates from Git
before rendering them again for a different commit.
Using the configuration above, render both directories in place:
gitvars render models docs
For generated artifacts, keep templates intact and render copies instead:
gitvars render models docs --output-dir build
models/nested/model.sql becomes build/models/nested/model.sql: paths
are always relative to the repository root, without flattening. Unselected files
in input directories are copied unchanged as bytes, including images and other
assets. Excluded files are omitted.
The output directory must be empty or absent. Use -f / --force to clear a
nonempty output directory before generating it again. Its contents are removed,
including files unrelated to the previous run. The flag never permits overlapping
inputs and outputs: an output directory cannot be inside an input directory,
equal an input, or contain an input. Symlink destinations and destinations inside
.git are refused. A common safe layout is sibling models/, docs/, and
build/ directories, with only models docs as the inputs. Omitting the paths
would scan the current directory, so a build/ destination inside it would be
rejected even if build/** is excluded.
gitvars render models docs --output-dir build --force
gitvars render models docs --output-dir build --force --dry-run
--dry-run prints unified diffs without changing any files, creating directories,
or clearing existing output. check reports whether rendering would change
selected files. Both can inspect dirty or untracked inputs.
Configuration
Use TOML in .gitvars. The nearest file found by searching upward from the
working directory wins. --config FILE overrides discovery. A directory input
requires include patterns from configuration or --include; with no paths,
the input is the working directory.
prefix = "__git_"
escape = true
remote = "origin"
provider = "auto"
include = ["**/*.sql", "**/*.md", "**/*.yml", "**/*.yaml"]
exclude = ["build/**", "dist/**", ".venv/**"]
exclude_regex = ['(^|/)README\.md$']
# Optional repository web root, including owner/group and repository:
# url_prefix = "https://git.company.com/team/analytics"
# provider = "gitlab" # Required for self-hosted hosts.
All selection patterns are repository-relative, regardless of the working
directory or configuration location. Paths use /, and matching is case-sensitive:
| Pattern | Matches |
|---|---|
*.md |
Markdown files at the repository root |
**/*.md |
Markdown files at any depth, including the root |
build/** |
The root build directory and everything beneath it |
templates/?.sql |
One-character SQL filenames directly under templates/ |
*, ?, and character classes match within a path segment. A standalone **
segment matches zero or more directories. Globs match the whole relative path;
regex exclusions search it. Matching an excluded directory excludes its subtree.
There are no negation/reinclude rules. .gitignore is not implicitly consulted.
CLI --include, --exclude, and --exclude-regex are repeatable and extend their
configuration lists. CLI --prefix and --escape / --no-escape override scalar
configuration. Matching any exclusion wins over inclusion or an explicit file.
Symlinks, .git, and nested repositories are skipped during traversal. Explicit
inputs from different repositories are rejected. All files in an invocation use
one commit snapshot. A repository without a commit at HEAD is an error.
Placeholders
| Placeholder | Value |
|---|---|
__git_commit__ |
Full HEAD commit SHA |
__git_commit_short__ |
Git's abbreviated SHA |
__git_author_name__ |
Commit author name |
__git_author_email__ |
Commit author email |
__git_committer_name__ |
Committer name |
__git_committer_email__ |
Committer email |
__git_commit_subject__ |
Commit subject |
__git_commit_message__ |
Full commit message, including newlines |
__git_commit_date__ |
Author date in ISO 8601 with timezone |
__git_branch__ |
Current branch; empty in detached HEAD |
__git_tag__ |
Highest version-ordered tag pointing at HEAD; empty if none |
__git_repository__ |
Repository name |
__git_commit_url__ |
Commit browser URL, if available |
__git_file_url__ |
Input-file permalink at HEAD, if the file exists there |
Commit metadata describes checked-out HEAD, including when output-directory
inputs have local edits. It does not identify the deployer or the last author of
each file. File links refer to the version committed at HEAD, which can differ
from a dirty local input. Tags are ordered using Git's version sort, not creation
date; only tags available in the local checkout participate.
Supported placeholders are replaced everywhere, including Markdown inline code
and fenced code blocks. Unknown or incomplete placeholders are left untouched.
Prefix strings are literal, not regexes. Unavailable known values become empty
strings, with warnings under --verbose.
Escape a supported placeholder with an immediately preceding backslash:
__git_commit__ -> the actual SHA
\__git_commit__ -> literal __git_commit__
One escape backslash is consumed; all other backslashes remain unchanged.
Disable escape handling with escape = false or --no-escape. Then a backslash
is ordinary text: \__git_commit__ becomes a backslash followed by the SHA.
Replacement is always one-pass; inserted metadata and escaped tokens are never
processed again during that invocation.
Values are inserted verbatim by default. Use --html-escape with render or
check to encode inserted values for HTML text and quoted attributes, including
&, <, >, and quotes. Template text and literal placeholders stay unchanged.
This is separate from backslash placeholder escaping. Template authors remain
responsible for quoting and escaping values for other output formats.
Repository URLs
GitHub, GitLab, and Bitbucket Cloud are supported, including common HTTPS,
SCP-style SSH, and ssh:// remotes. provider = "auto" recognizes github.com,
gitlab.com, and bitbucket.org. Self-hosted GitHub/GitLab installations require
an explicit provider. A self-hosted url_prefix with auto is a configuration
error; an unrecognized remote without an override simply produces empty links.
url_prefix takes precedence over the selected remote for links and repository
names. Name discovery uses its final path component, then the selected remote's,
then the local repository-root directory name. A trailing .git is removed from
URL-derived names. The default remote is origin. No hosting APIs are used.
File URLs use the original input's repository-relative path, with components
URL-encoded, and the full commit SHA. A file absent from HEAD has no permalink.
Files, failures, and exit codes
Only selected files are decoded as UTF-8; there is no binary detection. A selected file that cannot be decoded produces an error. Original newline characters, including mixed line endings and the UTF-8 BOM, are preserved outside substituted values. Multiline metadata is inserted verbatim. File permissions are preserved; unchanged in-place files are not rewritten. Each changed file is replaced atomically.
Inputs, repository membership, selections, and destination safety are checked before writing or clearing output. In-place clean-input checks also happen before writing. Files are then processed sequentially: there is no bulk token validation or automatic rollback. A later decoding, reading, or writing failure can leave earlier files rendered. Restore affected in-place files from Git, or delete and regenerate output-directory contents.
| Exit | Meaning |
|---|---|
0 |
Successful render/dry-run, even with changes; or check needs no changes |
1 |
Check found files rendering would change |
2 |
Invalid arguments/configuration, missing Git/commit/repository, dirty in-place inputs, or an I/O/encoding error |
All logging and diagnostics go to stdout. Default output is a concise summary;
--verbose adds paths and unavailable-value warnings, and --quiet suppresses
routine logging. Dry-run diffs and errors remain visible with --quiet.
Development and demo
uv sync --locked --extra demo
uv run --locked --extra demo python -m unittest discover -s tests -v
uv.lock is versioned so local development, CI, and demo builds use the same
resolved dependencies. --locked fails if it needs updating. After changing
dependencies, run uv lock and commit both pyproject.toml and uv.lock.
The runtime package still has no dependencies; the default development group
adds the version-comparison helper used by release automation.
To build and preview the page locally:
uv run --locked --extra demo python demo/build.py
uv run --locked --extra demo gitvars render demo/index.html --html-escape --output-dir build --force
| Path | Purpose |
|---|---|
README.md |
Page content, maintained in Markdown. |
demo/layout.html |
Tracked HTML layout and styles. |
demo/index.html |
Generated HTML with Git placeholders intact; ignored by Git. |
build/demo/index.html |
Final rendered page with actual Git values; ignored by Git. Open this file in your browser. |
demo/build.py only combines the README and layout into demo/index.html,
leaving the Git placeholders intact. The separate gitvars render command
replaces them in a copy at build/demo/index.html, preserving the input path
under build/. --force clears an existing build/ directory before rendering.
Neither generated HTML file is committed. Run both commands again after editing
the README or layout. CI uses the same two steps on pushes and pull requests.
The table is inserted immediately after the introduction. Its three columns show
the literal variable, its replaced value, and its explanation. README examples
stay literal; only the table's value cells are substituted. Values are escaped
for HTML, including multiline commit messages. The file URL cell is empty
because the generated input is absent from HEAD; the commit URL still links
to the source revision. Both commands run once and exit. The optional demo
dependencies are needed only to convert the README to HTML; the gitvars CLI
itself still has no runtime dependencies.
GitHub Pages output
On a push to main, the Pages workflow first runs demo/build.py, then invokes
gitvars render to produce build/demo/index.html. It uploads build/demo/ as the
Pages artifact. That directory becomes the website root, so its index.html
is the homepage. With the default project-site URL, it appears at
https://andresvelasco.github.io/gitvars-render/,
without build/demo/ in the URL. The workflow can also be run manually.
Select GitHub Actions as the repository's Pages publishing source to enable deployment, following the GitHub Pages workflow documentation. Generated output is not committed, so the page can link to the exact source commit without a self-referential SHA update.
Publishing releases
The distribution on PyPI and TestPyPI is gitvars-render. The GitHub
repository is AndresVelasco/gitvars-render; the executable and Python import
remain gitvars. pyproject.toml is the single version source, and
gitvars --version reads the installed distribution's metadata.
The publishing workflow is .github/workflows/publish.yml:
| Event | Result after successful checks |
|---|---|
| Pull request | Tests and distribution builds; no publishing. |
Push to main with no release tags yet |
Publish a development build to TestPyPI. |
Push to main with an unchanged released version |
Publish a development build to TestPyPI. |
Push to main with a version above the latest release tag |
Create the matching vMAJOR.MINOR.PATCH tag and publish to PyPI. |
| Push of a matching version tag | Verify the tag, version, and tests, then publish to PyPI. |
With no existing release tags, the first successful main build publishes
0.1.0.dev... to TestPyPI. After verifying that build, request the initial
production release explicitly:
git tag v0.1.0
git push origin v0.1.0
This starts the production publishing workflow, including tests and a check that
the tag matches the version in pyproject.toml. For subsequent releases, update
the version instead:
uv version 0.1.1
Commit the updated pyproject.toml and uv.lock, preferably in a release PR,
and merge into main. Release versions use stable MAJOR.MINOR.PATCH numbers.
The workflow compares versions numerically, rejects version regressions and
tag/version mismatches, and never moves an existing tag. It tags the commit CI
actually tested, even if main has advanced. Tag creation and publishing are
jobs in the same workflow, so no additional token is needed to trigger another
workflow from an automated tag.
After v0.1.0, ordinary pushes publish versions such as
0.1.1.dev1230001: the next patch version plus a serial derived from the
workflow run ID and attempt. Only the temporary build checkout gets this
development version; it is not committed. Re-running a failed release on its
tagged commit retries publishing and skips distributions already uploaded.
Wheel and source distributions are installed and checked before publishing.
Configure pending or existing trusted publishers separately on PyPI and TestPyPI, and create the corresponding GitHub environments:
| Setting | PyPI | TestPyPI |
|---|---|---|
| Project name | gitvars-render |
gitvars-render |
| Repository owner | AndresVelasco |
AndresVelasco |
| Repository name | gitvars-render |
gitvars-render |
| Workflow filename | publish.yml |
publish.yml |
| Environment | pypi |
testpypi |
Publishing uses OIDC trusted publishing; no PyPI API token is stored in GitHub. The first upload creates a project configured with a pending publisher. See the PyPI trusted-publishing documentation.
CI is configured to run the standard-library test suite on Python 3.11–3.14 on Linux, plus Python 3.12 on Windows and macOS. The local implementation has been validated on Python 3.12 and Git 2.43; hosted CI results determine broader compatibility.
Metadata
Release files for gitvars-render 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| gitvars_render-0.1.0.tar.gz | 39.7 kB | With provenance | Details for gitvars_render-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| gitvars_render-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for gitvars_render-0.1.0-py3-none-any.whl |
Total release size: 60.0 kB
Release files / gitvars_render-0.1.0.tar.gz
| Download URL | gitvars_render-0.1.0.tar.gz |
|---|---|
| Size | 39.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7f40a71c430fd6af9dcc4fb8267eed38efff62cbf667af558837fb65967a5119
|
|
BLAKE2b-256 checksum How to use checksums |
7bd4c3ba1ff275506d37be81beff54bcc430d81d85aab1c262cca17ef880c298
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logRelease files / gitvars_render-0.1.0-py3-none-any.whl
| Download URL | gitvars_render-0.1.0-py3-none-any.whl |
|---|---|
| Size | 20.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
15d1f94af4f3d174d48fc845a0b24ea4dd7536e32e084a18e79c686a95941b0f
|
|
BLAKE2b-256 checksum How to use checksums |
f022f5d434f6d18cd3093f031930ae73fab4039715428401849d8342e3353603
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logSecurity for gitvars-render 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for gitvars_render-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.