← repoman internals

internal/run

import "git.griefed.de/griefed/repoman/internal/run"
package run // import "git.griefed.de/griefed/repoman/internal/run"

Package run defines the domain types for migration runs and run events. The
DB implementations live in internal/db; the engine lives in internal/migrate.
Both import from here to avoid cycles.

CONSTANTS

const OKLinePrefix = "[OK]"
    OKLinePrefix is the marker the engine prints at the start of a log line for
    a repository it freshly migrated (e.g. "[OK] owner/repo"). It is the single
    source of truth for that convention: the line parser classifies on it,
    and the recovery loop-guard detects "this run migrated a new repository" by
    it (such a repo is idempotently skipped on the next attempt, so its presence
    proves the migration advanced).

    It lives HERE, in the domain-types package, only because internal/db needs
    it: RunProgress builds an anchored `message LIKE '[OK]%'` query from it,
    and db must not import internal/migrate (that would couple storage to
    the engine). This package is therefore the lowest common one the parser,
    the engine and the DB layer already share. That is a tolerated exception,
    not a pattern to copy — the engine's other line markers have no consumer
    outside internal/migrate and are defined there (see errorLinePrefix in
    migrate/lineparser.go). Don't move one here for symmetry alone.


TYPES

type Counters struct {
	TotalRepos int
	Migrated   int
	Skipped    int
	Errors     int
	// Warnings counts problems that did not abort a repo's migration but did
	// stop something from happening — a release listing refused, an asset that
	// could not be downloaded, a ref push that failed. They are deliberately
	// separate from Errors so a repo archived on the source (whose webhook
	// provisioning will always 403) cannot make every run report failure.
	Warnings          int
	UsersCreated      int
	MirrorsConfigured int
	ReposSynced       int
}
    Counters holds the summary stats parsed from migrate.sh output.

type Event struct {
	ID        int64
	RunID     int64
	Timestamp time.Time
	Level     EventLevel
	Source    string
	Message   string
}
    Event is one row from the run_events table.

type EventLevel string
    EventLevel mirrors the run_events.level CHECK constraint.

const (
	// LevelDebug is diagnostic detail, normally hidden from operators.
	LevelDebug EventLevel = "debug"
	// LevelInfo is normal progress output.
	LevelInfo EventLevel = "info"
	// LevelWarn flags a non-fatal problem (e.g. a skipped repo).
	LevelWarn EventLevel = "warn"
	// LevelError flags a per-repo failure that did not abort the run.
	LevelError EventLevel = "error"
)
type EventRepository interface {
	// Append writes a batch of events in one transaction.
	Append(ctx context.Context, events []Event) error

	// ListForRun returns all events for a run in order, with IDs
	// greater than afterID. Pass 0 to get all events.
	ListForRun(ctx context.Context, runID int64, afterID int64) ([]Event, error)

	// ListForRunPage returns one page of a run's events for the lazy log view:
	// the newest `limit` events older than `before` (before<=0 means the newest
	// events overall), returned oldest-first. hasMore reports whether still-older
	// events exist, so the client knows scroll-up can load another page. This
	// backs the monitor's "load only what fits, fetch more on scroll-up" view, so
	// a multi-thousand-line run never ships its whole log in one response.
	ListForRunPage(ctx context.Context, runID, before int64, limit int) (events []Event, hasMore bool, err error)

	// RunProgress reports a run's resumable-progress facts for the recovery
	// loop-guard. hasMigratedRepo is true when the run logged at least one
	// OKLinePrefix line (a repository it freshly migrated, which the idempotent
	// engine skips on the next attempt, so the crash point necessarily advances);
	// lastActivityAt is the timestamp of its most recent event (zero when it logged
	// none), used to tell a continuation that did sustained idempotent work — e.g.
	// a multi-GB release-asset sync on an already-present repo, which emits no
	// OKLinePrefix line — apart from one that crashed almost immediately.
	RunProgress(ctx context.Context, runID int64) (hasMigratedRepo bool, lastActivityAt time.Time, err error)
}
    EventRepository is the persistence interface for run events.

type ExitStatus string
    ExitStatus is the terminal state of a run.

const (
	// StatusRunning means the run is still in progress (not terminal).
	StatusRunning ExitStatus = "running"
	// StatusSuccess means every repo migrated without error.
	StatusSuccess ExitStatus = "success"
	// StatusPartial means the run completed but some repos errored.
	StatusPartial ExitStatus = "partial"
	// StatusFailed means the run aborted before completing.
	StatusFailed ExitStatus = "failed"
	// StatusCancelled means an operator cancelled the run.
	StatusCancelled ExitStatus = "cancelled"
)
type QueueEntry struct {
	ID                int64
	MigrationConfigID int64
	TriggeredBy       TriggerSource
	TriggeredByUserID *int64
	Mode              RunMode
	QueuedAt          time.Time

	// IsRecovery marks an entry queued to continue an interrupted run after a
	// restart; it flows into the created run's IsRecovery so the one-retry guard
	// persists. False for ordinary manual/scheduled triggers.
	IsRecovery bool
}
    QueueEntry is one row from the run_queue table.

type QueueRepository interface {
	// Enqueue adds an entry. Returns ErrAlreadyQueued if a pending
	// entry for the same config already exists and dedup=true.
	Enqueue(ctx context.Context, entry *QueueEntry, dedup bool) error

	// Pop removes and returns the oldest pending entry. Returns
	// (nil, nil) when the queue is empty.
	Pop(ctx context.Context) (*QueueEntry, error)

	// List returns all pending entries in FIFO order (oldest first) without
	// removing them, so the UI can show queued-but-unstarted runs.
	List(ctx context.Context) ([]*QueueEntry, error)

	// HasForConfig reports whether an entry for one config is waiting. Separate
	// from List because the callers that ask this — the confirmation wizard's
	// step and its status poll, which runs every few seconds per open page —
	// want one boolean, not every pending entry scanned into structs and then
	// searched.
	HasForConfig(ctx context.Context, configID int64) (bool, error)

	// Len returns how many entries are waiting.
	Len(ctx context.Context) (int, error)
}
    QueueRepository manages the FIFO run queue.

type Repository interface {
	// Create inserts a new run row with status=running and returns its ID.
	Create(ctx context.Context, r *Run) (int64, error)

	// GetByID returns one run by ID.
	GetByID(ctx context.Context, id int64) (*Run, error)

	// ListForConfig returns runs for one config, newest first.
	ListForConfig(ctx context.Context, configID int64, limit int) ([]*Run, error)

	// ListUnfinished returns every run still in StatusRunning. After a clean
	// boot these are orphans (the worker is single-instance and not yet started,
	// so none can be live): a previous process was interrupted before it could
	// finalize the row. Used by Worker.RecoverOrphans.
	ListUnfinished(ctx context.Context) ([]*Run, error)

	// UpdateFinished sets finished_at, exit_status, and counter fields.
	UpdateFinished(ctx context.Context, id int64, at time.Time, status ExitStatus, counters Counters) error

	// UpdateStatus updates only exit_status. Used for cancellation.
	UpdateStatus(ctx context.Context, id int64, status ExitStatus) error
}
    Repository is the persistence interface for run records.

type Run struct {
	ID                       int64
	MigrationConfigID        *int64
	MigrationConfigVersionID *int64
	MigrationConfigName      string // joined from migration_configs; may be empty for deleted configs
	TriggeredBy              TriggerSource
	TriggeredByUserID        *int64
	Mode                     RunMode
	StartedAt                time.Time
	FinishedAt               *time.Time
	ExitStatus               ExitStatus

	// IsRecovery marks a run that was automatically queued to continue an
	// interrupted run after a daemon restart/crash. Such a run is never itself
	// re-queued on a subsequent interruption (the one-retry guard against a
	// config that crashes the daemon mid-run); see Worker.RecoverOrphans.
	IsRecovery bool

	// Counters — populated from the script's summary block.
	TotalRepos int
	Migrated   int
	Skipped    int
	Errors     int
	// Warnings counts non-fatal problems — a refused release listing, an asset
	// that could not be downloaded, a failed ref push. Separate from Errors so a
	// repository archived upstream cannot make every run report failure.
	Warnings          int
	UsersCreated      int
	MirrorsConfigured int
	ReposSynced       int
}
    Run is one row from the runs table.

func (r *Run) Duration(now time.Time) time.Duration
    Duration returns the elapsed time for a finished run, or time since start
    for a still-running one.

func (r *Run) IsFinished() bool
    IsFinished reports whether the run has reached a terminal state.

type RunMode string
    RunMode distinguishes real runs from previews.

const (
	// ModeReal performs the migration and writes to Forgejo.
	ModeReal RunMode = "real"
	// ModeDryRun previews the migration without writing anything.
	ModeDryRun RunMode = "dry-run"
	// ModeFirstRunForcedDry forces a dry run the first time a config is
	// executed, before the operator has confirmed its target namespace.
	ModeFirstRunForcedDry RunMode = "first-run-forced-dry"
)
type TriggerSource string
    TriggerSource is how a run was initiated.

const (
	// TriggerManual marks a run an operator started from the web UI.
	TriggerManual TriggerSource = "manual"
	// TriggerScheduled marks a run the cron scheduler started.
	TriggerScheduled TriggerSource = "scheduled"
)