trigger

package
v1.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 22 Imported by: 0

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

View Source
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.

View Source
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.

View 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.

View Source
const SubscriptionsCollectionName = "trigger_subscriptions"

SubscriptionsCollectionName is the Mongo collection backing the cloud-mode SubscriptionStore.

Variables

View Source
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

func IsCardEvent(t native.EventType) bool

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

func RunOutcomeEventID(runID, status, interactionID string, updatedAt time.Time) string

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

func (e *Evaluator) Handle(ctx context.Context, ev Event) error

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 BuildRunOutcome(ctx context.Context, rs store.RunStore, runID string, bodyErr error) Event

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

func (m Matcher) Match(ev Event) bool

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 (*MemorySubscriptionStore) Delete

func (*MemorySubscriptionStore) Get

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

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 (*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 (*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

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

type Publisher interface {
	Publish(ctx context.Context, ev Event) error
}

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.

func (*Scheduler) Stop

func (s *Scheduler) Stop()

Stop halts the ticker (idempotent-safe for a single call).

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

func (s Subscription) NextFire(after time.Time) (next time.Time, ok bool, err error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL