Documentation
¶
Overview ¶
Package trigger is the unifying spine for event-driven runs. It defines one canonical Event envelope that every trigger source (forge webhook, schedule tick, native-board transition, run completion, custom ingest) maps onto, plus a Subscription registry that binds (event filter) → (bot launch into a target repo/workspace). The Evaluator consumes events from pkg/eventbus, matches them against subscriptions, and hands a LaunchPlan to a Launcher.
Two layers compose here, deliberately:
- Capability (pkg/bundle Invocation, unchanged) — "what surfaces can fire me", authored on the bot manifest. No repo/tenant/cron knowledge.
- Binding (trigger.Subscription, new) — "on tenant T / repo R, when an event matches M, launch bot B". Generated FROM invocations at provision time. Repo/tenant/cron live here, never in the manifest.
Index ¶
- Constants
- Variables
- func IsCardEvent(t native.EventType) bool
- func RunOutcomeEventID(runID, status, interactionID string, updatedAt time.Time) string
- type BoardEffect
- type BoardSource
- type BoardSourceOption
- type Evaluator
- type EvaluatorOption
- type Event
- type LabelConsumer
- type LaunchPlan
- type Launcher
- type Matcher
- type MemorySubscriptionStore
- func (m *MemorySubscriptionStore) Create(_ context.Context, s Subscription) error
- func (m *MemorySubscriptionStore) Delete(_ context.Context, id string) error
- func (m *MemorySubscriptionStore) Get(_ context.Context, id string) (Subscription, error)
- func (m *MemorySubscriptionStore) ListByBot(_ context.Context, tenantID, botID string) ([]Subscription, error)
- func (m *MemorySubscriptionStore) ListByOrigin(_ context.Context, tenantID, origin string) ([]Subscription, error)
- func (m *MemorySubscriptionStore) ListByRepo(_ context.Context, tenantID, repo string) ([]Subscription, error)
- func (m *MemorySubscriptionStore) ListByTenant(_ context.Context, tenantID string) ([]Subscription, error)
- func (m *MemorySubscriptionStore) ListCandidates(_ context.Context, ev Event) ([]Subscription, error)
- func (m *MemorySubscriptionStore) Update(_ context.Context, s Subscription) error
- type MongoSubscriptionStore
- func (s *MongoSubscriptionStore) Create(ctx context.Context, sub Subscription) error
- func (s *MongoSubscriptionStore) Delete(ctx context.Context, id string) error
- func (s *MongoSubscriptionStore) DistinctBoardTenants(ctx context.Context) ([]string, error)
- func (s *MongoSubscriptionStore) EnsureSchema(ctx context.Context) error
- func (s *MongoSubscriptionStore) Get(ctx context.Context, id string) (Subscription, error)
- func (s *MongoSubscriptionStore) ListByBot(ctx context.Context, tenantID, botID string) ([]Subscription, error)
- func (s *MongoSubscriptionStore) ListByOrigin(ctx context.Context, tenantID, origin string) ([]Subscription, error)
- func (s *MongoSubscriptionStore) ListByRepo(ctx context.Context, tenantID, repo string) ([]Subscription, error)
- func (s *MongoSubscriptionStore) ListByTenant(ctx context.Context, tenantID string) ([]Subscription, error)
- func (s *MongoSubscriptionStore) ListCandidates(ctx context.Context, ev Event) ([]Subscription, error)
- func (s *MongoSubscriptionStore) Update(ctx context.Context, sub Subscription) error
- type NativeBoardEffect
- type Nudger
- type Publisher
- type ScheduleGate
- type Scheduler
- type SchedulerOption
- type SchedulerStatus
- type Source
- type Subject
- type Subscription
- func FromBoardInvocation(id, tenantID, repo, botID, origin string, inv bundle.Invocation, now time.Time) (Subscription, bool)
- func FromKeepaliveInvocation(id, tenantID, repo, botID, origin string, inv bundle.Invocation, now time.Time) (Subscription, bool)
- func FromScheduleInvocation(id, tenantID, repo, botID, origin string, inv bundle.Invocation, now time.Time) (Subscription, bool)
- type SubscriptionStore
Constants ¶
const ( KindCardCreated = "card.created" KindCardMoved = "card.moved" KindCardLabeled = "card.labeled" KindCardUpdated = "card.updated" )
Board event kinds (the Kind field when Source == SourceBoard). They are a normalized projection of native.EventType — a card's lifecycle as the trigger spine sees it, independent of the board's internal audit vocabulary.
const ( KindRunFinished = "run.finished" KindRunFailed = "run.failed" KindRunCancelled = "run.cancelled" // KindRunPaused fires when a run suspends waiting for a human answer // (paused_waiting_human) or an operator soft-pause (paused_operator). It // is NOT a terminal kind: the run holds a valid checkpoint plus a pending // interaction and re-enters the graph on resume. A board projection keys // off this to mark a card "awaiting input"; Payload carries node_id + // interaction_id so a consumer can pinpoint the paused node. KindRunPaused = "run.paused" )
Run lifecycle kinds (the Kind field when Source == SourceRun) — the "runned by iterion" chaining source.
const ( // PayloadVars carries per-event dynamic launch vars (a map the evaluator // merges under the subscription's static Vars). PayloadVars = "vars" // PayloadLaunchedRunID, when present, marks an event that an authoritative // path already turned into a run (today: the inline forge webhook). The // evaluator treats such events as observational and never re-launches them, // so publishing them onto the bus cannot double-launch. PayloadLaunchedRunID = "launched_run_id" )
Reserved Payload keys.
const SubscriptionsCollectionName = "trigger_subscriptions"
SubscriptionsCollectionName is the Mongo collection backing the cloud-mode SubscriptionStore.
Variables ¶
var ErrSubscriptionNotFound = fmt.Errorf("trigger: subscription not found")
ErrSubscriptionNotFound is returned by Get/Update/Delete for an unknown id.
Functions ¶
func IsCardEvent ¶ added in v1.8.0
IsCardEvent reports whether a native board event is one of the card transitions the trigger spine reacts to — the shared filter between the local tail (enqueue) and the cloud poll-tail.
func RunOutcomeEventID ¶ added in v1.1.0
BuildRunOutcome derives the run.<outcome> Event for a run leaving the engine — the "runned by iterion" source shared by the in-process runview emitter and the cloud runner. The kind spans terminal outcomes (run.finished/failed/cancelled) AND the non-terminal run.paused.
The kind is first classified from bodyErr, then overridden by the persisted run status when it is unambiguous (the engine is the source of truth; the bodyErr arm keeps the event load-failure-resilient). The event carries the run's TenantID and the launching owner (payload "owner_id") so tenant-scoped consumers (notifications, board projections) can target it without a second store read.
The event ID is distinct per outcome episode: a run that pauses, resumes and pauses again must not dedup against its earlier pause, so the pending interaction id (or the status) is folded into the key. RunOutcomeEventID derives the per-episode event ID: the pending interaction when the run is paused on one, else the status — plus the run's updated_at second, which moves on every pause/terminal transition. The timestamp is what makes REPEAT episodes distinct: a failed_resumable run resumed and failing again, or a review-gate node re-pausing on the same interaction id, must notify again rather than dedup against the earlier episode. Exported so the usernotify sweep can derive the episode key from a run listing without loading each run; truncation to the second keeps the key stable across the stores' differing timestamp precisions.
Types ¶
type BoardEffect ¶
type BoardEffect interface {
Promote(ctx context.Context, plan LaunchPlan) (issueID string, err error)
}
BoardEffect realises an ExecutionBoard plan by promoting a native board card (stamping Bot/BotArgs) so the dispatcher's existing Claim picks it up. It never launches directly — the dispatcher stays the sole launch authority, which is what makes the event fast-path and the poll safety-net structurally unable to double-launch.
type BoardSource ¶
type BoardSource struct {
// contains filtered or unexported fields
}
BoardSource turns native-board transitions into trigger.Events on the bus. It tails the shared events.jsonl via native.Store.Subscribe (the only writer-agnostic seam — every Store instance appends to the same log), so it observes transitions made by the server, the dispatcher, or a per-run executor alike. Lifecycle mirrors watch_coordinator: a buffered channel fed by the tailer goroutine, drained by a single worker that does the store read + publish.
func StartBoardSource ¶
func StartBoardSource(store *native.Store, bus Publisher, logger *iterlog.Logger, opts ...BoardSourceOption) *BoardSource
StartBoardSource subscribes to the native store's event tail and begins publishing board events to the bus. It returns nil (a no-op) when a prerequisite is missing or the tail can't start — board triggering is an enhancement layered on the dispatcher poll, never a hard dependency, so a host without fsnotify simply falls back to the poll.
func (*BoardSource) Stop ¶
func (b *BoardSource) Stop()
Stop tears down the tail and worker (idempotent-safe via the once-guarded cancel returned by Subscribe).
type BoardSourceOption ¶
type BoardSourceOption func(*BoardSource)
BoardSourceOption configures a BoardSource.
func WithBoardName ¶
func WithBoardName(name string) BoardSourceOption
WithBoardName records the board's name (informational, carried in payload).
func WithBoardRepo ¶
func WithBoardRepo(repo string) BoardSourceOption
WithBoardRepo stamps a repo slug onto emitted events so repo-scoped subscriptions match.
func WithBoardTenant ¶
func WithBoardTenant(id string) BoardSourceOption
WithBoardTenant stamps a tenant id onto emitted events (cloud mode).
type Evaluator ¶
type Evaluator struct {
// contains filtered or unexported fields
}
Evaluator is the consumer side of the spine: it receives an Event (from the bus), finds the subscriptions that match, and dispatches each to the right effect — Launcher for direct mode, BoardEffect for board mode. Its Handle method has the eventbus.Handler signature so the wiring layer can register it with bus.Subscribe without the trigger package importing eventbus.
func NewEvaluator ¶
func NewEvaluator(subs SubscriptionStore, opts ...EvaluatorOption) *Evaluator
NewEvaluator builds an Evaluator over a subscription store.
func (*Evaluator) Handle ¶
Handle evaluates one event against the subscription store and fires every matching subscription. It never returns the effect errors fatally — a single subscription failure is logged and the rest still fire (one bad trigger must not silence the others). The signature matches eventbus.Handler.
type EvaluatorOption ¶
type EvaluatorOption func(*Evaluator)
EvaluatorOption configures an Evaluator.
func WithBoardEffect ¶
func WithBoardEffect(b BoardEffect) EvaluatorOption
WithBoardEffect sets the board-mode promote effect.
func WithLauncher ¶
func WithLauncher(l Launcher) EvaluatorOption
WithLauncher sets the direct-mode launcher.
func WithLogger ¶
func WithLogger(l *iterlog.Logger) EvaluatorOption
WithLogger sets the leveled logger (nil-safe).
type Event ¶
type Event struct {
// ID is a stable per-event identifier used for idempotency/dedup
// (e.g. "board:<board>:<issue>:<seq>"). A source that has no natural
// key generates a ULID/uuid.
ID string `json:"id" bson:"_id"`
Source Source `json:"source" bson:"source"`
// Kind is the source-specific event type (forge event name, board
// card.* kind, "run_finished", "cron", ...).
Kind string `json:"kind" bson:"kind"`
// Action narrows Kind where the source has one (forge "opened"/"reopened").
Action string `json:"action,omitempty" bson:"action,omitempty"`
// TenantID scopes the event to an org in cloud mode; "" in local single-host.
TenantID string `json:"tenant_id,omitempty" bson:"tenant_id,omitempty"`
// Repo is the forge slug ("group/project") or the local repo root the
// event pertains to; "" for tenant-wide events.
Repo string `json:"repo,omitempty" bson:"repo,omitempty"`
Subject Subject `json:"subject" bson:"subject"`
// Actor is the triggering identity (PR author login, commenter, "schedule").
Actor string `json:"actor,omitempty" bson:"actor,omitempty"`
// Labels are the labels on the subject at event time (board card labels,
// issue labels). Matched by Matcher.Labels.
Labels []string `json:"labels,omitempty" bson:"labels,omitempty"`
// Payload carries source-specific extras used for var templating
// (issue title/body, comment args, board from/to state, ...).
Payload map[string]any `json:"payload,omitempty" bson:"payload,omitempty"`
OccurredAt time.Time `json:"occurred_at" bson:"occurred_at"`
}
Event is the canonical envelope every source maps onto. Storage-agnostic and JSON+bson tagged so the same value can ride NATS (NATSBus), sit in Mongo (audit), or append to a file — mirroring the dual-tagging the forge and cloudsched records already use.
func BuildRunOutcome ¶ added in v1.1.0
func NormalizeBoardEvent ¶ added in v1.8.0
func NormalizeBoardEvent(get func(id string) (*native.Issue, error), evt native.Event, tenantID, repo, boardName string) (Event, bool)
NormalizeBoardEvent converts a native board event into a trigger.Event by reading the CURRENT issue through get (the audit event payload is sparse — labels/title/body live on the issue). Shared by the local BoardSource and the cloud board source; returns false when the issue can't be read (deleted between the transition and the read). When the card links an external forge issue, its repo slug is stamped on the event (falling back to the source-wide repo) so repo-scoped subscriptions match.
type LabelConsumer ¶ added in v1.8.0
type LabelConsumer interface {
ConsumeMatchLabels(ctx context.Context, tenantID, issueID string, labels []string) (consumed bool, err error)
}
LabelConsumer is the optional capability a BoardEffect exposes for consume_labels subscriptions: atomically strip the matcher's label set from a card before a direct launch. consumed=false (no error) means another evaluation already stripped them — the caller must skip the launch, which is what makes the label a one-shot trigger under duplicate card events. tenantID scopes the card on a multi-tenant (cloud) board; the local single-store effect ignores it.
type LaunchPlan ¶
type LaunchPlan struct {
BotID string
TenantID string
Repo string
Mode bundle.ExecutionMode
// Vars are the fully-resolved launch vars (subscription Vars + the
// ArgsVar payload), ready to stamp on the run / card.
Vars map[string]string
KeyOverrides map[string]string
SecretOverrides map[string]string
// RepoURL/RepoRef target a cloud clone when the event carries them.
RepoURL string
RepoRef string
// Event is the originating event (provenance, run→source back-link).
Event Event
// SourceRef, when non-nil, stamps typed provenance on the launched
// run (runview.LaunchSpec.SourceRef → run.json source). The
// Scheduler populates it for schedule fires so the schedgate
// overlap gate can count this schedule's live runs; other paths
// leave it nil.
SourceRef *store.RunSource
}
LaunchPlan is the resolved intent the Evaluator hands to an effect: launch (or promote a board card for) this bot, with these vars, on behalf of this event. It is the source-agnostic translation of (Subscription, Event) — the production Launcher maps it onto a runview.LaunchSpec, the BoardEffect maps it onto a native card stamp.
type Launcher ¶
type Launcher interface {
Launch(ctx context.Context, plan LaunchPlan) (runID string, err error)
}
Launcher launches a run directly (ExecutionDirect). The production impl wraps runview.Service.Launch; it lives in a separate wiring package so the trigger package stays free of a runview import (runview emits events back into the bus, which would otherwise cycle).
type Matcher ¶
type Matcher struct {
Sources []Source `json:"sources,omitempty" bson:"sources,omitempty"`
Kinds []string `json:"kinds,omitempty" bson:"kinds,omitempty"`
Actions []string `json:"actions,omitempty" bson:"actions,omitempty"`
Repos []string `json:"repos,omitempty" bson:"repos,omitempty"`
Authors []string `json:"authors,omitempty" bson:"authors,omitempty"`
Labels []string `json:"labels,omitempty" bson:"labels,omitempty"`
SubjectStates []string `json:"subject_states,omitempty" bson:"subject_states,omitempty"`
}
Matcher is the declarative filter on an Event. It is the union of the four existing trigger families' allowlists (webhooks.Config.{Event,Project, Author,Label}Allowlist, the dispatcher's label/state selectors, the forge invocation Actions) so every legacy config maps onto it without losing fidelity. An empty slice means "match any" for that dimension; a non-empty slice requires at least one match (OR within a dimension, AND across dimensions). Labels is the exception — it requires ALL listed labels to be present (the board "all_labels" gate), which is what implementer triggers like {state: ready, labels: [feature]} need.
func (Matcher) Match ¶
Match reports whether ev satisfies every dimension of m. It is pure and total (no I/O, no panics) so it is cheap to call on the hot path and trivial to unit-test against each family's allowlist shape. All string comparisons are case-insensitive except Kind/Source/State, which are machine-generated enums compared exactly.
type MemorySubscriptionStore ¶
type MemorySubscriptionStore struct {
// contains filtered or unexported fields
}
MemorySubscriptionStore is the in-memory SubscriptionStore for tests and local single-host runs. It loads from subscriptions.yaml + the dispatcher config + schedules.yaml at boot and is the store the InProcBus evaluator queries. Goroutine-safe; mirrors forge.MemoryRepoIntegrationStore.
func NewMemorySubscriptionStore ¶
func NewMemorySubscriptionStore() *MemorySubscriptionStore
func (*MemorySubscriptionStore) Create ¶
func (m *MemorySubscriptionStore) Create(_ context.Context, s Subscription) error
func (*MemorySubscriptionStore) Delete ¶
func (m *MemorySubscriptionStore) Delete(_ context.Context, id string) error
func (*MemorySubscriptionStore) Get ¶
func (m *MemorySubscriptionStore) Get(_ context.Context, id string) (Subscription, error)
func (*MemorySubscriptionStore) ListByBot ¶
func (m *MemorySubscriptionStore) ListByBot(_ context.Context, tenantID, botID string) ([]Subscription, error)
func (*MemorySubscriptionStore) ListByOrigin ¶
func (m *MemorySubscriptionStore) ListByOrigin(_ context.Context, tenantID, origin string) ([]Subscription, error)
func (*MemorySubscriptionStore) ListByRepo ¶
func (m *MemorySubscriptionStore) ListByRepo(_ context.Context, tenantID, repo string) ([]Subscription, error)
func (*MemorySubscriptionStore) ListByTenant ¶
func (m *MemorySubscriptionStore) ListByTenant(_ context.Context, tenantID string) ([]Subscription, error)
func (*MemorySubscriptionStore) ListCandidates ¶
func (m *MemorySubscriptionStore) ListCandidates(_ context.Context, ev Event) ([]Subscription, error)
func (*MemorySubscriptionStore) Update ¶
func (m *MemorySubscriptionStore) Update(_ context.Context, s Subscription) error
type MongoSubscriptionStore ¶
type MongoSubscriptionStore struct {
// contains filtered or unexported fields
}
MongoSubscriptionStore is the cloud-mode SubscriptionStore. Its index shape mirrors forge.MongoRepoIntegrationStore: a {tenant_id, repo} index for the candidate/by-repo queries and a {tenant_id, bot_id} index for by-bot.
func NewMongoSubscriptionStore ¶
func NewMongoSubscriptionStore(db *mongo.Database) *MongoSubscriptionStore
func (*MongoSubscriptionStore) Create ¶
func (s *MongoSubscriptionStore) Create(ctx context.Context, sub Subscription) error
func (*MongoSubscriptionStore) Delete ¶
func (s *MongoSubscriptionStore) Delete(ctx context.Context, id string) error
func (*MongoSubscriptionStore) DistinctBoardTenants ¶ added in v1.8.0
func (s *MongoSubscriptionStore) DistinctBoardTenants(ctx context.Context) ([]string, error)
DistinctBoardTenants returns the tenants holding at least one ENABLED board-kind subscription — the set the cloud board source poll-tails. Tenants without board triggers cost nothing.
func (*MongoSubscriptionStore) EnsureSchema ¶
func (s *MongoSubscriptionStore) EnsureSchema(ctx context.Context) error
func (*MongoSubscriptionStore) Get ¶
func (s *MongoSubscriptionStore) Get(ctx context.Context, id string) (Subscription, error)
func (*MongoSubscriptionStore) ListByBot ¶
func (s *MongoSubscriptionStore) ListByBot(ctx context.Context, tenantID, botID string) ([]Subscription, error)
func (*MongoSubscriptionStore) ListByOrigin ¶
func (s *MongoSubscriptionStore) ListByOrigin(ctx context.Context, tenantID, origin string) ([]Subscription, error)
func (*MongoSubscriptionStore) ListByRepo ¶
func (s *MongoSubscriptionStore) ListByRepo(ctx context.Context, tenantID, repo string) ([]Subscription, error)
func (*MongoSubscriptionStore) ListByTenant ¶
func (s *MongoSubscriptionStore) ListByTenant(ctx context.Context, tenantID string) ([]Subscription, error)
func (*MongoSubscriptionStore) ListCandidates ¶
func (s *MongoSubscriptionStore) ListCandidates(ctx context.Context, ev Event) ([]Subscription, error)
func (*MongoSubscriptionStore) Update ¶
func (s *MongoSubscriptionStore) Update(ctx context.Context, sub Subscription) error
type NativeBoardEffect ¶
type NativeBoardEffect struct {
// contains filtered or unexported fields
}
NativeBoardEffect is the BoardEffect for the native board: it promotes a card by stamping the subscription's bot + bot-args so the dispatcher's existing Claim picks it up. After a real change it nudges an optional Nudger (the dispatcher's Refresh) to dispatch now instead of at the next poll. Stamping is idempotent — a card already pinned to the same bot is left untouched, so a board event storm converges.
func NewNativeBoardEffect ¶
func NewNativeBoardEffect(store *native.Store, nudger Nudger, logger *iterlog.Logger) *NativeBoardEffect
NewNativeBoardEffect builds the native board promote effect. nudger and logger may be nil.
func (*NativeBoardEffect) ConsumeMatchLabels ¶ added in v1.8.0
func (n *NativeBoardEffect) ConsumeMatchLabels(_ context.Context, _ string, issueID string, labels []string) (bool, error)
ConsumeMatchLabels strips the given labels from the card, reporting whether any were actually present. The evaluator runs on a single serial worker (InProcBus), so read-strip-launch is race-free locally; consumed=false means a previous evaluation already stripped them and the caller must skip. The tenant is ignored — the local store IS one tenant.
func (*NativeBoardEffect) Promote ¶
func (n *NativeBoardEffect) Promote(_ context.Context, plan LaunchPlan) (string, error)
Promote stamps the matched card's bot + bot-args so the dispatcher's Claim picks it up, then nudges it to dispatch now. A board trigger fires when a card ENTERS an eligible state (the matcher's subject_states gate), so the card is already dispatchable — promoting only needs to pin which bot runs. Idempotent: a card already pinned to this bot is left untouched, so a board event storm converges.
type Nudger ¶
type Nudger interface {
Refresh()
}
Nudger asks a consumer to act on a just-promoted card immediately instead of waiting for its next poll. *dispatcher.Dispatcher satisfies it via Refresh(). Optional — when absent, the dispatcher's 30s poll still picks up the promoted card (the safety net).
type Publisher ¶
Publisher is the minimal sink the board source needs. eventbus.Bus satisfies it structurally; declaring it here (instead of importing eventbus) keeps the trigger package free of an eventbus import, so eventbus can depend on trigger without a cycle.
type ScheduleGate ¶ added in v0.50.0
type ScheduleGate struct {
Lister schedgate.ScheduleRunLister
Audit func(schedgate.TickRecord)
GuardDir string
// Reap cancels keepalive runs found stale at a tick (so the zombie
// frees resources once a fresh run has relaunched). Nil is safe — the
// stale run simply lingers but no longer blocks relaunch.
Reap func(ctx context.Context, runIDs []string)
}
ScheduleGate bundles the overlap/guard dependencies (pkg/schedgate) the scheduler consults before firing a due subscription. Lister counts the schedule's live runs (nil disables the overlap check); Audit receives one TickRecord per decision (nil disables auditing). GuardDir is the working directory guards run in (the workspace the server was started from when empty).
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler is the in-process timer source for schedule-kind subscriptions: it ticks each schedule subscription's Cron and fires the due ones via the Launcher. It is the local single-host counterpart to cloudsched (which owns the multi-replica CAS path for non-empty tenants) — it scopes itself to the local tenant "", so in cloud mode (where subscriptions carry real tenants) it is a no-op and cloudsched remains authoritative.
A schedule subscription is its own launch target, so the scheduler fires it DIRECTLY via the launcher rather than publishing an event the matcher would re-resolve (which is the round-trip board/forge events need but schedules do not). Firing a finished scheduled run still emits a run-completion event, so chaining off a scheduled run works through the run source.
func NewScheduler ¶
func NewScheduler(subs SubscriptionStore, launcher Launcher, opts ...SchedulerOption) *Scheduler
NewScheduler builds a scheduler over a subscription store + launcher.
func (*Scheduler) Start ¶
func (s *Scheduler) Start()
Start launches the ticking goroutine. No-op-safe to skip when launcher is nil (nothing to fire).
func (*Scheduler) Status ¶ added in v0.50.0
func (s *Scheduler) Status() SchedulerStatus
Status reports the scheduler's liveness snapshot. Safe on nil (zero value) so callers don't have to gate on wiring.
type SchedulerOption ¶
type SchedulerOption func(*Scheduler)
SchedulerOption configures a Scheduler.
func WithSchedulerClock ¶
func WithSchedulerClock(fn func() time.Time) SchedulerOption
WithSchedulerClock injects a clock for tests.
func WithSchedulerGate ¶ added in v0.50.0
func WithSchedulerGate(g *ScheduleGate) SchedulerOption
WithSchedulerGate wires the overlap/guard gate (nil-safe: a nil gate keeps the pre-gate fire-always behavior).
func WithSchedulerInterval ¶ added in v1.0.0
func WithSchedulerInterval(d time.Duration) SchedulerOption
WithSchedulerInterval overrides the loop tick resolution (default 1 minute). Set it below a minute (e.g. 5s) so sub-minute keepalive subscriptions can actually fire at their cadence — the loop can only fire a subscription as often as it ticks. Values <= 0 are ignored.
func WithSchedulerLogger ¶
func WithSchedulerLogger(l *iterlog.Logger) SchedulerOption
WithSchedulerLogger sets the leveled logger (nil-safe).
type SchedulerStatus ¶ added in v0.50.0
type SchedulerStatus struct {
// LastTickAt is when tick() last ran (zero before the first tick).
LastTickAt time.Time `json:"last_tick_at"`
// Subscriptions is the schedule-kind subscription count seen on the
// last tick.
Subscriptions int `json:"subscriptions"`
// Armed is how many subscriptions currently hold a next-fire slot.
Armed int `json:"armed"`
// IntervalSeconds is the tick cadence, so a reader can judge
// staleness ("last tick 3 intervals ago = dead").
IntervalSeconds float64 `json:"interval_seconds"`
}
SchedulerStatus is the read-only health snapshot behind /api/v1/triggers/health.
type Source ¶
type Source string
Source classifies where an event originated. Closed set — a new source adapter adds a constant here and emits onto the bus; nothing downstream changes.
const ( // SourceForge is an inbound git-forge webhook (GitLab/GitHub/Forgejo/generic). SourceForge Source = "forge" // SourceSchedule is a cron tick (host crontab or cloudsched). SourceSchedule Source = "schedule" // SourceBoard is a native-board transition (a native.Event). SourceBoard Source = "board" // SourceRun is a run-lifecycle event (run finished/failed) — the // "runned by iterion" chaining source. SourceRun Source = "run" // SourceManual is an operator/CLI explicit launch (iterion run). SourceManual Source = "manual" // SourceCustom is an external integration via the signed emit ingress. SourceCustom Source = "custom" )
type Subject ¶
type Subject struct {
Type string `json:"type" bson:"type"` // "pull_request" | "issue" | "card" | "run" | "repo" | "comment"
ID string `json:"id,omitempty" bson:"id,omitempty"`
URL string `json:"url,omitempty" bson:"url,omitempty"`
SHA string `json:"sha,omitempty" bson:"sha,omitempty"`
Ref string `json:"ref,omitempty" bson:"ref,omitempty"`
Title string `json:"title,omitempty" bson:"title,omitempty"`
Body string `json:"body,omitempty" bson:"body,omitempty"`
// State is the subject's workflow/board state at the time of the event
// (e.g. the board column a card entered). Matched by Matcher.SubjectStates.
State string `json:"state,omitempty" bson:"state,omitempty"`
}
Subject is the thing an event is about (a PR, an issue/card, a run, a repo). Fields are best-effort per source; templating reads from here and Payload.
type Subscription ¶
type Subscription struct {
ID string `json:"id" bson:"_id"`
TenantID string `json:"tenant_id,omitempty" bson:"tenant_id,omitempty"`
// Repo scopes the subscription to one repo ("group/project"); "" makes it
// tenant-wide (e.g. a board-wide trigger on the local single-host board).
Repo string `json:"repo,omitempty" bson:"repo,omitempty"`
BotID string `json:"bot_id" bson:"bot_id"`
// Invocation names which bot capability this subscription activates, so a
// deprovision/regeneration can rebuild exactly the rows derived from a
// given invocation kind.
Invocation bundle.InvocationKind `json:"invocation" bson:"invocation"`
// Mode is direct (launch now) vs board (materialise a card the dispatcher
// claims). Empty inherits the invocation's EffectiveMode at evaluation.
Mode bundle.ExecutionMode `json:"mode,omitempty" bson:"mode,omitempty"`
Match Matcher `json:"match" bson:"match"`
// ConsumeLabels (board-source, direct-mode only) strips the Match.Labels
// set from the card before launching, making the labels a one-shot
// trigger: duplicate card events can't double-launch, and re-adding the
// label re-arms it. Mirrors bundle.InvocationBoard.ConsumeLabels.
ConsumeLabels bool `json:"consume_labels,omitempty" bson:"consume_labels,omitempty"`
// Vars are launch-var overrides stamped on the run (ContextVars +
// operator LaunchVars merged at provision time; operator wins).
Vars map[string]string `json:"vars,omitempty" bson:"vars,omitempty"`
// ArgsVar names the workflow input var that receives the event's free-text
// payload (issue title+body, comment args). Empty injects no payload.
ArgsVar string `json:"args_var,omitempty" bson:"args_var,omitempty"`
// Cron is set only for Invocation == schedule (the timer source matches it).
Cron string `json:"cron,omitempty" bson:"cron,omitempty"`
// IntervalSeconds is set only for Invocation == keepalive (the sub-minute
// counterpart of Cron): the always-on relaunch cadence in seconds.
IntervalSeconds int `json:"interval_seconds,omitempty" bson:"interval_seconds,omitempty"`
KeyOverrides map[string]string `json:"key_overrides,omitempty" bson:"key_overrides,omitempty"`
SecretOverrides map[string]string `json:"secret_overrides,omitempty" bson:"secret_overrides,omitempty"`
// Overlap policy + pre-launch guard for schedule-kind subscriptions
// (pkg/schedgate). Overlap "" normalizes to "skip": a cron tick
// whose previous run is still live is skipped and audited instead
// of piling up. Guard is an optional `sh -lc` gate: exit 0 fires
// (stdout → vars[GuardVar]), non-zero skips the tick.
Overlap string `json:"overlap,omitempty" bson:"overlap,omitempty"`
MaxConcurrent int `json:"max_concurrent,omitempty" bson:"max_concurrent,omitempty"`
Guard string `json:"guard,omitempty" bson:"guard,omitempty"`
GuardTimeout string `json:"guard_timeout,omitempty" bson:"guard_timeout,omitempty"`
GuardVar string `json:"guard_var,omitempty" bson:"guard_var,omitempty"`
// StaleAfter is the keepalive silence cutoff (Go duration); empty
// normalizes to schedgate.DefaultStaleAfter. Only meaningful with
// Overlap == keepalive.
StaleAfter string `json:"stale_after,omitempty" bson:"stale_after,omitempty"`
// Origin records where this subscription came from so dedup and cleanup
// are possible: "forge:<repo_integration_id>" (orchestrator-generated,
// deleted by Origin on deprovision), "operator" (studio), "schedule.yaml"
// / "dispatcher.yaml" (local config load).
Origin string `json:"origin,omitempty" bson:"origin,omitempty"`
Enabled bool `json:"enabled" bson:"enabled"`
CreatedBy string `json:"created_by,omitempty" bson:"created_by,omitempty"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
Subscription binds an event filter to a bot launch into a target. One row per (tenant, repo, bot, invocation-kind). This is the unit the "by repo / by bot" studio surfaces and the Evaluator's hot-path query hit, and the projection the forge orchestrator generates from a bot's Invocations.
func FromBoardInvocation ¶
func FromBoardInvocation(id, tenantID, repo, botID, origin string, inv bundle.Invocation, now time.Time) (Subscription, bool)
FromBoardInvocation derives a board-trigger Subscription from a bot's kind=board invocation that carries a board: block. A plain kind=board invocation with no board: block stays poll-only (the legacy dispatcher target) and yields no subscription — opting into a board: block is what activates event-driven promotion. Returns ok=false for any other invocation.
The caller supplies id (a uuid), tenant, and repo scope; the board block's On/ToStates/AllLabels become the Matcher. The default mode is board (promote the card so the dispatcher claims it); an explicit mode: direct launches the bot itself on the matching card event (with the card id in vars["issue_id"]) — the triage-style "run a bot ON the card without routing the card TO it" shape.
func FromKeepaliveInvocation ¶ added in v1.0.0
func FromKeepaliveInvocation(id, tenantID, repo, botID, origin string, inv bundle.Invocation, now time.Time) (Subscription, bool)
FromKeepaliveInvocation derives an always-on Subscription from a bot's kind=keepalive invocation. The interval (validated >= KeepaliveMinInterval at manifest parse) becomes IntervalSeconds; the overlap policy is keepalive so the scheduler relaunches a fresh run each tick with at-most-one-live + staleness reaping. Returns ok=false for any other invocation or a missing/unparseable interval.
func FromScheduleInvocation ¶
func FromScheduleInvocation(id, tenantID, repo, botID, origin string, inv bundle.Invocation, now time.Time) (Subscription, bool)
FromScheduleInvocation derives a schedule-trigger Subscription from a bot's kind=schedule invocation that carries a suggested_cron. The cron is advisory (the operator may retune it via the Triggers REST), but seeding it enabled makes the bot's recurring run work out of the box. Returns ok=false for any other invocation or a missing cron.
func (Subscription) EffectiveMode ¶
func (s Subscription) EffectiveMode() bundle.ExecutionMode
EffectiveMode returns the subscription's execution mode, defaulting an empty value to ExecutionDirect (mirrors bundle.Invocation.EffectiveMode).
func (Subscription) NextFire ¶ added in v1.0.0
NextFire computes the subscription's next-fire instant after `after`, the single seam that resolves the cron-vs-interval cadence (mirrors cloudsched.NextFireForBot for the resident scheduler). ok=false means this subscription has no timer cadence (not a schedule/keepalive kind, or missing cron/interval) and the scheduler skips it; a non-nil err is a malformed cron on an otherwise-eligible schedule subscription.
func (Subscription) Policy ¶ added in v0.50.0
func (s Subscription) Policy() schedgate.Policy
Policy projects the subscription's schedgate fields into a normalized overlap/guard policy.
type SubscriptionStore ¶
type SubscriptionStore interface {
Create(ctx context.Context, s Subscription) error
Get(ctx context.Context, id string) (Subscription, error)
Update(ctx context.Context, s Subscription) error
Delete(ctx context.Context, id string) error
// ListByTenant returns every subscription owned by a tenant
// ("" tenant = the local single-host scope).
ListByTenant(ctx context.Context, tenantID string) ([]Subscription, error)
// ListByRepo returns subscriptions scoped to a specific repo plus the
// tenant-wide (Repo == "") ones, since both can fire for a repo event.
ListByRepo(ctx context.Context, tenantID, repo string) ([]Subscription, error)
// ListByBot returns every subscription that launches a given bot.
ListByBot(ctx context.Context, tenantID, botID string) ([]Subscription, error)
// ListByOrigin returns subscriptions provisioned under an Origin marker
// (e.g. "forge:<repo_integration_id>") so a deprovision can delete exactly
// the rows it created.
ListByOrigin(ctx context.Context, tenantID, origin string) ([]Subscription, error)
// ListCandidates returns the enabled subscriptions that COULD match ev:
// same tenant, and Repo matching ev.Repo or tenant-wide. The Evaluator
// applies the full Matcher.Match on the returned set — this is only the
// indexed pre-filter, kept coarse so it stays a single-index query.
ListCandidates(ctx context.Context, ev Event) ([]Subscription, error)
}
SubscriptionStore persists trigger subscriptions. It mirrors forge.RepoIntegrationStore: an interface with an in-memory impl (tests / local single-host) and a Mongo impl (cloud multitenant). The three query primitives are the product surfaces — ListByRepo and ListByBot power the studio "by repo" / "by bot" views, ListCandidates is the Evaluator hot path.