Documentation
¶
Overview ¶
Package hooks is dstow's hook engine (DESIGN.md §5 + A11): discovery of the eight per-event executables in one hooks directory, the DSTOW_HOOK_* environment contract each hook runs under, direct exec of a hook, and the per-invocation sequencer that fires the nested/LIFO lifecycle (REQUIREMENTS §9.1) once per scope.
The package returns data, never output (A4): diagnostics come back as Warning values and failures as typed errors; the caller (ui) decides how to render them, and only ui touches process streams. The package never references os.Stdout/os.Stderr — the Runner's streams are injected — and it reads os.Getenv/os.Environ only at the point of use (A2): once when building a hook's inherited environment, and once in InHook.
hooks owns ordering and once-per-invocation firing (A11); ops owns the iteration loop. The no-op contract (§9.1.5) is the CALLER's: ops invokes the Invocation methods only for scopes that actually change something, and the once-per-invocation firing then makes repeated bulk runs quiet and idempotent. hooks does not second-guess the caller — a hook file that simply does not exist is a silent no-op (hooks are optional).
Index ¶
Constants ¶
const ( EnvLevel = "DSTOW_HOOK_LEVEL" EnvAction = "DSTOW_HOOK_ACTION" EnvPhase = "DSTOW_HOOK_PHASE" EnvFQN = "DSTOW_HOOK_FQN" EnvScheme = "DSTOW_HOOK_SCHEME" EnvCoordinate = "DSTOW_HOOK_COORDINATE" EnvPackage = "DSTOW_HOOK_PACKAGE" EnvPackageDir = "DSTOW_HOOK_PACKAGE_DIR" EnvTarget = "DSTOW_HOOK_TARGET" EnvRepoFQN = "DSTOW_HOOK_REPO_FQN" EnvRepoDir = "DSTOW_HOOK_REPO_DIR" EnvPackages = "DSTOW_HOOK_PACKAGES" )
The twelve DSTOW_HOOK_* variable names (H2). Each is a named constant so no other file spells a variable name as a literal.
Variables ¶
This section is empty.
Functions ¶
func Discover ¶
Discover scans ONE hooks directory, non-recursively (M6). It returns the Set of present executable hooks plus any M6/M7 warnings the directory's contents draw. An absent directory is an empty Set with no warnings and no error (hooks are optional); any other ReadDir failure is returned as the error.
Executability is tested with os.Stat, not Lstat (mode&0111 != 0): a symlink to an executable is a hook.
func InHook ¶
func InHook() bool
InHook reports whether this process is running inside a dstow hook — the entire H7 surface in this package. Detection is DSTOW_HOOK_ACTION present in the environment (H7), read at the point of use (A2). The actual write refusals live in the write commands (later tickets); this predicate only answers the question.
func ScopeHooksDir ¶
ScopeHooksDir returns the hooks directory of a repo or package root: <scopeRoot>/.dstow/hooks (M6). Discovery goes through config's metadata accessor (A11), so hooks and config can never disagree about where metadata lives. The global level's hooks directory is not this — it is GlobalScope.Dir/hooks (the caller supplies the global config dir for testability; see GlobalScope).
Types ¶
type Action ¶
type Action int
Action is one of the four lifecycle actions (§5 H2). Restow is its own pair, not a stow+unstow composition: M6 pins "Restow fires only the restow pair", so the caller passes ActionRestow and the restow hooks fire, nothing else.
type GlobalScope ¶
type GlobalScope struct {
Dir string // global config dir; hooks dir = Dir/hooks; also the hook cwd (H5)
Packages []name.FQN // all packages acting this invocation
}
GlobalScope carries a global-level firing's context (H2). Dir is the global config dir ($XDG_CONFIG_HOME/dstow in production, supplied by the caller for testability); the hooks directory is Dir/hooks and the hook cwd is Dir (H5). Packages are all packages acting this invocation, for DSTOW_HOOK_PACKAGES.
type Hook ¶
Hook identifies one of the eight lifecycle hooks: a (Phase, Action) pair (M6). It is the key of a Set.
type HookError ¶
type HookError struct {
Level Level
Action Action
Phase Phase
Path string // the hook file that failed, or the hooks dir when discovery failed
Err error
}
HookError reports that one firing failed — a non-zero exit, an exec error, or the scope's hooks directory being unreadable at discovery. hooks only classifies: it names the Level and Phase off which the caller (ops, ticket #44) applies REQUIREMENTS §9.1.4 blocking (a failed package-pre blocks that package; a failed repo/global-pre blocks everything under it; a failed post marks its scope failed but completed work stays).
type Invocation ¶
type Invocation struct {
// contains filtered or unexported fields
}
Invocation drives the nested/LIFO hook lifecycle for one dstow invocation (A11 + REQUIREMENTS §9.1.3): global-pre → repo-pre → package-pre → action → package-post → repo-post → global-post. It owns ordering and once-per-invocation firing; ops owns the iteration loop and calls the methods below in nesting order for each acting scope.
Each hooks directory is discovered lazily, at most once per invocation, and its warnings are surfaced exactly once — from whichever method first reaches it. Each pre hook fires at most once per scope: once fired is fired, whether it succeeded or failed, so a later BeforePackage never retries a pre hook that already ran (a failed global/repo-pre means ops blocks everything under it per §9.1.4, and hooks must not undo that by re-firing).
func NewInvocation ¶
func NewInvocation(action Action, r Runner, global GlobalScope) *Invocation
NewInvocation begins a hook invocation for one action, over the injected Runner and the global scope (its Dir supplies the global hooks dir and cwd).
func (*Invocation) AfterPackage ¶
func (inv *Invocation) AfterPackage(pkg PackageScope) ([]Warning, error)
AfterPackage fires this package's package-post hook.
func (*Invocation) AfterRepo ¶
func (inv *Invocation) AfterRepo(repo RepoScope) ([]Warning, error)
AfterRepo fires a repo's repo-post hook, at most once per repo (keyed by FQN). A second AfterRepo for the same repo is a no-op with no error.
func (*Invocation) BeforePackage ¶
func (inv *Invocation) BeforePackage(pkg PackageScope) ([]Warning, error)
BeforePackage fires the pre hooks nesting down to one package, each at most once per invocation and in order: global-pre (first BeforePackage only), repo-pre (first BeforePackage for this repo, keyed by FQN.Repo()), then this package's package-pre. It stops at the first failure and returns it — the firing that failed is still marked fired, so a later BeforePackage will not retry it. Returned warnings are those newly incurred by directories this call discovered for the first time.
func (*Invocation) Finish ¶
func (inv *Invocation) Finish() ([]Warning, error)
Finish fires the global-post hook, at most once, and only if the global scope ever activated — i.e. at least one BeforePackage happened. With no package having acted, global-pre never fired and global-post must not either (§9.1.5: nothing changed, so the global scope stays quiet).
type PackageScope ¶
type PackageScope struct {
FQN name.FQN // the package FQN
Dir string // absolute package dir; also the hook cwd (H5)
Target string // effective target root, absolute
RepoDir string // absolute repo dir
}
PackageScope carries a package-level firing's context (H2). Every field is caller-supplied; hooks derives the env values from it. Dir is the package directory and also the hook cwd (H5). The repo's FQN is not a field: a package FQN's repo is FQN.Repo() by construction, so carrying it separately could only ever disagree.
type RepoScope ¶
type RepoScope struct {
FQN name.FQN
Dir string // absolute repo dir; also the hook cwd (H5)
Packages []name.FQN // all packages acting under this repo
}
RepoScope carries a repo-level firing's context (H2). Dir is the repo directory and also the hook cwd (H5); Packages are all packages acting under this repo this invocation, for DSTOW_HOOK_PACKAGES.
type Runner ¶
type Runner struct {
Stdin io.Reader // passed through to the hook (H6)
Stderr io.Writer // both hook streams land here (H6)
}
Runner carries the injected streams a hook runs with (A4: only ui touches the process streams; hooks receives them). Both hook output streams land on Stderr (H6) — nothing a hook prints is dstow's answer to a question, so hook output is commentary definitionally — and Stdin passes through to the hook.
type Set ¶
Set maps each of the eight (Phase, Action) pairs that is present and executable to the absolute path of its hook file (M6). A pair with no present, executable file is simply absent from the map — its firing is a silent no-op.