Documentation
¶
Overview ¶
Package freshness computes the checksum.changed/timestamp.changed/preconditions.success/ sources/artifacts facts consumed via the existing `when:` condition engine for a step's `inputs:`/`artifacts:`/`preconditions:`, and persists sources-hash state across runs for checksum comparison. See pkg/condition for how these facts are exposed to `when:`.
Index ¶
- Variables
- func EffectiveWhen(when schema.Condition, declared StepDeclarations) schema.Condition
- func MentionsAnyFreshnessFact(when schema.Condition) bool
- func StateDir(basePath string) string
- type Checker
- type Facts
- type Globber
- type LookupTool
- type Option
- type Record
- type StateStore
- type StepDeclarations
- type StepIdentity
Constants ¶
This section is empty.
Variables ¶
var ErrGlobInvalid = errors.New("invalid sources/artifacts glob pattern")
ErrGlobInvalid is returned when an inputs.sources/artifacts.paths glob pattern is malformed (distinct from "matched nothing," which is not an error).
Functions ¶
func EffectiveWhen ¶
func EffectiveWhen(when schema.Condition, declared StepDeclarations) schema.Condition
EffectiveWhen returns when, or an implicit condition synthesized from which of declared.Inputs/Artifacts/Preconditions are present, if when is unset (zero) -- so declaring `inputs:`/`artifacts:`/`preconditions:` alone (no explicit `when:`) is enough to skip a step whose work is already done, matching go-task's zero-boilerplate default.
func MentionsAnyFreshnessFact ¶
MentionsAnyFreshnessFact reports whether when references any freshness-derived CEL identifier. Callers that need a cheap "might this step run" answer without a freshness.Checker on hand yet (facts would all read as their Go zero value, i.e. always "unchanged" -- wrong even for a step's very first-ever run) should treat a true result as "assume runnable" and defer the real decision to wherever Compute's actual facts get evaluated, rather than evaluating when here against an empty Context.
func StateDir ¶
StateDir returns the project-relative directory where freshness state (recorded sources hashes for checksum.changed) persists, rooted under the project's own base path rather than an XDG user-cache directory specifically so it composes with the existing CI-cache feature: a user can add this same directory to ci.cache.includes: and freshness state survives across CI runs the same way `.terraform/` would.
Types ¶
type Checker ¶
type Checker struct {
// contains filtered or unexported fields
}
Checker computes freshness Facts for a step's schema.Inputs/Artifacts/Preconditions and persists checksum state across runs. Constructed via NewChecker with Options (>2-3 logical dependencies), defaulting to real production implementations.
func NewChecker ¶
NewChecker constructs a Checker with real production defaults, overridable via Option.
func (*Checker) Compute ¶
func (c *Checker) Compute(effectiveWhen schema.Condition, declared StepDeclarations, id StepIdentity) (Facts, error)
Compute lazily computes only the facts effectiveWhen actually references (see schema.Condition.MentionsCELIdentifier), so a step whose `when:` only mentions `timestamp` never pays the cost of hashing file content, and a step that never references the bare `sources`/`artifacts` identifiers never pays the cost of building per-file records. Id.BaseDir is the step's own working directory; id.StateDir is where checksum state persists; id.Scope+id.StepName form the stable per-step identity used to key that state (see stateKey).
func (*Checker) RecordSuccess ¶
func (c *Checker) RecordSuccess(inputs *schema.Inputs, baseDir, stateDir, scope, stepName string) error
RecordSuccess persists the current sources checksum for the next run's comparison. Call ONLY after the step's own Execute() returns success -- a failed step must never mark itself falsely up to date. Preconditions has no persisted state (exec.LookPath is always evaluated live), so it has nothing to record here -- callers gate this call on inputs/artifacts being declared at all, not on inputs.Sources being non-empty: an artifacts-only step (inputs == nil) still needs a record of the (empty) sources hash, or checksumChanged's `!found` branch reports "changed" forever and the step never stabilizes to "skip" even once its artifacts exist and are unchanged.
type Facts ¶
type Facts struct {
ChecksumChanged bool
TimestampChanged bool
PreconditionsSuccess bool
// Sources/Artifacts are structured per-file records, populated only when a step's `when:`
// actually references the bare `sources`/`artifacts` identifiers (see Compute) -- building
// per-file mtime/checksum data is wasted work for the common case where only the
// checksum.changed/timestamp.changed convenience facts are used.
Sources []condition.FileFact
Artifacts []condition.FileFact
}
Facts carries the freshness facts computed for one step evaluation, merged into the caller's schema.ConditionContext before evaluating `when:` (see pkg/condition's ChecksumChanged/ TimestampChanged/PreconditionsSuccess/Sources/Artifacts fields).
type Globber ¶
Globber resolves a glob pattern (relative to baseDir) to matching absolute file paths. Abstracted for testability (see mockGlobber in tests) -- the real implementation wraps pkg/filesystem.GetGlobMatches, the same doublestar-based, LRU-cached glob engine already used elsewhere in Atmos, rather than pkg/filematch (used by `atmos validate schema`'s `matches:`), because filematch.MatchFiles resolves against the process's current working directory with no baseDir parameter, and freshness must resolve relative to the step's own working directory, which can differ from process CWD.
type LookupTool ¶
LookupTool resolves one preconditions.tools entry, mirroring exec.LookPath's signature. Abstracted for testability; the default is exec.LookPath directly -- no shell involved, so there's no which-vs-where cross-platform mismatch to work around.
type Option ¶
type Option func(*Checker)
Option configures a Checker.
func WithGlobber ¶
WithGlobber overrides the Globber (default: NewGlobber(), wrapping pkg/filesystem).
func WithHasher ¶
WithHasher overrides the content hasher (default: pkg/hashfile.HashFiles).
func WithLookupTool ¶
func WithLookupTool(l LookupTool) Option
WithLookupTool overrides the LookupTool (default: exec.LookPath).
func WithStateStore ¶
func WithStateStore(s StateStore) Option
WithStateStore overrides the StateStore (default: NewStateStore(), one JSON file per key).
type Record ¶
type Record struct {
SourcesHash string `json:"sources_hash"`
}
Record is the persisted state for one step's checksum-based freshness check.
type StateStore ¶
type StateStore interface {
Load(stateDir, key string) (Record, bool, error)
Save(stateDir, key string, r Record) error
}
StateStore persists/retrieves the last-recorded Record for a step, keyed by a caller-computed stable identity (see Checker.stateKey). Abstracted for testability -- the real implementation is one JSON file per key under stateDir, guarded by pkg/cache.FileLock on platforms where it provides real mutual exclusion. On Windows, pkg/cache.FileLock is explicitly best-effort (no native locking there); Save always writes to a uniquely-named temp file before the final rename, so concurrent writers never collide on the temp file itself regardless of platform, but a concurrent Save and Load can still transiently fail against each other on Windows if they land on the exact same instant -- callers already treat a Save/RecordSuccess failure as log-and-continue, not fatal, which is the correct posture for a best-effort cache.
func NewStateStore ¶
func NewStateStore() StateStore
NewStateStore returns the real, production StateStore.
type StepDeclarations ¶
type StepDeclarations struct {
Inputs *schema.Inputs
Artifacts *schema.Artifacts
Preconditions *schema.Preconditions
}
StepDeclarations groups a step's three freshness-related declarations (Inputs/Artifacts/ Preconditions are deliberate siblings, not one nested inside another -- see their own doc comments in pkg/schema/task.go for why). Grouped into one struct so EffectiveWhen/Compute stay within the project's per-function argument limit, and so callers build the group once and pass it to both.