Skip to content

Variable interpolation ({{env.X}})

alint resolves {{env.NAME}} references in your .alint.yml against the environment at config-load time. This lets one committed config adapt to its environment — a PR base SHA, a per-team registry host, an environment-driven path root — without templating the YAML file externally.

rules:
- id: pr-conventional-commits
kind: git_commit_message
pattern: '^(feat|fix|chore|docs|refactor|test)(\(.+\))?!?: '
subject_max_length: 72
since: "{{env.ALINT_BASE_SHA | default('origin/main')}}"
level: error
  • {{env.NAME}} — substitute the value of environment variable NAME. If NAME is unset (or empty) and there is no default, config load fails with an error naming the field.
  • {{env.NAME | default('fallback')}} — substitute NAME, or the quoted fallback when NAME is unset/empty. Single or double quotes are accepted.
  • Interpolation composes with surrounding text: "https://policy.{{env.TEAM}}.example.com/v1.yml".

The | default(...) filter follows the Jinja / Liquid / Nunjucks convention. It is the only filter in this release.

Every string-typed value field is interpolated at load:

FieldInterpolatedWhy
extends: entry (URL #sha256-…)yesper-environment / per-team registry
paths: globyesper-environment path roots
pattern: regexyesenvironment-driven matcher
since:, changed_since:yesthe motivating case
policy_url:yesper-team policy URLs
message:yesadds {{env.X}} alongside {{vars.X}} / {{ctx.X}}
content:, content_from:yestemplated fix bodies
vars: valuesyeslets vars themselves be env-driven
id:, kind:, level:noidentity / type / severity must be stable across environments — env-driven values break audit trails and reproducibility

Interpolation only happens for local config files — the ones you author. Bundled rulesets are static, and a remote extends: ruleset is never interpolated against your environment.

A value that is fully resolved by interpolation is re-typed, so an env-driven number or bool lands in a typed field correctly:

subject_max_length: "{{env.MAX_SUBJECT | default('72')}}" # validates as the integer 72

A consequence: a string-typed field whose env value is bare-numeric ("72") or bare-bool ("true") re-types too; if the field truly wants those characters as a string, the subsequent validation surfaces a clear type error.

The when: expression language gains env as a third namespace alongside facts and vars:

- id: ci-only-rule
kind: file_exists
paths: [coverage.xml]
when: env.CI == "true" or env.GITHUB_ACTIONS == "true"
level: warning

Unlike string interpolation (load-time), when: env.X resolves at evaluation time — but since the environment is constant during a run, the result is the same. An unset variable evaluates to null (falsy), matching the “missing fact is falsy” rule. env is value-only — there are no callable methods on it.

env values are always strings. Compare against string literals: when: env.PORT == "8080", not == 8080. A bare-integer comparison is a type mismatch that evaluates silently to false (the rule then never applies), so quote the right-hand side.

{{...}} is shared — foreign templates pass through

Section titled “{{...}} is shared — foreign templates pass through”

alint does not own the {{...}} namespace. Go templates ({{json .}}, {{end}}), cookiecutter ({{cookiecutter.slug}}), Jinja, and others appear legitimately in command: args and pattern: regexes. alint only acts on spans it is confident are its own — env., vars., ctx. (and close typos of env/vars, which it flags). Everything else passes through verbatim:

rules:
- id: lint-workflows
kind: command
command: ["actionlint", "-format", "{{json .}}"] # Go template — left untouched

v0.9.21 shipped POSIX-style ${VAR} / ${VAR:-default} interpolation on git_commit_message.since: only. That syntax still works in v0.11 but emits a deprecation warning at config load, with the canonical rewrite shown inline:

alint: warning: rule "pr-conventional-commits": `since: ${ALINT_BASE_SHA}` uses the
deprecated v0.9.21 `${VAR}` interpolation syntax. The canonical v0.11+ form is
`{{env.ALINT_BASE_SHA}}`; the `${VAR}` form will be removed in v1.0.

Rewrite ${VAR}{{env.VAR}} and ${VAR:-default}{{env.VAR | default('default')}}. The ${VAR} path is removed in v1.0.

{{env.X}} reads via the standard environment lookup — no shell evaluation, no system(). The extends: case is the one to be aware of: an env var referenced in an extends: entry could change where a config load fetches from. Interpolation covers the whole entry string (including any #sha256-… hash), so it does not by itself pin the host.

The real trust boundary is your local .alint.yml: it is trusted source. Influencing an extends: URL through interpolation requires the ability to (a) write {{env.X}} into your config and (b) control the environment — and (a) alone is already a full compromise (an attacker who can edit your config can just write the malicious URL directly). For configs whose extends: URLs are not env-driven, the author-fixed #sha256-… hash continues to pin the content exactly as before.