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
- func BumpMessage(library Library, from string) string
- type AgentBranch
- type BatchResult
- type BumpResult
- type Bumper
- type CIMechanisms
- type Element
- type Engine
- type Event
- type Events
- type ExecBumper
- type ExecGit
- func (git ExecGit) AbortRebase(ctx context.Context, dir string) error
- func (git ExecGit) Checkout(ctx context.Context, dir, branch string) error
- func (git ExecGit) CherryPick(ctx context.Context, dir, sha string) error
- func (git ExecGit) CommitAll(ctx context.Context, dir, message string) (string, bool, error)
- func (git ExecGit) CommitsAhead(ctx context.Context, dir, branch, upstream string) (int, error)
- func (git ExecGit) CreateBranch(ctx context.Context, dir, branch, revision string) error
- func (git ExecGit) CurrentBranch(ctx context.Context, dir string) (string, error)
- func (git ExecGit) DeleteBranch(ctx context.Context, dir, branch string) error
- func (git ExecGit) Fetch(ctx context.Context, dir string) error
- func (git ExecGit) Head(ctx context.Context, dir, revision string) (string, error)
- func (git ExecGit) IsClean(ctx context.Context, dir string) (bool, error)
- func (git ExecGit) PushWithLease(ctx context.Context, dir, branch, expectedRemoteHead string) (string, error)
- func (git ExecGit) Rebase(ctx context.Context, dir, branch, upstream string) ([]string, error)
- func (git ExecGit) ResetHard(ctx context.Context, dir, revision string) error
- func (git ExecGit) RestoreTo(ctx context.Context, dir, revision string) error
- type Git
- type Library
- type Options
- type PushDecision
- type PushTrigger
- type RebaseResult
- type Refusal
- type Result
- type UnpushedReport
- type VerificationRun
- type Verifier
Constants ¶
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.
const RefusalUnjustifiedPush = "unjustified-push"
RefusalUnjustifiedPush is the stable code for a push with no trigger.
Variables ¶
This section is empty.
Functions ¶
func BumpMessage ¶
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 ¶
Sync brings the stream branch current and applies the batch, locally.
The ORDER is the mechanism, not an implementation detail:
- fetch, so the base is live rather than a session-start snapshot;
- rebase the stream branch onto the fresh base — never merge;
- rebase every agent branch onto the new stream head, per-branch;
- 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 ExecBumper ¶
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 ¶
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.
type ExecGit ¶
ExecGit runs real Git.
func (ExecGit) AbortRebase ¶
AbortRebase implements Git.
func (ExecGit) CherryPick ¶
CherryPick implements Git.
func (ExecGit) CommitAll ¶
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 ¶
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 ¶
CreateBranch implements Git.
func (ExecGit) CurrentBranch ¶
CurrentBranch implements Git.
func (ExecGit) DeleteBranch ¶
DeleteBranch 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 ¶
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.
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
// 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 Result ¶
type Result struct {
Stream string `json:"stream"`
Repository string `json:"repository"`
// 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.
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.