fleetsync

package
v0.136.0 Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package fleetsync decides and performs the sync action for a single repo: clone or pull an active repo, or — only when the caller explicitly opts in — remove an archived one's local clone. It has no TUI or terminal output of its own — callers (e.g. the wb sync worker pool) drive it and render results.

Index

Constants

View Source
const (
	PhasePlanned = "planned"
	PhaseRemoved = "removed"
	PhaseFailed  = "failed"
)

Receipt phases. A receipt is written as `planned` before anything is deleted, so an interrupted or crashed run still leaves proof of intent, and updated to `removed` or `failed` afterwards. A receipt left at `planned` is itself a finding: something stopped mid-deletion.

Variables

This section is empty.

Functions

func IssuesMarkdown added in v0.75.0

func IssuesMarkdown(meta RunMeta, results []Result) string

IssuesMarkdown renders the attention and error groups as Markdown for a human or an AI agent to act on. It performs no IO and is deterministic: identical input always renders identical bytes.

Types

type RemovalReceipt added in v0.78.0

type RemovalReceipt struct {
	SchemaVersion int        `json:"schema_version"`
	Phase         string     `json:"phase"`
	Repository    string     `json:"repository"`
	ClonePath     string     `json:"clone_path"`
	HeadSHA       string     `json:"head_sha"`
	Reason        string     `json:"reason"`
	CreatedAt     time.Time  `json:"created_at"`
	RemovedAt     *time.Time `json:"removed_at,omitempty"`
	Error         string     `json:"error,omitempty"`
}

RemovalReceipt records one archived clone that `wb sync --prune-archived` deleted, or was about to.

Sync is the only WB command that removes a canonical clone without leaving evidence: `wb archive clean` performs the same os.RemoveAll and writes a receipt for it, while sync printed a line to a terminal and forgot. A deleted clone is not recoverable from WB, so "what was removed, from where, at which commit, and on what grounds" has to outlive the terminal it scrolled past.

HeadSHA is the point of the record. The repository still exists on GitHub — archived, read-only, but intact — so the clone can be restored, and the commit it stood at is the difference between restoring the right state and guessing.

type Result

type Result struct {
	Repo   discover.Repo
	Status Status
	// PullPlanned is true only for a dry-run existing clone. PullAttempted and
	// PullSucceeded describe the real action independently of Status, which may
	// still end as Unpushed after a successful pull. Updated is a successful
	// pull whose checked-out commit moved forward.
	PullPlanned   bool
	PullAttempted bool
	PullSucceeded bool
	Updated       bool
	// BeforeHeadSHA is populated for an existing checkout when its pre-pull
	// HEAD was readable. Together with HeadSHA it is the exact lifecycle-hook
	// boundary; an unchanged pair never emits checkout-updated.
	BeforeHeadSHA string
	Detail        gitops.RepoStatus
	// Tracking is filled in only for Diverged and NoUpstream, whose reports
	// are meaningless without the branch names and ahead/behind counts.
	Tracking gitops.TrackingState
	Err      error

	// Archived mirrors repo.Archived, for callers that render results without
	// keeping the original discover.Repo alongside them.
	Archived bool
	// ArchivedNotPruned is true when Archived is true but the caller did not
	// request pruning (Sync's pruneArchived parameter was false): this
	// repository was pulled or left alone exactly like any other clone,
	// never evaluated for deletion. It exists so an archived repository is
	// never silently indistinguishable from an ordinary one in a report —
	// the whole point of making pruning opt-in is defeated if turning it off
	// also makes archived repositories invisible.
	ArchivedNotPruned bool
	// Reason explains, in prose, exactly why an archived repository was or
	// was not eligible for removal when pruning was requested. It is the
	// verbatim explanation from internal/archiveprune.Evaluate — the same
	// safety predicate wb archive clean uses — never a re-derived summary.
	Reason string
	// HeadSHA is the clone's HEAD when Sync finished with it, empty when
	// there was no clone to read (never cloned, or just removed). A report is
	// read and acted on later — sometimes hours later, by an agent that
	// cannot see the fleet — and a remedy like "reset the clone to its
	// upstream" is unrecoverable if the clone has moved since. Recording the
	// commit the finding was made against lets a reader prove the state it
	// describes still holds before mutating anything.
	HeadSHA string
	// ReceiptPath is where the deletion receipt for this clone was written,
	// set only when --prune-archived actually removed (or tried to remove)
	// it. A removal with no path here is a removal with no evidence, which
	// this package refuses to perform.
	ReceiptPath          string
	RepositoryRelocation *worktrees.RepositoryRelocateResult
}

Result is the outcome of syncing one repo.

func Sync

func Sync(ctx context.Context, repo discover.Repo, projectsRoot string, dryRun, pruneArchived bool) Result

Sync reconciles a single repo's local clone with its GitHub state: clone if missing, pull if present and clean, skip if the working tree is dirty. Forks and repos not owned by the authenticated user or their orgs (repo.Remote == false) are left untouched (NoOp). Repos marked with `wb repo ignore` are left untouched too (SkippedIgnored), including archived ones. In dryRun mode no mutation happens; Status still reports what would be done.

An archived repository is never deleted unless pruneArchived is true. With pruneArchived false — the default for `wb sync` — an archived repository with a local clone is pulled exactly like any other clone (its Status may be Pulled, SkippedDirty, Unpushed, and so on); one with no local clone yet is reported AbsentArchived, since cloning it here would be an unrequested behavior change and it carries no risk either way. Every archived result still carries Archived and, when not pruning, ArchivedNotPruned, so a report can never make an archived repository indistinguishable from an ordinary one just because pruning was left off.

With pruneArchived true, an archived repository's local clone is removed only when it passes internal/archiveprune.Evaluate — the exact safety predicate `wb archive clean` uses (live-confirmed archived status, no uncommitted/untracked changes, no stash, no unpushed commits on any branch, no local-only branch, no unpushed tag, no linked worktree, no non-terminal WB Work Log claim, not marked wb.skip-sync). That predicate is called verbatim, not re-derived, so this path and `wb archive clean` can never drift into different definitions of "safe to delete".

func (Result) PullSummary added in v0.55.0

func (r Result) PullSummary() string

PullSummary renders the pull action independently of the final repository status. Empty means this result did not concern an existing active clone.

type RunMeta added in v0.75.0

type RunMeta struct {
	StartedAt    time.Time
	ProjectsRoot string
	Scanned      int
	DryRun       bool
	// PruneArchived records whether --prune-archived was passed. Without it
	// the ArchivedNotPruned entries cannot be explained.
	PruneArchived bool
	// RunErr is set when the run failed before scanning anything — a GitHub
	// authentication or discovery failure. Results is empty in that case, and
	// the failure is itself the issue worth reporting.
	RunErr error

	// Owners and Filter record how the run was scoped. Without them the
	// report cannot distinguish "the fleet is clean" from "the two
	// repositories I looked at are clean" — and because every run overwrites
	// the same file, a scoped run would otherwise silently replace a
	// fleet-wide finding set with a false all-clear.
	Owners []string
	Filter string
	// Discovered is how many repositories the run selected; Scanned is how
	// many it finished. Fewer scanned than discovered means the run did not
	// complete, which no report may describe as health.
	Discovered int
}

RunMeta describes the sync run that produced an issues report. It carries what the results themselves cannot say: when the run happened, whether it was a dry run, whether archived pruning was requested, and whether the run failed before it scanned anything at all.

func (RunMeta) Complete added in v0.77.0

func (m RunMeta) Complete() bool

Complete reports whether this run may speak for the whole fleet. Only a complete, unscoped, non-dry run can honestly say everything is in sync.

func (RunMeta) Interrupted added in v0.77.0

func (m RunMeta) Interrupted() bool

Interrupted reports whether the run stopped before finishing every repository it selected — the shape a Ctrl-C in the progress UI leaves, which returns the results collected so far.

func (RunMeta) Scoped added in v0.77.0

func (m RunMeta) Scoped() bool

Scoped reports whether the run looked at less than the whole fleet.

type Status

type Status int

Status is the outcome fleetsync.Sync took for a single repo.

const (
	Cloned Status = iota
	Pulled
	SkippedDirty
	RemovedArchived
	KeptArchived
	AbsentArchived
	NoOp
	Failed
	// SkippedIgnored, EmptyRemote, Diverged and NoUpstream are appended last
	// deliberately: Status is rendered only through String() and never
	// persisted numerically, but appending keeps the existing values stable
	// regardless.
	SkippedIgnored
	EmptyRemote
	// Diverged: the branch and its upstream each hold commits the other
	// lacks, so no fast-forward is possible and sync must not guess a
	// reconciliation.
	Diverged
	// NoUpstream: the checked-out branch tracks nothing, so there is nowhere
	// to pull from.
	NoUpstream
	// Unpushed: the pull succeeded, but the clone holds commits that exist on
	// no remote. Nothing is wrong with the sync; the work has simply never
	// left this machine.
	Unpushed
	// ArchivedUnlandable: an archived clone holding unpushed commits. The
	// remote is read-only, so those commits can never be pushed — unlike
	// KeptArchived, this state cannot resolve itself and needs a decision.
	ArchivedUnlandable
	RepositoryTransferred
	RepositoryTransferRequired
)

func (Status) String

func (s Status) String() string

type SummaryGroup added in v0.56.0

type SummaryGroup struct {
	Label   string
	Section SummarySection
	Results []Result
}

SummaryGroup is one selectable/countable row in the final sync summary. Results are sorted by repository slug so every renderer is deterministic. A result may belong to both an action row and a final-outcome row.

func Summary added in v0.56.0

func Summary(results []Result) []SummaryGroup

Summary builds the ordered category model shared by plain output and the interactive results browser.

func SummaryGroupByLabel added in v0.56.0

func SummaryGroupByLabel(groups []SummaryGroup, label string) (SummaryGroup, bool)

SummaryGroupByLabel finds a summary row by its stable display label.

type SummarySection added in v0.56.0

type SummarySection string

SummarySection groups related rows in both the plain and interactive sync summaries. Its value is the human-readable section heading.

const (
	SummaryFinalOutcomes SummarySection = "Final outcomes"
	SummaryPullActions   SummarySection = "Pull actions"
	SummaryAttention     SummarySection = "Attention"
	SummaryErrors        SummarySection = "Failures"
)

Jump to

Keyboard shortcuts

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