Documentation
¶
Overview ¶
Package writeback lands the work product of an isolated executor in the project's repository on the hub.
It is the receiving half of pkg/executor/gitwriteback, and the trust boundary between them is total. Everything it is handed — a branch name, a commit SHA, a bundle — was produced inside a sandbox running model-authored code. This package's job is to get that work into pkg/mergequeue, which is where local parallel work already goes, without letting a sandbox use the trip to write somewhere it was never given.
Quarantine before branch ¶
Objects are fetched into refs/cloop/writeback/<branch>, never straight into refs/heads/<branch>, and the ref is only promoted after the commit range has passed inspection. The ordering is the point: a ref under refs/heads is a branch — it shows up in `git branch`, it can be checked out by a person or by the merge queue, and a checkout is what makes a .git/hooks entry execute. A ref that has not been vetted must never be reachable by any of that, so it spends its whole life somewhere `git checkout` will not find it, and a rejection deletes it.
Two layers of content checks, and why neither may be assumed ¶
Every fetch here runs with fetch.fsckObjects and core.protectNTFS/protectHFS set, so git refuses malformed objects and — the part that matters — trees containing a path named .git in any of the spellings a case-insensitive filesystem would fold back to it.
Whether that layer fires on the bundle path depends on the git in $PATH, so it is not something this package may rely on. Git runs its fsck rules from index-pack, and which of index-pack or unpack-objects handles a bundle fetch changed under us. Measured, with a bundle carrying a tree entry named .git and both fsck settings on:
git 2.43, 2.45 fetch succeeds — objects unpack loose, fsck never runs git 2.47, 2.54 fetch fails — hasDotgit, "fsck error in packed object"
The push path has always run through index-pack and so has always had it.
This is why executor.InspectWriteBack is not a second opinion: on any git that unpacks loose it is the only opinion, on the transport that needs one most — the one requiring no credential. It is also the layer that knows things fsck does not care about: that a symlink pointing outside the project root turns every later write through it into a write anywhere on the control plane, and that a submodule entry names a URL rather than content.
Because either layer may be the one that speaks, both classify a content refusal the same way — as ErrWriteBackRejected, never as the retryable ErrWriteBackUnavailable. A hostile write-back must not be reported as an outage on one git and a rejection on another. tests/security/writeback_bundle_test.go asserts the refusals over both transports for exactly this reason — the two paths do not have the same defences, so neither may be assumed to cover the other.
Both transports are inspected ¶
A pushed branch goes through exactly the same vetting as a bundle. A sandbox holding a push credential can push a hook as easily as it can bundle one, and securing only the path that needs no credential would be securing the wrong one.
Index ¶
Constants ¶
const DefaultTimeout = 2 * time.Minute
DefaultTimeout bounds each git invocation when a Request names none.
const QuarantineRefPrefix = "refs/cloop/writeback/"
QuarantineRefPrefix is where an unvetted write-back lives between arriving and being accepted. Deliberately outside refs/heads: see the package comment.
Variables ¶
This section is empty.
Functions ¶
func Record ¶
func Record(db Recorder, taskID int, taskTitle string, reported executor.WriteBackResult, applied Result, applyErr error) error
Record writes a write-back's outcome to the task record and the event journal.
It records failures as loudly as successes. A run whose work was lost looks exactly like a run that changed nothing — same transcript, same exit code, same green tick — so the case that most needs a row is the one where there is no branch to name.
Errors are returned for a caller that wants to log them and safe to ignore for one that does not: nothing here is load-bearing for the work itself, which is already on a branch by the time this runs.
Types ¶
type Merger ¶
type Merger interface {
Submit(req mergequeue.Request) *mergequeue.Result
}
Merger is the subset of pkg/mergequeue.Queue this package needs.
It is an interface so a test can prove the handoff happens without starting a queue goroutine, and so the caller keeps ownership of the real queue's lifecycle — a package that lands one branch has no business deciding when the project's merge queue starts and stops.
type Recorder ¶
type Recorder interface {
LoadTask(id int) (*pm.Task, error)
UpsertTask(t *pm.Task) error
RecordEvent(row statedb.EventRow) error
}
Recorder is the subset of *statedb.DB this package needs.
An interface so a caller can pass a real database, a fake, or nothing at all. It is deliberately the two narrowest methods rather than the DB: a package that writes a task's branch name has no business being able to delete a plan.
type Request ¶
type Request struct {
// RepoDir is the project's git repository on the hub.
RepoDir string
// Reported is what the executor said the sandbox produced.
Reported executor.WriteBackResult
// Bundle is the git bundle received for mode bundle. Ignored for push.
Bundle []byte
// Remote is the name of the git remote the branch was pushed to, for mode
// push. Empty means "origin".
Remote string
// TaskID and TaskTitle attribute the merge commit. They are the merge
// queue's, not git's — nothing here trusts them.
TaskID int
TaskTitle string
// Queue receives the vetted branch. Nil means "land the branch and stop",
// which is what a caller wants when the operator merges by hand.
Queue Merger
// MergeTimeout bounds the wait for the queue's verdict. 0 means ten
// minutes: a merge may sit behind other tasks' merges.
MergeTimeout time.Duration
// Timeout bounds each git invocation. 0 means DefaultTimeout.
Timeout time.Duration
// Emit receives progress. May be nil.
Emit func(string)
}
Request is one write-back to land.
type Result ¶
type Result struct {
// Branch and CommitSHA are the vetted ref, once it has been promoted.
Branch string
CommitSHA string
// Commits and FilesChanged describe the range that was accepted.
Commits int
FilesChanged int
// Merged and MergeSHA report the merge queue's outcome. Merged is false
// when no queue was supplied or when the merge conflicted; MergeErr
// carries the reason in the latter case.
Merged bool
MergeSHA string
MergeErr error
// Skipped reports that there was nothing to land.
Skipped bool
}
Result reports what landed.
func Apply ¶
Apply lands a reported write-back in RepoDir and hands the branch to the merge queue.
A rejection is a *executor.WriteBackRejection wrapping executor.ErrWriteBackRejected, and it leaves nothing behind: the quarantine ref is deleted, no branch is created, and the merge queue is never told about it. A merge conflict is *not* an error — the branch landed, the merge did not, and Result.MergeErr says so — because losing the branch would throw away the work the conflict is about.