streamsync

package
v0.155.1 Latest Latest
Warning

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

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

Documentation

Overview

Package streamsync keeps a stream branch current and verifies its batch.

It owns two things rows 8 and 9 of the Feature call for, and both are shared with the absorb verb that follows:

  • the REBASE path — bring `stream/<name>` onto a freshly fetched `origin/main` without ever merging, then bring every open agent branch onto the new stream head, reporting conflicts per branch;
  • the BATCH VERIFICATION path — apply the whole batch, run the suite once, and on failure find the culprit by cumulative prefix re-apply on a local scratch branch that is never pushed.

Sync pushes ONLY on a justified trigger. Under `pushes-are-justified-and-counted` a dependency bump is never one: bumps are local commits on the stream branch, verified once as a batch. When a trigger IS given, the push really happens and is verified — reporting a push that did not occur is the failure `verbs-assert-effects-not-exit-codes` forbids.

Implements: dependency-streams#req:sync-rebases-and-never-merges, dependency-streams#req:sync-is-idempotent-against-landed-bumps, dependency-streams#req:dependency-bumps-are-commits-on-the-stream-branch, dependency-streams#req:batch-verifies-once, dependency-streams#req:the-batch-element-is-defined, dependency-streams#req:batch-failure-is-found-by-prefix-re-apply, dependency-streams#req:every-verification-run-is-bounded, dependency-streams#req:pushes-are-justified-and-counted.

Index

Constants

View Source
const (
	// BumpApplied means one commit was written.
	BumpApplied = "bumped"
	// BumpAtTarget means the consumer already requires the target — the
	// normal outcome when Renovate landed it first.
	BumpAtTarget = "already-at-target"
	// BumpNotRequired means the consumer does not declare the library.
	BumpNotRequired = "not-required"
	// BumpUnreadableVersion means WB could not compare the declared version.
	// It is deliberately NOT "already-at-target": no commit is written either
	// way, but claiming the consumer is at target is an assertion WB cannot
	// support, and a false assurance is worse than no answer.
	BumpUnreadableVersion = "version-unreadable"
	// BumpFailed means the manifest edit or the lockfile refresh failed.
	BumpFailed = "failed"
)

Bump actions are contract: the report and the exit code branch on them.

View Source
const RefusalUnjustifiedPush = "unjustified-push"

RefusalUnjustifiedPush is the stable code for a push with no trigger.

Variables

This section is empty.

Functions

func BumpMessage

func BumpMessage(library Library, from string) string

BumpMessage formats one bump commit to the repository's own convention.

Types

type AgentBranch

type AgentBranch struct {
	Branch string `json:"branch"`
	// Agent is the claiming session, so a conflict report can name who has to
	// resolve it.
	Agent string `json:"agent,omitempty"`
	// InReview marks a branch whose review is open. Rebasing it invalidates
	// the review, so sync refuses by default.
	InReview bool `json:"in_review,omitempty"`
}

AgentBranch is one agent's branch on the stream, known from the stream's claims and recorded membership — the operator never names them.

type BatchResult

type BatchResult struct {
	Passed bool `json:"passed"`
	// Runs is how many full verification runs it cost. One in the passing
	// case; 1+k when the culprit is element k.
	Runs int `json:"runs"`
	// Elements is the batch as applied, after lockstep families are grouped.
	Elements []Element `json:"elements"`
	// Culprit is the last element of the first failing prefix.
	Culprit *Element `json:"culprit,omitempty"`
	// ProvenGood are the elements every failing prefix had already cleared.
	ProvenGood []Element `json:"proven_good,omitempty"`
	// InteractionFailure is set when every prefix passed: the failure came
	// from the base or a rebased change, not from any element.
	InteractionFailure bool `json:"interaction_failure,omitempty"`
	// FailingCheck names what actually failed.
	FailingCheck string `json:"failing_check,omitempty"`
	// Skipped names CI mechanisms this run did not execute, printed only
	// where CI is proved to carry them.
	Skipped []string `json:"skipped,omitempty"`
	// Unguarded names mechanisms neither this run nor CI carries.
	Unguarded []string `json:"unguarded,omitempty"`
	// Unverified names mechanisms WB could not decide about, because the
	// stream-PR workflow calls a reusable workflow in another repository.
	// "I could not tell" is not the same claim as "CI does not run it".
	Unverified []string `json:"unverified,omitempty"`
	// UnexaminedElements is how many elements the prefix scan never reached.
	// The scan stops at the FIRST failing prefix by design, so a green re-run
	// after fixing the culprit is expected rather than surprising.
	UnexaminedElements int `json:"unexamined_elements,omitempty"`
	// ScratchBranch is the local branch prefix re-apply ran on. It is never
	// pushed, which is what keeps the whole search to zero CI runs.
	ScratchBranch string `json:"scratch_branch,omitempty"`
}

BatchResult is the outcome of verifying a batch.

type BumpResult

type BumpResult struct {
	Library Library `json:"library"`
	// Action is one of the Bump* constants.
	Action string `json:"action"`
	// Required is the version the consumer required after the rebase.
	Required string `json:"required,omitempty"`
	Commit   string `json:"commit,omitempty"`
	Detail   string `json:"detail,omitempty"`
}

BumpResult is one library's outcome.

type Bumper

type Bumper interface {
	// Required reports the version the consumer currently requires for one
	// library, after the rebase. found=false means the consumer does not
	// declare it at all.
	Required(ctx context.Context, dir string, library Library) (version string, found bool, err error)
	// Apply updates the manifests and lockfiles to target. It must run the
	// toolchain that refreshes go.sum / pnpm-lock.yaml, and it must not
	// commit — the engine owns the commit so the message follows the
	// repository's convention and so a no-op leaves no commit behind.
	Apply(ctx context.Context, dir string, library Library) error
}

Bumper applies one library's version bump inside the stream worktree.

It is a port because the bump mechanics already exist in the deps package and because the interesting behaviour — writing a commit ONLY where the required version is still below the target — has to be provable without a real module graph.

type CIMechanisms

type CIMechanisms interface {
	// Present reports which mechanisms the stream-PR workflows carry, and
	// whether any of them calls a REUSABLE workflow whose body WB cannot
	// read. An opaque callee makes a mechanism unverified, never absent.
	Present(dir string) (mechanisms map[string]bool, opaque bool, err error)
}

CIMechanisms reports which mechanisms a member's stream-PR workflows carry.

`batch-verification-runs-what-ci-runs` allows naming a mechanism as skipped only after proving CI actually runs it. An unverified "CI owns it" is worse than no gate, so this is a read of the workflows rather than an assumption.

type Element

type Element struct {
	Name        string `json:"name"`
	SHA         string `json:"sha"`
	Description string `json:"description,omitempty"`
	// Family groups a lockstep-versioned set (Angular, Nx, Ionic, Capacitor,
	// @sneat/*) that must be applied together and never split during prefix
	// re-apply.
	Family string `json:"family,omitempty"`
	// contains filtered or unexported fields
}

Element is one batch element: one dependency-bump commit, one lockstep family applied together, or one absorbed agent commit.

The rebase onto a moved base is deliberately NOT an element — it cannot be reverted independently, so it is the batch's base rather than part of it.

type Engine

type Engine struct {
	Git      Git
	Bumper   Bumper
	Verifier Verifier
	CI       CIMechanisms
	Events   Events
	Now      func() time.Time
}

Engine performs sync against injected ports.

func (*Engine) Sync

func (engine *Engine) Sync(ctx context.Context, options Options) (Result, error)

Sync brings the stream branch current and applies the batch, locally.

The ORDER is the mechanism, not an implementation detail:

  1. fetch, so the base is live rather than a session-start snapshot;
  2. rebase the stream branch onto the fresh base — never merge;
  3. rebase every agent branch onto the new stream head, per-branch;
  4. THEN compare each library's required version against the target.

Step 4 after step 2 is what makes sync idempotent against Renovate: a bump Renovate already landed is present in the tree after the rebase, so the required version is already at target and no commit is written. Comparing first would write a duplicate bump on top of one that already landed.

func (*Engine) VerifyBatch

func (engine *Engine) VerifyBatch(ctx context.Context, options Options, elements []Element) (BatchResult, error)

VerifyBatch applies the whole batch, runs the suite once, and on failure finds the culprit by cumulative prefix re-apply.

The cost is honest: one full run when the batch passes, and `1 + k` runs when the culprit is element k — worst case `1 + N`. It is a linear prefix scan, not a bisection.

Prefix re-apply runs on a local scratch branch that is NEVER pushed. Running it on the stream branch would push k intermediate states, each firing a stream-PR CI run — exactly the cost this design exists to remove.

Implements: dependency-streams#req:batch-verifies-once, dependency-streams#req:batch-failure-is-found-by-prefix-re-apply, dependency-streams#req:a-lockstep-family-is-one-batch-element.

type Event

type Event struct {
	Stream     string            `json:"stream"`
	Verb       string            `json:"verb"`
	Phase      string            `json:"phase,omitempty"`
	Repository string            `json:"repository,omitempty"`
	Outcome    string            `json:"outcome"`
	Detail     string            `json:"detail,omitempty"`
	Evidence   map[string]string `json:"evidence,omitempty"`
}

Event is one structured record.

type Events

type Events interface {
	Append(event Event) error
}

Events records what a verb did.

type ExecBumper

type ExecBumper struct{ Timeout time.Duration }

ExecBumper applies a library version inside the stream worktree.

It needs a checkout because the toolchain has to refresh `go.sum` / `pnpm-lock.yaml`; a manifest edit alone would leave the lockfile describing the old version.

func (ExecBumper) Apply

func (bumper ExecBumper) Apply(ctx context.Context, dir string, library Library) error

Apply implements Bumper. It never commits — the engine owns that, so a bump that changes nothing leaves no commit behind.

`GOWORK=off` is set for the Go path: `go get` and `go mod tidy` resolve against a workspace, so under a live local link they would write a `go.sum` describing an unpublished library tree.

func (ExecBumper) Required

func (bumper ExecBumper) Required(ctx context.Context, dir string, library Library) (string, bool, error)

Required implements Bumper by reading the consumer's own manifests — the same canonical dependency sections graph discovery uses.

type ExecGit

type ExecGit struct{ Timeout time.Duration }

ExecGit runs real Git.

func (ExecGit) AbortRebase

func (git ExecGit) AbortRebase(ctx context.Context, dir string) error

AbortRebase implements Git.

func (ExecGit) Checkout

func (git ExecGit) Checkout(ctx context.Context, dir, branch string) error

Checkout implements Git.

func (ExecGit) CherryPick

func (git ExecGit) CherryPick(ctx context.Context, dir, sha string) error

CherryPick implements Git.

func (ExecGit) CommitAll

func (git ExecGit) CommitAll(ctx context.Context, dir, message string) (string, bool, error)

CommitAll implements Git. ok=false when there was nothing to commit, which is what keeps a re-run of sync from writing an empty commit.

func (ExecGit) CommitsAhead

func (git ExecGit) CommitsAhead(ctx context.Context, dir, branch, upstream string) (int, error)

CommitsAhead implements Git. A branch with no remote counterpart is entirely unpushed, which is the normal state under this model rather than an error.

func (ExecGit) CreateBranch

func (git ExecGit) CreateBranch(ctx context.Context, dir, branch, revision string) error

CreateBranch implements Git.

func (ExecGit) CurrentBranch

func (git ExecGit) CurrentBranch(ctx context.Context, dir string) (string, error)

CurrentBranch implements Git.

func (ExecGit) DeleteBranch

func (git ExecGit) DeleteBranch(ctx context.Context, dir, branch string) error

DeleteBranch implements Git.

func (ExecGit) FastForwardToRemote added in v0.125.6

func (git ExecGit) FastForwardToRemote(ctx context.Context, dir, branch, remote string) (string, bool, bool, error)

FastForwardToRemote implements Git. A remote stream branch can move after a draft pull request is updated or landed elsewhere. Sync must absorb that movement before rebasing onto the base; otherwise it reports a clean run with a stale checkout and stale force-with-lease head.

`merge --ff-only` is intentionally used only after proving the local branch is an ancestor. It cannot create a merge commit or overwrite local commits. A local-ahead branch is left untouched; a divergent branch is refused so its owner chooses the resolution rather than sync inventing one.

func (ExecGit) Fetch

func (git ExecGit) Fetch(ctx context.Context, dir string) error

Fetch implements Git.

func (ExecGit) Head

func (git ExecGit) Head(ctx context.Context, dir, revision string) (string, error)

Head implements Git.

func (ExecGit) IsClean

func (git ExecGit) IsClean(ctx context.Context, dir string) (bool, error)

IsClean implements Git.

func (ExecGit) PushWithLease

func (git ExecGit) PushWithLease(ctx context.Context, dir, branch, expectedRemoteHead string) (string, error)

PushWithLease implements Git.

`--force-with-lease` against the head WB recorded is required because a rebase of a shared branch is a force-push, and a bare force discards whatever another agent pushed in between. The pushed ref is then re-read: a push exit code is not evidence the intended commit landed.

func (ExecGit) Rebase

func (git ExecGit) Rebase(ctx context.Context, dir, branch, upstream string) ([]string, error)

Rebase implements Git, returning conflicting paths rather than leaving the worktree mid-rebase. A conflict in one agent's branch must not abort the others, so the caller needs it as data and the tree back in a usable state.

func (ExecGit) ResetHard

func (git ExecGit) ResetHard(ctx context.Context, dir, revision string) error

ResetHard implements Git.

func (ExecGit) RestoreTo

func (git ExecGit) RestoreTo(ctx context.Context, dir, revision string) error

RestoreTo implements Git.

type Git

type Git interface {
	// Fetch refreshes origin. Sync rebases onto a freshly fetched base, never
	// onto whatever the local clone happens to hold.
	Fetch(ctx context.Context, dir string) error
	// FastForwardToRemote advances branch only when the fetched remote branch
	// contains it. It never rewrites local commits or resolves a divergence;
	// callers receive the fetched remote head so its lease can be refreshed.
	FastForwardToRemote(ctx context.Context, dir, branch, remote string) (remoteHead string, present, advanced bool, err error)
	// CurrentBranch reports the checked-out branch.
	CurrentBranch(ctx context.Context, dir string) (string, error)
	// Rebase replays branch onto upstream. It reports the conflicting paths
	// rather than leaving the worktree mid-rebase: a conflict in one agent's
	// branch must not abort the others, so the caller needs the conflict as
	// data and the tree back in a usable state.
	Rebase(ctx context.Context, dir, branch, upstream string) (conflicts []string, err error)
	// AbortRebase restores the tree after a conflicted rebase.
	AbortRebase(ctx context.Context, dir string) error
	// Head resolves a revision to a SHA.
	Head(ctx context.Context, dir, revision string) (string, error)
	// CommitsAhead counts commits on branch that upstream does not carry.
	CommitsAhead(ctx context.Context, dir, branch, upstream string) (int, error)
	// CommitAll stages every change and writes one commit. It returns the new
	// SHA, and ok=false when there was nothing to commit — which is what makes
	// a re-run of sync produce no new commits.
	CommitAll(ctx context.Context, dir, message string) (sha string, ok bool, err error)
	// CreateBranch points a new branch at a revision, replacing it if it
	// exists. Prefix re-apply runs on one of these and it is never pushed.
	CreateBranch(ctx context.Context, dir, branch, revision string) error
	// Checkout switches the worktree to a branch.
	Checkout(ctx context.Context, dir, branch string) error
	// ResetHard moves the current branch to a revision, discarding changes.
	ResetHard(ctx context.Context, dir, revision string) error
	// CherryPick applies one commit onto the current branch.
	CherryPick(ctx context.Context, dir, sha string) error
	// DeleteBranch removes a local branch.
	DeleteBranch(ctx context.Context, dir, branch string) error
	// IsClean reports whether the worktree has no uncommitted change.
	IsClean(ctx context.Context, dir string) (bool, error)
	// RestoreTo discards every change and returns the worktree to a revision.
	// A bump whose lockfile refresh failed must not be left half-applied.
	RestoreTo(ctx context.Context, dir, revision string) error
	// PushWithLease publishes branch using --force-with-lease against the head
	// WB last recorded, and verifies the ref it pushed. A rebase of a shared
	// branch is a force-push, and a bare force discards whatever another agent
	// pushed in between.
	PushWithLease(ctx context.Context, dir, branch, expectedRemoteHead string) (sha string, err error)
}

Git is the local Git surface sync and absorb share.

Every method is deliberately narrow: the engine composes them, so a fake can prove the ORDER of operations — rebase before bump is the whole mechanism behind idempotence against a bump Renovate already landed.

type Library

type Library struct {
	// Name is the module path or package name.
	Name string `json:"name"`
	// Target is the version the consumer should require.
	Target string `json:"target"`
	// Ecosystem is "go" or "npm"; it selects the commit-message convention.
	Ecosystem string `json:"ecosystem"`
}

Library is one own-library version the stream is moving to.

type Options

type Options struct {
	Stream string
	// Worktree is the stream worktree the bumps are applied in. Bumps need a
	// checkout because the toolchain has to refresh go.sum / pnpm-lock.yaml.
	Worktree string
	// Repository is the member being synced, for reporting.
	Repository string
	// Branch is `stream/<name>`; Base is what it rebases onto.
	Branch string
	Base   string
	// Libraries are the own-library targets in scope for this sync.
	Libraries []Library
	// AgentBranches are the stream's open agent branches.
	AgentBranches []AgentBranch
	// Verify runs the batch verification after applying.
	Verify bool
	// AllowMidReview proceeds past a branch whose review is open, with a
	// warning. Rebasing it invalidates that review.
	AllowMidReview bool
	// PushTrigger and PushReason justify a push. Empty means no push, which
	// is the normal outcome: sync's whole point is that bumps stay local.
	// A justified trigger performs a REAL push, verified against the remote.
	PushTrigger PushTrigger
	PushReason  string
	// RecordedRemoteHead is the stream head WB last recorded, used as the
	// --force-with-lease expectation.
	RecordedRemoteHead string
	// Timeout bounds each verification run.
	Timeout time.Duration
}

Options is one `wb stream sync` invocation.

type PushDecision

type PushDecision struct {
	Trigger PushTrigger `json:"trigger"`
	Reason  string      `json:"reason"`
	// SHA is what origin holds after the push. It is set only once the push
	// has been verified, so a caller reading this field is reading an effect
	// rather than an intention.
	SHA string `json:"sha,omitempty"`
}

PushDecision is the outcome of asking whether a push may happen.

func JustifyPush

func JustifyPush(trigger PushTrigger, reason string) (PushDecision, error)

JustifyPush decides whether a push is allowed, and why.

A push with no recognised trigger is refused, listing all four — a caller that does not know why it is pushing has no business pushing. `explicit` additionally requires a reason: it is the escape hatch, so it is the one trigger that must say what it is for in the operator's own words.

type PushTrigger

type PushTrigger string

PushTrigger is the justification for a push.

A push costs agent time, tokens, CI minutes and money, and ten dependency bumps pushed one at a time cost ten of each for one landing. So a push happens only on one of exactly four named triggers, and a dependency bump is not one of them.

Implements: dependency-streams#req:pushes-are-justified-and-counted.

const (
	// TriggerLanding is a push after a green batch verification, on the way
	// to landing.
	TriggerLanding PushTrigger = "landing"
	// TriggerReview is the stream's draft pull request being made ready.
	TriggerReview PushTrigger = "review"
	// TriggerPark is a hand-off where unpushed work would otherwise be lost.
	TriggerPark PushTrigger = "park"
	// TriggerExplicit is `--push --reason "<text>"`, the only escape hatch.
	TriggerExplicit PushTrigger = "explicit"
)

func PushTriggers

func PushTriggers() []PushTrigger

PushTriggers is the complete set, in the order a refusal lists them.

type RebaseResult

type RebaseResult struct {
	Branch    string   `json:"branch"`
	Agent     string   `json:"agent,omitempty"`
	Rebased   bool     `json:"rebased"`
	Conflicts []string `json:"conflicts,omitempty"`
	Detail    string   `json:"detail,omitempty"`
}

RebaseResult is one branch's rebase outcome.

type Refusal

type Refusal struct {
	Code       string
	Message    string
	Sanctioned []string
}

Refusal is a guard that fired.

func (*Refusal) Error

func (refusal *Refusal) Error() string

type Result

type Result struct {
	Stream     string `json:"stream"`
	Repository string `json:"repository"`
	// RecordedRemoteHead is the fetched stream branch head that the caller
	// must persist for a later --force-with-lease. It is deliberately the
	// remote head, not an unpushed local rebase or dependency bump.
	RecordedRemoteHead string `json:"recorded_remote_head,omitempty"`
	// RemoteAdvanced reports that the clean checkout was fast-forwarded to a
	// newer remote stream head before its ordinary rebase onto the base.
	RemoteAdvanced bool `json:"remote_advanced,omitempty"`
	// BaseBefore and BaseAfter show what the rebase moved onto.
	BaseBefore string `json:"base_before,omitempty"`
	BaseAfter  string `json:"base_after,omitempty"`
	// StreamRebase is the stream branch's own rebase onto the base.
	StreamRebase RebaseResult `json:"stream_rebase"`
	// AgentRebases are the agent branches, reported per branch: a conflict in
	// one must not stop the others.
	AgentRebases []RebaseResult `json:"agent_rebases,omitempty"`
	Bumps        []BumpResult   `json:"bumps"`
	// Batch is the single verification over the resulting tree.
	Batch *BatchResult `json:"batch,omitempty"`
	// Unpushed is what the operator sees accumulating locally.
	Unpushed UnpushedReport `json:"unpushed"`
	// Push is set only when a trigger justified one AND the push landed. It
	// carries the SHA the remote actually holds.
	Push *PushDecision `json:"push,omitempty"`
	// PushSkipped states, in words, that the remote was left untouched.
	PushSkipped string   `json:"push_skipped,omitempty"`
	Errors      []string `json:"errors,omitempty"`
}

Result is the whole sync report.

func (Result) Failed

func (result Result) Failed() bool

Failed reports whether the sync needs attention.

type UnpushedReport

type UnpushedReport struct {
	Repository string `json:"repository"`
	Branch     string `json:"branch"`
	Commits    int    `json:"commits"`
}

UnpushedReport is what `stream status` prints for a member: how far the local stream branch has run ahead of the remote.

Local commits are the normal state under this model, not a warning. The count is shown so the operator can see the batch accumulating and knows exactly what a landing push will carry.

func (UnpushedReport) String

func (report UnpushedReport) String() string

String renders the line `stream status` shows.

type VerificationRun

type VerificationRun struct {
	Passed  bool     `json:"passed"`
	Command string   `json:"command,omitempty"`
	Details []string `json:"details,omitempty"`
	// Skipped names each CI mechanism this run did not execute. It is only
	// ever printed alongside evidence that CI actually carries it.
	Skipped []string `json:"skipped,omitempty"`
	// Duration is the wall time, so a boundary's cost is measurable rather
	// than remembered.
	Duration time.Duration `json:"-"`
}

VerificationRun is one batch verification pass.

type Verifier

type Verifier interface {
	// Verify runs the full suite once over the tree as it stands.
	Verify(ctx context.Context, dir string) (VerificationRun, error)
}

Verifier runs the batch verification over a tree.

Jump to

Keyboard shortcuts

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