Cross-Cutting Concepts¶
A handful of ideas show up in almost every section of .stagefreight.yml — template
variables, credential resolution, retention policies, and the pattern/condition syntax. They
are documented once here and referenced from the feature pages, so the behavior is
identical everywhere it appears.
Template variables¶
Any text field that renders content — badge values, tag/alias templates, text items, link
URLs — expands these tokens. Expansion happens at run time against the resolved version and
git state.
| Template | Description |
|---|---|
{version} |
Full semantic version (e.g. 1.2.3) |
{major}, {minor}, {patch} |
Semver components |
{base} |
Base version without pre-release/build metadata |
{sha}, {sha:N} |
Commit SHA (default 7 chars, or N) |
{branch} |
Current branch name |
{var:name} |
User-defined variable from the top-level vars: map |
{env:VAR} |
Environment variable value |
{date}, {datetime}, {timestamp} |
UTC date formats |
{date:FORMAT} |
Custom Go time layout |
{commit.date} |
HEAD commit date |
{project.name} |
Repo name from the git remote |
{project.url} |
Repo URL (SSH→HTTPS normalized) |
{project.license} |
SPDX identifier from the LICENSE file |
{project.module} |
Canonical module/package name from the build manifest (go.mod module path, Cargo.toml [package]/pyproject.toml [project] name, or package.json name) — the value a shared build preset embeds so one preset serves a whole bucket |
{registry.<id>.pulls}, {registry.<id>.stars} |
Repo stats for the registry with config id <id> — pull/star counts, available only where the provider exposes them (Docker Hub) |
{registry.<id>.tag.<tag>.size}, .updated, .digest |
Per-tag metadata for a tag in registry <id> (size/size:raw/updated/digest); read from the Docker Hub API, or the OCI manifest for any other provider (ghcr, harbor, quay, …) |
{stagefreight.version}, {stagefreight.commit} |
StageFreight's OWN tool version / commit (ldflags-injected) — the engine managing the repo, not the repo's {version} |
{inventory.<cluster>.count} |
Number of applications the k8s inventory discovers on the gitops cluster with name <cluster> (a gitops: map key); matches the k8s-inventory region's "Apps & Services (N)" heading. Resolves only where the cluster is reachable (else n/a) |
Registry tokens address a registry by the id you gave it under registries: — there is no
provider-literal shorthand, so which registry a token reads is always explicit (never inferred).
Likewise {inventory.<cluster>.count} addresses a cluster by its gitops: name.
Credential resolution¶
Credential fields (e.g. a registry or release target's credentials:) never hold a secret.
They name an environment-variable prefix; StageFreight resolves the real value from your
CI/CD variables at run time, so nothing sensitive lives in .stagefreight.yml.
For a prefix (e.g. HARBOR), the username is always {PREFIX}_USER, and the secret is the
first non-empty of these suffixes:
| Suffix | Example | Notes |
|---|---|---|
_TOKEN |
HARBOR_TOKEN |
Preferred. Scoped, revocable API token. |
_PASS |
HARBOR_PASS |
Accepted; emits a warning recommending _TOKEN. |
_PASSWORD |
HARBOR_PASSWORD |
Accepted; emits a warning recommending _TOKEN. |
At the protocol level all three are identical — they become the --password-stdin value for
docker login. The distinction is on the issuing side: a password authenticates the
account directly, while a token is issued separately, can be scoped (push-only, no delete),
revoked individually, and attributed in audit logs.
Recommendation
Create a robot account or scoped API token with the minimum permissions needed (usually:
push to one project), store it as {PREFIX}_TOKEN, and keep the account password out of
CI entirely. The _PASS/_PASSWORD warning is based purely on the matched suffix —
StageFreight cannot tell a password from a token by value, so rename the variable to
_TOKEN to silence it.
publish:
harbor-dev:
kind: registry
build: myapp
url: cr.example.com
provider: harbor
path: myorg/myapp
tags: ["dev-{sha:8}", "latest-dev"]
when: { branches: [main], events: [push] }
credentials: HARBOR # → HARBOR_USER + HARBOR_TOKEN
Retention policies¶
Registry and release targets accept a retention: policy that prunes old tags/releases.
Policies are additive, restic-style: a tag survives if any rule wants to keep it.
# Shorthand — keep the last 10 (per series; see below)
retention: 10
# Full policy
retention:
keep_last: 3
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
keep_yearly: 2
keep_branches: 10 # keep the 10 most-recently-active branches' series
protect: ["latest"] # never deleted, regardless of the policies
Which remote tags are candidates is derived from the target's tags: templates —
StageFreight only ever prunes tags it recognizes as its own.
Retention is per-series, not per-target¶
Retention evaluates each series independently, not across all of a target's tags at
once. A tag's series is (template, identity): its tags: template plus the values of
that template's identity variables ({branch}, {env}). keep_last and the time
buckets apply within each series:
- Two accumulating templates never share slots.
tags: ["dev-{sha:8}", "nightly-{sha:8}"]withkeep_last: 6keeps 6 dev and 6 nightly — one never evicts the other. - Per-branch series prune independently.
tags: ["test-{branch}-{sha:8}"]keepskeep_lastbuilds for each branch — a busy branch never prunes a quiet one's history. - Rolling tags are auto-exempt. A template with no sequence variable (
latest-dev,latest-test-{branch}) is a single value overwritten in place — nothing accumulates, so it is never pruned. You do not needprotect: ["latest-dev"].
keep_branches — bound the number of series¶
keep_branches: N keeps only the N most-recently-active identity groups per template
(e.g. the 10 most-recent branches) and prunes the rest wholesale. This is what stops
retired branches' tags from accumulating forever. Templates with no identity variable are
unaffected (there is only one series).
∞ and wipe semantics¶
A keep value of 0, -1, or unset all mean ∞ (keep everything) for that rule — the
Go zero-value is safe, and an all-∞ policy is a graceful no-op. There is no value that
wipes a repo; retention never mass-deletes. (Removing all tags of a series is a deliberate,
out-of-band action, never a policy number.)
identity: (advanced)¶
By default {branch} and {env} partition tags into independent series. If you tag by
another partition dimension — e.g. img-{region}-{sha:8} across us/eu/asia — list it
so each region prunes independently instead of competing for the same slots:
Most projects never need this — {branch}/{env} cover the common cases.
Patterns & conditions¶
The same matching syntax drives when.branches, when.git_tags, and every tag:/branch:
conditional rule (security detail rules, and so on).
"^main$" # regex match (the default)
"!^feature/.*" # negated regex (! prefix)
"main" # literal match
"!develop" # negated literal
- An empty list/field is no filter — it always matches.
- Multiple patterns are evaluated in order; first match wins.
- Where a rule has multiple condition fields (e.g.
tag:andbranch:), all set fields must match (AND). A rule with no fields set is a catch-all.
Named routing patterns (referenced by when.git_tags: [stable] / when.branches: [main]) are
defined once under git.tags / git.branches and reused across publish targets — see
Policy.